Skip to content

Commit f64bbd0

Browse files
hyperpolymathclaude
andcommitted
feat(tg-7): out-of-band braid-group equivalence (Dehornoy handle reduction)
TG-7 non-semantic rung. compiler/lib/braid_equiv.ml decides braid-GROUP equivalence (equiv / is_trivial) via Dehornoy handle reduction, plus the writhe/permutation invariants. It is OUT-OF-BAND: the language's == on braids (eval.ml VBraid equality and the Lean Step.eqBraids rule) is left as list equality -- no semantics change. Whether to route == through it is a separate language-design decision left to the owner (PROOF-NEEDS.md TG-7); a mechanised Garside/Dehornoy correctness proof in Lean is the research-grade rung. A sigma_i-handle sigma_i^e w0 sigma_{i+1}^d ... sigma_i^-e (interior only sigma_{>=i+2} plus consistent-sign sigma_{i+1}) reduces by deleting the outer sigma_i^+-e and rewriting each sigma_{i+1}^d -> sigma_{i+1}^-e sigma_i^d sigma_{i+1}^e; a word is trivial iff it reduces to empty, so equiv u v iff u.v^-1 reduces to empty. A step-bound guards termination. Validation (compiler/test/tg7, 2220 assertions, no semantic ground truth assumed): the defining relations (commutation, braid relation both ways, cancellation, exponents); 400 randomly-constructed equivalent pairs where v is derived from u by relation-preserving moves (ground truth from the group relations; writhe + permutation invariants asserted to guard the generator); and invariant-distinguished negatives (differing writhe => provably not equiv). New library module is auto-included (lib/dune has no module list); no shared files or == semantics touched. dune runtest green. Docs: PROOF-NEEDS (TG-7 rung landed, semantics still owner-gated), CHANGELOG, STATE.a2ml. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8a87c1f commit f64bbd0

7 files changed

Lines changed: 358 additions & 6 deletions

File tree

.machine_readable/6a2/STATE.a2ml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ milestones = [
3737
"DONE (TG-3): OCaml typecheck.ml refines Lean HasType (translation validation) — closure proof + proofs/TG3Differential.lean (496 by-decide obligations generated from infer_expr) + compiler/test/tg3 (1008 --check assertions) + proofs/check-tg3-differential.sh wired into lean-proofs.yml; divergences D1 close / D2 bool-eq; proofs/TG3-REFINEMENT.md",
3838
"DONE (TG-5): compositional PD-lowering preserves structural invariants + verbatim echo residue — compiler/test/tg5/tg5_invariants.ml (189 assertions in dune runtest)",
3939
"BLOCKED (TG-8): dialects are README-only — need one dialect implemented (AST+parser+HasType/Step+eval) before conservativity is even stateable",
40-
"OWNER-DECISION (TG-7): braid-group eqBraids changes == semantics on braids (eval.ml + Lean Step) — decide: change semantics vs out-of-band braid_equiv checker",
40+
"RUNG-DONE (TG-7): out-of-band braid_equiv (Dehornoy handle reduction) in compiler/lib/braid_equiv.ml — decides braid-group equivalence WITHOUT changing == (compiler/test/tg7, 2220 assertions). Routing == through it stays an owner decision; Lean correctness research-grade.",
4141
"BLOCKED (TG-6): no wasm runtime wired in — add wasmtime/wasmer dev-dep + exec harness before differential testing; full bisimulation research-grade",
4242
]
4343

@@ -46,13 +46,13 @@ issues = []
4646

4747
[critical-next-actions]
4848
actions = [
49-
"TG-7 OWNER DECISION: braid `==` semantics — change to braid-group equivalence (touches eval.ml + Lean Step + proofs) OR add out-of-band braid_equiv checker leaving == as-is. Blocks TG-7.",
49+
"TG-7 OWNER DECISION (rung done): out-of-band braid_equiv now exists (compiler/lib/braid_equiv.ml). Decide whether to route `==`/Step.eqBraids through it (semantics change to eval.ml + Lean + proofs) or keep them list-equality and use braid_equiv only as a tool.",
5050
"TG-8 prerequisite: implement one dialect end-to-end (virtual-knot suggested) as the conservativity template; dialects are currently README-only.",
5151
"TG-6 prerequisite: wire a wasm runtime (wasmtime/wasmer dev-dep) + exec harness to enable differential testing.",
5252
"TangleIR Julia: implement EchoClosed node in KRLAdapter.jl per contract (quandledb session)",
5353
]
5454

5555
[maintenance-status]
56-
last-run-utc = "2026-06-14T15:00:00Z"
56+
last-run-utc = "2026-06-14T16:00:00Z"
5757
last-result = "pass" # unknown | pass | warn | fail
58-
notes = "dune build + dune test GREEN (incl. compiler/test/tg3 1008 + compiler/test/tg5 189 assertions); lean Tangle.lean 0 errors; proofs/check-tg3-differential.sh GREEN (496 obligations); TG-1+TG-2+TG-3+TG-5+TG-9+TG-10 LANDED; remaining TG-6/7/8 each blocked on a prerequisite (TG-8 dialect impl, TG-7 owner decision on braid == semantics, TG-6 wasm runtime); echo-types f7a965f grade semiring experimental (firewalled)"
58+
notes = "dune build + dune test GREEN (incl. tg3 1008 + tg5 189 + tg7 2220 assertions); lean Tangle.lean 0 errors; proofs/check-tg3-differential.sh GREEN (496 obligations); TG-1+TG-2+TG-3+TG-5+TG-9+TG-10 LANDED; TG-7 non-semantic rung LANDED (braid_equiv, == unchanged); remaining: TG-8 blocked (dialects README-only), TG-6 blocked (no wasm runtime), TG-7 semantics change owner-gated; echo-types f7a965f grade semiring experimental (firewalled)"

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
1313

1414
## [Unreleased]
1515

16+
### TG-7 (non-semantic rung): out-of-band braid-group equivalence
17+
18+
- **`compiler/lib/braid_equiv.ml`** decides braid-GROUP equivalence via Dehornoy
19+
handle reduction (`equiv`, `is_trivial`, plus `writhe`/`permutation`
20+
invariants). It is **out-of-band**: the language's `==` on braids (`eval.ml`
21+
and the Lean `Step.eqBraids` rule) is left as list equality — no semantics
22+
change. Routing `==` through it remains an owner decision (PROOF-NEEDS.md TG-7).
23+
- Tested in `compiler/test/tg7/tg7_braid_equiv.ml` (2220 assertions): the
24+
defining relations (commutation, braid relation, cancellation), 400
25+
randomly-constructed equivalent pairs (relation-preserving moves give ground
26+
truth; writhe/permutation invariants guard the generator), and
27+
invariant-distinguished negatives. Correctness is by-testing; a mechanised
28+
Garside/Dehornoy proof is the research-grade rung.
29+
1630
### TG-5 LANDED + readiness map for TG-6/7/8
1731

1832
- **TG-5 LANDED**: `compiler/test/tg5/tg5_invariants.ml` (189 assertions, in

PROOF-NEEDS.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ Cross-referenced to [PROOF-NARRATIVE.md §3](PROOF-NARRATIVE.md#3-remaining-obli
5959
| TG-4 | Pretty-print/parse round-trip on closed values | INV | OCaml property test (cheap) | P2 | 4h | **LANDED** (PR #46 — OCaml property test in `compiler/test/test_roundtrip.ml`, 26-entry corpus including 8 echo/product constructors; 52 round-trip runs) |
6060
| TG-5 | `compositional.ml` (418 LoC) rewriter preserves types | TP | OCaml property test | P2 || **LANDED** (`compiler/test/tg5/tg5_invariants.ml`, 189 assertions in `dune runtest`). compositional is below the Ty layer, so "preserves types" = preserves the PD-lowering structural invariants + echo residue-recovery: `OpenWord`/`ClosedDiagram`/`EchoClosed` each pinned (closedness, `\|crossings\|`=unit-length, source unit-expanded, **verbatim residue** for `EchoClose` with `expand(residue)=diagram word` and echo-diagram pdv1-identical to plain `close`), error paths, count pins. Lean IR model = optional later rung |
6161
| TG-6 | WASM compilation preserves semantics (source eval ≡ wasm exec) | TP / ALG | Lean 4 bisimulation | P1 | 3w (research-grade) | NOT STARTED |
62-
| TG-7 | `Step.eqBraids` decides braid-group equivalence (not list equality) | ALG / DOM | OCaml + Lean 4 | P2 | 2w | NOT STARTED (current impl is soundness-floor, not completeness) |
62+
| TG-7 | `Step.eqBraids` decides braid-group equivalence (not list equality) | ALG / DOM | OCaml + Lean 4 | P2 | | **RUNG LANDED (non-semantic)**: `compiler/lib/braid_equiv.ml` decides braid-group equivalence via Dehornoy handle reduction, out-of-band (leaves `==`/`Step.eqBraids` as list equality). Tested (`compiler/test/tg7`, 2220 assertions: defining relations + 400 constructed-equivalent pairs + invariant-distinguished negatives). **Still OWNER-GATED**: whether to route `==` through it (a semantics change to eval.ml + Lean Step + proofs) is a language-design decision; Lean correctness (Garside/Dehornoy) remains research-grade |
6363
| TG-8 | Each dialect (braid-calculus, quantum-circuit, skein-algebra, string-diagram, virtual-knot) is a conservative extension of core | TP | Lean 4 per-dialect | P3 | 1w each | NOT STARTED |
6464
| TG-9 | LSP diagnostics are a subset of `HasType` failures (no LSP-only diagnostics) | INV | Audit + refactor | P2 || **LANDED** (`tangle-lsp` delegates all diagnostics to `tanglec --check``compiler/lib/check.ml`; hand-rolled LSP-only false positives removed. Subset holds by construction. Tests: `test_check.ml` + tangle-lsp unit/delegation tests) |
6565
| TG-10 | Echo-types integrated into the type system: `Echo[ρ,τ]` former + `echoClose`/`lower`/`residue`/`echoAdd`/`echoEq` + product type (`pair`/`fst`/`snd`), with Progress/Preservation/Determinism/TypeSafety extended to cover them and the non-injectivity / residue-recovery capstones proven | TP / DOM | Lean 4 | P1 || **LANDED** (`proofs/Tangle.lean` §ECHO-TYPES) |
@@ -109,7 +109,14 @@ auto-landing without those prerequisites.**
109109
A Lean model of the PD IR + a mechanised preservation theorem is an optional
110110
later rung (Lean currently has no planar-diagram type).
111111

112-
### TG-7 — `eqBraids` decides braid-group equivalence — ⛔ **NEEDS OWNER DECISION**
112+
### TG-7 — `eqBraids` decides braid-group equivalence — 🟡 **RUNG LANDED, semantics OWNER-GATED**
113+
-**Non-semantic rung landed 2026-06-14**: `compiler/lib/braid_equiv.ml`
114+
(`equiv`/`is_trivial`) decides braid-group equivalence via Dehornoy handle
115+
reduction, *out-of-band*`==` / `Step.eqBraids` are untouched. Validated by
116+
`compiler/test/tg7/tg7_braid_equiv.ml` (2220 assertions): the defining
117+
relations, 400 constructed-equivalent pairs (writhe/permutation invariants
118+
guard the generator), and invariant-distinguished negatives. Correctness is
119+
by-testing; a Lean Garside/Dehornoy proof is the research-grade rung.
113120
- The only `eqBraids` is the Lean `Step` rule `eq (braidLit gs₁) (braidLit gs₂)
114121
→ boolLit (gs₁ == gs₂)` = **list equality**; OCaml `eval.ml` matches it.
115122
- Moving to Dehornoy handle reduction would **change the observable semantics of

compiler/lib/braid_equiv.ml

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
(* SPDX-License-Identifier: MPL-2.0 *)
2+
(* braid_equiv.ml — decide braid-GROUP equivalence of braid words via Dehornoy
3+
* handle reduction.
4+
*
5+
* TG-7, NON-SEMANTIC rung. The language's `==` on braids (eval.ml `VBraid`
6+
* equality and the Lean `Step.eqBraids` rule) is LIST equality and is left
7+
* UNCHANGED by this module — `braid_equiv` is an out-of-band decision procedure
8+
* for callers who want true braid-group equivalence (e.g. tooling, future
9+
* `compute`-style checks). Changing `==` itself to use this is a separate
10+
* language-design decision (see PROOF-NEEDS.md TG-7).
11+
*
12+
* Algorithm (Dehornoy 1997, "A fast method for comparing braids"). A braid
13+
* word on σ₁,σ₂,… A σᵢ-handle is a factor
14+
* σᵢ^e · w₀ · σ_{i+1}^d · w₁ · σ_{i+1}^d · ⋯ · σ_{i+1}^d · w_m · σᵢ^{-e}
15+
* with e,d ∈ {±1}, where every wₖ uses only generators σⱼ with j ≥ i+2 (so the
16+
* interior contains no σᵢ, no σ_{<i}, and the σ_{i+1} all share the sign d). It
17+
* reduces to
18+
* w₀ · (σ_{i+1}^{-e} σᵢ^d σ_{i+1}^e) · w₁ · ⋯ · (σ_{i+1}^{-e} σᵢ^d σ_{i+1}^e) · w_m
19+
* (the m = 0 case is plain free cancellation σᵢ^e wₐ σᵢ^{-e} → wₐ). Handle
20+
* reduction terminates and a reduced word is empty iff the braid is trivial, so
21+
* equiv u v ⇔ reduce (u · v⁻¹) = ε.
22+
*
23+
* Correctness here is established BY TESTING (compiler/test/tg7): the defining
24+
* relations, randomly-constructed equivalent pairs (ground truth from the group
25+
* relations), and the writhe/permutation invariants. A mechanised Garside/
26+
* Dehornoy correctness proof in Lean is the research-grade rung and is NOT
27+
* claimed. A safety step-bound guards against any non-termination surprise.
28+
*)
29+
30+
(* A braid word as a list of UNIT letters (index ≥ 1, sign ±1). *)
31+
type letter = { idx : int; sgn : int }
32+
33+
let units_of (gs : Ast.generator list) : letter list =
34+
List.concat_map (fun (g : Ast.generator) ->
35+
let s = if g.gen_exponent >= 0 then 1 else -1 in
36+
List.init (abs g.gen_exponent) (fun _ -> { idx = g.gen_index; sgn = s })) gs
37+
38+
(* inverse braid: reverse order, negate each sign. (ab)⁻¹ = b⁻¹a⁻¹. *)
39+
let inverse (w : letter list) : letter list =
40+
List.rev_map (fun l -> { l with sgn = - l.sgn }) w
41+
42+
(* free reduction: cancel adjacent σ·σ⁻¹. Sound (group inverses); keeps words
43+
short. (Adjacent cancellation is itself the empty-interior handle case, but
44+
doing it eagerly speeds convergence.) *)
45+
let free_reduce (w : letter list) : letter list =
46+
List.fold_right (fun x acc ->
47+
match acc with
48+
| y :: rest when x.idx = y.idx && x.sgn = - y.sgn -> rest
49+
| _ -> x :: acc) w []
50+
51+
(* Find and reduce the leftmost handle. Returns None if the word is handle-free. *)
52+
let reduce_one (w : letter list) : letter list option =
53+
let arr = Array.of_list w in
54+
let n = Array.length arr in
55+
(* Does position [a] open a handle? If so return its closing index [b]. *)
56+
let handle_at a =
57+
let i = arr.(a).idx and e = arr.(a).sgn in
58+
let rec scan k d_opt =
59+
if k >= n then None (* no close → not a handle *)
60+
else
61+
let j = arr.(k).idx and s = arr.(k).sgn in
62+
if j = i then (if s = -e then Some k else None) (* close, or same-sign σᵢ → blocked *)
63+
else if j < i then None (* σ_{<i} inside → blocked *)
64+
else if j = i + 1 then
65+
(match d_opt with
66+
| None -> scan (k + 1) (Some s)
67+
| Some d -> if d = s then scan (k + 1) d_opt else None) (* mixed σ_{i+1} signs *)
68+
else scan (k + 1) d_opt (* j ≥ i+2 → interior wₖ, ok *)
69+
in
70+
scan (a + 1) None
71+
in
72+
let rec find a =
73+
if a >= n then None
74+
else match handle_at a with Some b -> Some (a, b) | None -> find (a + 1)
75+
in
76+
match find 0 with
77+
| None -> None
78+
| Some (a, b) ->
79+
let i = arr.(a).idx and e = arr.(a).sgn in
80+
let interior = Array.to_list (Array.sub arr (a + 1) (b - a - 1)) in
81+
let rewritten =
82+
List.concat_map (fun l ->
83+
if l.idx = i + 1 then
84+
(* σ_{i+1}^d → σ_{i+1}^{-e} σᵢ^d σ_{i+1}^e *)
85+
[ { idx = i + 1; sgn = -e }; { idx = i; sgn = l.sgn }; { idx = i + 1; sgn = e } ]
86+
else [ l ]) interior
87+
in
88+
let prefix = Array.to_list (Array.sub arr 0 a) in
89+
let suffix = Array.to_list (Array.sub arr (b + 1) (n - b - 1)) in
90+
Some (prefix @ rewritten @ suffix)
91+
92+
let default_max_steps = 1_000_000
93+
94+
(* Reduce to a handle-free (and freely-reduced) word. *)
95+
let reduce ?(max_steps = default_max_steps) (w0 : letter list) : letter list =
96+
let rec loop steps w =
97+
if steps <= 0 then w (* safety net; never hit in tests *)
98+
else match reduce_one w with
99+
| None -> w
100+
| Some w' -> loop (steps - 1) (free_reduce w')
101+
in
102+
loop max_steps (free_reduce w0)
103+
104+
(* A braid word is trivial (= identity) iff it reduces to the empty word. *)
105+
let is_trivial_word (w : letter list) : bool = reduce w = []
106+
107+
let is_trivial (gs : Ast.generator list) : bool = is_trivial_word (units_of gs)
108+
109+
(* u ≡ v in the braid group ⇔ u·v⁻¹ is trivial. *)
110+
let equiv (u : Ast.generator list) (v : Ast.generator list) : bool =
111+
is_trivial_word (units_of u @ inverse (units_of v))
112+
113+
(* ---- invariants (necessary conditions; exposed for callers/tests) ---- *)
114+
115+
(* writhe = exponent sum (abelianisation); invariant under braid relations. *)
116+
let writhe (gs : Ast.generator list) : int =
117+
List.fold_left (fun a (g : Ast.generator) -> a + g.gen_exponent) 0 gs
118+
119+
(* underlying permutation on 1..n as an array p where p.(k-1) = image of strand k;
120+
each σᵢ (any sign) transposes strands i,i+1. *)
121+
let permutation ~(strands : int) (gs : Ast.generator list) : int array =
122+
let p = Array.init strands (fun k -> k + 1) in
123+
List.iter (fun (g : Ast.generator) ->
124+
let i = g.gen_index in
125+
if i >= 1 && i < strands then begin
126+
let t = p.(i - 1) in p.(i - 1) <- p.(i); p.(i) <- t
127+
end) gs;
128+
p

compiler/lib/braid_equiv.mli

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
(* SPDX-License-Identifier: MPL-2.0 *)
2+
(* braid_equiv.mli — out-of-band braid-GROUP equivalence (TG-7, non-semantic).
3+
*
4+
* Decides braid-group equality via Dehornoy handle reduction. This does NOT
5+
* change the language's `==` on braids (eval.ml / Lean Step keep list equality);
6+
* it is a separate decision procedure for callers wanting true equivalence. *)
7+
8+
(** A braid word as unit letters (index >= 1, sign +/-1). *)
9+
type letter = { idx : int; sgn : int }
10+
11+
(** Expand a braid literal (with arbitrary nonzero exponents) to unit letters. *)
12+
val units_of : Ast.generator list -> letter list
13+
14+
(** Inverse braid word: reverse order, negate signs. *)
15+
val inverse : letter list -> letter list
16+
17+
(** Dehornoy handle reduction to a handle-free, freely-reduced word.
18+
[max_steps] is a safety bound (default 1_000_000). *)
19+
val reduce : ?max_steps:int -> letter list -> letter list
20+
21+
(** A unit word represents the trivial braid iff it reduces to the empty word. *)
22+
val is_trivial_word : letter list -> bool
23+
24+
(** A braid literal represents the trivial (identity) braid. *)
25+
val is_trivial : Ast.generator list -> bool
26+
27+
(** [equiv u v] is true iff [u] and [v] denote the same braid-group element. *)
28+
val equiv : Ast.generator list -> Ast.generator list -> bool
29+
30+
(** Writhe (exponent sum); a braid-relation invariant. *)
31+
val writhe : Ast.generator list -> int
32+
33+
(** Underlying permutation on [1..strands]; a braid-relation invariant. *)
34+
val permutation : strands:int -> Ast.generator list -> int array

compiler/test/tg7/dune

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
; SPDX-License-Identifier: MPL-2.0
2+
; TG-7 braid-group equivalence (Braid_equiv, Dehornoy handle reduction).
3+
; dune runtest -> runs ./tg7_braid_equiv.exe --check.
4+
5+
(executable
6+
(name tg7_braid_equiv)
7+
(modules tg7_braid_equiv)
8+
(libraries tangle))
9+
10+
(rule
11+
(alias runtest)
12+
(deps tg7_braid_equiv.exe)
13+
(action (run ./tg7_braid_equiv.exe --check)))

0 commit comments

Comments
 (0)