Skip to content

count loop iteration cap is inverted: keyed on the end value, so count from 1 to 20000 aborts while count from 1 to 1000001 runs uncapped #699

Description

@logbie

Summary

count loops abort at runtime after 10,001 iterations — but only when the end value is ≤ 1,000,000. A loop with a larger end value runs uncapped, so count from 1 to 20000 fails while count from 1 to 1000001 succeeds. The guard is keyed on the end value rather than on the iteration count, and it protects the small loops while abandoning the large ones.

Reproduction

store total as 0
count from 1 to 20000:
    change total to total plus 1
end count
display "total: " with total

Command:

wfl g6_count_cap.wfl

Expected

total: 20000, exit 0.

Docs/03-language-basics/loops-and-iteration.md:5-29 documents the construct as
count from <start> to <end> with no stated bound ("The count loop repeats a
specific number of times"), and :274 uses count from 1 to 100 as a plain
example. No documentation anywhere mentions an iteration cap
grep -rniE "maximum iterations|iteration limit|10001|10000 iterations" Docs/
returns zero hits.

Actual

Runtime errors:
error[ERROR]: Count loop exceeded maximum iterations (10001)
  ┌─ g6_count_cap.wfl:4:10
  │
4 │   end count
  │ ╭─────────^
5 │ │ display "total: " with total
  │ ╰^ Runtime error occurred here

Exit code: 1.

The guard is inverted

// src/interpreter/mod.rs:7385-7392
let max_iterations = if end_num > 1000000.0 {
    u64::MAX // Effectively no limit for large end values, rely on timeout instead
} else {
    // Allow up to 10001 iterations to accommodate loops that need exactly 10000
    10001
};

Measured boundary, one run per row (count from 1 to N, body increments a counter):

N exit result
10000 0 total: 10000
10001 1 Count loop exceeded maximum iterations (10001)
10002 1 same
999999 1 same
1000000 1 same
1000001 0 total: 1000001

So the runtime refuses a 10,001-iteration loop and then happily runs a
1,000,001-iteration one.

Because the guard reads end_num and not the trip count, a downward loop is
capped by the value it counts down to:

count from 2000000 down to 1:      // end_num = 1, so max_iterations = 10001

error[ERROR]: Count loop exceeded maximum iterations (10001), exit 1 — a
2,000,000-iteration loop rejected because its end value is small.

The cap is also redundant and inconsistent

  • The comment's fallback already works on its own: count from 1 to 100000000000
    is terminated by the execution timeout after 60 s, exit 1
    (timeout_seconds default 60, src/config.rs:159). Runaway protection does not
    depend on the 10001 cap.
  • No other loop form is capped. Verified at 20,000 iterations and beyond:
    repeat while → completes (also verified at 630,000); for each over a
    20,000-element list → completes.

Environment

  • wfl --version: WebFirst Language (WFL) version 26.8.4
  • binary: system install C:\Program Files\wfl\bin\wfl.exe
  • commit: c277d8f
  • OS: Windows 11 Pro 10.0.26200
  • build: release

Also reproduces identically on the repo build G:\repos\wfl\target\release\wfl.exe
(version 26.8.2) — not a regression, long-standing.
No .wflcfg in scope for the repro (the repo's own .wflcfg was not in the
repro's directory chain).

Context

Found while porting G:/repos/JShrink/src/JShrink/Minifier.php (a 738-line PHP
JavaScript minifier) to WFL. The minifier walks a 630,176-character input, so its
main loop needs hundreds of thousands of iterations.

This was encountered, not blocking: the port uses repeat while throughout,
which is uncapped, so nothing was stubbed. It is filed because the failure hits
ordinary, correct programs — a beginner writing count from 1 to 20000 gets a
runtime abort for a construct the docs present without qualification, and the
error text names a limit that appears nowhere in the documentation.

Suggested direction (maintainer's call): if a guard is wanted at all, key it on
the actual trip count rather than on end_num, apply it uniformly to repeat
and for each, make the limit configurable in .wflcfg, and document it. If the
timeout is considered sufficient — which the 60 s result above suggests — removing
the cap is the simpler fix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions