Skip to content

Commit c0a362f

Browse files
fix(typecheck): implement [T-Twist-Strand] — (~a) on a named strand (#96) (#101)
`conformance/valid/v09_twist.tangle` parsed but failed to typecheck: ```tangle def twisted = weave strands a:Q into (~a) yield strands a:Q ``` ``` Strand name 'a' cannot be used as a standalone expression ``` ## The spec licenses it explicitly `FORMAL-SEMANTICS.md` §3.10: ``` Σ(a) = (i, T) ─────────────────────────────────── [T-Twist-Strand] Γ; Σ ⊢ (~a) : Tangle[[T], [T]] ``` and `spec/grammar.ebnf`: *"In weave context: `(~a)` twists named strand a."* The `Twist` case inferred its operand as an ordinary expression, and the `Var` rule rejects strand names outright — so the strand-name guard fired before the twist rule could apply. `[T-Twist-Strand]` is now tried **first**, leaving `[T-Twist-Word]`/`[T-Twist-Tangle]` to the standalone forms. ## One of a matched pair was missing The spec presents this rule immediately alongside `[T-Self-Cross]`: ``` Σ(a) = (i, T) Σ(a) = (i, T) ───────────────────────────────── ───────────────────────────────── Γ; Σ ⊢ (~a) : Tangle[[T], [T]] Γ; Σ ⊢ (a > a) : Tangle[[T], [T]] ``` `[T-Self-Cross]` was already implemented; its twin was not. **A test now pins that the two agree**, so they can't drift apart again. ## Evaluation Mirrors `Crossing`: structural, yielding the empty word. Weave bodies only became evaluable in #93, so this construct had **never run**. That's also defensible mathematically — a single-strand twist is invisible to the braid *word*: it's a framing change (Reidemeister I) that the braid group doesn't see. Detecting it as "a `Var` the environment doesn't bind" is safe because typechecking runs first and admits `Twist (Var a)` only when `a` is a strand. ## Result conformance **16/19 → 17/19 evaluating** (18/19 parse). The corpus ratchet demanded the manifest update — exactly what it's for. **`v11_add_block` (#94, the Harvard sub-language) is now the only remaining gap.** Tests: the rule itself; agreement with `[T-Self-Cross]`; a regression guard that standalone twist on a `Word` is unchanged; and that a nonsense twist still fails. All suites green; corpus and RSR gates pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 0366a8e commit c0a362f

4 files changed

Lines changed: 66 additions & 12 deletions

File tree

compiler/lib/eval.ml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,18 @@ let rec eval_expr (env : env) (e : expr) : value =
428428
cup/cap pair. Represented as an empty closed tangle. *)
429429
VTangle { tv_word = []; tv_closed = true }
430430

431+
(* [T-Twist-Strand]: `(~a)` on a NAMED STRAND inside a weave. Treated as
432+
structural, exactly as `Crossing` is below — weave bodies describe a
433+
morphism, and the interpreter carries no strand context at runtime.
434+
A single-strand twist is in any case invisible to the braid WORD: it is a
435+
framing change (Reidemeister I), which the braid group does not see.
436+
437+
Safe to detect by "a Var the environment does not bind": the typechecker
438+
runs first and accepts `Twist (Var a)` ONLY when `a` is in the strand
439+
context, so a genuinely unbound variable never reaches here. *)
440+
| Twist (Var a) when env_lookup env a = None ->
441+
ignore a; VBraid []
442+
431443
| Twist e1 ->
432444
let v = eval_expr env e1 in
433445
begin match v with

compiler/lib/typecheck.ml

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -373,11 +373,32 @@ let rec infer_expr (gamma : env) (sigma : strand_ctx) (e : expr) : ty =
373373

374374
(* [T-Twist-Word], [T-Twist-Tangle] (D1.18) *)
375375
| Twist e1 ->
376-
let t = infer_expr gamma sigma e1 in
377-
begin match t with
378-
| TWord n -> TWord n
379-
| TTangle (a, b) -> TTangle (a, b)
380-
| _ -> type_error "twist requires Word[n] or Tangle[A,B], got %s" (pp_ty t)
376+
(* [T-Twist-Strand] (D1.18, spec section 3.10) must be tried FIRST:
377+
*
378+
* Sigma(a) = (i, T)
379+
* ---------------------------------
380+
* Gamma; Sigma |- (~a) : Tangle[[T], [T]]
381+
*
382+
* `(~a)` on a NAMED STRAND inside a weave twists that strand. Falling
383+
* through to the general case would infer `a` as an ordinary expression,
384+
* and the Var rule rejects strand names outright ("cannot be used as a
385+
* standalone expression") — which is why conformance/valid/v09_twist.tangle
386+
* never typechecked despite the spec licensing it.
387+
*
388+
* Exactly parallel to [T-Self-Cross] below, which types `(a > a)` the same
389+
* way; the spec presents them as a pair. *)
390+
begin match e1 with
391+
| Var a when strand_lookup sigma a <> None ->
392+
let ea = Option.get (strand_lookup sigma a) in
393+
TTangle ([strand_to_type ea.strand_ty], [strand_to_type ea.strand_ty])
394+
| _ ->
395+
(* [T-Twist-Word] / [T-Twist-Tangle]: the standalone forms. *)
396+
let t = infer_expr gamma sigma e1 in
397+
begin match t with
398+
| TWord n -> TWord n
399+
| TTangle (a, b) -> TTangle (a, b)
400+
| _ -> type_error "twist requires Word[n] or Tangle[A,B], got %s" (pp_ty t)
401+
end
381402
end
382403

383404
(* ---- Crossings in weave context [T-Cross-Over], [T-Cross-Under] ---- *)

compiler/test/test_typecheck.ml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,31 @@ let test_isotopy () =
357357
infer gamma (BinOp (Isotopy, Var "t1", Var "t2")) = TBool);
358358

359359
(* Isotopy: non-topological types -> error *)
360+
(* #96 [T-Twist-Strand] (D1.18): `(~a)` on a NAMED STRAND inside a weave.
361+
Specified in FORMAL-SEMANTICS section 3.10 and in spec/grammar.ebnf, but
362+
infer_expr rejected any strand name used as an expression, so the
363+
construct was licensed by the spec and unimplemented. Parallel to
364+
[T-Self-Cross], which the spec presents alongside it. *)
365+
test "#96 T-Twist-Strand: (~a) on a strand is Tangle[[T],[T]]" (fun () ->
366+
let sigma = [("a", { strand_pos = 1; strand_ty = StrandNamed "Q" })] in
367+
infer_expr [] sigma (Twist (Var "a"))
368+
= TTangle ([StrandNamed "Q"], [StrandNamed "Q"]));
369+
370+
test "#96 twist agrees with the self-crossing rule it parallels" (fun () ->
371+
(* [T-Self-Cross] types (a > a) the same way; the two must not diverge. *)
372+
let sigma = [("a", { strand_pos = 1; strand_ty = StrandNamed "Q" })] in
373+
infer_expr [] sigma (Twist (Var "a"))
374+
= infer_expr [] sigma (Crossing ("a", Over, "a")));
375+
376+
test "#96 standalone twist on a Word is unchanged" (fun () ->
377+
(* Regression guard: [T-Twist-Word] must still apply outside a weave. *)
378+
infer [] (Twist (BraidLit [sigma 1])) = TWord 2);
379+
380+
test "#96 twist of a non-strand, non-word is still rejected" (fun () ->
381+
(* `raises` is defined in a later group; inline it here. *)
382+
try let _ = infer [] (Twist (StringLit "x")) in false
383+
with Type_error _ -> true);
384+
360385
test "T-Isotopy (type error)" (fun () ->
361386
try
362387
let _ = infer [] (BinOp (Isotopy, IntLit 1, IntLit 2)) in

scripts/check-corpus.sh

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,10 @@ CONFORMANCE_KNOWN_UNPARSED=(
7575
)
7676

7777
# conformance/valid programs that PARSE but do not yet TYPECHECK+EVALUATE.
78-
# v09_twist : `(~a)` — twisting a named strand inside a weave. spec/grammar.ebnf
79-
# says "In weave context: (~a) twists named strand a", but
80-
# infer_expr rejects any strand name used as an expression, so the
81-
# construct is specified and unimplemented. Tracked in #96.
82-
# v11 : does not parse at all (see above); listed here too so the eval
83-
# loop does not double-report it.
78+
# v11 : does not parse at all (see above); listed here too so the eval loop
79+
# does not double-report it.
80+
# (v09_twist was listed here for [T-Twist-Strand] until #96 implemented it.)
8481
CONFORMANCE_KNOWN_UNRUNNABLE=(
85-
v09_twist.tangle
8682
v11_add_block.tangle
8783
)
8884

0 commit comments

Comments
 (0)