Repository navigation
ty_holds opens a datatype once with DUMMY for its value arguments (#1513) - #1542
Lorenzobattistela wants to merge 8 commits into
Conversation
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>
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>
…ooked into apart (#1513)
nicolas-abril
left a comment
There was a problem hiding this comment.
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".
| // 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 |
There was a problem hiding this comment.
incomprehensible comment
| 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; |
There was a problem hiding this comment.
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.
| return false; | ||
| } | ||
| seen.add(t.k); | ||
| const key = rep ? t.x.map((x, i) => vs[i] |
There was a problem hiding this comment.
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.
| 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)); |
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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.
) 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>
…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.
Fixes #1513. Supersedes #1522 (its test is kept as
gpu_reach_closure_instances).The bug
ty_holdsopens each datatype once per walk, keyed by name, using the arguments it first meets. ForDuo{plain: Box<Num{}>, callable: Box<Fun{}>},Box<Num{}>gets opened (its field isU32), andBox<Fun{}>is skipped. Soty_closays "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
DUMMYin 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 areU32,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
repflag and no depth cap. Which parameters are values is now memoized per datatype (FL.vals), sovsisn't recomputed on every visit.DUMMY is used only when the predicate itself takes an unknown type as a hit (
p(DUMMY)): that'sty_cloandty_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)}withPickgiving 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)
chain,instancesandmutualfail on main and pass here.deep,grow,nestedandwrapguard against the hangs and stack overflows the earlier versions had.family_cyclic_nestedguardslay_of.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.--gateruns gave 111/124 and 123/124, which look like noise.Not run: CUDA, Linux, Windows (GPU checked on Metal only).
Please squash-merge.