Rework generic Boxy descriptor and nominal backing resolution - #10777
Rework generic Boxy descriptor and nominal backing resolution#10777lukewilliamboswell wants to merge 2 commits into
Conversation
Two separately cloned but isomorphic recursive solved types, or a transparent alias and its backing, could hash to different erased callable digests, so equivalent source function types failed Lambda Solved unification when a boxed callback was erased through two paths. Digest traversal now resolves transparent aliases to their backing, re-roots after lazy leaf materialization links a node to an existing clone, and keys cycle references by traversal stack position instead of TypeVarId so clone-isomorphic graphs hash identically. Record field digests include the field default identity. The end-to-end fixture guards the digest-unification failure surfaced by the generic Boxy lowering work; the unit tests pin the alias-backing and clone-isomorphism properties directly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Validating real applications against the generic (--specialize=no) Boxy backend surfaced a family of descriptor identity bugs: runtime type descriptors for erased callables, stored callables, and aggregate fields were resolved from the wrong context when nominal backing, transparent aliases, or specialization branches separated a value's construction site from its use. - Resolve nominal backing substitutions through the declaring module and scope them to their lexical traversal paths, avoiding self-cycles. - Preserve stored callable representations and descriptor-wrapper substitutions through Boxy nominal backings. - Reserve descriptors for pending aggregate and tag payload fields from source provenance before construction, and track runtime descriptor writes so materialized slots are marked bound. - Read erased callable argument and result descriptors at runtime via two new low-level ops (erased_callable_arg_desc/result_desc), wired through the interpreter, dev, LLVM, and wasm backends and the Boxy runtime. - Plan representations for field-access segment success types; the segment types are interned per access and appear nowhere else in the checked body. - Prune static body specialization branches with identical contexts and align worker body alias descriptor contexts. - Keep fixed-lifetime LLVM allocations in procedure entry blocks when an erased call is emitted from a branch. Regression fixtures cover stored function fields (plain and generic), generic platform required-init consts, and boxed erased callables across the host boundary inside control flow. Depends on the erased-callable digest canonicalization commit; without it the plan trips 'conflicting specialization terminals' on digest-distinct but equivalent solved types. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Greptile SummaryThis PR reworks generic Boxy representation planning and lowering so descriptors retain the correct nominal, alias, aggregate, and callable provenance across construction and use sites.
Confidence Score: 4/5The PR appears safe to merge after replacing the non-blocking nested-callable fallback with an explicit context-keyed plan or invariant failure. The runtime and backend descriptor wiring is internally consistent in the reviewed paths, while the accepted concern is a compiler-planning discipline violation that can hide missing contextual planning rather than a demonstrated current blocking failure. Files Needing Attention: src/postcheck/boxy/lower.zig
|
| Filename | Overview |
|---|---|
| src/postcheck/boxy/plan.zig | Extensively reworks representation identity, nominal-backing substitutions, aggregate descriptor planning, field-access planning, and specialization contexts. |
| src/postcheck/boxy/lower.zig | Consumes the expanded plans and emits runtime descriptor operations, but introduces a prohibited fallback for missing context-keyed nested-callable plans. |
| src/eval/boxy_abi.zig | Adds registered erased-callable metadata lookups with keyed argument-offset and bounds validation. |
| src/eval/interpreter.zig | Implements interpreter equivalents of the new erased-callable descriptor reads using interpreter-owned callable context. |
| src/backend/llvm/MonoLlvmCodeGen.zig | Wires the new low-level operations and relocates fixed-lifetime scratch allocations to entry-block slots. |
| src/backend/dev/LirCodeGen.zig | Registers and emits calls to the two new Boxy runtime descriptor wrappers. |
| src/backend/wasm/WasmCodeGen.zig | Registers wasm signatures for the new erased-callable descriptor runtime symbols. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
A[Checked types and callable uses] --> B[Boxy representation plan]
B --> C[Nominal and alias descriptor substitutions]
B --> D[Aggregate field descriptor reservations]
B --> E[Erased callable descriptor keys]
C --> F[Boxy lowering to LIR]
D --> F
E --> F
F --> G[Interpreter]
F --> H[Dev backend]
F --> I[LLVM backend]
F --> J[Wasm backend]
G --> K[Boxy runtime descriptors]
H --> K
I --> K
J --> K
Reviews (1): Last reviewed commit: "Rework generic Boxy descriptor and nomin..." | Re-trigger Greptile
| var found: ?Plan.NestedCallableUsePlan = null; | ||
| for (self.parent.plan.nested_callable_uses.items) |candidate| { | ||
| if (candidate.worker != worker_id or | ||
| candidate.caller != self.worker_layout.worker or | ||
| !planExprRefEql(candidate.use, use_ref)) | ||
| { | ||
| continue; | ||
| } | ||
| if (found) |existing| { | ||
| const existing_desc_args = self.parent.plan.directCallHiddenDescriptorArgSlice(existing.hidden_desc_args); | ||
| const candidate_desc_args = self.parent.plan.directCallHiddenDescriptorArgSlice(candidate.hidden_desc_args); | ||
| const existing_dict_args = self.parent.plan.directCallHiddenDictionaryArgSlice(existing.hidden_dict_args); | ||
| const candidate_dict_args = self.parent.plan.directCallHiddenDictionaryArgSlice(candidate.hidden_dict_args); | ||
| if (!self.parent.plan.directCallHiddenDescriptorArgsEql(existing_desc_args, candidate_desc_args) or | ||
| !std.meta.eql(existing_dict_args, candidate_dict_args)) | ||
| { | ||
| boxyLowerInvariant("nested callable context had multiple incompatible fallback use plans"); | ||
| } | ||
| continue; | ||
| } | ||
| found = candidate; | ||
| } | ||
| return found; |
There was a problem hiding this comment.
Context-blind callable plan fallback
When the exact contextual lookup returns no plan, this scan drops contextual_type and reuses a plan selected only by expression, caller, and worker. This violates the compiler's explicit-planning requirement and can conceal a missing context-specific plan by silently coupling lowering to another specialization's compatible-looking arguments.
Context Used: AGENTS.md (source)
Split out of #10717. Stacked on #10776 — the first commit here is that PR's digest canonicalization; review the second commit. Without it, planning trips
one worker body representation had conflicting specialization terminalson digest-distinct but equivalent solved types.Problem
Validating real applications (RocRay) against the generic (
--specialize=no) Boxy backend surfaced a family of descriptor identity bugs: runtime type descriptors for erased callables, stored callables, and aggregate fields were resolved from the wrong context when nominal backing, transparent aliases, or specialization branches separated a value's construction site from its use.Fix
erased_callable_arg_desc/erased_callable_result_desc), wired through the interpreter, dev, LLVM, and wasm backends and the Boxy runtime.lowerFieldAccessInto's prefix locals could not resolve them.Tests
Regression fixtures cover stored function fields (plain and generic), generic platform required-init consts (red on main: panics with
checked body referenced a type missing from the boxy representation plan), and boxed erased callables across the host boundary inside control flow.Full MiniCI is green on this stack (75/75 phases).
Known limitation (pre-existing, out of scope)
erased_callable_alias_digest/app.rocstill panics under--specialize=no(conflicting specialization terminals); its registered test intentionally uses the specialized path. Worth a follow-up issue.🤖 Generated with Claude Code