ctrlflow-1: break/continue/return legal in expression position (FB-015)#35
Merged
Conversation
break, continue and return (with a value or bare) may now appear as the value
of a match arm or if branch, exactly the way throw already does. Two additive
frontend fixes; zero backend change.
- Ast.fx (get_exp_ctx): ExpBreak/ExpContinue/ExpReturn now carry the pseudo-type
TypErr (like ExpThrow) instead of TypVoid, so a jumping arm/branch unifies
with any value-producing sibling without binding it. The return-value type
check is unaffected (the value's type is taken from e_opt separately), so a
bare return in a non-void function still reports a return-type mismatch.
- Lexer.fx: a `return` whose next significant char is one of } ) ] , | (tokens
that cannot begin an expression) is classified bare, fixing `{ return }` and
`return | ...` which previously misreported as `unexpected token '}'`.
K-normalization still lowers all three to void K-nodes routed through the block
cleanup chain, so generated C is byte-identical for existing programs: the
bootstrap regen touched only Ast.c + Lexer.c, and the -pr-resolve census
(351 sites) is unchanged. Legality (check_inside_for) and the @parallel/fold
restrictions fire identically in statement and expression position.
Tests: test/test_ctrlflow.fx (10 directed cases: the {break,continue,bare
return,return value} x {match arm,if branch,both jump,nested break,comprehension
body} matrix with ref-counted temps live at the jump, values asserted);
test/negative/216-219 (targeted diagnostics replacing `unexpected token '}'`).
Leak oracle: the suite and a 1000x1000-local stress are clean under ASan
detect_leaks at O0 and O3 -- every cleanup section runs on the jump. Full ladder
green (test_all 146, fxtest all, determinism, sanitize, fixpoint).
Unblocks the imperative-fold reform. See docs/ctrlflow1_report.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The one-argument diag built its dimension from the not-yet-defined result `a` instead of the input vector `d`, so the array form was unusable (unnoticed because nothing instantiates it -- generic bodies typecheck at instantiation; exposed by the annotate-2 sweep). One token: size(a) -> size(d). Locked by matrix.diag_from_vector in test/test_matrix.fx. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…piler Most of the compiler is accumulator recursion / immutable bindings / match; that is the house style to match. Historically more robust (fewer side effects => fewer state bugs). An imperative rewrite (e.g. accumulator recursion -> a for-loop with break/continue) is not automatically better: not faster (often micro-slower -- break/continue are checked per block cleanup section) and longer to write. Prompted by the ctrlflow-1 decision to leave lookup_id_opt's scan_ as idiomatic recursion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
break,continueandreturn(with a value or bare) may now appear as thevalue of a
matcharm orifbranch — exactly the waythrowalready does:All of these previously failed —
| _ => continuewith a type error, barereturnbefore}/|with a misleadingunexpected token '}'. This isadditive (accepts strictly more programs) and unblocks the imperative-fold
reform, where
match e { | A(x) => acc += x | _ => continue }is the naturalfold-body idiom.
Why it was two different bugs
The brief framed both as "parser limitations"; they weren't:
Ast.fxget_exp_ctx:ExpBreak/Continue/Returncarry pseudo-typeTypErr(likeExpThrow) instead ofTypVoid— unifies with any sibling without binding itreturnbefore}/|Lexer.fx: areturnwhose next significant char is} ) ] , |is barethrowis the template: it already flows through all four passes asTypErr.Mirroring that for the three jump nodes is the whole change — ~20 lines of
source, zero backend change.
Zero backend change — verified
break/continue/return/throwalready lower uniformly to void/err K-nodes routedthrough the block cleanup chain in
C_gen_code.fx; tail dispatch isposition-agnostic. Consequences, all checked:
Ast.c+Lexer.c— no other module's Cchanged ⇒ existing programs' K-form is byte-identical.
-pr-resolvecensus byte-identical (351 sites) — resolution-neutral.cannot use 'break' outside of loop, return-type mismatch for bare-return-in-non-void).break/continuein
@parallel/foldstay rejected in both statement and expressionposition (no widening).
Leak oracle
Ref-counted locals live at the jump must be freed by the cleanup chain.
Machine-checked, not eyeballed:
return/break/continue— clean underASAN_OPTIONS=detect_leaks=1atO0 and O3.
FX_FREE_ARR(&big)→FX_CHECK_*. K-form shows the matrices survive DCE (read into the branchcondition).
Tests
test/test_ctrlflow.fx— 10 directed cases:{break, continue, bare return, return value}×{match arm, if branch, both branches jump, nested break→innermost, comprehension/loop body}with ref-counted temps live at thejump, values asserted.
test/negative/216–219— targeted diagnostics replacingunexpected token '}'.test_all147,fxtest all(unit + negative + ir + cfolddeterminism,sanitize, fixpoint.Ride-along
Array.diag(d)usedsize(a)beforeaisdefined →
size(d); locked bymatrix.diag_from_vector.scan_resolverstays idiomatic recursion — an imperative rewrite would be micro-slower and no
clearer).
Details:
docs/ctrlflow1_report.md.