Skip to content

feat(#92): widths need not agree for ==, and match arms join on width - #95

Merged
hyperpolymath merged 1 commit into
mainfrom
feat/width-widening-92
Jul 29, 2026
Merged

feat(#92): widths need not agree for ==, and match arms join on width#95
hyperpolymath merged 1 commit into
mainfrom
feat/width-widening-92

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Implements the #92 ruling across both engines — plus two bugs found while verifying it.

The rule

T-Eq-Word required both operands at the same width. Four ways that was inconsistent with the language itself (evidence in #92):

  • tComposeWord already widens to max n m — you could compose two braids you were then forbidden to compare.
  • ~ accepted differing widths and, since TG-7, evaluates through the same Braid_equiv.equiv. Identical operands, identical answer, one rejected by the typechecker.
  • eqIdBraid/eqBraidId decide "is this braid trivial?" against identity : word 0, so under the old rule they could only fire when the braid was empty. The step relation had rules for a question the typing rule forbade asking.
  • braid_equiv.equiv has no width parameter at all.

Lean's tEqWord now takes (n m); infer's .eq case drops the if n = m; OCaml Eq mirrors it. Match arms join on width for words and still require exact agreement otherwise.

The metatheory needed no patching

Progress, Preservation, Determinism, TypeSafety, infer_sound, infer_complete all still holdlean Tangle.lean reports 0 errors, sorry/axiom gate passes.

infer_sound's proof got simpler: with no if n = m there's no inner split to case on. A restriction whose removal shortens the proof was carrying no weight.

TG-3 regenerated and kernel-checked: 496 obligations, 0 errors. 46 flipped from = none to = some .bool — exactly the identity == braid shape that was unreachable before.

Two bugs found while verifying

1. Statement order. parse_file_recovering did stmts := prog @ !stmts then List.rev on the flattened result. That reversal is correct for an accumulator built by prepending single items (as diagnostics is) — but whole segments were prepended, so every program came out backwards:

def x = 5     parses to    def z = 7
def y = 6      ------->    def y = 6
def z = 7                  def x = 5

Any program whose statements depend on order died with "Unbound variable". The test suites never caught it because they call Tangle.Parser.program directly — only the CLI goes through the recovering path.

2. A false assertion in examples/trefoil.tangle:

assert reversed == braid[s1, s1, s1]     # reversed = reverse(trefoil)

reverse reverses the word and negates exponents, yielding the inverse braid. Verified by invariant: writhe(trefoil) = 3, writhe(reversed) = -3, and writhe is invariant under the braid relations — so they cannot be equal. The assertion had been wrong since it was written; nothing ran the examples until #89. Corrected to braid[s1^-1, s1^-1, s1^-1], which passes.

Result

before after
examples evaluating 2/7 7/7
lib/stdlib.tangle did not typecheck typechecks
conformance 18/19 18/19 (v11 is the Harvard sub-language, #94)

All seven examples are now in the corpus gate's must-run set, so none can regress.

⚠ Trusted base — registered as A-TG-92.1

The embedding Bₙ ↪ Bₙ₊₁ that justifies cross-width comparison is standard mathematics, but it is asserted in prose, not mechanised — no Lean lemma states it.

What is machine-checked: that the metatheory holds under the widened rule, and that OCaml infer_expr still agrees with Lean infer on the corpus. That distinction is recorded in ASSUMPTIONS.md rather than left implicit.

Tests

6 new typecheck cases, including two negatives — mismatched kinds still rejected, match arms of different kinds still rejected — so widening cannot quietly become "anything compares to anything". Three test_check cases repointed at an error that is still an error (and an assertion helper left unused by the change was put back to work rather than deleted).

🤖 Generated with Claude Code

Implements the #92 ruling across both engines, plus two bugs found while
verifying it.

## The rule

`T-Eq-Word` required both operands at the same width. That was inconsistent
with the rest of the language in four ways, all recorded in #92:

  * `tComposeWord` already WIDENS to max n m — a program could compose two
    braids it was then forbidden to compare;
  * `~` accepted differing widths and, since TG-7, evaluates through the SAME
    `Braid_equiv.equiv` — identical operands, identical answer, one rejected;
  * `eqIdBraid`/`eqBraidId` decide "is this braid trivial?" against
    `identity : word 0`, so under the old rule they could only ever fire when
    the braid was empty — the step relation had rules for a question the
    typing rule forbade asking;
  * `braid_equiv.equiv` has no width parameter at all.

Lean `tEqWord` now takes (n m); `infer`'s `.eq` case drops the `if n = m`.
OCaml `Eq` mirrors it. Match arms join on width for words (max) and still
require exact agreement for everything else.

## The metatheory did not need patching

Progress, Preservation, Determinism, TypeSafety, infer_sound and
infer_complete all still hold — `lean Tangle.lean` reports 0 errors, and the
sorry/axiom gate passes. The `infer_sound` proof got SIMPLER: with no `if
n = m` there is no inner `split` left to case on, which is a decent sign the
restriction was load-bearing for nothing.

TG-3 regenerated and kernel-checked: 496 obligations, 0 errors. 46 of them
flipped from `= none` to `= some .bool` — precisely the `identity == braid`
shape that was previously unreachable.

## Two bugs found while verifying

1. STATEMENT ORDER. `parse_file_recovering` in bin/main.ml did
   `stmts := prog @ !stmts` and then `List.rev` on the flattened result. That
   reversal is right for an accumulator built by prepending single items — as
   `diagnostics` is — but whole SEGMENTS were prepended, so every program came
   out backwards: `def x; def y; def z` parsed to [z; y; x]. Any program whose
   statements depend on order failed at evaluation with "Unbound variable".
   The test suites never caught it because they call `Tangle.Parser.program`
   directly; only the CLI goes through the recovering path. Now accumulates
   segments and flattens in order.

2. A FALSE ASSERTION in examples/trefoil.tangle:
       assert reversed == braid[s1, s1, s1]
   `reverse` reverses the word AND negates exponents, so it yields the INVERSE
   braid. writhe(trefoil) = 3 but writhe(reversed) = -3, and writhe is
   invariant under the braid relations, so they cannot be equal. The assertion
   had been wrong since it was written; nothing ran the examples until #89.
   Corrected to braid[s1^-1, s1^-1, s1^-1], which passes.

## Result

  examples evaluating : 2/7 -> 7/7   (all now in the must-run set)
  lib/stdlib.tangle   : did not typecheck -> typechecks
  conformance         : 18/19 (v11 add{} is the Harvard sub-language, #94)

## Trusted base

Registered as A-TG-92.1: the embedding Bn -> Bn+1 that justifies cross-width
comparison is standard mathematics but is asserted IN PROSE, not mechanised.
What is machine-checked is that the metatheory holds under the widened rule
and that OCaml still agrees with Lean on the corpus.

Tests: 6 new typecheck cases including two negatives (mismatched KINDS still
rejected; match arms of different kinds still rejected) so widening cannot
degrade into "anything compares to anything". Three test_check cases
repointed at an error that is still an error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@hyperpolymath
hyperpolymath merged commit 478857f into main Jul 29, 2026
26 checks passed
@hyperpolymath
hyperpolymath deleted the feat/width-widening-92 branch July 29, 2026 01:20
@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

Removes strict width agreement requirements for equality comparisons and adds match arm width joining across both engines, enabling all 7 example programs to evaluate successfully. No issues found.

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.

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 added a commit that referenced this pull request Jul 29, 2026
…ertion (#97)

The false assertion corrected in #95 was **not an isolated slip**.
Auditing whether the conformance corpus actually *runs* — the gate only
ever checked that it *parsed* — turned up a second mathematically false
claim of exactly the same kind, plus one genuine unimplemented feature.

## The second false assertion

`conformance/valid/v16_close_mirror_reverse.tangle`:

```tangle
assert reverse(braid[s1, s2]) == braid[s2, s1]
```

`reverse` reverses the word **and** negates every exponent — it yields
the *inverse* braid, so `reverse(s1 s2) = s2⁻¹ s1⁻¹`.

Disproved by invariant: `writhe(reverse(braid[s1,s2])) = -2` while
`writhe(braid[s2,s1]) = +2`, and writhe is invariant under the braid
relations, so they cannot be the same element.

## Two authors, same mistake → a documentation failure

`examples/trefoil.tangle` and this file made the *identical* wrong
assumption. That is not carelessness. `FORMAL-SEMANTICS.md` described
the operation as:

```
|  reverse(e)     -- reverse word
```

which reads as order-only. Now stated as an identity:

```
reverse(g₁ g₂ … gₙ)  =  gₙ⁻¹ … g₂⁻¹ g₁⁻¹        i.e.  reverse(w) = w⁻¹
```

with the contrast against `mirror` (which negates **in place**) spelled
out, and the writhe argument recorded so the next reader can check it
rather than trust it.

## The systemic fix

`scripts/check-corpus.sh` now **evaluates** `conformance/valid`, not
just parses it. Parsing was never enough — a program can parse perfectly
and assert something false. Both bad assertions survived precisely
because nothing ran them.

**Verified in both directions:** reintroducing the exact false assertion
makes the gate fail (`exit 1`, *"parses but does not evaluate"*);
restoring it returns `exit 0`.

## The genuine gap

`v09_twist.tangle` uses `(~a)` to twist a named strand inside a weave.
`spec/grammar.ebnf` says *"In weave context: (~a) twists named strand
a"*, but `infer_expr` rejects any strand name used as an expression —
specified and unimplemented. Recorded in the new
`CONFORMANCE_KNOWN_UNRUNNABLE` list rather than papered over, and raised
as **#96**.

Worth noting *why* it was invisible: until #93 a weave block could not
be bound to anything, so its body was never evaluated. The construct has
literally never run.

## Regression tests

Four pinning `reverse`, chosen so they cannot pass vacuously:

| Test | Why it can't be faked |
|---|---|
| reverse is **not** order-reversal | asserts inequality with the wrong
answer |
| `reverse(reverse(w)) = w` | only holds if it genuinely inverts |
| reverse negates writhe | the invariant that disproved both false
claims |
| `mirror` negates *in place* | pins the contrast that caused the
confusion |

Suites 112 → 116, all green; TG-3 still 1008/0.

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

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
hyperpolymath added a commit that referenced this pull request Jul 29, 2026
…ent-order bug (#99)

TG-11 (#98) landed the type former, typing, evaluation and six proofs —
but there was **no way to write one**. Zero occurrences of the
constructors in `lexer.mll` or `parser.mly`, against 4 for echo. Fully
implemented and unreachable.

## Surface syntax

```tangle
def w     = warrant[0](42, braid[s1, s2])   -- at standpoint 0
def token = evidence(w)                      -- the ONLY elimination
```

The standpoint is bracketed because it's an *index*, not an operand —
the same reading as a braid generator's subscript. No grammar conflicts.

**Non-factivity reaches the syntax.** There is deliberately no keyword
extracting the claim: `claim(w)` parses as an ordinary call to an
undefined function, never as a language form. A test pins that
distinction — if a `Claim` form ever appears, it contradicts
`epi_only_yields_evidence`.

## A *second* copy of the statement-order bug

Writing the first epistemic program surfaced it immediately:

```
def w   = warrant[0](42, braid[s1])
def tok = evidence(w)
→ In definition 'tok': evidence requires an Epi[k, rho, tau], got Word[0]
```

That `Word[0]` is the pass-1a **placeholder** — `tok` was being checked
*before* `w` was refined. Same root cause as #95 (`stmts := prog @
!stmts` then `List.rev` on the flattened list), but in a **second
copy**, in `lib/check.ml`.

**That copy feeds `tanglec --check` and the LSP.** Both have been
analysing programs with their statements reversed.

And it was never epistemic-specific:

```
def e = echoClose(braid[s1])
def r = residue(e)
→ residue requires Echo[_, _], got Word[0]
```

**Any cross-definition reference to a non-`Word` type was mis-typed.**
Echo has been broken this way for as long as the recovering path has
existed.

The test suites missed *both* copies for the same reason: they call
`Parser.program` directly and never exercise the recovering path.

## Tests

**Parser** — warrant with bracketed standpoint; evidence; TG-4
round-trip; the no-claim-form guard.
**Check** — three cross-definition cases (plain, echo, epistemic), all
of which would have failed before this.

## Example

`examples/epistemic.tangle`, wired into the corpus gate's must-run set —
warrants at three standpoints, evidence recovery, and the echo
composition `residue(evidence(we))`. All assertions pass.

It also documents *why* the claim is unreachable: if it were
extractable, anything anyone attested would become true by fiat —
precisely the bug you don't want in a provenance system.

All gates green: Lean 0 errors, full suite, corpus, RSR.

🤖 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