Skip to content

BendTT: kinds as quantity terms, meets, Σ at any kind and subsumption under ∀; --verdict emits generic defs once - #1203

Merged
nicolas-abril merged 16 commits into
mainfrom
kernel-kind-terms
Oct 5, 2026
Merged

nicolas-abril merged 16 commits into
mainfrom
kernel-kind-terms

Conversation

@nicolas-abril

@nicolas-abril nicolas-abril commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

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. --verdict no longer monomorphizes on quantities: a Quant parameter, 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:

  • no quantity is computed by bend2's evaluator before the kernel sees it;
  • subsumption under ∀ is a proven kernel rule, not a bridge in safe.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, and gates/safe.ts tests gives 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 --verdict on main and passes here.

what it was why it goes away
#1175 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) the root is emitted once, with Kind(q1(f)) as a kernel term
#1179 spec_of's cycle guard under-approximated its fixpoint, so --verdict depended on declaration order ("a kind that depends on a run-time value") spec_of only marks ~ parameters, so there is no fixpoint. ab.bend and self.bend both check, in either order
#1180 spec_of lowered each domain at depth 0, so an inner binder marked a later live parameter as specialized. Its second file silently dropped h same; use checks, and h is now emitted and checked by the kernel
#1201 (PR, for #1175) split a root stuck on a constant at the constructors or Quant literals it reads nothing is stuck on a Quant constant any more. Both of its tests pass here: spec_root_split.bend checks, and so does spec_root_split_out.bend, whose ap_f and void_f it left out of scope; both are emitted and checked here
#1208 (PR, for #1179) made spec_of a least fixpoint the computation it fixes is gone; its test spec_cycle.bend checks here

Checked and not affected:

Every Bend snippet in the 82 open issues (92 files) was also run under --verdict on main and here. #1175 and #1180 now pass. #984's chain and #1205's file now fail, as described under Elaborator.

Kernel (bend2/bendtt.lean)

  • Typ carries a quantity term, as bend2's Kind(g) does: *(q) is the kind of a quantity q : <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: .Q2 is its identity and .Q0 absorbs, on either side, even next to a stuck quantity (Term.qmin). It may be live: Term.live and Term.uses read both sides, and a live meet steps to a label.

  • Σ at any kind *(g), with each field at kindof(q, *(g)), and *(g) shifted under the field binder. So Either<a, b> is Kind(a <&> b) goes out as written.

  • Fits is inductive, with these rules:

    • conversion;
    • kinds: *(g) fits *(h) when g is .Q2 wherever h is, under every substitution;
    • enums: fewer labels;
    • ∀q A B fits ∀q C D when C fits A and B fits D: the domain reversed and the codomain along, as in bend2's LE;
    • transitivity.

    The checker decides kinds with bend2's rules (qge): .Q2 is the top, .Q0/.Q1 the bottom, a meet on the left needs both sides, and a meet on the right needs either. Term.fits threads fuel, compares two ∀s' binder quantities first, and then fits them by their parts. qge_sound and fits_sound prove the checker against the rules.

  • Fits.at / fits_at read a fit at a type former, in both directions: where T converts to a former, U converts to one of the same former whose parts fit. fits_all, fits_conv, fits2 and fits_former follow from it. Subject reduction and progress use them:

    • β casts the argument along the domains' fit and the result along the codomains';
    • a split, a hit and a miss do the same;
    • a λ-match on an enum may receive a smaller enum.
  • Unchanged: binder quantities stay literal, so the live check still reads quantities off the syntax. A type of kind *(q) is Data only where q reduces to .Q2, so canon_data keeps its route.

  • lean bendtt.lean checks with no sorry under 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 against main at 1a2e5cd: 21,749 → 21,334, cap 24,000)

  • Kind(g) becomes *(g) with g's term, and a <&> b becomes (a <&> b), as bend2 writes them. A literal Kind(&0) and a generic Kind(q) at &0 are then one kernel term, *(.Q0): no quantity is raised to its rung.
  • No quantity is folded, closed or not: the kernel computes every one itself, so --verdict does not trust bend2's evaluator for them. (main folded closed meets, and its specialized quantities, by bend2's evaluator.) Two consequences, both from the open issues' snippets:
  • A datatype's kind is emitted as a term.
  • spec_of specializes template parameters only.
  • A self-call's argument goes out whole when its binder quantities match the column's (qsig_eq), since the kernel fits domains, codomains and kinds itself. For example, K0 : Nat -> Kind(&0) passed at a Nat -> Type column is no longer η-expanded, and a rewrite goes out as written.
  • uses counts 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 shares closed_val with spec_val, which no longer throws to say that a term is open.
  • The quantity-only out-of-scope guards are gone: a Quant bound inside a match, a Quant argument to a variable, a type over Quant.

Now in scope

Hand-written books, run through the built kernel, check what should be rejected:

  • a + copy of an Either of stuck kind, generic or Either .Q2 b, is rejected;
  • a function type in a Data Either is rejected;
  • a Data type passed at a stuck kind is accepted;
  • through ∀, these are rejected: K : <A> -> *1 at <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

  • New tests in tests/check/, each passing --verdict here:
    • kind_terms.bend (fails on main), with a law on main'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 on main): a self-call passing Kq(q) : Nat -> Kind(q) at a Nat -> Type column.
    • Regression guards that main passes and this branch once failed:
      • kind_none_is_type.bend: the same self-call with Kind(&0).
      • kind_generic_none.bend: a generic kind at &0 against a literal Kind(&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 passing g : Type -> Nat at a Kind(&0) -> Nat column.
      • kind_rewrite_goal.bend: a rewrite of type Nat -> Kind(&0) at a Nat -> Type goal.
      • meet_uses_in_arms.bend: a variable used inside a meet in one arm and again in another.
  • Each rule for quantity terms in bendtt.lean was compared with bend2's. These agree: the meet's reduction, conv against EQ, fits against LE (now under ∀ too), Kind/Min typing, kindof against 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.
  • Known, harmless difference: Quant is an enum in the kernel, so it is Data there and Type in bend2; the kernel accepts more.
  • Full local --verdict sweep over every file in tests/, on main, 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 as main. gates/repo.ts: PASS 50 / 50.
  • Cluster gates at 3548a98, rebased on 1a2e5cd:
    • 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, and main itself scored 49–89 / 124 back to back. This PR doesn't touch bend.ts, comp.ts or main.ts. ping was skipped: there is no site repo checkout here.
    • gates/safe.ts tests, on 26 nodes, side by side with main at 1a2e5cd: per file, identical to main's table plus the eight new tests/check files, all "agree". That is 1000 agree (992 on main), 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.
    • Not run: gates/safe.ts hub (no BendHub store here).
  • A fresh review of the final diff found no soundness issue. Its findings and what happened to each:
    • Fixed: the open-kind case of kind() (above), and the order of Term.fits's ∀ case.
    • Not reproduced: a law over Quant losing 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: see below.

Known limitations

  • The kernel's meet evaluates its right side even when the left is .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 a wnf case and its proofs, and the kernel has 15 ttok left under its cap.
  • A linear chain of quantity definitions makes kind checking exponential #984's chain 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.

elaborator kernel
all 994 files bend2 accepts, sum of per-file medians (3 runs) main 5,687 ms, this PR 5,974 ms (+5%) main 19,945 ms, this PR 20,196 ms (+1%)
300 generic defs with Kind(a <&> (b <&> (a <&> b))), used at &2, &1 (15 runs, load average ~55) main 76.9 ms, this PR 11.7 ms main 44.6 ms, this PR 12.6 ms

On 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.typ and paper/BendTT.pdf (rebuilt with typst 0.15.1, now 10 pages) describe the kernel as it now is:

  • kinds over quantity terms, *(g) and (g <&> h), with Q = <Q0, Q1, Q2>;
  • Σ at any kind, and the kind case of U <= T with the checker's rules for it;
  • the SORT and MEET rules;
  • the meet's evaluation step and its place in the live check and in the measure;
  • *(g) ≡ *(h) only when g ≡ h;
  • generic defs going out once;
  • the ∀ rule of U <= 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

@nicolas-abril
nicolas-abril marked this pull request as draft September 29, 2026 20:13
@nicolas-abril nicolas-abril changed the title BendTT: kinds of a quantity term, *(q) BendTT: kinds as quantity terms, meets and Σ at any kind; --verdict emits generic defs Sep 29, 2026
@nicolas-abril nicolas-abril reopened this Oct 5, 2026
@Lorenzobattistela

Copy link
Copy Markdown
Collaborator

Ran this locally (merged onto main a3b0dfe) and through a /code-review pass. The kernel side holds up: all 7 claims print only propext, Classical.choice, Quot.sound, the 8 new tests pass --verdict (kind_terms and kind_self_call_eta fail on main), #1201's spec_root_split{,_out} pass too, and gates/repo.ts is 50/50. One regression though.

Regression: a rewrite passed as a self-call argument now fails --verdict

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

  • rwt_type runs twice for a rewrite at an application head, and the Ann re-elaborates the evidence and motive after rwt_term already did, which compounds when rewrites nest. rwt_type also repeats rwt_term's lookup of the evidence type; better to share it so they can't drift.
  • Term.wnf on Min normalizes both sides even when the left is .Q0.
  • Dead code: datatypes are never specialized now, so adt_emit/tele_open keep unused cols paths and a stale comment. spec_val is a one-line wrapper with one caller.
  • bend2/docs/BendTT/main.typ still describes *1/*2 and one copy per quantity, so paper/BendTT.pdf no longer matches the kernel.

nicolas-abril and others added 16 commits October 5, 2026 15:17
…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>
@nicolas-abril nicolas-abril changed the title BendTT: kinds as quantity terms, meets and Σ at any kind; --verdict emits generic defs BendTT: kinds as quantity terms, meets, Σ at any kind and subsumption under ∀; --verdict emits generic defs once Oct 5, 2026
@nicolas-abril
nicolas-abril merged commit ffeddf0 into main Oct 5, 2026
1 check passed
@nicolas-abril
nicolas-abril deleted the kernel-kind-terms branch October 5, 2026 13:19
nicolas-abril added a commit to nicolas-abril/bend that referenced this pull request Oct 5, 2026
… 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment