v0.1.5
Added
-
Release documentation-consistency sweep. The spec's §9.3/§9.4 Map-key and Set-element lists no longer offer
Unit(contradicting §2.2.2's E135 zero-size rejection), and SKILL's Map/Set sections state the E135 constraint; SKILL's conformance count is re-anchored tocheck_doc_counts.py(the gate's regex had silently stopped matching a reworded sentence, letting the count drift 18 stale — the re-anchored pattern is mutation-validated); FAQ leads with the PyPI install route and carries live test/example counts plus the<Diverge>effect; TOOLCHAIN drops stale release-relative claims; CONTRIBUTING's release checklist names everycheck_version_sync.pysurface including README and the unconditionaluv.lockregen. -
The
veralangdistribution ships to production PyPI (#737), through the approval-gated Trusted Publishing workflow, and the installation documentation carries both routes with an accurate account of what each installs:python -m pip install veralang(LSP extra:"veralang[lsp]") delivers the compiler and theveracommand only, while the GitHub source checkout additionally provides the bundled examples, the conformance suite, and the specification — so the source route is the recommended default in the agent-facing docs (SKILL.md, the FAQ) whose teaching material lives in the checkout, and the PyPI route leads on the toolchain-oriented surfaces (PYPI_README, the site's install section). The distribution is namedveralang; the installed command remainsveraand Python code importsvera—pip install verais an unrelated PyPI project and is never the right command. -
check_doc_counts.pygates the README project-status test count (PR #1088 review). The README's "N tests" figure in the Project status section matched none of the oracle's README patterns, so it had silently drifted hundreds of tests stale; a new anchored pattern locks it to the live collection count, mutation-validated (a stale figure fails the gate). -
Approval-gated, tokenless release automation (#481) turns a strictly increasing version on
maininto one tested wheel/sdist artifact, publishes that exact artifact through PyPI Trusted Publishing, verifies registry filenames and SHA-256 hashes, then creates the immutable tag and GitHub Release. A separate manual TestPyPI path stages the current version; a guarded recovery path cannot overwrite a published version or bypass package changes. Build, OIDC publication, registry verification, and GitHub release privileges remain separate jobs, and release policy now forbids tag-moving/fold-in releases after publication. -
PyPI publication readiness (#737) renames the Python distribution to
veralangwhile preserving theveracommand and import package, adds a dedicated registry README, and gates the built sdist/wheel contents plus an installed-wheel CLI smoke test in CI. The existing GitHub-source installation path remains supported; the first live production package remains the completion gate for #737. -
Inference + JSON composition example (#379) demonstrates an effectful model call flowing through pure JSON parsing, typed integer extraction, and a statically proved 0–100 normalization contract. Raw JSON and tagged or untagged fenced responses are accepted, with the original completion retained in malformed-response diagnostics.
Fixed
- A mismatch diagnostic renders a leaked type variable as
?, not a bare letter (#1069). A refinement alias over a generic container —type M = { @Map<String, Int> | ... }— read through an element derivation inference cannot thread (map_values(@M.0)[0]) leaves the built-in's value-type variable unsubstituted; the mismatch message then stripped its internal#bnamespacing marker (#970/#982) down to a bareVthat reads as a real type (body has type V, expected Int). Such a leaked internal placeholder — a namespaced built-in var or a fresh inference hole — now renders as the unknown marker?(body has type ?, expected Int) at the actual-type slot of every reachable type-mismatch message — the subsumption sites (function body [E121],letvalue [E170], anonymous-function body [E171], constructor field [E213], effect- and ability-operation argument [E204]/[E241],apply_fnargument [E202], Ord-operation operand [E242], handler state initial value /withupdate [E331]/[E335]), the operator/index/interpolation family (arithmetic operands [E140], equality and ordering comparison [E142]/[E143], logical operands [E144]/[E145], unary!and-[E146]/[E147], string-interpolation part [E148], array index [E160], non-indexable collection [E161]),assert/assumearguments [E172]/[E173],ifcondition and branch types [E300]/[E301] (description and fix text), and the contract and refinement predicates (requires[E123],ensures[E124], refinement [E126]) — recursing into type arguments (Array<?>) and into a function type's effect row (effects(<State<?>>);pretty_effectrenders each effect instance's type arguments, so an unscrubbed row would surface the same bare letter). The handful of actual-type renders a leaked placeholder provably cannot reach keep plainpretty_type: the numeric-join [E141] and ordering-compatibility [E142] arms (operands proven numeric/orderable first), match-arm unification [E302] (is_subtypeaccepts a TypeVar arm), the Eq-ability operand message (contains_typevarearly-return),apply_fn's arity message (the value is proven a function type), and thedata-invariant message (invariantindatais grammar-rejected, #686). The change is message text only: what the checker accepts or rejects is unchanged, a genuine userforallvar (plainT) keeps its own spelling, and a built-in's unsubstituted expected signature (Array<T>) is deliberately left untouched. - A fn-typed slot works as the closure argument to
array_map/array_mapi(#1056).array_map([1, 2, 3], @Mapper.0)— wheretype Mapper = fn(Int -> Int);and@Mapper.0is a let-bound or parameter slot — was check-green but dropped the enclosing function via [E602] ("could not infer array_map closure return type"), while an inlinefn(...) { ... }at the same position compiled andapply_fnover the identical slot already resolved fine. The map emission inferred the output element type only from an inlineAnonFn; it now routes through the same_closure_arg_return_typeresolverapply_fnuses, recovering the return type from the slot'sFnTypealias signature (including a type-changingInt -> Stringmapper, whose output element type must come from the slot rather than the input array) and emitting thecall_indirectexactly as for an inline closure. The shared inference also coversarray_mapiand thearray_foldaccumulator. - A self-referential type alias through a type argument is rejected at check time (E132) (#1059).
type F = Future<F>;, the mutualtype A = Future<B>; type B = Future<A>;, andtype L = Array<L>;were all admitted byvera check— the #648 cyclic-alias walk followed only baretype A = Breferences and never descended into a generic's type arguments — and then either crashed code generation with an unguardedRecursionErrorin_type_expr_to_wasm_type(theFuturespellings, whose type argument the WAT-type mapper recurses through) or compiled to a degenerate type inhabited only by[](Array<L>). The cycle detector now walks the full alias-reference graph, descending into everytype_argandRefinementTypebase while excluding a generic alias's own type parameters, so every such previously-admitted self-referential alias is now the same [E132]Cyclic type aliasdiagnostic the directtype A = B; type B = Acycle already produced. The rule is structural — a self-reference inside a type argument the generic alias discards (type Wrap<T> = Int; type C = Wrap<C>;) is rejected on the same rule, and the edge set covers arbitrarily nested type arguments (type A = Future<Array<B>>; type B = A;). Function-type parameter/return positions are exempt (type FA = fn(FA -> Int) effects(pure);registers — a function value is a table-index indirection, the same exemptiondataADTs get; spec §2.6.3 now says so). The cycle walk runs on an explicit stack, so a thousand-alias legal chain checks clean instead of crashing the checker with aRecursionError(PR #1066 review). A legal acyclic nesting (type A = Future<Int>; type B = Future<A>;) still checks. - Container builtins over alias-spelled or user-fn arguments emit with the right host tag (#1063). The Map/Set builtins' emission inference (
map_keys/map_values/map_get/set_to_array) fell through to the"b"(i32) host-import tag — the empty-collection escape hatch — for the same aliased and user-fn argument spellings, and returned silently WRONG values on a check-green, verify-green program (i64 values truncated to their low 32 bits, String keys garbled; pre-existing onmain, reproduced there before fixing; #1055's aliased index pins passed over it only by fresh-heap luck). All three container inference helpers now consult the shared rebuilder first, with values above 2^32 pinning the tags; the genuinely-unknown empty-collection shape keeps its permissive fall-through, also pinned. - An alias to a Future (
type FA = Future<Option<Int>>;) compiles at the match-scrutinee and constructor-argument sites (#1054). The alias spelling was check- and verify-green but E602-skipped where the direct spelling compiled: the SlotRef/ResultRef WAT-type mapper resolved the slot name through the name-only base-name hop (dropping the alias's type arguments) and its Future-transparency arm guarded on AST type arguments an alias-spelled ref does not carry. The mapper now canonicalizes an alias to its target's full compound spelling first (the #1037 walk) and adds the string-form Future arm its sibling scalar mapper carries — the alias analog of #1046, closing the last mapper in the family without the canonicalizer. Found by this PR's adversarial review. - Indexing directly on a builtin call result compiles (#1048).
array_concat([1, 2], [3, 4])[2]— a subscript applied straight to a builtin call, for any element type — was check-green but dropped the enclosing function via [E602], while the let-bound form (let @Array<Int> = array_concat(...); @Array<Int>.0[2]) compiled._infer_index_element_type_expr's FnCall arm resolved only user-function returns (the_fn_ret_type_exprsregistry holds no builtins), so element-type inference returnedNoneand the function was skipped. The arm now recovers a builtin call's returnNamedTypethrough the same_get_arg_type_info_wasmconsultor the sibling Vera-type inference and instantiation discovery use — arg-forwarding forarray_concat/array_append/array_slice/array_filterand the_BUILTIN_PARAMETERIZED_RETURNStable for the concrete-Arraybuiltins (array_range,string_split,json_keys, …) — then extracts the element type exactly as the user-function path does; chained indexing through a nestedArray<Array<T>>result resolves too. Builtins whose element is a type variable (array_map/array_mapi/array_flatten) stay absent from that consultor and still route through the let-bound form. - Alias-spelled container arguments to the element-type derivations canonicalize (#1055).
map_values(@M.0)[0]viatype M = Map<String, Int>;(andset_to_arrayvia a Set alias) E602-dropped where the direct spelling compiled: the argument reached the #1051 derivations as its bare alias name with no type arguments, so the class arms never saw the container shape. The shared argument-type rebuilder now canonicalizes a bare alias name to its target's full compound spelling (the #1037 walk) before parsing. The array builtins with aliased arguments (array_flatten(@Grid.0)) additionally drop earlier, in the builtin's call emission — that alias extension rides #1053. - Indexing directly on a type-variable-element builtin call result compiles (#1051, the type-variable follow-up to #1048). The eight
Array-returning builtins whose element type depends on the call's arguments —array_reverse,array_sort_by,array_flatten,map_keys,map_values,set_to_array,array_map,array_mapi— were check-green but dropped the enclosing function via [E602] when indexed directly (array_reverse([10, 20, 30])[0]), while the let-bound form (let @Array<Int> = array_reverse(...); @Array<Int>.0[0]) compiled. They are deliberately absent from the shared_get_arg_type_info_wasmconsultor #1048 reuses —_BUILTIN_PARAMETERIZED_RETURNSis registry-locked to type-variable-free returns (aTypeVar-carrying entry would bind a phantom var in clone-name discovery, rejected bytest_parameterized_table_matches_registry) — so_builtin_call_ret_named_typenow derives their returnNamedTypefrom the call's arguments, per mechanism class: argument-forwarding (array_reverse/array_sort_byreturn arg0's type verbatim,array_flattenunwraps oneArray<>layer), container-arg-derived (map_keys/map_values/set_to_arrayread K/V/T off theMap/Setargument), and closure-return-derived (array_map/array_mapitake the element from the closure argument's declared return type). The derivation stays off the shared consultor, so clone-name discovery is untouched; a genuinely unresolvable argument shape (e.g. a nested type-variable-element builtin, whose call already fails to emit) keeps the loud [E602] skip. array_flattenof an inline nested array literal compiles (#1052).array_flatten([[10, 20], [30, 40]])— a nested array literal passed straight as a call argument — was check-green but dropped the enclosing function via [E602] ("could not recover inner element type for array_flatten"), while the let-bound-argument form (let @Array<Array<Int>> = [[..], [..]]; array_flatten(@Array<Array<Int>>.0)) compiled._translate_array_flattenrecovered the inner element typeTonly from aSlotRef@Array<Array<T>>argument; an inline literal fell through to the loud skip.Tis now taken from the inner literal's element type through the same_infer_array_element_typerecovery a nested literal in aletposition already uses; theArray<String>inner variant resolves too. An empty inline literal (array_flatten([])) carries no element type and still keeps the loud skip.- A type-variable-element builtin nested as another builtin's call argument compiles (#1053, the call-emission sibling of #1051).
array_reverse(array_flatten(x))— a type-variable-elementArraybuiltin passed straight as another combinator's argument — was check-green but dropped the enclosing function via [E602] ("could not infer array_reverse element type"), even fully let-bound. The outer combinator's element-type probe (_infer_concat_elem_type) dropped the inner<T>layer of aSlotRef@Array<Array<T>>(it returned the bareArray), so the innerarray_flattencould not be unwrapped; inference now falls back to the shared #1051_builtin_call_ret_named_typederivation and reads back the element name, soarray_reverse/array_sort_byof an innerarray_flattenresolve. The converse nesting — a builtin call asarray_flatten's own argument (array_flatten(array_map(xs, |x| [..]))) — recoversTthe same way in_translate_array_flatten. Consultor-resolvable inner calls (array_reverse(array_concat(a, b))) and typevar-in-typevar (array_reverse(array_reverse(x))) already worked; a genuinely unresolvable inner call keeps the loud [E602] skip. Three more call-argument spellings ride the same fix: an alias-spelled array argument (type Grid = Array<Array<Int>>; array_flatten(@Grid.0),type Row = Array<Int>; array_reverse(@Row.0)) reached the combinators' emission probes as its bare alias name and dropped — they now canonicalize through the same shared rebuilder #1055 taught the index-side derivations (the call-emission dual of that fix). A user-fn call argument with a flatArray<T>return (array_reverse(mk()),array_sort_by(mk(), cmp)) already resolved through this fix's fallback and is pinned; a NESTED return (array_flatten(mkn())withmkn(-> @Array<Array<Int>>)) stayed unresolvable because the shared consultor deliberately blanks nested type-arg positions (clone-name-discovery lockstep) — the rebuilder now reads the registered fn's declared return type directly, off the consultor, so discovery is untouched. - Directly indexing a nested type-variable-element builtin result compiles (#1094, completing the #1048/#1051/#1053 family).
array_reverse(array_reverse([10, 20, 30]))[0],array_sort_by(map_values(m), cmp)[0],array_reverse(map_values(m))[0], andarray_flatten(array_reverse(x))[0]— a subscript applied straight to a type-variable-elementArraybuiltin whose own argument is another type-variable-element builtin — were check- and verify-green but dropped the enclosing function via [E602], while every let-bound spelling (let @Array<Int> = array_reverse(array_reverse(x)); @Array<Int>.0[0]) ran. The #1051 per-class element-type derivation resolved its argument through the shared_get_arg_type_info_wasmconsultor only, which deliberately cannot report a type-variable-element inner call (its element type depends on the call's arguments), so the derivation returnedNoneand the index dropped. The class-1 argument-forwarding arm (array_reverse/array_sort_by), thearray_flattenunwrap arm, and the class-2 container arm now resolve aFnCallargument through_builtin_call_ret_named_type— the shared consultor first, then the same per-class derivation, recursing — so the inner call's element type is recovered; every other argument shape (SlotRef, array/nested literal) still routes through the consultor, and a genuinely unresolvable argument keeps the loud [E602] skip. ABlock-wrapped argument (array_reverse({ array_reverse(x) })[0]) resolves via its tail expression on both the element inference and the deep resolution, matching the container emissions' Block handling. The newexamples/scoreboard.verareads a top score with the natural direct-index spellingarray_sort_by(map_values(@Board.0), cmp)[0]over atype Board = Map<String, Int>;alias. - Alias-spelled inner element names classify as their target's representation at the array emissions (#1067). With
type Row = Array<Int>; type Grid = Array<Row>;,array_reverse(@Grid.0)[0][0]returned 4626322722586886145 andarray_length(array_reverse(@Grid.0)[0])returned 81948 — check-green, verify-green, garbage: the emission probes recovered the element as the bare alias nameRow, and the size/pair classification fell to the 4-byte opaque-pointer default for what is really an 8-byte (ptr, len) pair, so every combinator copied half of each element;array_concatand the depth-2array_flatten(@Array<Array<Row>>) read past their allocations (unreachable traps), and the@Row-comparatorarray_sort_bytrapped mid-sort. Element names now canonicalize to the target's compound spelling at the element-probe exit, inarray_flatten's input gate and T-recovery (whose alias-layer unwrap also sees through an alias-spelled middle layer, soarray_flatten(@Grid.0)andarray_flatten(@Array<Row>.0)compile instead of dropping), and all five shapes plus the sort compute correct values. The direct spellings (@Array<Row>,@Array<Array<Row>>) mis-classified identically through the pre-existing direct arm — latent onmain, where those shapes still E602-dropped for unrelated reasons — and are fixed and pinned alongside the alias spellings. - A generic alias of a container as a declared return derives through its target, never its own type args (#1068).
type MyMap<V> = Map<String, V>; fn mk(-> @MyMap<Int>)thenmap_keys(mk())[0]handed the runner a VALIDATION-FAILING module whilevera compileexited 0: the container derivation consumed("MyMap", ("Int",))'s type args positionally with no container-name check, deriving key elementIntwhere the target saysString, and the emission contradicted the derivation in the same function. The shared rebuilder now substitutes a generic alias's args through its target (substitute_type_vars) and canonicalizes — soMyMap<Int>resolves toMap<String, Int>and bothmap_keysandmap_valuesover it compute correct values — and the container derivation additionally verifies the resolved argument ISMap/Setbefore reading K/V/T off it, so an unresolvable alias shape stays a loud [E602] skip rather than an invalid module. Caught before merge — the consuming derivation is this PR's own (#1051) machinery, so no released version is affected. - Bare-alias user-fn returns and Block-wrapped arguments resolve through the container emissions (#1071, completing the #1063 family). A user fn whose DECLARED return is a bare alias of a container (
fn mkm(-> @M)withtype M = Map<String, Int>;) silently truncated throughmap_keys/map_values/map_get/set_to_array— the shared consultor's user-fn arm only reports parameterized returns, so the bare alias exited unresolved before any alias handling and the emission fell to the"b"mis-tag (values returned as their low 32 bits, String keys garbled; pre-existing onmain, reproduced there before fixing). The rebuilder now recovers a registered non-generic user fn's declared return directly when the consultor reports nothing, off the consultor, and a Block-wrapped container argument (map_values({ @M.0 })) resolves via its tail expression instead of riding the same mis-tag. The_map_wasm_tagfall-through comments now state exactly which argument shapes resolve and which still reach the permissive tag. - A
MaporSetwith a zero-size key, value, or element type is a checker error (E135) (#1075).Map<String, Unit>— direct or alias spelling — checked clean and compiled exit-0 to INVALID WASM ("expected i32 but nothing on stack": the zero-size value pushes no operand where the container's host import expects one); pre-existing onmain, surfaced by the PR #1061 adversarial delta review. The determination follows spec §2.2's declared-vs-materialized line:Mapkeys/values andSetelements are raw, unboxed host-serialized values — representationally theArray<Unit>element case its E135 gate (#945) already rejects with this exact rationale — not the boxed-ADT-field case (Box<Unit>/Option<Unit>fields live inside a heap layout and stay legal, as doesMap<String, Option<Unit>>, pinned). Rejection also keeps one canonical form (DESIGN.md principle 3):Map<K, Unit>is informationally aSet<K>, which the fix text names. The type-resolution gate now coversMap(both positions, reported per position) andSet, keyed on erasure (Future<Unit>rejects identically); the annotation-free spellings, whose types exist only through inference, are backstopped at codegen —_map_wasm_tagrefuses the tag through the same RECURSIVE erasure oracle the checker keys on (_slot_name_erases_to_unit: alias chains canonicalized,Future<...>payloads recursed), and container entry types resolve rebuilder-first so a parameterized user-fn return reaches the tag as its full spelling rather than a bareFuturehead — a literal name comparison here was defeated by any indirection (async(async(())), a fn returning@Future<Unit>directly or behindtype FU = Future<Unit>/type Task = Future<Unit>, all previously exit-0 invalid modules; PR #1083 adversarial review) — so the emission takes the loud [E602] skip instead of emitting an invalid module, while the genuinely element-type-free empty-collection shape (map_keys(map_new())) keeps its permissive fall-through. The same entry resolver routes an alias-of-Arrayuser-fn return (type Names = Array<String>) into the existing Array-typed loud skip instead of a mis-tagged scalar slot. Spec §2.2 names the container rule alongside the Array one, andch02_map_unit_value_rejectedjoins the conformance suite's negative fixtures (161 programs). - A nested constructor pattern after a zero-size component selects the right match arm (#1042).
match Tuple((), MkBox(4242)) { Tuple(@Unit, MkBox(@Int)) -> ..., Tuple(@Unit, MkNot(@Int)) -> ... }silently ran theMkNotarm — check-green, verify-green, wrong value. Construction stores nothing for a zero-size component, but the nested-pattern tag walk gave it four bytes (the registered layout's generic type or ani32default), so every nested constructor tag after it was checked at the wrong offset, failed against garbage, and match fall-through picked a later arm — whose extraction (computed correctly) then returned the real value through the wrong arm. The walk now gives erased components zero width, mirroring construction and the extraction walks; the same shape through a transparentFuture<Unit>and through a user-ADT parent is pinned. Pre-existing for bareUnitsince zero-size components landed (#902); surfaced by the #1035 review. The registered-layout side of the divergence (wildcard sub-patterns, structuralEq) is #1043. - A
Future<Unit>component in alet-destructure or match binding erases like bareUnit(#1031). A zero-sizeFuture<Unit>tuple/ADT component was check-green butCodegenSkipped the whole enclosing function (E602): the three codegen declaration-position guards — and the constructor-argument WASM-type inference that gates reachability (constructing aFuture<Unit>field needsasync(())) — compared the bare stringUnit, so the transparentFuture<Unit>wrapper (representation-identical to itsUnitpayload, #841) missed the zero-size branch and fell through to the skip. All four sites now key on erasure (mirroringerases_to_unit/ codegen's_type_expr_to_wasm_type), so a declaration binding aFuture<Unit>component compiles and binds nothing, exactly like bareUnit, with the non-zero-size sibling component landing at the correct offset. An alias to the compound (type FU = Future<Unit>;, including alias-of-alias chains) erases the same way: the keying canonicalizes an alias to its target's full compound spelling rather than resolving the base name only, which dropped the type arguments (FUresolved toFuture, notFuture<Unit>) and skipped the function — an alias insideFuture<...>already worked. Reads of the erased component stay checker-rejected (E182, #1005), unchanged. - An alias to a representable compound (
type FI = Future<Int>;) maps to its WAT type (#1037). AFIdestructure/match component or standalonelet @FI = async(41);binding was check+verify green but E602-skipped the whole enclosing function, while the directFuture<Int>spelling compiled:_slot_name_to_wasm_typeresolved aliases through the same name-only recursion the zero-size keying had (FIresolved to bareFuture, matched no branch, returned no representation). The mapper now canonicalizes an alias to its target's full compound spelling through the walk shared with the zero-size keying, so an alias binds exactly like its target written directly — the component binds a real local (awaitrecovers the payload) and siblings land at the correct offsets. Zero-size erasure is unaffected, and a genuinely unrepresentable name still skips loudly. awaitof aFuture<T>compiles in match-scrutinee position (#1038).match await(@Future<T>.0) { ... }was check/verify-green but skipped the enclosing function (E602, "could not infer match scrutinee WASM type") for every non-Unitpayload — anOption<Int>, a user ADT, and a scalarIntalike — because the SlotRef/ResultRef WASM-type mapper did not treatFuture<T>as transparent the way the let-binding path already does. It now unwrapsFuture<T>toT's representation, so awaiting a future directly in scrutinee position matches the await-into-a-let-then-match form that already worked. (Future<String>in that position remains blocked upstream by its own let-binding limitation, a separate gap.)- Reading a zero-size slot is a checker error (E182) (#1005). A
@Unit.nread — a normal function's@Unitparam or agetclause's@Unitop parameter alike — passedcheckand died at codegen with the E699 dangling-slot internal error, since a Unit value compiles to no WASM local. The checker now rejects the read everywhere with a fix pointing at the unit literal(), keyed on representation (erases_to_unit) so aFuture<Unit>slot is rejected the same way; declaring the parameter stays legal, mirroring the declare-vs-read line the E206 generic-at-Unit guard draws. - Effect ops in array-literal elements compile; a
letof a zero-size type is a checker error (E183) (#1006).[get(()), 1]was check/verify-green but skipped the enclosing function (E602) because codegen's element-type inference did not know effect ops — both op-injection sites now recordget's Vera result type (State<T>'s T) alongside the WAT type they already recorded, and the element dispatches through the clause-aware lowering (a transforminggetclause applies to the element).let @Unit = put(5);, whose binding could never be read, is now rejected at check with a fix pointing at the statement formput(5);— uniformly for any zero-size binding, effect-op or plain RHS. - A
letof a pair-representedFuture<T>payload binds two locals (#1039).let @Future<String> = async("hello");— and anyFuture<T>whose payload is pair-represented (String,Array<T>) — was check/verify-green but skipped the enclosing function (E602, "has no WASM representation").Future<T>is representation-transparent (#841), soFuture<String>is ani32_pair, but the let translator's pair detection keyed on the literalString/Arraynames rather than the transparent payload, so the wrapper fell to the scalar branch — where the scalar mapper recursed through it to a pair inner and returned no WASM type. The pair predicate is now Future-transparent, mirroring the existing Future arm in the scalar mapper, so a pair-payload Future binds and reads two locals exactly like a bare String/Array let; a scalar (Future<Int>) or pointer (Future<Box>) payload stays a single local, and a zero-size payload (Future<Unit>) still skips loudly. - A closure that captures a
Future<T>free variable serialises it at the payload's width (#1044). Capturing aFuture<String>returned 0 and capturing aFuture<Int>trapped at WASM validation (expected i32, found i64): the capture-width decision in the closure free-variable walker matched only the literalString/Arraynames and routed everything else — including a representation-transparentFuture<T>wrapper (#841) — through the scalar mapper that maps unknowns toi32, so a pair payload stored ptr-only (its length read back as adjacent zero) and an i64 payload pushed an i64 into ani32.store. The decision now runs through the same Future-transparent_is_pair_type_name/_slot_name_to_wasm_typedeciders the let-binding and slot-read paths use; both the capture-store and capture-load sides read the corrected width from the one capture record, so they stay in agreement. Array<Future<T>>elements are sized, loaded, and stored at the payload's representation (#1045).Array<Future<Int>>trapped at WASM validation (expected i32, found i64) andArray<Future<String>>trapped (values remaining on stack): the five module-level array-element helpers matchedString/Array/ the scalar dict with noFuturestrip, so an i64 payload was sized as a 4-byte i32 and a pair payload as a single word. SinceFuture<T>is representation-transparent (#841), the helpers now stripFuture<…>(nesting included) to the payload before deciding. The parallel index-read path needed the same fix: element-type inference dropped the wrapper's type argument, collapsingFuture<Int>to a bareFuturethat fell to the i32 default — it now preserves theFuture<…>payload so the read stride and load op match the store.- An alias to a pair-payload Future (
type FS = Future<String>;) binds like its target (#1046).let @FS = async("hello");was check/verify-green but E602-skipped the enclosing function even with the #1039 and #1037 fixes in place: the pair predicate resolved the alias through the name-only base-name hop, which drops type arguments —FSresolved to bareFuture, missed the Future-transparency arm, and fell to the scalar mapper, whose canonicalStringspelling has no scalar representation. The predicate now canonicalizes an alias to its target's full compound spelling first (the #1037_canonicalize_alias_slot_namewalk, cycle-cut threaded through the Future recursion), so an alias — including alias-of-alias chains — behaves exactly like its target written directly; the scalar-alias sibling (type FI = Future<Int>;, #1037) is unchanged. vera rundisplays a bareFuture<String>return as its string (#1047). A function returningFuture<String>printed a raw heap pointer instead of the string, because the return-is-Stringpredicate that populatesfn_string_returns(soexecute()decodes the(ptr, len)pair for display) lacked theFuturestrip.Future<T>is representation-transparent (#841), so aFuture<String>return has the same pair shape as aString; the predicate now recurses through the wrapper. The emitted WASM was already sound — a caller that awaits the future gets the right value; only the top-level display was affected.- Array combinators over
Array<Future<T>>copy elements at the payload's representation (#1057). Once #1045 madeArray<Future<T>>literals and index reads sound, the array combinators became reachable on those arrays and returned garbage on check/verify-green programs:array_concat/array_slice/array_reverse/array_filteroverArray<Future<Int>>returned wrong values (a 4-byte stride copied over the 8-byte i64 payloads),Array<Future<String>>concat half-read the two-word pair,array_appendtrapped at WASM validation (expected i32, found i64), andarray_flattenflattened garbage. Each combinator recovered its element type through a site that returned the bare headFuturewith the type argument dropped —_infer_concat_elem_typefor concat/slice/reverse/filter, a separate element-local decision forarray_append, and a separate inner-type AST walk forarray_flatten— so the element helpers could not stripFuture<…>(#841) and collapsed to the 4-byte i32 default. Every site now preserves the fullFuture<…>spelling (andarray_appendstrips the wrapper before typing its element local), mirroring the #1045 index-read fix. - An aliased
Future<T>array element is stored at the payload's representation (#1058).type FI = Future<Int>; let @Array<FI> = [async(1)];was check-green then trapped at WASM validation (expected i32, found i64): the array-literal store resolved the element type through the name-only base-name hop, which drops type arguments, soFIresolved to bareFutureand the i64 payload was stored withi32.store. The literal store now canonicalizes an alias to its target's full compound spelling (Future<Int>) before resolving, mirroring the #1046_canonicalize_alias_slot_nameorder, so an aliased Future element — scalar or pair payload — stores exactly like its target spelled directly. - Array combinators over alias-spelled element types copy at the target's representation (#1062). The alias-spelled sibling of #1057, one layer down: once the #1058 fix made
Array<FI>(type FI = Future<Int>) arrays constructible, the combinators became reachable on them and received the bare alias name as the element type — from the shared element-type probe (concat/slice/reverse/filter), thearray_appendelement inference, and thearray_flatteninner-type walk. The module-level element helpers have no alias table, so the raw name fell to the 4-byte i32 default on check/verify-green programs: silent wrong values for Future aliases (concat returned 9448928052300 for an expected 1122), anexpected i32, found i64validation trap forarray_append, and — reachable even before #1058 — a wrong element for a scalar alias liketype Flag = Bool, whose 1-byte elements were copied at the 4-byte stride. Each site now canonicalizes a bare alias name to its target's full compound spelling before resolving, mirroring the #1058 literal-store fix; the index-read path was already alias-clean via its canonical element resolution (#559). - Array combinators over alias-named collections resolve the real element type (#1064). Pre-existing on main (no #1057/#1058/#1062 enabler involved): with
type Flags = Array<Bool>;,array_concat(@Flags.1, @Flags.0)was check/verify-green but wrong — the collection slot's own name misses theArraymatch in the element-type probe, so the probe returned None and concat fell back to an 8-byte default stride. The fallback is coincidentally correct for Int, String, and array elements (why the shape survived unnoticed) and silently wrong for 1-byte Bool/Byte elements;array_sliceand the map-family combinators loudly skipped the enclosing function instead. A literal whose elements are alias-typed slots (array_concat([@Flags.1], [@Flags.0])) hit the sibling hole in the probe's literal branch — the bare alias name sized the two-word pair elements at 4 bytes and indexing the result trappedunreachable. The probe now canonicalizes an alias-named collection to its target's full spelling and takes the element from it, canonicalizing a bare element name in turn (sotype Grid = Array<Row>— whose target spelling keepsRowopaque — resolves to the row's real pair shape rather than regressing to the 4-byte default), and the literal branch canonicalizes bare inferred names the same way. Aliased collections of Int and of aliased rows keep their previous values, now by construction rather than by the 8-byte coincidence. - An array element spelled
Future<Alias>is sized at the payload's representation (#1074). The alias-inside-Future<…>sibling of #1062, one layer deeper: an element writtenArray<Future<FlagA>>(type FlagA = Bool) — or hidden behind another alias,type X = Future<FlagA>; Array<X>— was check+verify+compile-green but mis-sized. The alias canonicalization resolved only a top-level alias name (or peeled theFuture<…>wrapper), never an alias sitting inside the wrapper's type argument, so the element deciders_strip_future'dFuture<FlagA>to a bareFlagAthe name-keyed size dict fell to the 4-byte i32 default: a Bool/Byte payload's 1-byte-packed data was read at a 4-byte stride (silent wrong values — every element misread), while an Int/Nat/Float64/String payload trapped at WASM validation (expected i32, found i64/values remaining on stack) at instantiation, behind an exit-0 compile. Pre-existing, not a regression: before this branch's combinator commits the same sites returned the bare headFuture, whose element size is the identical 4-byte i32 default — the same wrong stride; #1057/#1058/#1062 fixed the non-alias and alias-of-Futurepayloads but the alias-inside-Futurevariant was never within the canonicalizer's reach and had no fixture._canonicalize_alias_slot_namenow recurses into the transparentFuture<…>payload (Future<FlagA>→Future<Bool>) and the four element-type deciders (_infer_concat_elem_typefor concat/slice/reverse/filter, the array-literal store, thearray_flatteninner walk, and the index-read element inference) route through it uniformly;array_append's element inference already consulted the shared canonicalizer. Found by the adversarial delta review of this PR's combinator commits; the fix ships on the same branch. - Registered constructor layouts erase zero-size fields (#1043). A declared
Unitfield — or a transparentFuture<Unit>, or an alias/alias-chain to either — was given a spurious 4-bytei32slot in the registeredConstructorLayout(the_resolve_field_wasm_typewt is Nonefallback returned"i32"), while construction lays such a field out erasure-aware: zero-size, storing nothing. Every consumer trusting the registered offsets then read a shifted address on a check-green program: a wildcard over the erased field plus a nested constructor pattern matched the wrong arm off zeroed fresh-alloc memory, and structuralEq/show/hashcompared, rendered, and folded the wrong bytes — all silently wrong. Registration now returns the"unit"sentinel for any erases-to-Unit field (mirroring construction),_wasm_type_size/_wasm_type_alignlearn it as size 0 / align 1, the erased field's Vera type name canonicalises toUnit(so aFuture<Unit>field isEq-derivable and rendered like the zero-size value it is), and the pattern-match and structural-eq/show/hash consumers treat a zero-size field as the equal-by-definition value it represents. - A wildcard over a type-parameter field instantiated to
Unitreads the right offset (#1060). A generic constructor's type-parameter field (Box<T>fieldT) registers as the generic 4-bytei32placeholder, but construction lays each instantiation out concretely —Box<Unit>erases the field to zero bytes (it is not boxed). A WILDCARD sub-pattern over that field advanced the match offset walk by the generici32width regardless, so on aBox<Unit>every field after the erased one was read four bytes too high — silently, on a check-green program with no diagnostics:match b { MkB(@Int, _, @Int) -> @Int.0 }returned0instead of the real trailingInt,N(_, @String)onNamed<Unit>read theStringheader at the wrong offset, andEn(_, MkBox(@Int))onEntry<Unit>read the nested constructor tag at a shifted address and matched the wrong arm. This is the type-parameter sibling of #1043's declared-Unitfield: only the two wildcard walks (_extract_constructor_fieldsand_sub_pattern_wasm_type) were wrong —@Unitbindings, let-destructures, and structuralEq/show/hashalready recompute each field from the concrete instantiation and were correct for the same shapes spelled with the literalUnitargument (a non-literal erases-to-Unit argument — an alias,Future<Unit>— hit the same class of hole inEqand in the width recomputation itself; that is #1070, fixed below). The wildcard walks now recompute a bare type-parameter field's width from the scrutinee's concrete type arguments, mirroring the eq/show recomputation. Where the instantiation is unrecoverable (a direct-call scrutinee whose inferred type dropped its arguments) and a later field is read, the function now LOUD-skips (E602) rather than reading a wrong offset; a trailing unrecoverable wildcard — whose width is never consumed — still compiles. - Erases-to-Unit type arguments work spelled non-literally:
Box<U>,Box<Future<Unit>>, aliases, chains (#1070). The #1060 wildcard width recomputation resolved a type-parameter field's concrete type from the scrutinee's type arguments — but the width function's zero-size test was the literal nameUnit. Registration canonicalises a declared erased field toUnit(#1043); a type argument keeps its use-site spelling, soBox<U>(type U = Unit;),Box<Future<Unit>>,Box<FU>(type FU = Future<Unit>;), and alias chains got 4 bytes and every later field was again read at a shifted offset — silently, check-green: the trailingIntread 0 instead of 22, and the nested-constructor variant matched off garbage. The same literal-test disease broke structuralEqover the same spellings — pre-existing, not introduced by #1060 (the #1060 claim that eq/show/hash "were already correct" held only for the literal spelling): the==dispatch's concreteness gate and free-type-variable heuristic both classifiedUas an unresolved type variable, so the comparison silently fell back to the scalar pointer compare and two structurally equal values compared unequal;show/hashover the same shapes loud-skipped (E602). Every site now keys on erasure rather than the literal name — the width function (_eq_field_wasm_type), the resolved-field-type canonicalisation (_resolve_field_type_for_eq, mirroring registration), the dispatch gates (_eq_type_name_fully_concrete,_type_arg_is_free_var), and theEq-derivability gate (_type_eq_derivable, kept in lockstep with the$eqgenerator per the #732 differential) — so all these spellings behave exactly like the literalUnit: wildcards read the constructed offsets,Eqcompares structurally, andshow/hashrender and fold the real fields. Rider: a zero-size binding (@Unit) after an unrecoverable type-parameter wildcard no longer counts as a "later read" (it binds nothing and loads nothing), so that trailing shape compiles instead of over-conservatively loud-skipping; a genuine read beyond the erased binding still loud-skips. - Structural
==over non-Unit alias type arguments compares values, not pointers (#1076).Box<MyInt>(type MyInt = Int;),Box<MyStr>,Box<MyBool>,Box<Future<Int>>, and alias chains: the==dispatch's free-type-variable heuristic and concreteness gate classified the alias spelling as an unresolved type variable, so the comparison silently fell back to the scalar pointer compare — two structurally equal values compared unequal (0) on a check-green program, while the literal spellings compared correctly. This is the non-Unit half of the heuristic #1070 patched for erases-to-Unit spellings; pre-existing alongside it. The fix grounds a spelling — alias chains resolved, transparentFuture<...>wrappers peeled to their payload — at every site the #1070 pass keyed on erasure: the classifier and concreteness gate (so the dispatch routes structurally), the E613 derivability gate (kept in lockstep with the$eqgenerator per the #732 differential — a wrong loud E613 was the alternative failure), the field-type resolution (so aMyStrfield dispatches to the String content comparison and aFuture<Int>field compares its payload), and the width function (so the #1060 wildcard walks size aMyInt/Future<Int>field as i64, not the 4-byte pointer default — a followingBoolread the right offset). Distinct values keep comparing unequal, including payloads that collide in their low 32 bits (2^32 + 7vs7), and a genuine free type variable still classifies free (the dead base-generic clone keeps its harmless scalar lowering, #912). Grounding the shared field resolution also letsshow/hashrender and fold such fields instead of loud-skipping. show/hashaccept aliased-Unit spellings at the two sites the #1070 pass missed (#1077).show/hashof aTuple<U, Int>(type U = Unit;) loud-skipped the enclosing function: the Tuple-variadic plan branch passed raw type arguments to the per-field dispatch — unlike the registered-ADT branch #1070 fixed — so the unknown nameUabandoned the whole render; the arguments are now grounded exactly like ADT field resolutions, and the erased component takes zero width so the followingIntis read at its constructed offset. A bare value of an erases-to-Unit alias type (a function returning@U) missed the top-level show/hashUnitarms, which compared the literal name; both arms now key on erasure. All four shapes were loud drops (E602), never wrong values; the literal spellings were already correct and stay pinned.- Element-wise
==on arrays of parameterized ADTs compares structurally (#1078).Array<Box<Int>>— literal spelling included — silently pointer-compared its elements: anIndexExproperand's element type reaches the dispatch as the bare head (Box; the element-type inference drops the recoveredNamedType's type arguments), so a parameterized element was classified as a lost-type-argument clone (#772/#912 residue class) and==took the scalar fallback — equal elements compared unequal on a check-green program, while the same values compared directly (no array) and arrays of non-generic ADTs were correct. The==recovery chain now re-derives the full element spelling from the indexed collection, which carries its complete type arguments; aliased instantiations (Array<Box<U>>,Array<Box<MyInt>>) ride the #1070/#1076 grounding from there. Distinct elements keep comparing unequal, i64-wide. - Structural
==over an alias of a WHOLE ADT compares values, not pointers (#1085).type MyBox = Box<Int>;then@MyBox.0 == @MyBox.1: the operand reached the==dispatch as the bare alias nameMyBox, absent from_adt_type_names, so the structural branch was skipped and==fell to the scalar pointer compare — two structurally equal, distinct-pointer values compared unequal (0) on a check-green program, while the array-element form and the directBox<Int>spelling compared correctly. This is the whole-OPERAND sibling of #1076, which grounded type ARGUMENTS (Box<MyInt>) but not an operand whose own type is an alias of the ADT. The operand's spelling is now grounded through the shared_canonical_field_typecanonicalizer at the dispatch site (alias chains resolved, transparentFuture<...>peeled), soMyBox→Box<Int>and the baseBoxroutes to the structural$eqhelper; an alias to a non-generic ADT and an alias chain ground the same way. Distinct values keep comparing unequal, i64-wide (2^32 + 7vs7), and a registered ADT or a genuine freeTis returned unchanged, so the lost-type-arg and dead base-generic-clone controls still take the harmless scalar fallback (#912). The same grounding also fixes==through a refinement of a whole ADT (type NB = { @Box<Int> | true };then@NB.0 == @NB.1) — the refinement alias resolves to its base spelling in the same walk, where it previously fell to the same silent pointer compare (equal values → 0 on a check-green program; pinned equal-and-distinct). Pre-existing alongside #1076; found by the adversarial review of PR #1084. - A
forall<T where Eq<T>>instantiated at an Eq alias is accepted, not wrong-loud E613'd (#1086).same(mk(), mk())where the argument is@Box<MyInt>(type MyInt = Int;) was rejected with a wrong-loud [E613] "Type 'MyInt' does not satisfy ability 'Eq'": the monomorphizer's top-level constraint gate (_check_constraints) testedconcrete in type_set(a primitive) then_adt_satisfies_eq(a registered ADT layout) — an alias name is neither, so a legal program the alias-resolving checker accepted was refused before codegen. The gate now grounds the spelling through the shared_type_eq_derivableoracle — the SAME derivability check the$eqgenerator's field resolution mirrors, so the #732 checker↔codegen differential holds at this entry point too — acceptingMyInt(→Int), an alias of a whole ADT, and a transparent-Futurealias (FI/Future<Int>→Int) alike. A genuinely non-Eq alias (type BadArr = Array<Int>;) grounds to a non-Eq type and still raises the correct [E613] with no codegen [E699] invariant hit (the two sides stay in lockstep). The emitted clone name and message keep the un-ground spelling (the #772/#932 hard constraint). Pre-existing; found by the adversarial review of PR #1084. show/hashof a bare or aliasedFuturevalue renders the payload, not E602 (#1087).show(@FI.0)(type FI = Future<Int>;) andshow(@Future<Int>.0)loud-skipped the enclosing function ([E602]): the argument's inferred type reached the top-level show/hash dispatch asFI/Future<Int>, matched no primitive / Unit / composite arm, and abandoned the render — whileshowof the literalIntpayload rendered fine. #1077 keyed the top-level Unit arm on erasure (covering aliased-Unit spellings); this is the non-Unit bare-Futuresibling at the same dispatch._translate_show/_translate_hashnow ground the inferred type through the shared_canonical_field_typecanonicalizer (alias chains resolved, transparentFuture<...>peeled to its payload —FI→Int), so the render / fold dispatches exactly as the literal payload would; a non-alias, non-Futurename is returned unchanged, leaving every other arm untouched. Distinct payloads still hash distinctly at i64 width (2^32 + 7vs7). The grounding also covers a bare aliased-primitive (type MyInt = Int;,show(@MyInt.0)) and refinement (type Pos = { @Int | ... };) value, which loud-skipped the same way. That top-level grounding alone still missed two spellings (PR #1090 review): a compositeFuturepayload —show(@FOI.0)withtype FOI = Future<Option<Int>>;, bare spelling included — and an alias of a whole ADT (type MyBox = Box;, #1091, found during this PR), because the composite path then recovers the argument's PARAMETERIZED type from its declared spelling (_parameterized_arg_type), undoing the grounding before_show_value/_hash_valueresolves constructor plans. The recovered spelling is now grounded at both dispatch sites, and the composite render's Array arms ground their element type the same way (a@Array<FI>slot's element reached the size table, the element load, and the recursive render as the raw alias —[mkf(), mkf()]array literals included). ATuplecomponent and a generic-ADT constructor argument of an aliased-Futuretype already rendered correctly — the recovery's raw component spellings are grounded at consumption by the #1076/#1077 plan resolutions — and stay pinned. All these shapes were loud drops ([E602]), never wrong values. Pre-existing; found by the adversarial review of PR #1084 and CodeRabbit's review of PR #1090.- An int literal coerced into a
@Byte-instantiated generic constructor field is stored at the field's width (#1092).let @Box<Byte> = MkB(0);— the one literal-at-Byte-field spelling the checker admits (an in-range0..255literal coerces to@Bytethrough the generic instantiation; a declaredBytefield rejects the literal with [E213], and out-of-range, negative, or non-literal@Intarguments are loud [E170] rejections) — was constructed at the literal's own i64 width: construction sized the field from the ARGUMENT's inferred WASM type and stored at the 8-byte-aligned i64 slot, while every READER — field extraction, the structural-$eqhelper,show/hash— sizes the field from the instantiated type (Byte→ i32 at the i32 offset). Extraction read0for a stored255, andMkB(0) == MkB(255)compared the same wrong bytes on both sides and returned equal — silently, on a check-green program; the@Byte-slot passthrough (MkB(@Byte.0)) always stored i32 and was correct. Construction now stores the coerced literal at the field's i32 Byte width, keyed on the checker-recorded target type (the #820 side-table; the target's argument name is grounded, soBox<MB>viatype MB = Byte;— the spelling the #1086 fix newly admits underforall<T where Eq<T>>— coerces like the literalByte, and anInt-instantiated literal field keeps its i64 store,2^32staying distinct from0). Pre-existing on the base (direct spelling included); found by the independent adversarial review of PR #1090. #1060 made the type-parameter wildcard walks instantiation-aware for a slot scrutinee, but a direct-call scrutinee —match mk() { MkB(@Int, _, @Int) -> @Int.0 }wheremk() -> @Box<Unit>— still LOUD-skipped (E602) whenever a later field was read: codegen's_infer_vera_type(FnCall)returns the bare base head ("Box") for a parameterized i32-pointer return, dropping the<Unit>the walk needs, so it refused to guess a shifted offset — the sound interim behavior. The match lowering now recovers the scrutinee's full concrete type (Box<Unit>) from the callee's declared return type (_fn_ret_type_exprs, which a non-generic signature declares fully — no type variables to resolve), matching the slot form, so those shapes compile and read the real value (a trailingInt, a nested constructor tag, aNamed<Unit>Stringread-back). - A generic-call
matchscrutinee resolves its instantiation from the call site (#1072). The generic sibling of #1065:match wrap(5) { … }whereforall<T> fn wrap(@T -> @P2<T, Unit>)— the declared return carries type variables, so the #1065 non-generic recovery did not apply and the wildcard walk could not learn the erased field's width. Onmainthis family read shifted offsets silently (the pre-#1060 walk; the repro returns0instead of22on a check-green program); the #1049 stack turned it into a sound E602 LOUD-skip. The match lowering now binds the callee's type variables from the call site — the same_unify_param_arg_wasmunification the generic call-rewrite performs — substitutes them into the declared return, and renders the full instantiation (P2<Int, Unit>), covering the concrete-Unit-type-arg return, a var-typed field atString(i32_pair width), a fully concrete parameterized return on a generic fn, and the nested-constructor andString-read-back variants. An unresolved variable still falls back to the LOUD-skip (sound); instantiating the variable itself atUnitstays E206-rejected at check, and a phantom-var callee stays E121-rejected. - A module-call
matchscrutinee recovers its instantiation through the shared resolver (#1073). The module door of the same family:match boxlib::mk() { … }never entered the #1065/#1072 recovery (it dispatches on the bare-name call form) and fell to the same bare-base-head collapse — a silent wrong value onmain, a sound E602 LOUD-skip on the #1049 stack. The match lowering now resolves the qualified target through the single shared module-call resolver (the #774-reviewed source of truth) and recurses into the same declared-return recovery, covering imported non-generic and imported generic (#1072 x #1073 compound) scrutinees. array_map/array_mapi/array_foldoverArray<Future<T>>size the output element (and fold accumulator) at the payload's representation (#1079). The bare-head family (#1057's mechanism) at the closure-return / fold-accumulator inference site, found during the #1074 work:_infer_closure_return_vera_type(and_infer_fold_init_vera_type's SlotRef-init fallback) returned the canonical NamedType's bare.name—"Future", the type argument dropped — before the element deciders run, so the #1045/#1057 element fixes and the #1074 payload canonicalizer never reached this path, and the DIRECTFuture<Int>spelling mis-sized too, not only aliases.array_maprejects an async closure, so the trigger is a pure closure forwarding an existing Future value — the identity/reshuffle pattern. Failure modes behind a check+verify-green, exit-0 compile split by payload: Int/Nat/Float64/String payloads trapped —indirect call type mismatchat run for map/mapi (the registeredcall_indirectsignature said the closure returns i32 while the closure compiled returning i64/f64/i32_pair),expected i32, found i64at WASM validation for a fold accumulator (the acc local was typed i32 while the init pushes i64) — while Bool payloads were silently wrong (both signatures agree at i32, so the 4-byte-stride stores simply misread through the 1-byte-packed index reads). Both sites now render the FULL compound spelling —_format_named_typeplus the #1074-extended_canonicalize_alias_slot_namepayload walk — covering the direct, aliased-element (type FI = Future<Int>), and aliased-payload (Future<Big>,type Big = Int) spellings, thearray_mapiandarray_foldtwins, a block-wrapped closure literal reaching the SlotRef-init fallback, and a chainedarray_concat(array_map(...), …). Twelve regression tests, each mutation-validated against its exact pre-fix failure.array_mapoverArray<Future<Bool>>reads back the values it wrote (#1081). A silent regression the #1041 merge introduced and the #1079 write-side fix closes: #1041's element-READ stripping sizesFuture<Bool>index reads at the payload's 1-byte stride, butarray_map's element WRITE path still inferred the bare head"Future"(#1079's mechanism) and stored at the 4-byte default — the previously-consistent 4/4 round-trip became 1/4, and an identity map over[false, true, false, true]read back all-false (2222 for the base-correct 2121) with every downstream element misread. Pinned with the base-correct 2121-pattern regression test plus a no-map control from the adversarial review of the merge.- Collection-alias
Array<Future<Alias>>elements canonicalize through the array combinators (#1082). The #1074 residual arm, found by the #1041-merge adversarial review:_infer_concat_elem_type's collection-alias arm (#1064) resolvestype Rows = Array<Future<FlagA>>to the target spelling and extracts the element — but returned a COMPOUND element (Future<FlagA>) verbatim, skipping the payload canonicalization the sibling bare-element branch performs (type FF = Future<FlagA>; type Rows = Array<FF>already worked)._strip_futurethen handed the size dict the unresolved aliasFlagA, which fell to the 4-byte default: silent wrong values througharray_concat/array_reversefor Bool AND i64 payloads (concat of two distinct >2^32 futures returned 0; reverse returned byte-scrambled garbage) and coincidence-dependent reads througharray_slice/array_filter, all check+verify-green. On this branch the arm's verbatim return is neutralized by the #1067 single-exit canonicalization (_infer_concat_elem_typecanonicalizes every raw result, and the #1074-extended payload walk resolvesFuture<FlagA>→Future<Bool>); six regression tests — including the previously-coincidence-green slice shape — pin the behavior, four of them flipping RED if the exit canonicalization is removed. - Nested generic where-helpers are parent-qualified mono bases (#1014). Two same-named
forallhelpers under different parents were keyed flat, first-seen-wins, so the second parent's call silently ran the first parent's body (check-green, wrong value). A shared transform now qualifies every nested generic base (a$where$g) and rewrites its lexically-visible calls, on both the codegen and verifier sides — the same collision also merged the two helpers' instance sets in the verifier, which now keys them distinctly. - Imported module where-helpers get the parent-qualified hoist (#1015). Two imported functions each carrying a same-named non-generic
where-helper silently resolved to the first one's body; each resolved module's AST now receives the same #991 hoist the main program gets before registration. - A grandchild helper calling an aunt inside a generic clone's where-tree compiles (#1012). The generic clone hoister rewrote calls with this-level names only; it now threads the full ancestor scope, mirroring the non-generic hoister, so the call resolves to the hoisted aunt instead of dangling at WAT assembly.
- A
forallhelper under a generic ancestor is instantiated per clone (#1002). Clone hoisting substituted only the ancestor's type variables, leaving an own-forallhelper as a still-generic template whose call dangled on a check-green, verify-green program; a per-clone instantiation worklist now clones it at its concrete call sites, and the verifier discovers and verifies the same instances (a lying nested contract is a loud E500). - Imported function bodies seed generic-instantiation discovery (#999). An imported non-generic function calling its own nested
forallhelper compiled to a dangling name because discovery only scanned the importer's declarations; resolved-module bodies are now qualified (module-namespaced, so two modules' same-named nested helpers stay distinct bases), harvested, and seeded on both the codegen and verifier sides, with #998 origin threading so clones compile against their module's span tables; nested-helper instances key on one canonical concrete-free module-qualified chain across emission, discovery, and per-instance verification. - Private module generics reached transitively through a public generic are harvested — module-qualified (#1000, supersedes draft PR #1026). The public generic's clone body call now routes to the module's own private helper under
mod$<path>$<name>— never a bare name, so a same-named local function keeps its body in BOTH directions (pre-fix the transitive call was silently captured by the local). Every imported declaration's private-generic calls are routed — non-generic callers and private-to-private chains included — and the verifier discovers and verifies the same qualified instances on every path (including through a locally-shadowed public generic), so a lying private contract is a loud E500 even when another module has a truthful same-named one. - A module function constructing its own ADT compiles regardless of the importer's type imports (#1008). Constructor layouts were registered per the importer's filter, so
make's body dropped tounknown constructorunless the importer also named the type — module-own layouts (public, private, or out-of-filter) now register unconditionally, while importer visibility and the checker's E210/E320 rails are unchanged. vera compileprints the E602 "function skipped" warning on the text error path (#1004). When aCodegenSkipdrops a called function, the caller's danglingcall $ffails WAT assembly with an opaqueunknown func; the warning explaining why the function was dropped was suppressed on the text error path (the--jsonenvelope already carried it) and is now printed alongside the error. The type-error and--target wasi-p2family-gate text paths are corrected the same way, so a warning alongside those errors is no longer dropped.- The
@Int -> @Natnarrowing into anapply_fnclosure formal is now obligated and runtime-guarded (#1017). A provably-negative@Intargument narrowing into a@Natclosure formal viaapply_fnverified clean (a false Tier 1) and, for a runtime value, entered the formal reinterpreted with no trap. The verifier'sapply_fnbranch now emits the@Natnarrowing obligation (mirroring the generic call-argument path — E503 for a provable negative, a guarded Tier 3 otherwise) and code generation guards thecall_indirectargument — the narrowing dual of the #820 argument-widening handler at this site. - The refinement-predicate narrowing into an
apply_fnclosure formal is now obligated and runtime-guarded (#1024).apply_fn(f, 0)wheref's formal is{ @Nat | @Nat.0 > 0 }verified clean (a false Tier 1) and ran silently: a refinement over@Natis a@Nattype, so the #1017 handler claimed the site and discharged only the base's>= 0— which0satisfies — leaving the strict predicate unchecked with no runtime backstop. The verifier'sapply_fnbranch now discharges the full predicate refined-first (ahead of the #1017@Natarm, mirroring the generic call-argument path — E505 for a provable violation, a guarded Tier 3 otherwise), and code generation guards each refined closure formal at the lifted body's prologue — the closure-side dual of the refined-parameter guard named functions already carry. - A refined closure RETURN is now obligated and runtime-guarded (#1032, the return-side dual of #1024, found while fixing it).
fn(@Int -> @Pos) { @Int.0 }(withPos = { @Nat | @Nat.0 > 0 }) applied to-5— or to0, which clears the@Natbase's>= 0— returned the violating value through the refined slot on a verify-clean program: the verifier's closure-return handling covered the widening (#820) and bare-@Natnarrowing (#984) directions but no refinement, and the lifted closure emitted no return guard while the #984 gate excluded refinements assuming a boundary guard that did not exist. The verifier now records a refined closure return as a guarded Tier 3 (the closure body is opaque to the SMT layer — never a false Tier 1), and code generation checks the lifted body's return value against the full predicate, mirroring the named path's refined-return guard, so a violating value traps with the refinement message instead of leaking. The refinement obligation stream is also now honest about the boundaries codegen cannot guard: a refined base with a non-plain type argument (Array<{ @Int | ... }>-style) recordstier3_unguarded(E506 disclosure) at every parameter/return boundary — named or closure — instead of claiming a runtime guard that never fires (#1036 tracks emitting the guard itself). Map/Setentries andarray_concatovermap_valueshandle representation-transparentFuture<T>values (#1097). AMap<String, Future<Int>>value round-trip (map_insert/map_get/map_values) was check- and verify-green but compiled exit-0 to an INVALID module.Future<T>is representation-transparent (#841) — aFuture<Int>IS an i64, aFuture<String>an i32_pair — but the Map/Set host-tag classifier (_map_wasm_tag, shared by keys, values, and Set elements) did not strip the transparent wrapper, so aFuture<Int>value fell through to the"b"(single-i32) tag while the value expression pushed an i64: the registered host import disagreed with the stack ("type mismatch: expected i32, found i64", or "values remaining on stack" for the pair payload). The classifier now canonicalizes an alias name (type FI = Future<Int>;) and stripsFuture<…>wrappers to the payload before tagging — so aFuture<Unit>value still takes the zero-size loud [E602] skip and aFuture<Array<Int>>value the existing Array-reject skip, never an invalid module — and the same fix coversSet<Future<T>>elements andFuture-typed map keys. A second, latent site rode behind it:array_concat(map_values(m), map_values(m))over such a map ran a wrong-stride copy (silent garbage) — the two rebuilt arms of the array combinators' element-type inference (_infer_concat_elem_type) returned the bare head"Future"with the type argument dropped, so the copy loop strided the 8-byte i64 payloads at the 4-byte i32 default, while the let-bound@Array<Future<Int>>SlotRef form was already correct (#1057). Both arms now mirror the direct SlotRef arm through a shared helper, preserving the fullFuture<…>spelling.
Documentation
- Spec §11.8 now matches code generation: every non-trivial contract is compiled as a runtime check regardless of the verifier's tier (#958). The section previously claimed Tier-1-proved contracts are omitted from the compiled output; code generation is tier-agnostic (
vera/codegen/contracts.pyhas no notion of tiers), so a Tier-1 proof means the emitted check provably never fires, not that it is absent — the runtime guard is the deliberate backstop that keeps a false Tier 1 a loud trap rather than a silent wrong answer. scripts/check_pypi_readme_examples.pygates the PyPI project page's Vera blocks at parse + check + verify (pre-commit hookpypi-readme-examples+ a CI lint step): the Try-it program on PyPI is now held to the same standard as the other doc surfaces, and a broken contract in it fails the gate.- Spec §0.5.1 and SKILL.md now state that
?inside a type printed by a diagnostic marks a component the checker could not infer (Array<?>) — a rendering marker distinct from the typed-hole expression?(§4.17) written in source. - The concurrent-await alias-
Futurelimitation (spec §9.5.4) is now anchored to a tracking issue (#1095), with a matchingKNOWN_ISSUES.mdlimitation row: confirming it still reproduces requires a genuinely concurrent repro, since an eager-path probe never reaches the handle-check.