Skip to content

emitter: one counting header for every range loop - #308

Merged
shreeve merged 1 commit into
mainfrom
range-loops
Sep 16, 2026
Merged

shreeve merged 1 commit into
mainfrom
range-loops

Conversation

@shreeve

@shreeve shreeve commented Sep 16, 2026

Copy link
Copy Markdown
Owner

Summary

Every range loop now lowers to the same counting header. The postfix and comprehension forms, and a block loop in tail position, used to build the range with Array.from and iterate it with for...of; they now emit the loop the block form always emitted. The component render path keeps the materialized range because __reconcile consumes it.

Direction rule. A literal step's sign decides. A variable step binds once and the header tests its sign. With no step, two literal bounds decide statically, so [3..1] descends in every form, and any variable bound ascends, with by -1 to descend.

Behavior that changes

  • [1..n] with n = 0 ran twice in the materialized forms and now runs zero times. This fixes the sites test runner spawning two workers for zero tests and swarm spawning two for zero jobs.
  • A variable range with start above end ran downward in the materialized forms and is now empty, matching the block form. No site in this tree or in the other Rip codebases on this machine writes one.
  • A fractional bound compares directly instead of flooring through Array length. The pdf name-record reader relied on the floor and now spells it with >> 1.
  • for v, i in [1..3] never bound i in block form; it does now, in every form.

Measured on the loop alone: 4.8x faster at 8 iterations, 9.0x at 1024, 6.9x at 65536.

Artifacts. compose4.js changes one line, from the helper to for (let n = 1; n <= 3; n++) {, with its map; 184 corpus artifacts unchanged; parser untouched; browser bundle regenerated under Bun 1.4.0.

Test

36 new battery rows in loops.rip and comprehensions.rip. Language battery 3250 passing, toolchain gates 661 passing, barcodes parity oracles at zero mismatches with no materialized range left in the package.

A range loop now lowers to the same counting header in every form:
block statement, block in tail or value position, postfix, and
parenthesized comprehension. The postfix and comprehension forms, and a
block loop in tail position, used to materialize the range with
Array.from and iterate it with for-of; only the component render path
still builds the range, because __reconcile consumes the array.

Direction: a literal step's sign decides, and a zero literal rejects; a
variable step binds once and the header tests its sign; with no step,
two literal bounds decide statically, so [3..1] descends in every form,
and any variable bound ascends, with `by -1` to descend. Negative
literal bounds are literals, so [2..-2] needs no temp. A second loop
variable is the iteration index, which the block paths never bound.

Behavior that changes: [1..n] with n = 0 ran twice in the materialized
forms and now runs zero times, which fixes the zero-tests and zero-jobs
cases in the sites and swarm packages; a variable range with start
above end ran downward in those forms and is now empty; a fractional
bound compares directly instead of flooring through Array length, and
the one site that relied on the floor, the pdf name-record reader, now
spells it with >> 1.

Corpus: compose4.js line 16 changes from the Array.from helper to
`for (let n = 1; n <= 3; n++) {`, with its map; 184 artifacts unchanged.
The parser is untouched. The browser bundle is regenerated under the
pinned Bun.

Measured on the loop alone with a trivial body: 4.8x faster at 8
iterations, 9.0x at 1024, 6.9x at 65536. The barcodes package emits no
materialized range anywhere, its parity oracles report zero mismatches,
and its bench is unchanged because its hot loops were already block
form.

Battery: 36 rows across loops.rip and comprehensions.rip pin every form
and direction, the two-variable form, evaluation order, per-iteration
capture, fractional and negative bounds, and the emitted headers.
@shreeve
shreeve merged commit 1eacafe into main Sep 16, 2026
2 checks passed
@shreeve
shreeve deleted the range-loops branch September 16, 2026 06:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant