Skip to content

loops - #106

Merged
varun-r-mallya merged 8 commits into
masterfrom
feat/loops
Sep 25, 2026
Merged

loops#106
varun-r-mallya merged 8 commits into
masterfrom
feat/loops

Conversation

@varun-r-mallya

Copy link
Copy Markdown
Member
image

@varun-r-mallya
varun-r-mallya requested review from r41k0u and a balanced review from Copilot September 24, 2026 19:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

The annotation types the slot, so `x: c_int32 = 0` is an i32 where the
literal alone would give an i64; the store goes through the same
handle_variable_assignment plain assignment uses. A bare `x: T` makes the
slot and binds nothing.

The already-bound and global-shadowing bookkeeping that Assign allocation did
inline moves into _bind_name, which both statements now call.
handle_if processed its branches without passing ret_type down, so a
return inside an if fell back to process_stmt's i64 default: a -> c_int32
function got `ret i64 7`, which llc rejects. The branches now go through
process_block, which threads ret_type and stops at a statement that ends
the block instead of emitting after its terminator.
`for i in range(...)` steps a hidden induction counter, allocated in the
entry block next to the loop variable, and copies it into the variable each
iteration, so rebinding the variable in the body leaves the trip count alone
as in Python (and the loop stays visibly bounded for the verifier). The
counter is 64-bit, signed unless the bounds make C's arithmetic unsigned.
Bounds are evaluated once, before the loop; the step must be a nonzero
integer literal, because its sign picks the loop test.

`while` re-evaluates its test at the top of each iteration. `break` and
`continue` branch through a loop-target stack on the compilation context;
outside a loop they are a SyntaxError, as in Python. A loop's else-branch
runs on normal exit only. Anything but range() as the iterable is rejected
with NotImplementedError.

The allocation pass now descends into loop bodies and counts helper temps in
loop headers. Checked against clang -O2 on tests/c-form/loops.bpf.c: the
constant-bound cases fold to the same ret, and a helper-bounded loop gets the
same rotated loop.
- passing_tests/loops/: the seven range/while/break/continue/nested tests
  move over from failing_tests/, joined by rebinding the loop variable, a
  negative step, loop else-branches, and a helper-bounded loop over a
  @bpfglobal that survives -O2 as a real loop for the verifier.
- failing_tests/loops/: break outside a loop, and a non-literal range step.
- for_map_items stays xfail: map iteration needs its own design.
- c-form/loops.bpf.c: the clang reference the lowering was checked against.
The remove-tabs pre-commit hook rejects tab indentation outside docs/ and
Makefiles, which failed the Format job.
- `pass` is accepted as a statement, the natural body of a loop run for its
  side effects.
- Falling off the end of a function returns 0 in the function's own return
  type. After `while True` that block is unreachable, but a c_int32 function
  still emitted `ret i64 0`, which llc rejected.
- A range() step other than 1 could wrap the counter past INT64_MAX or
  UINT64_MAX back inside the range and loop forever. The step block now ends
  the loop when the distance left to stop, exact as an unsigned number, is
  no longer than the step. That is a normal exit, so the else-branch runs.
  Steps of 1 cannot overshoot and keep the plain loop, so the helper-bounded
  loop the verifier walks is unchanged.
- A step of 2**64 or more no longer truncates to 0: it is a compile error,
  since it does not fit the counter. Steps from 2**63 up are emitted by
  their bits.
- The counter's signedness folds each bound into a signed 64-bit type rather
  than joining the bounds alone, so only a c_uint64 bound makes it unsigned:
  range(-1, n) over a c_uint32 n runs 4 times, as in C with an __s64 counter.
Levels 1 and 2 only prove a loop compiles; one can still run the wrong
number of times, or forever. test_loops_run.py JIT-compiles each
helper-free passing_tests/loops case on the host (framework/host_jit.py,
in a subprocess with a timeout) and checks its return value against what
CPython returns for the same body.

- passing_tests/loops/: pass bodies, `while True` exited by a return in a
  c_int32 function, step overflow past INT64_MAX, INT64_MIN and UINT64_MAX,
  a c_uint32 stop with a negative start, and loop variables that must stay
  one local (a nested same-name loop, a read after the loop).
- failing_tests/loops/range_step_too_wide.py: a 2**64 step is a compile error.
- reuse_loop_var_signedness.py is xfail in test_loops_run.py: every local
  gets one slot typed by its first binding, so a second loop's `i` inherits
  the first loop's unsigned type. `x = n_u64; x = -3` does the same, so the
  fix is general, not loop-specific.
@varun-r-mallya
varun-r-mallya merged commit 40aad02 into master Sep 25, 2026
2 checks passed
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.

2 participants