Skip to content

Finish issue 10770: phantom nominal arguments and shared top-level constants - #10829

Draft
rtfeldman wants to merge 2 commits into
mainfrom
issue-10770/phantom-argument-and-shared-constant
Draft

Finish issue 10770: phantom nominal arguments and shared top-level constants#10829
rtfeldman wants to merge 2 commits into
mainfrom
issue-10770/phantom-argument-and-shared-constant

Conversation

@rtfeldman

Copy link
Copy Markdown
Contributor

Closes #10770. With this, the issue's own reproduction mounts: roc build --target=wasm32 --opt=size of repro/nightly-2026-08-13-generic-browser-signal/app.roc against roc-signals at c0e4a9c, mounted with scripts/browser/mount_wasm_example.mjs, prints mounted minimal, and the location-source example fixture passes its spec end to end.

Two remaining causes, both independent of #10814 and #10818.

Relating a call's request to the callee's checked signature descended into a nominal's backing and returned, never relating the nominal's own declaration arguments. Every argument the backing mentions is reached that way, so only phantom arguments were lost: a Capability(a) whose payload never mentions a left a unresolved in the callee's request, and an unresolved type variable finalizes as uninhabited, so the capability's closures specialized at an uninhabited type and lowered to a reachable runtime error. That is the hit a runtime error the issue reports, and it is why the failure tracked the payload type: Str, Bool, U64 and List(U64) worked while { path : Str }, (Str, Str) and [Visible, Hidden] did not. A declaration's arguments are components of the value the same way a record's fields are, so the traversal now relates them like every other constructor it walks.

Separately, a top-level binding is one value, so every reference must read the same allocation, and a binding whose value contains a callable got neither: the ConstStore's function values were ruled out of static data, so each reference materialized its own erased callable and a platform handed that binding twice received two pointers. Static data already emits an erased callable as one allocation naming its procedure through a relocation, so a capture-free function value is fully decided by the ConstStore and belongs there. Two conditions make that safe: a restoration reached while an enclosing ConstStore node is still being built reads that node's binding local, which a standalone static initializer procedure cannot name, so it no longer becomes static data; and a request that already names a committed type is sealed before it keys the static-data use, which is what lets two references to one constant share a single allocation. Without the first condition test/cli/issue_9889_roc_parser/Issue10303Sgf.roc fails ARC certification with an unbound local in a static initializer.

test/fx/phantom_capability_parameter.roc covers the first, driven through the io-spec matrix on every backend; it prints hit a runtime error without the change. It needs the fx platform's new store_seed!/take_seed! pair, whose result type is the only place its type variable appears, since nothing else lets a test build a phantom capability without constraining it from a call site. test/provided-callable-host gains a top-level binding read by both fields of a provided root's result for the second; those two fields arrive 0x40 apart without the change.

Relating a call's request to the callee's checked signature descended
into a nominal's backing and returned, never relating the nominal's own
declaration arguments. Every argument the backing mentions is reached
that way, so only phantom arguments were lost: a `Capability(a)` whose
payload never mentions `a` left `a` unresolved in the callee's request,
and an unresolved type variable finalizes as uninhabited, so the
capability's closures specialized at an uninhabited type and lowered to
a reachable runtime error. That is the "hit a runtime error" issue 10770
reports.

A declaration's arguments are components of the value the same way a
record's fields are, so the traversal now relates them like every other
constructor it walks.

The fx platform gains `store_seed!`/`take_seed!`, a pair whose result
type is the only place its type variable appears, which is what lets a
test build a phantom capability without constraining it from a call
site.
A top-level binding is one value, so every reference must read the same
allocation. A binding whose value contains a callable got neither: the
ConstStore's function values were ruled out of static data, so each
reference materialized its own erased callable, and a platform handed
that binding twice received two pointers.

Static data already emits an erased callable as one allocation naming
its procedure through a relocation, so a capture-free function value is
fully decided by the ConstStore and belongs there. Two conditions make
that safe. A restoration reached while an enclosing ConstStore node is
still being built reads that node's binding local, which a standalone
static initializer procedure cannot name, so it no longer becomes static
data. And a request that already names a committed type is sealed before
it keys the static-data use, which is what lets two references to one
constant share a single allocation instead of interning separately.
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.

New compiler: generic host-owned signal traps at runtime

1 participant