Skip to content

feat(loops): wire break / continue end-to-end (closes #459)#465

Merged
hyperpolymath merged 1 commit into
mainfrom
feat/break-continue-459
May 30, 2026
Merged

feat(loops): wire break / continue end-to-end (closes #459)#465
hyperpolymath merged 1 commit into
mainfrom
feat/break-continue-459

Conversation

@hyperpolymath
Copy link
Copy Markdown
Owner

Summary

Closes #459break and continue now parse, type-check (rejected outside loop bodies with a clear error), and lower to JS break;/continue; in the Deno-ESM and Node JS backends. Pre-fix: BREAK/CONTINUE were lexer-reserved tokens with no parser production consuming them; any use was a syntax error.

Pipeline changes

File Change
lib/ast.ml ExprBreak of Span.t, ExprContinue of Span.t
lib/parser.mly BREAK/CONTINUE productions in expr_assign (diverging prefix, next to RETURN/RESUME)
lib/resolve.ml pass-through (resolve_expr + lower_expr)
lib/typecheck.ml new ctx.in_loop : mutable bool flipped on StmtWhile/StmtFor body entry; synth returns ty_never; new NotInLoop of string error
lib/borrow.ml pass-through (span lookup, visit-recurse, free-var collection, main checker)
lib/quantity.ml, lib/effect_sites.ml pass-through (no resources, no call sites)
lib/codegen_deno.ml, lib/js_codegen.ml statement-position lowering to bare JS keywords

Test fixture

tests/codegen-deno/loop_break_continue.affine + harness — 14 assertions across:

  • while + break (threshold-driven early exit)
  • while + continue (skip-evens accumulator)
  • for + break (find-first-match)
  • for + continue (count-positive filter)
  • Edge cases: break on first iteration, no-break path, empty array

Out of scope

  • Non-JS backends (wasm/GC/lua/c/rust/etc.): fall through existing wildcards. Full backend support files separately if needed.
  • JS-codegen expression-position IIFE wrapper (legacy MVP path) emits (() => { break; })() which would throw SyntaxError: Illegal break statement at runtime — legal AffineScript places break/continue inside loop bodies so the statement path fires. Deno backend uses the correct statement-position emit.

Test plan

  • ./tools/run_codegen_deno_tests.sh: 15/15 harnesses green
  • dune test: 352/352 unit tests green
  • Misuse check: pub fn bad() -> () { break; } emits the new NotInLoop error with the expected message

Refs

🤖 Generated with Claude Code

`BREAK` and `CONTINUE` were lexer-reserved tokens with no parser
production consuming them — any use produced a syntax error. Surfaced
by standards#284 (TS->AS port) which had to restructure several
loops into combined-guard / sentinel-boolean forms to work around it.

Now wired through the full pipeline:

- **ast.ml**: `ExprBreak of Span.t`, `ExprContinue of Span.t`.
- **parser.mly**: `BREAK`/`CONTINUE` added to `expr_assign` next to
  `RETURN`/`RESUME`. Diverging prefix expressions, no operand role.
- **resolve.ml**: pass-through (no name resolution needed). Touched
  both `resolve_expr` and `lower_expr` arms.
- **typecheck.ml**: synth returns `ty_never` (matching `ExprReturn`).
  Loop-context check via new `ctx.in_loop : mutable bool` flipped on
  `StmtWhile`/`StmtFor` body entry and restored on exit. Misuse
  outside a loop yields a new `NotInLoop of string` type-error with a
  message naming the keyword and citing #459.
- **always_diverges**: both new cases return true (a loop-body `break`
  exits the body normally, no value produced — same diverging shape
  as `return`).
- **borrow.ml**: pass-through in span lookup, visit-recurse, free-var
  collection, and the main checker (no expression carried, no borrow
  state mutated).
- **quantity.ml**, **effect_sites.ml**: pass-through (no resources
  used, no call sites involved).
- **codegen_deno.ml**, **js_codegen.ml**: statement-position emission
  lowers to bare JS `break;` / `continue;`. Expression-position IIFE
  fallback is a defensive stub — legal AffineScript places these
  inside a loop body, so the statement path (`gen_stmt_expr`) is what
  fires. The wasm/GC/other-backend codegens fall through their
  existing wildcard arms; full backend support is out of scope for
  this PR (file separately if a non-JS target needs it).

Regression fixture `tests/codegen-deno/loop_break_continue.affine`
+ harness exercise 14 assertions across:

- `while` + `break` (threshold-driven early exit)
- `while` + `continue` (skip-evens accumulator)
- `for` + `break` (find-first-match index)
- `for` + `continue` (count-positive filter)
- Edge cases: break on first iteration, no-break path, empty array

Verified:
- `./tools/run_codegen_deno_tests.sh`: 15/15 harnesses green
- `dune test`: 352/352 unit tests green
- Misuse check: `break;` outside a loop emits the new `NotInLoop`
  error with the expected message ("`break` used outside a loop body
  (#459). `break` and `continue` must be lexically enclosed by a
  `while` or `for` loop.").

Closes #459
Refs hyperpolymath/standards#284 (workarounds documented in the
"Seam findings" section that surfaced this gap)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@hyperpolymath hyperpolymath enabled auto-merge (squash) May 30, 2026 14:49
@github-actions
Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 83 issues detected

Severity Count
🔴 Critical 4
🟠 High 11
🟡 Medium 68

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Action perpolymath/standards/.github/workflows/governance-reusable.yml@main\n needs attention",
    "type": "unpinned_action",
    "file": "governance.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Action ons/checkout@v6\n    needs attention",
    "type": "unpinned_action",
    "file": "publish-jsr.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Action land/setup-deno@v2\n    needs attention",
    "type": "unpinned_action",
    "file": "publish-jsr.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in affine-vscode-publish.yml",
    "type": "unknown",
    "file": "affine-vscode-publish.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in casket-pages.yml",
    "type": "unknown",
    "file": "casket-pages.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in casket-pages.yml",
    "type": "unknown",
    "file": "casket-pages.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in ci.yml",
    "type": "unknown",
    "file": "ci.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in ci.yml",
    "type": "unknown",
    "file": "ci.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in ci.yml",
    "type": "unknown",
    "file": "ci.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in ci.yml",
    "type": "unknown",
    "file": "ci.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

@hyperpolymath hyperpolymath merged commit ae3fbae into main May 30, 2026
26 of 27 checks passed
@hyperpolymath hyperpolymath deleted the feat/break-continue-459 branch May 30, 2026 15:01
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.

parser: BREAK/CONTINUE tokens reserved in lexer but no production rule uses them

1 participant