Skip to content

Rework generic Boxy descriptor and nominal backing resolution - #10777

Open
lukewilliamboswell wants to merge 2 commits into
roc-lang:mainfrom
lukewilliamboswell:split-boxy-descriptors
Open

Rework generic Boxy descriptor and nominal backing resolution#10777
lukewilliamboswell wants to merge 2 commits into
roc-lang:mainfrom
lukewilliamboswell:split-boxy-descriptors

Conversation

@lukewilliamboswell

Copy link
Copy Markdown
Collaborator

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 terminals on 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

  • 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 / erased_callable_result_desc), wired through the interpreter, dev, LLVM, and wasm backends and the Boxy runtime.
  • Plan representations for field-access segment success types; segment types are interned per access and appear nowhere else in the checked body, so lowerFieldAccessInto's prefix locals could not resolve them.
  • 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.

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.roc still panics under --specialize=no (conflicting specialization terminals); its registered test intentionally uses the specialized path. Worth a follow-up issue.

🤖 Generated with Claude Code

lukewilliamboswell and others added 2 commits August 14, 2026 12:27
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-apps

greptile-apps Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR reworks generic Boxy representation planning and lowering so descriptors retain the correct nominal, alias, aggregate, and callable provenance across construction and use sites.

  • Adds runtime erased-callable argument and result descriptor operations across the interpreter, dev, LLVM, and wasm backends.
  • Introduces lexical nominal-backing substitutions and explicit descriptor reservation and binding for aggregate fields.
  • Plans field-access segment representations and aligns specialization and worker-alias descriptor contexts.
  • Moves fixed-lifetime LLVM scratch allocations into procedure entry blocks and adds regression coverage for stored functions and host-boundary erased callables.

Confidence Score: 4/5

The 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

Important Files Changed

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
Loading

Reviews (1): Last reviewed commit: "Rework generic Boxy descriptor and nomin..." | Re-trigger Greptile

Comment on lines +16641 to +16663
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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant