Skip to content

feat(typecheck): QTT quantity semiring — strands are linear in weave - #103

Merged
hyperpolymath merged 1 commit into
mainfrom
feat/qtt-linear-strands
Jul 29, 2026
Merged

feat(typecheck): QTT quantity semiring — strands are linear in weave#103
hyperpolymath merged 1 commit into
mainfrom
feat/qtt-linear-strands

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

The gap

weave did no usage checking at all. It built the strand context, checked the body, computed Tangle[A,B] — and discarded it, returning gamma unchanged. Three unsound programs were accepted in silence:

weave strands a, b into (a > a) yield strands a, b     # a crossed with itself
weave strands a, b into (a > b) yield strands a, b, a  # a strand duplicated
weave strands a, b into (a > b) yield strands a        # a strand vanished

The spec already forbade the first (i ≠ j on [T-Cross-Over]) and the third ("yield declarations match B"). Both side conditions were written down and never enforced.

Why a semiring, not "make the language linear"

The requirement is genuinely mixed, so a single discipline is wrong in both directions:

quantity why
braid words ω x . x is σ₁² — a legitimate braid. Blanket linearity would reject valid programs.
strands 1 A strand is a physical thread. A braid on n strands is a permutation of them, so strand count is a conservation law.
the claim in Epi[κ,ρ,τ] 0 A-TG-11.1's recorded gap.

Linear, not affine — and the vanishing-strand case is what decides it. Affine permits discarding; a strand cannot vanish. That is the distinguishing test, and test_quantity.ml asserts it by name.

compiler/lib/quantity.ml gives {0, 1, ω} with add/mul/permits. Independent uses combine with semiring addition, so two uses of one strand give 1 + 1 = ω, which is not permitted where 1 was declared.

Applied in BOTH weave forms

Wiring the check into the weave statement rule alone left def x = weave … — the idiomatic spelling — entirely unchecked, because it reaches the weave expression rule and never touches the statement rule. The new conformance tier caught this on its first run, which is the argument for the tier existing.

Gating

New conformance/ill-typed/ tier carrying a double assertion: the file must parse, and must then fail to typecheck.

Asserting only "the compiler rejects it" is precisely the failure this suite already had once — three invalid/ cases scored points because the runner command was wrong and failed on every input. Requiring the parse first proves the rejection came from the typechecker and not from a typo in the fixture.

Wired into scripts/check-corpus.sh (what CI actually runs) as well as run_conformance.sh. valid/ is now also required to typecheck, not merely parse — all 16 already do.

Negative control run: making one ill-typed fixture well-typed turns the gate red with the right error; restoring it turns it green. The gate is not decorative.

Verification

  • Semiring laws checked exhaustively over all 27 triples. A "semiring" whose operations don't satisfy the laws is two arbitrary tables, and every soundness claim resting on it is worth nothing.
  • 677 compiler tests pass (dune runtest --force)
  • conformance 23/23; 8/8 examples evaluate; lib/stdlib.tangle typechecks
  • corpus gate + RSR instantiation gate green

Spec

New §3.10.1 with the [T-Weave-Linear] rule, the quantity table, the rejected-programs table, and an explicit scope note: this is the semiring applied to strands. The core judgement is still Γ ⊢ e : τ — a full Γ ⊢ e :^q τ changes the judgement shape and needs the metatheory re-proved, tracked separately.

🤖 Generated with Claude Code

`weave` performed no usage checking at all. It built the strand context,
checked the body, computed `Tangle[A,B]` — and discarded it, returning gamma
unchanged. Three unsound programs were accepted in silence:

    weave strands a, b into (a > a) yield strands a, b     # contraction
    weave strands a, b into (a > b) yield strands a, b, a  # contraction
    weave strands a, b into (a > b) yield strands a        # weakening

The spec already forbade the first (`i != j` on [T-Cross-Over]) and the third
("yield declarations match B"). Both side conditions were written down and
never enforced.

## Why a semiring and not "make the language linear"

The requirement is genuinely mixed, so a single discipline is the wrong answer
in both directions:

  * braid WORDS are unrestricted (omega) — `x . x` is sigma_1^2, a legitimate
    braid. Blanket linearity would reject valid programs.
  * STRANDS are linear (1) — a strand is a physical thread. A braid on n
    strands is a permutation of those n strands, so strand count is a
    conservation law. AFFINE is specifically wrong here: affine permits
    discarding, and a strand cannot vanish. That case is what decides it.
  * the CLAIM in `Epi[k, rho, tau]` is erased (0) — A-TG-11.1's recorded gap.

`compiler/lib/quantity.ml` provides {0, 1, omega} with add/mul/permits.
Independent uses combine with semiring addition, so two uses of one strand give
1 + 1 = omega, which is not permitted where 1 was declared.

## Applied in BOTH weave forms

Wiring the check into the weave STATEMENT rule alone left `def x = weave ...`
— the idiomatic spelling — entirely unchecked, because it reaches the weave
EXPRESSION rule and never touches the statement rule. The new conformance tier
caught this on its first run.

## Gating

New `conformance/ill-typed/` tier with a DOUBLE assertion: the file must PARSE
and then FAIL to typecheck. Asserting only "the compiler rejects it" is the
failure this suite already had once, when three invalid/ cases scored points
because the command was wrong and failed on every input. Requiring the parse
first proves the rejection came from the typechecker, not from a typo in the
fixture.

Wired into `scripts/check-corpus.sh` (what CI runs) as well as
`run_conformance.sh`, and verified with a negative control: making one
ill-typed fixture well-typed turns the gate red, restoring it turns it green.

`valid/` is now also required to TYPECHECK, not merely parse. All 16 already do.

## Verification

  * semiring laws checked EXHAUSTIVELY over all 27 triples — a "semiring"
    whose operations don't satisfy the laws is two arbitrary tables, and every
    soundness claim resting on it is worth nothing
  * 677 compiler tests pass (`dune runtest --force`)
  * conformance 23/23; 8/8 examples evaluate; stdlib typechecks
  * corpus + RSR gates green

Spec: new section 3.10.1 with the [T-Weave-Linear] rule and the quantity table.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread compiler/lib/typecheck.ml
Comment thread conformance/run_conformance.sh
@gitar-bot

gitar-bot Bot commented Jul 29, 2026

Copy link
Copy Markdown

Note

Automatic reviews are paused because your trial's included automatic processing has been used for this period. Upgrade now, or comment "Gitar review" to run a review anytime.
Learn more

Code Review ✅ Approved 2 resolved / 2 findings

Adds QTT quantity semiring enforcing linear strands in weave expressions and statements, accompanied by a new conformance tier. Consider addressing the dead [T-Self-Cross] rule and adding an empty-glob guard to run_conformance.sh.

Auto-approved and auto-merge armed: No blocking issues found.
Please see Auto-approve Docs for details on setting custom approval criteria. — merges when pipeline and required approvals pass.

✅ 2 resolved
Quality: Self-crossing rule [T-Self-Cross] is now dead / contradicts spec

📄 compiler/lib/typecheck.ml:585-588 📄 compiler/lib/typecheck.ml:378 📄 compiler/lib/typecheck.ml:1080 📄 docs/spec/FORMAL-SEMANTICS.md:480-494
check_strand_linearity runs before body inference in both weave forms, so any body containing a > a is rejected (uses(a)=1+1=omega) before reaching the Crossing rule's a = b branch (lines 585-588), which still types a self-crossing as a valid twist per [T-Self-Cross]. That branch is now unreachable inside a weave, and the spec simultaneously keeps [T-Self-Cross] (accepts a>a) and the new [T-Weave-Linear] (rejects it, per t01). This is a genuine but harmless internal inconsistency; the behavior is correct, but the dead branch and contradictory spec rule will confuse future maintainers. Consider removing the self-cross branch (or noting it as superseded) and reconciling the two spec rules.

Quality: run_conformance.sh ill-typed loop lacks empty-glob guard

📄 conformance/run_conformance.sh:87-101 📄 scripts/check-corpus.sh:204-216
The new ill-typed loop in run_conformance.sh iterates .../ill-typed/*.tangle without the [[ -e "$f" ]] || continue guard that the equivalent loop in check-corpus.sh (line 205) uses. If the directory is ever empty (and nullglob is unset), the glob stays literal, TOTAL is incremented, and the runner reports a spurious PARSE FAILED. Impact is low today since four fixtures exist, but the two runners should behave consistently. Add the same guard as the first line of the loop body.

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@gitar-bot

gitar-bot Bot commented Jul 29, 2026

Copy link
Copy Markdown

⚠️ Gitar auto-approved this PR but could not enable auto-merge: auto-merge is disabled for this repository — enable "Allow auto-merge" in the repository settings.

@gitar-bot gitar-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Gitar has auto-approved this PR and enabled auto-merge (configure)

@gitar-bot gitar-bot Bot added the gitar-approved Added by Gitar label Jul 29, 2026
@hyperpolymath
hyperpolymath marked this pull request as ready for review July 29, 2026 07:31
@hyperpolymath
hyperpolymath merged commit bfe25c1 into main Jul 29, 2026
25 checks passed
@hyperpolymath
hyperpolymath deleted the feat/qtt-linear-strands branch July 29, 2026 07:32
hyperpolymath added a commit that referenced this pull request Jul 29, 2026
…#104)

## The problem

Seventeen rules matched a `-> ()` arm in `check`. That is not a partial
check — **it is an accept**. A forged node naming `T-Close`, `T-App` or
`T-Weave` could conclude *any type at all* and the graph still checked
green.

The JEG's entire claim is *"evidence, not a log"* — that a derivation
can be validated without trusting whoever produced it. Those seventeen
arms were the part that made the claim false.

## Why three of them were genuinely underivable

`T-App`, `T-Crossing` and `T-Weave` could not be re-derived from the
recorded data, because the judgement didn't carry what the rule reads.
So the judgement was widened:

- **`j_ctx` now holds `env_entry`, not `ty`** — a `T-App` node cannot be
checked without the callee's *signature*.
- **`j_sigma` (new) holds the strand context** — `T-Crossing`/`T-Weave`
read Σ rather than premise types. They were previously bare **leaves**:
nodes asserting a type with *nothing* licensing it.

The other fourteen were derivable all along and simply hadn't been
written.

## Shared rule functions — the JEG now *cannot* drift

`infer_binop` was already type-in/type-out. The remaining rules were
inline in `infer_expr`, so the JEG would have had to **re-implement**
them — and a JEG that re-implements the rules can drift from the
typechecker, at which point it certifies a rule the compiler doesn't
apply and the evidence is worthless.

Extracted 16 (`infer_close`, `infer_mirror`, `infer_echo_eq`,
`join_arm_ty`, …) into one definition each, called by **both**. Drift is
impossible by construction rather than by discipline. The extraction is
behaviour-preserving — every pre-existing test passes unchanged.

## Two rules with real substance

- **`T-Let`** checks the conclusion is the *body's* type **and** that
the body was checked under the binding the let actually makes. Type-only
checking would accept a derivation whose body silently assumed `x` had a
more convenient type — there's a test for exactly that forgery.
- **`T-Weave`** re-runs the strand **linearity** check from #103, so a
graph cannot launder a weave that duplicates or drops a strand.

## The remaining hole is reported, not hidden

`T-Add-Block`'s island has its own judgement (⊢_hd). Rather than a
silent accept it's **recorded** — new `unchecked : derivation -> (string
* judgement) list`. So *"check succeeded"* and *"check succeeded **and
re-derived every node**"* are now distinguishable results, and
`--derive` says which:

```
== c ==  (3 nodes, depth 3, every node re-derived)
[T-Close] |- close(mirror(w)) : Tangle[I, I]
  [T-Mirror] |- mirror(w) : Word[3]
    [T-Var] w:Word[3] |- w : Word[3]
```

## Verification

- JEG suite **19 → 51 tests**, structured as an honest/forged **pair**
per rule closed — so each test's forged half is a case that **would have
passed before**
- **709 compiler tests** pass (`dune runtest --force`)
- conformance 23/23; corpus + RSR gates green

Follows #103 (merged). `feat/jeg-complete` on origin is a superseded
pre-rebase copy of this branch — safe to delete.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gitar-approved Added by Gitar

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant