Skip to content

ty_holds opens a datatype once with DUMMY for its value arguments (#1513) - #1542

Open
Lorenzobattistela wants to merge 8 commits into
mainfrom
fix-1513-ty-holds
Open

Lorenzobattistela wants to merge 8 commits into
mainfrom
fix-1513-ty-holds

Conversation

@Lorenzobattistela

@Lorenzobattistela Lorenzobattistela commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #1513. Supersedes #1522 (its test is kept as gpu_reach_closure_instances).

The bug

ty_holds opens each datatype once per walk, keyed by name, using the arguments it first meets. For Duo{plain: Box<Num{}>, callable: Box<Fun{}>}, Box<Num{}> gets opened (its field is U32), and Box<Fun{}> is skipped. So ty_clo says "no closure", and the GPU build fails with "a function the device does not hold".

The fix

This follows @nicolas-abril's suggestion. A datatype is still opened once per walk, but with DUMMY in place of its value arguments, so what its fields say holds for every index it can be met with:

  • Box<-t>{value: Pick(t)}: Pick(DUMMY) is stuck, so it may hold a closure (true).
  • Expr<-t>, Vec<A, -n>, Fin, typed ASTs: the fields are U32, A, or the family again, so they hold no closure (false, same as main).

Type arguments are still looked into on every visit, as on main. There's no key, no rep flag and no depth cap. Which parameters are values is now memoized per datatype (FL.vals), so vs isn't recomputed on every visit.

DUMMY is used only when the predicate itself takes an unknown type as a hit (p(DUMMY)): that's ty_clo and ty_value. lay_of's re-entry check reads a stuck field as "doesn't re-enter", so with DUMMY it laid a cyclic family flat and overflowed the stack (family_cyclic_nested); it keeps main's first-met arguments.

Trade-off: a family whose computed field is closure-free in every branch (Box<-t>{value: Pick(t)} with Pick giving U32 or F32) now counts as may-hold, so its host closures are kept out of #if !DEVICE. Build size only, never a wrong answer; none of the tests or demos hit it.

comp.ts: +11/-5 lines, 63,511 tokens (main 63,391).

Checks (merged with main f846cb5)

  • repo 54/54, test 1622/1622
  • chain, instances and mutual fail on main and pass here. deep, grow, nested and wrap guard against the hangs and stack overflows the earlier versions had. family_cyclic_nested guards lay_of.
  • The C output of all 766 test programs and 18 demos is byte-identical to main, except for the 3 ty_holds skips a closure in a later instance of the same datatype #1513 tests.
  • Nico's Expr<-t: Ty> / ev!(e: Expr<TBool{}>) example: the C output is identical to main, and the host closures stay under #if !DEVICE. The previous head (0f6b929) moved them out.
  • Gr<A, -n>{More{next: Gr<P<A, A>, (1n + n : Nat)>}}: compiles in 0.07 s, same as main.
  • perf: 124/124 on a full run; two --gate runs gave 111/124 and 123/124, which look like noise.

Not run: CUDA, Linux, Windows (GPU checked on Metal only).

Please squash-merge.

ty_holds marked a datatype seen by name, so a later instance of the same
family (Chain<Num{}> after Chain<Fun{}>, or mutually nested A/B) was never
looked into, and a closure it held never reached the device. It now keys
seen by the instance, and stops 1000 datatypes deep.

Fixes #1513. Supersedes #1522.

Co-authored-by: oxura <grozafroza44@gmail.com>
@Lorenzobattistela Lorenzobattistela self-assigned this Oct 13, 2026
Lorenzobattistela and others added 4 commits October 13, 2026 09:14
Type arguments are walked on their own, so keying them too let a nested
datatype (Grow<P<A, A>>) open 2^d instances. Tests: a nested datatype
compiles at once; a chain with a growing index stops at the cap.
…s may hold a closure (#1513)

Back to main's walk: each datatype is opened once, keyed by name. When
ty_clo meets it again with different arguments it answers yes, so the
GPU reach set takes the closures in. No per-instance walk, no depth cap.

Co-authored-by: oxura <oxura@users.noreply.github.com>
@Lorenzobattistela Lorenzobattistela changed the title ty_holds opens each datatype instance once (#1513) ty_clo takes a datatype met again with other value arguments to hold a closure (#1513) Oct 13, 2026

@nicolas-abril nicolas-abril left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This fixes #1513, but the repeat check also says "may hold a closure" for common value-indexed families that hold none, which turns on wide and pulls every host closure into the device build. Nothing breaks, but it costs size and build time. Details inline.

One more point, outside the diff: ty_value (line 904) and lay_of's re-entry check (line 951) still call ty_holds with the name-keyed skip that caused #1513. In a kind like Duo{a: K<X>, b: K<Y>} where only K<Y> computes to Type, the second K is skipped, ty_value says "value", and that argument gets keyed by syntax instead of walked as a type argument. That breaks the assumption the new comment rests on, that type arguments are "looked into apart".

Comment thread bend2/comp.ts Outdated
// but for the words.
function ty_holds(A: HTerm | null,
p: (t: HTerm | null) => boolean, seen = new Set<Name>()): boolean {
// but for the words. Each datatype is opened once; met again with other

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

incomprehensible comment

Comment thread bend2/comp.ts Outdated
const key = rep ? t.x.map((x, i) => vs[i]
? Bend.term_key(Bend.term_lower(x)) : "").join("\n") : "";
if (seen.has(t.k)) {
return rep && seen.get(t.k) !== key;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This answers true for any datatype whose recursion changes its value index, even when it holds no closure.

Example: type Expr<-t: Ty> is Data: Num{n: U32}; IsZero{a: Expr<TNum{}>} and ev!(e: Expr<TBool{}>). The walk meets Expr again with TNum instead of TBool, so ty_clo says true and dev takes in all of FL.clos. Compiling this moves main_c7, main_k8 and IO.print out of #if !DEVICE; on the merge base they stay host-only. The same happens for the typed Expr in demos/proof_typed_eval, the V.Con/Word.Con length-indexed vectors, Fin/Vec, and typed-AST shapes like App{f: Tm<Arr(a,b)>, x: Tm<a>}.

Suggestion: instead of a blanket true on a repeat, ask whether the datatype can hold a closure for any value of its value arguments. Open each datatype once with its value arguments replaced by DUMMY, memoized by name. It is about as cheap and still sound: for Box<-t>, Pick(DUMMY) is stuck and p returns true; for Expr<-t> or Vec<A,-n> the fields are U32/A/Expr<..> and the answer is no. That also removes the syntax-key problem below, without the per-instance blowup the earlier attempts hit.

Comment thread bend2/comp.ts Outdated
return false;
}
seen.add(t.k);
const key = rep ? t.x.map((x, i) => vs[i]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The key is the raw syntax of each value argument (term_key(term_lower(x)), not normalized, Ann nodes kept), so equal indices written differently count as different. E.g. type Ch<-t: Tag> Link{next: Ch<(t : Tag)>} or next: Ch<Id(t)>, or a literal against its constructor-tree form after substitution, all give a spurious "may hold a closure". Plain recursion only stays "no" when the field writes the index exactly the same way.

Also a perf nit: with rep set, this runs term_lower + JSON.stringify on every datatype visit, even when there are no value arguments (the key is just empty strings joined). Computing it only when vs.some(Boolean) avoids that.

Comment thread bend2/comp.ts Outdated
const ks = tld?.$ === "ADT" ? tele_unbind(tld.T).doms : [];
if (t.x.some((x, i) => !ty_value(ks[i]?.[2] ?? null)
&& ty_holds(x, p, seen))) {
const vs = t.x.map((x, i) => ty_value(ks[i]?.[2] ?? null));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

vs now runs ty_value (a full, unmemoized ty_holds walk of the kind) on every argument at every visit, including repeat visits that return right away. The old .some stopped at the first argument that held a closure. The kinds depend only on the declaration (ks from tele_unbind is already memoized), so memoizing ty_value per datatype and parameter index, or computing vs lazily, would remove this repeated work from every ty_clo, ty_value and lay_of walk.

Comment thread bend2/comp.ts Outdated
function ty_clo(A: HTerm | null): boolean {
return ty_holds(A, (t) =>
!["ADT", "Typ", "Qua", "Min", "Eql"].includes(t?.$ ?? ""));
!["ADT", "Typ", "Qua", "Min", "Eql"].includes(t?.$ ?? ""), true);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: the bare positional true (rep) is hard to read here without opening ty_holds, and any future caller that passes seen must also pass rep. A named options object, or a separate on-repeat callback, would keep the call site self-explanatory.

Lorenzobattistela and others added 2 commits October 13, 2026 11:46
)

Its fields then cover every index it is met with, so a later instance
with another index needs no second walk, and plain index recursion
(Expr<-t>, Vec<A,-n>) still holds no closure. The value/type split of
the parameters is memoized per datatype (FL.vals).

Co-authored-by: oxura <oxura@users.noreply.github.com>
@Lorenzobattistela Lorenzobattistela changed the title ty_clo takes a datatype met again with other value arguments to hold a closure (#1513) ty_holds opens a datatype once with DUMMY for its value arguments (#1513) Oct 13, 2026
…1513)

lay_of's re-entry walk seeks one datatype, so a field stuck on DUMMY
read as no re-entry: Nest<n> hiding Nest<1n+n> behind a family in a
nested datatype was laid flat and lay_of overflowed the stack.
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.

ty_holds skips a closure in a later instance of the same datatype

2 participants