Skip to content

feat(compiler): lower for...in to while in all forms#68

Merged
kwakker35 merged 3 commits into
mainfrom
feat/for-in
Jun 19, 2026
Merged

feat(compiler): lower for...in to while in all forms#68
kwakker35 merged 3 commits into
mainfrom
feat/for-in

Conversation

@kwakker35

@kwakker35 kwakker35 commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Add for...in iteration in every form — array single, index (i, item), map (k, v) and numeric range with step and descending — each lowered to a while machine over the Increment B loop-context model. There is no FOR opcode; everything is a lowering.

Type checker: validate the iteration subject (array, map or numeric range; else E0501), require the two-identifier map form (E0502 on a single identifier, suggesting for k in m.keys), reject a literal descending range with no negative step (E0503), and forbid reassigning an iterator variable in the body (E0504). Iteration variables infer as the element type (item), int (the index i) and string (the map key k); value and array-element types stay unknown until generics (Sprint 5), so comparison against an unknown operand is now permissive.

Compiler: each form lowers over synthetic for-scope locals (counter, limit, step, keys array, map) with the visible iteration variables re-bound per iteration in a body scope. continue forward-jumps to the increment step via a new settable continue-site list on LoopContext, so the counter always advances; break and the synthetic PopN cleanup land at the loop exit. Array literals now compile to NewArray so arrays are constructible from source.

VM: implement the NewArray, GetIndex (array by int with E5101, map by string returning nil on a miss) and GetProperty (array length, map keys) arms the lowering relies on. GrobMap now uses OrderedDictionary so the key set walks in insertion order.

Map iteration has no source construction path in v1 (there is no map literal in the parser — out-of-scope parser work), so the map form is covered through the type checker via a fn-param annotation, a hand-built compiler AST and a hand-built VM lowering; array and range run end-to-end from source. New code is covered at 97%+; the only uncovered lines are defensive slot-overflow guards.

Registers E0501-E0504 in the error registry and ErrorCatalog (98 codes).

Summary by CodeRabbit

Release Notes

  • New Features

    • Added for...in loops to iterate over arrays, maps, and numeric ranges with automatic iterator variable binding
    • break and continue statements now work within for...in loops
  • Documentation

    • Added four new error codes (E0501–E0504) for invalid for...in usage including non-iterable subjects, incorrect single-identifier map iteration, descending ranges without explicit steps, and immutable iterator variables

Add for...in iteration in every form — array single, index (i, item),
map (k, v) and numeric range with step and descending — each lowered to
a while machine over the Increment B loop-context model. There is no FOR
opcode; everything is a lowering.

Type checker: validate the iteration subject (array, map or numeric
range; else E0501), require the two-identifier map form (E0502 on a
single identifier, suggesting `for k in m.keys`), reject a literal
descending range with no negative step (E0503), and forbid reassigning
an iterator variable in the body (E0504). Iteration variables infer as
the element type (item), int (the index i) and string (the map key k);
value and array-element types stay unknown until generics (Sprint 5),
so comparison against an unknown operand is now permissive.

Compiler: each form lowers over synthetic for-scope locals (counter,
limit, step, keys array, map) with the visible iteration variables
re-bound per iteration in a body scope. continue forward-jumps to the
increment step via a new settable continue-site list on LoopContext, so
the counter always advances; break and the synthetic PopN cleanup land
at the loop exit. Array literals now compile to NewArray so arrays are
constructible from source.

VM: implement the NewArray, GetIndex (array by int with E5101, map by
string returning nil on a miss) and GetProperty (array length, map keys)
arms the lowering relies on. GrobMap now uses OrderedDictionary so the
key set walks in insertion order.

Map iteration has no source construction path in v1 (there is no map
literal in the parser — out-of-scope parser work), so the map form is
covered through the type checker via a fn-param annotation, a hand-built
compiler AST and a hand-built VM lowering; array and range run
end-to-end from source. New code is covered at 97%+; the only uncovered
lines are defensive slot-overflow guards.

Registers E0501-E0504 in the error registry and ErrorCatalog (98 codes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kwakker35 kwakker35 self-assigned this Jun 19, 2026
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@kwakker35, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 43 minutes and 56 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 89178c6c-7573-418d-b97b-9ea6fa7f4c6e

📥 Commits

Reviewing files that changed from the base of the PR and between 9860346 and 2589ea9.

📒 Files selected for processing (3)
  • src/Grob.Compiler/TypeChecker.Expressions.cs
  • src/Grob.Core/GrobMap.cs
  • src/Grob.Vm/VirtualMachine.cs
📝 Walkthrough

Walkthrough

This PR implements for...in loop compilation end-to-end for Sprint 4 Increment C. It adds Array/Map enum values to GrobType, switches GrobMap to an OrderedDictionary backing, registers four new error codes (E0501E0504) in ErrorCatalog, extends the TypeChecker with full for...in semantic validation, lowers all three iterable shapes (array, numeric range, map) in the Compiler with forward-continue backpatching, and adds NewArray/GetIndex/GetProperty opcode dispatch in the VM.

Changes

for...in loop full pipeline

Layer / File(s) Summary
Core types, ordered map, and error catalog
src/Grob.Core/GrobType.cs, src/Grob.Core/GrobMap.cs, src/Grob.Core/ErrorCatalog.cs, src/Grob.Compiler/TypeChecker.cs
GrobType gains Array and Map enum values; GrobMap switches to OrderedDictionary with an InsertionOrderKeys property; ErrorCatalog defines E0501E0504 and inserts them into All; TypeChecker resolves array/map type-ref tags and renders their TypeName strings.
TypeChecker for...in semantic validation
src/Grob.Compiler/TypeChecker.ControlFlow.cs, src/Grob.Compiler/TypeChecker.Expressions.cs, src/Grob.Compiler/TypeChecker.Statements.cs
VisitForIn now resolves iteration-variable types via ResolveIterationVariableTypes, which dispatches on iterable shape and emits E0501/E0502/E0503; VisitNumericRange validates int components and emits E0503 for descending ranges lacking a negative step; VisitArrayLiteral returns GrobType.Array; ResolveComparison is permissive for Unknown operands; TryResolveAndBindMutableTarget emits E0504 for iterator-variable reassignment.
LoopContext extension and Compiler for...in lowering
src/Grob.Compiler/Compiler.cs, src/Grob.Compiler/Compiler.ControlFlow.cs, src/Grob.Compiler/Compiler.Expressions.cs
LoopContext gains HasForwardContinue, _continueSites, and RecordContinue/ContinueSites accessors; VisitContinue branches on HasForwardContinue; VisitForIn dispatches to EmitRangeForIn, EmitArrayForIn, or EmitMapForIn; EmitForInLoop is the shared loop machine with condition, exit-jump, body scope with PopN, continue-site backpatching, and break-site patching; VisitArrayLiteral emits NewArray with element-count operand.
VM opcode dispatch
src/Grob.Vm/VirtualMachine.cs
Adds NewArray (builds GrobArray from byte-counted stack values in source order), GetIndex (bounds-checked array indexing with E5101, map key lookup with nil-on-missing, nil-receiver E5201), and a real GetProperty implementation resolving array.length and map.keys.
Tests and error-code docs
tests/Grob.Vm.Tests/VirtualMachineForInTests.cs, tests/Grob.Compiler.Tests/CompilerForInTests.cs, tests/Grob.Compiler.Tests/TypeCheckerForInTests.cs, tests/Grob.Compiler.Tests/TypeCheckerTests.cs, tests/Grob.Integration.Tests/Sprint4IncrementCTests.cs, docs/design/grob-error-codes.md
VM tests cover NewArray, GetIndex/GetProperty error paths, and a hand-built map loop; compiler tests decode bytecode to assert opcode shape across all three iterable forms and continue targeting; type-checker tests assert well-typed forms and all four E05xx diagnostics; integration tests drive the full pipeline end-to-end; error-code registry adds E0501E0504 and updates total count to 98.

Sequence Diagrams

sequenceDiagram
    actor Source
    participant TypeChecker
    participant Compiler
    participant VirtualMachine

    Source->>TypeChecker: for v in xs { }
    TypeChecker->>TypeChecker: ResolveIterationVariableTypes(xs)
    note over TypeChecker: Dispatch: array → int index + Unknown elem<br>range → int counter/limit/step<br>map → string key + Unknown val
    TypeChecker->>TypeChecker: PushScope, register iteration vars
    TypeChecker->>TypeChecker: VisitBody (_loopDepth++)
    TypeChecker-->>Compiler: typed AST, no diagnostics

    Compiler->>Compiler: VisitForIn → EmitArrayForIn / EmitRangeForIn / EmitMapForIn
    Compiler->>Compiler: EmitForInLoop: condition + ConditionalJump(exit)
    Compiler->>Compiler: LoopContext(hasForwardContinue=true)
    Compiler->>Compiler: body scope + PopN cleanup
    Compiler->>Compiler: Backpatch continue sites → increment step
    Compiler->>Compiler: Loop backward jump + patch exit/break
    Compiler-->>VirtualMachine: Chunk bytecode

    VirtualMachine->>VirtualMachine: NewArray: pop N values, push GrobArray
    VirtualMachine->>VirtualMachine: GetProperty: array.length / map.keys
    VirtualMachine->>VirtualMachine: GetIndex: arr[i] bounds-check (E5101) / map[k] nil-on-missing
    VirtualMachine-->>Source: stdout output
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • grob-lang/grob#67: Introduced the while/break/continue LoopContext infrastructure that this PR directly extends with HasForwardContinue and forward-jump continue backpatching for for...in.
  • grob-lang/grob#43: Stubbed the original TypeChecker.VisitForIn with minimal body-walking logic that this PR replaces with full semantic validation, type inference, and E0501E0504 diagnostics.
  • grob-lang/grob#41: Established the VM dispatch-loop skeleton that this PR extends with NewArray, GetIndex, and the real GetProperty implementation.

Poem

🔁 for k, v in map — the loop unrolled at last,
Keys pop in order, synthetic locals amassed.
E0504 guards the counter — thou shalt not assign!
Descending without step? E0503 draws the line.
The backpatcher stitches continue to increment's beat,
Sprint 4 Increment C: the iteration's complete. ✅

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title uses the correct Conventional Commits format with a valid prefix (feat), scope (compiler), and accurately describes the main change—lowering for...in loops to while loops.
Docstring Coverage ✅ Passed Docstring coverage is 47.00% which is sufficient. The required threshold is 10.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/for-in

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Grob.Compiler/TypeChecker.Expressions.cs`:
- Around line 356-358: Fix the typographical error in the error message string
parameter of the EmitError method call for ErrorCatalog.E0503. The quoted
example in the message has mismatched quotes: '..' step -1' should be corrected
to either '.. step -1' (with a single opening quote before the dots) or .. step
-1 (with quotes removed entirely) to maintain proper quote balancing in the
diagnostic message text.

In `@src/Grob.Vm/VirtualMachine.cs`:
- Around line 484-485: The LINQ Select() call in the GrobArray initialization
near the map!.InsertionOrderKeys line is allocating an iterator wrapper on every
dispatch iteration, violating the hot path performance guidelines. Replace the
.Select(GrobValue.FromString) LINQ expression with a manual loop (such as a
foreach loop) that converts each key from InsertionOrderKeys to a GrobValue
using FromString, building a collection or array to pass to the GrobArray
constructor without the allocation overhead of the iterator wrapper.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f6d60db2-2b15-423c-9005-425006057bbe

📥 Commits

Reviewing files that changed from the base of the PR and between db77969 and 9860346.

📒 Files selected for processing (17)
  • docs/design/grob-error-codes.md
  • src/Grob.Compiler/Compiler.ControlFlow.cs
  • src/Grob.Compiler/Compiler.Expressions.cs
  • src/Grob.Compiler/Compiler.cs
  • src/Grob.Compiler/TypeChecker.ControlFlow.cs
  • src/Grob.Compiler/TypeChecker.Expressions.cs
  • src/Grob.Compiler/TypeChecker.Statements.cs
  • src/Grob.Compiler/TypeChecker.cs
  • src/Grob.Core/ErrorCatalog.cs
  • src/Grob.Core/GrobMap.cs
  • src/Grob.Core/GrobType.cs
  • src/Grob.Vm/VirtualMachine.cs
  • tests/Grob.Compiler.Tests/CompilerForInTests.cs
  • tests/Grob.Compiler.Tests/TypeCheckerForInTests.cs
  • tests/Grob.Compiler.Tests/TypeCheckerTests.cs
  • tests/Grob.Integration.Tests/Sprint4IncrementCTests.cs
  • tests/Grob.Vm.Tests/VirtualMachineForInTests.cs

Comment thread src/Grob.Compiler/TypeChecker.Expressions.cs
Comment thread src/Grob.Vm/VirtualMachine.cs Outdated
kwakker35 and others added 2 commits June 19, 2026 08:54
The descending-range diagnostic showed the example as '..' step -1'
with a stray quote between the dots and step. Use the balanced,
concrete example '3..0 step -1'. Raised by CodeRabbit on PR #68.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GrobMap.InsertionOrderKeys returned _entries.Keys.ToList(), copying the
key collection on a property access (Sonar S2365), and the VM's
GetProperty 'keys' arm wrapped it in a LINQ Select on the dispatch path
(against the no-LINQ-on-hot-paths guideline). The property now returns
the backing OrderedDictionary's live ordered-key view without copying,
and the VM builds the GrobArray with a manual indexed loop. The caller
snapshots into a GrobArray, so exposing the live view is safe.

No behaviour change. Raised by CodeRabbit and Sonar (S2365) on PR #68.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@kwakker35 kwakker35 merged commit 6fc481d into main Jun 19, 2026
16 checks passed
@kwakker35 kwakker35 deleted the feat/for-in branch June 19, 2026 07:57
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