Skip to content

Commit 40e917d

Browse files
feat(tg-11): surface syntax for epistemic types + fix a second statement-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>
1 parent cf56397 commit 40e917d

8 files changed

Lines changed: 150 additions & 4 deletions

File tree

compiler/bin/main.ml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,9 @@ let dump_tokens (filename : string) : unit =
187187
| FST -> print_string "FST"
188188
| SND -> print_string "SND"
189189
| ECHOADD -> print_string "ECHOADD"
190-
| ECHOEQ -> print_string "ECHOEQ");
190+
| ECHOEQ -> print_string "ECHOEQ"
191+
| WARRANT -> print_string "WARRANT"
192+
| EVIDENCE -> print_string "EVIDENCE");
191193
print_newline ();
192194
if tok <> EOF then loop ()
193195
in

compiler/lib/check.ml

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,25 @@ let parse_with_recovery (source : string) : Ast.program * diag list =
3535
lexbuf.Lexing.lex_curr_p <-
3636
{ lexbuf.Lexing.lex_curr_p with Lexing.pos_lnum = 1 };
3737
let diags = ref [] in
38-
let stmts = ref [] in
38+
(* Accumulate SEGMENTS (each a statement list from one parser run), newest
39+
first, and flatten in order at the end.
40+
41+
This is the SECOND copy of this bug. bin/main.ml had the identical
42+
`stmts := prog @ !stmts` followed by `List.rev` on the flattened list —
43+
correct for an accumulator built by prepending single items (as `diags`
44+
is) but wrong when whole segments are prepended, so every program came out
45+
with its statements REVERSED. That copy was fixed in #95; this one feeds
46+
`--check` AND the LSP, so both have been analysing reversed programs:
47+
`def w = ...` / `def tok = f(w)` reported "In definition 'tok': ... got
48+
Word[0]" because `tok` was checked before `w` was refined past its
49+
placeholder. The test suites missed it — they call Parser.program
50+
directly. *)
51+
let segments = ref [] in
3952
let at_eof = ref false in
4053
while not !at_eof do
4154
(try
4255
let prog = Parser.program Lexer.token lexbuf in
43-
stmts := prog @ !stmts;
56+
segments := prog :: !segments;
4457
at_eof := true
4558
with
4659
| Lexer.Lexer_error msg ->
@@ -56,7 +69,8 @@ let parse_with_recovery (source : string) : Ast.program * diag list =
5669
message = "Parse error: unexpected token" } :: !diags;
5770
at_eof := synchronize lexbuf)
5871
done;
59-
(List.rev !stmts, List.rev !diags)
72+
(* Flatten oldest-segment-first, preserving order WITHIN each segment. *)
73+
(List.concat (List.rev !segments), List.rev !diags)
6074

6175
(* The full diagnostic set for a source string: parse diagnostics followed by
6276
type-checker diagnostics. This is exactly the set of failures that would

compiler/lib/lexer.mll

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@
5454
| "snd" -> SND
5555
| "echoAdd" -> ECHOADD
5656
| "echoEq" -> ECHOEQ
57+
(* Epistemic forms (TG-11). `evidence` is the ONLY elimination — there is
58+
deliberately no keyword that extracts the claim from a warrant. *)
59+
| "warrant" -> WARRANT
60+
| "evidence" -> EVIDENCE
5761
| "jones" -> JONES
5862
| "alexander" -> ALEXANDER
5963
| "homfly" -> HOMFLY

compiler/lib/parser.mly

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434

3535
(* Echo / product forms — surface syntax mirrors pretty.ml output *)
3636
%token ECHOCLOSE LOWER RESIDUE PAIR FST SND ECHOADD ECHOEQ
37+
%token WARRANT EVIDENCE
3738

3839
(* Invariant names *)
3940
%token JONES ALEXANDER HOMFLY KAUFFMAN WRITHE LINKING
@@ -290,6 +291,17 @@ unary_expr:
290291
{ EchoAdd (e1, e2) }
291292
| ECHOEQ LPAREN e1 = expr COMMA e2 = expr RPAREN
292293
{ EchoEq (e1, e2) }
294+
(* ---- Epistemic (TG-11) ----
295+
`warrant[k](claim, evidence)` — at standpoint k, evidence purporting to
296+
support claim. The standpoint is bracketed like a braid index because it
297+
is an index, not an operand.
298+
`evidence(e)` is the sole elimination: it yields the TOKEN. There is no
299+
surface form that extracts the claim, because there is no such rule —
300+
see epi_only_yields_evidence in proofs/Tangle.lean. *)
301+
| WARRANT LBRACKET k = INT RBRACKET LPAREN c = expr COMMA ev = expr RPAREN
302+
{ Warrant (k, c, ev) }
303+
| EVIDENCE LPAREN e = expr RPAREN
304+
{ Evidence e }
293305
| t = twist_expr { t }
294306
| MINUS e = primary_expr { UnaryOp (Neg, e) }
295307
| e = primary_expr { e }

compiler/test/test_check.ml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,24 @@ let () =
7272
test "a single type error yields exactly one diagnostic (no duplicate)" (fun () ->
7373
let ds = check_source "def bad = braid[s1] + 3\n" in
7474
List.length (List.filter (fun d -> d.level = Error) ds) = 1);
75+
(* Statement ORDER through the recovering parser. This path (check.ml) is
76+
what --check and the LSP use, and it reversed every program: the copy in
77+
bin/main.ml was fixed in #95, this one was not. A definition referring to
78+
an earlier definition was therefore checked BEFORE that definition was
79+
refined past its Word[0] placeholder. *)
80+
test "cross-definition reference typechecks (statement order)" (fun () ->
81+
not (has_error (check_source
82+
"def a = braid[s1]\ndef b = a . a\n")));
83+
84+
test "echo cross-reference typechecks" (fun () ->
85+
(* Was broken: "residue requires Echo[_, _], got Word[0]". *)
86+
not (has_error (check_source
87+
"def e = echoClose(braid[s1])\ndef r = residue(e)\n")));
88+
89+
test "epistemic cross-reference typechecks" (fun () ->
90+
not (has_error (check_source
91+
"def w = warrant[0](42, braid[s1])\ndef tok = evidence(w)\n")));
92+
7593
test "format_diag is tab-separated with 4 fields" (fun () ->
7694
let line = format_diag { level = Error; line = 3; col = 5; message = "boom" } in
7795
String.split_on_char '\t' line = ["ERROR"; "3"; "5"; "boom"]);

compiler/test/test_parser.ml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,51 @@ let test_weave_blocks () =
210210
let p2 = parse_ok printed in
211211
assert_eq "pretty then re-parse yields the same AST" p1 p2);
212212

213+
(* TG-11 surface syntax: warrant[k](claim, evidence) and evidence(e). *)
214+
test "TG-11 warrant parses with a bracketed standpoint" (fun () ->
215+
let prog = parse_ok "def w = warrant[3](42, braid[s1])" in
216+
match prog with
217+
| [Definition d] ->
218+
(match d.def_body with
219+
| Warrant (3, IntLit 42, BraidLit _) -> ()
220+
| _ -> failwith "expected Warrant with standpoint 3")
221+
| _ -> failwith "expected a single Definition");
222+
223+
test "TG-11 evidence parses" (fun () ->
224+
let prog = parse_ok "def t = evidence(w)" in
225+
match prog with
226+
| [Definition d] ->
227+
(match d.def_body with
228+
| Evidence (Var "w") -> ()
229+
| _ -> failwith "expected Evidence")
230+
| _ -> failwith "expected a single Definition");
231+
232+
test "TG-11 there is NO surface form extracting the claim" (fun () ->
233+
(* Non-factivity reaches the syntax. `evidence` is a KEYWORD and becomes
234+
the Evidence form; `claim` is not a keyword, so `claim(w)` is just an
235+
ordinary call to an undefined function — it can never be an elimination
236+
the proofs forbid. If a `Claim` form ever appears here, someone has
237+
added a rule that contradicts epi_only_yields_evidence. *)
238+
(match parse "def c = claim(w)" with
239+
| Some [Definition d] ->
240+
(match d.def_body with
241+
| Call ("claim", _) -> () (* plain call — correct *)
242+
| _ -> failwith "claim(w) must parse as an ordinary Call, not a form")
243+
| _ -> failwith "expected claim(w) to parse as a call");
244+
(* Whereas `evidence` IS a form: *)
245+
match parse "def c = evidence(w)" with
246+
| Some [Definition d] ->
247+
(match d.def_body with
248+
| Evidence _ -> ()
249+
| _ -> failwith "evidence(w) must parse as the Evidence form")
250+
| _ -> failwith "expected evidence(w) to parse");
251+
252+
test "TG-11 warrant round-trips (TG-4 property)" (fun () ->
253+
let src = "def w = warrant[0](42, braid[s1])" in
254+
let p1 = parse_ok src in
255+
let p2 = parse_ok (Tangle.Pretty.program_to_string p1) in
256+
assert_eq "pretty then re-parse" p1 p2);
257+
213258
test "#88 weave is still valid as a statement" (fun () ->
214259
(* Regression guard: the change is additive. *)
215260
let prog = parse_ok

examples/epistemic.tangle

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
# epistemic.tangle — warranted claims (TG-11)
3+
#
4+
# A warrant is EVIDENCE FOR a claim, not the claim itself. Holding one does
5+
# not give you the thing warranted. That is the difference between knowing
6+
# something and having a reason to believe it.
7+
#
8+
# Demonstrates:
9+
# - warrant[k](claim, evidence) — at standpoint k
10+
# - evidence(w) — the ONLY elimination
11+
# - non-factivity: nothing extracts the claim
12+
13+
# --- A warrant at standpoint 0 ---
14+
15+
# "At standpoint 0, this braid is my evidence for the claim 42."
16+
def w = warrant[0](42, braid[s1, s2])
17+
18+
# The evidence can be recovered.
19+
def token = evidence(w)
20+
assert token == braid[s1, s2]
21+
22+
# --- The claim is NOT recoverable ---
23+
#
24+
# There is no `claim(w)`. It is not a language form, and no typing rule
25+
# produces the claim's type from a warrant. See epi_only_yields_evidence
26+
# and epi_claim_is_opaque in proofs/Tangle.lean.
27+
#
28+
# If it were extractable, anything anyone attested would become true by
29+
# fiat — which is exactly the bug you do not want in a provenance system.
30+
31+
# --- Standpoints are distinct positions ---
32+
#
33+
# The same evidence for the same claim, held at different standpoints, is a
34+
# different warrant. Who holds it is part of what it is.
35+
36+
def w1 = warrant[1](42, braid[s1, s2])
37+
def w2 = warrant[2](42, braid[s1, s2])
38+
39+
# Both still yield their evidence.
40+
assert evidence(w1) == braid[s1, s2]
41+
assert evidence(w2) == braid[s1, s2]
42+
43+
# --- Epistemic composes over echo ---
44+
#
45+
# A warrant whose evidence is an echo: standpoint 1 has access to an echo of
46+
# a braid. The two modalities nest — echo grades irrecoverability, epi
47+
# indexes standpoints. Neither absorbs the other.
48+
49+
def we = warrant[1](0, echoClose(braid[s1]))
50+
assert residue(evidence(we)) == braid[s1]

scripts/check-corpus.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ fi
4444
# Examples that must fully TYPECHECK and EVALUATE (all their asserts hold).
4545
EXAMPLES_MUST_RUN=(
4646
braids_as_data.tangle
47+
epistemic.tangle # TG-11 — warranted claims, non-factive
4748
compositional_pd.tangle
4849
echo_pd.tangle
4950
isotopy.tangle # the TG-7 witness — see header

0 commit comments

Comments
 (0)