Skip to content

Commit fadd7a1

Browse files
feat(jtv): the add{} injection island — Harvard data expressions (#94) (#102)
`conformance/valid/v11_add_block.tangle` was the last program the parser rejected — and the only one that wasn't a parse-rule gap. `add{}` is a **sub-language**, specified across 288 lines of `FORMAL-SEMANTICS.md`. **Not stale**, unlike `TangleIR`. `README-jtv.adoc` documents it as deliberate design: *"Computation is braiding… But sometimes, you need arithmetic."* Two syntactically isolated islands give TANGLE data manipulation without polluting the topological core. ## Semantic separation is the whole point `+` in TANGLE is **connect-sum**; `+` inside `add{}` is **arithmetic**. So the island gets its own grammar (`hv_expr`), its own type language (`hv_ty`), its own judgement (`⊢_hd`) and its own value space (`hv_value`) — sharing TANGLE's `expr` would have lost exactly the distinction the design exists to make. Verified: | | | |---|---| | `braid[s1] + braid[s2]` | connect-sum | | `add{ 2 + 3 }` | `5` | | `add{ braid[s1] }` | **does not parse** — the island is closed | Results cross back through **Embed** (D2.4): `Int`/`Float` → `Num`, `Bool` → `Bool`, `String` → `Str`. ## `add` is NOT reserved My first attempt made `add` a keyword — which **broke `def add(a, b) = a . b`**, a valid TANGLE program that the e2e suite contains. The spec's own first design principle points at the fix: *"Delimited Syntax: `add{...}` cannot conflict with TANGLE operators."* **The delimiter is what prevents conflict**, so `add{` is lexed as a single `ADDBRACE` token. `add` alone stays an ordinary identifier — the token dump shows both in one file: ``` 1:7 IDENT(add) def add(a,b) = a . b 2:12 ADDBRACE def z = add{ 1 } ``` ## Scope — stated, not implied **Implemented:** the full operator hierarchy (`+ - * / %`, `== != < <= > >=`, `&& || !`), the **total** conditional (both branches required, D2.1), and Int/Float/Bool/String literals. Guaranteed terminating: structural recursion on a finite term, no loops, no assignment, no side effects. **Not implemented, and not pretended** — recorded in the source, not just here: rationals, complex, lists, tuples (§7.1); Hex/Binary/Symbolic types; variables in the `Π` environment and function calls (§8.2, §9.5); and the `harvard{...}` **control** block (§6.3) entirely. The island is outside the mechanised core — `⊢_hd` has no Lean image — so `AddBlock` joins the non-core constructors `tg3_emit` rejects outright, and the JEG lists `T-Add-Block` among its **deferred** rules rather than pretending to re-derive a judgement it doesn't implement. ## Result **conformance 18/19 → 19/19.** Both corpus manifest gap-lists are now **empty**: for the first time every valid program parses, typechecks and evaluates, and every invalid one is still rejected. 13 tests: precedence, conditional, logic, modulo, int-vs-float division, mixed promotion, division by zero, Embed — plus two negatives (arithmetic on a `Bool`; if-branches that disagree). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent c0a362f commit fadd7a1

12 files changed

Lines changed: 423 additions & 16 deletions

File tree

‎compiler/bin/main.ml‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,18 @@ let dump_tokens (filename : string) : unit =
189189
| ECHOADD -> print_string "ECHOADD"
190190
| ECHOEQ -> print_string "ECHOEQ"
191191
| WARRANT -> print_string "WARRANT"
192-
| EVIDENCE -> print_string "EVIDENCE");
192+
| EVIDENCE -> print_string "EVIDENCE"
193+
| ADDBRACE -> print_string "ADDBRACE"
194+
| IF -> print_string "IF"
195+
| THEN -> print_string "THEN"
196+
| ELSE -> print_string "ELSE"
197+
| AMPAMP -> print_string "AMPAMP"
198+
| BARBAR -> print_string "BARBAR"
199+
| BANGEQ -> print_string "BANGEQ"
200+
| LE -> print_string "LE"
201+
| GE -> print_string "GE"
202+
| PERCENT -> print_string "PERCENT"
203+
| BANG -> print_string "BANG");
193204
print_newline ();
194205
if tok <> EOF then loop ()
195206
in

‎compiler/lib/ast.ml‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,19 @@ and expr =
9797
* proofs/Tangle.lean and epistemic-types' Warrant.agda.
9898
* NON-FACTIVE: `Evidence` is the only elimination. There is no
9999
* operation taking a warrant to the thing warranted. ---- *)
100+
(* ---- JTV injection island (D2.1) ----
101+
* `add{ he }` embeds a Harvard DATA expression into TANGLE.
102+
*
103+
* Deliberately a SEPARATE grammar, not more TANGLE expressions: the whole
104+
* point of the island is semantic separation. `+` in TANGLE is connect-sum
105+
* on tangles; `+` inside add{} is arithmetic. Sharing one `expr` type would
106+
* lose exactly the distinction the design exists to make (README-jtv.adoc,
107+
* "Semantic Separation").
108+
*
109+
* The block is total and pure by construction: no side effects, no loops, no
110+
* assignment, guaranteed terminating (D2.1). *)
111+
| AddBlock of hv_expr (* add{ he } *)
112+
100113
| Warrant of int * expr * expr (* warrant κ claim evidence (redex) *)
101114
| EpiVal of int * expr * expr (* formed warrant: standpoint, claim, token *)
102115
| Evidence of expr (* project the evidence token (ONLY elimination) *)
@@ -128,6 +141,32 @@ and expr =
128141
* touches no proof obligation. *)
129142
| Weave of weave_block
130143

144+
(** Harvard DATA expressions — the `add{...}` island (spec section 6.2).
145+
Separate from [expr] on purpose; see [AddBlock].
146+
147+
Implemented here: the scalar-literal fragment with the full operator
148+
hierarchy and the conditional. NOT implemented, and NOT pretended:
149+
rationals, complex numbers, lists and tuples (spec section 7.1); variables
150+
resolving in the Pi environment and function calls (sections 8.2, 9.5); and
151+
the `harvard{...}` CONTROL block (section 6.3) entirely. *)
152+
and hv_expr =
153+
| HvInt of int
154+
| HvFloat of float
155+
| HvStr of string
156+
| HvBool of bool
157+
| HvUn of hv_unop * hv_expr
158+
| HvBin of hv_binop * hv_expr * hv_expr
159+
| HvIf of hv_expr * hv_expr * hv_expr (* total: both branches required *)
160+
161+
and hv_unop =
162+
| HvNeg (* - *)
163+
| HvNot (* ! *)
164+
165+
and hv_binop =
166+
| HvAdd | HvSub | HvMul | HvDiv | HvMod
167+
| HvEq | HvNe | HvLt | HvLe | HvGt | HvGe
168+
| HvAnd | HvOr
169+
131170
(** Binary operator tag. *)
132171
and binop =
133172
| Add (** + *)

‎compiler/lib/eval.ml‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,85 @@ let gens_of_value (v : value) : gen list =
282282
| VTangle tv -> tv.tv_word
283283
| _ -> eval_error "Expected a braid or tangle value, got %s" (pp_value v)
284284

285+
(** Harvard data VALUES — the island's own value space, kept separate from
286+
TANGLE's [value] so the two cannot be confused. *)
287+
type hv_value =
288+
| HvVInt of int
289+
| HvVFloat of float
290+
| HvVBool of bool
291+
| HvVStr of string
292+
293+
(** Evaluate a Harvard data expression. Total by construction (D2.1): no
294+
loops, no assignment, no side effects — every case is structural recursion
295+
on a finite term, so this terminates. Division by zero is the one runtime
296+
error the fragment admits. *)
297+
let rec eval_hv (e : hv_expr) : hv_value =
298+
let num2 f g a b =
299+
match a, b with
300+
| HvVInt x, HvVInt y -> HvVInt (f x y)
301+
| HvVInt x, HvVFloat y -> HvVFloat (g (float_of_int x) y)
302+
| HvVFloat x, HvVInt y -> HvVFloat (g x (float_of_int y))
303+
| HvVFloat x, HvVFloat y -> HvVFloat (g x y)
304+
| _ -> eval_error "add{}: arithmetic on non-numbers"
305+
in
306+
let cmp2 f g a b =
307+
match a, b with
308+
| HvVInt x, HvVInt y -> HvVBool (f x y)
309+
| HvVInt x, HvVFloat y -> HvVBool (g (float_of_int x) y)
310+
| HvVFloat x, HvVInt y -> HvVBool (g x (float_of_int y))
311+
| HvVFloat x, HvVFloat y -> HvVBool (g x y)
312+
| _ -> eval_error "add{}: comparison on non-numbers"
313+
in
314+
match e with
315+
| HvInt n -> HvVInt n
316+
| HvFloat f -> HvVFloat f
317+
| HvStr s -> HvVStr s
318+
| HvBool b -> HvVBool b
319+
| HvUn (HvNeg, a) ->
320+
(match eval_hv a with
321+
| HvVInt n -> HvVInt (-n) | HvVFloat f -> HvVFloat (-.f)
322+
| _ -> eval_error "add{}: negation of a non-number")
323+
| HvUn (HvNot, a) ->
324+
(match eval_hv a with
325+
| HvVBool b -> HvVBool (not b)
326+
| _ -> eval_error "add{}: ! of a non-boolean")
327+
| HvBin (op, a, b) ->
328+
let va = eval_hv a and vb = eval_hv b in
329+
begin match op with
330+
| HvAdd -> num2 ( + ) ( +. ) va vb
331+
| HvSub -> num2 ( - ) ( -. ) va vb
332+
| HvMul -> num2 ( * ) ( *. ) va vb
333+
| HvDiv ->
334+
(match va, vb with
335+
| _, HvVInt 0 -> eval_error "add{}: division by zero"
336+
| _, HvVFloat 0.0 -> eval_error "add{}: division by zero"
337+
| _ -> num2 ( / ) ( /. ) va vb)
338+
| HvMod ->
339+
(match va, vb with
340+
| HvVInt _, HvVInt 0 -> eval_error "add{}: modulo by zero"
341+
| HvVInt x, HvVInt y -> HvVInt (x mod y)
342+
| _ -> eval_error "add{}: modulo requires integers")
343+
| HvLt -> cmp2 ( < ) ( < ) va vb
344+
| HvLe -> cmp2 ( <= ) ( <= ) va vb
345+
| HvGt -> cmp2 ( > ) ( > ) va vb
346+
| HvGe -> cmp2 ( >= ) ( >= ) va vb
347+
| HvEq -> HvVBool (va = vb)
348+
| HvNe -> HvVBool (va <> vb)
349+
| HvAnd ->
350+
(match va, vb with
351+
| HvVBool x, HvVBool y -> HvVBool (x && y)
352+
| _ -> eval_error "add{}: && on non-booleans")
353+
| HvOr ->
354+
(match va, vb with
355+
| HvVBool x, HvVBool y -> HvVBool (x || y)
356+
| _ -> eval_error "add{}: || on non-booleans")
357+
end
358+
| HvIf (c, t, e2) ->
359+
(match eval_hv c with
360+
| HvVBool true -> eval_hv t
361+
| HvVBool false -> eval_hv e2
362+
| _ -> eval_error "add{}: if condition is not a boolean")
363+
285364
(** Evaluate an expression in the given environment. *)
286365
let rec eval_expr (env : env) (e : expr) : value =
287366
match e with
@@ -406,6 +485,16 @@ let rec eval_expr (env : env) (e : expr) : value =
406485
(* Epistemic. `warrant` forms the value; `evidence` is the sole projection
407486
and yields the TOKEN. There is deliberately no operation returning the
408487
claim — holding a warrant is not holding the fact. *)
488+
(* The add{} island evaluates in its own world and crosses back as a TANGLE
489+
value. Total and pure: every case terminates, nothing escapes. *)
490+
| AddBlock he ->
491+
begin match eval_hv he with
492+
| HvVInt n -> VInt n
493+
| HvVFloat f -> VFloat f
494+
| HvVBool b -> VBool b
495+
| HvVStr s -> VString s
496+
end
497+
409498
| Warrant (k, claim, ev) ->
410499
VEpi (k, eval_expr env claim, eval_expr env ev)
411500

‎compiler/lib/jeg.ml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ let rec derive (gamma : env) (e : expr) : derivation =
9999
(derive gamma scrut :: List.map (fun a -> derive gamma a.arm_body) arms)
100100

101101
| Call (f, args) -> node "T-App" gamma [f] e ty (List.map (derive gamma) args)
102+
| AddBlock _ -> leaf "T-Add-Block"
102103
| Crossing _ -> leaf "T-Crossing"
103104
| Weave _ -> leaf "T-Weave"
104105

@@ -225,6 +226,9 @@ let rec check_node (d : derivation) : unit =
225226
| "T-Pipeline" | "T-Unary" | "T-Close" | "T-Mirror" | "T-Reverse"
226227
| "T-Simplify" | "T-Twist" | "T-Cap" | "T-Cup" | "T-Echo-Add" | "T-Echo-Eq"
227228
| "T-Let" | "T-Match" | "T-App" | "T-Crossing" | "T-Weave" -> ()
229+
(* T-Add-Block: the island has its own judgement (|-_hd), so re-deriving it
230+
here would mean re-implementing that checker. Deferred, and listed. *)
231+
| "T-Add-Block" -> ()
228232
| r -> fail r "unknown rule name" c
229233

230234
let check (d : derivation) : (unit, check_error list) result =

‎compiler/lib/lexer.mll‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,16 @@
5858
deliberately no keyword that extracts the claim from a warrant. *)
5959
| "warrant" -> WARRANT
6060
| "evidence" -> EVIDENCE
61+
(* JTV island (D2.1). `if`/`then`/`else` are island-only keywords — TANGLE
62+
has no conditional of its own.
63+
NOTE `add` is deliberately NOT here: it must stay a usable identifier
64+
(`def add(a, b) = a + b` is valid TANGLE and appears in the e2e suite).
65+
The island is entered by the two-character opener `add{`, lexed as a
66+
single ADDBRACE token below — which is exactly the "Delimited Syntax"
67+
principle in README-jtv.adoc: the delimiter is what prevents conflict. *)
68+
| "if" -> IF
69+
| "then" -> THEN
70+
| "else" -> ELSE
6171
| "jones" -> JONES
6272
| "alexander" -> ALEXANDER
6373
| "homfly" -> HOMFLY
@@ -100,6 +110,20 @@ rule token = parse
100110
| "=>" { ARROW }
101111
| "==" { EQEQ }
102112
| ">>" { GTGT }
113+
(* JTV island operators. `&&`, `||`, `!=`, `<=`, `>=`, `%` and `!` appear
114+
only inside add{...}: TANGLE has no logical operators and no inequality,
115+
so these cannot collide with core syntax. Multi-char forms must precede
116+
the single-char rules below. *)
117+
(* The island opener, matched as ONE token so `add` alone stays an IDENT.
118+
Must precede the identifier rule. *)
119+
| "add" [' ' '\t']* '{' { ADDBRACE }
120+
| "&&" { AMPAMP }
121+
| "||" { BARBAR }
122+
| "!=" { BANGEQ }
123+
| "<=" { LE }
124+
| ">=" { GE }
125+
| '%' { PERCENT }
126+
| '!' { BANG }
103127

104128
(* Single-character operators and punctuation *)
105129
| '.' { DOT }

‎compiler/lib/parser.mly‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
(* Echo / product forms — surface syntax mirrors pretty.ml output *)
3636
%token ECHOCLOSE LOWER RESIDUE PAIR FST SND ECHOADD ECHOEQ
3737
%token WARRANT EVIDENCE
38+
%token ADDBRACE IF THEN ELSE
39+
%token AMPAMP BARBAR BANGEQ LE GE PERCENT BANG
3840

3941
(* Invariant names *)
4042
%token JONES ALEXANDER HOMFLY KAUFFMAN WRITHE LINKING
@@ -302,6 +304,12 @@ unary_expr:
302304
{ Warrant (k, c, ev) }
303305
| EVIDENCE LPAREN e = expr RPAREN
304306
{ Evidence e }
307+
(* ---- JTV injection island (D2.1) ----
308+
`add{ he }` switches to the Harvard DATA grammar entirely. The island is
309+
delimited precisely so its operators cannot conflict with TANGLE's:
310+
`+` here is arithmetic, `+` outside is connect-sum. *)
311+
| ADDBRACE he = hv_expr RBRACE
312+
{ AddBlock he }
305313
| t = twist_expr { t }
306314
| MINUS e = primary_expr { UnaryOp (Neg, e) }
307315
| e = primary_expr { e }
@@ -347,6 +355,66 @@ primary_expr:
347355
{ e }
348356
;
349357

358+
(* ================================================================== *)
359+
(* JTV Harvard DATA grammar (spec section 6.2) *)
360+
(* ================================================================== *)
361+
(* A SEPARATE hierarchy from TANGLE's. Precedence, loosest to tightest:
362+
if/then/else < || < && < comparison < + - < * / % < unary
363+
Note `if` is total: both branches are required (D2.1). *)
364+
365+
hv_expr:
366+
| IF c = hv_expr THEN t = hv_expr ELSE e = hv_expr { HvIf (c, t, e) }
367+
| e = hv_or { e }
368+
;
369+
370+
hv_or:
371+
| a = hv_or BARBAR b = hv_and { HvBin (HvOr, a, b) }
372+
| e = hv_and { e }
373+
;
374+
375+
hv_and:
376+
| a = hv_and AMPAMP b = hv_cmp { HvBin (HvAnd, a, b) }
377+
| e = hv_cmp { e }
378+
;
379+
380+
hv_cmp:
381+
| a = hv_sum EQEQ b = hv_sum { HvBin (HvEq, a, b) }
382+
| a = hv_sum BANGEQ b = hv_sum { HvBin (HvNe, a, b) }
383+
| a = hv_sum LT b = hv_sum { HvBin (HvLt, a, b) }
384+
| a = hv_sum LE b = hv_sum { HvBin (HvLe, a, b) }
385+
| a = hv_sum GT b = hv_sum { HvBin (HvGt, a, b) }
386+
| a = hv_sum GE b = hv_sum { HvBin (HvGe, a, b) }
387+
| e = hv_sum { e }
388+
;
389+
390+
hv_sum:
391+
| a = hv_sum PLUS b = hv_prod { HvBin (HvAdd, a, b) }
392+
| a = hv_sum MINUS b = hv_prod { HvBin (HvSub, a, b) }
393+
| e = hv_prod { e }
394+
;
395+
396+
hv_prod:
397+
| a = hv_prod STAR b = hv_unary { HvBin (HvMul, a, b) }
398+
| a = hv_prod SLASH b = hv_unary { HvBin (HvDiv, a, b) }
399+
| a = hv_prod PERCENT b = hv_unary { HvBin (HvMod, a, b) }
400+
| e = hv_unary { e }
401+
;
402+
403+
hv_unary:
404+
| MINUS e = hv_unary { HvUn (HvNeg, e) }
405+
| BANG e = hv_unary { HvUn (HvNot, e) }
406+
| e = hv_atom { e }
407+
;
408+
409+
hv_atom:
410+
| n = INT { HvInt n }
411+
| f = FLOAT { HvFloat f }
412+
| s = STRING { HvStr s }
413+
| TRUE { HvBool true }
414+
| FALSE { HvBool false }
415+
| LPAREN e = hv_expr RPAREN { e }
416+
;
417+
350418
(* ---- Crossings: (a > b) or (a < b) ---- *)
351419

352420
crossing:

‎compiler/lib/pretty.ml‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,28 @@ let pp_strand_list ctx strands =
100100
pp_typed_strand ctx s
101101
) strands
102102

103+
(* Harvard data expressions print fully parenthesised: the island has its own
104+
precedence, and re-parsing must not depend on the reader sharing TANGLE's. *)
105+
let rec pp_hv ctx = function
106+
| HvInt n -> emit ctx (string_of_int n)
107+
| HvFloat f -> emit ctx (Printf.sprintf "%g" f)
108+
| HvStr s -> emit ctx (Printf.sprintf "%S" s)
109+
| HvBool b -> emit ctx (if b then "true" else "false")
110+
| HvUn (op, a) ->
111+
emit ctx "("; emit ctx (match op with HvNeg -> "-" | HvNot -> "!");
112+
pp_hv ctx a; emit ctx ")"
113+
| HvBin (op, a, b) ->
114+
emit ctx "("; pp_hv ctx a;
115+
emit ctx (match op with
116+
| HvAdd -> " + " | HvSub -> " - " | HvMul -> " * " | HvDiv -> " / "
117+
| HvMod -> " % " | HvEq -> " == " | HvNe -> " != " | HvLt -> " < "
118+
| HvLe -> " <= " | HvGt -> " > " | HvGe -> " >= "
119+
| HvAnd -> " && " | HvOr -> " || ");
120+
pp_hv ctx b; emit ctx ")"
121+
| HvIf (c, t, e) ->
122+
emit ctx "(if "; pp_hv ctx c; emit ctx " then "; pp_hv ctx t;
123+
emit ctx " else "; pp_hv ctx e; emit ctx ")"
124+
103125
let rec pp_expr ctx = function
104126
| Match (scrut, arms) ->
105127
emit ctx "match ";
@@ -276,6 +298,9 @@ let rec pp_expr ctx = function
276298
emit ctx " yield strands ";
277299
pp_strand_list ctx w.weave_outputs
278300

301+
| AddBlock he ->
302+
emit ctx "add{ "; pp_hv ctx he; emit ctx " }"
303+
279304
| Warrant (k, c, ev) ->
280305
emit ctx "warrant["; emit ctx (string_of_int k); emit ctx "](";
281306
pp_expr ctx c; emit ctx ", "; pp_expr ctx ev; emit ctx ")"

0 commit comments

Comments
 (0)