Skip to content

experiment: infer label type from its fallthrough default (#6163)#6176

Draft
ggreif wants to merge 2 commits into
gabor/label-defaultsfrom
gabor/label-default-inference
Draft

experiment: infer label type from its fallthrough default (#6163)#6176
ggreif wants to merge 2 commits into
gabor/label-defaultsfrom
gabor/label-default-inference

Conversation

@ggreif

@ggreif ggreif commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Experimental, stacked on #6174 (the parser desugaring). #6174 makes the annotated label x : T = <dflt> <body> work; this completes the issue by letting the compact label x = <dflt> <body> take its type from the default — no : T, and it works in synthesis positions too:

let v = label found = false                         // v : Bool, inferred from the default
  for (x in xs.vals()) { if (x == t) break found true };

Mechanism

LabelE grows a bool, set by the parser action only when a default is present and no annotation is written (accepted grammar unchanged). For that flag the typechecker infers the block's final expression (the default — it can't mention the label) and uses it as the label type; breaks check against it.

To infer that sub-expression without committing its type notes (so the whole block is then checked intact), env grows commit_notes : bool (default true, cleared for that one inference). It threads inward via the usual {env with …}; record-completeness means no note-setter can silently skip it. No global state.

Semantics ("type wins")

form label type
label x = false …; break x true Bool (from the default)
label r = (null : ?Nat) …; break r (?x) ?Nat — annotate the default to widen a sentinel
label p : ?Int = null …; break p (?1) ?Intannotation wins (flag not consulted)
label r = null …; break r (?x) error: ?Nat ⊄ Null (Null isn't bottom)
label s = (return …) …; break s bottom default is forgotten → unit fallback

No lub over break sites; the default (optionally annotated) is authoritative — like a var/let initializer.

Tests

test/run/label-default.mo (compact infer in synthesis + checking, annotation-wins, annotated-default sentinel, bottom-default fallback, laziness, bare label) — all backends; test/fail/label-default-compact.mo (bare narrow sentinel rejects a wider break).

🤖 Generated with Claude Code

Builds on the parser desugaring: `label x = <default> <body>` now takes the
label's type from the default (the body block's final expression) when there
is no annotation, so the compact form needs no `: T` and works in synthesis
positions too (`let v = label found = false …` ⟹ `v : Bool`).

`LabelE` grows a bool flag, set by the parser action only when a default is
present and no annotation is written (the accepted grammar is unchanged).  The
typechecker, for that flag:
  - infers the block's final expression (the default — it cannot mention the
    label) under a new `env.commit_notes = false`, which makes the central
    exp/dec note-setters skip committing, so the whole block is then checked
    intact and the body's `break`s check against that type;
  - if the default diverges (`return`/trap : None) the inference is discarded
    and the label falls back to the unit default, so unit `break`s still work.
An annotation, when present, wins (the flag is not consulted) — `label punt :
?Int = null …` keeps working, and sentinels are widened by annotating the
default itself: `label r = (null : ?Nat) …`.

`env.commit_notes` threads inward via the usual `{env with …}` (one literal
construction, in env_of_scope); record-completeness means no note-setter can
silently skip it.  Desugaring takes the IR label type from the LabelE's own
inferred note rather than the annotation's (the unit placeholder in the infer
case).

Tests: test/run/label-default.mo (compact infer in synthesis + checking,
annotation-wins, annotated-default sentinel, bottom-default fallback, laziness,
bare label); test/fail/label-default-compact.mo (bare narrow sentinel rejects a
wider break).  All backends green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ggreif ggreif requested a review from a team as a code owner June 4, 2026 10:11
@ggreif ggreif self-assigned this Jun 4, 2026
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Cursor AI review

👍 APPROVE — looks safe to merge

Category Assessment Details
Summary Extends compact label x = <default> so the label type is inferred from the default expression (when unannotated), via a parser infer flag, commit_notes in the typechecker, and desugar using the expression note rather than the unit placeholder annotation.
Code Quality Reuses existing infer/check paths; commit_notes is a small, localized guard on note writes rather than new global state.
Consistency Follows existing env field threading, LabelE pattern updates across frontend/lowering, and OCaml record-update conventions.
Correctness Inference-then-check flow, bottom-default T.is_non fallback to unit, and desugar.ml using note.Note.typ are coherent and match the documented semantics.
Tests test/run/label-default.mo and test/fail/label-default-compact.mo with matching .tc.ok / .tc-human.ok updates cover synthesis, checking, annotation-wins, sentinel narrowing, bottom default, and laziness.
Changelog ⚠️ User-visible language behavior changes, but no top-of-file Changelog.md entry was added (repo uses pre-release bullets at the top rather than a ## Next section).

Verdict

Decision: APPROVE
Risk: Low
Reason: The typechecker and desugar changes are tight, well-tested, and internally consistent; the only gap is a missing changelog bullet for an experimental surface, which does not indicate a correctness or regression risk.


Generated for commit c95e053

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Comparing from fae550a to c95e053:
In terms of gas, no changes are observed in 5 tests.
In terms of size, no changes are observed in 5 tests.

Group the annotation and the infer-from-default flag: `LabelE of id *
(typ * bool) * exp` instead of `… * typ * exp * bool`.  Every site that
ignores the middle field — the wildcard matches in definedness, effect,
traversals, interpret, and the `is_explicit_exp` arm — keeps its original
3-pattern form (no `, _` churn).  Only the sites that read or build the
type spec are touched: parser (construct the pair), typing (destructure),
desugar (construct + lower), arrange/astjs (extract the typ).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ggreif ggreif marked this pull request as draft June 7, 2026 06:16
Comment thread src/mo_def/arrange.ml
| LoopE (e1, Some e2, _) -> "LoopE" $$ [exp e1; exp e2]
| ForE (p, e1, e2, _) -> "ForE" $$ [pat p; exp e1; exp e2]
| LabelE (i, t, e) -> "LabelE" $$ [id i; typ t; exp e]
| LabelE (i, (t, b), e) -> "LabelE" $$ [id i; typ t; exp e; Atom (string_of_bool b)]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

the , defaulted should not be at the end

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