Skip to content

[Task 53] Parser: effect declarations + handle expressions#19

Merged
boldfield merged 2 commits into
mainfrom
plan-b-task-53
Apr 25, 2026
Merged

[Task 53] Parser: effect declarations + handle expressions#19
boldfield merged 2 commits into
mainfrom
plan-b-task-53

Conversation

@boldfield
Copy link
Copy Markdown
Owner

@boldfield boldfield commented Apr 25, 2026

Plan B Stage 6 begins. This PR ships the parser surface for effect
declarations and handle expressions, the foundational scaffolding
for the algebraic-effects runtime that Tasks 54–61 will build on.

Per the Plan B convention, this is a parser-only commit. The
typechecker emits E0133 for every effect decl and E0134 for
every handle expression, ensuring no partial Plan B program can
reach monomorphization, CPS transform, or codegen until Task 54's
typing rules and Task 55's CPS transform land.

Surface forms shipped

// Effect declaration (default one-shot continuations)
effect Raise { fail: (String) -> Int }

// Effect with generic params
effect State[T] { get: () -> T, put: (T) -> Unit }

// Multi-shot continuation (opt-in)
effect Choose resumes: many { choose: (Int) -> Int }

// Handler expression (qualified arm form)
handle parse(s) with {
  return(v) => v,
  Raise.fail(msg, k) => 0,
}

Landed in this PR

Item Status
Task 53 — parser surface for effect + handle done-pending-ci
Task 53 review fixups (PR #19 items 1, 2, 4, 7, 8, 9 + 10/11 prep) done-pending-ci

Highlights

Lexer: new keywords effect, handle, with. The attribute
words resumes and many stay as plain identifiers and are matched
contextually inside parse_effect_decl, so user code that
legitimately wants let resumes = 5 is not regressed. Two
[DEVIATION Task 53] entries in PLAN_B_DEVIATIONS.md document the
qualified-arm surface choice and the context-keyword choice with
their respective closure points.

AST: Item::Effect(Box<EffectDecl>) with supporting
EffectDecl / EffectOp types; Expr::Handle { body, return_arm, op_arms, span } with HandleReturnArm and HandleOpArm.

Design choice — qualified arm shape: the plan body writes
handler arms as op(args, k) => arm (bare op name). This PR ships
the qualified form Effect.op(args, k) => arm so a single handle
can dispatch operations from more than one effect, and so the
surface mirrors perform Effect.op(args). Documented as
[DEVIATION Task 53] in PLAN_B_DEVIATIONS.md and tracked as
[PLAN-B] Task 54: revisit handler arm surface (qualified-only vs bare-op-as-sugar) in QUESTIONS.md — Task 54+ ergonomic data can
decide whether to introduce bare-op desugaring within unambiguous
handler contexts. The AST records both effect and op names, so
bare-op-as-sugar is a strict forward-compatible extension.

Parser-level rules (rejected before typecheck):

  • An effect declaration must contain at least one operation arm.
  • A handle expression must have at least one operation arm.
  • A handle expression has at most one return arm. Duplicates are
    rejected with first-wins AST semantics so downstream passes see
    a stable target.
  • An operation arm requires a trailing continuation binding k.
  • The resumes: attribute accepts only many in v1.

Errors: E0133 (effect decl pending Task 54) and E0134 (handle
expr pending Task 54/55) with full long-form text + workaround
examples. The no_user_facing_error_uses_e0001 discipline sweep
gains two new programs covering the staged shapes.

Downstream stubs: every pass that exhaustively matches on
Item or Expr gains a Task-53 arm. Pre-codegen passes preserve
structural shape (with arm-body recursion); codegen rejects with
unreachable! since typecheck E0134 must catch any well-formed
program.

Tests

  • 5 lexer tests (keyword recognition + resumes/many-stay-Idents).
  • 21 parser tests (happy paths + 11 error paths, including the
    keyword-side adversarials added in fixup: let effect: Int = 1,
    let handle: Int = 1, let with: Int = 1 all parse-error
    because the words are now reserved; the duplicate-return AST
    test pinning first-wins semantics).
  • 5 typecheck tests pin E0133 / E0134 emission.

Total: 31 new unit tests. Pod-verify clean. CI is authoritative for
the full test suite + multi-host build.

Plan B context

Stage 5 closed at 2026-04-25 with PR #18 (6305bcb); Stage 6
(Tasks 53–61) is the algebraic-effects runtime. Per the plan body's
explicit instruction, every Stage 6 PR receives human review before
merging. Tasks 54–61 still pending after this lands.

Memory discipline

This commit adds AST surface, parser code, and structural pass-through
arms only. Non-effect programs see an unchanged Cranelift baseline
because typecheck rejects every effect / handle shape with
E0133 / E0134 before lowering. Programs that do contain effect
declarations or handler expressions allocate the new AST nodes
(EffectDecl, HandleOpArm, the Vec<HandleArmParam> per arm) at
parse time, but those programs abort before codegen so no
allocation-sensitive runtime paths are affected. The Cranelift
per-compile peak should remain at the post-PR-#18 baseline; Task 56's
runtime work is the next inflection point.

Plan B PROGRESS hygiene

Task 53 status done-pending-ci with [HEAD] placeholder; flips
to done with the squash-merge hash in the next task's PR per
established convention. Items 10 + 11 from review (typecheck
arm-walk binding-scope extension; codegen-gate alignment) recorded
under Task 54's "prep" entry in PLAN_B_PROGRESS.md.

Stage-6 surface scaffolding for Plan B. Adds parser support for
`effect Name[T] { op: (T) -> R, ... }` (default one-shot) and
`effect Name[T] resumes: many { ... }` (multi-shot opt-in)
declarations, plus `handle <body> with { return(v) => arm,
Effect.op(p, ..., k) => arm, ... }` handler expressions. The
typechecker (Task 54) and CPS transform (Task 55) consume these
shapes; this commit ships the parser surface plus staged
diagnostics so a partial Plan B program cannot reach downstream
passes until the typing rules land.

Lexer:
- New keywords `effect`, `handle`, `with`. The attribute words
  `resumes` and `many` stay as plain identifiers and are matched
  contextually inside `parse_effect_decl` so user code that
  legitimately wants `let resumes = 5` is not regressed.

AST:
- `Item::Effect(Box<EffectDecl>)` with `EffectDecl { name,
  generic_params, resumes_many, ops, ... }` and
  `EffectOp { name, params: Vec<TypeExpr>, return_type, ... }`.
- `Expr::Handle { body, return_arm, op_arms, ... }` with the
  matching `HandleReturnArm` (single value binding + body) and
  `HandleOpArm { effect, op, params, k_name, body, ... }`. The
  trailing parameter of an op arm is always `k`; arms with empty
  parameter lists are rejected at parse time.

Parser:
- Top-level dispatch wires `Effect` into `parse_program`. The
  empty-body case (`effect E { }`) is rejected at parse time
  because handlers cannot meaningfully discharge no operations.
- `parse_handle_expr` parses the body under `no_record_lits=true`
  so `handle Foo with { ... }` doesn't try to read `Foo {` as a
  record literal. Arms enforce at-least-one-op-arm,
  at-most-one-return-arm, and mandatory trailing `k` rules.
- The arm grammar uses fully-qualified `Effect.op(...)` rather
  than the plan body's bare `op(...)` so a single `handle` can
  dispatch operations from more than one effect; the qualified
  form also mirrors `perform Effect.op(args)`. Documented as
  Plan B Task 53 design choice in PLAN_B_DEVIATIONS.md.

Errors:
- Catalog entries `E0133` (effect decl pending Task 54) and
  `E0134` (handle expression pending Tasks 54/55) with full
  long-form text plus workaround examples.

Downstream stubs (workspace compile-clean):
- typecheck emits `E0133` per effect decl + `E0134` per handle
  expression, then walks children so nested type errors still
  surface in the same compile pass. `no_user_facing_error_uses_e0001`
  sweep extended with two new programs covering the staged shapes.
- elaborate / closure_convert / monomorphize / color all gain
  pass-through arms with structural recursion through arm bodies
  and arm-binding scope tracking where applicable.
- codegen `lower_expr` and `type_of_expr` use `unreachable!`
  (Plan A pattern); the codegen-entry walker hard-rejects any
  `Item::Effect` or `Expr::Handle` that slips past typecheck.

Tests:
- 5 lexer tests pin keyword recognition + `resumes`/`many`
  remaining-as-Idents.
- 18 parser tests cover happy and error paths (effect decls
  with/without generics/resumes, multi-op decls with trailing
  comma, empty body / non-`many` rejection, `resumes` outside-
  effect-decl regression guard, minimal handle, return-arm,
  multi-effect handle, k-binding-as-trailing-param,
  missing-with / empty-arms / no-k / duplicate-return error
  paths, body-no-record-lit regression).
- 5 typecheck tests pin `E0133` / `E0134` emission and the
  cascade-friendly recursion behavior.

Pod-verify clean. CI is authoritative for the full test suite
and multi-host verification.
@boldfield
Copy link
Copy Markdown
Owner Author

Review — please address before merge

1. Bug — stale doc comment contradicts code

compiler/src/parser.rs:1097-1099:

/// At least one operation arm is required. The parser does not
/// reject duplicate `return` arms — the typechecker (Task 54)
/// produces a more specific diagnostic with both spans.

But lines 1125-1135 do reject duplicates with self.err(... "duplicate return arm in handler; only one is allowed"). Update the doc to reflect that the parser rejects duplicates today.

2. Design — duplicate return arm: error pushed, but first arm clobbered

compiler/src/parser.rs:1131-1136: after pushing the duplicate error, return_arm = Some(Box::new(ra)) overwrites the first arm. Consequences:

  1. Typecheck never sees the first arm — the cross-span "first / second" diagnostic the doc imagines is impossible from the AST that lands.
  2. Any later span-based error references the second arm, not the first.

Pick one of:

  • Keep the first (if return_arm.is_none() { return_arm = Some(...) }) — conventional "first wins" for duplicate-decl errors and gives downstream a stable target.
  • Drop the doc's promise about future refinement.

3. Minor — eat_resumes_many_attr returns Option<bool>

compiler/src/parser.rs:923 — three states (present / absent / malformed) packed into Option<bool> is awkward at the call site. A small enum ResumesAttr { Many, None, Malformed } or splitting the malformed path into the existing ?-propagation would read better. Skip if you don't want the churn.

4. Minor — expr_uses_generic defensive walker comment is misleading

compiler/src/codegen.rs:352-372 recurses into Expr::Handle arm bodies for generic use. This is unreachable in practice today: contains_apply_or_generic_ref already returns true on Item::Effect(_) (line 335), so any program with an effect decl hard-rejects at the entry walker before this fn runs. The current comment implies the walker matters today. Tighten it to: "dead code today; kept for the case where Task 54 lifts the Item::Effect gate but Handle still needs a guard."

5. Minor — PR description claim "no new allocation sites" is strictly false

The "Memory discipline" section says "No new allocation sites." Every EffectDecl / HandleOpArm / Vec<HandleArmParam> parse is a new allocation when such code exists. The intended claim is "non-effect programs see unchanged Cranelift baseline because typecheck rejects before lowering." Tighten the wording so a future reviewer in Task 56+ doesn't quote this back if something regresses.

6. Risk to flag for Task 54's reviewer

Codegen has unreachable! in both lower_expr and type_of_expr for Expr::Handle. The codegen-entry hard-reject (Item::Effect → return true from contains_apply_or_generic_ref) only catches programs with an effect decl in scope. A handle over an unhandled effect with no decl in scope would skip that gate. If Task 54 lifts E0134 before Task 55 lands the CPS expansion, codegen will panic. Worth a note in Task 54's PR description that both gates need to stay aligned.


Items 1, 2, 4, and 5 are required fixes. Item 3 is optional. Item 6 is forward-looking — call it out in the next PR rather than blocking this one.

@boldfield
Copy link
Copy Markdown
Owner Author

Review — context-aware pass

Verdict: merge-with-fixes. Combined with the prior review (#issuecomment-4320067338, items 1–6), the consolidated directive list is below. Mechanical sweep is clean.

Per-task

Task Deliverable Status
53 Parser surface for effect decls + handle exprs, lexer keywords, AST nodes, downstream pass-through stubs, E0133/E0134 staged diagnostics, 28 new tests ship after fixes

Mechanical (all green)

  • cargo fmt --all -- --check, clippy -D warnings on all four workspace crates, cargo test -p sigil-runtime (36 passed), scripts/pod-verify.sh.
  • Discipline greps: zero HashMap/HashSet in non-test compiler/src; no new unwrap()/expect(); non-test panic!/unreachable! confined to documented Plan-A pattern + Task-53 codegen unreachable! arms.
  • Cargo.toml diff vs 6305bcb: empty (zero new deps).
  • CI green on ubuntu-24.04 + macos-14 (matrix + cold-checkout, all 4 lanes).
  • [HEAD] placeholder in PLAN_B_PROGRESS.md is genuinely a placeholder — no leaked branch-tip hash this round.

Must-fix before merge

Items 1, 2, 4, 5 from the prior review stand as written. Plus:

7. Second deviation entry missing. PR documents the qualified-arm shape in PLAN_B_DEVIATIONS.md but not the second deviation: resumes/many shipped as context-sensitive idents matched inside parse_effect_decl, contrary to the design doc keyword list at docs/plans/2026-04-21-sigil-design.md:61. The engineering choice (narrower name reservation — let resumes = 5 doesn't regress) is sound; it just needs to be documented. Add a sibling [DEVIATION Task 53] entry with the same four-section structure (plan said / PR did / why / closure point).

8. Qualified arm syntax confirmed as the v1 surface. After review, qualified Effect.op(args, k) => arm is accepted over the design doc's bare op(args, k) => arm (3 concordant sites: docs/plans/2026-04-21-sigil-design.md:95-99, :207-211, :215; plan queue/2026-04-21-sigil-effects.md:142). Reasoning: conservative parse-time resolution; clean multi-effect dispatch from a single handle; AST stores both effect and op names so bare-op-as-sugar is a strict forward-compatible extension.

Action: add a [PLAN-B] Task 54: revisit handler arm surface (qualified-only vs bare-op-as-sugar) entry to QUESTIONS.md so Task 54+ ergonomic data can decide whether to introduce bare-op desugaring within unambiguous handler contexts. The current PLAN_B_DEVIATIONS.md deviation entry's "closure point: open" line should cross-reference the new QUESTIONS.md entry rather than leaving the closure point dangling.

9. Adversarial lexer test for let effect = 1. The ident-side regression guard resumes_outside_effect_decl_remains_ident is already in. The symmetric keyword-side adversarial — let effect = 1 should fail because effect is now reserved — isn't pinned. Add a one-line parse_errs("fn main() -> Int ![] { let effect: Int = 1; 0 }\n") test to close the matrix.

Follow-up (Task 54 PR — note in PLAN_B_PROGRESS.md Task-54 prep)

10. Typecheck arm-walk does not extend binding scope. compiler/src/typecheck.rs:2150-2167 walks body, return_arm.body, and each op_arm.body after emitting E0134 but does not introduce op-params or k into the local environment. Programs like E.op(x, k) => x + 1 will emit a spurious E0046 "unknown identifier x" alongside E0134. Test handle_arm_bodies_walked_during_e0134_emission uses body: true which sidesteps the issue. Task 54 replaces this entire path with real handler typing — record this in PLAN_B_PROGRESS.md Task-54 prep so binding-scope extension lands as part of 54's scope, not as separate cleanup.

11. Codegen-gate alignment for Task 54. Per item 6 of the prior review. The Item::Effect → return true gate at contains_apply_or_generic_ref (compiler/src/codegen.rs:335) only catches programs with an effect decl in scope; a handle over an unhandled effect with no decl in scope would skip that gate and reach the codegen unreachable!. Task 54's PR description must explicitly verify both gates stay aligned if E0134 is lifted before Task 55's CPS expansion lands.

Deferred (Task 54+ / Stage 6 later / v2+)

  • Unused-k ergonomics (Effect.op(msg, _) => 0 anonymous trailing position) — defer until Task 54 typing data shows whether unused-k warnings get noisy in real programs.
  • Per-monomorph color variance under handler context — still live for Tasks 56–58 review per PR [Tasks 51 + 52] Stage 5 closeout: generic_map example + P16/P17 prompts #18 round 2.
  • Item 3 from prior review (eat_resumes_many_attr Option<bool>enum ResumesAttr { Many, None, Malformed }) — optional; skip if no churn justified.

Regressions

None. The discipline sweep no_user_facing_error_uses_e0001 was correctly extended at compiler/src/typecheck.rs:3895-3896 with two new programs covering the E0133 and E0134 staged shapes.


Once items 7–9 plus prior-review items 1, 2, 4, 5 land, this is ready to merge. Note items 10 and 11 in PLAN_B_PROGRESS.md Task-54 prep so they don't get lost across the PR boundary.

Addresses PR #19 review items 1, 2, 4, 7, 8, 9; records items
10 + 11 as Task 54 prep. Item 3 skipped per reviewer's
"skip if no churn"; item 5 lands as a separate PR-body edit;
item 6 forwarded to Task 54's PR description per reviewer
direction.

**Item 1 — stale doc comment on `parse_handle_expr`.** The pre-fix
doc claimed "the parser does not reject duplicate `return` arms"
while the body explicitly emitted a duplicate error at lines
1131–1135. Updated the doc to describe the actual behaviour
(parser rejects duplicates with first-wins AST semantics; Task 54
can build a cross-span diagnostic from the recorded error and the
preserved AST).

**Item 2 — duplicate `return` arm: first-wins AST semantics.**
Pre-fix the duplicate `return_arm = Some(Box::new(ra))` after the
error overwrote the first arm with the second. Now the assignment
is gated on `return_arm.is_none()` so the first arm survives in
the AST; the duplicate is dropped on the floor and the error span
points at the second (offending) arm. New unit test
`handle_expr_duplicate_return_arm_first_wins_in_ast` pins the
behaviour by parsing a program with `return(first) => first,
return(second) => second` and asserting the resulting AST's
`return_arm.binding == "first"`.

**Item 4 — `expr_uses_generic` Handle walker comment.** Pre-fix
the comment implied the walker was load-bearing today. Tightened
to call out that the arm is dead code under the current
`Item::Effect → return true` short-circuit; kept for the future
case where Task 54 lifts the `Item::Effect` gate but `Expr::Handle`
still needs a guard during the CPS-transform handoff in Task 55.

**Item 7 — second `[DEVIATION Task 53]` entry.** Adds the
`resumes` / `many` context-ident deviation to PLAN_B_DEVIATIONS.md
with the same four-section structure (plan said / PR did / why /
closure point). Documents the engineering choice to keep the words
as plain Idents matched contextually inside `parse_effect_decl`
rather than reserving them at the lexer level — the cost of
reserving common English words like `many` outweighs the surface
ambiguity benefit, and the existing `resumes_and_many_remain_idents`
+ `resumes_outside_effect_decl_remains_ident` tests serve as
regression guards.

**Item 8 — QUESTIONS.md `[PLAN-B]` handler-arm-surface entry.**
Adds an open question on whether Task 54 should keep qualified-only
arms or introduce bare-op-as-sugar within unambiguous handler
contexts. The first-deviation entry's "Closure point" line now
cross-references the new question rather than leaving the closure
point dangling.

**Item 9 — keyword-side adversarial tests.** Three lexer-keyword
adversarials pinning that `let effect: Int = 1`, `let handle: ...`,
and `let with: ...` all parse-error because the words are reserved
by the lexer as of Task 53. Together with the existing ident-side
regression guard `resumes_outside_effect_decl_remains_ident` for
the context-keyword half, this closes the keyword matrix.

**Items 10 + 11 — recorded as Task 54 prep in PLAN_B_PROGRESS.md.**
Item 10: the staged-E0134 arm-walker doesn't extend the local env
with op-arm bindings or `k` before walking arm bodies; programs
like `E.op(x, k) => x + 1` would emit a spurious E0046 alongside
E0134 today. Item 11: the codegen-entry `Item::Effect → return
true` short-circuit only catches programs with an effect decl in
scope; if Task 54 lifts E0134 before Task 55's CPS expansion lands,
codegen will panic on a `handle` over an unhandled effect with no
decl in scope. Both items recorded under Task 54's "prep" section
so they don't get lost across the PR boundary.

Test deltas: +3 parser tests (first-wins AST shape, effect-as-keyword
adversarial, handle/with-as-keyword adversarial). Pod-verify clean.
@boldfield
Copy link
Copy Markdown
Owner Author

Review fixups pushed (dc6a917)

Addressed required items 1, 2, 4, 5, 7, 8, 9 + recorded items 10 and 11 as Task 54 prep. Item 3 skipped per "skip if no churn"; item 6 forwarded to Task 54's PR description.

# Item Disposition Where
1 Stale doc comment on parse_handle_expr Fixed compiler/src/parser.rs:1097-1106
2 Duplicate return arm clobbers first Fixed (first-wins); test added parser.rs:1136-1141 + new test handle_expr_duplicate_return_arm_first_wins_in_ast
3 eat_resumes_many_attr Option<bool> → enum Skipped (optional, no churn justified)
4 expr_uses_generic Handle walker comment misleading Comment tightened compiler/src/codegen.rs:355-364
5 PR body "no new allocation sites" too strong PR body rewritten (gh pr edit applied)
6 Codegen-gate alignment — note in Task 54 PR Recorded as Task 54 prep item 11 PLAN_B_PROGRESS.md
7 Second [DEVIATION Task 53] entry missing Added PLAN_B_DEVIATIONS.md (new section: "resumes and many shipped as context-sensitive idents")
8 QUESTIONS.md entry + cross-ref from deviation Added; first deviation entry's "Closure point" now cross-refs the question QUESTIONS.md (new entry "Task 54: revisit handler arm surface"); PLAN_B_DEVIATIONS.md first-deviation entry's "Closure point" line
9 Adversarial lexer test for let effect: Int = 1 Added 3 tests covering effect, handle, with parser.rs::effect_as_user_ident_in_let_is_rejected, handle_with_keywords_rejected_in_user_ident_position
10 Typecheck arm-walk binding-scope extension Recorded as Task 54 prep PLAN_B_PROGRESS.md Task 54 prep
11 Codegen-gate alignment for Task 54 Recorded as Task 54 prep PLAN_B_PROGRESS.md Task 54 prep

Test deltas: +3 parser tests (first-wins AST shape; effect-as-keyword adversarial; handle/with-as-keyword adversarials). All pre-existing tests still green; pod-verify clean.

Notes on item 2 (first-wins): the AST now keeps the first return arm; the duplicate is dropped on the floor and the error span points at the second (offending) arm so the user's eye lands on the line they need to remove. New test handle_expr_duplicate_return_arm_first_wins_in_ast parses return(first) => first, return(second) => second and asserts return_arm.binding == "first" to pin the contract.

Notes on items 7/8: the second deviation entry uses the same four-section structure as the qualified-arm one. The "Closure point" line on the qualified-arm deviation now cross-references the new QUESTIONS.md entry. The new question carries forward implications and notes that HandleOpArm::effect: String becomes Option<String> if Task 54 desugars bare-op-as-sugar — purely additive, no breaking change.

Notes on items 10/11: recorded under Task 54's prep: section in PLAN_B_PROGRESS.md. Item 10 calls out that the staged-E0134 arm-walker doesn't extend the local env with op-arm bindings or k before walking arm bodies (so E.op(x, k) => x + 1 would emit a spurious E0046 today; the existing test sidesteps this with body: true). Item 11 calls out that the codegen-entry Item::Effect → return true short-circuit only catches programs with an effect decl in scope, so a handle over an unhandled effect with no decl in scope would skip the gate and reach the unreachable! if Task 54 lifts E0134 before Task 55's CPS expansion lands.

CI polling on the fixup push.

@boldfield
Copy link
Copy Markdown
Owner Author

Re-review — merge

Spot-check confirms all directives addressed; CI green on all 4 lanes.

Item Status Verification
1. Stale doc comment (parser.rs:1097-1103) fixed Doc now describes first-wins semantics correctly
2. Duplicate-return-arm clobber (parser.rs:1130-1146) fixed if return_arm.is_none() gate added; pinned by new test handle_expr_duplicate_return_arm_first_wins_in_ast
3. Option<bool> enum refactor skipped Per reviewer's "skip if no churn justified"
4. Walker comment (codegen.rs:352-367) fixed Now correctly identifies as dead-code-today, kept for Task 54 gate-lift case
5. PR description "Memory discipline" wording tightened Now precise: non-effect programs unchanged baseline, effect programs allocate AST nodes but abort before codegen
6. Forward-looking codegen-gate note forwarded Recorded as Task-54 prep item 11 in PLAN_B_PROGRESS.md with concrete recommendation (keep E0134 live until Task 55, preferred)
7. Second [DEVIATION Task 53] entry added Comprehensive entry in PLAN_B_DEVIATIONS.md covering resumes/many context-ident choice with all four sections + closure-point closed
8. QUESTIONS.md handler-arm-surface entry added Substantive entry; first deviation's closure-point line now cross-references it
9. Adversarial keyword lexer tests added Three tests: effect_as_user_ident_in_let_is_rejected, handle_with_keywords_rejected_in_user_ident_position (covers both handle and with)
10. Typecheck arm-walk binding scope noted Task-54 prep entry in PLAN_B_PROGRESS.md with concrete instructions for Task 54's binding-scope-extending test
11. Codegen-gate alignment noted Task-54 prep entry as above

CI: build + test (ubuntu-24.04), build + test (macos-14), cold-checkout test (ubuntu-24.04), cold-checkout test (macos-14) — all SUCCESS on dc6a917.

Merge. Squash-merge SHA lands in PLAN_B_PROGRESS.md Task 53 commits list as part of Task 54's PR per established convention; flip status from done-pending-ci to done at the same time.

@boldfield boldfield marked this pull request as ready for review April 25, 2026 16:54
@boldfield boldfield merged commit 2ed6628 into main Apr 25, 2026
4 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.

1 participant