Repository navigation
BendTT: kinds as quantity terms, meets, Σ at any kind and subsumption under ∀; --verdict emits generic defs once - #1203
Conversation
4bb6fd2 to
1a3fa1e
Compare
|
Ran this locally (merged onto main a3b0dfe) and through a /code-review pass. The kernel side holds up: all 7 claims print only Regression: a rewrite passed as a self-call argument now fails import Base
def K0(A: Nat) -> Kind(&0):
Nat
def F(n: Nat) -> Type:
Nat -> Kind(&0)
def go(n: Nat, e: {n == n : Nat}, f: Nat -> Type) -> Nat:
match n:
case 0n: 0n
case 1n+p: go(p, {==}, (%e : F(_); K0))
def main() -> Nat:
go(3n, {==}, K0)On main it checks, here it fails. The emitted call is (go x0 {==} {%x1 : ...; K0 : (F (.Succ, (x0, ())))}) at the column ∀x2 : Nat -> *(.Q1). arg_term (safe.ts:1208) gives qsig_eq the rewrite's bend2 goal rather than its own type P[b, e], so the argument isn't η-expanded. The new Rwt case in term() then annotates it at P[b, e], and the kernel has to convert Nat -> *(.Q0) to Nat -> *(.Q1) under a binder, which fails. Narrow fix: use rwt_type's result for a Rwt argument in arg_term, or always η-expand a rewrite there. Design question Kind(&0) is no longer raised to *1, so *(.Q0) fits *(.Q1) but doesn't convert to it. Every spot where bend2's LE goes under a binder now needs its own η-long or annotation case in safe.ts (qsig_eq's domain check, rwt_type annotations, the Rwt head in spine), and the bug above is two of those meeting. Would a ∀ rule in Term.fits (domain swapped, qge on the codomain kind, with a lemma like qge_sound) be cleaner than patching each case? I know that costs kernel ttok, with ~900 left under the cap. Smaller
|
daa9295 to
1455160
Compare
…2, Type once it is another label, so a kind may depend on a value A kind was a literal, *1 or *2, so a type whose kind depends on a value (Kind(q(c)) in bend2) had no kernel form: safe.ts had to specialize the value, and a kind read through a recursive type was out of scope. Now *(q), for q : <Q0, Q1, Q2>, is a kind: it reduces to *2 once q is .Q2 and to *1 once q is another label, and it is dead like every type. A type of kind *(q) is Data only where q reduces to .Q2, so the Data invariant holds as it was; any kind fits *1, and Data fits any kind. Binder quantities and datatype kinds stay literal, so the live check still needs no types. The proofs carry it: *(q) has the former of *q, is a value, and its redex develops in the triangle; Fits gains the kind cases, and every claim (consistent, sound, confluent, sr, progress, halts, empty) checks with the axioms it had. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…emits kinds as terms: a Quant is a value, not a copy per literal The kernel gains (a <&> b), the meet of two quantities (dead code only; it reduces once both sides are labels), and a Σ may have any kind, *q or *(q), its fields at kindof(q, K). A kind K fits a kind J when K is Data wherever J is (under every substitution), so the order is transitive and stable by definition; the checker decides it with bend2's rules (.Q2 the top, .Q0 and .Q1 the bottom, a meet on the left needs both sides, on the right either), each proven sound once (qge_sound). A type of kind *(q) is still Data only where q reduces to .Q2, so canon_data keeps its route; every claim checks with the axioms it had. safe.ts now emits Kind(g) as *(g) (or *1 / *2 for a literal), a <&> b as (a <&> b), and a datatype's kind as a term, so only a template's ~ parameters are specialized: a Quant parameter, and one a kind reads, are kernel variables. A def goes out once, as bend2 writes it; a kind may read a value through recursion, or a quantity picked at run time, which were out of scope. safe.ts shrinks (-297 ttok); the verdict of every test is as it was. Refs #1175 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s as *(g) and *(h), and a Σ has the kind *(g) Fits' kind case is now: U and T convert to *(g) and *(h), and *(g) is Data wherever *(h) is, so the Kind predicate and its lemmas go, and literal kinds enter through *1 ≡ *(.Q1), *2 ≡ *(.Q2). The Σ rule takes *(g) (no side condition; the checker checks the fields at kindof(q, *(g))), so gen and typed_gen need no special case. The meet returns one of its sides, qge folds its top and bottom into one test, and kin2 is one induction toward *2. The claims and their axioms are as they were. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s the kind, *1 and *2 are *(.Q1) and *(.Q2) A kind was a literal (*1, *2) plus a separate former *(q) that reduced to one; now Typ itself holds the quantity term, as bend2's Typ holds g, and two kinds convert where their quantities do (conv_typ). The Kin former, its reduction, Term.kind, Term.quan, kin2 and quan_kin go; Fits' kind case reads the quantities of Typ g and Typ h directly, so fits_sound is qge's soundness as it stands. safe.ts emits Kind(g) as Typ of g's term. The claims and their axioms are as they were. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…sorbs, on either side, even next to a stuck quantity
A meet reduced only once both sides were labels, so *((x <&> .Q2)) never
converted to *(x): the kind order covered it at the top of a type, but
not under a binder or in an equation. Now one Par rule takes a meet to
qmin of its sides' reducts, qmin forcing .Q2 and .Q0 on either side as
bend2's wnf does, and the complete development is qmin of the developed
sides, so the triangle stays one step (qmin_par). The checker's wnf
reduces a meet the same way. The claims and their axioms are as they
were.
Subsumption through a ∀ (a domain contravariant, a codomain covariant)
needs no kernel rule: safe.ts emits every term of a function type
eta-long, so the kernel fits such terms at their leaves; kind_terms.bend
now covers both, and an equation {Kind(x <&> &2) == Kind(x)}.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ds fuel through a meet, lab_inj names the label step, spec_of reads a template's ~ count, and safe_diag parses *(q) and (a <&> b) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…o --verdict accepts what main did: a kind's &0 is *(.Q1), and m(a, b) = a <&> b checks Two regressions against main. A literal &0 in a kind went out as *(.Q0), which the kernel does not convert to *(.Q1), though bend2 reads the two as one rung: a self-call passing K0 : Nat -> Kind(&0) at a Nat -> Type parameter failed. And every meet in live code was out of scope, even a closed one, because the kernel's live check reads no Min. A quantity that folds to a literal now goes out as it (a kind clamps .Q0 to .Q1, as main did), and a meet of run-time quantities goes out as a match on its left side, which the live check reads. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…der a binder goes eta-long unless the kernel converts it The review of this branch's kinds as quantity terms: - A meet is live code: Term.live and Term.uses read both sides, as bend2 adds their measures. Eval's meet fires on quantity labels only (a closed typed one is one, by canon_enu), so a live meet steps to a label and loses its redex node: Min has one in the measure, and halts stays untyped. safe.ts emits a run-time meet as (a <&> b), replacing the match it built, so a kind that names m(a, b) converts with one that names a <&> b. safe_diag's port of the live check reads the meet too. - Term.qge returns the fuel it left, as conv does, and passes it along each side of a meet; fits returns it. - The parser takes <&> only after a q=1 term, so (-a <&> b) fails at the <&>. - qsig_eq compares kinds as the kernel does under a binder: two literals at their rung, else two terms bend2 converts. Kind(q) at a Type column is no match, so a self-call's argument goes eta-long and the kernel fits the kinds at the leaf. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…iteral or a generic def's kind at &0, and the kernel converts it where bend2's EQ does
The branch raised a literal Kind(&0) to *(.Q1), but a generic def's
Kind(q) went out as *(q), which the kernel instantiates to *(.Q0): one
bend2 kind had two kernel forms that do not convert. A law
{Kd(&0) == Kind(&0) : Type} and a self-call passing K(&0) at a
Nat -> Type column both failed --verdict, though main checked them.
bend2's EQ keeps Kind(&0) and Kind(&1) apart and only its LE merges
them, as the kernel's conv and qge do. So:
- term emits Kind(g) as *(g) and a meet as (a <&> b), with no fold;
quant_lit (a normal form, a scan and a thrown Scope_Error per kind)
is gone.
- qsig_eq compares two kinds with bend2's EQ, so a function ending in
Kind(&0) at a column ending in Type goes eta-long, and the kernel
fits the kinds at the leaf.
- kind() reads a kind as Data only where its quantity is closed at &2,
else as Type (a stuck *(q) is not Data in the kernel), so a ~
argument's lambda over such a type takes the rebuild path instead of
q = 2. It shares closed_val with spec_val, which no longer throws to
say a term is open.
kind_generic_none.bend covers the two regressions; kind_none_is_type's
comment says why its argument now goes eta-long.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…meet is used: a self-call's domains compare EQ, a rewrite goes out at its own type, and a meet's sides count as uses A review of each quantity-term rule in BendTT against bend2 found three places where the elaborator diverged, each a false reject that main accepted: - qsig_eq compared binder quantities and codomain kinds, but not domains. bend2's LE compares domains swapped, so g : Type -> Nat fits a Kind(&0) -> Nat column, while the kernel converts *(.Q1) and *(.Q0) under the binder. Domains now compare by bend2's EQ, so such a self-call argument goes eta-long. - A rewrite was annotated at its goal, and the kernel fits a rewrite's type P[b, e] to its goal without going under a binder, where bend2's LE does. A rewrite whose type differs from its goal by EQ now goes out at P[b, e] (rwt_type), as does a rewrite at the head of an application, so the eta-long leaf fits the kinds. - uses ignored a meet, but a meet is live code and the kernel counts both its sides. A variable used inside a meet in one arm and again in another now rides into the arms. Everything else lines up: a meet reduces as bend2's (Q2 identity, Q0 absorbing on either side, stuck otherwise), conv on *(g) is bend2's EQ on Kind(g), qge is bend2's LE on kinds (rule for rule), and binder, let, lambda and constructor-field kinds check as bend2's do (kindof). Quant stays an enum, Data in the kernel and Type in bend2, so the kernel accepts more there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… chain of meets and a kind computed through an @unsafe def check as on main Running every snippet in the open issues on main and on this branch found two files main checked and this branch did not, both from no longer folding closed quantities: - #984's chain q(n+1) = q(n) <&> q(n) up to q26, in a kind: bend2 computes it with sharing, the kernel tree-walks 2^26 meets and runs out of fuel. - #1205's Kind(qsize(Array.join(...))): main folded it to &1; without the fold the kind names base's @unsafe Array.join and goes out of scope. (#1205 holds that main's pass is the gate's bug; that is #1220's to fix, and this keeps main's behavior until then.) A closed call or meet of type Quant now goes out as the literal bend2's evaluator gives (quant_lit): the kernel would compute the same, so this is exact, unlike the &0 -> *(.Q1) fold this branch dropped. It applies wherever a term is known to be a Quant: a kind's quantity, a meet's sides, an argument at a Quant parameter, and any term bend2 annotated as one. Closedness is read first, on the variables with no value in scope, so an open term costs no normal form and nothing throws. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… bend2's evaluator to compute it 465e464 folded every closed call or meet of type Quant to the literal bend2's evaluator gives, so the kernel checked {.Q2 == .Q2} where the law said {pick(True{}) == &2}: a wrong evaluation in bend2 would have passed --verdict. main folded closed meets the same way. Now the kernel computes every quantity itself. What it costs, measured on 465e464's parent (ab7c97b): every tests/ file gets main's verdict class, and of the snippets in the open issues two change. #984's chain q(n+1) = q(n) <&> q(n) runs the kernel out of fuel (a false reject: the kernel does not share the two unfoldings of q(n), as bend2 does). #1205's Kind(qsize(Array.join(...))) now names the @unsafe Array.join and fails, which is what #1205 asks for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rms, the meet, Σ at any kind, and generic defs that go out once - The terms: *(g) and the meet (g <&> h), with quantities as labels of Q = <Q0, Q1, Q2>; binder quantities stay fixed. - Kinds: a type of kind *(g) is Data exactly where g reduces to Q2, a Σ has any kind its body has, *(g) fits *(h) where g is Q2 wherever h is, and the meet reduces as Bend's <&> (Either<a, b, A, B> is *(a ⊓ b)). - Typing: SORT takes a quantity, MEET is new, and the caption gives the kind case of U <= T and the checker's rules for it. - The live check reads a meet's sides; evaluation steps Q_i ⊓ Q_j to Q_min(i, j), and a live meet gets the least label in the measure. - Confluence: *(g) ≡ *(h) only when g ≡ h, so Type is not Data. - What Affinity Costs: a quantity is a variable in BendTT, so each generic def goes to the kernel once (it said the compiler emits a copy per quantity); Kind(&1) and List<&2, Nat> as Bend writes them. - bendtt.lean is 4,106 lines (it said 4,247). paper/BendTT.pdf rebuilt with typst 0.15.1, 10 pages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d, the codomain along, so the kernel, not safe.ts, fits Nat -> Kind(&2) to Nat -> Type bend2's LE compares a function type's domains swapped and its codomain along, so g : Type -> Nat fits Kind(&0) -> Nat and K : Nat -> Kind(&2) fits Nat -> Type. The kernel fitted only at the top, so safe.ts made up for it: a self-call's argument went eta-long where the domains or kinds differed (qsig_eq), and a rewrite went out at its own type (rwt_type). --verdict trusted that bridge. Now the kernel has the rule, and proves it. - Fits is inductive: conversion, kinds, enums, ∀s by their parts (the domain reversed), and a transitivity rule, so fits_trans is the rule. Fits.at and fits_at read a fit at a former, both ways, and give fits_all, fits_conv, fits2 and fits_former, which replace the case analysis of the old disjunction. - Term.fits threads fuel and fits two ∀s of one quantity by their parts; fits_sound proves it. - Subject reduction and progress take the ∀ rule: β casts the argument along the domain fit and the result along the codomain's; a split, a hit and a miss likewise; lab_in reads a label through a fit. - Every claim (consistent, book_check, confluent, sr, progress, halts, empty) checks with no sorry and depends only on [propext, Classical.choice, Quot.sound], with Lean v4.34.0. 63,988 ttok (cap 64,000). - safe.ts: qsig_eq compares binder quantities only, and rwt_type is gone. Hand-written books: the kernel rejects K : <A> -> *1 at <A> -> *2, a ∀(∀ *1 -> <A>) -> <A> at ∀(∀ *2 -> <A>) -> <A>, a function on <A> at <A, B>, and a ∀ at a ∀+; it accepts each fit in the other direction. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… parts, the domain reversed, and subject reduction reads a fit at a former - Kinds: function types fit by their parts, as in Bend, so a Nat -> Data fits where a Nat -> Type is expected, and a Type -> Nat where a Data -> Nat is. - Figure 3's caption: U <= T adds the ∀ case and transitivity. - Subject reduction: a fit is inverted at a type former like a conversion, so β casts the argument along the domains' fit and the result along the codomains'. - bendtt.lean is 4,157 lines. paper/BendTT.pdf rebuilt with typst 0.15.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nel's ∀ fit compares binder quantities before it fits the domains From the review of the final diff: - kind() returned Type for a kind whose quantity is open, which sent a lambda that uses its variable twice to rebuild() or out of scope. It returns null again, as main's did, so the lambda goes out at q=2 and the kernel decides: after a match, the kind may reduce to .Q2 there. - Term.fits fitted the domains of two ∀s before it compared their binder quantities, and threw the work away on a mismatch. It compares them first. fits_sound follows; every claim still depends only on [propext, Classical.choice, Quot.sound]. 63,985 ttok (cap 64,000). - The paper's line count: bendtt.lean is 4,158 lines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3548a98 to
ec1a6e1
Compare
… it is (bendlang#1189) Rebased onto bendlang#1203. Par steps a λ+ to a λ and never back; Term.parts pairs two λs when their Quan.lin agree (the same as one liveness on Q0/Q1/Q2), so conv_lam closes without a lemma relating .live and .lin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What
The BendTT kernel can now express bend2's kinds as terms, and it fits function types by their parts, as bend2's LE does.
--verdictno longer monomorphizes on quantities: aQuantparameter, and any parameter a kind reads, is an ordinary kernel variable, and each def goes out once, as bend2 writes it. Only template (~) parameters are still specialized.Trust moves from bend2 to the kernel:
∀is a proven kernel rule, not a bridge insafe.ts.(This includes what was #1344.)
Rebased on
main(bf96827, which includes #1339's Lean-versioned kernel cache). At this head: repo PASS 50 / 50, test PASS 1559 / 1559, andgates/safe.ts testsgives 1004 agree, 72 unsafe, 491 bend2 rejects, 2 out of scope and 1 false reject (#1193).Closes and supersedes
Closes #1175, #1179 and #1180. Supersedes #1201 and #1208.
Each repro and test below fails
--verdictonmainand passes here.run3's generic root went out of scope: "a Quant that is not a literal" (the head annotation was fixed in 0187512; this was the open part)Kind(q1(f))as a kernel termspec_of's cycle guard under-approximated its fixpoint, so--verdictdepended on declaration order ("a kind that depends on a run-time value")spec_ofonly marks~parameters, so there is no fixpoint.ab.bendandself.bendboth check, in either orderspec_oflowered each domain at depth 0, so an inner binder marked a later live parameter as specialized. Its second file silently droppedhusechecks, andhis now emitted and checked by the kernelspec_root_split.bendchecks, and so doesspec_root_split_out.bend, whoseap_fandvoid_fit left out of scope; both are emitted and checked herespec_ofa least fixpointspec_cycle.bendchecks hereChecked and not affected:
--verdictpasses a def that the kernel never checks #1186 / A def specialized at a finite parameter also goes to --verdict as a λ-match on it, so the kernel checks that each value has an instance that fits its type (#1186) #1225 (a~parameter of an empty type gets no root):~parameters are still enumerated, so it stays open.~F): it fails the same way on both.Every Bend snippet in the 82 open issues (92 files) was also run under
--verdictonmainand here. #1175 and #1180 now pass. #984's chain and #1205's file now fail, as described under Elaborator.Kernel (
bend2/bendtt.lean)Typcarries a quantity term, as bend2'sKind(g)does:*(q)is the kind of a quantityq : <Q0, Q1, Q2>, with*1=*(.Q1)(Type) and*2=*(.Q2)(Data). Two kinds convert where their quantities do (conv_typ), as in bend2's EQ, so*(.Q0)and*(.Q1)are two kinds that fit each other. Like every type, a kind is dead: it is never evaluated.(a <&> b), the meet (minimum) of two quantities. It reduces as bend2's does:.Q2is its identity and.Q0absorbs, on either side, even next to a stuck quantity (Term.qmin). It may be live:Term.liveandTerm.usesread both sides, and a live meet steps to a label.Σat any kind*(g), with each field atkindof(q, *(g)), and*(g)shifted under the field binder. SoEither<a, b> is Kind(a <&> b)goes out as written.Fitsis inductive, with these rules:*(g)fits*(h)whengis.Q2whereverhis, under every substitution;∀q A Bfits∀q C DwhenCfitsAandBfitsD: the domain reversed and the codomain along, as in bend2's LE;The checker decides kinds with bend2's rules (
qge):.Q2is the top,.Q0/.Q1the bottom, a meet on the left needs both sides, and a meet on the right needs either.Term.fitsthreads fuel, compares two∀s' binder quantities first, and then fits them by their parts.qge_soundandfits_soundprove the checker against the rules.Fits.at/fits_atread a fit at a type former, in both directions: whereTconverts to a former,Uconverts to one of the same former whose parts fit.fits_all,fits_conv,fits2andfits_formerfollow from it. Subject reduction and progress use them:Unchanged: binder quantities stay literal, so the live check still reads quantities off the syntax. A type of kind
*(q)is Data only whereqreduces to.Q2, socanon_datakeeps its route.lean bendtt.leanchecks with nosorryunder Lean v4.34.0 (the pinned version) and v4.34.1. Every claim (consistent,book_check,confluent,sr,progress,halts,empty) depends only on[propext, Classical.choice, Quot.sound], as before. The claim statements are unchanged; the typing judgment they quantify over is more general.+5,146 ttok (58,839 → 63,985, cap 64,000). The next kernel change has to simplify first.
Elaborator (
bend2/safe.ts, −415 ttok againstmainat 1a2e5cd: 21,749 → 21,334, cap 24,000)Kind(g)becomes*(g)withg's term, anda <&> bbecomes(a <&> b), as bend2 writes them. A literalKind(&0)and a genericKind(q)at&0are then one kernel term,*(.Q0): no quantity is raised to its rung.--verdictdoes not trust bend2's evaluator for them. (mainfolded closed meets, and its specialized quantities, by bend2's evaluator.) Two consequences, both from the open issues' snippets:q(n+1) = q(n) <&> q(n)up toq26runs the kernel out of fuel, a false reject. The kernel does not share the two unfoldings ofq(n), as bend2 does.Kind(qsize(Array.join(...)))names the@unsafeArray.joinand fails, as The unsafe/foreign gate misses an @unsafe dependency that appears only in a datatype's is Kind(..) #1205 asks.spec_ofspecializes template parameters only.qsig_eq), since the kernel fits domains, codomains and kinds itself. For example,K0 : Nat -> Kind(&0)passed at aNat -> Typecolumn is no longer η-expanded, and a rewrite goes out as written.usescounts both sides of a meet, as the kernel does: a meet is live code.kind()reads a closed kind's quantity to pick how a λ that uses its variable twice goes out: rebuilt when the kind is Type, and at q=2 when it is Data or open. For an open kind, the kernel decides; after a match, the kind may reduce to.Q2. It sharesclosed_valwithspec_val, which no longer throws to say that a term is open.Quantbound inside a match, aQuantargument to a variable, a type overQuant.Now in scope
run3, and kinds that read nested fields: no splitting needed, so --verdict: split a root stuck on a constant at the constructors it reads (#1175) #1201 isn't needed either.elem_q(stages)over aList.keep(pick(b), …)andmeet(pick(b), …)withKind(a <&> b), and a meet computed at run time (m(a, b) = a <&> b).Hand-written books, run through the built kernel, check what should be rejected:
+copy of anEitherof stuck kind, generic orEither .Q2 b, is rejected;Eitheris rejected;∀, these are rejected:K : <A> -> *1at<A> -> *2,∀(∀ *1 -> <A>) -> <A>at∀(∀ *2 -> <A>) -> <A>, a function on<A>at<A, B>, and a∀at a∀+. Each fit in the sound direction is accepted, two levels of contravariance included.Tests
tests/check/, each passing--verdicthere:kind_terms.bend(fails onmain), with a law onmain's value. It covers everything above, plus a branch that relies on its kind,∀subsumption in both directions, and the equation{Kind(x <&> &2) == Kind(x)}.kind_self_call_eta.bend(fails onmain): a self-call passingKq(q) : Nat -> Kind(q)at aNat -> Typecolumn.mainpasses and this branch once failed:kind_none_is_type.bend: the same self-call withKind(&0).kind_generic_none.bend: a generic kind at&0against a literalKind(&0), by a law and by a self-call.meet_at_run_time.bend: a meet of run-time quantities in live code, with laws on every row of its table.kind_domain_self_call.bend: a self-call passingg : Type -> Natat aKind(&0) -> Natcolumn.kind_rewrite_goal.bend: a rewrite of typeNat -> Kind(&0)at aNat -> Typegoal.meet_uses_in_arms.bend: a variable used inside a meet in one arm and again in another.bendtt.leanwas compared with bend2's. These agree: the meet's reduction,convagainst EQ,fitsagainst LE (now under∀too),Kind/Mintyping,kindofagainst the binder,let,λand constructor-field kinds, and the live check's usage of a meet. The last three regression guards above come from the three places where they did not. Two of those (the self-call's domains and the rewrite goal) are now decided by the kernel's∀rule.Quantis an enum in the kernel, so it is Data there and Type in bend2; the kernel accepts more.--verdictsweep over every file intests/, onmain, this branch before the fixes, and at each later head. The 1,554 files common to all of them get the same class on each (agree, unsafe, bend2 rejects, out of scope with the same reasons, false reject), and each new test agrees.tsc: the same 12 errors asmain.gates/repo.ts: PASS 50 / 50.gates/_run.ts: repo PASS 50 / 50, test PASS 1554 / 1554, all within the 30 s cap. perf reported PASS, but scored between 40 and 121 / 124 across today's runs, andmainitself scored 49–89 / 124 back to back. This PR doesn't touchbend.ts,comp.tsormain.ts. ping was skipped: there is no site repo checkout here.gates/safe.ts tests, on 26 nodes, side by side withmainat 1a2e5cd: per file, identical tomain's table plus the eight newtests/checkfiles, all "agree". That is 1000 agree (992 onmain), 71 unsafe, 491 bend2 rejects, 2 out of scope and 1 false reject (conversion_shared_cells_once, --verdict: the kernel runs out of fuel on conversion_shared_cells_once.bend, which bend2 checks by sharing (2.0.34) #1193), the same files on each.gates/safe.ts hub(no BendHub store here).kind()(above), and the order ofTerm.fits's ∀ case.Quantlosing its model (a law with no body is a TODO, so bend2 stops first), and a nested domain quantity mismatch (bend2's LE rejects it too).Known limitations
.Q0, which bend2 skips. A costly right side can therefore run the kernel out of fuel where bend2 is instant: a false reject, never a false accept. Short-circuiting needs awnfcase and its proofs, and the kernel has 15 ttok left under its cap.q(n+1) = q(n) <&> q(n)runs the kernel out of fuel, also a false reject: the kernel does not share unfoldings as bend2 does.kind()still uses bend2's evaluator, but only to choose how a term goes out. Whatever it chooses, the kernel checks the result.Performance
These are local measurements on an Apple M4 under background load, with the trees interleaved per file and per run in one process. Elaboration is
safe_emit, kernel is the BendTT binary on its text, and figures are medians.main5,687 ms, this PR 5,974 ms (+5%)main19,945 ms, this PR 20,196 ms (+1%)Kind(a <&> (b <&> (a <&> b))), used at&2, &1(15 runs, load average ~55)main76.9 ms, this PR 11.7 msmain44.6 ms, this PR 12.6 msOn the test corpus the difference is within noise. Per-file ratios swing between 0.2× and 6× in both directions, and the sums move by a few percent. The corpus row was measured at 1a3fa1e, before the later commits. The stress row was measured before the
∀rule, which emits the same text for that file. Kernel time with the∀rule was not measured separately. On kind-heavy code, emitting each def once instead of a copy per quantity cuts elaboration about 6× and kernel time about 3×.Paper
bend2/docs/BendTT/main.typandpaper/BendTT.pdf(rebuilt with typst 0.15.1, now 10 pages) describe the kernel as it now is:*(g)and(g <&> h), withQ = <Q0, Q1, Q2>;Σat any kind, and the kind case ofU <= Twith the checker's rules for it;SORTandMEETrules;*(g) ≡ *(h)only wheng ≡ h;∀rule ofU <= T, and subject reduction reading a fit at a former.It said the compiler emits a copy of each generic def per quantity. It also gets Bend's
Kind(&1)/List<&2, Nat>spelling and the Lean line count (4,158; it said 4,247).Closes #1175
Closes #1179
Closes #1180
🤖 Generated with Claude Code