feat(laurel): user-level polymorphism — generic composites, procedures, datatypes, type aliases#1394
Draft
fabiomadge wants to merge 47 commits into
Conversation
fabiomadge
force-pushed
the
laurel-polymorphism-landing
branch
from
June 19, 2026 00:30
bf29841 to
1776dc6
Compare
fabiomadge
force-pushed
the
laurel-polymorphism-landing
branch
7 times, most recently
from
June 19, 2026 12:10
9917acf to
0b0acb5
Compare
fabiomadge
force-pushed
the
laurel-polymorphism-landing
branch
4 times, most recently
from
June 20, 2026 15:10
9c13d6e to
bd4d182
Compare
fabiomadge
force-pushed
the
laurel-polymorphism-landing
branch
4 times, most recently
from
June 22, 2026 11:33
2217644 to
0f75fea
Compare
…s, and datatypes Adds user-level polymorphism to Laurel, verified end-to-end through Core/SMT. Two mechanisms, forced by the "SMT wall" (every composite erases to one Core type): generic COMPOSITES monomorphize; generic DATATYPES map to native Core parametric datatypes (no monomorphization). Value-level `T` polymorphism rides Core HM freshening. Layers (each builds green on the prior): 1. Differential-verification harness — verifyToMetrics / corpus runner / cross-solution diff, plus the Core.verifyWithStats split (surfaces previously-discarded statistics). 2. Type-variable substrate — HighType.TVar, Procedure/CompositeType.typeArgs, ResolvedNode.typeVar, resolution scoping, translateType → .ftvar. 3. Polymorphic functions — Core HM instantiation; reference args via the uniform Composite. 4. Generic composites (monomorphization) — MonomorphizeComposites: per-instantiation emission across every type position, with a depth cap + fuel guard that fail LOUD. 5. Polymorphic procedures — CallElim per-call-site type-var freshening; the procedure↔composite monomorphization fixpoint; uncalled-poly-proc witness verification. 6. `new C<τ>` explicit-type-argument syntax — grammar → AST → resolution → monomorphize. 7. Gradual-checker .TVar arms (atop strata-org#1121) — polymorphic programs type-check and translate; soundness guarded both directions. 8. Generic composite methods — lifted to polymorphic procedures, reusing layer 5. 9. Generic composite inheritance — concrete parent and generic parent (`Box<T> extends Base<T>`), with remap-aware field inheritance and topological monomorph emission. 10. Remap-aware generic upcasting — substitutedAncestors; `C<args> <: P<remapped-args>`. 11. Generic-composite field-type concretization — reject-only; closes a field-write wildcard wrong-accept without touching the load-bearing .TVar consistency arm. 12. Generic datatypes — native Core parametric datatypes, including the heap-stored composite-field box wrapper; instantiations stay distinct sorts (cross-instantiation rejected). Recursive and nested generics round-trip. Invariant (verified by execution throughout): everything unsupported fails LOUD — translated=false with a diagnostic, or a thrown type error. No silent unsoundness. Reviewed pre-merge by a comprehensive adversarial whole-diff pass: 0 soundness findings, 0 wrong-accepts; the fail-loud invariant holds at every cross-feature seam. Tests: PolymorphismCorpusTest (the table-driven feature corpus — poly procedures, generic composites/methods/inheritance/upcast/field-tvar, generic datatypes), VerifyMetricsHarness (shared Case/checkCase machinery), VerifyMetricsCorpusTest (harness self-tests + monomorphic baseline), VerifyMetricsTest, MatchTypeArgTest, and AbstractToConcreteTreeTranslatorTest (AST→concrete→AST round-trips for the new generic emit productions; writing them surfaced and fixed a formatter bug where the `prec(80)` `appliedType`/`newTypeArgs` ops were parenthesized into non-re-parseable text — `(Box<int>)`, `new Box(<int>)` — corrected via `:0` annotations on the `typeAnnotation`/`new` references), and Core/Tests/PolyProcFreshenTest (Core-layer regression tests for CallElim per-call-site type-var freshening — multi-instantiation verifies, the soundness twin fails at the exact obligation, and the abort-masking ship-blocker is pinned by obligation label). Integrates strata-org#1381 (LiftInstanceProcedure ordering) — its T9_ValueReturningInstanceMethods test passes under the merged pass order (our lift sits at pipeline position 0, satisfying strata-org#1381's lift-before-eliminateValueInReturns requirement). Full build green; corpus green.
Five low-risk follow-ups from the polymorphism-PR backlog, all verified by ablation where they make a behavioral claim: - comesBefore pin (MonomorphizeComposites → HeapParameterization) at the pipeline-assembly site; load-enforced (ablation: a violated order throws at load when a consumer module builds). - numVCs > 0 added to the corpus `.verifies` oracle, closing the vacuous-pass hole (ablation: a 0-obligation translating program is now rejected; none of the existing ~45 .verifies cases regress). - resolveOutputParameter: thread a seen-outputs accumulator so a second same-name output routes to defineNameCheckDup. COSMETIC only — ablation showed the `$heap`-output case already fails loud (type mismatch); this just yields a cleaner "Duplicate definition" diagnostic. NOT a soundness fix (the claimed silent wrong-accept does not reproduce). Pinned by a fail-loud guard. - Reword two stale "Laurel doesn't support polymorphism yet" comments (CoreDefinitionsForLaurel, Resolution) — premise now false. - Delete dead parseOpaqueType (no grammar op, zero callers). lake test green (only the pre-existing ion-java JAR failure).
…rdening Implements the spike-verified follow-ups from the polymorphism backlog. All sound (false twins fail), full lake test green (only pre-existing ion-java). Features: - Item 1: Map/Set-typed composite fields heap-box. instTagCommon gains .TMap/.TSet arms (injective `Map$a2$k$v` tags, fail-loud on untaggable nested args); the three HeapParameterization box-name fns route .TMap/.TSet through appliedBoxTag. Reading/ writing a Map-typed field now round-trips (was boxDestructorNameError). Distinct (K,V) stay distinct boxes (no coalescing — corpus-pinned). Also lets `Box<Map int int>` monomorphize; the two former `untaggable_*` .rejected cases are now .verifies with value-coupled false twins (the rejects were un-implemented limitations, not soundness boundaries). - Item 7B: `is`/`as` operands widened Ident->LaurelType, so `x is Box<int>` / `x as Box<int>` parse, resolve, and verify (operand monomorphizes to the tag lowerIsType keys; `as` lowers via the HeapParam is-guard). Emit routes through highTypeToArg (fixes the .Applied->"Unknown" collapse). - Item 7C: `type Name = Target` surface form (grammar + parseTopLevel arm). Composite -typed aliases work via a transitive alias-unfold in resolveFieldInTypeScope (field resolution is name-keyed and runs before TypeAliasElim). Hardening: - Item 10C: matchTypeArg self-guards on the .Applied base name (Box<T> no longer structurally matches Pair<int>); MatchTypeArgTest case 7 pins it. - Item 10E: value-coupled twins for poly_proc_concrete_composite_param (now calls take + observes the field) and generic_datatype_recursive (reads back by equality). - Item 9+10B: runCrossCheck folds a polyCrossCheckCorpus (verify/fail/reject) so the post-monomorphize re-resolution fold is dual-path-checked. - Item 10D: Expect.rejected gains an optional DiagnosticType kind, checked via a 2nd verifyToDiagnosticModelsCapturing pass; representative cases tagged (.UserError / .NotYetImplemented) so a net<->clean diagnostic move fails loud.
Extends the monomorphic type aliases from 4131c09 with type parameters. All sound (false twins fail, wrong-arg rejects), full lake test green (only pre-existing ion-java). - Grammar/AST/parse: `Option TypeParams` on the `typeAlias` op + `typeArgs` on the `TypeAlias` AST; `target:0` so a Map/composite target emits without parens and re-parses. - Resolution: scope the alias's type params as `.typeVar` (mirrors .Datatype), so `A`/`B` in the target resolve to `.TVar`. - TypeAliasElim: parameterized substitution — `Foo<int>` binds the alias params to the args and substitutes into the target (via substTypeVars). Reordered BEFORE monomorphize so an alias of a generic-composite instantiation unfolds to the real `.Applied` the monomorphizer rewrites (mono is alias-agnostic). - TypeLattice.unfold: `unfoldMap` now carries (params, target); a new `.Applied`-alias arm substitutes args into the target, so isConsistent agrees with elimination at the initial resolve (fixes cross-spelling assignability, e.g. `var b: Foo<int> := new Box<int>` and passing `Box<int>` to a `Foo<int>` param). Unfold-then-compare stays strict, so `Box<bool>` into `Foo<int>` rejects. - Emit: AbstractToConcrete wraps the alias in `typeAliasCommand` and emits typeParams; round-trip test pins `type MyPair<A,B> = Map A B` / `type Foo<T> = Box<T>`. Corpus: generic alias to Map (+ param-order `Swapped<A,B> = Map B A`), to a generic composite (assign/param/wrong-arg/false twins), chained generic aliases, and arity-mismatch reject.
Review follow-ups on the poly test corpora: - GenericCompositeTest: fix the section-doc monomorph tag (Box$int → Box$a1$int, the actual instTagCommon arity-tagged form used everywhere else in the file); pin map_concrete_mismatch + generic_alias_wrong_arg to .rejected (some .UserError). - GenericDatatypeTest: pin generic_datatype_cross_instantiation_rejected to (some .UserError). - PolyProcedureTest: pin the 7 tc_* checker-not-weakened cases to (some .UserError) and the 2 divergent-generic cases to (some .NotYetImplemented) — bare .rejected can't distinguish a clean user error / depth-cap NYI from a StrataBug internal error. All kinds probed + build-confirmed present on THIS branch (user_heap left .StrataBug — its .UserError reclassification is a dispatch-branch change, absent here). Comment/outcome only.
An applied type at the wrong type-argument arity (`MyPair<int>` for a `type MyPair<A,B>`, or `Box<int,bool>` for a `composite Box<T>`) now reports a plain UserError from the resolver instead of surfacing as an internal StrataBug. - Resolution.resolveHighType's `.Applied` arm checks the argument count against the declared param count (unfoldMap for a generic alias, parentExprMap for a generic composite/datatype) and reports a clean user error on mismatch. Before, an arity-wrong alias resolved silently, then TypeAliasElim deleted the alias def while leaving the reference unfolded, so re-resolution reported the dangling name as an 'Internal error' StrataBug. - The pipeline's re-resolution net now fires only when the initial resolution was clean. It exists to police transforms that break an initially-VALID program; when the input was already user-rejected, a downstream dangling reference is just noise on top of the real error. - Pin generic_alias_arity_wrong to .rejected (some .UserError).
Test comments should describe what the mechanism IS, not narrate branch history. Drop 'previously fail-loud', 'spuriously over-rejected', 'was a crash', and the 'SUPERSEDES the runUnsupportedGates gate' narrative from GenericCompositeTest and PolyProcedureTest; keep the mechanism descriptions and what each case pins.
…cases The corpus's `.rejected (some kind)` asserts a diagnostic kind is PRESENT, not exclusive — a spurious extra diagnostic of another kind alongside it still passes. That gap let a cascade regression hide: a divergent generic correctly emits `.NotYetImplemented` (depth cap), but a broken re-resolution suppression folded three `.StrataBug` internal-errors on top, and the presence-only pin stayed green. Add `Expect.rejectedExactly (kind)`: !translated AND every non-Warning diagnostic is exactly `kind`. Pin poly_proc_chain_divergent and poly_proc_uncalled_divergent_witness with it. Verified by negative control: reintroducing the broken suppression makes the pin fail with 'got kinds #[NotYetImplemented, NotYetImplemented, StrataBug, StrataBug, StrataBug]', which the old presence-only pin passed.
Cut self-describing narration and a stale reference (PolymorphismCorpusTest, split into per-feature files) from LaurelCorpusHarness. Keep the load-bearing rationale: the .UserError-vs-.StrataBug kind distinction, why .rejected(some k) is presence-only, and what .rejectedExactly guards against. Comments only; -39/+21 lines.
Remove comments that restate a case's own `why` field, duplicate a section/module docstring, or carry stale external item-labels (7(B), 7(C), Item-5, gap-(ii), '(this commit)'). Keep all load-bearing mechanism/soundness rationale — e.g. the cross-instantiation-distinctness note, the remap-aware upcast reasoning, and the richer freshening-vacuity docstrings were deliberately left intact. Comments only.
…Test
'Pair' was used for three different shapes in one file. Rename the one-param wrapper
`Pair<T> { b: Box<T> }` (nested_generic cases) to `Wrap<T>` — it holds one Box, not a
pair — and give the `is`-operand `Pair<A,B>` its honest second field so every remaining
`Pair` is the conventional two-param/two-field pair. No collision risk either way
('Pair' is not a reserved/prelude/internal name, unlike 'Box'); this is readability.
Five fixes to the polymorphism primitives in this PR, surfaced by building dynamic method dispatch on top of them. Folded back here (squashed) rather than shipped as a follow-up, since this PR has not merged. All verified by execution; the fail-loud soundness invariant holds throughout. 1. Chained field access through a generic-composite field (p#b#az where the intermediate field has a .TVar/.Applied type) was spuriously rejected; resolveFieldRef now threads the synthesized concrete holder type. (Completeness.) 2. A user identifier colliding with a generated monomorph name (e.g. a composite named Box$a1$int, or a $heap output) was reported as an internal StrataBug; now a clean UserError with a rename hint. Corrected six comments that falsely asserted no source identifier can contain '$' (it is a legal identifier char; the monomorph tag encoding is non-injective, with soundness enforced by the downstream duplicate-definition net). (Diagnostic quality.) 3. 'as'-casts in heap-neutral procedures were left un-lowered (the HeapParameterization early-return for heap-neutral procs skipped AsType), hard-failing the Core translator; now the heap-independent AsType rewrite is applied in that branch too. (Completeness.) 4. A non-generic composite extending a generic instantiation (composite IntBox extends Box<int>) did not type-check. Three coordinated fixes: the monomorphizer seeds + rewrites extending-list references; isSubtype gained a .UserDefined-child / .Applied-parent arm (remap-aware via substitutedAncestors, args invariant, so IntBox <: Box<int> succeeds while IntBox <: Box<bool> and the remap-swap shape both still fail); and the final type list is topo-sorted as a whole so a concrete child is emitted after its monomorph parent. (Feature gap; soundness re-verified adversarially.) 5. Re-resolution cascade suppression: a "new" error at post-pass re-resolution on an already-rejected program is downstream cascade, not an internal failure, and is no longer mislabeled StrataBug. Also fixes a name-collision regression introduced by fix 4 in the whole-list topo-sort, which now tracks entries by list index so a user/monomorph name collision is preserved and reported, not coalesced. (Diagnostic quality + regression fix.) Adds 14 corpus cases (generic_chained_field_*, user_heap_output_rejected, user_name_collides_with_monomorph, as_*_downcast_heap_neutral, concrete_extends_geninst_*).
- Rename the nested-generic composite Wrap<T> -> Nest<T>: 'Wrap' collided with the
existing 'datatype Wrap' in the same file (two meanings, reader-confusing). Nest<T>
is free and matches the 'nested generic' intent.
- Inline genericBoxProgram/genericBoxMultiProgram: both single-use, hoisted for no
reason while the other 64 cases inline their src.
- Correct the user_heap_output_rejected comment: it described the pre-fix behavior
('expected Heap got int', 'either way !translated'); the actual diagnostic is one
clean UserError 'Duplicate definition $heap' + rename hint (verified by execution).
- Tighten the alias_composite_field comment (drop 'was wired'/'new grammar' history).
- The two byte-identical worklist-drain loops (main drain + post-witness drain) are extracted into a single `drainWorklist` helper. It recurses structurally on `fuel : Nat` (so it's provably terminating — no `partial`, no `mut fuel`, no `while`) and both call sites share it, so the drains can't drift. Behavior verified identical: divergent→[NYI], multi-inst→[NYI,NYI], normal→verifies. - Fix the module header: a monomorph is `Box$a1$int` (arity-tagged), not `Box$int` — the rest of the file already uses the $a1$ form.
…se recursion) Reviewing TypeAliasElim surfaced that a bare generic name in an annotation (var m: MyPair, for type MyPair<A,B>) reaches Core as a dangling ref -> StrataBug, unlike the clean UserError for MyPair<int>. Attempted to extend resolveHighType's arity check to the bare .UserDefined arm; it over-rejected. ROOT CAUSE (traced by instrumentation, not guessed): the .Applied arm recurses 'resolveHighType base', so the base 'Box' of any 'Box<int>' arrives at the .UserDefined arm as a bare zero-arg name — indistinguishable from a genuinely-complete bare 'Box'. A zero-args check there fires on every applied type's base and rejects 'Box<int>' itself (verified: new_box_int AND param_box_int both trigger it). Reverted; documented the residual gap + its true cause inline. The gap is fail-loud (a rejection, never a wrong-accept), so acceptable — just a less precise message. (An earlier draft of this note blamed the 'new C' constructor head; that was wrong — corrected here after tracing the actual trigger.)
The comments claimed all arity-wrong alias references are handled upstream. Only .Applied (Pair<int>) is — resolveHighType's .Applied arity check catches it. A bare generic alias at zero args (Pair for type Pair<A,B>) is NOT caught upstream (that check can't fire on a bare name — see the Resolution.lean NOTE) and falls through here unfolded, surfacing as a fail-loud StrataBug at re-resolution. Both cases stated once in the docstring; the two inline comments just tag their arm and defer. Rejection, never a wrong-accept.
The comment claimed 'a method's own typeArgs are empty in the surface syntax, so this is a pure addition' — false. Instance methods CAN declare their own type params (e.g. id2<U>(self: Box<T>)), which is exactly why the lifted proc's typeArgs are ct.typeArgs ++ proc.typeArgs, not just ct.typeArgs. Verified: method_own_U (id2<U>) translates only because proc.typeArgs=[U] is carried. Rewrote to state the real composite-then-method ordering (Box$id2<T,U>) and drop the false 'empty/pure' framing.
The comment claimed 'reference-kinded T is erased to a composite by an earlier pass, so it never reaches this arm as .TVar' — false, and it directly contradicts PolymorphicFunctionTest's comment (+ its executing poly_ref_fn case). There is NO erase-reference-T pass: a reference-kinded T reaches translateType as .TVar, lowers to .ftvar, and Core's HM unifies it with the single Composite sort — verified (ref_T_fn: a poly fn over a composite arg translates + verifies). The .ftvar lowering is kind- agnostic. Rewrote the comment to state the real mechanism; the code was already correct.
38 comment lines -> ~20 for the cascade-suppression + collision block. Cut: the three-way enumeration of cascade sources (one example makes the point), the strata-org#1389 'supersedes an earlier workaround' changelog history, the '$body' example, and the 'HeapParam/TypeHierarchy set needsResolves:=false' aside (a DUPLICATE — that rationale already lives inline at both pass definitions). Kept every load-bearing reason: why suppress, why fold-loud, the duplicate-can-only-be-a-user/generated-collision proof that justifies UserError, and the source-location TODO. Comments only; corpus green.
The appliedBoxTag docstring re-explained the non-injectivity caveat in full while also pointing to the canonical copy on instTagCommon — compressed to the mechanism + the pointer (12 lines -> 7). Also fixed a contradiction: the boxDestructorName arm called it 'the shared INJECTIVE tag' while the function's own doc (correctly) says NON-injective — reworded to 'named via appliedBoxTag'. Left the boxConstructorDef arm comment intact (it carries distinct load-bearing content: the box holds the FULL type so translateType picks the right Core sort). Comments only; builds.
…tionsForLaurel
The rewritten docstring already explains the select/update/const stand-ins fully
(external, filtered before Core, int placeholders inert because the polymorphism lives
in the Core primitives). The inline comment above the functions repeated the same
'int placeholders ON PURPOSE / filtered before Core' point ~10 lines below the docstring
— cut it, and tightened the docstring's placeholder clause. Verified while reviewing: the
select/update/const RETURN type changed Box->int by the poly cleanup is inert (these are
external + filtered), and the deleted 'datatype Box {MkBox()}' was vestigial — the heap
'Box' is SYNTHESIZED by HeapParameterization (line 648), a separate datatype. Comments only.
…ontractPass The mentionsTVar docstring and the mkTempAssignments docstring stated the SAME 4-clause rationale near-verbatim (poly callee param is T -> resolution treats T authoritative -> T unbound at Core -> type the temp from the argument's concrete type). The logic lives in mkTempAssignments, so it keeps the full rationale; mentionsTVar is now a one-line 'what it tests' + pointer. Verified while reviewing (with corpusMetricsOf, the correct numFailures measure — NOT a diag count): a contracted poly proc gates correctly across instantiations (false postcondition -> numFailures=1, violated precond via pos(-3,..) -> numFailures=1, multi-inst true -> 0). Comments only.
The extends comment restated that 'bare Base parses as compositeType, Base<T> as appliedType' — which is inherent to having both productions (already documented at appliedType). Cut that clause; kept the two load-bearing parts: WHY parents became LaurelTypes (generic parent instantiation) and the no-ambiguity-with-fields-block note. Left the new<>/lt and FieldPath/multiAssign comments intact (each documents a real disambiguation / anti-regression rationale). Verified all four disambiguation claims by PARSING the sensitive cases (bare-vs-applied, new<> vs '<', FieldPath vs multiAssign value, generic-parent extends) — all parse clean. Comment only.
This 6-line #load_dialect wrapper was buried under a ~25-line running changelog ('Then:
... Then: ...') narrating every past grammar change, scattered both between module/import
and after 'end'. The current grammar is self-documented in LaurelGrammar.st (48 comment
lines on the productions themselves), so the history here was pure redundancy. Replaced it
with a tight header: the file's purpose + the one load-bearing note — the build system does
NOT track the .st as an input, so you must touch this file to force a recompile after
editing the grammar (verified: no .st dep in the lakefile). 46 -> 25 lines; dialect loads,
corpus parses.
…rammar
The translateFieldPath docstring re-argued WHY FieldPath is a dedicated category (the
multiAssign-value parse collision) — but that is a GRAMMAR concern already stated in full
in LaurelGrammar.st where the category is declared. Kept the translation-specific content
(the two-arm mapping + the 'same .Var(.Field) chain a field read produces, so downstream
lowering is uniform' insight, which the grammar does NOT cover) and replaced the parse
re-argument with a pointer. Also fixed a stray-comma typo ('object,)'). Left the extends
inline comment intact (it documents translation-routing, not the grammar's disambiguation).
Verified while reviewing: every poly production translates faithfully (multi-arg
Pair<int,bool>, new<>, is/as LaurelType, datatype/alias typeparams); the removed
parseOpaqueType was dead (no 'opaqueType' production anywhere). Separately noted (NOT this
file's bug): a generic alias of a generic DATATYPE (type Foo<T> = Opt<T>) translates fine
but is rejected downstream ('expected Foo<int>, got Opt') — a real alias-consistency gap,
out of scope here. Comments only.
A generic alias of a generic DATATYPE assigned from a constructor result was wrongly
rejected: 'type Foo<T> = Opt<T>; var o: Foo<int> := Som(5)' gave "expected 'Foo<int>',
got 'Opt'". Root cause: a datatype constructor synthesizes the BARE datatype type
(Som(5) : Opt, no args), so the assign reaches isConsistent's bare-name~instantiation arm
(LaurelAST.lean) as .UserDefined Opt vs .Applied Foo [int]; that arm compared RAW base
names ('Opt' == 'Foo' -> false) without unfolding the alias.
Fix: compare base names AFTER ctx.unfold (which already turns a generic-alias application
Foo<int> into its target Opt<int>). Completeness only — a wrong-REJECT, never a wrong-accept;
verified the guard: a DIFFERENT datatype's constructor into the same alias-typed var is
still rejected (generic_datatype_alias_wrong). The alias-of-COMPOSITE sibling already worked
(its RHS 'new Box<int>' carries args, routing through the .Applied/.Applied arm); this brings
the datatype case to parity. 2 corpus cases; termination proof still discharges; full
Laurel corpus green.
The .Applied emission comment narrated the old lossy behavior ('Previously this dropped
the args (Base<int> -> bare Base)...') — changelog, not current state. Kept the mechanism
(emit appliedType, peel base to Ident name, mirrors newTypeArgs/isType) and restated the
round-trip point as a property of the current code. Verified: the dedicated round-trip
test (AbstractToConcreteTreeTranslatorTest — already covers Box<T>, new Box<int>, generic
extends, generic datatype, poly function) stays green. Comment only.
… poly hunks
The comment-trim pass I should have run on these two densest files (missed earlier —
I'd only accuracy-spot-checked them). MonomorphizeComposites: the 'fuel = coarse backstop /
maxInstDepth = real guard' point was stated 4x — dropped the two redundant restatements (the
step-3 trailing clause and the drainWorklist call-site comment, both covered by
drainWorklist's docstring); kept the fixpoint-algorithm + FUEL-EXHAUSTION-guard blocks (each
a distinct load-bearing point). Resolution: dropped two changelog parentheticals ('Was
resolveRef over bare-name Identifiers', 'an earlier guard ... was removed') — kept the
current-state rationale. Comments only; builds + corpus green.
…instead Reverts the over-rewrite in 8bf1ea5 that gutted this file's running 'Then: ...' grammar changelog. That log IS the file's mechanism: since the build system does not track the .st as an input, appending a line is how you trigger a recompile after a grammar edit — so the right update is one appended entry, not a rewrite. Restored the original verbatim and added a single 'Then:' line for this pass's .st comment trim. Builds; corpus parses.
…ecompile NOTE) Removes the running 'Then: ...' grammar changelog (noise) and the 'Last grammar change' bump line. Keeps only what is load-bearing: the file loads the dialect from LaurelGrammar.st, and the NOTE that the build system does not track the .st as an input (so edit this file to force a recompile). Undoes the over-eager revert in 33138db that restored the whole log. Builds; corpus parses.
… verbatim
Delete ONLY the 'Then: ...'/'Grammar change:' changelog lines. The three original lines —
'loaded from LaurelGrammar.st' + the two-line NOTE ('not tracked by the build system; update
this file (e.g. this comment) to trigger a recompile') — are preserved BYTE-FOR-BYTE, not
paraphrased. That NOTE's 'update this file' line IS the recompile-trigger / last-grammar-change
bump point. Supersedes my earlier churn on this file (rewrite -> revert -> paraphrase); the diff
vs the original is now pure deletion, nothing added.
…grammar delta The NOTE says to bump 'this comment' to trigger a recompile, but there was no designated comment to bump. Add one 'Last grammar change (polymorphism): ...' line that summarizes ALL the grammar (.st) changes this PR makes — <T> binders, appliedType, new C<τ>, is/as over LaurelType, generic extends, the type-alias command, the FieldPath category — replacing the old running 'Then: ...' log with a single line that serves the log's purpose (record the latest grammar change + the point to bump). Builds; corpus parses.
…ination proof Replace `partial def` with a terminating `def`: match on `op.args.toList` (list patterns generate the match unfold-equations a well-founded def needs; the `#[...]` array literals do not) and discharge termination with an extracted reusable lemma `sizeOf_mem_args_lt` (an element of `op.args` is a strict subterm of the operation). This is the only `partial def` the polymorphism PR added to this file; the other five predate it and are unchanged. No behavior change: chained-field-write tests (T9_ChainedFieldAccess, T1_MutableFields, T9_OldHeapTwoState, GenericCompositeTest, AbstractToConcreteTreeTranslatorTest) all pass.
fabiomadge
force-pushed
the
laurel-polymorphism-landing
branch
from
July 17, 2026 20:46
b52136b to
dcc9704
Compare
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
User-level polymorphism for Laurel: generic composites, polymorphic procedures/functions, and generic datatypes. Verified end-to-end through Core/SMT.
Three mechanisms
The SMT wall (composites erase to one Core type; no sort quantification) rules out a uniform approach:
T→ Core HM freshening (per call site).Covers generic fields, methods, inheritance (incl.
Box<T> extends Base<T>), remap-aware upcasting,new C<τ>, polymorphic procedures, and recursive/nested generic datatypes.Follow-up work (commits on top of the original squash)
Five commits extend the original landing. Each is individually built +
lake test-green; soundness is pinned by paired must-fail twins (a.verifiescase is matched with a_wrongcase that must fail).chore): stale-comment rewords; dead-code (parseOpaqueType) deletion; acomesBeforepin (monomorphize → heap-parameterization, load-enforced); anumVCs > 0vacuity guard on the.verifiestest oracle; a cosmeticresolveOutputParameterdup-check (a$heap-named user output now fails loud withDuplicate definitioninstead of a type-mismatch — both already rejected, so not a soundness change).Map/Set-typed composite fields now heap-box (reading/writing aMap-typed field works; was a fail-loud limitation). Implemented as a 1:1 mirror of the generic-datatype tag; distinct(K,V)stay distinct boxes (no coalescing — corpus-pinned).is/asoperands:x is Box<int>/x as Box<int>now parse, resolve, and verify (operand was a bareIdent). Also fixes a pre-existing emit bug (.Appliedis/as operands were serialized as the literal"Unknown").type Name = Target, including generic aliasestype Foo<T> = …(Map/primitive and composite targets; transitive/chained; param-order-preserving). Cyclic aliases fail loud.matchTypeArgbase-name self-guard (monomorphizer is now self-guarding, not trusting an upstream gate); a polymorphic program added to the differential cross-check; value-coupled twins; an optional diagnostic-kind assertion on.rejectedcorpus cases.Soundness
Anything unsupported fails loud (
translated=false+ diagnostic, or a type error) — no silent unsoundness, verified by execution.An adversarial whole-diff review found 0 soundness findings, 0 wrong-accepts — scoped to the original squash. The five follow-up commits added soundness-relevant surface (the type-consistency relation
isConsistent/TypeLattice.unfold, the monomorphizer'smatchTypeArg,resolveOutputParameter, and new grammar/parse) that this review did not cover. That surface is instead backed by: paired must-fail twins per feature, a no-coalescing pair for Map-field boxing, a cross-spelling-reject twin for generic aliases (Box<bool>into aFoo<int>param rejects), and fulllake testgreen. (A separate adversarial pass over the follow-up diff found no live wrong-accept either, but it is not the same "comprehensive whole-diff" review the original got.)Tests
PolymorphismCorpusTest(the feature corpus — every feature paired with a must-fail twin),VerifyMetricsCorpusTest(+ a polymorphic differential cross-check),VerifyMetricsTest,MatchTypeArgTest, and anAbstractToConcreteTreeTranslatorTestround-trip case (incl. generic aliases). Full build andlake testpass locally (only the pre-existing ion-java JAR fixture is absent); CI in progress.Pipeline ordering
liftInstanceProceduresPassruns at position 0 (must precede monomorphization). Type-alias elimination runs before monomorphization (so an alias of a generic-composite instantiation unfolds to the real.Appliedthe monomorphizer rewrites). The monomorphize → heap-parameterization order is enforced by a load-timecomesBeforeconstraint.Known debt (non-blocking)
CallElimCorrectis execution-guarded — its correctness theorem is stated over value/store semantics and quantifies over no types, so per-call-site type freshening is outside its scope (and that proof file is standalone, imported nowhere). A Strata-team conversation, not a code gap.The shipped
.TSetarms are unreachable (noSetsurface syntax yet) and kept only for symmetry with.TMap.Migration note (breaking changes to
HighType/CompositeType)Two AST changes affect anyone who pattern-matches on
HighTypeor constructsCompositeType:HighType.TVar(a type variable). Non-exhaustivematch HighTypenow needs a.TVararm (Lean fills a missing arm withsorry, which compiles but aborts at evaluation — see the StrataPython fix in this PR for the pattern).CompositeType.extending : List Identifier → List HighTypeMd(a composite may now extend a generic parentBase<T>). Constructors passing bare names wrap each as⟨.UserDefined name, none⟩.In-repo consumers (incl. the Python→Laurel translator) are updated here; the separate Java→Laurel translator will need the same two one-line changes.
(Follow-up surface changes —
is/asgrammar operandIdent→LaurelType,TypeAlias.typeArgs,TypeLattice.unfoldMapshape — are internal or have defaults, so they are not breaking for external pattern-matchers.)Post-review fixes (found while building dynamic dispatch on top of this work)
Building method dispatch on top of these polymorphism primitives stress-tested them and
surfaced five issues in this PR's own code. They are folded in here (squashed into one
fix(laurel)commit) rather than shipped as a follow-up, since this PR has not merged.All are verified by execution; the fail-loud invariant still holds everywhere.
Chained field access through a generic-composite field —
p#b#azwherep : Pair<int,Z>and field
bhas a type-variable (or.Applied) declared type was spuriously rejected("'az' is not defined").
targetTypeName/resolveFieldRefdropped.TVar/.Appliedfieldtypes; fixed by threading the synthesized concrete holder type into
resolveFieldRef.(Completeness — rejected valid programs.)
$-name collision diagnostics + false comments — a user identifier colliding with agenerated monomorph name (e.g. a composite literally named
Box$a1$int, or a$heapoutput) was reported as an
Internal error/.StrataBug, blaming the compiler for a usermistake. Now a clean
.UserErrorwith a rename hint. Also corrected six comments asserting"no source identifier can contain
$" — factually false ($is a legal identifier char;the monomorph tag encoding is non-injective, with soundness enforced by the downstream
duplicate-definition net, not by the tag). (Diagnostic quality.)
as-cast in heap-neutral procedures —HeapParameterizationonly loweredas(
x as T→{ assert (x is T); x }) in procedures that read/write the heap; a heap-neutralprocedure took an early return, leaving
AsTypeun-lowered and hard-failing the Coretranslator with
NotYetImplemented. Now lowered in the heap-neutral branch too (only theheap-independent
AsTyperewrite, not the heap-dependent ones). (Completeness.)A non-generic composite extending a generic instantiation —
composite IntBox extends Box<int>did not type-check (upcast rejected, inherited field unresolved). Three coordinatedfixes: the monomorphizer now seeds + rewrites
extending-list references (Box<int>→Box$a1$int);isSubtypegained a.UserDefined-child /.Applied-parent arm (remap-awarevia
substitutedAncestors, args invariant — soIntBox <: Box<int>succeeds whileIntBox <: Box<bool>and the prior-bug remap-swap shape both still fail); and the final typelist is topo-sorted as a whole so a concrete child emitted after its monomorph parent.
(Feature gap. Soundness re-verified adversarially: ~30 wrong-upcast / false-twin programs,
none accepted.)
Re-resolution cascade suppression — when a program already has a non-warning error, a
"new" error at post-pass re-resolution is downstream cascade (the same error restated over
the rewritten types), not a genuine internal failure; it is now suppressed rather than
mislabeled
.StrataBug. (Diagnostic quality. Also fixed a name-collision regression in thewhole-list topo-sort — it now tracks entries by list index so a user/monomorph name collision
is preserved and reported, not coalesced.)
Corpus: 14 new cases (
generic_chained_field_*,user_heap_output_rejected,user_name_collides_with_monomorph,as_*_downcast_heap_neutral,concrete_extends_geninst_*).Full
lake testgreen (only the pre-existing ion-java JAR failure).