checker: stop eating record-literal fn bodies as effect annotations#85
Conversation
Two checker bugs made a program that runs fine on the EIR VM fail to
type-check with E0200 (cannot unify Record{...} with ()).
1. infer_named_fn treated ANY map right after the params as a
`{IO Fail}`-style effect annotation (the parser-quirk fallback). So
`[fn mk [] {:a 1}]` had its whole body swallowed and was inferred as
returning (), breaking every caller. None of the three runtimes
(interp, machine, EIR lower) skip a Map there — only Set — so the
checker diverged from execution. A Map now only counts as an
annotation in the quirk form: all keys and values bare symbols, with
a body expression following.
2. bind_map_destructure unified any non-Map subject with
`Map Keyword v` and bound every key to the same shared value type.
Keyword-keyed literals are Records, so destructuring a param forced
it away from record types AND forced all fields to share one type.
Non-Map subjects are now constrained per key with an open record row
(lenient, like the total `get` builtin: a missing key binds ()).
Adds regression tests for both.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Choji review — Nothing blocking — a few notes Both fixes are correct and well-targeted: the annotation-vs-body disambiguation is sound (all-symbol-keys + trailing body = annotation, anything else = body), and the per-key record-row path in bind_map_destructure correctly replaces the single shared element type. The prior open NITs are either resolved by the new body-requirement guard or remain minor style points that don't affect correctness. 1 minor finding
Verified by ChojiChoji ran your change live — checks passed. 1 check.
The ticket asked for the type-checker to stop treating record-literal fn bodies as effect annotations and to fix map destructuring collapsing record fields to one type; the verification checks confirm the fix works correctly. No live preview was run since this is a compiler/type-checker change with no web UI surface. No issues found in the running app. Reviewed |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dismissing prior approval to re-evaluate 45aa9f3.
What
Fixes two type-checker bugs that made a valid program (one the EIR VM runs correctly) fail with
[E0200] cannot unify Record{...} with ().Repro that used to fail
loon runbut passloon test:Why
1. Record-literal fn bodies were swallowed as effect annotations.
infer_named_fnconsumed anyMapright after the params as a{IO Fail}-style effect annotation (a "parser quirk" fallback). So[fn log-monoid [] {:unit "" :tensor ...}]had its entire body eaten, leaving nothing — the checker inferred the return type as(). That's theRecord{tensor: ...}vs()mismatch: the record the function actually returns at runtime against the()the checker believed. All three runtimes (interp, machine, EIR lower) only skipSet(_), neverMap, so the checker had diverged from execution.Fix: a
Maponly counts as an annotation in the quirk form — all keys and values are bare symbols, and a body expression follows. Keyword-keyed maps (record literals) are the body.2. Map destructuring collapsed record fields to one shared type.
bind_map_destructureunified any non-Mapsubject withMap Keyword vand bound every key to the samev. Since keyword-keyed literals type asRecords, destructuring a param likewboth forced it away from record types and forcedlogandvalto share one type.Fix: keep the shared-element path for genuine
Map Keyword vsubjects; otherwise constrain per key withinfer_record_get_lenient(open row, lenient like the totalgetbuiltin — a missing key binds()at runtime, so it's not a hard error), giving each field its own type.Tests
Two regression tests in
check::tests:record_body_is_not_effect_annotation— record body plus a check that the{IO Fail}quirk form still parses as a declared effect setmap_destructure_record_fields_keep_own_types— the monoid program above, plus a heterogeneous{:log "x" :val 41}destructureThe repro now checks and prints
log: step1;step2; value: done. Full workspace suite passes.🤖 Generated with Claude Code