Skip to main content

rml/
lib.rs

1// RML — minimal relative meta-logic over LiNo (Links Notation)
2// Supports many-valued logics from unary (1-valued) through continuous probabilistic (∞-valued).
3// See: https://en.wikipedia.org/wiki/Many-valued_logic
4//
5// - Uses official links-notation crate to parse LiNo text into links
6// - Terms are defined via (x: x is x)
7// - Probabilities are assigned ONLY via: ((<expr>) has probability <p>)
8// - Redefinable ops: (=: ...), (!=: not =), (and: avg|min|max|product|probabilistic_sum), (or: ...), (not: ...), (both: ...), (neither: ...)
9// - Range: (range: 0 1) for [0,1] or (range: -1 1) for [-1,1] (balanced/symmetric)
10// - Valence: (valence: N) to restrict truth values to N discrete levels (N=2 → Boolean, N=3 → ternary, etc.)
11// - Query: (? <expr>)
12
13use std::collections::{BTreeMap, HashMap, HashSet};
14use std::fmt;
15use std::fs;
16use std::io::{Read, Write};
17use std::panic::{catch_unwind, AssertUnwindSafe};
18use std::path::{Path, PathBuf};
19use std::process::{Command, Stdio};
20use std::thread::{self, sleep, JoinHandle};
21use std::time::{Duration, Instant};
22
23pub mod lean_export;
24pub use lean_export::{export_lean, lean_ident, LeanExportResult};
25
26// ========== Structured Diagnostics ==========
27// Every parser/evaluator error is reported as a `Diagnostic` with an error
28// code, human-readable message, and source span (file/line/col, 1-based).
29// See `docs/DIAGNOSTICS.md` for the full code list.
30
31/// A source span: 1-based `line`/`col`, optional file path, and a `length`
32/// of the offending region (used to render carets in the CLI).
33#[derive(Debug, Clone, PartialEq)]
34pub struct Span {
35    pub file: Option<String>,
36    pub line: usize,
37    pub col: usize,
38    pub length: usize,
39}
40
41impl Span {
42    pub fn new(file: Option<String>, line: usize, col: usize, length: usize) -> Self {
43        Self {
44            file,
45            line,
46            col,
47            length,
48        }
49    }
50
51    pub fn unknown() -> Self {
52        Self {
53            file: None,
54            line: 1,
55            col: 1,
56            length: 0,
57        }
58    }
59}
60
61/// A single diagnostic emitted by parser, evaluator, or type checker.
62#[derive(Debug, Clone, PartialEq)]
63pub struct Diagnostic {
64    pub code: String,
65    pub message: String,
66    pub span: Span,
67}
68
69impl Diagnostic {
70    pub fn new(code: &str, message: impl Into<String>, span: Span) -> Self {
71        Self {
72            code: code.to_string(),
73            message: message.into(),
74            span,
75        }
76    }
77}
78
79/// Result of `evaluate(src)`: a list of query results (numeric or type) plus
80/// any diagnostics emitted while parsing/evaluating. When tracing is enabled
81/// via `evaluate_with_options`, `trace` carries the deterministic sequence of
82/// `TraceEvent` values recorded during evaluation; otherwise it is empty.
83/// When proof production is enabled (via `EvaluateOptions::with_proofs` or
84/// any per-query `(? expr with proof)` keyword), `proofs[i]` carries a
85/// derivation tree for `results[i]`; bare queries that did not request a
86/// witness get `None` so the vec stays index-aligned with `results`.
87/// Mirrors the JavaScript `{results, diagnostics, trace, proofs}` shape.
88#[derive(Debug, Clone, Default)]
89pub struct EvaluateResult {
90    pub results: Vec<RunResult>,
91    pub diagnostics: Vec<Diagnostic>,
92    pub trace: Vec<TraceEvent>,
93    pub proofs: Vec<Option<Node>>,
94    /// Equality-layer provenance (issue #97). For every query that is a
95    /// direct equality (`(? (L = R))`), records which of the four equality
96    /// layers fired: `assigned-equality`, `structural-equality`,
97    /// `definitional-equality`, or `numeric-equality`. Non-equality queries
98    /// get `None`. The vec is empty when no equality query was observed,
99    /// matching JavaScript's lazy `out.provenance` shape.
100    pub provenance: Vec<Option<String>>,
101}
102
103/// Options for `evaluate_with_options` — bundles environment settings with
104/// runtime flags like `trace` and `with_proofs`. Keeps `evaluate()`
105/// backwards compatible.
106#[derive(Debug, Clone, Default)]
107pub struct EvaluateOptions {
108    pub env: Option<EnvOptions>,
109    pub trace: bool,
110    /// When true, every query result is accompanied by a derivation tree at
111    /// the same index in `EvaluateResult.proofs`. The inline
112    /// `(? expr with proof)` keyword pair opts in per-query without flipping
113    /// this global flag.
114    pub with_proofs: bool,
115}
116
117// ========== Trace events ==========
118// When `evaluate` is called with `EvaluateOptions { trace: true }` the
119// evaluator records a deterministic sequence of `TraceEvent` values describing
120// operator resolutions, assignment lookups, and reduction steps. The CLI's
121// `--trace` flag prints each one as `[span <file>:<line>:<col>] <kind> <details>`.
122// Mirrors `TraceEvent` / `formatTraceEvent` in `js/src/rml-links.mjs`.
123
124/// A single trace event emitted by the evaluator.
125#[derive(Debug, Clone, PartialEq)]
126pub struct TraceEvent {
127    pub kind: String,
128    pub detail: String,
129    pub span: Span,
130}
131
132impl TraceEvent {
133    pub fn new(kind: &str, detail: impl Into<String>, span: Span) -> Self {
134        Self {
135            kind: kind.to_string(),
136            detail: detail.into(),
137            span,
138        }
139    }
140}
141
142/// Format a trace event as `[span <file>:<line>:<col>] <kind> <details>`.
143pub fn format_trace_event(event: &TraceEvent) -> String {
144    let file = event.span.file.as_deref().unwrap_or("<input>");
145    format!(
146        "[span {}:{}:{}] {} {}",
147        file, event.span.line, event.span.col, event.kind, event.detail
148    )
149}
150
151/// Format a numeric value for trace output — strips trailing zeros so
152/// `1.000000` reads as `1` and `0.5` stays `0.5`. Mirrors `formatTraceValue`
153/// in the JavaScript implementation so cross-runtime traces match exactly.
154pub fn format_trace_value(v: f64) -> String {
155    if !v.is_finite() {
156        return v.to_string();
157    }
158    let rounded = format!("{:.6}", v);
159    // Trim trailing zeros and possibly the decimal point.
160    let trimmed = rounded.trim_end_matches('0').trim_end_matches('.');
161    if trimmed.is_empty() || trimmed == "-" {
162        "0".to_string()
163    } else {
164        trimmed.to_string()
165    }
166}
167
168/// Format a diagnostic for human-readable CLI output:
169///     `<file>:<line>:<col>: <CODE>: <message>`
170///         `<source line>`
171///         `^`
172pub fn format_diagnostic(diag: &Diagnostic, source: Option<&str>) -> String {
173    let file = diag.span.file.as_deref().unwrap_or("<input>");
174    let mut out = format!(
175        "{}:{}:{}: {}: {}",
176        file, diag.span.line, diag.span.col, diag.code, diag.message
177    );
178    if let Some(src) = source {
179        let lines: Vec<&str> = src.split('\n').collect();
180        if diag.span.line >= 1 && diag.span.line <= lines.len() {
181            let line_text = lines[diag.span.line - 1];
182            out.push('\n');
183            out.push_str(line_text);
184            out.push('\n');
185            let pad = diag.span.col.saturating_sub(1);
186            let caret_count = diag.span.length.max(1);
187            out.push_str(&" ".repeat(pad));
188            out.push_str(&"^".repeat(caret_count));
189        }
190    }
191    out
192}
193
194/// Compute (line, col) source positions for every top-level link in `text`.
195/// Mirrors `compute_form_spans` in the JavaScript implementation.
196///
197/// A "top-level link" is a parenthesized form not nested inside another; the
198/// position is the 1-based line/col of its opening `(`. Full-line `# ...`
199/// comments and inline `# ...` comments after a closing paren plus whitespace
200/// are skipped so that parens inside a comment don't disturb the depth
201/// counter.
202pub fn compute_form_spans(text: &str, file: Option<&str>) -> Vec<Span> {
203    let mut spans = Vec::new();
204    let mut depth: i32 = 0;
205    let mut line: usize = 1;
206    let mut col: usize = 1;
207    let mut pending_start: Option<(usize, usize)> = None;
208    let mut in_line_comment = false;
209    let mut line_start_idx: usize = 0;
210    let mut last_closing_depth_zero_col: i32 = -1;
211    let mut saw_ws_after_close = false;
212    let bytes = text.as_bytes();
213    for (off, &b) in bytes.iter().enumerate() {
214        let ch = b as char;
215        if ch == '\n' {
216            in_line_comment = false;
217            line += 1;
218            col = 1;
219            line_start_idx = off + 1;
220            last_closing_depth_zero_col = -1;
221            saw_ws_after_close = false;
222            continue;
223        }
224        if in_line_comment {
225            col += 1;
226            continue;
227        }
228        if ch == '#' && depth == 0 {
229            // Full-line comment: line so far is all whitespace.
230            let line_so_far = &text[line_start_idx..off];
231            if line_so_far.chars().all(|c| c == ' ' || c == '\t') {
232                in_line_comment = true;
233                col += 1;
234                continue;
235            }
236            // Inline comment after `)` + whitespace: discard rest of line.
237            if last_closing_depth_zero_col >= 0 && saw_ws_after_close {
238                in_line_comment = true;
239                col += 1;
240                continue;
241            }
242        }
243        if ch == '(' {
244            if depth == 0 {
245                pending_start = Some((line, col));
246            }
247            depth += 1;
248            saw_ws_after_close = false;
249        } else if ch == ')' {
250            depth -= 1;
251            if depth == 0 {
252                if let Some((sl, sc)) = pending_start.take() {
253                    spans.push(Span::new(file.map(|s| s.to_string()), sl, sc, 1));
254                }
255                last_closing_depth_zero_col = col as i32;
256                saw_ws_after_close = false;
257            }
258        } else if ch == ' ' || ch == '\t' {
259            if last_closing_depth_zero_col >= 0 {
260                saw_ws_after_close = true;
261            }
262        } else {
263            // Any other character resets the inline-comment-eligible state.
264            last_closing_depth_zero_col = -1;
265            saw_ws_after_close = false;
266        }
267        col += 1;
268    }
269    spans
270}
271
272// ========== LiNo Parser ==========
273// Uses the official links-notation crate for parsing LiNo text.
274// See: https://github.com/link-foundation/links-notation
275
276// Find the index of an inline comment marker `#` that follows a `)` plus
277// whitespace, mirroring the JS regex `(\)[ \t]+)#.*$`.
278fn inline_comment_index(line: &str) -> Option<usize> {
279    let bytes = line.as_bytes();
280    let mut last_close: Option<usize> = None;
281    for (i, b) in bytes.iter().enumerate() {
282        match *b {
283            b')' => last_close = Some(i),
284            b'#' => {
285                if let Some(close_idx) = last_close {
286                    let between = &line[close_idx + 1..i];
287                    if !between.is_empty() && between.chars().all(|c| c == ' ' || c == '\t') {
288                        return Some(i);
289                    }
290                }
291            }
292            _ => {}
293        }
294    }
295    None
296}
297
298/// Parse LiNo text into a vector of link strings (each a top-level parenthesized expression).
299pub fn parse_lino(text: &str) -> Vec<String> {
300    parse_lino_with_errors(text).0
301}
302
303/// Parse LiNo text and return both the parsed links and any error messages from
304/// the underlying parser. Used by `evaluate_inner` to surface E006 diagnostics
305/// for unbalanced/invalid input — mirrors `parseLinoForms` in
306/// `js/src/rml-links.mjs`, which throws and is caught into an E006 diagnostic.
307fn parse_lino_with_errors(text: &str) -> (Vec<String>, Vec<String>) {
308    // Strip both full-line and inline comments (# ...) before parsing —
309    // the LiNo parser doesn't handle them and an inline comment containing a
310    // colon would otherwise be misread as a binding.
311    let stripped: String = text
312        .lines()
313        .map(|line| {
314            let trimmed = line.trim_start();
315            if trimmed.starts_with('#') {
316                String::new()
317            } else if let Some(idx) = inline_comment_index(line) {
318                line[..idx].trim_end().to_string()
319            } else {
320                line.to_string()
321            }
322        })
323        .collect::<Vec<String>>()
324        .join("\n");
325
326    // The links-notation crate treats blank lines as group separators,
327    // so we split the input by blank lines and parse each segment separately.
328    let mut all_links = Vec::new();
329    let mut errors = Vec::new();
330    for segment in stripped.split("\n\n") {
331        let trimmed = segment.trim();
332        if trimmed.is_empty() {
333            continue;
334        }
335        match links_notation::parse_lino_to_links(trimmed) {
336            Ok(links) => {
337                for link in links {
338                    all_links.push(link.to_string());
339                }
340            }
341            Err(e) => {
342                errors.push(format!("{}", e));
343            }
344        }
345    }
346    (all_links, errors)
347}
348
349fn is_literate_lino_path(file: Option<&str>) -> bool {
350    file.map(|path| path.to_ascii_lowercase().ends_with(".lino.md"))
351        .unwrap_or(false)
352}
353
354fn parse_markdown_fence(line: &str) -> Option<(char, usize, &str)> {
355    let trimmed = line.trim_start_matches(|c| c == ' ' || c == '\t');
356    let marker = trimmed.chars().next()?;
357    if marker != '`' && marker != '~' {
358        return None;
359    }
360    let count = trimmed.chars().take_while(|c| *c == marker).count();
361    if count < 3 {
362        return None;
363    }
364    Some((marker, count, &trimmed[count..]))
365}
366
367fn is_closing_markdown_fence(line: &str, marker: char, min_len: usize) -> bool {
368    let Some((found_marker, found_len, rest)) = parse_markdown_fence(line) else {
369        return false;
370    };
371    found_marker == marker
372        && found_len >= min_len
373        && rest.chars().all(|c| c == ' ' || c == '\t')
374}
375
376fn is_lino_fence_info(info: &str) -> bool {
377    info.trim()
378        .split_whitespace()
379        .next()
380        .map(|tag| tag.eq_ignore_ascii_case("lino"))
381        .unwrap_or(false)
382}
383
384/// Extract LiNo code from fenced `lino` blocks in a literate `.lino.md` file.
385///
386/// Non-LiNo prose and other code fences become blank lines so diagnostics keep
387/// the original Markdown line numbers.
388pub fn extract_literate_lino(text: &str) -> String {
389    let mut out = Vec::new();
390    let mut active_fence: Option<(char, usize, bool)> = None;
391    for line in text.split('\n') {
392        if let Some((marker, min_len, include)) = active_fence {
393            if is_closing_markdown_fence(line, marker, min_len) {
394                active_fence = None;
395                out.push(String::new());
396            } else if include {
397                out.push(line.to_string());
398            } else {
399                out.push(String::new());
400            }
401            continue;
402        }
403
404        if let Some((marker, len, info)) = parse_markdown_fence(line) {
405            active_fence = Some((marker, len, is_lino_fence_info(info)));
406            out.push(String::new());
407        } else {
408            out.push(String::new());
409        }
410    }
411    out.join("\n")
412}
413
414// ========== AST ==========
415
416/// AST node: either a leaf string or a list of child nodes.
417#[derive(Debug, Clone, PartialEq)]
418pub enum Node {
419    Leaf(String),
420    List(Vec<Node>),
421}
422
423impl fmt::Display for Node {
424    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
425        match self {
426            Node::Leaf(s) => write!(f, "{}", s),
427            Node::List(children) => {
428                write!(f, "(")?;
429                for (i, child) in children.iter().enumerate() {
430                    if i > 0 {
431                        write!(f, " ")?;
432                    }
433                    write!(f, "{}", child)?;
434                }
435                write!(f, ")")
436            }
437        }
438    }
439}
440
441// ========== Helpers ==========
442
443/// Tokenize a single link string into tokens (parens and words).
444pub fn tokenize_one(s: &str) -> Vec<String> {
445    let mut s = s.to_string();
446
447    // Strip inline comments (everything after #) but balance parens
448    if let Some(comment_idx) = s.find('#') {
449        s = s[..comment_idx].to_string();
450        // Count unmatched opening parens and add closing parens to balance
451        let mut depth: i32 = 0;
452        for c in s.chars() {
453            if c == '(' {
454                depth += 1;
455            } else if c == ')' {
456                depth -= 1;
457            }
458        }
459        while depth > 0 {
460            s.push(')');
461            depth -= 1;
462        }
463    }
464
465    let mut out = Vec::new();
466    let chars: Vec<char> = s.chars().collect();
467    let mut i = 0;
468
469    while i < chars.len() {
470        let c = chars[i];
471        if c.is_whitespace() {
472            i += 1;
473            continue;
474        }
475        if c == '(' || c == ')' {
476            out.push(c.to_string());
477            i += 1;
478            continue;
479        }
480        let j_start = i;
481        while i < chars.len() && !chars[i].is_whitespace() && chars[i] != '(' && chars[i] != ')' {
482            i += 1;
483        }
484        out.push(chars[j_start..i].iter().collect());
485    }
486    out
487}
488
489/// Parse tokens into an AST node.
490pub fn parse_one(tokens: &[String]) -> Result<Node, String> {
491    let mut i = 0;
492
493    fn read(tokens: &[String], i: &mut usize) -> Result<Node, String> {
494        if *i >= tokens.len() || tokens[*i] != "(" {
495            return Err("expected \"(\"".to_string());
496        }
497        *i += 1;
498        let mut arr = Vec::new();
499        while *i < tokens.len() && tokens[*i] != ")" {
500            if tokens[*i] == "(" {
501                arr.push(read(tokens, i)?);
502            } else {
503                arr.push(Node::Leaf(tokens[*i].clone()));
504                *i += 1;
505            }
506        }
507        if *i >= tokens.len() || tokens[*i] != ")" {
508            return Err("expected \")\"".to_string());
509        }
510        *i += 1;
511        Ok(Node::List(arr))
512    }
513
514    let ast = read(tokens, &mut i)?;
515    if i != tokens.len() {
516        return Err("extra tokens after link".to_string());
517    }
518    Ok(ast)
519}
520
521/// Higher-order abstract syntax (issue #51, D7): rewrite the surface keyword
522/// `forall` to the kernel binder `Pi`. Both forms share identical structure
523/// `(<binder> (Type x) body)`, so the desugarer walks the AST and rewrites
524/// the head leaf in place. Object-language binders are encoded as
525/// host-language `lambda` and `Pi`/`forall`, letting substitution and
526/// capture-avoidance reuse the kernel primitives without a separate
527/// object-level binder representation.
528pub fn desugar_hoas(node: Node) -> Node {
529    match node {
530        Node::Leaf(_) => node,
531        Node::List(children) => {
532            let mapped: Vec<Node> = children.into_iter().map(desugar_hoas).collect();
533            // Rewrite `(forall (T x) body)` → `(Pi (T x) body)` only when the
534            // binder is a list (HOAS synonym). A bare leaf, e.g. `(forall A body)`,
535            // is prenex-polymorphism sugar and must reach `synth`/`is_forall_node` intact.
536            if mapped.len() == 3 {
537                if let Node::Leaf(ref head) = mapped[0] {
538                    if head == "forall" {
539                        if let Node::List(_) = mapped[1] {
540                            let mut rewritten = Vec::with_capacity(3);
541                            rewritten.push(Node::Leaf("Pi".to_string()));
542                            let mut iter = mapped.into_iter();
543                            iter.next();
544                            rewritten.extend(iter);
545                            return Node::List(rewritten);
546                        }
547                    }
548                }
549            }
550            Node::List(mapped)
551        }
552    }
553}
554
555/// Check if a string is numeric (including negative).
556pub fn is_num(s: &str) -> bool {
557    let s = s.trim();
558    if s.is_empty() {
559        return false;
560    }
561    let s = if let Some(stripped) = s.strip_prefix('-') {
562        stripped
563    } else {
564        s
565    };
566    if s.is_empty() {
567        return false;
568    }
569    if let Some(rest) = s.strip_prefix('.') {
570        // .digits
571        !rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit())
572    } else {
573        // digits or digits.digits
574        let parts: Vec<&str> = s.splitn(2, '.').collect();
575        if parts.is_empty() || !parts[0].chars().all(|c| c.is_ascii_digit()) || parts[0].is_empty()
576        {
577            return false;
578        }
579        if parts.len() == 2 {
580            parts[1].chars().all(|c| c.is_ascii_digit())
581        } else {
582            true
583        }
584    }
585}
586
587/// Create a canonical key representation of a node.
588pub fn key_of(node: &Node) -> String {
589    match node {
590        Node::Leaf(s) => s.clone(),
591        Node::List(children) => {
592            let inner: Vec<String> = children.iter().map(key_of).collect();
593            format!("({})", inner.join(" "))
594        }
595    }
596}
597
598fn parse_universe_level_token(token: &str) -> Option<u64> {
599    if token.is_empty() || !token.chars().all(|c| c.is_ascii_digit()) {
600        return None;
601    }
602    if token.len() > 1 && token.starts_with('0') {
603        return None;
604    }
605    token.parse::<u64>().ok()
606}
607
608fn universe_type_key(node: &Node) -> Option<String> {
609    let Node::List(children) = node else {
610        return None;
611    };
612    if children.len() != 2 {
613        return None;
614    }
615    let (Node::Leaf(head), Node::Leaf(level_s)) = (&children[0], &children[1]) else {
616        return None;
617    };
618    if head != "Type" {
619        return None;
620    }
621    let level = parse_universe_level_token(level_s)?;
622    Some(format!("(Type {})", level.checked_add(1)?))
623}
624
625fn infer_type_key(node: &Node, env: &mut Env) -> Option<String> {
626    let key = match node {
627        Node::Leaf(s) => s.clone(),
628        other => key_of(other),
629    };
630    if let Some(recorded) = env.get_type(&key) {
631        return Some(recorded.clone());
632    }
633    if let Some(type_key) = universe_type_key(node) {
634        env.set_type(&key, &type_key);
635        return Some(type_key);
636    }
637    None
638}
639
640/// Check structural equality of two nodes.
641pub fn is_structurally_same(a: &Node, b: &Node) -> bool {
642    match (a, b) {
643        (Node::Leaf(sa), Node::Leaf(sb)) => sa == sb,
644        (Node::List(la), Node::List(lb)) => {
645            la.len() == lb.len()
646                && la
647                    .iter()
648                    .zip(lb.iter())
649                    .all(|(x, y)| is_structurally_same(x, y))
650        }
651        _ => false,
652    }
653}
654
655// ========== Decimal-precision arithmetic ==========
656// Round to at most DECIMAL_PRECISION significant decimal places to eliminate
657// IEEE-754 floating-point artefacts (e.g. 0.1+0.2 → 0.3, not 0.30000000000000004).
658const DECIMAL_PRECISION: i32 = 12;
659
660pub fn dec_round(x: f64) -> f64 {
661    if !x.is_finite() {
662        return x;
663    }
664    let factor = 10f64.powi(DECIMAL_PRECISION);
665    (x * factor).round() / factor
666}
667
668// ========== Quantization ==========
669
670/// Quantize a value to N discrete levels in range [lo, hi].
671/// For N=2 (Boolean): levels are {lo, hi}
672/// For N=3 (ternary): levels are {lo, mid, hi}
673/// For N<2 (continuous/unary): no quantization
674/// See: <https://en.wikipedia.org/wiki/Many-valued_logic>
675pub fn quantize(x: f64, valence: u32, lo: f64, hi: f64) -> f64 {
676    if valence < 2 {
677        return x; // unary or continuous — no quantization
678    }
679    let step = (hi - lo) / (valence as f64 - 1.0);
680    let level = ((x - lo) / step).round();
681    let level = level.max(0.0).min(valence as f64 - 1.0);
682    lo + level * step
683}
684
685// ========== Aggregator Types ==========
686
687/// Supported aggregator types for AND/OR operators.
688#[derive(Debug, Clone, Copy, PartialEq)]
689pub enum Aggregator {
690    Avg,
691    Min,
692    Max,
693    Prod,
694    Ps, // Probabilistic sum: 1 - ∏(1-xi)
695}
696
697impl Aggregator {
698    pub fn apply(&self, xs: &[f64], lo: f64) -> f64 {
699        if xs.is_empty() {
700            return lo;
701        }
702        match self {
703            Aggregator::Avg => xs.iter().sum::<f64>() / xs.len() as f64,
704            Aggregator::Min => xs.iter().copied().fold(f64::INFINITY, f64::min),
705            Aggregator::Max => xs.iter().copied().fold(f64::NEG_INFINITY, f64::max),
706            Aggregator::Prod => xs.iter().copied().fold(1.0, |a, b| a * b),
707            Aggregator::Ps => 1.0 - xs.iter().copied().fold(1.0, |a, b| a * (1.0 - b)),
708        }
709    }
710
711    pub fn from_name(name: &str) -> Option<Self> {
712        match name {
713            "avg" => Some(Aggregator::Avg),
714            "min" => Some(Aggregator::Min),
715            "max" => Some(Aggregator::Max),
716            "product" | "prod" => Some(Aggregator::Prod),
717            "probabilistic_sum" | "ps" => Some(Aggregator::Ps),
718            _ => None,
719        }
720    }
721}
722
723/// Resolve a truth-table token (input or output) to its numeric value.
724/// Numeric literals stay numeric; symbolic constants flow through
725/// `env.symbol_prob` so user-declared truth constants like `(true: 1)` or
726/// `(unknown: 0.5)` are honoured. Returns `None` when the token cannot
727/// be resolved so the caller can skip the row.
728fn resolve_truth_table_value(env: &Env, tok: &str) -> Option<f64> {
729    if let Ok(num) = tok.parse::<f64>() {
730        if num.is_finite() {
731            return Some(num);
732        }
733    }
734    env.symbol_prob.get(tok).copied()
735}
736
737fn truth_table_key(values: &[f64]) -> String {
738    values
739        .iter()
740        .map(|v| format!("{:.15}", v))
741        .collect::<Vec<_>>()
742        .join("\u{1}")
743}
744
745fn resolved_carrier_values(env: &Env, foundation: &FoundationDescriptor) -> Option<Vec<f64>> {
746    if !foundation.strict_carrier || foundation.carrier.is_empty() {
747        return None;
748    }
749    let mut values = Vec::new();
750    let mut seen = HashSet::new();
751    for tok in &foundation.carrier {
752        let value = resolve_truth_table_value(env, tok)?;
753        let key = truth_table_key(&[value]);
754        if seen.insert(key) {
755            values.push(value);
756        }
757    }
758    if values.is_empty() {
759        None
760    } else {
761        Some(values)
762    }
763}
764
765fn truth_table_rows_complete_for_carrier(
766    env: &Env,
767    rows: &[TruthTableRow],
768    foundation: &FoundationDescriptor,
769) -> bool {
770    let carrier = match resolved_carrier_values(env, foundation) {
771        Some(values) => values,
772        None => return false,
773    };
774    let mut arity: Option<usize> = None;
775    let mut seen_rows: HashSet<String> = HashSet::new();
776    for row in rows {
777        if arity.is_none() {
778            arity = Some(row.inputs.len());
779        }
780        if Some(row.inputs.len()) != arity {
781            return false;
782        }
783        let mut inputs = Vec::with_capacity(row.inputs.len());
784        for tok in &row.inputs {
785            match resolve_truth_table_value(env, tok) {
786                Some(v) => inputs.push(v),
787                None => return false,
788            }
789        }
790        if resolve_truth_table_value(env, &row.output).is_none() {
791            return false;
792        }
793        seen_rows.insert(truth_table_key(&inputs));
794    }
795    let arity = match arity {
796        Some(a) => a,
797        None => return false,
798    };
799    let required = carrier.len().pow(arity as u32);
800    if seen_rows.len() < required {
801        return false;
802    }
803    fn visit(
804        carrier: &[f64],
805        seen_rows: &HashSet<String>,
806        arity: usize,
807        prefix: &mut Vec<f64>,
808    ) -> bool {
809        if prefix.len() == arity {
810            return seen_rows.contains(&truth_table_key(prefix));
811        }
812        for value in carrier {
813            prefix.push(*value);
814            if !visit(carrier, seen_rows, arity, prefix) {
815                prefix.pop();
816                return false;
817            }
818            prefix.pop();
819        }
820        true
821    }
822    visit(&carrier, &seen_rows, arity, &mut Vec::new())
823}
824
825fn truth_table_fallback_dependencies(
826    env: &Env,
827    op_name: &str,
828    previous_impl: Option<&ActiveImplementationDescriptor>,
829) -> Vec<String> {
830    let mut deps = Vec::new();
831    if let Some(implementation) = previous_impl {
832        deps.extend(implementation.depends_on.iter().cloned());
833    } else if let Some(rc) = env.root_constructs.get(op_name) {
834        deps.extend(rc.depends_on.iter().cloned());
835    }
836    deps.push("truth-table-fallback".to_string());
837    let mut seen = HashSet::new();
838    deps.into_iter()
839        .filter(|dep| seen.insert(dep.clone()))
840        .collect()
841}
842
843// ========== Operator ==========
844
845/// Operator types supported by the environment.
846#[derive(Debug, Clone)]
847pub enum Op {
848    /// Negation: mirrors around midpoint. not(x) = hi - (x - lo)
849    Not,
850    /// Aggregator-based operator (for and/or).
851    Agg(Aggregator),
852    /// Equality operator: checks assigned probability or structural equality.
853    Eq,
854    /// Inequality: not(eq(...))
855    Neq,
856    /// Composition: outer(inner(...))
857    Compose {
858        outer: String,
859        inner: String,
860    },
861    /// Arithmetic: +, -, *, / (decimal-precision)
862    Add,
863    Sub,
864    Mul,
865    Div,
866    /// Numeric comparisons: <, <=
867    Less,
868    LessOrEqual,
869    /// Links-defined finite truth table (issue #97, Section 3 of
870    /// netkeep80's punch-list). When invoked the evaluator looks up the
871    /// first row whose inputs match `xs` (±1e-12 tolerance) and returns
872    /// the row's output. If no row matches, the call delegates to
873    /// `fallback` so partial tables overlay cleanly onto the host
874    /// default. `rows` carries values pre-resolved against
875    /// `env.symbol_prob` at activation time.
876    TruthTable {
877        rows: Vec<TruthTableEntry>,
878        fallback: Option<Box<Op>>,
879    },
880}
881
882/// One resolved row inside an `Op::TruthTable`. Inputs and output are
883/// stored as `f64` after symbolic constants have been looked up.
884#[derive(Debug, Clone, PartialEq)]
885pub struct TruthTableEntry {
886    pub inputs: Vec<f64>,
887    pub output: f64,
888}
889
890// ========== Environment ==========
891
892/// Options for creating an Env.
893#[derive(Debug, Clone)]
894pub struct EnvOptions {
895    pub lo: f64,
896    pub hi: f64,
897    pub valence: u32,
898}
899
900impl Default for EnvOptions {
901    fn default() -> Self {
902        Self {
903            lo: 0.0,
904            hi: 1.0,
905            valence: 0,
906        }
907    }
908}
909
910/// Options for definitional equality / convertibility checks.
911#[derive(Debug, Clone, Copy, Default)]
912pub struct ConvertOptions {
913    /// Enable eta-contraction, e.g. `(lambda (A x) (apply f x)) == f`
914    /// when `x` is not free in `f`.
915    pub eta: bool,
916}
917
918/// A stored lambda definition (param name, param type, body).
919#[derive(Debug, Clone)]
920pub struct Lambda {
921    pub param: String,
922    pub param_type: String,
923    pub body: Node,
924}
925
926/// A pre-evaluation template declaration (issue #59).
927/// `(template (<name> <param>...) <body>)` records a reusable link shape;
928/// later `(<name> arg...)` uses are expanded before they reach `eval_node`.
929#[derive(Debug, Clone)]
930pub struct TemplateDecl {
931    pub name: String,
932    pub params: Vec<String>,
933    pub body: Node,
934}
935
936/// A domain plugin receives the body of `(domain <name> ...)` and mutates the
937/// evaluator environment with any decisions it can certify.
938pub type DomainPluginFn = fn(&[Node], &mut Env) -> Result<(), String>;
939
940/// Decision record produced by the built-in automatic-sequences plugin.
941#[derive(Debug, Clone, PartialEq)]
942pub struct AutomaticSequenceDecision {
943    pub theorem: String,
944    pub value: bool,
945    pub method: String,
946    pub certificate: Node,
947}
948
949/// The evaluation environment: holds terms, assignments, operators, and range/valence config.
950pub struct Env {
951    pub terms: HashSet<String>,
952    pub assign: HashMap<String, f64>,
953    pub symbol_prob: HashMap<String, f64>,
954    pub lo: f64,
955    pub hi: f64,
956    pub valence: u32,
957    pub ops: HashMap<String, Op>,
958    pub types: HashMap<String, String>,
959    pub lambdas: HashMap<String, Lambda>,
960    pub templates: HashMap<String, TemplateDecl>,
961    /// Tracing state. When `trace_enabled` is true, key evaluation events
962    /// (operator resolutions, assignment lookups, top-level reductions) are
963    /// appended to `trace_events`. The current top-level form span is stashed
964    /// on the Env so leaf hooks can attach a location without threading spans
965    /// through every helper. Mirrors the `_tracer`/`_currentSpan` design in
966    /// `js/src/rml-links.mjs`.
967    pub trace_enabled: bool,
968    pub trace_events: Vec<TraceEvent>,
969    pub current_span: Option<Span>,
970    pub default_span: Span,
971    /// Namespace state (issue #34): a file can declare `(namespace foo)`, which
972    /// prefixes every name it subsequently introduces with `foo.`. Imports can
973    /// be aliased via `(import "x.lino" as a)`, which records `a` -> the
974    /// imported file's declared namespace so `a.name` resolves to that name.
975    /// `imported` tracks names that came from an import (not declared in the
976    /// importing file) so we can emit a shadowing warning (E008) when a later
977    /// top-level definition rebinds them.
978    pub namespace: Option<String>,
979    pub aliases: HashMap<String, String>,
980    pub imported: HashSet<String>,
981    pub shadow_diagnostics: Vec<Diagnostic>,
982    pub file_namespaces: HashMap<PathBuf, String>,
983    /// Mode declarations (issue #43, D15): each relation may declare an
984    /// argument mode pattern via `(mode <name> +input -output ...)`. The
985    /// map records the per-argument flag list used by the call-site checker
986    /// to reject mode mismatches.
987    pub modes: HashMap<String, Vec<ModeFlag>>,
988    /// Relation declarations (issue #44, D12): the clause list for each
989    /// declared relation, keyed by relation name. Each clause is the
990    /// original AST list `(name arg1 arg2 ... result)`. The totality
991    /// checker reads these clauses to verify structural decrease on
992    /// recursive calls.
993    pub relations: HashMap<String, Vec<Node>>,
994    /// World declarations (issue #54, D16): each relation may declare a
995    /// list of constants permitted to appear free in its arguments via
996    /// `(world <name> (<const1> <const2> ...))`. The world checker
997    /// rejects relation calls and clauses whose arguments contain any
998    /// other free constant. Relations without a recorded world are
999    /// unconstrained (the feature is opt-in per relation).
1000    pub worlds: HashMap<String, Vec<String>>,
1001    /// Inductive declarations (issue #45, D10): a first-class inductive
1002    /// datatype encoded as link signatures plus a generated eliminator.
1003    /// Stored by type name; see [`InductiveDecl`] for the full layout.
1004    pub inductives: HashMap<String, InductiveDecl>,
1005    /// Recursive definition declarations (issue #49, D13): each
1006    /// `(define <name> [(measure ...)] (case ...) ...)` form is recorded
1007    /// here so the termination checker (`is_terminating`) can verify
1008    /// structural decrease across recursive calls. Stored by definition
1009    /// name; see [`DefineDecl`] for the full layout.
1010    pub definitions: HashMap<String, DefineDecl>,
1011    /// Coinductive declarations (issue #53, D11): a first-class coinductive
1012    /// datatype dual to the inductive form, encoded as link signatures plus
1013    /// a generated corecursor `Name-corec`. Each entry stores the type name,
1014    /// the ordered constructors, and the name and Pi-type of the
1015    /// corecursor. The kernel additionally enforces a syntactic productivity
1016    /// check at declaration time: at least one constructor must take a
1017    /// recursive argument so non-productive types (which cannot generate
1018    /// any infinite values) are rejected up front.
1019    pub coinductives: HashMap<String, CoinductiveDecl>,
1020    /// Domain plugins (issue #63): domain-specific decision procedures keyed
1021    /// by `(domain <name> ...)`. The default registry ships the
1022    /// automatic-sequences plugin below; callers may register additional
1023    /// function-pointer plugins on their Env instance.
1024    pub domain_plugins: HashMap<String, DomainPluginFn>,
1025    /// Decisions recorded by the built-in automatic-sequences plugin.
1026    pub automatic_sequence_decisions: HashMap<String, AutomaticSequenceDecision>,
1027    /// Root-construct registry (issue #97). Records what every kernel
1028    /// construct depends on and whether the user has overridden it.
1029    /// Data-only: descriptors never alter evaluator behaviour. Consumed by
1030    /// the foundation report (`(foundation-report)`) and the CLI trust audit.
1031    pub root_constructs: HashMap<String, RootConstructDescriptor>,
1032    /// Foundation registry (issue #97). A foundation bundles a coherent
1033    /// set of root-construct interpretations. `default-rml` is preregistered
1034    /// with the host-implemented semantics; user files can register
1035    /// alternative foundations and select them with `(with-foundation …)`.
1036    /// Backward compatibility is preserved by defaulting to `default-rml`.
1037    pub foundations: HashMap<String, FoundationDescriptor>,
1038    pub active_foundation: String,
1039    pub foundation_stack: Vec<FoundationFrame>,
1040    pub active_implementations: HashMap<String, ActiveImplementationDescriptor>,
1041    /// Carrier enforcement state (issue #97, Section 2). Off by default so
1042    /// legacy programs are not constrained; flipped on by an enclosing
1043    /// `(with-foundation <name>)` whose descriptor includes both
1044    /// `(carrier ...)` and `(strict-carrier)` clauses.
1045    pub strict_carrier: bool,
1046    pub carrier: Option<Vec<f64>>,
1047    pub carrier_label: Option<String>,
1048    /// Proof-object substrate (issue #97, Phase 3 of netkeep80's punch-list).
1049    /// `proof_rules` maps a declared rule name to its premise patterns and
1050    /// conclusion pattern (with `?meta` leaves as metavariables). The map
1051    /// `proof_objects` records concrete derivations consumed by
1052    /// `(check-proof <name>)`. Both are data-only: declaring a rule never
1053    /// alters evaluator behaviour. The CLI's foundation report surfaces them.
1054    pub proof_rules: HashMap<String, ProofRule>,
1055    pub proof_assumptions: HashMap<String, ProofAssumption>,
1056    pub proof_objects: HashMap<String, ProofObject>,
1057    /// Pure-links strict mode (issue #97, Phase 6 of netkeep80's punch-list).
1058    /// When `strict_pure_links` is true, every queried form is audited
1059    /// against the root-construct registry; any operator whose status is
1060    /// `host-primitive` or `host-derived` triggers an E065 diagnostic unless
1061    /// the construct is in `allowed_host_primitives`. Off by default so
1062    /// legacy programs run unchanged.
1063    pub strict_pure_links: bool,
1064    pub allowed_host_primitives: HashSet<String>,
1065}
1066
1067/// Stack frame pushed when entering a foundation scope. Stores the previous
1068/// active foundation name plus a snapshot of any operators the foundation
1069/// rebinds, so `exit_foundation` can restore the prior semantics exactly.
1070/// `snapshot` maps operator name -> previous Op (None if the op did not
1071/// exist before). Carrier snapshot fields (issue #97 Section 2) preserve the
1072/// strict-carrier state of the enclosing scope so nested `(with-foundation
1073/// ...)` bodies roll back cleanly when their inner scope exits.
1074#[derive(Debug, Clone)]
1075pub struct FoundationFrame {
1076    pub previous_active: String,
1077    pub snapshot: Vec<(String, Option<Op>)>,
1078    pub previous_active_implementations: Vec<(String, Option<ActiveImplementationDescriptor>)>,
1079    pub previous_strict_carrier: bool,
1080    pub previous_carrier: Option<Vec<f64>>,
1081    pub previous_carrier_label: Option<String>,
1082}
1083
1084/// A root-construct descriptor. Stored on the `Env` for the foundation
1085/// registry (issue #97). Every field is purely informational: declaring a
1086/// descriptor never changes evaluator behaviour. The CLI's foundation
1087/// report and tests inspect these records to verify the trust contract.
1088#[derive(Debug, Clone, Default, PartialEq)]
1089pub struct RootConstructDescriptor {
1090    pub name: String,
1091    pub status: Option<String>,
1092    pub semantic_status: Option<String>,
1093    pub kind: Option<String>,
1094    pub depends_on: Vec<String>,
1095    pub encoded_as: Option<String>,
1096    pub pure_links_ready: Option<bool>,
1097    pub override_with: Option<String>,
1098    pub planned_as: Option<String>,
1099    pub foundation: Option<String>,
1100}
1101
1102/// A foundation descriptor. Bundles a coherent set of root-construct
1103/// interpretations. Selecting a foundation never silently rewires
1104/// behaviour; the host operators always run, but the active-foundation
1105/// tag is exposed via the foundation report so users can audit which
1106/// foundation they are trusting.
1107#[derive(Debug, Clone, Default, PartialEq)]
1108pub struct FoundationDescriptor {
1109    pub name: String,
1110    pub description: Option<String>,
1111    pub uses: Vec<String>,
1112    pub defines: Vec<(String, String)>, // construct -> implementation
1113    pub extends: Option<String>,
1114    pub numeric_domain: Option<String>,
1115    pub truth_domain: Option<String>,
1116    /// Carrier (issue #97, Section 2): the explicit set of values the
1117    /// foundation considers legal for queries and probability assignments.
1118    /// Each entry is stored as a string so `enter_foundation` can resolve
1119    /// symbolic constants (`true`, `false`, `unknown`) through
1120    /// `env.symbol_prob` at activation time. Numeric literals stay literal.
1121    pub carrier: Vec<String>,
1122    /// When true, the active `with-foundation` scope enforces the carrier
1123    /// at runtime: out-of-carrier query results and probability
1124    /// assignments raise an `E063` diagnostic instead of being silently
1125    /// clamped. Defaults to `false` for backward compatibility — declaring
1126    /// `(carrier ...)` alone is informational.
1127    pub strict_carrier: bool,
1128    /// Links-defined finite truth tables (issue #97, Section 3 of
1129    /// netkeep80's punch-list). Each entry rebinds the named operator to
1130    /// the listed row set for the duration of `(with-foundation ...)`.
1131    /// Inputs and outputs are stored as strings so `enter_foundation` can
1132    /// resolve symbolic truth constants (`true`, `false`, `unknown`)
1133    /// through `env.symbol_prob` at activation time. Numeric literals stay
1134    /// literal. A row whose inputs don't match falls through to the
1135    /// previously installed op so partial tables remain backward-
1136    /// compatible.
1137    pub truth_tables: Vec<(String, Vec<TruthTableRow>)>,
1138    /// Experimental MTC/anum foundation profile metadata (issue #97,
1139    /// Phase 9). When `experimental` is true the trust audit prints an
1140    /// `[experimental]` tag next to the foundation name so consumers can
1141    /// see it carries no stability guarantees. `root` is the foundation's
1142    /// root symbol (e.g. `∞` for mtc-anum) and `abits` lists its
1143    /// foundational alphabet pairs (symbol → meaning).
1144    pub experimental: bool,
1145    pub root: Option<String>,
1146    pub abits: Vec<(String, String)>,
1147}
1148
1149/// Active implementation selected by the current foundation scope for a
1150/// construct such as `and` or `not`. This is the behaviour-facing counterpart
1151/// to the global root-construct descriptor: strict pure-links mode consults
1152/// it before falling back to the global registry.
1153#[derive(Debug, Clone, Default, PartialEq)]
1154pub struct ActiveImplementationDescriptor {
1155    pub construct: String,
1156    pub foundation: Option<String>,
1157    pub implementation: Option<String>,
1158    pub status: Option<String>,
1159    pub semantic_status: Option<String>,
1160    pub depends_on: Vec<String>,
1161}
1162
1163/// One row of a `(truth-table <op> ...)` declaration: a sequence of input
1164/// tokens and the output token. See `FoundationDescriptor::truth_tables`.
1165#[derive(Debug, Clone, Default, PartialEq)]
1166pub struct TruthTableRow {
1167    pub inputs: Vec<String>,
1168    pub output: String,
1169}
1170
1171/// Snapshot of the foundation/root-construct state for the trust report.
1172#[derive(Debug, Clone, Default, PartialEq)]
1173pub struct FoundationReport {
1174    pub active_foundation: String,
1175    pub description: Option<String>,
1176    pub numeric_domain: Option<String>,
1177    pub truth_domain: Option<String>,
1178    pub root_constructs: Vec<RootConstructDescriptor>,
1179    pub by_status: Vec<(String, Vec<String>)>,
1180    pub by_semantic_status: Vec<(String, Vec<String>)>,
1181    pub foundations: Vec<FoundationDescriptor>,
1182    pub active_implementations: Vec<ActiveImplementationDescriptor>,
1183    /// Proof-object substrate (issue #97, Phase 3). Surfaced on the report so
1184    /// the trust audit can list every declared rule and concrete derivation.
1185    /// Names are kept sorted for stable output across runs.
1186    pub proof_rules: Vec<ProofRuleSnapshot>,
1187    pub proof_assumptions: Vec<ProofAssumptionSnapshot>,
1188    pub proof_objects: Vec<ProofObjectSnapshot>,
1189    /// Pure-links strict mode state (issue #97, Phase 6). Surfaced so the
1190    /// trust audit can prove the engine is running in strict mode and list
1191    /// every host primitive that was explicitly allow-listed.
1192    pub strict_pure_links: bool,
1193    pub allowed_host_primitives: Vec<String>,
1194    /// Dependency-graph traversal (issue #97, Phase 7). For every registered
1195    /// root-construct, the transitive closure of its `depends_on` chain,
1196    /// sorted deterministically. Leaf constructs map to an empty vector.
1197    /// Pairs are kept sorted by name so the report is reproducible.
1198    pub dependency_graph: Vec<(String, Vec<String>)>,
1199}
1200
1201/// A declared rule of inference. Premises and the conclusion are stored as
1202/// AST nodes; leaves whose token starts with `?` are metavariables that
1203/// bind during `check_proof_object`. Repeated metavariables must
1204/// structurally match.
1205#[derive(Debug, Clone, PartialEq)]
1206pub struct ProofRule {
1207    pub name: String,
1208    pub premises: Vec<Node>,
1209    pub conclusion: Node,
1210}
1211
1212/// An explicit proof leaf. Proof objects cite these with `(premise-by name)`
1213/// or `(uses name)` so assumptions and axioms are visible in the proof graph.
1214#[derive(Debug, Clone, PartialEq)]
1215pub struct ProofAssumption {
1216    pub name: String,
1217    pub kind: String,
1218    pub judgement: Node,
1219}
1220
1221/// A concrete derivation that claims to be an instance of a rule. Stored
1222/// alongside the rule so `(check-proof <name>)` can re-validate it on
1223/// demand without re-parsing the source.
1224#[derive(Debug, Clone, PartialEq)]
1225pub struct ProofObject {
1226    pub name: String,
1227    pub rule: String,
1228    pub premises: Vec<Node>,
1229    pub premise_refs: Vec<String>,
1230    pub conclusion: Node,
1231}
1232
1233/// Printed view of a `ProofRule` for `foundation_report()`. Patterns are
1234/// stringified via `key_of` so consumers can pretty-print without owning
1235/// the AST representation.
1236#[derive(Debug, Clone, Default, PartialEq)]
1237pub struct ProofRuleSnapshot {
1238    pub name: String,
1239    pub premises: Vec<String>,
1240    pub conclusion: String,
1241}
1242
1243/// Printed view of a proof assumption/axiom for `foundation_report()`.
1244#[derive(Debug, Clone, Default, PartialEq)]
1245pub struct ProofAssumptionSnapshot {
1246    pub name: String,
1247    pub kind: String,
1248    pub judgement: String,
1249}
1250
1251/// Printed view of a `ProofObject` for `foundation_report()`. Mirrors
1252/// `ProofRuleSnapshot` and additionally records the referenced rule.
1253#[derive(Debug, Clone, Default, PartialEq)]
1254pub struct ProofObjectSnapshot {
1255    pub name: String,
1256    pub rule: String,
1257    pub premises: Vec<String>,
1258    pub premise_refs: Vec<String>,
1259    pub conclusion: String,
1260}
1261
1262/// Verdict for `(proof-report <name>)`. Mirrors `CheckProofVerdict` shape
1263/// without the substitution table.
1264#[derive(Debug, Clone, PartialEq)]
1265pub struct ProofReportVerdict {
1266    pub ok: bool,
1267    pub error: Option<String>,
1268}
1269
1270/// One entry of the transitive dependency walk inside a `ProofReport`.
1271#[derive(Debug, Clone, PartialEq)]
1272pub struct ProofReportDependency {
1273    pub name: String,
1274    pub kind: String,
1275    /// Rule referenced by a proof-object dependency (empty for axioms/assumptions).
1276    pub rule: Option<String>,
1277    /// Stringified judgement, when known.
1278    pub judgement: Option<String>,
1279}
1280
1281/// Per-proof dependency/trust report (issue #97, Phase 13). Built by
1282/// `Env::proof_report` and surfaced as `RunResult::Proof` so the
1283/// trust audit can be performed for an individual proof object instead
1284/// of only globally via `foundation-report`.
1285#[derive(Debug, Clone, PartialEq)]
1286pub struct ProofReport {
1287    pub name: String,
1288    pub rule: Option<String>,
1289    pub conclusion: Option<String>,
1290    pub premises: Vec<String>,
1291    pub premise_refs: Vec<String>,
1292    pub verdict: ProofReportVerdict,
1293    pub dependencies: Vec<ProofReportDependency>,
1294    pub rules: Vec<String>,
1295    pub root_constructs_used: Vec<String>,
1296    pub by_semantic_status: Vec<(String, Vec<String>)>,
1297    pub by_trust_status: Vec<(String, Vec<String>)>,
1298    pub active_foundation: String,
1299    pub strict_pure_links: bool,
1300}
1301
1302/// One constructor of an inductive datatype.
1303#[derive(Debug, Clone)]
1304pub struct ConstructorDecl {
1305    /// Constructor name (e.g. `zero`, `succ`).
1306    pub name: String,
1307    /// Ordered binder list of the constructor's Pi-type, each `(name, type)`.
1308    /// A constant constructor (`(constructor zero)`) has an empty list.
1309    pub params: Vec<(String, Node)>,
1310    /// The constructor's recorded type — either a bare leaf naming the
1311    /// inductive type (constant constructor) or the original `(Pi …)` chain.
1312    pub typ: Node,
1313}
1314
1315/// A parsed `(inductive Name (constructor …) …)` declaration.
1316#[derive(Debug, Clone)]
1317pub struct InductiveDecl {
1318    /// Inductive type name (must start with an uppercase letter).
1319    pub name: String,
1320    /// Ordered list of declared constructors.
1321    pub constructors: Vec<ConstructorDecl>,
1322    /// Generated eliminator name (`Name-rec`).
1323    pub elim_name: String,
1324    /// Generated eliminator's dependent Pi-type.
1325    pub elim_type: Node,
1326}
1327
1328/// One `(case <pattern-args> <body>)` clause of a `(define …)` declaration.
1329#[derive(Debug, Clone, PartialEq)]
1330pub struct DefineClause {
1331    /// The clause's pattern arguments — the children of the parenthesised
1332    /// pattern list, in left-to-right order.
1333    pub pattern: Vec<Node>,
1334    /// The clause body, which may contain recursive references to the
1335    /// declared name.
1336    pub body: Node,
1337}
1338
1339/// Optional measure attached to a `(define …)` declaration.
1340#[derive(Debug, Clone, PartialEq, Eq)]
1341pub enum DefineMeasure {
1342    /// Lexicographic measure: the listed argument indices (0-based) must
1343    /// strictly decrease in the standard left-to-right lexicographic order
1344    /// on every recursive call.
1345    Lex(Vec<usize>),
1346}
1347
1348/// A parsed `(define <name> [(measure …)] (case …) …)` declaration.
1349#[derive(Debug, Clone, PartialEq)]
1350pub struct DefineDecl {
1351    /// Definition name.
1352    pub name: String,
1353    /// Optional explicit measure. When `None`, the termination checker
1354    /// uses the default rule: structural decrease on the first argument.
1355    pub measure: Option<DefineMeasure>,
1356    /// Ordered list of `(case …)` clauses.
1357    pub clauses: Vec<DefineClause>,
1358}
1359
1360/// A parsed `(coinductive Name (constructor …) …)` declaration. Mirrors
1361/// [`InductiveDecl`] but additionally guarantees the productivity check
1362/// (at least one recursive constructor) has succeeded.
1363#[derive(Debug, Clone)]
1364pub struct CoinductiveDecl {
1365    /// Coinductive type name (must start with an uppercase letter).
1366    pub name: String,
1367    /// Ordered list of declared constructors.
1368    pub constructors: Vec<ConstructorDecl>,
1369    /// Generated corecursor name (`Name-corec`).
1370    pub corec_name: String,
1371    /// Generated corecursor's dependent Pi-type.
1372    pub corec_type: Node,
1373}
1374
1375/// Per-argument mode flag for a relation declared via `(mode …)`.
1376#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1377pub enum ModeFlag {
1378    /// `+input`: caller must supply a ground argument here.
1379    In,
1380    /// `-output`: the relation is expected to produce a value here.
1381    Out,
1382    /// `*either`: no directionality constraint.
1383    Either,
1384}
1385
1386impl ModeFlag {
1387    pub fn from_token(token: &str) -> Option<Self> {
1388        match token {
1389            "+input" => Some(ModeFlag::In),
1390            "-output" => Some(ModeFlag::Out),
1391            "*either" => Some(ModeFlag::Either),
1392            _ => None,
1393        }
1394    }
1395}
1396
1397impl Env {
1398    pub fn new(options: Option<EnvOptions>) -> Self {
1399        let opts = options.unwrap_or_default();
1400        let mut ops = HashMap::new();
1401        ops.insert("not".to_string(), Op::Not);
1402        ops.insert("and".to_string(), Op::Agg(Aggregator::Avg));
1403        ops.insert("or".to_string(), Op::Agg(Aggregator::Max));
1404        // Belnap operators: AND-altering operators for four-valued logic
1405        // "both" (gullibility): avg — contradiction resolves to midpoint
1406        // "neither" (consensus): product — gap resolves to zero (no info propagates)
1407        // See: https://en.wikipedia.org/wiki/Four-valued_logic#Belnap
1408        ops.insert("both".to_string(), Op::Agg(Aggregator::Avg));
1409        ops.insert("neither".to_string(), Op::Agg(Aggregator::Prod));
1410        ops.insert("=".to_string(), Op::Eq);
1411        ops.insert("!=".to_string(), Op::Neq);
1412        ops.insert("+".to_string(), Op::Add);
1413        ops.insert("-".to_string(), Op::Sub);
1414        ops.insert("*".to_string(), Op::Mul);
1415        ops.insert("/".to_string(), Op::Div);
1416        ops.insert("<".to_string(), Op::Less);
1417        ops.insert("<=".to_string(), Op::LessOrEqual);
1418
1419        let mut env = Self {
1420            terms: HashSet::new(),
1421            assign: HashMap::new(),
1422            symbol_prob: HashMap::new(),
1423            lo: opts.lo,
1424            hi: opts.hi,
1425            valence: opts.valence,
1426            ops,
1427            types: HashMap::new(),
1428            lambdas: HashMap::new(),
1429            templates: HashMap::new(),
1430            trace_enabled: false,
1431            trace_events: Vec::new(),
1432            current_span: None,
1433            default_span: Span::unknown(),
1434            namespace: None,
1435            aliases: HashMap::new(),
1436            imported: HashSet::new(),
1437            shadow_diagnostics: Vec::new(),
1438            file_namespaces: HashMap::new(),
1439            modes: HashMap::new(),
1440            relations: HashMap::new(),
1441            worlds: HashMap::new(),
1442            inductives: HashMap::new(),
1443            definitions: HashMap::new(),
1444            coinductives: HashMap::new(),
1445            domain_plugins: HashMap::new(),
1446            automatic_sequence_decisions: HashMap::new(),
1447            root_constructs: HashMap::new(),
1448            foundations: HashMap::new(),
1449            active_foundation: "default-rml".to_string(),
1450            foundation_stack: Vec::new(),
1451            active_implementations: HashMap::new(),
1452            strict_carrier: false,
1453            carrier: None,
1454            carrier_label: None,
1455            proof_rules: HashMap::new(),
1456            proof_assumptions: HashMap::new(),
1457            proof_objects: HashMap::new(),
1458            strict_pure_links: false,
1459            allowed_host_primitives: HashSet::new(),
1460        };
1461
1462        // Initialize truth constants: true, false, unknown, undefined
1463        // These are predefined symbol probabilities based on the current range.
1464        // By default: (false: min(range)), (true: max(range)),
1465        //             (unknown: mid(range)), (undefined: mid(range))
1466        // They can be redefined by the user via (true: <value>), (false: <value>), etc.
1467        env.init_truth_constants();
1468        env.register_domain_plugin("automatic-sequences", automatic_sequences_domain_plugin);
1469        env.register_default_foundation();
1470        env
1471    }
1472
1473    /// Midpoint of the range.
1474    pub fn mid(&self) -> f64 {
1475        (self.lo + self.hi) / 2.0
1476    }
1477
1478    /// Initialize truth constants based on current range.
1479    /// (false: min(range)), (true: max(range)),
1480    /// (unknown: mid(range)), (undefined: mid(range))
1481    pub fn init_truth_constants(&mut self) {
1482        self.symbol_prob.insert("true".to_string(), self.hi);
1483        self.symbol_prob.insert("false".to_string(), self.lo);
1484        let mid = self.mid();
1485        self.symbol_prob.insert("unknown".to_string(), mid);
1486        self.symbol_prob.insert("undefined".to_string(), mid);
1487        // Note: "both" and "neither" are operators (not constants) — see Env::new()
1488        // See: https://en.wikipedia.org/wiki/Four-valued_logic#Belnap
1489    }
1490
1491    /// Clamp and optionally quantize a value to the valid range.
1492    pub fn clamp(&self, x: f64) -> f64 {
1493        let clamped = x.max(self.lo).min(self.hi);
1494        if self.valence >= 2 {
1495            quantize(clamped, self.valence, self.lo, self.hi)
1496        } else {
1497            clamped
1498        }
1499    }
1500
1501    /// Parse a numeric string respecting current range.
1502    pub fn to_num(&self, s: &str) -> f64 {
1503        self.clamp(s.parse::<f64>().unwrap_or(0.0))
1504    }
1505
1506    pub fn define_op(&mut self, name: &str, op: Op) {
1507        self.ops.insert(name.to_string(), op);
1508    }
1509
1510    pub fn register_domain_plugin(&mut self, name: &str, plugin: DomainPluginFn) {
1511        self.domain_plugins.insert(name.to_string(), plugin);
1512    }
1513
1514    pub fn get_domain_plugin(&self, name: &str) -> Option<DomainPluginFn> {
1515        self.domain_plugins.get(name).copied()
1516    }
1517
1518    // ---------- Foundation / root-construct registry (issue #97) ----------
1519    /// Preregister the `default-rml` foundation and seed the built-in
1520    /// root-construct descriptors that describe the current host
1521    /// implementation. These are data-only and never change behaviour.
1522    pub fn register_default_foundation(&mut self) {
1523        let default = FoundationDescriptor {
1524            name: "default-rml".to_string(),
1525            description: Some(
1526                "Default RML foundation: host-implemented configurable kernel".to_string(),
1527            ),
1528            uses: Vec::new(),
1529            defines: Vec::new(),
1530            extends: None,
1531            numeric_domain: Some("decimal-12".to_string()),
1532            truth_domain: Some("default-truth".to_string()),
1533            carrier: Vec::new(),
1534            strict_carrier: false,
1535            truth_tables: Vec::new(),
1536            experimental: false,
1537            root: None,
1538            abits: Vec::new(),
1539        };
1540        self.foundations.insert(default.name.clone(), default);
1541        // Pre-seed the experimental MTC/anum foundation (issue #97, Phase
1542        // 9). Opt-in only — never activated implicitly. Selecting it via
1543        // `(with-foundation mtc-anum ...)` does NOT rewire host arithmetic;
1544        // it is descriptive metadata plus a serialization alphabet.
1545        let mtc_anum = FoundationDescriptor {
1546            name: "mtc-anum".to_string(),
1547            description: Some(
1548                "experimental metatheory-of-links foundation (anum serialization)".to_string(),
1549            ),
1550            uses: Vec::new(),
1551            defines: Vec::new(),
1552            extends: None,
1553            numeric_domain: None,
1554            truth_domain: Some("mtc-abits".to_string()),
1555            carrier: Vec::new(),
1556            strict_carrier: false,
1557            truth_tables: Vec::new(),
1558            experimental: true,
1559            root: Some("∞".to_string()),
1560            abits: vec![
1561                ("[".to_string(), "start-of-meaning".to_string()),
1562                ("]".to_string(), "end-of-meaning".to_string()),
1563                ("1".to_string(), "unit-of-meaning".to_string()),
1564                ("0".to_string(), "zero-of-meaning".to_string()),
1565            ],
1566        };
1567        self.foundations.insert(mtc_anum.name.clone(), mtc_anum);
1568        let boolean_links = FoundationDescriptor {
1569            name: "boolean-links".to_string(),
1570            description: Some(
1571                "links-defined two-valued Boolean logic via finite truth tables".to_string(),
1572            ),
1573            uses: Vec::new(),
1574            defines: Vec::new(),
1575            extends: None,
1576            numeric_domain: Some("boolean-zero-one".to_string()),
1577            truth_domain: Some("boolean-two-valued".to_string()),
1578            carrier: vec!["0".to_string(), "1".to_string()],
1579            strict_carrier: true,
1580            truth_tables: vec![
1581                (
1582                    "and".to_string(),
1583                    vec![
1584                        TruthTableRow {
1585                            inputs: vec!["1".to_string(), "1".to_string()],
1586                            output: "1".to_string(),
1587                        },
1588                        TruthTableRow {
1589                            inputs: vec!["1".to_string(), "0".to_string()],
1590                            output: "0".to_string(),
1591                        },
1592                        TruthTableRow {
1593                            inputs: vec!["0".to_string(), "1".to_string()],
1594                            output: "0".to_string(),
1595                        },
1596                        TruthTableRow {
1597                            inputs: vec!["0".to_string(), "0".to_string()],
1598                            output: "0".to_string(),
1599                        },
1600                    ],
1601                ),
1602                (
1603                    "or".to_string(),
1604                    vec![
1605                        TruthTableRow {
1606                            inputs: vec!["1".to_string(), "1".to_string()],
1607                            output: "1".to_string(),
1608                        },
1609                        TruthTableRow {
1610                            inputs: vec!["1".to_string(), "0".to_string()],
1611                            output: "1".to_string(),
1612                        },
1613                        TruthTableRow {
1614                            inputs: vec!["0".to_string(), "1".to_string()],
1615                            output: "1".to_string(),
1616                        },
1617                        TruthTableRow {
1618                            inputs: vec!["0".to_string(), "0".to_string()],
1619                            output: "0".to_string(),
1620                        },
1621                    ],
1622                ),
1623                (
1624                    "not".to_string(),
1625                    vec![
1626                        TruthTableRow {
1627                            inputs: vec!["1".to_string()],
1628                            output: "0".to_string(),
1629                        },
1630                        TruthTableRow {
1631                            inputs: vec!["0".to_string()],
1632                            output: "1".to_string(),
1633                        },
1634                    ],
1635                ),
1636            ],
1637            experimental: false,
1638            root: None,
1639            abits: Vec::new(),
1640        };
1641        self.foundations
1642            .insert(boolean_links.name.clone(), boolean_links);
1643        // Pre-seed the links-defined typed-kernel foundation (issue #97,
1644        // Phase 5). Selecting it via
1645        // `(with-foundation typed-kernel-links ...)` records the proof
1646        // substrate rules `pi-formation`, `lambda-introduction`,
1647        // `application-elimination`, and `beta-conversion` as the
1648        // canonical links-defined replacements for the host kernel's
1649        // typing judgements. Evaluation still runs through the host kernel;
1650        // the foundation is selected so the trust audit can list the four
1651        // rules as the active derivations.
1652        let typed_kernel_links = FoundationDescriptor {
1653            name: "typed-kernel-links".to_string(),
1654            description: Some(
1655                "links-defined typed-kernel fragment (Pi/lambda/apply/beta as proof rules)"
1656                    .to_string(),
1657            ),
1658            uses: vec![
1659                "pi-formation".to_string(),
1660                "lambda-introduction".to_string(),
1661                "application-elimination".to_string(),
1662                "beta-conversion".to_string(),
1663            ],
1664            defines: Vec::new(),
1665            extends: Some("default-rml".to_string()),
1666            numeric_domain: Some("decimal-12".to_string()),
1667            truth_domain: Some("default-truth".to_string()),
1668            carrier: Vec::new(),
1669            strict_carrier: false,
1670            truth_tables: Vec::new(),
1671            experimental: false,
1672            root: None,
1673            abits: Vec::new(),
1674        };
1675        self.foundations
1676            .insert(typed_kernel_links.name.clone(), typed_kernel_links);
1677        // Pre-seed the links-defined Peano naturals foundation (issue #97,
1678        // Phase 12). Selecting it via `(with-foundation nat-links ...)`
1679        // records the Nat proof-substrate rules, the dedicated `nat-equality`
1680        // layer, and the rule-driven `eval-nat` normalizer as active
1681        // foundation dependencies. The host's decimal numeric domain and
1682        // default equality layers are unaffected.
1683        let nat_links = FoundationDescriptor {
1684            name: "nat-links".to_string(),
1685            description: Some(
1686                "links-defined Peano naturals (zero/succ formation, add by recursion, induction with explicit forall/implication/predicate-application, nat-equality with reflexivity and successor congruence, nat-recursion/nat-eliminator, multiplication, rule-driven eval-nat normalizer)"
1687                    .to_string(),
1688            ),
1689            uses: vec![
1690                "nat-zero-formation".to_string(),
1691                "nat-succ-formation".to_string(),
1692                "nat-add-zero".to_string(),
1693                "nat-add-succ".to_string(),
1694                "nat-induction".to_string(),
1695                "nat-equality".to_string(),
1696                "nat-refl".to_string(),
1697                "nat-cong-succ".to_string(),
1698                "forall".to_string(),
1699                "implication".to_string(),
1700                "predicate-application".to_string(),
1701                "nat-recursion".to_string(),
1702                "nat-eliminator".to_string(),
1703                "nat-rec-zero".to_string(),
1704                "nat-rec-succ".to_string(),
1705                "mul".to_string(),
1706                "nat-mul-zero".to_string(),
1707                "nat-mul-succ".to_string(),
1708                "eval-nat-normalize".to_string(),
1709                "eval-nat".to_string(),
1710                "nat-normal-form-to-host-number".to_string(),
1711            ],
1712            defines: Vec::new(),
1713            extends: Some("default-rml".to_string()),
1714            numeric_domain: Some("decimal-12".to_string()),
1715            truth_domain: Some("default-truth".to_string()),
1716            carrier: Vec::new(),
1717            strict_carrier: false,
1718            truth_tables: Vec::new(),
1719            experimental: false,
1720            root: None,
1721            abits: Vec::new(),
1722        };
1723        self.foundations
1724            .insert(nat_links.name.clone(), nat_links);
1725        seed_builtin_root_constructs(self);
1726    }
1727
1728    pub fn register_root_construct(
1729        &mut self,
1730        descriptor: RootConstructDescriptor,
1731    ) -> Result<RootConstructDescriptor, String> {
1732        if descriptor.name.is_empty() {
1733            return Err("root-construct descriptor requires a name".to_string());
1734        }
1735        let prev = self.root_constructs.get(&descriptor.name).cloned();
1736        let merged = merge_root_construct_descriptors(prev, descriptor);
1737        self.root_constructs
1738            .insert(merged.name.clone(), merged.clone());
1739        Ok(merged)
1740    }
1741
1742    pub fn get_root_construct(&self, name: &str) -> Option<&RootConstructDescriptor> {
1743        self.root_constructs.get(name)
1744    }
1745
1746    pub fn list_root_constructs(&self) -> Vec<RootConstructDescriptor> {
1747        let mut v: Vec<RootConstructDescriptor> = self.root_constructs.values().cloned().collect();
1748        v.sort_by(|a, b| a.name.cmp(&b.name));
1749        v
1750    }
1751
1752    pub fn register_foundation(
1753        &mut self,
1754        foundation: FoundationDescriptor,
1755    ) -> Result<FoundationDescriptor, String> {
1756        if foundation.name.is_empty() {
1757            return Err("foundation declaration requires a name".to_string());
1758        }
1759        let prev = self.foundations.get(&foundation.name).cloned();
1760        let merged = merge_foundation_descriptors(prev, foundation);
1761        self.foundations.insert(merged.name.clone(), merged.clone());
1762        Ok(merged)
1763    }
1764
1765    pub fn get_foundation(&self, name: &str) -> Option<&FoundationDescriptor> {
1766        self.foundations.get(name)
1767    }
1768
1769    pub fn enter_foundation(&mut self, name: &str) -> Result<(), String> {
1770        let foundation = match self.foundations.get(name) {
1771            Some(f) => f.clone(),
1772            None => return Err(format!("Unknown foundation: {}", name)),
1773        };
1774        // Snapshot the operators that this foundation rebinds so
1775        // `exit_foundation` can restore them. Only `(defines <op> <agg>)`
1776        // entries that name a known truth aggregator are applied (avg, min,
1777        // max, product, probabilistic_sum); other entries are data-only.
1778        let mut snapshot: Vec<(String, Option<Op>)> = Vec::new();
1779        let mut previous_active_implementations: Vec<(
1780            String,
1781            Option<ActiveImplementationDescriptor>,
1782        )> = Vec::new();
1783        let snapshot_impl = |env: &Env,
1784                             store: &mut Vec<(String, Option<ActiveImplementationDescriptor>)>,
1785                             op_name: &str| {
1786            if store.iter().any(|(n, _)| n == op_name) {
1787                return;
1788            }
1789            store.push((
1790                op_name.to_string(),
1791                env.active_implementations.get(op_name).cloned(),
1792            ));
1793        };
1794        for (op_name, impl_name) in &foundation.defines {
1795            if let Some(agg) = Aggregator::from_name(impl_name) {
1796                snapshot_impl(self, &mut previous_active_implementations, op_name);
1797                let prev = self.ops.get(op_name).cloned();
1798                snapshot.push((op_name.clone(), prev));
1799                self.ops.insert(op_name.clone(), Op::Agg(agg));
1800                self.active_implementations.insert(
1801                    op_name.clone(),
1802                    ActiveImplementationDescriptor {
1803                        construct: op_name.clone(),
1804                        foundation: Some(name.to_string()),
1805                        implementation: Some(impl_name.clone()),
1806                        status: Some("host-primitive".to_string()),
1807                        semantic_status: Some("host-trusted".to_string()),
1808                        depends_on: vec![impl_name.clone()],
1809                    },
1810                );
1811            }
1812        }
1813        // Truth tables (issue #97, Section 3 of netkeep80's punch-list).
1814        // Layered on top of `(defines ...)` so a foundation can pin a
1815        // finite slice of an operator and let the aggregator-based default
1816        // handle the rest.
1817        for (op_name, rows) in &foundation.truth_tables {
1818            let mut resolved: Vec<TruthTableEntry> = Vec::new();
1819            for row in rows {
1820                let mut inputs: Vec<f64> = Vec::with_capacity(row.inputs.len());
1821                let mut row_ok = true;
1822                for tok in &row.inputs {
1823                    match resolve_truth_table_value(self, tok) {
1824                        Some(v) => inputs.push(v),
1825                        None => {
1826                            row_ok = false;
1827                            break;
1828                        }
1829                    }
1830                }
1831                if !row_ok {
1832                    continue;
1833                }
1834                let output = match resolve_truth_table_value(self, &row.output) {
1835                    Some(v) => v,
1836                    None => continue,
1837                };
1838                resolved.push(TruthTableEntry { inputs, output });
1839            }
1840            if resolved.is_empty() {
1841                continue;
1842            }
1843            if !snapshot.iter().any(|(n, _)| n == op_name) {
1844                let prev = self.ops.get(op_name).cloned();
1845                snapshot.push((op_name.clone(), prev));
1846            }
1847            snapshot_impl(self, &mut previous_active_implementations, op_name);
1848            let previous_impl = self.active_implementations.get(op_name).cloned();
1849            let is_total = truth_table_rows_complete_for_carrier(self, rows, &foundation);
1850            let depends_on = if is_total {
1851                Vec::new()
1852            } else {
1853                truth_table_fallback_dependencies(self, op_name, previous_impl.as_ref())
1854            };
1855            let fallback = self.ops.get(op_name).cloned().map(Box::new);
1856            self.ops.insert(
1857                op_name.clone(),
1858                Op::TruthTable {
1859                    rows: resolved,
1860                    fallback,
1861                },
1862            );
1863            self.active_implementations.insert(
1864                op_name.clone(),
1865                ActiveImplementationDescriptor {
1866                    construct: op_name.clone(),
1867                    foundation: Some(name.to_string()),
1868                    implementation: Some(format!("truth-table:{}/{}", name, op_name)),
1869                    status: Some("links-defined".to_string()),
1870                    semantic_status: Some("links-checked".to_string()),
1871                    depends_on,
1872                },
1873            );
1874        }
1875        // Carrier snapshot for opt-in enforcement (issue #97, Section 2).
1876        // `strict_carrier` is what the evaluator hot path checks; `carrier`
1877        // is the resolved numeric set. Symbolic carrier values (`true`,
1878        // `false`, `unknown`, ...) resolve through `symbol_prob` so
1879        // user-defined truth constants flow in.
1880        let previous_strict_carrier = self.strict_carrier;
1881        let previous_carrier = self.carrier.clone();
1882        let previous_carrier_label = self.carrier_label.clone();
1883        if foundation.strict_carrier && !foundation.carrier.is_empty() {
1884            let mut resolved: Vec<f64> = Vec::new();
1885            for tok in &foundation.carrier {
1886                if let Ok(num) = tok.parse::<f64>() {
1887                    if num.is_finite() {
1888                        resolved.push(num);
1889                        continue;
1890                    }
1891                }
1892                if let Some(p) = self.symbol_prob.get(tok) {
1893                    resolved.push(*p);
1894                }
1895            }
1896            self.strict_carrier = true;
1897            self.carrier = Some(resolved);
1898            self.carrier_label = Some(foundation.carrier.join(" "));
1899        }
1900        let frame = FoundationFrame {
1901            previous_active: std::mem::take(&mut self.active_foundation),
1902            snapshot,
1903            previous_active_implementations,
1904            previous_strict_carrier,
1905            previous_carrier,
1906            previous_carrier_label,
1907        };
1908        self.foundation_stack.push(frame);
1909        self.active_foundation = name.to_string();
1910        Ok(())
1911    }
1912
1913    pub fn exit_foundation(&mut self) {
1914        if let Some(frame) = self.foundation_stack.pop() {
1915            for (op_name, prev) in frame.snapshot.into_iter().rev() {
1916                match prev {
1917                    Some(op) => {
1918                        self.ops.insert(op_name, op);
1919                    }
1920                    None => {
1921                        self.ops.remove(&op_name);
1922                    }
1923                }
1924            }
1925            for (op_name, prev) in frame.previous_active_implementations.into_iter().rev() {
1926                match prev {
1927                    Some(implementation) => {
1928                        self.active_implementations.insert(op_name, implementation);
1929                    }
1930                    None => {
1931                        self.active_implementations.remove(&op_name);
1932                    }
1933                }
1934            }
1935            self.active_foundation = frame.previous_active;
1936            self.strict_carrier = frame.previous_strict_carrier;
1937            self.carrier = frame.previous_carrier;
1938            self.carrier_label = frame.previous_carrier_label;
1939        } else {
1940            self.active_foundation = "default-rml".to_string();
1941            self.active_implementations.clear();
1942            self.strict_carrier = false;
1943            self.carrier = None;
1944            self.carrier_label = None;
1945        }
1946    }
1947
1948    /// Check `value` against the active foundation's carrier. Returns `None`
1949    /// when the carrier is inactive or the value is legal, or a
1950    /// human-readable message otherwise (consumed by the caller to build an
1951    /// `E063` diagnostic). Mirrors the JS `Env.checkCarrierValue` helper.
1952    pub fn check_carrier_value(&self, value: f64) -> Option<String> {
1953        if !self.strict_carrier {
1954            return None;
1955        }
1956        let carrier = self.carrier.as_ref()?;
1957        if carrier.is_empty() {
1958            return None;
1959        }
1960        if !value.is_finite() {
1961            return None;
1962        }
1963        if carrier.iter().any(|c| (*c - value).abs() < 1e-12) {
1964            return None;
1965        }
1966        let mut sorted = carrier.clone();
1967        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1968        let allowed: Vec<String> = sorted.iter().map(|v| format_trace_value(*v)).collect();
1969        Some(format!(
1970            "value {} is not in active carrier {{{}}}",
1971            format_trace_value(value),
1972            allowed.join(", ")
1973        ))
1974    }
1975
1976    pub fn foundation_report(&self) -> FoundationReport {
1977        let active = if self.active_foundation.is_empty() {
1978            "default-rml".to_string()
1979        } else {
1980            self.active_foundation.clone()
1981        };
1982        let foundation = self.foundations.get(&active).cloned();
1983        let mut constructs = self.list_root_constructs();
1984        for rc in &mut constructs {
1985            if rc.semantic_status.is_none() {
1986                rc.semantic_status = semantic_status_for_trust_status(rc.status.as_deref());
1987            }
1988        }
1989        let mut by_status_map: std::collections::BTreeMap<String, Vec<String>> =
1990            std::collections::BTreeMap::new();
1991        let mut by_semantic_status_map: std::collections::BTreeMap<String, Vec<String>> =
1992            std::collections::BTreeMap::new();
1993        for rc in &constructs {
1994            let key = rc.status.clone().unwrap_or_else(|| "unknown".to_string());
1995            by_status_map.entry(key).or_default().push(rc.name.clone());
1996            let semantic_key =
1997                semantic_status_for_descriptor(rc).unwrap_or_else(|| "unknown".to_string());
1998            by_semantic_status_map
1999                .entry(semantic_key)
2000                .or_default()
2001                .push(rc.name.clone());
2002        }
2003        for v in by_status_map.values_mut() {
2004            v.sort();
2005        }
2006        for v in by_semantic_status_map.values_mut() {
2007            v.sort();
2008        }
2009        let by_status: Vec<(String, Vec<String>)> = by_status_map.into_iter().collect();
2010        let by_semantic_status: Vec<(String, Vec<String>)> =
2011            by_semantic_status_map.into_iter().collect();
2012        let mut foundations: Vec<FoundationDescriptor> =
2013            self.foundations.values().cloned().collect();
2014        foundations.sort_by(|a, b| a.name.cmp(&b.name));
2015        let mut active_implementations: Vec<ActiveImplementationDescriptor> =
2016            self.active_implementations.values().cloned().collect();
2017        for implementation in &mut active_implementations {
2018            if implementation.semantic_status.is_none() {
2019                implementation.semantic_status =
2020                    semantic_status_for_trust_status(implementation.status.as_deref());
2021            }
2022        }
2023        active_implementations.sort_by(|a, b| a.construct.cmp(&b.construct));
2024        let mut proof_rules: Vec<ProofRuleSnapshot> = self
2025            .proof_rules
2026            .values()
2027            .map(|r| ProofRuleSnapshot {
2028                name: r.name.clone(),
2029                premises: r.premises.iter().map(key_of).collect(),
2030                conclusion: key_of(&r.conclusion),
2031            })
2032            .collect();
2033        proof_rules.sort_by(|a, b| a.name.cmp(&b.name));
2034        let mut proof_assumptions: Vec<ProofAssumptionSnapshot> = self
2035            .proof_assumptions
2036            .values()
2037            .map(|a| ProofAssumptionSnapshot {
2038                name: a.name.clone(),
2039                kind: a.kind.clone(),
2040                judgement: key_of(&a.judgement),
2041            })
2042            .collect();
2043        proof_assumptions.sort_by(|a, b| a.name.cmp(&b.name));
2044        let mut proof_objects: Vec<ProofObjectSnapshot> = self
2045            .proof_objects
2046            .values()
2047            .map(|po| ProofObjectSnapshot {
2048                name: po.name.clone(),
2049                rule: po.rule.clone(),
2050                premises: po.premises.iter().map(key_of).collect(),
2051                premise_refs: po.premise_refs.clone(),
2052                conclusion: key_of(&po.conclusion),
2053            })
2054            .collect();
2055        proof_objects.sort_by(|a, b| a.name.cmp(&b.name));
2056        let mut allowed: Vec<String> = self.allowed_host_primitives.iter().cloned().collect();
2057        allowed.sort();
2058        let dependency_graph = build_dependency_graph(self);
2059        FoundationReport {
2060            active_foundation: active,
2061            description: foundation.as_ref().and_then(|f| f.description.clone()),
2062            numeric_domain: foundation.as_ref().and_then(|f| f.numeric_domain.clone()),
2063            truth_domain: foundation.as_ref().and_then(|f| f.truth_domain.clone()),
2064            root_constructs: constructs,
2065            by_status,
2066            by_semantic_status,
2067            foundations,
2068            active_implementations,
2069            proof_rules,
2070            proof_assumptions,
2071            proof_objects,
2072            strict_pure_links: self.strict_pure_links,
2073            allowed_host_primitives: allowed,
2074            dependency_graph,
2075        }
2076    }
2077
2078    /// Build a per-proof report (issue #97, Phase 13). Walks the proof
2079    /// object tree starting at `name`, collects the transitive
2080    /// dependencies (proof-objects, axioms, assumptions) and the
2081    /// registered root constructs that appear as leaf operators in the
2082    /// proof's premises/conclusion and in the rule patterns it
2083    /// transitively applies. Returns the report in all cases — when the
2084    /// proof object is missing, `verdict.ok` is `false`.
2085    pub fn proof_report(&self, name: &str) -> ProofReport {
2086        let active = if self.active_foundation.is_empty() {
2087            "default-rml".to_string()
2088        } else {
2089            self.active_foundation.clone()
2090        };
2091        if name.is_empty() {
2092            return ProofReport {
2093                name: String::new(),
2094                rule: None,
2095                conclusion: None,
2096                premises: Vec::new(),
2097                premise_refs: Vec::new(),
2098                verdict: ProofReportVerdict {
2099                    ok: false,
2100                    error: Some("proof name required".to_string()),
2101                },
2102                dependencies: Vec::new(),
2103                rules: Vec::new(),
2104                root_constructs_used: Vec::new(),
2105                by_semantic_status: Vec::new(),
2106                by_trust_status: Vec::new(),
2107                active_foundation: active,
2108                strict_pure_links: self.strict_pure_links,
2109            };
2110        }
2111        let po = match self.get_proof_object(name) {
2112            Some(po) => po.clone(),
2113            None => {
2114                return ProofReport {
2115                    name: name.to_string(),
2116                    rule: None,
2117                    conclusion: None,
2118                    premises: Vec::new(),
2119                    premise_refs: Vec::new(),
2120                    verdict: ProofReportVerdict {
2121                        ok: false,
2122                        error: Some(format!("unknown proof-object {}", name)),
2123                    },
2124                    dependencies: Vec::new(),
2125                    rules: Vec::new(),
2126                    root_constructs_used: Vec::new(),
2127                    by_semantic_status: Vec::new(),
2128                    by_trust_status: Vec::new(),
2129                    active_foundation: active,
2130                    strict_pure_links: self.strict_pure_links,
2131                };
2132            }
2133        };
2134        let verdict = check_proof_object(self, name);
2135        let verdict = match verdict {
2136            CheckProofVerdict::Ok(_) => ProofReportVerdict {
2137                ok: true,
2138                error: None,
2139            },
2140            CheckProofVerdict::Err(msg) => ProofReportVerdict {
2141                ok: false,
2142                error: Some(msg),
2143            },
2144        };
2145        let mut dependencies: Vec<ProofReportDependency> = Vec::new();
2146        let mut seen: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
2147        let mut rules: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
2148        let mut stack: Vec<String> = po.premise_refs.iter().cloned().rev().collect();
2149        while let Some(refname) = stack.pop() {
2150            if seen.contains(&refname) {
2151                continue;
2152            }
2153            seen.insert(refname.clone());
2154            if let Some(ax) = self.get_proof_assumption(&refname) {
2155                dependencies.push(ProofReportDependency {
2156                    name: ax.name.clone(),
2157                    kind: ax.kind.clone(),
2158                    rule: None,
2159                    judgement: Some(key_of(&ax.judgement)),
2160                });
2161                continue;
2162            }
2163            if let Some(dep) = self.get_proof_object(&refname) {
2164                for sub in dep.premise_refs.iter().rev() {
2165                    stack.push(sub.clone());
2166                }
2167                if !dep.rule.is_empty() {
2168                    rules.insert(dep.rule.clone());
2169                }
2170                dependencies.push(ProofReportDependency {
2171                    name: dep.name.clone(),
2172                    kind: "proof-object".to_string(),
2173                    rule: Some(dep.rule.clone()),
2174                    judgement: Some(key_of(&dep.conclusion)),
2175                });
2176                continue;
2177            }
2178            dependencies.push(ProofReportDependency {
2179                name: refname.clone(),
2180                kind: "unknown".to_string(),
2181                rule: None,
2182                judgement: None,
2183            });
2184        }
2185        if !po.rule.is_empty() {
2186            rules.insert(po.rule.clone());
2187        }
2188        let mut root_names: std::collections::BTreeSet<String> = [
2189            "proof-replay",
2190            "structural-equality",
2191            "structural-matcher",
2192            "substitution",
2193        ]
2194        .iter()
2195        .map(|s| s.to_string())
2196        .collect();
2197        fn collect_terms(
2198            node: &Node,
2199            registry: &std::collections::HashMap<String, RootConstructDescriptor>,
2200            into: &mut std::collections::BTreeSet<String>,
2201        ) {
2202            match node {
2203                Node::Leaf(s) => {
2204                    if registry.contains_key(s) {
2205                        into.insert(s.clone());
2206                    }
2207                }
2208                Node::List(children) => {
2209                    for child in children {
2210                        collect_terms(child, registry, into);
2211                    }
2212                }
2213            }
2214        }
2215        collect_terms(&po.conclusion, &self.root_constructs, &mut root_names);
2216        for prem in &po.premises {
2217            collect_terms(prem, &self.root_constructs, &mut root_names);
2218        }
2219        for rule_name in &rules {
2220            if let Some(rule) = self.get_proof_rule(rule_name) {
2221                collect_terms(&rule.conclusion, &self.root_constructs, &mut root_names);
2222                for prem in &rule.premises {
2223                    collect_terms(prem, &self.root_constructs, &mut root_names);
2224                }
2225            }
2226        }
2227        let root_constructs_used: Vec<String> = root_names.into_iter().collect();
2228        let mut by_semantic_status_map: std::collections::BTreeMap<String, Vec<String>> =
2229            std::collections::BTreeMap::new();
2230        let mut by_trust_status_map: std::collections::BTreeMap<String, Vec<String>> =
2231            std::collections::BTreeMap::new();
2232        for rc_name in &root_constructs_used {
2233            if let Some(rc) = self.root_constructs.get(rc_name) {
2234                let semantic =
2235                    semantic_status_for_descriptor(rc).unwrap_or_else(|| "unknown".to_string());
2236                let trust = rc.status.clone().unwrap_or_else(|| "unknown".to_string());
2237                by_semantic_status_map
2238                    .entry(semantic)
2239                    .or_default()
2240                    .push(rc_name.clone());
2241                by_trust_status_map
2242                    .entry(trust)
2243                    .or_default()
2244                    .push(rc_name.clone());
2245            }
2246        }
2247        for v in by_semantic_status_map.values_mut() {
2248            v.sort();
2249        }
2250        for v in by_trust_status_map.values_mut() {
2251            v.sort();
2252        }
2253        ProofReport {
2254            name: name.to_string(),
2255            rule: Some(po.rule.clone()),
2256            conclusion: Some(key_of(&po.conclusion)),
2257            premises: po.premises.iter().map(key_of).collect(),
2258            premise_refs: po.premise_refs.clone(),
2259            verdict,
2260            dependencies,
2261            rules: rules.into_iter().collect(),
2262            root_constructs_used,
2263            by_semantic_status: by_semantic_status_map.into_iter().collect(),
2264            by_trust_status: by_trust_status_map.into_iter().collect(),
2265            active_foundation: active,
2266            strict_pure_links: self.strict_pure_links,
2267        }
2268    }
2269
2270    /// Return the transitive closure of a construct's dependencies,
2271    /// breadth-first and deterministically sorted at every level.
2272    /// Missing intermediate deps are silently retained (so a downstream
2273    /// caller can detect dangling names by intersecting against
2274    /// `root_constructs.keys()`). Returns `None` if the root itself is
2275    /// unknown.
2276    pub fn dependency_closure(&self, name: &str) -> Option<Vec<String>> {
2277        if name.is_empty() {
2278            return None;
2279        }
2280        if !self.root_constructs.contains_key(name) {
2281            return None;
2282        }
2283        Some(closure_for(self, name))
2284    }
2285
2286    /// Register a declared rule of inference. Data-only.
2287    pub fn register_proof_rule(&mut self, rule: ProofRule) {
2288        self.proof_rules.insert(rule.name.clone(), rule);
2289    }
2290
2291    /// Register an explicit proof assumption/axiom. Data-only; proof objects
2292    /// cite these leaves via `(premise-by <name>)` or `(uses <name>...)`.
2293    pub fn register_proof_assumption(&mut self, assumption: ProofAssumption) {
2294        self.proof_assumptions
2295            .insert(assumption.name.clone(), assumption);
2296    }
2297
2298    /// Register a concrete derivation. Data-only; verification runs lazily
2299    /// when `(check-proof <name>)` evaluates.
2300    pub fn register_proof_object(&mut self, po: ProofObject) {
2301        self.proof_objects.insert(po.name.clone(), po);
2302    }
2303
2304    pub fn get_proof_rule(&self, name: &str) -> Option<&ProofRule> {
2305        self.proof_rules.get(name)
2306    }
2307
2308    pub fn get_proof_assumption(&self, name: &str) -> Option<&ProofAssumption> {
2309        self.proof_assumptions.get(name)
2310    }
2311
2312    pub fn get_proof_object(&self, name: &str) -> Option<&ProofObject> {
2313        self.proof_objects.get(name)
2314    }
2315
2316    pub fn get_op(&self, name: &str) -> Option<&Op> {
2317        if let Some(op) = self.ops.get(name) {
2318            return Some(op);
2319        }
2320        let resolved = self.resolve_qualified(name);
2321        if resolved != name {
2322            return self.ops.get(&resolved);
2323        }
2324        None
2325    }
2326
2327    pub fn has_op(&self, name: &str) -> bool {
2328        if self.ops.contains_key(name) {
2329            return true;
2330        }
2331        let resolved = self.resolve_qualified(name);
2332        resolved != name && self.ops.contains_key(&resolved)
2333    }
2334
2335    /// Apply the active namespace to a freshly declared name, e.g. inside
2336    /// `(namespace classical)` the form `(and: min)` registers `classical.and`,
2337    /// not `and`. Names that already contain a `.` are passed through.
2338    /// Mirrors `Env.qualifyName` in `js/src/rml-links.mjs`.
2339    pub fn qualify_name(&self, name: &str) -> String {
2340        if let Some(ns) = &self.namespace {
2341            if !name.contains('.') {
2342                return format!("{}.{}", ns, name);
2343            }
2344        }
2345        name.to_string()
2346    }
2347
2348    /// Resolve a possibly-qualified name to its canonical storage key. Order:
2349    ///   1. Alias prefix: `cl.foo` with alias `cl -> classical` becomes
2350    ///      `classical.foo`.
2351    ///   2. Active namespace: an unqualified name lives in `<ns>.<name>`.
2352    ///   3. Bare name: returned unchanged.
2353    /// Used by lookup helpers (operators, symbol probabilities) to find
2354    /// namespaced bindings without forcing every call site to spell them out.
2355    /// Mirrors `Env._resolveQualified` in `js/src/rml-links.mjs`.
2356    pub fn resolve_qualified(&self, name: &str) -> String {
2357        if let Some(dot_idx) = name.find('.') {
2358            if dot_idx > 0 {
2359                let prefix = &name[..dot_idx];
2360                let rest = &name[dot_idx + 1..];
2361                if let Some(target_ns) = self.aliases.get(prefix) {
2362                    return format!("{}.{}", target_ns, rest);
2363                }
2364            }
2365            return name.to_string();
2366        }
2367        if let Some(ns) = &self.namespace {
2368            let qualified = format!("{}.{}", ns, name);
2369            if self.ops.contains_key(&qualified)
2370                || self.symbol_prob.contains_key(&qualified)
2371                || self.terms.contains(&qualified)
2372                || self.types.contains_key(&qualified)
2373                || self.lambdas.contains_key(&qualified)
2374                || self.templates.contains_key(&qualified)
2375            {
2376                return qualified;
2377            }
2378        }
2379        name.to_string()
2380    }
2381
2382    pub fn set_expr_prob(&mut self, expr_node: &Node, p: f64) {
2383        self.assign.insert(key_of(expr_node), self.clamp(p));
2384    }
2385
2386    pub fn set_symbol_prob(&mut self, sym: &str, p: f64) {
2387        self.symbol_prob.insert(sym.to_string(), self.clamp(p));
2388    }
2389
2390    pub fn get_symbol_prob(&self, sym: &str) -> f64 {
2391        if let Some(&v) = self.symbol_prob.get(sym) {
2392            return v;
2393        }
2394        let resolved = self.resolve_qualified(sym);
2395        if resolved != sym {
2396            if let Some(&v) = self.symbol_prob.get(&resolved) {
2397                return v;
2398            }
2399        }
2400        self.mid()
2401    }
2402
2403    /// Push a trace event when tracing is enabled. The event's span is taken
2404    /// from `current_span` if set, else `default_span`. Mirrors `Env.trace`
2405    /// in the JavaScript implementation.
2406    pub fn trace(&mut self, kind: &str, detail: impl Into<String>) {
2407        if !self.trace_enabled {
2408            return;
2409        }
2410        let span = self
2411            .current_span
2412            .clone()
2413            .unwrap_or_else(|| self.default_span.clone());
2414        self.trace_events.push(TraceEvent::new(kind, detail, span));
2415    }
2416
2417    pub fn set_type(&mut self, expr: &str, type_expr: &str) {
2418        self.types.insert(expr.to_string(), type_expr.to_string());
2419    }
2420
2421    pub fn get_type(&self, expr: &str) -> Option<&String> {
2422        if let Some(recorded) = self.types.get(expr) {
2423            return Some(recorded);
2424        }
2425        let resolved = self.resolve_qualified(expr);
2426        if resolved != expr {
2427            return self.types.get(&resolved);
2428        }
2429        None
2430    }
2431
2432    pub fn set_lambda(&mut self, name: &str, lambda: Lambda) {
2433        self.lambdas.insert(name.to_string(), lambda);
2434    }
2435
2436    pub fn get_lambda(&self, name: &str) -> Option<&Lambda> {
2437        if let Some(l) = self.lambdas.get(name) {
2438            return Some(l);
2439        }
2440        let resolved = self.resolve_qualified(name);
2441        if resolved != name {
2442            return self.lambdas.get(&resolved);
2443        }
2444        None
2445    }
2446
2447    /// Apply an operator by name to the given values.
2448    pub fn apply_op(&self, name: &str, vals: &[f64]) -> f64 {
2449        let op = match self.ops.get(name) {
2450            Some(op) => op.clone(),
2451            None => {
2452                let resolved = self.resolve_qualified(name);
2453                if resolved != name {
2454                    match self.ops.get(&resolved) {
2455                        Some(op) => op.clone(),
2456                        None => panic!("Unknown op: {}", name),
2457                    }
2458                } else {
2459                    panic!("Unknown op: {}", name)
2460                }
2461            }
2462        };
2463        match op {
2464            Op::Not => {
2465                if vals.is_empty() {
2466                    self.lo
2467                } else {
2468                    self.hi - (vals[0] - self.lo)
2469                }
2470            }
2471            Op::Agg(agg) => dec_round(agg.apply(vals, self.lo)),
2472            Op::Eq | Op::Neq => self.lo,
2473            Op::Compose {
2474                ref outer,
2475                ref inner,
2476            } => {
2477                let inner_result = self.apply_op(inner, vals);
2478                self.apply_op(outer, &[inner_result])
2479            }
2480            Op::Add => {
2481                if vals.len() >= 2 {
2482                    dec_round(vals[0] + vals[1])
2483                } else {
2484                    0.0
2485                }
2486            }
2487            Op::Sub => {
2488                if vals.len() >= 2 {
2489                    dec_round(vals[0] - vals[1])
2490                } else {
2491                    0.0
2492                }
2493            }
2494            Op::Mul => {
2495                if vals.len() >= 2 {
2496                    dec_round(vals[0] * vals[1])
2497                } else {
2498                    0.0
2499                }
2500            }
2501            Op::Div => {
2502                if vals.len() >= 2 && vals[1] != 0.0 {
2503                    dec_round(vals[0] / vals[1])
2504                } else {
2505                    0.0
2506                }
2507            }
2508            Op::Less => {
2509                if vals.len() >= 2 && vals[0] < vals[1] {
2510                    self.hi
2511                } else {
2512                    self.lo
2513                }
2514            }
2515            Op::LessOrEqual => {
2516                if vals.len() >= 2 && vals[0] <= vals[1] {
2517                    self.hi
2518                } else {
2519                    self.lo
2520                }
2521            }
2522            Op::TruthTable {
2523                ref rows,
2524                ref fallback,
2525            } => {
2526                for row in rows {
2527                    if row.inputs.len() != vals.len() {
2528                        continue;
2529                    }
2530                    if row
2531                        .inputs
2532                        .iter()
2533                        .zip(vals.iter())
2534                        .all(|(a, b)| (*a - *b).abs() < 1e-12)
2535                    {
2536                        return row.output;
2537                    }
2538                }
2539                match fallback {
2540                    Some(prev) => self.apply_op_inner(prev, vals),
2541                    None => self.lo,
2542                }
2543            }
2544        }
2545    }
2546
2547    /// Internal helper used by `Op::TruthTable` fallback dispatch so a
2548    /// table can delegate to a previously installed op without going
2549    /// through the name lookup path again.
2550    fn apply_op_inner(&self, op: &Op, vals: &[f64]) -> f64 {
2551        let owned = op.clone();
2552        match owned {
2553            Op::Not => {
2554                if vals.is_empty() {
2555                    self.lo
2556                } else {
2557                    self.hi - (vals[0] - self.lo)
2558                }
2559            }
2560            Op::Agg(agg) => dec_round(agg.apply(vals, self.lo)),
2561            Op::Eq | Op::Neq => self.lo,
2562            Op::Compose { outer, inner } => {
2563                let inner_result = self.apply_op(&inner, vals);
2564                self.apply_op(&outer, &[inner_result])
2565            }
2566            Op::Add => {
2567                if vals.len() >= 2 {
2568                    dec_round(vals[0] + vals[1])
2569                } else {
2570                    0.0
2571                }
2572            }
2573            Op::Sub => {
2574                if vals.len() >= 2 {
2575                    dec_round(vals[0] - vals[1])
2576                } else {
2577                    0.0
2578                }
2579            }
2580            Op::Mul => {
2581                if vals.len() >= 2 {
2582                    dec_round(vals[0] * vals[1])
2583                } else {
2584                    0.0
2585                }
2586            }
2587            Op::Div => {
2588                if vals.len() >= 2 && vals[1] != 0.0 {
2589                    dec_round(vals[0] / vals[1])
2590                } else {
2591                    0.0
2592                }
2593            }
2594            Op::Less => {
2595                if vals.len() >= 2 && vals[0] < vals[1] {
2596                    self.hi
2597                } else {
2598                    self.lo
2599                }
2600            }
2601            Op::LessOrEqual => {
2602                if vals.len() >= 2 && vals[0] <= vals[1] {
2603                    self.hi
2604                } else {
2605                    self.lo
2606                }
2607            }
2608            Op::TruthTable { rows, fallback } => {
2609                for row in &rows {
2610                    if row.inputs.len() != vals.len() {
2611                        continue;
2612                    }
2613                    if row
2614                        .inputs
2615                        .iter()
2616                        .zip(vals.iter())
2617                        .all(|(a, b)| (*a - *b).abs() < 1e-12)
2618                    {
2619                        return row.output;
2620                    }
2621                }
2622                match fallback {
2623                    Some(prev) => self.apply_op_inner(&prev, vals),
2624                    None => self.lo,
2625                }
2626            }
2627        }
2628    }
2629
2630    /// Apply equality operator, checking assigned probabilities first.
2631    /// Takes `&mut self` so it can emit `lookup` trace events.
2632    pub fn apply_eq(&mut self, left: &Node, right: &Node) -> f64 {
2633        if let Some(value) = lookup_assigned_infix(self, "=", left, right) {
2634            return self.clamp(value);
2635        }
2636        let options = ConvertOptions::default();
2637        let left_term = normalize_term(left, self, options);
2638        let right_term = normalize_term(right, self, options);
2639        equality_truth_value(left, right, &left_term, &right_term, self, options)
2640    }
2641
2642    /// Apply inequality operator: not(eq(L, R))
2643    pub fn apply_neq(&mut self, left: &Node, right: &Node) -> f64 {
2644        let eq_val = self.apply_eq(left, right);
2645        self.apply_op("not", &[eq_val])
2646    }
2647
2648    /// Reinitialize ops when range changes (resets to defaults for current range).
2649    pub fn reinit_ops(&mut self) {
2650        self.ops.insert("not".to_string(), Op::Not);
2651        self.ops.insert("and".to_string(), Op::Agg(Aggregator::Avg));
2652        self.ops.insert("or".to_string(), Op::Agg(Aggregator::Max));
2653        self.ops
2654            .insert("both".to_string(), Op::Agg(Aggregator::Avg));
2655        self.ops
2656            .insert("neither".to_string(), Op::Agg(Aggregator::Prod));
2657        self.ops.insert("=".to_string(), Op::Eq);
2658        self.ops.insert("!=".to_string(), Op::Neq);
2659        self.ops.insert("+".to_string(), Op::Add);
2660        self.ops.insert("-".to_string(), Op::Sub);
2661        self.ops.insert("*".to_string(), Op::Mul);
2662        self.ops.insert("/".to_string(), Op::Div);
2663        self.ops.insert("<".to_string(), Op::Less);
2664        self.ops.insert("<=".to_string(), Op::LessOrEqual);
2665        // Re-initialize truth constants for new range
2666        self.init_truth_constants();
2667    }
2668}
2669
2670// ========== Query Result ==========
2671
2672/// Result of evaluating an expression: either a plain value or a query result.
2673#[derive(Debug, Clone)]
2674pub enum EvalResult {
2675    Value(f64),
2676    Query(f64),
2677    TypeQuery(String),
2678    Term(Node),
2679}
2680
2681impl EvalResult {
2682    pub fn as_f64(&self) -> f64 {
2683        match self {
2684            EvalResult::Value(v) | EvalResult::Query(v) => *v,
2685            EvalResult::TypeQuery(_) | EvalResult::Term(_) => 0.0,
2686        }
2687    }
2688
2689    pub fn is_query(&self) -> bool {
2690        matches!(self, EvalResult::Query(_) | EvalResult::TypeQuery(_))
2691    }
2692
2693    pub fn is_type_query(&self) -> bool {
2694        matches!(self, EvalResult::TypeQuery(_))
2695    }
2696
2697    pub fn type_string(&self) -> Option<&str> {
2698        match self {
2699            EvalResult::TypeQuery(s) => Some(s),
2700            _ => None,
2701        }
2702    }
2703}
2704
2705// ========== Binding Parser ==========
2706
2707/// Parse a binding form in two supported syntaxes:
2708/// 1. Colon form: (x: A) as ["x:", A] — standard LiNo link definition syntax
2709/// 2. Prefix type form: (A x) as ["A", "x"] — type-first notation for lambda/Pi bindings
2710///    e.g. (Natural x), used in (lambda (Natural x) body)
2711/// Returns (param_name, param_type_key) or None.
2712pub fn parse_binding(binding: &Node) -> Option<(String, String)> {
2713    if let Node::List(children) = binding {
2714        if children.len() == 2 {
2715            // Colon form: ["x:", A]
2716            if let Node::Leaf(ref s) = children[0] {
2717                if s.ends_with(':') {
2718                    let param_name = s[..s.len() - 1].to_string();
2719                    let param_type = match &children[1] {
2720                        Node::Leaf(s) => s.clone(),
2721                        other => key_of(other),
2722                    };
2723                    return Some((param_name, param_type));
2724                }
2725            }
2726            // Prefix type form: ["A", "x"] — type name first (must start with uppercase)
2727            if let (Node::Leaf(ref type_name), Node::Leaf(ref var_name)) =
2728                (&children[0], &children[1])
2729            {
2730                if type_name.starts_with(|c: char| c.is_uppercase()) && !var_name.ends_with(':') {
2731                    return Some((var_name.clone(), type_name.clone()));
2732                }
2733            }
2734            // Prefix complex-type form: [<list-type>, "x"] — type is a list expression
2735            // such as (Pi (A x) B) or (Type 0). Needed for higher-order parameters
2736            // (e.g. polymorphic apply / compose) where a parameter is itself function-typed.
2737            if let (Node::List(_), Node::Leaf(ref var_name)) = (&children[0], &children[1]) {
2738                if !var_name.ends_with(':') {
2739                    return Some((var_name.clone(), key_of(&children[0])));
2740                }
2741            }
2742        }
2743    }
2744    None
2745}
2746
2747/// Parse comma-separated bindings: (Natural x, Natural y) → vec of (name, type) pairs.
2748/// Tokens arrive as ["Natural", "x,", "Natural", "y"] or ["Natural", "x"] (single binding).
2749pub fn parse_bindings(binding: &Node) -> Option<Vec<(String, String)>> {
2750    // Try single binding first
2751    if let Some(single) = parse_binding(binding) {
2752        return Some(vec![single]);
2753    }
2754    // Try comma-separated
2755    if let Node::List(children) = binding {
2756        let mut tokens: Vec<String> = Vec::new();
2757        for child in children {
2758            if let Node::Leaf(ref s) = child {
2759                if s.ends_with(',') {
2760                    tokens.push(s[..s.len() - 1].to_string());
2761                    tokens.push(",".to_string());
2762                } else {
2763                    tokens.push(s.clone());
2764                }
2765            } else {
2766                return None;
2767            }
2768        }
2769        let mut bindings = Vec::new();
2770        let mut i = 0;
2771        while i < tokens.len() {
2772            if tokens[i] == "," {
2773                i += 1;
2774                continue;
2775            }
2776            if i + 1 < tokens.len() && tokens[i + 1] != "," {
2777                let type_name = &tokens[i];
2778                let var_name = &tokens[i + 1];
2779                if type_name.starts_with(|c: char| c.is_uppercase()) {
2780                    bindings.push((var_name.clone(), type_name.clone()));
2781                    i += 2;
2782                    continue;
2783                }
2784            }
2785            return None;
2786        }
2787        if !bindings.is_empty() {
2788            return Some(bindings);
2789        }
2790    }
2791    None
2792}
2793
2794// ========== Substitution ==========
2795
2796/// Capture-avoiding substitution for kernel terms. `subst` is the public
2797/// primitive name; `substitute` remains as the backwards-compatible helper.
2798#[derive(Debug, Clone, PartialEq)]
2799enum BinderKind {
2800    Lambda,
2801    Pi,
2802    Fresh,
2803}
2804
2805#[derive(Debug, Clone)]
2806struct BinderInfo {
2807    kind: BinderKind,
2808    params: Vec<String>,
2809    body_index: usize,
2810    binding_index: usize,
2811}
2812
2813fn non_variable_token(s: &str) -> bool {
2814    matches!(
2815        s,
2816        "lambda"
2817            | "Pi"
2818            | "fresh"
2819            | "in"
2820            | "subst"
2821            | "apply"
2822            | "type"
2823            | "of"
2824            | "has"
2825            | "probability"
2826            | "with"
2827            | "proof"
2828            | "range"
2829            | "valence"
2830            | "namespace"
2831            | "import"
2832            | "as"
2833            | "is"
2834            | "?"
2835            | "mode"
2836            | "relation"
2837            | "total"
2838            | "coverage"
2839            | "world"
2840            | "inductive"
2841            | "coinductive"
2842            | "constructor"
2843            | "define"
2844            | "case"
2845            | "measure"
2846            | "lex"
2847            | "terminating"
2848            | "whnf"
2849            | "nf"
2850            | "normal-form"
2851            | "template"
2852            | "+"
2853            | "-"
2854            | "*"
2855            | "/"
2856            | "<"
2857            | "<="
2858            | "="
2859            | "!="
2860            | "and"
2861            | "or"
2862            | "not"
2863            | "both"
2864            | "neither"
2865            | "nor"
2866    )
2867}
2868
2869fn token_base_name(token: &str) -> String {
2870    token.trim_end_matches(|c| c == ':' || c == ',').to_string()
2871}
2872
2873fn is_variable_token(token: &str) -> bool {
2874    let base = token_base_name(token);
2875    !base.is_empty() && base == token && !is_num(&base) && !non_variable_token(&base)
2876}
2877
2878fn binding_param_names(binding: &Node) -> Vec<String> {
2879    parse_bindings(binding)
2880        .map(|bindings| bindings.into_iter().map(|(name, _)| name).collect())
2881        .unwrap_or_default()
2882}
2883
2884fn binder_info(expr: &Node) -> Option<BinderInfo> {
2885    if let Node::List(children) = expr {
2886        if children.len() == 3 {
2887            if let Node::Leaf(head) = &children[0] {
2888                if head == "lambda" || head == "Pi" {
2889                    let params = binding_param_names(&children[1]);
2890                    if !params.is_empty() {
2891                        return Some(BinderInfo {
2892                            kind: if head == "lambda" {
2893                                BinderKind::Lambda
2894                            } else {
2895                                BinderKind::Pi
2896                            },
2897                            params,
2898                            body_index: 2,
2899                            binding_index: 1,
2900                        });
2901                    }
2902                }
2903            }
2904        }
2905        if children.len() == 4 {
2906            if let (Node::Leaf(head), Node::Leaf(var_name), Node::Leaf(in_kw)) =
2907                (&children[0], &children[1], &children[2])
2908            {
2909                if head == "fresh" && in_kw == "in" {
2910                    return Some(BinderInfo {
2911                        kind: BinderKind::Fresh,
2912                        params: vec![var_name.clone()],
2913                        body_index: 3,
2914                        binding_index: 1,
2915                    });
2916                }
2917            }
2918        }
2919    }
2920    None
2921}
2922
2923fn free_variables(expr: &Node) -> HashSet<String> {
2924    fn walk(expr: &Node, bound: &HashSet<String>, out: &mut HashSet<String>) {
2925        match expr {
2926            Node::Leaf(s) => {
2927                if is_variable_token(s) && !bound.contains(s) {
2928                    out.insert(s.clone());
2929                }
2930            }
2931            Node::List(children) => {
2932                if let Some(binder) = binder_info(expr) {
2933                    if binder.kind != BinderKind::Fresh {
2934                        let params: HashSet<String> = binder.params.iter().cloned().collect();
2935                        if let Node::List(binding_children) = &children[binder.binding_index] {
2936                            for child in binding_children {
2937                                if let Node::Leaf(s) = child {
2938                                    if params.contains(&token_base_name(s)) {
2939                                        continue;
2940                                    }
2941                                }
2942                                walk(child, bound, out);
2943                            }
2944                        }
2945                    }
2946                    let mut nested = bound.clone();
2947                    for param in binder.params {
2948                        nested.insert(param);
2949                    }
2950                    walk(&children[binder.body_index], &nested, out);
2951                    return;
2952                }
2953                for child in children {
2954                    walk(child, bound, out);
2955                }
2956            }
2957        }
2958    }
2959
2960    let mut out = HashSet::new();
2961    walk(expr, &HashSet::new(), &mut out);
2962    out
2963}
2964
2965fn contains_free(expr: &Node, name: &str) -> bool {
2966    free_variables(expr).contains(name)
2967}
2968
2969fn env_can_evaluate_name(env: &Env, name: &str) -> bool {
2970    if env.symbol_prob.contains_key(name)
2971        || env.terms.contains(name)
2972        || env.types.contains_key(name)
2973        || env.lambdas.contains_key(name)
2974        || env.ops.contains_key(name)
2975        || env.templates.contains_key(name)
2976    {
2977        return true;
2978    }
2979    let resolved = env.resolve_qualified(name);
2980    resolved != name
2981        && (env.symbol_prob.contains_key(&resolved)
2982            || env.terms.contains(&resolved)
2983            || env.types.contains_key(&resolved)
2984            || env.lambdas.contains_key(&resolved)
2985            || env.ops.contains_key(&resolved)
2986            || env.templates.contains_key(&resolved))
2987}
2988
2989fn has_unresolved_free_variables(expr: &Node, env: &Env) -> bool {
2990    free_variables(expr)
2991        .iter()
2992        .any(|name| !env_can_evaluate_name(env, name))
2993}
2994
2995fn collect_names(expr: &Node, out: &mut HashSet<String>) {
2996    match expr {
2997        Node::Leaf(s) => {
2998            let base = token_base_name(s);
2999            if !base.is_empty() && !is_num(&base) && !non_variable_token(&base) {
3000                out.insert(base);
3001            }
3002        }
3003        Node::List(children) => {
3004            for child in children {
3005                collect_names(child, out);
3006            }
3007        }
3008    }
3009}
3010
3011fn fresh_name(base: &str, avoid: &HashSet<String>) -> String {
3012    let mut i = 1;
3013    loop {
3014        let candidate = format!("{}_{}", base, i);
3015        if !avoid.contains(&candidate) {
3016            return candidate;
3017        }
3018        i += 1;
3019    }
3020}
3021
3022fn rename_binding_param(binding: &Node, old_name: &str, new_name: &str) -> Node {
3023    if let Node::List(children) = binding {
3024        return Node::List(
3025            children
3026                .iter()
3027                .map(|child| match child {
3028                    Node::Leaf(s) if s == old_name => Node::Leaf(new_name.to_string()),
3029                    Node::Leaf(s) if s == &format!("{},", old_name) => {
3030                        Node::Leaf(format!("{},", new_name))
3031                    }
3032                    Node::Leaf(s) if s == &format!("{}:", old_name) => {
3033                        Node::Leaf(format!("{}:", new_name))
3034                    }
3035                    _ => child.clone(),
3036                })
3037                .collect(),
3038        );
3039    }
3040    binding.clone()
3041}
3042
3043fn rename_bound_occurrences(expr: &Node, old_name: &str, new_name: &str) -> Node {
3044    match expr {
3045        Node::Leaf(s) => {
3046            if s == old_name {
3047                Node::Leaf(new_name.to_string())
3048            } else {
3049                expr.clone()
3050            }
3051        }
3052        Node::List(children) => {
3053            if let Some(binder) = binder_info(expr) {
3054                if binder.params.iter().any(|param| param == old_name) {
3055                    return expr.clone();
3056                }
3057            }
3058            Node::List(
3059                children
3060                    .iter()
3061                    .map(|child| rename_bound_occurrences(child, old_name, new_name))
3062                    .collect(),
3063            )
3064        }
3065    }
3066}
3067
3068fn rename_binder(expr: &Node, binder: &BinderInfo, old_name: &str, new_name: &str) -> Node {
3069    if let Node::List(children) = expr {
3070        let mut out = children.clone();
3071        if binder.kind == BinderKind::Fresh {
3072            out[binder.binding_index] = Node::Leaf(new_name.to_string());
3073        } else {
3074            out[binder.binding_index] =
3075                rename_binding_param(&out[binder.binding_index], old_name, new_name);
3076        }
3077        out[binder.body_index] =
3078            rename_bound_occurrences(&out[binder.body_index], old_name, new_name);
3079        Node::List(out)
3080    } else {
3081        expr.clone()
3082    }
3083}
3084
3085/// Substitute all free occurrences of variable `name` with `replacement` in `expr`.
3086pub fn subst(expr: &Node, name: &str, replacement: &Node) -> Node {
3087    match expr {
3088        Node::Leaf(s) => {
3089            if s == name {
3090                replacement.clone()
3091            } else {
3092                expr.clone()
3093            }
3094        }
3095        Node::List(children) => {
3096            if let Some(binder) = binder_info(expr) {
3097                if binder.params.iter().any(|param| param == name) {
3098                    return expr.clone(); // shadowed
3099                }
3100                let mut current = expr.clone();
3101                let replacement_free = free_variables(replacement);
3102                if contains_free(&children[binder.body_index], name) {
3103                    let mut avoid = HashSet::new();
3104                    collect_names(&current, &mut avoid);
3105                    collect_names(replacement, &mut avoid);
3106                    avoid.insert(name.to_string());
3107                    for param in &binder.params {
3108                        if replacement_free.contains(param) {
3109                            let next = fresh_name(param, &avoid);
3110                            avoid.insert(next.clone());
3111                            let current_binder = binder_info(&current).expect("renamed binder");
3112                            current = rename_binder(&current, &current_binder, param, &next);
3113                        }
3114                    }
3115                }
3116                if let Node::List(current_children) = current {
3117                    return Node::List(
3118                        current_children
3119                            .iter()
3120                            .map(|child| subst(child, name, replacement))
3121                            .collect(),
3122                    );
3123                }
3124            }
3125            Node::List(
3126                children
3127                    .iter()
3128                    .map(|child| subst(child, name, replacement))
3129                    .collect(),
3130            )
3131        }
3132    }
3133}
3134
3135/// Backwards-compatible alias for [`subst`].
3136pub fn substitute(expr: &Node, name: &str, replacement: &Node) -> Node {
3137    subst(expr, name, replacement)
3138}
3139
3140// ========== Template expansion (issue #59) ==========
3141
3142fn template_key_for(env: &Env, name: &str) -> Option<String> {
3143    if env.templates.contains_key(name) {
3144        return Some(name.to_string());
3145    }
3146    let resolved = env.resolve_qualified(name);
3147    if resolved != name && env.templates.contains_key(&resolved) {
3148        return Some(resolved);
3149    }
3150    None
3151}
3152
3153fn validate_template_pattern(pattern: &Node) -> Result<(String, Vec<String>), String> {
3154    let children = match pattern {
3155        Node::List(items) if !items.is_empty() => items,
3156        _ => {
3157            return Err(
3158                "Template declaration must be `(template (<name> <param>...) <body>)`".to_string(),
3159            );
3160        }
3161    };
3162    let name = match &children[0] {
3163        Node::Leaf(s) if is_variable_token(s) => s.clone(),
3164        Node::Leaf(s) => {
3165            return Err(format!(
3166                "Template name must be a bare identifier (got \"{}\")",
3167                s
3168            ));
3169        }
3170        other => {
3171            return Err(format!(
3172                "Template name must be a bare identifier (got \"{}\")",
3173                key_of(other)
3174            ));
3175        }
3176    };
3177
3178    let mut params = Vec::new();
3179    let mut seen = HashSet::new();
3180    for param in &children[1..] {
3181        let p = match param {
3182            Node::Leaf(s) if is_variable_token(s) => s.clone(),
3183            Node::Leaf(s) => {
3184                return Err(format!(
3185                    "Template parameter must be a bare identifier (got \"{}\")",
3186                    s
3187                ));
3188            }
3189            other => {
3190                return Err(format!(
3191                    "Template parameter must be a bare identifier (got \"{}\")",
3192                    key_of(other)
3193                ));
3194            }
3195        };
3196        if !seen.insert(p.clone()) {
3197            return Err(format!(
3198                "Template parameter \"{}\" is declared more than once",
3199                p
3200            ));
3201        }
3202        params.push(p);
3203    }
3204    Ok((name, params))
3205}
3206
3207/// Merge an incoming root-construct descriptor with the previously stored
3208/// one. The merge prefers explicitly set fields from the new descriptor
3209/// but preserves information the new descriptor leaves unspecified, so
3210/// multiple `(root-construct …)` forms can build the record incrementally
3211/// without clobbering already-known fields.
3212fn merge_root_construct_descriptors(
3213    previous: Option<RootConstructDescriptor>,
3214    next: RootConstructDescriptor,
3215) -> RootConstructDescriptor {
3216    let mut base = previous.unwrap_or_else(|| RootConstructDescriptor {
3217        name: next.name.clone(),
3218        ..Default::default()
3219    });
3220    base.name = next.name;
3221    if next.status.is_some() {
3222        base.status = next.status;
3223    }
3224    if next.semantic_status.is_some() {
3225        base.semantic_status = next.semantic_status;
3226    }
3227    if next.kind.is_some() {
3228        base.kind = next.kind;
3229    }
3230    if !next.depends_on.is_empty() {
3231        let mut seen: std::collections::HashSet<String> =
3232            base.depends_on.iter().cloned().collect();
3233        for d in next.depends_on {
3234            if !seen.contains(&d) {
3235                seen.insert(d.clone());
3236                base.depends_on.push(d);
3237            }
3238        }
3239    }
3240    if next.encoded_as.is_some() {
3241        base.encoded_as = next.encoded_as;
3242    }
3243    if next.pure_links_ready.is_some() {
3244        base.pure_links_ready = next.pure_links_ready;
3245    }
3246    if next.override_with.is_some() {
3247        base.override_with = next.override_with;
3248    }
3249    if next.planned_as.is_some() {
3250        base.planned_as = next.planned_as;
3251    }
3252    if next.foundation.is_some() {
3253        base.foundation = next.foundation;
3254    }
3255    base
3256}
3257
3258const SEMANTIC_STATUS_ORDER: [&str; 5] = [
3259    "host-trusted",
3260    "links-described",
3261    "links-checked",
3262    "links-evaluated",
3263    "self-hosted",
3264];
3265
3266fn semantic_status_for_trust_status(status: Option<&str>) -> Option<String> {
3267    match status {
3268        Some("host-primitive")
3269        | Some("host-derived")
3270        | Some("external-trusted")
3271        | Some("user-configurable")
3272        | Some("user-overridden") => Some("host-trusted".to_string()),
3273        Some("links-encoded") | Some("planned") => Some("links-described".to_string()),
3274        Some("links-defined") => Some("links-checked".to_string()),
3275        _ => None,
3276    }
3277}
3278
3279fn semantic_status_for_descriptor(descriptor: &RootConstructDescriptor) -> Option<String> {
3280    descriptor
3281        .semantic_status
3282        .clone()
3283        .or_else(|| semantic_status_for_trust_status(descriptor.status.as_deref()))
3284}
3285
3286fn merge_foundation_descriptors(
3287    previous: Option<FoundationDescriptor>,
3288    next: FoundationDescriptor,
3289) -> FoundationDescriptor {
3290    let mut base = previous.unwrap_or_else(|| FoundationDescriptor {
3291        name: next.name.clone(),
3292        ..Default::default()
3293    });
3294    base.name = next.name;
3295    if next.description.is_some() {
3296        base.description = next.description;
3297    }
3298    if !next.uses.is_empty() {
3299        let mut seen: std::collections::HashSet<String> = base.uses.iter().cloned().collect();
3300        for u in next.uses {
3301            if !seen.contains(&u) {
3302                seen.insert(u.clone());
3303                base.uses.push(u);
3304            }
3305        }
3306    }
3307    if !next.defines.is_empty() {
3308        for (k, v) in next.defines {
3309            if let Some(existing) = base.defines.iter_mut().find(|(name, _)| name == &k) {
3310                existing.1 = v;
3311            } else {
3312                base.defines.push((k, v));
3313            }
3314        }
3315    }
3316    if next.extends.is_some() {
3317        base.extends = next.extends;
3318    }
3319    if next.numeric_domain.is_some() {
3320        base.numeric_domain = next.numeric_domain;
3321    }
3322    if next.truth_domain.is_some() {
3323        base.truth_domain = next.truth_domain;
3324    }
3325    // Carrier (issue #97 Section 2): a later registration with the same name
3326    // replaces the carrier list but only flips `strict_carrier` to true
3327    // (never silently back off).
3328    if !next.carrier.is_empty() {
3329        base.carrier = next.carrier;
3330    }
3331    if next.strict_carrier {
3332        base.strict_carrier = true;
3333    }
3334    // Truth tables (issue #97, Section 3 of netkeep80's punch-list). A later
3335    // registration adds/overwrites table entries operator-by-operator so the
3336    // user can extend a previously declared foundation with more tables.
3337    for (op_name, rows) in next.truth_tables {
3338        if let Some(existing) = base
3339            .truth_tables
3340            .iter_mut()
3341            .find(|(name, _)| name == &op_name)
3342        {
3343            existing.1 = rows;
3344        } else {
3345            base.truth_tables.push((op_name, rows));
3346        }
3347    }
3348    // Experimental foundation profile metadata (issue #97, Phase 9). A later
3349    // registration can flip `experimental` to true (never silently back to
3350    // false), set or replace the root symbol, and append additional abits.
3351    if next.experimental {
3352        base.experimental = true;
3353    }
3354    if next.root.is_some() {
3355        base.root = next.root;
3356    }
3357    if !next.abits.is_empty() {
3358        let mut seen: std::collections::HashSet<String> =
3359            base.abits.iter().map(|(s, _)| s.clone()).collect();
3360        for (symbol, meaning) in next.abits {
3361            if !seen.contains(&symbol) {
3362                seen.insert(symbol.clone());
3363                base.abits.push((symbol, meaning));
3364            }
3365        }
3366    }
3367    base
3368}
3369
3370// ---------- Dependency graph traversal (issue #97, Phase 7) ----------
3371//
3372// Compute the transitive closure of a single root-construct's dependencies,
3373// breadth-first. Missing intermediate deps are silently retained (so the
3374// final closure can surface dangling names for downstream tools to detect
3375// against the registry). The traversal uses a seen-set so the helper does
3376// not loop forever in the presence of cycles. The result is sorted so two
3377// invocations against the same registry yield byte-identical output.
3378fn closure_for(env: &Env, name: &str) -> Vec<String> {
3379    let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
3380    let mut order: Vec<String> = Vec::new();
3381    let mut queue: std::collections::VecDeque<String> = std::collections::VecDeque::new();
3382    queue.push_back(name.to_string());
3383    while let Some(next) = queue.pop_front() {
3384        if seen.contains(&next) {
3385            continue;
3386        }
3387        seen.insert(next.clone());
3388        if next != name {
3389            order.push(next.clone());
3390        }
3391        if let Some(rc) = env.root_constructs.get(&next) {
3392            let mut deps = rc.depends_on.clone();
3393            deps.sort();
3394            for dep in deps {
3395                if !seen.contains(&dep) {
3396                    queue.push_back(dep);
3397                }
3398            }
3399        }
3400    }
3401    order.sort();
3402    order
3403}
3404
3405/// Build a `[(name, [dep, ...]), ...]` listing covering every registered
3406/// root-construct in deterministic, sorted order at every level.
3407/// Constructs with no dependencies map to an empty vector so the trust
3408/// audit can still see them. Complement of `Env::dependency_closure(name)`
3409/// which gives a per-construct slice.
3410pub fn build_dependency_graph(env: &Env) -> Vec<(String, Vec<String>)> {
3411    let mut names: Vec<String> = env.root_constructs.keys().cloned().collect();
3412    names.sort();
3413    let mut out: Vec<(String, Vec<String>)> = Vec::with_capacity(names.len());
3414    for name in names {
3415        let closure = closure_for(env, &name);
3416        out.push((name, closure));
3417    }
3418    out
3419}
3420
3421// ---------- MTC/anum serialization (issue #97, Phase 9) ----------
3422//
3423// Encode a link expression into a string using only the four abits of the
3424// experimental `mtc-anum` foundation: `[`, `]`, `0`, `1`. Each Node is
3425// wrapped in `[ ... ]`; the first character after `[` is a tag — `0` for a
3426// leaf, `1` for a list. A leaf's payload is its UTF-8 bytes encoded
3427// most-significant-bit-first, 8 bits per byte. A list's payload is the
3428// concatenated encoding of its children. Round-trippable via `decode_anum`.
3429pub fn encode_anum(node: &Node) -> String {
3430    let mut out = String::new();
3431    encode_anum_into(node, &mut out);
3432    out
3433}
3434
3435fn encode_anum_into(node: &Node, out: &mut String) {
3436    match node {
3437        Node::Leaf(s) => {
3438            out.push('[');
3439            out.push('0');
3440            for byte in s.as_bytes() {
3441                for shift in (0..8).rev() {
3442                    out.push(if (byte >> shift) & 1 == 1 { '1' } else { '0' });
3443                }
3444            }
3445            out.push(']');
3446        }
3447        Node::List(children) => {
3448            out.push('[');
3449            out.push('1');
3450            for child in children {
3451                encode_anum_into(child, out);
3452            }
3453            out.push(']');
3454        }
3455    }
3456}
3457
3458/// Decode an anum-encoded string into a Node. Strictly enforces the
3459/// `[tag payload]` shape; any character outside the four-abit alphabet
3460/// raises an error. Returns the decoded Node; errors if trailing content
3461/// remains after the top-level frame.
3462pub fn decode_anum(s: &str) -> Result<Node, String> {
3463    let bytes = s.as_bytes();
3464    let (node, pos) = decode_anum_at(bytes, 0)?;
3465    if pos != bytes.len() {
3466        return Err(format!("anum-decode: trailing data at position {}", pos));
3467    }
3468    Ok(node)
3469}
3470
3471fn decode_anum_at(bytes: &[u8], mut pos: usize) -> Result<(Node, usize), String> {
3472    if pos >= bytes.len() || bytes[pos] != b'[' {
3473        return Err(format!("anum-decode: expected '[' at position {}", pos));
3474    }
3475    pos += 1;
3476    if pos >= bytes.len() {
3477        return Err("anum-decode: truncated input after '['".to_string());
3478    }
3479    let tag = bytes[pos];
3480    if tag == b'0' {
3481        pos += 1;
3482        let mut bits = String::new();
3483        while pos < bytes.len() && bytes[pos] != b']' {
3484            let b = bytes[pos];
3485            if b != b'0' && b != b'1' {
3486                return Err(format!(
3487                    "anum-decode: leaf payload may only contain '0' or '1' (got '{}' at {})",
3488                    b as char, pos
3489                ));
3490            }
3491            bits.push(b as char);
3492            pos += 1;
3493        }
3494        if pos >= bytes.len() || bytes[pos] != b']' {
3495            return Err(format!(
3496                "anum-decode: unterminated leaf starting before position {}",
3497                pos
3498            ));
3499        }
3500        pos += 1;
3501        if bits.len() % 8 != 0 {
3502            return Err(format!(
3503                "anum-decode: leaf bit-count {} is not byte-aligned",
3504                bits.len()
3505            ));
3506        }
3507        let mut payload: Vec<u8> = Vec::with_capacity(bits.len() / 8);
3508        for chunk in bits.as_bytes().chunks(8) {
3509            let mut byte: u8 = 0;
3510            for &c in chunk {
3511                byte = (byte << 1) | (if c == b'1' { 1 } else { 0 });
3512            }
3513            payload.push(byte);
3514        }
3515        let s = String::from_utf8(payload)
3516            .map_err(|e| format!("anum-decode: invalid UTF-8 in leaf ({})", e))?;
3517        Ok((Node::Leaf(s), pos))
3518    } else if tag == b'1' {
3519        pos += 1;
3520        let mut items: Vec<Node> = Vec::new();
3521        while pos < bytes.len() && bytes[pos] != b']' {
3522            if bytes[pos] != b'[' {
3523                return Err(format!(
3524                    "anum-decode: list child must start with '[' (got '{}' at {})",
3525                    bytes[pos] as char, pos
3526                ));
3527            }
3528            let (child, next) = decode_anum_at(bytes, pos)?;
3529            items.push(child);
3530            pos = next;
3531        }
3532        if pos >= bytes.len() || bytes[pos] != b']' {
3533            return Err(format!(
3534                "anum-decode: unterminated list starting before position {}",
3535                pos
3536            ));
3537        }
3538        pos += 1;
3539        Ok((Node::List(items), pos))
3540    } else {
3541        Err(format!(
3542            "anum-decode: expected tag '0' or '1' after '[' at position {}",
3543            pos
3544        ))
3545    }
3546}
3547
3548/// Seed the registry with the built-in descriptors that describe what the
3549/// current host implementation actually trusts. Mirrors the JS
3550/// `seedBuiltinRootConstructs` list verbatim so the trust report is
3551/// identical across JS and Rust.
3552fn seed_builtin_root_constructs(env: &mut Env) {
3553    let seeds: Vec<(&str, &str, &str, Vec<&str>, Option<&str>, Option<bool>)> = vec![
3554        // (name, kind, status, depends_on, encoded_as, pure_links_ready)
3555        ("lino-parser", "parser", "external-trusted", vec![], Some("links-notation"), Some(false)),
3556        ("canonical-printer", "printer", "host-primitive", vec![], Some("keyOf"), None),
3557        ("structural-equality", "equality-layer", "host-primitive", vec![], Some("isStructurallySame"), None),
3558        ("structural-matcher", "matcher", "external-trusted", vec![], Some("match_proof_pattern"), None),
3559        ("decimal-12-arithmetic", "numeric-domain", "host-primitive", vec![], Some("decRound"), Some(false)),
3560        ("+", "arithmetic-operator", "host-primitive", vec!["decimal-12-arithmetic"], None, None),
3561        ("-", "arithmetic-operator", "host-primitive", vec!["decimal-12-arithmetic"], None, None),
3562        ("*", "arithmetic-operator", "host-primitive", vec!["decimal-12-arithmetic"], None, None),
3563        ("/", "arithmetic-operator", "host-primitive", vec!["decimal-12-arithmetic"], None, None),
3564        ("<", "comparison-operator", "host-primitive", vec!["decimal-12-arithmetic"], None, None),
3565        ("<=", "comparison-operator", "host-primitive", vec!["decimal-12-arithmetic"], None, None),
3566        ("truth-range", "truth-domain", "user-configurable", vec![], Some("Env.lo/Env.hi"), None),
3567        ("valence", "truth-domain", "user-configurable", vec![], Some("Env.valence"), None),
3568        ("clamp", "truth-normalization", "host-primitive", vec![], Some("Env.clamp"), None),
3569        ("quantize", "truth-normalization", "host-primitive", vec![], Some("quantize"), None),
3570        ("true", "truth-constant", "user-configurable", vec![], None, None),
3571        ("false", "truth-constant", "user-configurable", vec![], None, None),
3572        ("unknown", "truth-constant", "user-configurable", vec![], None, None),
3573        ("undefined", "truth-constant", "user-configurable", vec![], None, None),
3574        ("avg", "aggregator", "host-primitive", vec![], None, None),
3575        ("min", "aggregator", "host-primitive", vec![], None, None),
3576        ("max", "aggregator", "host-primitive", vec![], None, None),
3577        ("product", "aggregator", "host-primitive", vec![], None, None),
3578        ("probabilistic_sum", "aggregator", "host-primitive", vec![], None, None),
3579        ("truth-table-fallback", "truth-table-fallback", "host-derived", vec![], None, None),
3580        ("not", "truth-operator", "user-configurable", vec!["truth-range", "decimal-12-arithmetic"], None, None),
3581        ("and", "truth-operator", "user-configurable", vec!["avg"], None, None),
3582        ("or", "truth-operator", "user-configurable", vec!["max"], None, None),
3583        ("both", "truth-operator", "user-configurable", vec!["avg"], None, None),
3584        ("neither", "truth-operator", "user-configurable", vec!["product"], None, None),
3585        ("=", "equality-layer", "host-primitive", vec!["structural-equality", "decimal-12-arithmetic"], None, None),
3586        ("!=", "equality-layer", "host-derived", vec!["=", "not"], None, None),
3587        ("assigned-equality", "equality-layer", "host-primitive", vec![], None, None),
3588        ("numeric-equality", "equality-layer", "host-primitive", vec!["decimal-12-arithmetic"], None, None),
3589        ("definitional-equality", "equality-layer", "host-primitive", vec!["beta-reduction", "structural-equality"], None, None),
3590        ("Type", "universe-form", "host-primitive", vec![], None, Some(false)),
3591        ("Prop", "universe-form", "host-primitive", vec!["Type"], None, None),
3592        ("Pi", "binder", "host-primitive", vec!["Type", "substitution", "freshness"], None, None),
3593        ("lambda", "binder", "host-primitive", vec!["Pi", "substitution"], None, None),
3594        ("apply", "eliminator", "host-primitive", vec!["lambda", "beta-reduction"], None, None),
3595        ("beta-reduction", "reduction-rule", "host-primitive", vec!["substitution", "freshness", "alpha-renaming"], None, None),
3596        ("substitution", "meta-operation", "host-primitive", vec![], Some("substitute"), None),
3597        ("freshness", "meta-operation", "host-primitive", vec![], Some("evalFresh"), None),
3598        ("alpha-renaming", "meta-operation", "host-primitive", vec![], None, None),
3599        ("normalization", "reduction-rule", "host-primitive", vec!["beta-reduction"], Some("normalizeTerm"), None),
3600        ("whnf", "reduction-rule", "host-primitive", vec!["beta-reduction"], Some("whnfTerm"), None),
3601        ("conversion", "equality-layer", "host-primitive", vec!["beta-reduction", "normalization", "structural-equality"], None, None),
3602        ("pi-formation", "typing-rule", "links-defined", vec!["Pi"], None, None),
3603        ("lambda-introduction", "typing-rule", "links-defined", vec!["lambda"], None, None),
3604        ("application-elimination", "typing-rule", "links-defined", vec!["apply"], None, None),
3605        ("beta-conversion", "reduction-rule", "links-defined", vec!["beta-reduction"], None, None),
3606        ("inductive", "declaration", "host-primitive", vec!["Type", "Pi"], None, None),
3607        ("coinductive", "declaration", "host-primitive", vec!["Type", "Pi"], None, None),
3608        ("proof-replay", "replay-checker", "host-primitive", vec![], Some("check.mjs"), None),
3609        ("proof-object", "proof-data", "links-encoded", vec![], Some("proof-object"), None),
3610        ("proof-rule-declaration", "proof-data", "links-encoded", vec![], Some("rule"), None),
3611        ("proof-checking-relation", "checking-relation", "links-defined", vec!["proof-replay", "structural-equality", "proof-object"], None, None),
3612        ("rule-application-check", "checking-relation", "links-defined", vec!["proof-replay", "structural-equality", "proof-rule-declaration"], None, None),
3613        ("by", "proof-rule", "host-primitive", vec![], None, None),
3614        ("Nat", "inductive-type", "links-defined", vec![], None, None),
3615        ("zero", "constructor", "links-defined", vec!["Nat"], None, None),
3616        ("succ", "constructor", "links-defined", vec!["Nat"], None, None),
3617        ("nat-equality", "equality-layer", "links-defined", vec!["Nat", "structural-equality"], Some("nat-equals"), None),
3618        ("nat-recursion", "recursor", "links-defined", vec!["Nat", "zero", "succ", "nat-equality", "proof-replay", "structural-equality"], None, None),
3619        ("add", "derived-operation", "links-defined", vec!["Nat", "zero", "succ", "nat-recursion", "nat-equality"], None, None),
3620        ("nat-add-zero", "computation-rule", "links-defined", vec!["add", "zero", "nat-recursion", "nat-equality"], None, None),
3621        ("nat-add-succ", "computation-rule", "links-defined", vec!["add", "succ", "nat-recursion", "nat-equality"], None, None),
3622        ("nat-zero-formation", "typing-rule", "links-defined", vec!["Nat", "zero"], None, None),
3623        ("nat-succ-formation", "typing-rule", "links-defined", vec!["Nat", "succ"], None, None),
3624        ("forall", "universal-quantifier", "links-defined", vec!["Nat"], None, None),
3625        ("implication", "logical-connective", "links-defined", vec![], Some("implies"), None),
3626        ("predicate-application", "logical-form", "links-defined", vec![], Some("at"), None),
3627        ("nat-induction", "proof-principle", "links-defined", vec!["Nat", "forall", "implication", "predicate-application", "substitution", "freshness", "proof-replay", "structural-equality"], None, None),
3628        ("nat-refl", "equality-rule", "links-defined", vec!["Nat", "nat-equality"], None, None),
3629        ("nat-cong-succ", "equality-rule", "links-defined", vec!["Nat", "succ", "nat-equality"], None, None),
3630        ("nat-eliminator", "eliminator", "links-defined", vec!["Nat", "nat-recursion", "nat-induction"], None, None),
3631        ("nat-rec-zero", "computation-rule", "links-defined", vec!["nat-recursion", "zero", "nat-equality"], None, None),
3632        ("nat-rec-succ", "computation-rule", "links-defined", vec!["nat-recursion", "succ", "nat-equality"], None, None),
3633        ("mul", "derived-operation", "links-defined", vec!["Nat", "zero", "succ", "add", "nat-recursion", "nat-equality"], None, None),
3634        ("nat-mul-zero", "computation-rule", "links-defined", vec!["mul", "zero", "nat-recursion", "nat-equality"], None, None),
3635        ("nat-mul-succ", "computation-rule", "links-defined", vec!["mul", "succ", "add", "nat-recursion", "nat-equality"], None, None),
3636        ("eval-nat-normalize", "evaluator-fragment", "links-defined", vec!["Nat", "zero", "succ", "add", "mul", "nat-add-zero", "nat-add-succ", "nat-mul-zero", "nat-mul-succ", "structural-matcher"], None, None),
3637        ("eval-nat", "evaluator", "links-defined", vec!["eval-nat-normalize", "nat-normal-form-to-host-number"], None, None),
3638        ("nat-normal-form-to-host-number", "renderer", "host-derived", vec!["eval-nat-normalize"], None, None),
3639        ("smt-trusted", "external-decision", "external-trusted", vec![], None, None),
3640        ("atp-trusted", "external-decision", "external-trusted", vec![], None, None),
3641        ("mode", "mode-declaration", "host-primitive", vec![], None, None),
3642        ("totality-check", "metatheorem", "host-primitive", vec![], None, None),
3643        ("coverage-check", "metatheorem", "host-primitive", vec![], None, None),
3644        ("termination-check", "metatheorem", "host-primitive", vec![], None, None),
3645        ("self.evaluator", "self-bootstrap", "links-encoded", vec![], Some("lib/self/evaluator.lino"), None),
3646        ("self.grammar", "self-bootstrap", "links-encoded", vec![], Some("lib/self/grammar.lino"), None),
3647        ("self.types", "self-bootstrap", "links-encoded", vec![], Some("lib/self/types.lino"), None),
3648        ("self.operators", "self-bootstrap", "links-encoded", vec![], Some("lib/self/operators.lino"), None),
3649        ("self.metatheorem", "self-bootstrap", "links-encoded", vec![], Some("lib/self/metatheorem.lino"), None),
3650    ];
3651    // Special-case the universe form's planned-as field (Type is planned as links-defined).
3652    for (name, kind, status, depends_on, encoded_as, pure_links_ready) in seeds {
3653        let descriptor = RootConstructDescriptor {
3654            name: name.to_string(),
3655            status: Some(status.to_string()),
3656            semantic_status: None,
3657            kind: Some(kind.to_string()),
3658            depends_on: depends_on.into_iter().map(String::from).collect(),
3659            encoded_as: encoded_as.map(String::from),
3660            pure_links_ready,
3661            override_with: None,
3662            planned_as: if name == "Type" { Some("links-defined".to_string()) } else { None },
3663            foundation: None,
3664        };
3665        env.root_constructs.insert(descriptor.name.clone(), descriptor);
3666    }
3667    for name in ["eval-nat-normalize", "eval-nat"] {
3668        if let Some(descriptor) = env.root_constructs.get_mut(name) {
3669            descriptor.semantic_status = Some("links-evaluated".to_string());
3670        }
3671    }
3672    if let Some(descriptor) = env.root_constructs.get_mut("structural-matcher") {
3673        descriptor.semantic_status = Some("host-trusted".to_string());
3674    }
3675    if let Some(descriptor) = env.root_constructs.get_mut("nat-normal-form-to-host-number") {
3676        descriptor.semantic_status = Some("host-trusted".to_string());
3677    }
3678}
3679
3680/// Parse a `(root-construct <name> (status …) (kind …) …)` form into a
3681/// descriptor record. Mirrors the JS `parseRootConstructForm` helper.
3682fn parse_root_construct_form(node: &Node) -> Result<RootConstructDescriptor, String> {
3683    let children = match node {
3684        Node::List(items) => items,
3685        _ => return Err("root-construct form must be `(root-construct <name> …)`".to_string()),
3686    };
3687    if children.len() < 2 {
3688        return Err("root-construct form must be `(root-construct <name> …)`".to_string());
3689    }
3690    let head = match &children[0] {
3691        Node::Leaf(s) if s == "root-construct" => s,
3692        _ => return Err("root-construct form must be `(root-construct <name> …)`".to_string()),
3693    };
3694    let _ = head;
3695    let name = match &children[1] {
3696        Node::Leaf(s) if !s.is_empty() => s.clone(),
3697        _ => return Err("root-construct name must be a non-empty identifier".to_string()),
3698    };
3699    let mut descriptor = RootConstructDescriptor {
3700        name,
3701        ..Default::default()
3702    };
3703    for child in &children[2..] {
3704        let clause = match child {
3705            Node::List(items) => items,
3706            _ => {
3707                return Err(
3708                    "root-construct child clauses must be lists led by a keyword".to_string(),
3709                );
3710            }
3711        };
3712        if clause.is_empty() {
3713            return Err(
3714                "root-construct child clauses must be lists led by a keyword".to_string(),
3715            );
3716        }
3717        let key = match &clause[0] {
3718            Node::Leaf(s) => s.as_str(),
3719            _ => {
3720                return Err(
3721                    "root-construct child clauses must be lists led by a keyword".to_string(),
3722                );
3723            }
3724        };
3725        let rest: Vec<&Node> = clause.iter().skip(1).collect();
3726        match key {
3727            "status" => {
3728                if rest.len() != 1 {
3729                    return Err("(status …) requires one symbol".to_string());
3730                }
3731                if let Node::Leaf(v) = rest[0] {
3732                    descriptor.status = Some(v.clone());
3733                } else {
3734                    return Err("(status …) requires one symbol".to_string());
3735                }
3736            }
3737            "semantic-status" => {
3738                if rest.len() != 1 {
3739                    return Err("(semantic-status …) requires one symbol".to_string());
3740                }
3741                if let Node::Leaf(v) = rest[0] {
3742                    descriptor.semantic_status = Some(v.clone());
3743                } else {
3744                    return Err("(semantic-status …) requires one symbol".to_string());
3745                }
3746            }
3747            "kind" => {
3748                if rest.len() != 1 {
3749                    return Err("(kind …) requires one symbol".to_string());
3750                }
3751                if let Node::Leaf(v) = rest[0] {
3752                    descriptor.kind = Some(v.clone());
3753                } else {
3754                    return Err("(kind …) requires one symbol".to_string());
3755                }
3756            }
3757            "depends-on" => {
3758                for r in &rest {
3759                    descriptor.depends_on.push(key_of(r));
3760                }
3761            }
3762            "encoded-as" | "implemented-by" => {
3763                let joined = rest
3764                    .iter()
3765                    .map(|n| key_of(n))
3766                    .collect::<Vec<_>>()
3767                    .join(" ");
3768                descriptor.encoded_as = Some(joined);
3769            }
3770            "pure-links-ready" => {
3771                if rest.len() != 1 {
3772                    return Err("(pure-links-ready …) must be `yes` or `no`".to_string());
3773                }
3774                if let Node::Leaf(v) = rest[0] {
3775                    descriptor.pure_links_ready = Some(match v.as_str() {
3776                        "yes" => true,
3777                        "no" => false,
3778                        _ => {
3779                            return Err(
3780                                "(pure-links-ready …) must be `yes` or `no`".to_string()
3781                            );
3782                        }
3783                    });
3784                } else {
3785                    return Err("(pure-links-ready …) must be `yes` or `no`".to_string());
3786                }
3787            }
3788            "override" => {
3789                let joined = rest
3790                    .iter()
3791                    .map(|n| key_of(n))
3792                    .collect::<Vec<_>>()
3793                    .join(" ");
3794                descriptor.override_with = Some(joined);
3795            }
3796            "planned-as" => {
3797                let joined = rest
3798                    .iter()
3799                    .map(|n| key_of(n))
3800                    .collect::<Vec<_>>()
3801                    .join(" ");
3802                descriptor.planned_as = Some(joined);
3803            }
3804            "foundation" => {
3805                if rest.len() != 1 {
3806                    return Err("(foundation …) must be a single name".to_string());
3807                }
3808                if let Node::Leaf(v) = rest[0] {
3809                    descriptor.foundation = Some(v.clone());
3810                } else {
3811                    return Err("(foundation …) must be a single name".to_string());
3812                }
3813            }
3814            "surface" | "description" | "used-by" => {
3815                // free-form annotations; accepted syntactically.
3816            }
3817            _ => {
3818                // Unknown clauses are accepted for forward compatibility.
3819            }
3820        }
3821    }
3822    Ok(descriptor)
3823}
3824
3825/// Parse a `(foundation <name> (description …) (uses …) …)` form into a
3826/// descriptor record. Mirrors the JS `parseFoundationForm` helper.
3827fn parse_foundation_form(node: &Node) -> Result<FoundationDescriptor, String> {
3828    let children = match node {
3829        Node::List(items) => items,
3830        _ => return Err("foundation form must be `(foundation <name> …)`".to_string()),
3831    };
3832    if children.len() < 2 {
3833        return Err("foundation form must be `(foundation <name> …)`".to_string());
3834    }
3835    let head = match &children[0] {
3836        Node::Leaf(s) if s == "foundation" => s,
3837        _ => return Err("foundation form must be `(foundation <name> …)`".to_string()),
3838    };
3839    let _ = head;
3840    let name = match &children[1] {
3841        Node::Leaf(s) if !s.is_empty() => s.clone(),
3842        _ => return Err("foundation name must be a non-empty identifier".to_string()),
3843    };
3844    let mut foundation = FoundationDescriptor {
3845        name,
3846        ..Default::default()
3847    };
3848    for child in &children[2..] {
3849        // LiNo collapses single-element parenthesized clauses such as
3850        // `(strict-carrier)` into a bare `Leaf("strict-carrier")` because
3851        // `Link::to_string()` strips the parens around a single token.
3852        // Treat a bare leaf as a zero-argument clause.
3853        let (key, rest): (&str, Vec<&Node>) = match child {
3854            Node::Leaf(s) if !s.is_empty() => (s.as_str(), Vec::new()),
3855            Node::List(items) if !items.is_empty() => match &items[0] {
3856                Node::Leaf(s) => (s.as_str(), items.iter().skip(1).collect()),
3857                _ => {
3858                    return Err(
3859                        "foundation child clauses must be lists led by a keyword".to_string(),
3860                    );
3861                }
3862            },
3863            _ => {
3864                return Err(
3865                    "foundation child clauses must be lists led by a keyword".to_string(),
3866                );
3867            }
3868        };
3869        match key {
3870            "uses" => {
3871                for r in &rest {
3872                    foundation.uses.push(key_of(r));
3873                }
3874            }
3875            "defines" => {
3876                if rest.is_empty() {
3877                    return Err(
3878                        "(defines <construct> <implementation>) requires a construct name"
3879                            .to_string(),
3880                    );
3881                }
3882                let construct = match rest[0] {
3883                    Node::Leaf(s) => s.clone(),
3884                    _ => {
3885                        return Err(
3886                            "(defines <construct> <implementation>) requires a construct name"
3887                                .to_string(),
3888                        );
3889                    }
3890                };
3891                let impl_str = if rest.len() > 1 {
3892                    rest.iter()
3893                        .skip(1)
3894                        .map(|n| key_of(n))
3895                        .collect::<Vec<_>>()
3896                        .join(" ")
3897                } else {
3898                    "links-defined".to_string()
3899                };
3900                foundation.defines.push((construct, impl_str));
3901            }
3902            "extends" => {
3903                if rest.len() != 1 {
3904                    return Err("(extends …) requires one foundation name".to_string());
3905                }
3906                if let Node::Leaf(v) = rest[0] {
3907                    foundation.extends = Some(v.clone());
3908                } else {
3909                    return Err("(extends …) requires one foundation name".to_string());
3910                }
3911            }
3912            "numeric-domain" => {
3913                if rest.len() != 1 {
3914                    return Err("(numeric-domain …) requires one name".to_string());
3915                }
3916                if let Node::Leaf(v) = rest[0] {
3917                    foundation.numeric_domain = Some(v.clone());
3918                } else {
3919                    return Err("(numeric-domain …) requires one name".to_string());
3920                }
3921            }
3922            "truth-domain" => {
3923                if rest.len() != 1 {
3924                    return Err("(truth-domain …) requires one name".to_string());
3925                }
3926                if let Node::Leaf(v) = rest[0] {
3927                    foundation.truth_domain = Some(v.clone());
3928                } else {
3929                    return Err("(truth-domain …) requires one name".to_string());
3930                }
3931            }
3932            "carrier" => {
3933                // `(carrier <val1> <val2> ...)` — list the values the active
3934                // foundation considers legal. Each value is kept as a string
3935                // so `enter_foundation` can resolve symbolic constants
3936                // (`true`, `false`, `unknown`) through `env.symbol_prob` at
3937                // activation time. Numeric literals stay literal.
3938                if rest.is_empty() {
3939                    return Err("(carrier ...) requires at least one value".to_string());
3940                }
3941                foundation.carrier = rest.iter().map(|n| key_of(n)).collect();
3942            }
3943            "strict-carrier" => {
3944                // `(strict-carrier)` opts the foundation into runtime
3945                // enforcement. Without this clause, `(carrier ...)` is
3946                // informational only and the evaluator stays
3947                // backward-compatible.
3948                foundation.strict_carrier = true;
3949            }
3950            "truth-table" => {
3951                // `(truth-table <op> (in1 in2 ... -> out) ...)` — links-defined
3952                // finite truth table that rebinds `<op>` for the duration of
3953                // the foundation. Inputs and outputs are kept as strings so
3954                // `enter_foundation` can resolve symbolic constants through
3955                // `env.symbol_prob` at activation time.
3956                if rest.is_empty() {
3957                    return Err(
3958                        "(truth-table <op> ...) requires an operator name".to_string(),
3959                    );
3960                }
3961                let op_name = match rest[0] {
3962                    Node::Leaf(ref s) if !s.is_empty() => s.clone(),
3963                    _ => {
3964                        return Err(
3965                            "(truth-table <op> ...) requires an operator name".to_string(),
3966                        );
3967                    }
3968                };
3969                let mut table_rows: Vec<TruthTableRow> = Vec::new();
3970                for raw in rest.iter().skip(1) {
3971                    let row_items = match raw {
3972                        Node::List(items) => items,
3973                        _ => {
3974                            return Err(format!(
3975                                "(truth-table {} ...) rows must be lists like (in1 in2 -> out)",
3976                                op_name
3977                            ));
3978                        }
3979                    };
3980                    let arrow_at = row_items
3981                        .iter()
3982                        .position(|n| matches!(n, Node::Leaf(s) if s == "->"));
3983                    let arrow_at = match arrow_at {
3984                        Some(idx) if idx >= 1 && idx == row_items.len() - 2 => idx,
3985                        _ => {
3986                            return Err(format!(
3987                                "(truth-table {} ...) row must be (input ... -> output)",
3988                                op_name
3989                            ));
3990                        }
3991                    };
3992                    let inputs: Vec<String> = row_items[..arrow_at]
3993                        .iter()
3994                        .map(|n| key_of(n))
3995                        .collect();
3996                    let output = key_of(&row_items[arrow_at + 1]);
3997                    table_rows.push(TruthTableRow { inputs, output });
3998                }
3999                if table_rows.is_empty() {
4000                    return Err(format!(
4001                        "(truth-table {} ...) requires at least one row",
4002                        op_name
4003                    ));
4004                }
4005                foundation
4006                    .truth_tables
4007                    .push((op_name, table_rows));
4008            }
4009            "description" => {
4010                foundation.description = Some(
4011                    rest.iter()
4012                        .map(|n| key_of(n))
4013                        .collect::<Vec<_>>()
4014                        .join(" "),
4015                );
4016            }
4017            "experimental" => {
4018                // `(experimental)` flags the foundation as experimental so
4019                // the trust audit can call it out (issue #97, Phase 9).
4020                // Data-only.
4021                foundation.experimental = true;
4022            }
4023            "root" => {
4024                // `(root <symbol>)` records the foundation's root concept
4025                // (e.g. `∞` for mtc-anum). Informational; surfaced on the
4026                // report.
4027                if rest.len() != 1 {
4028                    return Err("(root <symbol>) requires exactly one value".to_string());
4029                }
4030                foundation.root = Some(key_of(rest[0]));
4031            }
4032            "abit" => {
4033                // `(abit <symbol> <meaning...>)` records one atomic bit of
4034                // the foundation's alphabet. The mtc-anum profile lists
4035                // four abits: `[`, `]`, `1`, `0`. Informational; surfaced
4036                // on the report.
4037                if rest.is_empty() {
4038                    return Err("(abit <symbol> <meaning>) requires a symbol".to_string());
4039                }
4040                let symbol = key_of(rest[0]);
4041                let meaning = rest
4042                    .iter()
4043                    .skip(1)
4044                    .map(|n| key_of(n))
4045                    .collect::<Vec<_>>()
4046                    .join(" ");
4047                foundation.abits.push((symbol, meaning));
4048            }
4049            _ => {
4050                // Unknown clauses are accepted for forward compatibility.
4051            }
4052        }
4053    }
4054    Ok(foundation)
4055}
4056
4057// ---------- Proof-object substrate (issue #97, Phase 3) ----------
4058
4059/// Returns true when the `(rule ...)` form looks like a proof-substrate
4060/// rule (every non-name child is `(premise ...)` or `(conclusion ...)`, and
4061/// at least one `conclusion` is present). Data-only `(rule <name>
4062/// (sequence ...) ...)` forms used by self-bootstrap grammar files fall
4063/// through to the legacy data path because they do not pass this guard.
4064fn is_proof_rule_shape(children: &[Node]) -> bool {
4065    if children.len() < 3 {
4066        return false;
4067    }
4068    if !matches!(&children[1], Node::Leaf(s) if !s.is_empty()) {
4069        return false;
4070    }
4071    let mut saw_conclusion = false;
4072    for c in &children[2..] {
4073        let clause = match c {
4074            Node::List(items) => items,
4075            _ => return false,
4076        };
4077        let key = match clause.first() {
4078            Some(Node::Leaf(k)) => k.as_str(),
4079            _ => return false,
4080        };
4081        match key {
4082            "premise" => {}
4083            "conclusion" => {
4084                saw_conclusion = true;
4085            }
4086            _ => return false,
4087        }
4088    }
4089    saw_conclusion
4090}
4091
4092//
4093// Parse `(rule <name> (premise <pat>)... (conclusion <pat>))`. Patterns are
4094// plain `Node`s; leaves beginning with `?` are metavariables and bind during
4095// `check_proof_object` matching. The form's clauses must be lists led by
4096// `premise`/`conclusion`; this distinguishes the proof-substrate shape from
4097// the data-only `(rule <name> (sequence ...) ...)` forms used by the
4098// self-bootstrap grammar files, which fall through to the legacy data path.
4099pub fn parse_rule_form(node: &Node) -> Result<ProofRule, String> {
4100    let children = match node {
4101        Node::List(items) => items,
4102        _ => {
4103            return Err(
4104                "rule form must be `(rule <name> (premise <pat>)... (conclusion <pat>))`".to_string(),
4105            );
4106        }
4107    };
4108    if children.len() < 3 || !matches!(children.first(), Some(Node::Leaf(h)) if h == "rule") {
4109        return Err(
4110            "rule form must be `(rule <name> (premise <pat>)... (conclusion <pat>))`".to_string(),
4111        );
4112    }
4113    let name = match &children[1] {
4114        Node::Leaf(s) if !s.is_empty() => s.clone(),
4115        _ => return Err("rule name must be a non-empty identifier".to_string()),
4116    };
4117    let mut premises: Vec<Node> = Vec::new();
4118    let mut conclusion: Option<Node> = None;
4119    for child in &children[2..] {
4120        let clause = match child {
4121            Node::List(c) => c,
4122            _ => {
4123                return Err(format!(
4124                    "rule {}: clauses must be lists led by a keyword",
4125                    name
4126                ));
4127            }
4128        };
4129        let key = match clause.first() {
4130            Some(Node::Leaf(k)) => k.as_str(),
4131            _ => {
4132                return Err(format!(
4133                    "rule {}: clauses must be lists led by a keyword",
4134                    name
4135                ));
4136            }
4137        };
4138        match key {
4139            "premise" => {
4140                if clause.len() != 2 {
4141                    return Err(format!(
4142                        "rule {}: (premise <pat>) requires exactly one pattern",
4143                        name
4144                    ));
4145                }
4146                premises.push(clause[1].clone());
4147            }
4148            "conclusion" => {
4149                if clause.len() != 2 {
4150                    return Err(format!(
4151                        "rule {}: (conclusion <pat>) requires exactly one pattern",
4152                        name
4153                    ));
4154                }
4155                if conclusion.is_some() {
4156                    return Err(format!(
4157                        "rule {}: only one (conclusion ...) clause is allowed",
4158                        name
4159                    ));
4160                }
4161                conclusion = Some(clause[1].clone());
4162            }
4163            other => {
4164                return Err(format!(
4165                    "rule {}: unknown clause keyword {}",
4166                    name, other
4167                ));
4168            }
4169        }
4170    }
4171    let conclusion = conclusion.ok_or_else(|| {
4172        format!(
4173            "rule {}: at least one (conclusion <pat>) clause is required",
4174            name
4175        )
4176    })?;
4177    Ok(ProofRule {
4178        name,
4179        premises,
4180        conclusion,
4181    })
4182}
4183
4184// Parse `(proof-object <name> (applies <rule>) (premise <judgement>)...
4185// (conclusion <judgement>))` into a descriptor stored on the env.
4186pub fn parse_proof_assumption_form(node: &Node) -> Result<ProofAssumption, String> {
4187    let children = match node {
4188        Node::List(items) => items,
4189        _ => {
4190            return Err(
4191                "proof assumption form must be `(assumption <name> (judgement <j>))` or `(axiom <name> (judgement <j>))`".to_string(),
4192            );
4193        }
4194    };
4195    if children.len() < 2 {
4196        return Err(
4197            "proof assumption form must be `(assumption <name> (judgement <j>))` or `(axiom <name> (judgement <j>))`".to_string(),
4198        );
4199    }
4200    let kind = match &children[0] {
4201        Node::Leaf(s) if s == "assumption" || s == "axiom" => s.clone(),
4202        _ => {
4203            return Err(
4204                "proof assumption form must be `(assumption <name> (judgement <j>))` or `(axiom <name> (judgement <j>))`".to_string(),
4205            );
4206        }
4207    };
4208    let name = match &children[1] {
4209        Node::Leaf(s) if !s.is_empty() => s.clone(),
4210        _ => return Err(format!("{} name must be a non-empty identifier", kind)),
4211    };
4212    let mut judgement: Option<Node> = None;
4213    for child in &children[2..] {
4214        let clause = match child {
4215            Node::List(c) => c,
4216            _ => {
4217                return Err(format!(
4218                    "{} {}: clauses must be lists led by a keyword",
4219                    kind, name
4220                ));
4221            }
4222        };
4223        let key = match clause.first() {
4224            Some(Node::Leaf(k)) => k.as_str(),
4225            _ => {
4226                return Err(format!(
4227                    "{} {}: clauses must be lists led by a keyword",
4228                    kind, name
4229                ));
4230            }
4231        };
4232        match key {
4233            "judgement" => {
4234                if clause.len() != 2 {
4235                    return Err(format!(
4236                        "{} {}: (judgement <j>) requires one argument",
4237                        kind, name
4238                    ));
4239                }
4240                if judgement.is_some() {
4241                    return Err(format!(
4242                        "{} {}: only one (judgement ...) clause is allowed",
4243                        kind, name
4244                    ));
4245                }
4246                judgement = Some(clause[1].clone());
4247            }
4248            other => {
4249                return Err(format!(
4250                    "{} {}: unknown clause keyword {}",
4251                    kind, name, other
4252                ));
4253            }
4254        }
4255    }
4256    let judgement = judgement
4257        .ok_or_else(|| format!("{} {}: (judgement <j>) clause is required", kind, name))?;
4258    Ok(ProofAssumption {
4259        name,
4260        kind,
4261        judgement,
4262    })
4263}
4264
4265// Parse `(proof-object <name> (applies <rule>) (premise <judgement>)...
4266// (premise-by <dependency>)... (uses <dependency>...) (conclusion <judgement>))`
4267// into a descriptor stored on the env.
4268pub fn parse_proof_object_form(node: &Node) -> Result<ProofObject, String> {
4269    let children = match node {
4270        Node::List(items) => items,
4271        _ => {
4272            return Err(
4273                "proof-object form must be `(proof-object <name> (applies <rule>) ...)`"
4274                    .to_string(),
4275            );
4276        }
4277    };
4278    if children.len() < 2 || !matches!(children.first(), Some(Node::Leaf(h)) if h == "proof-object")
4279    {
4280        return Err(
4281            "proof-object form must be `(proof-object <name> (applies <rule>) ...)`".to_string(),
4282        );
4283    }
4284    let name = match &children[1] {
4285        Node::Leaf(s) if !s.is_empty() => s.clone(),
4286        _ => return Err("proof-object name must be a non-empty identifier".to_string()),
4287    };
4288    let mut rule: Option<String> = None;
4289    let mut premises: Vec<Node> = Vec::new();
4290    let mut premise_refs: Vec<String> = Vec::new();
4291    let mut conclusion: Option<Node> = None;
4292    for child in &children[2..] {
4293        let clause = match child {
4294            Node::List(c) => c,
4295            _ => {
4296                return Err(format!(
4297                    "proof-object {}: clauses must be lists led by a keyword",
4298                    name
4299                ));
4300            }
4301        };
4302        let key = match clause.first() {
4303            Some(Node::Leaf(k)) => k.as_str(),
4304            _ => {
4305                return Err(format!(
4306                    "proof-object {}: clauses must be lists led by a keyword",
4307                    name
4308                ));
4309            }
4310        };
4311        match key {
4312            "applies" => {
4313                if clause.len() != 2 {
4314                    return Err(format!(
4315                        "proof-object {}: (applies <rule>) requires a rule name",
4316                        name
4317                    ));
4318                }
4319                rule = match &clause[1] {
4320                    Node::Leaf(s) if !s.is_empty() => Some(s.clone()),
4321                    _ => {
4322                        return Err(format!(
4323                            "proof-object {}: (applies <rule>) requires a rule name",
4324                            name
4325                        ));
4326                    }
4327                };
4328            }
4329            "premise" => {
4330                if clause.len() != 2 {
4331                    return Err(format!(
4332                        "proof-object {}: (premise <judgement>) requires one argument",
4333                        name
4334                    ));
4335                }
4336                premises.push(clause[1].clone());
4337            }
4338            "premise-by" => {
4339                if clause.len() != 2 {
4340                    return Err(format!(
4341                        "proof-object {}: (premise-by <name>) requires a dependency name",
4342                        name
4343                    ));
4344                }
4345                match &clause[1] {
4346                    Node::Leaf(s) if !s.is_empty() => premise_refs.push(s.clone()),
4347                    _ => {
4348                        return Err(format!(
4349                            "proof-object {}: (premise-by <name>) requires a dependency name",
4350                            name
4351                        ));
4352                    }
4353                }
4354            }
4355            "uses" => {
4356                if clause.len() < 2 {
4357                    return Err(format!(
4358                        "proof-object {}: (uses <name>...) requires at least one dependency name",
4359                        name
4360                    ));
4361                }
4362                for dep in &clause[1..] {
4363                    match dep {
4364                        Node::Leaf(s) if !s.is_empty() => premise_refs.push(s.clone()),
4365                        _ => {
4366                            return Err(format!(
4367                                "proof-object {}: (uses ...) dependencies must be names",
4368                                name
4369                            ));
4370                        }
4371                    }
4372                }
4373            }
4374            "conclusion" => {
4375                if clause.len() != 2 {
4376                    return Err(format!(
4377                        "proof-object {}: (conclusion <judgement>) requires one argument",
4378                        name
4379                    ));
4380                }
4381                if conclusion.is_some() {
4382                    return Err(format!(
4383                        "proof-object {}: only one (conclusion ...) clause is allowed",
4384                        name
4385                    ));
4386                }
4387                conclusion = Some(clause[1].clone());
4388            }
4389            other => {
4390                return Err(format!(
4391                    "proof-object {}: unknown clause keyword {}",
4392                    name, other
4393                ));
4394            }
4395        }
4396    }
4397    let rule =
4398        rule.ok_or_else(|| format!("proof-object {}: (applies <rule>) clause is required", name))?;
4399    let conclusion = conclusion.ok_or_else(|| {
4400        format!(
4401            "proof-object {}: (conclusion <judgement>) clause is required",
4402            name
4403        )
4404    })?;
4405    Ok(ProofObject {
4406        name,
4407        rule,
4408        premises,
4409        premise_refs,
4410        conclusion,
4411    })
4412}
4413
4414// Structural matcher mirroring the JS `matchProofPattern`. `?meta` leaves
4415// bind into `subs`; repeated metavariables must structurally match via
4416// `key_of`. Lists must have equal length and match pair-wise. Returns true
4417// on success and mutates `subs` in place.
4418pub fn match_proof_pattern(
4419    pattern: &Node,
4420    candidate: &Node,
4421    subs: &mut HashMap<String, Node>,
4422) -> bool {
4423    match pattern {
4424        Node::Leaf(token) if token.starts_with('?') => {
4425            if let Some(prev) = subs.get(token) {
4426                key_of(prev) == key_of(candidate)
4427            } else {
4428                subs.insert(token.clone(), candidate.clone());
4429                true
4430            }
4431        }
4432        Node::Leaf(token) => matches!(candidate, Node::Leaf(c) if c == token),
4433        Node::List(pat_children) => match candidate {
4434            Node::List(cand_children) => {
4435                if pat_children.len() != cand_children.len() {
4436                    return false;
4437                }
4438                for (p, c) in pat_children.iter().zip(cand_children.iter()) {
4439                    if !match_proof_pattern(p, c, subs) {
4440                        return false;
4441                    }
4442                }
4443                true
4444            }
4445            _ => false,
4446        },
4447    }
4448}
4449
4450/// Result of validating a proof-object against its declared rule. On success
4451/// the substitution map records each metavariable's witness; on failure the
4452/// error string is suitable for surfacing as an E064 diagnostic.
4453pub enum CheckProofVerdict {
4454    Ok(HashMap<String, Node>),
4455    Err(String),
4456}
4457
4458pub fn check_proof_object(env: &Env, name: &str) -> CheckProofVerdict {
4459    match check_proof_object_inner(env, name, &[]) {
4460        Ok(subs) => CheckProofVerdict::Ok(subs),
4461        Err(message) => CheckProofVerdict::Err(message),
4462    }
4463}
4464
4465fn resolve_proof_dependency(env: &Env, name: &str, stack: &[String]) -> Result<Node, String> {
4466    if let Some(assumption) = env.get_proof_assumption(name) {
4467        return Ok(assumption.judgement.clone());
4468    }
4469    let po = env
4470        .get_proof_object(name)
4471        .ok_or_else(|| format!("unknown proof dependency {}", name))?;
4472    check_proof_object_inner(env, name, stack)?;
4473    Ok(po.conclusion.clone())
4474}
4475
4476fn check_proof_object_inner(
4477    env: &Env,
4478    name: &str,
4479    stack: &[String],
4480) -> Result<HashMap<String, Node>, String> {
4481    if stack.iter().any(|n| n == name) {
4482        let mut cycle = stack.to_vec();
4483        cycle.push(name.to_string());
4484        return Err(format!("cyclic proof dependency: {}", cycle.join(" -> ")));
4485    }
4486    let po = match env.get_proof_object(name) {
4487        Some(po) => po,
4488        None => return Err(format!("unknown proof-object {}", name)),
4489    };
4490    let rule = match env.get_proof_rule(&po.rule) {
4491        Some(r) => r,
4492        None => {
4493            return Err(format!(
4494                "proof-object {} references unknown rule {}",
4495                name, po.rule
4496            ));
4497        }
4498    };
4499
4500    let mut effective_premises = po.premises.clone();
4501    if !po.premise_refs.is_empty() {
4502        effective_premises.clear();
4503        let mut dependency_stack = stack.to_vec();
4504        dependency_stack.push(name.to_string());
4505        for (idx, dep) in po.premise_refs.iter().enumerate() {
4506            let judgement = resolve_proof_dependency(env, dep, &dependency_stack)?;
4507            if let Some(explicit) = po.premises.get(idx) {
4508                if key_of(explicit) != key_of(&judgement) {
4509                    return Err(format!(
4510                        "proof-object {}: premise {} does not match referenced judgement {}",
4511                        name,
4512                        idx + 1,
4513                        dep
4514                    ));
4515                }
4516            }
4517            effective_premises.push(judgement);
4518        }
4519        if !po.premises.is_empty() && po.premises.len() != po.premise_refs.len() {
4520            return Err(format!(
4521                "proof-object {}: has {} explicit premise(s) but {} proof dependency reference(s)",
4522                name,
4523                po.premises.len(),
4524                po.premise_refs.len()
4525            ));
4526        }
4527    } else if !po.premises.is_empty() {
4528        return Err(format!(
4529            "proof-object {}: premise 1 is unjustified; use (premise-by <name>) or declare an assumption/axiom",
4530            name
4531        ));
4532    }
4533
4534    if effective_premises.len() != rule.premises.len() {
4535        return Err(format!(
4536            "proof-object {}: expected {} premise(s) for rule {}, got {}",
4537            name,
4538            rule.premises.len(),
4539            po.rule,
4540            effective_premises.len()
4541        ));
4542    }
4543    let mut subs: HashMap<String, Node> = HashMap::new();
4544    for (i, (pat, cand)) in rule
4545        .premises
4546        .iter()
4547        .zip(effective_premises.iter())
4548        .enumerate()
4549    {
4550        if !match_proof_pattern(pat, cand, &mut subs) {
4551            return Err(format!(
4552                "proof-object {}: premise {} does not match rule {}",
4553                name,
4554                i + 1,
4555                po.rule
4556            ));
4557        }
4558    }
4559    if !match_proof_pattern(&rule.conclusion, &po.conclusion, &mut subs) {
4560        return Err(format!(
4561            "proof-object {}: conclusion does not match rule {}",
4562            name, po.rule
4563        ));
4564    }
4565    Ok(subs)
4566}
4567
4568// ---------- Pure-links strict mode (issue #97, Phase 6) ----------
4569//
4570// Mirrors the JS implementation in `js/src/rml-links.mjs`. The forms
4571// `(strict-foundation pure-links)` and `(allow-host-primitive <name>...)`
4572// flip the audit on and whitelist specific constructs respectively. Every
4573// query is then scanned: any operator leaf whose registered root-construct
4574// status is `host-primitive` or `host-derived` raises an E065 diagnostic
4575// unless the construct name is in `env.allowed_host_primitives`.
4576
4577/// Parsed `(strict-foundation <profile>)` directive.
4578#[derive(Debug, Clone, PartialEq, Eq)]
4579pub struct StrictFoundationDecl {
4580    pub profile: String,
4581}
4582
4583/// Parsed `(allow-host-primitive <name>...)` directive.
4584#[derive(Debug, Clone, PartialEq, Eq)]
4585pub struct AllowHostPrimitiveDecl {
4586    pub names: Vec<String>,
4587}
4588
4589pub fn parse_strict_foundation_form(node: &Node) -> Result<StrictFoundationDecl, String> {
4590    let children = match node {
4591        Node::List(items) => items,
4592        _ => return Err("(strict-foundation <profile>) is required".to_string()),
4593    };
4594    if children.is_empty() || !matches!(children.first(), Some(Node::Leaf(h)) if h == "strict-foundation") {
4595        return Err("(strict-foundation <profile>) is required".to_string());
4596    }
4597    if children.len() != 2 {
4598        return Err("(strict-foundation <profile>) requires a single profile name".to_string());
4599    }
4600    let profile = match &children[1] {
4601        Node::Leaf(s) if !s.is_empty() => s.clone(),
4602        _ => return Err("(strict-foundation <profile>) requires a single profile name".to_string()),
4603    };
4604    if profile != "pure-links" {
4605        return Err(format!(
4606            "unknown strict-foundation profile: {} (expected pure-links)",
4607            profile
4608        ));
4609    }
4610    Ok(StrictFoundationDecl { profile })
4611}
4612
4613pub fn parse_allow_host_primitive_form(node: &Node) -> Result<AllowHostPrimitiveDecl, String> {
4614    let children = match node {
4615        Node::List(items) => items,
4616        _ => return Err("(allow-host-primitive <name>...) is required".to_string()),
4617    };
4618    if children.is_empty() || !matches!(children.first(), Some(Node::Leaf(h)) if h == "allow-host-primitive") {
4619        return Err("(allow-host-primitive <name>...) is required".to_string());
4620    }
4621    if children.len() < 2 {
4622        return Err(
4623            "(allow-host-primitive <name>...) requires at least one construct name".to_string(),
4624        );
4625    }
4626    let mut names = Vec::new();
4627    for child in &children[1..] {
4628        match child {
4629            Node::Leaf(s) if !s.is_empty() => names.push(s.clone()),
4630            _ => {
4631                return Err(
4632                    "(allow-host-primitive ...) names must be non-empty identifiers".to_string()
4633                );
4634            }
4635        }
4636    }
4637    Ok(AllowHostPrimitiveDecl { names })
4638}
4639
4640/// Operator leaves the strict scanner explicitly ignores — surface keywords
4641/// (`with`, `proof`, `?`) and registry meta-forms that have nothing to do
4642/// with the host-primitive substrate.
4643fn pure_links_scanner_ignored(name: &str) -> bool {
4644    matches!(
4645        name,
4646        "?" | "with"
4647            | "proof"
4648            | "by"
4649            | "because"
4650            | "let"
4651            | "in"
4652            | "where"
4653            | ":"
4654            | "::"
4655            | "has"
4656            | "probability"
4657            | "is"
4658            | "a"
4659            | "an"
4660            | "sequence"
4661            | "normalizes-to"
4662            | "applies"
4663            | "premise"
4664            | "premise-by"
4665            | "conclusion"
4666            | "uses"
4667            | "judgement"
4668            | "assumption"
4669            | "axiom"
4670            | "rule"
4671            | "proof-object"
4672            | "check-proof"
4673            | "proof-report"
4674            | "foundation"
4675            | "with-foundation"
4676            | "foundation-report"
4677            | "foundation-report?"
4678            | "root-construct"
4679            | "strict-carrier"
4680            | "truth-table"
4681            | "strict-foundation"
4682            | "allow-host-primitive"
4683    )
4684}
4685
4686fn is_strictly_offending_status(status: Option<&String>) -> bool {
4687    match status {
4688        Some(s) => s == "host-primitive" || s == "host-derived",
4689        None => false,
4690    }
4691}
4692
4693fn strict_dependency_offenders(env: &Env, name: &str, path: &[String]) -> Vec<String> {
4694    if env.allowed_host_primitives.contains(name) {
4695        return Vec::new();
4696    }
4697    if path.iter().any(|n| n == name) {
4698        return Vec::new();
4699    }
4700    let mut current_path = path.to_vec();
4701    current_path.push(name.to_string());
4702    let active = env.active_implementations.get(name);
4703    let rc = env.root_constructs.get(name);
4704    let status = active
4705        .and_then(|i| i.status.as_ref())
4706        .or_else(|| rc.and_then(|r| r.status.as_ref()));
4707    let deps: Vec<String> = active
4708        .map(|i| i.depends_on.clone())
4709        .or_else(|| rc.map(|r| r.depends_on.clone()))
4710        .unwrap_or_default();
4711
4712    if matches!(
4713        active.and_then(|i| i.status.as_deref()),
4714        Some("links-defined")
4715    ) && deps.is_empty()
4716    {
4717        return Vec::new();
4718    }
4719    if is_strictly_offending_status(status) && deps.is_empty() {
4720        return vec![format!(
4721            "{} -> {}",
4722            current_path.join(" -> "),
4723            status.cloned().unwrap_or_default()
4724        )];
4725    }
4726
4727    let mut offenders: Vec<String> = Vec::new();
4728    for dep in deps {
4729        if env.allowed_host_primitives.contains(&dep) {
4730            continue;
4731        }
4732        offenders.extend(strict_dependency_offenders(env, &dep, &current_path));
4733    }
4734    if is_strictly_offending_status(status) && offenders.is_empty() {
4735        offenders.push(format!(
4736            "{} -> {}",
4737            current_path.join(" -> "),
4738            status.cloned().unwrap_or_default()
4739        ));
4740    }
4741    offenders
4742}
4743
4744/// Walk a queried node and return sorted, deduplicated dependency paths that
4745/// end at a `host-primitive` or `host-derived` construct and are not covered
4746/// by `(allow-host-primitive ...)`.
4747pub fn scan_pure_links_offenders(node: &Node, env: &Env) -> Vec<String> {
4748    if !env.strict_pure_links {
4749        return Vec::new();
4750    }
4751    let mut offenders: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
4752    fn check(name: &str, env: &Env, offenders: &mut std::collections::BTreeSet<String>) {
4753        if pure_links_scanner_ignored(name) || env.allowed_host_primitives.contains(name) {
4754            return;
4755        }
4756        for offender in strict_dependency_offenders(env, name, &[]) {
4757            offenders.insert(offender);
4758        }
4759    }
4760    fn visit(node: &Node, env: &Env, offenders: &mut std::collections::BTreeSet<String>) {
4761        match node {
4762            Node::List(children) => {
4763                if let Some(Node::Leaf(head)) = children.first() {
4764                    check(head, env, offenders);
4765                }
4766                // Infix (L op R) — operator is the middle element.
4767                if children.len() == 3 {
4768                    if let Node::Leaf(op) = &children[1] {
4769                        check(op, env, offenders);
4770                    }
4771                }
4772                for c in children {
4773                    visit(c, env, offenders);
4774                }
4775            }
4776            Node::Leaf(s) => {
4777                check(s, env, offenders);
4778            }
4779        }
4780    }
4781    visit(node, env, &mut offenders);
4782    offenders.into_iter().collect()
4783}
4784
4785/// Render the foundation report as a human-readable text block. Mirrors
4786/// the JS `formatFoundationReport` helper.
4787pub fn format_foundation_report(report: &FoundationReport) -> String {
4788    let mut lines: Vec<String> = Vec::new();
4789    lines.push("Foundation report:".to_string());
4790    lines.push(format!("  active foundation: {}", report.active_foundation));
4791    if let Some(d) = &report.description {
4792        lines.push(format!("  description: {}", d));
4793    }
4794    if let Some(n) = &report.numeric_domain {
4795        lines.push(format!("  numeric domain: {}", n));
4796    }
4797    if let Some(t) = &report.truth_domain {
4798        lines.push(format!("  truth domain: {}", t));
4799    }
4800    let ordered_statuses = [
4801        "host-primitive",
4802        "host-derived",
4803        "external-trusted",
4804        "user-configurable",
4805        "links-encoded",
4806        "links-defined",
4807        "user-overridden",
4808        "planned",
4809    ];
4810    let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
4811    for status in ordered_statuses.iter() {
4812        if let Some((_, names)) = report.by_status.iter().find(|(s, _)| s == status) {
4813            if !names.is_empty() {
4814                lines.push(String::new());
4815                lines.push(format!("{}:", status));
4816                for n in names {
4817                    lines.push(format!("  - {}", n));
4818                }
4819                seen.insert((*status).to_string());
4820            }
4821        }
4822    }
4823    for (status, names) in &report.by_status {
4824        if seen.contains(status) || names.is_empty() {
4825            continue;
4826        }
4827        lines.push(String::new());
4828        lines.push(format!("{}:", status));
4829        for n in names {
4830            lines.push(format!("  - {}", n));
4831        }
4832    }
4833    if !report.by_semantic_status.is_empty() {
4834        lines.push(String::new());
4835        lines.push("semantic statuses:".to_string());
4836        let mut seen_semantic: std::collections::HashSet<String> =
4837            std::collections::HashSet::new();
4838        for status in SEMANTIC_STATUS_ORDER.iter() {
4839            if let Some((_, names)) = report
4840                .by_semantic_status
4841                .iter()
4842                .find(|(s, _)| s == status)
4843            {
4844                if !names.is_empty() {
4845                    lines.push(format!("  {}: {}", status, names.join(", ")));
4846                    seen_semantic.insert((*status).to_string());
4847                }
4848            }
4849        }
4850        for (status, names) in &report.by_semantic_status {
4851            if seen_semantic.contains(status) || names.is_empty() {
4852                continue;
4853            }
4854            lines.push(format!("  {}: {}", status, names.join(", ")));
4855        }
4856    }
4857    if !report.active_implementations.is_empty() {
4858        lines.push(String::new());
4859        lines.push("active implementations:".to_string());
4860        for implementation in &report.active_implementations {
4861            let mut parts: Vec<String> = Vec::new();
4862            if let Some(status) = &implementation.status {
4863                parts.push(status.clone());
4864            }
4865            if let Some(semantic_status) = &implementation.semantic_status {
4866                parts.push(format!("semantic {}", semantic_status));
4867            }
4868            if let Some(implementation_name) = &implementation.implementation {
4869                parts.push(format!("via {}", implementation_name));
4870            }
4871            if let Some(foundation) = &implementation.foundation {
4872                parts.push(format!("foundation {}", foundation));
4873            }
4874            if !implementation.depends_on.is_empty() {
4875                parts.push(format!(
4876                    "depends on {}",
4877                    implementation.depends_on.join(", ")
4878                ));
4879            }
4880            lines.push(format!(
4881                "  - {}: {}",
4882                implementation.construct,
4883                parts.join("; ")
4884            ));
4885        }
4886    }
4887    if !report.proof_rules.is_empty() {
4888        lines.push(String::new());
4889        lines.push("proof rules:".to_string());
4890        for r in &report.proof_rules {
4891            lines.push(format!(
4892                "  - {} ({} premises → {})",
4893                r.name,
4894                r.premises.len(),
4895                r.conclusion
4896            ));
4897        }
4898    }
4899    if !report.proof_assumptions.is_empty() {
4900        lines.push(String::new());
4901        lines.push("proof assumptions:".to_string());
4902        for a in &report.proof_assumptions {
4903            lines.push(format!("  - {} [{}] : {}", a.name, a.kind, a.judgement));
4904        }
4905    }
4906    if !report.proof_objects.is_empty() {
4907        lines.push(String::new());
4908        lines.push("proof objects:".to_string());
4909        for po in &report.proof_objects {
4910            let refs = if po.premise_refs.is_empty() {
4911                String::new()
4912            } else {
4913                format!(" using {}", po.premise_refs.join(", "))
4914            };
4915            lines.push(format!(
4916                "  - {} : applies {} ({} premises{} → {})",
4917                po.name,
4918                po.rule,
4919                po.premises.len(),
4920                refs,
4921                po.conclusion
4922            ));
4923        }
4924    }
4925    if !report.foundations.is_empty() {
4926        lines.push(String::new());
4927        lines.push("foundations:".to_string());
4928        for f in &report.foundations {
4929            let suffix = f
4930                .description
4931                .as_ref()
4932                .map(|d| format!(" — {}", d))
4933                .unwrap_or_default();
4934            let tag = if f.experimental {
4935                " [experimental]"
4936            } else {
4937                ""
4938            };
4939            lines.push(format!("  - {}{}{}", f.name, tag, suffix));
4940            if let Some(n) = &f.numeric_domain {
4941                lines.push(format!("      numeric domain: {}", n));
4942            }
4943            if let Some(t) = &f.truth_domain {
4944                lines.push(format!("      truth domain: {}", t));
4945            }
4946            if let Some(r) = &f.root {
4947                lines.push(format!("      root: {}", r));
4948            }
4949            if !f.abits.is_empty() {
4950                let parts: Vec<String> = f
4951                    .abits
4952                    .iter()
4953                    .map(|(s, m)| format!("{}={}", s, m))
4954                    .collect();
4955                lines.push(format!("      abits: {}", parts.join(", ")));
4956            }
4957            if !f.uses.is_empty() {
4958                lines.push(format!("      uses: {}", f.uses.join(", ")));
4959            }
4960            if !f.defines.is_empty() {
4961                let parts: Vec<String> = f
4962                    .defines
4963                    .iter()
4964                    .map(|(k, v)| format!("{}={}", k, v))
4965                    .collect();
4966                lines.push(format!("      defines: {}", parts.join(", ")));
4967            }
4968            if !f.truth_tables.is_empty() {
4969                let mut sorted: Vec<&(String, Vec<TruthTableRow>)> =
4970                    f.truth_tables.iter().collect();
4971                sorted.sort_by(|a, b| a.0.cmp(&b.0));
4972                let parts: Vec<String> = sorted
4973                    .iter()
4974                    .map(|(op, rows)| format!("{}({} rows)", op, rows.len()))
4975                    .collect();
4976                lines.push(format!("      truth tables: {}", parts.join(", ")));
4977            }
4978        }
4979    }
4980    if report.strict_pure_links {
4981        lines.push(String::new());
4982        lines.push("pure-links strict mode: on".to_string());
4983        if !report.allowed_host_primitives.is_empty() {
4984            lines.push(format!(
4985                "  allowed host primitives: {}",
4986                report.allowed_host_primitives.join(", ")
4987            ));
4988        }
4989    }
4990    if !report.dependency_graph.is_empty() {
4991        let non_empty: Vec<&(String, Vec<String>)> = report
4992            .dependency_graph
4993            .iter()
4994            .filter(|(_, deps)| !deps.is_empty())
4995            .collect();
4996        if !non_empty.is_empty() {
4997            lines.push(String::new());
4998            lines.push("dependency graph (transitive):".to_string());
4999            for (name, deps) in non_empty {
5000                lines.push(format!("  - {} → {}", name, deps.join(", ")));
5001            }
5002        }
5003    }
5004    lines.join("\n")
5005}
5006
5007/// Pretty-print a [`ProofReport`] for the REPL / CLI. Mirrors the JS
5008/// `formatProofReport` shape so transcripts agree across runtimes.
5009pub fn format_proof_report(report: &ProofReport) -> String {
5010    let mut lines: Vec<String> = Vec::new();
5011    lines.push(format!("Proof report for {}:", report.name));
5012    lines.push(format!(
5013        "  verdict: {}",
5014        if report.verdict.ok { "ok" } else { "error" }
5015    ));
5016    if let Some(err) = &report.verdict.error {
5017        lines.push(format!("  error: {}", err));
5018    }
5019    if let Some(rule) = &report.rule {
5020        lines.push(format!("  rule: {}", rule));
5021    }
5022    if let Some(conc) = &report.conclusion {
5023        lines.push(format!("  conclusion: {}", conc));
5024    }
5025    if !report.premises.is_empty() {
5026        lines.push(format!("  premises: {}", report.premises.join(", ")));
5027    }
5028    if !report.premise_refs.is_empty() {
5029        lines.push(format!(
5030            "  premise refs: {}",
5031            report.premise_refs.join(", ")
5032        ));
5033    }
5034    if !report.dependencies.is_empty() {
5035        lines.push(String::new());
5036        lines.push("dependencies (transitive):".to_string());
5037        for d in &report.dependencies {
5038            let extra = match (&d.rule, &d.judgement) {
5039                (Some(r), Some(j)) => format!(" — applies {} → {}", r, j),
5040                (Some(r), None) => format!(" — applies {}", r),
5041                (None, Some(j)) => format!(" — {}", j),
5042                (None, None) => String::new(),
5043            };
5044            lines.push(format!("  - {} [{}]{}", d.name, d.kind, extra));
5045        }
5046    }
5047    if !report.rules.is_empty() {
5048        lines.push(String::new());
5049        lines.push(format!("rules applied: {}", report.rules.join(", ")));
5050    }
5051    if !report.root_constructs_used.is_empty() {
5052        lines.push(String::new());
5053        lines.push(format!(
5054            "root constructs used: {}",
5055            report.root_constructs_used.join(", ")
5056        ));
5057    }
5058    if !report.by_semantic_status.is_empty() {
5059        lines.push(String::new());
5060        lines.push("semantic statuses:".to_string());
5061        let mut seen: std::collections::HashSet<String> =
5062            std::collections::HashSet::new();
5063        for status in SEMANTIC_STATUS_ORDER.iter() {
5064            if let Some((_, names)) = report
5065                .by_semantic_status
5066                .iter()
5067                .find(|(s, _)| s == status)
5068            {
5069                if !names.is_empty() {
5070                    lines.push(format!("  {}: {}", status, names.join(", ")));
5071                    seen.insert((*status).to_string());
5072                }
5073            }
5074        }
5075        for (status, names) in &report.by_semantic_status {
5076            if seen.contains(status) || names.is_empty() {
5077                continue;
5078            }
5079            lines.push(format!("  {}: {}", status, names.join(", ")));
5080        }
5081    }
5082    if !report.by_trust_status.is_empty() {
5083        lines.push(String::new());
5084        lines.push("trust statuses:".to_string());
5085        for (status, names) in &report.by_trust_status {
5086            if names.is_empty() {
5087                continue;
5088            }
5089            lines.push(format!("  {}: {}", status, names.join(", ")));
5090        }
5091    }
5092    lines.push(String::new());
5093    lines.push(format!("  active foundation: {}", report.active_foundation));
5094    if report.strict_pure_links {
5095        lines.push("  pure-links strict mode: on".to_string());
5096    }
5097    lines.join("\n")
5098}
5099
5100/// Result of `(eval-nat <term>)`. `normal_form` is the semantic result; the
5101/// numeric `value` is only the legacy renderer for that Peano normal form.
5102#[derive(Debug, Clone, PartialEq)]
5103pub struct EvalNatResult {
5104    pub value: f64,
5105    pub normal_form: Node,
5106    pub steps: Vec<String>,
5107}
5108
5109fn leaf_node(s: &str) -> Node {
5110    Node::Leaf(s.to_string())
5111}
5112
5113fn list_node(items: Vec<Node>) -> Node {
5114    Node::List(items)
5115}
5116
5117fn default_eval_nat_rule(name: &str) -> Option<ProofRule> {
5118    match name {
5119        "nat-add-zero" => Some(ProofRule {
5120            name: name.to_string(),
5121            premises: vec![list_node(vec![
5122                leaf_node("?n"),
5123                leaf_node("has-type"),
5124                leaf_node("Nat"),
5125            ])],
5126            conclusion: list_node(vec![
5127                list_node(vec![leaf_node("add"), leaf_node("zero"), leaf_node("?n")]),
5128                leaf_node("nat-equals"),
5129                leaf_node("?n"),
5130            ]),
5131        }),
5132        "nat-add-succ" => Some(ProofRule {
5133            name: name.to_string(),
5134            premises: vec![list_node(vec![
5135                list_node(vec![leaf_node("add"), leaf_node("?m"), leaf_node("?n")]),
5136                leaf_node("nat-equals"),
5137                leaf_node("?k"),
5138            ])],
5139            conclusion: list_node(vec![
5140                list_node(vec![
5141                    leaf_node("add"),
5142                    list_node(vec![leaf_node("succ"), leaf_node("?m")]),
5143                    leaf_node("?n"),
5144                ]),
5145                leaf_node("nat-equals"),
5146                list_node(vec![leaf_node("succ"), leaf_node("?k")]),
5147            ]),
5148        }),
5149        "nat-mul-zero" => Some(ProofRule {
5150            name: name.to_string(),
5151            premises: vec![list_node(vec![
5152                leaf_node("?n"),
5153                leaf_node("has-type"),
5154                leaf_node("Nat"),
5155            ])],
5156            conclusion: list_node(vec![
5157                list_node(vec![leaf_node("mul"), leaf_node("zero"), leaf_node("?n")]),
5158                leaf_node("nat-equals"),
5159                leaf_node("zero"),
5160            ]),
5161        }),
5162        "nat-mul-succ" => Some(ProofRule {
5163            name: name.to_string(),
5164            premises: vec![
5165                list_node(vec![
5166                    list_node(vec![leaf_node("mul"), leaf_node("?m"), leaf_node("?n")]),
5167                    leaf_node("nat-equals"),
5168                    leaf_node("?k"),
5169                ]),
5170                list_node(vec![
5171                    list_node(vec![leaf_node("add"), leaf_node("?n"), leaf_node("?k")]),
5172                    leaf_node("nat-equals"),
5173                    leaf_node("?s"),
5174                ]),
5175            ],
5176            conclusion: list_node(vec![
5177                list_node(vec![
5178                    leaf_node("mul"),
5179                    list_node(vec![leaf_node("succ"), leaf_node("?m")]),
5180                    leaf_node("?n"),
5181                ]),
5182                leaf_node("nat-equals"),
5183                leaf_node("?s"),
5184            ]),
5185        }),
5186        _ => None,
5187    }
5188}
5189
5190fn instantiate_proof_pattern(pattern: &Node, subs: &HashMap<String, Node>) -> Node {
5191    match pattern {
5192        Node::Leaf(token) if token.starts_with('?') => {
5193            subs.get(token).cloned().unwrap_or_else(|| pattern.clone())
5194        }
5195        Node::Leaf(_) => pattern.clone(),
5196        Node::List(children) => Node::List(
5197            children
5198                .iter()
5199                .map(|child| instantiate_proof_pattern(child, subs))
5200                .collect(),
5201        ),
5202    }
5203}
5204
5205fn eval_nat_foundation_uses(
5206    env: &Env,
5207    foundation_name: &str,
5208    rule_name: &str,
5209    seen: &mut HashSet<String>,
5210) -> bool {
5211    if !seen.insert(foundation_name.to_string()) {
5212        return false;
5213    }
5214    let Some(foundation) = env.get_foundation(foundation_name) else {
5215        return false;
5216    };
5217    if foundation.uses.iter().any(|u| u == rule_name) {
5218        return true;
5219    }
5220    foundation
5221        .extends
5222        .as_deref()
5223        .map(|parent| eval_nat_foundation_uses(env, parent, rule_name, seen))
5224        .unwrap_or(false)
5225}
5226
5227fn eval_nat_active_foundation_uses(env: &Env, name: &str) -> bool {
5228    let active = if env.active_foundation.is_empty() {
5229        "default-rml"
5230    } else {
5231        env.active_foundation.as_str()
5232    };
5233    if active == "default-rml" {
5234        return true;
5235    }
5236    eval_nat_foundation_uses(env, active, name, &mut HashSet::new())
5237}
5238
5239fn eval_nat_rule(env: &Env, name: &str) -> Result<ProofRule, String> {
5240    if !eval_nat_active_foundation_uses(env, name) {
5241        return Err(format!(
5242            "eval-nat requires {}, but it is not available in active foundation {}",
5243            name,
5244            if env.active_foundation.is_empty() {
5245                "default-rml"
5246            } else {
5247                env.active_foundation.as_str()
5248            }
5249        ));
5250    }
5251    env.get_proof_rule(name)
5252        .cloned()
5253        .or_else(|| default_eval_nat_rule(name))
5254        .ok_or_else(|| {
5255            format!(
5256                "eval-nat requires {}, but no links-level rule is registered",
5257                name
5258            )
5259        })
5260}
5261
5262fn eval_nat_equality_conclusion<'a>(
5263    rule: &'a ProofRule,
5264    rule_name: &str,
5265) -> Result<(&'a Node, &'a Node), String> {
5266    let Node::List(children) = &rule.conclusion else {
5267        return Err(format!(
5268            "eval-nat rule {} must conclude (<term> nat-equals <term>)",
5269            rule_name
5270        ));
5271    };
5272    if children.len() != 3 || !matches!(&children[1], Node::Leaf(mid) if mid == "nat-equals") {
5273        return Err(format!(
5274            "eval-nat rule {} must conclude (<term> nat-equals <term>)",
5275            rule_name
5276        ));
5277    }
5278    Ok((&children[0], &children[2]))
5279}
5280
5281fn process_eval_nat_premises(
5282    env: &Env,
5283    rule: &ProofRule,
5284    subs: &mut HashMap<String, Node>,
5285    steps: &mut Vec<String>,
5286    depth: usize,
5287) -> Result<(), String> {
5288    for premise in &rule.premises {
5289        if let Node::List(children) = premise {
5290            if children.len() == 3 && matches!(&children[1], Node::Leaf(mid) if mid == "nat-equals")
5291            {
5292                let premise_input = instantiate_proof_pattern(&children[0], subs);
5293                let premise_normal =
5294                    normalize_eval_nat_term(env, &premise_input, steps, depth + 1)?;
5295                if !match_proof_pattern(&children[2], &premise_normal, subs) {
5296                    return Err(format!(
5297                        "eval-nat rule {} premise {} did not match normal form {}",
5298                        rule.name,
5299                        key_of(premise),
5300                        key_of(&premise_normal)
5301                    ));
5302                }
5303                continue;
5304            }
5305            if children.len() == 3
5306                && matches!(&children[1], Node::Leaf(mid) if mid == "has-type")
5307                && matches!(&children[2], Node::Leaf(ty) if ty == "Nat")
5308            {
5309                continue;
5310            }
5311        }
5312        return Err(format!(
5313            "eval-nat rule {} has unsupported premise {}",
5314            rule.name,
5315            key_of(premise)
5316        ));
5317    }
5318    Ok(())
5319}
5320
5321fn apply_eval_nat_rule(
5322    env: &Env,
5323    rule_name: &str,
5324    term: &Node,
5325    steps: &mut Vec<String>,
5326    depth: usize,
5327) -> Result<Node, String> {
5328    let rule = eval_nat_rule(env, rule_name)?;
5329    let (left, right) = eval_nat_equality_conclusion(&rule, rule_name)?;
5330    let mut subs: HashMap<String, Node> = HashMap::new();
5331    if !match_proof_pattern(left, term, &mut subs) {
5332        return Err(format!(
5333            "eval-nat rule {} does not apply to {}",
5334            rule_name,
5335            key_of(term)
5336        ));
5337    }
5338    steps.push(rule.name.clone());
5339    process_eval_nat_premises(env, &rule, &mut subs, steps, depth)?;
5340    let next = instantiate_proof_pattern(right, &subs);
5341    normalize_eval_nat_term(env, &next, steps, depth + 1)
5342}
5343
5344fn normalize_eval_nat_term(
5345    env: &Env,
5346    node: &Node,
5347    steps: &mut Vec<String>,
5348    depth: usize,
5349) -> Result<Node, String> {
5350    if depth > 10_000 {
5351        return Err("eval-nat exceeded its structural rewrite limit".to_string());
5352    }
5353    if let Node::Leaf(s) = node {
5354        if s == "zero" {
5355            return Ok(leaf_node("zero"));
5356        }
5357    }
5358    if let Node::List(children) = node {
5359        if children.len() == 2 {
5360            if let Node::Leaf(head) = &children[0] {
5361                if head == "succ" {
5362                    let inner = normalize_eval_nat_term(env, &children[1], steps, depth + 1)?;
5363                    return Ok(list_node(vec![leaf_node("succ"), inner]));
5364                }
5365            }
5366        }
5367        if children.len() == 3 {
5368            if let Node::Leaf(head) = &children[0] {
5369                if head == "add" {
5370                    let left = normalize_eval_nat_term(env, &children[1], steps, depth + 1)?;
5371                    let current =
5372                        list_node(vec![leaf_node("add"), left.clone(), children[2].clone()]);
5373                    if matches!(&left, Node::Leaf(s) if s == "zero") {
5374                        return apply_eval_nat_rule(
5375                            env,
5376                            "nat-add-zero",
5377                            &current,
5378                            steps,
5379                            depth + 1,
5380                        );
5381                    }
5382                    if matches!(&left, Node::List(items) if items.len() == 2 && matches!(&items[0], Node::Leaf(h) if h == "succ"))
5383                    {
5384                        return apply_eval_nat_rule(
5385                            env,
5386                            "nat-add-succ",
5387                            &current,
5388                            steps,
5389                            depth + 1,
5390                        );
5391                    }
5392                }
5393                if head == "mul" {
5394                    let left = normalize_eval_nat_term(env, &children[1], steps, depth + 1)?;
5395                    let current =
5396                        list_node(vec![leaf_node("mul"), left.clone(), children[2].clone()]);
5397                    if matches!(&left, Node::Leaf(s) if s == "zero") {
5398                        return apply_eval_nat_rule(
5399                            env,
5400                            "nat-mul-zero",
5401                            &current,
5402                            steps,
5403                            depth + 1,
5404                        );
5405                    }
5406                    if matches!(&left, Node::List(items) if items.len() == 2 && matches!(&items[0], Node::Leaf(h) if h == "succ"))
5407                    {
5408                        return apply_eval_nat_rule(
5409                            env,
5410                            "nat-mul-succ",
5411                            &current,
5412                            steps,
5413                            depth + 1,
5414                        );
5415                    }
5416                }
5417            }
5418        }
5419    }
5420    Err(format!(
5421        "eval-nat: not a closed Peano term: {}",
5422        key_of(node)
5423    ))
5424}
5425
5426fn peano_normal_form_to_host_number(node: &Node) -> Result<f64, String> {
5427    if let Node::Leaf(s) = node {
5428        if s == "zero" {
5429            return Ok(0.0);
5430        }
5431    }
5432    if let Node::List(children) = node {
5433        if children.len() == 2 && matches!(&children[0], Node::Leaf(head) if head == "succ") {
5434            return Ok(1.0 + peano_normal_form_to_host_number(&children[1])?);
5435        }
5436    }
5437    Err(format!(
5438        "eval-nat produced a non-Peano normal form: {}",
5439        key_of(node)
5440    ))
5441}
5442
5443/// Normalize a closed Peano term by dispatching through active links-level
5444/// computation rules. Host arithmetic is only used by the final renderer.
5445pub fn eval_nat_term(env: &Env, node: &Node) -> Result<EvalNatResult, String> {
5446    let mut steps: Vec<String> = Vec::new();
5447    let normal_form = normalize_eval_nat_term(env, node, &mut steps, 0)?;
5448    let value = peano_normal_form_to_host_number(&normal_form)?;
5449    Ok(EvalNatResult {
5450        value,
5451        normal_form,
5452        steps,
5453    })
5454}
5455
5456fn register_template_form(form: &Node, env: &mut Env) -> Result<String, String> {
5457    let children = match form {
5458        Node::List(items) => items,
5459        _ => {
5460            return Err(
5461                "Template declaration must be `(template (<name> <param>...) <body>)`".to_string(),
5462            );
5463        }
5464    };
5465    if children.len() != 3 || !matches!(children.first(), Some(Node::Leaf(h)) if h == "template") {
5466        return Err(
5467            "Template declaration must be `(template (<name> <param>...) <body>)`".to_string(),
5468        );
5469    }
5470    let (name, params) = validate_template_pattern(&children[1])?;
5471    let store_name = env.qualify_name(&name);
5472    maybe_warn_shadow(env, &store_name);
5473    env.templates.insert(
5474        store_name.clone(),
5475        TemplateDecl {
5476            name: store_name.clone(),
5477            params,
5478            body: children[2].clone(),
5479        },
5480    );
5481    Ok(store_name)
5482}
5483
5484fn substitute_template_placeholders(body: &Node, params: &[String], args: &[Node]) -> Node {
5485    let mut current = body.clone();
5486    let mut avoid = HashSet::new();
5487    collect_names(&current, &mut avoid);
5488    for arg in args {
5489        collect_names(arg, &mut avoid);
5490    }
5491    let mut sentinels = Vec::new();
5492    for param in params {
5493        let sentinel = fresh_name(&format!("__template_{}", param), &avoid);
5494        avoid.insert(sentinel.clone());
5495        sentinels.push(sentinel);
5496    }
5497    for (param, sentinel) in params.iter().zip(sentinels.iter()) {
5498        current = subst(&current, param, &Node::Leaf(sentinel.clone()));
5499    }
5500    for (sentinel, arg) in sentinels.iter().zip(args.iter()) {
5501        current = subst(&current, sentinel, arg);
5502    }
5503    current
5504}
5505
5506fn expand_templates(node: &Node, env: &Env, stack: &mut Vec<String>) -> Node {
5507    match node {
5508        Node::Leaf(_) => node.clone(),
5509        Node::List(children) => {
5510            if children.is_empty() {
5511                return node.clone();
5512            }
5513            if let Some(Node::Leaf(head)) = children.first() {
5514                if let Some(key) = template_key_for(env, head) {
5515                    let decl = env
5516                        .templates
5517                        .get(&key)
5518                        .cloned()
5519                        .expect("template key resolved to declaration");
5520                    let arg_count = children.len().saturating_sub(1);
5521                    if arg_count != decl.params.len() {
5522                        panic!(
5523                            "Template expansion error: Template \"{}\" expects {} argument{}, got {}",
5524                            head,
5525                            decl.params.len(),
5526                            if decl.params.len() == 1 { "" } else { "s" },
5527                            arg_count
5528                        );
5529                    }
5530                    if let Some(pos) = stack.iter().position(|item| item == &key) {
5531                        let mut cycle = stack[pos..].to_vec();
5532                        cycle.push(key.clone());
5533                        panic!(
5534                            "Template expansion error: Template expansion cycle detected: {}",
5535                            cycle.join(" -> ")
5536                        );
5537                    }
5538
5539                    let expanded_args: Vec<Node> = children[1..]
5540                        .iter()
5541                        .map(|arg| expand_templates(arg, env, stack))
5542                        .collect();
5543                    stack.push(key.clone());
5544                    let instantiated =
5545                        substitute_template_placeholders(&decl.body, &decl.params, &expanded_args);
5546                    let expanded = expand_templates(&instantiated, env, stack);
5547                    stack.pop();
5548                    return expanded;
5549                }
5550            }
5551            Node::List(
5552                children
5553                    .iter()
5554                    .map(|child| expand_templates(child, env, stack))
5555                    .collect(),
5556            )
5557        }
5558    }
5559}
5560
5561// ========== Evaluator ==========
5562
5563/// Evaluate a node in arithmetic context — numeric literals are NOT clamped to the logic range.
5564fn eval_arith(node: &Node, env: &mut Env) -> f64 {
5565    if let Node::Leaf(ref s) = node {
5566        if is_num(s) {
5567            return s.parse::<f64>().unwrap_or(0.0);
5568        }
5569    }
5570    match eval_node(node, env) {
5571        EvalResult::Term(term) => eval_arith(&term, env),
5572        other => other.as_f64(),
5573    }
5574}
5575
5576fn eval_term_node(node: &Node, env: &mut Env) -> Node {
5577    if let Node::List(children) = node {
5578        if children.len() == 4 {
5579            if let (Node::Leaf(head), Node::Leaf(var_name)) = (&children[0], &children[2]) {
5580                if head == "subst" {
5581                    let term = eval_term_node(&children[1], env);
5582                    let replacement = eval_term_node(&children[3], env);
5583                    let reduced = subst(&term, var_name, &replacement);
5584                    return eval_term_node(&reduced, env);
5585                }
5586            }
5587        }
5588
5589        if children.len() == 3 {
5590            if let Node::Leaf(head) = &children[0] {
5591                if head == "apply" {
5592                    let fn_node = &children[1];
5593                    let arg = eval_term_node(&children[2], env);
5594                    if let Node::List(fn_children) = fn_node {
5595                        if fn_children.len() == 3 {
5596                            if let Node::Leaf(fn_head) = &fn_children[0] {
5597                                if fn_head == "lambda" {
5598                                    if let Some((param_name, _)) = parse_binding(&fn_children[1]) {
5599                                        let reduced = subst(&fn_children[2], &param_name, &arg);
5600                                        return eval_term_node(&reduced, env);
5601                                    }
5602                                }
5603                            }
5604                        }
5605                    }
5606                    if let Node::Leaf(fn_name) = fn_node {
5607                        if let Some(lambda) = env.get_lambda(fn_name).cloned() {
5608                            let reduced = subst(&lambda.body, &lambda.param, &arg);
5609                            return eval_term_node(&reduced, env);
5610                        }
5611                    }
5612                }
5613            }
5614        }
5615
5616        if children.len() >= 2 {
5617            if let Node::List(head_children) = &children[0] {
5618                if head_children.len() == 3 {
5619                    if let Node::Leaf(fn_head) = &head_children[0] {
5620                        if fn_head == "lambda" {
5621                            if let Some((param_name, _)) = parse_binding(&head_children[1]) {
5622                                let arg = eval_term_node(&children[1], env);
5623                                let reduced = subst(&head_children[2], &param_name, &arg);
5624                                if children.len() == 2 {
5625                                    return eval_term_node(&reduced, env);
5626                                }
5627                                let mut next = vec![reduced];
5628                                next.extend_from_slice(&children[2..]);
5629                                return eval_term_node(&Node::List(next), env);
5630                            }
5631                        }
5632                    }
5633                }
5634            }
5635        }
5636    }
5637    node.clone()
5638}
5639
5640fn normalize_term(node: &Node, env: &mut Env, options: ConvertOptions) -> Node {
5641    if let Node::List(children) = node {
5642        if children.is_empty() {
5643            return Node::List(vec![]);
5644        }
5645
5646        if children.len() == 4 {
5647            if let (Node::Leaf(head), Node::Leaf(var_name)) = (&children[0], &children[2]) {
5648                if head == "subst" {
5649                    let term = normalize_term(&children[1], env, options);
5650                    let replacement = normalize_term(&children[3], env, options);
5651                    let reduced = subst(&term, var_name, &replacement);
5652                    return normalize_term(&reduced, env, options);
5653                }
5654            }
5655        }
5656
5657        if children.len() == 3 {
5658            if let Node::Leaf(head) = &children[0] {
5659                if head == "apply" {
5660                    // Normalize the head (fn) position first so beta-redexes
5661                    // exposed by inner reductions are caught here. Without
5662                    // this, terms like `(apply (apply (apply compose succ)
5663                    // succ) zero)` would print as nested `apply` calls.
5664                    let fn_node = normalize_term(&children[1], env, options);
5665                    let arg = normalize_term(&children[2], env, options);
5666                    if let Node::List(fn_children) = &fn_node {
5667                        if fn_children.len() == 3 {
5668                            if let Node::Leaf(fn_head) = &fn_children[0] {
5669                                if fn_head == "lambda" {
5670                                    if let Some((param_name, _)) = parse_binding(&fn_children[1]) {
5671                                        let reduced = subst(&fn_children[2], &param_name, &arg);
5672                                        return normalize_term(&reduced, env, options);
5673                                    }
5674                                }
5675                            }
5676                        }
5677                    }
5678                    if let Node::Leaf(fn_name) = &fn_node {
5679                        let resolved = env.resolve_qualified(fn_name);
5680                        let lambda = env
5681                            .get_lambda(fn_name)
5682                            .cloned()
5683                            .or_else(|| env.get_lambda(&resolved).cloned());
5684                        if let Some(lambda) = lambda {
5685                            let reduced = subst(&lambda.body, &lambda.param, &arg);
5686                            return normalize_term(&reduced, env, options);
5687                        }
5688                    }
5689                    return Node::List(vec![Node::Leaf("apply".into()), fn_node, arg]);
5690                }
5691
5692                if head == "lambda" {
5693                    let candidate = Node::List(vec![
5694                        Node::Leaf("lambda".into()),
5695                        normalize_term(&children[1], env, options),
5696                        normalize_term(&children[2], env, options),
5697                    ]);
5698                    return eta_contract(&candidate, env, options);
5699                }
5700            }
5701        }
5702
5703        if children.len() >= 2 {
5704            if let Node::List(head_children) = &children[0] {
5705                if head_children.len() == 3 {
5706                    if let Node::Leaf(fn_head) = &head_children[0] {
5707                        if fn_head == "lambda" {
5708                            if let Some((param_name, _)) = parse_binding(&head_children[1]) {
5709                                let arg = normalize_term(&children[1], env, options);
5710                                let reduced = subst(&head_children[2], &param_name, &arg);
5711                                if children.len() == 2 {
5712                                    return normalize_term(&reduced, env, options);
5713                                }
5714                                let mut next = vec![reduced];
5715                                next.extend_from_slice(&children[2..]);
5716                                return normalize_term(&Node::List(next), env, options);
5717                            }
5718                        }
5719                    }
5720                }
5721            }
5722        }
5723
5724        if children.len() >= 2 {
5725            if let Node::Leaf(head) = &children[0] {
5726                let resolved = env.resolve_qualified(head);
5727                let lambda = env
5728                    .get_lambda(head)
5729                    .cloned()
5730                    .or_else(|| env.get_lambda(&resolved).cloned());
5731                if let Some(lambda) = lambda {
5732                    let arg = normalize_term(&children[1], env, options);
5733                    let reduced = subst(&lambda.body, &lambda.param, &arg);
5734                    if children.len() == 2 {
5735                        return normalize_term(&reduced, env, options);
5736                    }
5737                    let mut next = vec![reduced];
5738                    next.extend_from_slice(&children[2..]);
5739                    return normalize_term(&Node::List(next), env, options);
5740                }
5741            }
5742        }
5743
5744        return Node::List(
5745            children
5746                .iter()
5747                .map(|child| normalize_term(child, env, options))
5748                .collect(),
5749        );
5750    }
5751    node.clone()
5752}
5753
5754/// Weak-head normal form (D4): reduce the spine of `node` — i.e. unfold the
5755/// head as long as there are arguments to apply to it — without descending
5756/// into binders or argument positions. Mirrors `whnfTerm` in the JS runtime
5757/// and `nf`/`is_convertible` already use `normalize_term` for the full
5758/// version. Substitution may expose a redex inside the residual body, but
5759/// that is no longer on the original spine, so this routine returns it
5760/// unevaluated; full normalization is the place that descends into those
5761/// positions.
5762pub fn whnf_term(node: &Node, env: &mut Env, options: ConvertOptions) -> Node {
5763    if let Node::List(children) = node {
5764        if children.is_empty() {
5765            return Node::List(vec![]);
5766        }
5767        if children.len() == 4 {
5768            if let (Node::Leaf(head), Node::Leaf(var_name)) = (&children[0], &children[2]) {
5769                if head == "subst" {
5770                    let term = whnf_term(&children[1], env, options);
5771                    let replacement = children[3].clone();
5772                    let reduced = subst(&term, var_name, &replacement);
5773                    return whnf_term(&reduced, env, options);
5774                }
5775            }
5776        }
5777    }
5778
5779    // Collect the leftmost-outermost `apply` spine into [head, arg1, arg2, ...]
5780    // so the loop below can β-reduce against any number of arguments without
5781    // re-entering whnf_term (which would descend into the substituted body's
5782    // spine and over-reduce — see the test "leaves arguments unevaluated").
5783    let mut spine_args: Vec<Node> = Vec::new();
5784    let mut head_node = node.clone();
5785    loop {
5786        if let Node::List(children) = &head_node {
5787            if children.len() == 3 {
5788                if let Node::Leaf(h) = &children[0] {
5789                    if h == "apply" {
5790                        spine_args.insert(0, children[2].clone());
5791                        head_node = children[1].clone();
5792                        continue;
5793                    }
5794                }
5795            }
5796        }
5797        break;
5798    }
5799
5800    // Prefix-call shape: `(f arg1 arg2 ...)` where `f` is a lambda value or a
5801    // bound name. Drain that into the spine before reducing.
5802    if spine_args.is_empty() {
5803        if let Node::List(children) = head_node.clone() {
5804            if children.len() > 1 {
5805                let head_is_lambda = matches!(
5806                    &children[0],
5807                    Node::List(lc)
5808                        if lc.len() == 3
5809                            && matches!(&lc[0], Node::Leaf(h) if h == "lambda")
5810                );
5811                let head_is_name = matches!(
5812                    &children[0],
5813                    Node::Leaf(name)
5814                        if name != "apply"
5815                            && name != "lambda"
5816                            && name != "Pi"
5817                            && name != "fresh"
5818                            && name != "subst"
5819                );
5820                if head_is_lambda || head_is_name {
5821                    head_node = children[0].clone();
5822                    spine_args.extend(children[1..].iter().cloned());
5823                }
5824            }
5825        }
5826    }
5827
5828    // Drain the spine by β-reducing against the head. Stop as soon as the
5829    // head can no longer reduce (not a lambda, not a bound name) or there
5830    // are no remaining args.
5831    while !spine_args.is_empty() {
5832        let lambda_match = if let Node::List(hc) = &head_node {
5833            if hc.len() == 3 {
5834                if let Node::Leaf(h) = &hc[0] {
5835                    if h == "lambda" {
5836                        parse_binding(&hc[1]).map(|(p, _)| (p, hc[2].clone()))
5837                    } else {
5838                        None
5839                    }
5840                } else {
5841                    None
5842                }
5843            } else {
5844                None
5845            }
5846        } else {
5847            None
5848        };
5849        if let Some((param, body)) = lambda_match {
5850            let arg = spine_args.remove(0);
5851            head_node = subst(&body, &param, &arg);
5852            continue;
5853        }
5854        if let Node::Leaf(name) = &head_node {
5855            let resolved = env.resolve_qualified(name);
5856            let lambda = env
5857                .get_lambda(name)
5858                .cloned()
5859                .or_else(|| env.get_lambda(&resolved).cloned());
5860            if let Some(lambda) = lambda {
5861                let arg = spine_args.remove(0);
5862                head_node = subst(&lambda.body, &lambda.param, &arg);
5863                continue;
5864            }
5865        }
5866        break;
5867    }
5868
5869    if spine_args.is_empty() {
5870        return head_node;
5871    }
5872    // Stuck spine: rebuild the unreduced applies around the residual head.
5873    let mut stuck = head_node;
5874    for arg in spine_args {
5875        stuck = Node::List(vec![Node::Leaf("apply".into()), stuck, arg]);
5876    }
5877    stuck
5878}
5879
5880/// True for an `(apply head arg)` whose head is a free symbol the env
5881/// cannot reduce further — i.e. an applied constructor or other neutral.
5882/// The printed normal form drops the explicit `apply` keyword for these
5883/// neutrals so `(apply succ zero)` shows as `(succ zero)`, matching the
5884/// surface example in issue #50.
5885fn is_neutral_apply(node: &Node, env: &Env) -> bool {
5886    if let Node::List(children) = node {
5887        if children.len() == 3 {
5888            if let Node::Leaf(head) = &children[0] {
5889                if head == "apply" {
5890                    if let Node::Leaf(name) = &children[1] {
5891                        if env.lambdas.contains_key(name) {
5892                            return false;
5893                        }
5894                        return is_variable_token(name);
5895                    }
5896                }
5897            }
5898        }
5899    }
5900    false
5901}
5902
5903/// Drop the explicit `apply` keyword on neutral applications, recursively.
5904/// `(apply f a)` whose head is a free constructor-like symbol becomes
5905/// `(f a)` so the printed normal form matches the LiNo surface example
5906/// from issue #50: `(succ (succ zero))` rather than the explicit
5907/// `(apply succ (apply succ zero))`.
5908pub fn flatten_neutral_applies(node: &Node, env: &Env) -> Node {
5909    if let Node::List(children) = node {
5910        if children.is_empty() {
5911            return node.clone();
5912        }
5913        if let Some(binder) = binder_info(node) {
5914            let mut out = children.clone();
5915            out[binder.body_index] =
5916                flatten_neutral_applies(&children[binder.body_index], env);
5917            return Node::List(out);
5918        }
5919        let flattened: Vec<Node> = children
5920            .iter()
5921            .map(|child| flatten_neutral_applies(child, env))
5922            .collect();
5923        let candidate = Node::List(flattened.clone());
5924        if is_neutral_apply(&candidate, env) {
5925            return Node::List(vec![flattened[1].clone(), flattened[2].clone()]);
5926        }
5927        return candidate;
5928    }
5929    node.clone()
5930}
5931
5932/// Public weak-head normal form API (issue #50, D4).
5933/// Reduces only the spine of `term` — leaves binders and arguments untouched.
5934pub fn whnf(term: &Node, env: &mut Env) -> Node {
5935    whnf_with_options(term, env, ConvertOptions::default())
5936}
5937
5938/// Variant of [`whnf`] that takes an explicit [`ConvertOptions`].
5939pub fn whnf_with_options(term: &Node, env: &mut Env, options: ConvertOptions) -> Node {
5940    whnf_term(term, env, options)
5941}
5942
5943/// Public full normal form API (issue #50, D4).
5944/// Reduces every redex in `term`, including those nested under binders and
5945/// in argument positions, until the term is in beta-(eta-)normal form. The
5946/// result is post-processed by [`flatten_neutral_applies`] so it prints in
5947/// the surface shape `(succ (succ zero))` from the issue.
5948pub fn nf(term: &Node, env: &mut Env) -> Node {
5949    nf_with_options(term, env, ConvertOptions::default())
5950}
5951
5952/// Variant of [`nf`] that takes an explicit [`ConvertOptions`].
5953pub fn nf_with_options(term: &Node, env: &mut Env, options: ConvertOptions) -> Node {
5954    let normalized = normalize_term(term, env, options);
5955    flatten_neutral_applies(&normalized, env)
5956}
5957
5958fn eta_contract(term: &Node, env: &mut Env, options: ConvertOptions) -> Node {
5959    if !options.eta {
5960        return term.clone();
5961    }
5962    let children = match term {
5963        Node::List(children) if children.len() == 3 => children,
5964        _ => return term.clone(),
5965    };
5966    if !matches!(&children[0], Node::Leaf(head) if head == "lambda") {
5967        return term.clone();
5968    }
5969    let bindings = parse_bindings(&children[1]).unwrap_or_default();
5970    if bindings.len() != 1 {
5971        return term.clone();
5972    }
5973    let param = &bindings[0].0;
5974    let body = &children[2];
5975    let fn_node = match body {
5976        Node::List(body_children) if body_children.len() == 3 => {
5977            if matches!(&body_children[0], Node::Leaf(head) if head == "apply")
5978                && is_structurally_same(&body_children[2], &Node::Leaf(param.clone()))
5979            {
5980                Some(body_children[1].clone())
5981            } else {
5982                None
5983            }
5984        }
5985        Node::List(body_children) if body_children.len() == 2 => {
5986            if is_structurally_same(&body_children[1], &Node::Leaf(param.clone())) {
5987                Some(body_children[0].clone())
5988            } else {
5989                None
5990            }
5991        }
5992        _ => None,
5993    };
5994    if let Some(fn_node) = fn_node {
5995        if !free_variables(&fn_node).contains(param) {
5996            return normalize_term(&fn_node, env, options);
5997        }
5998    }
5999    term.clone()
6000}
6001
6002fn lookup_assigned_infix(env: &mut Env, op: &str, left: &Node, right: &Node) -> Option<f64> {
6003    let candidates = [
6004        Node::List(vec![
6005            Node::Leaf(op.to_string()),
6006            left.clone(),
6007            right.clone(),
6008        ]),
6009        Node::List(vec![
6010            left.clone(),
6011            Node::Leaf(op.to_string()),
6012            right.clone(),
6013        ]),
6014    ];
6015    for candidate in candidates {
6016        let key = key_of(&candidate);
6017        if let Some(&value) = env.assign.get(&key) {
6018            env.trace("lookup", format!("{} → {}", key, format_trace_value(value)));
6019            return Some(value);
6020        }
6021    }
6022    None
6023}
6024
6025fn same_normalized_input(left: &Node, right: &Node, left_term: &Node, right_term: &Node) -> bool {
6026    is_structurally_same(left, left_term) && is_structurally_same(right, right_term)
6027}
6028
6029fn explicit_symbol_number(node: &Node, env: &Env) -> Option<f64> {
6030    if let Node::Leaf(name) = node {
6031        if let Some(value) = env.symbol_prob.get(name) {
6032            return Some(*value);
6033        }
6034        let resolved = env.resolve_qualified(name);
6035        if resolved != *name {
6036            return env.symbol_prob.get(&resolved).copied();
6037        }
6038    }
6039    None
6040}
6041
6042fn try_eval_numeric(node: &Node, env: &mut Env, options: ConvertOptions) -> Option<f64> {
6043    let term = normalize_term(node, env, options);
6044    match &term {
6045        Node::Leaf(s) if is_num(s) => s.parse::<f64>().ok(),
6046        Node::Leaf(_) => explicit_symbol_number(&term, env),
6047        Node::List(children) if children.is_empty() => None,
6048        Node::List(children) => {
6049            if children.len() == 3 {
6050                if let Node::Leaf(op) = &children[1] {
6051                    if matches!(op.as_str(), "+" | "-" | "*" | "/") {
6052                        let left = try_eval_numeric(&children[0], env, options)?;
6053                        let right = try_eval_numeric(&children[2], env, options)?;
6054                        return Some(env.apply_op(op, &[left, right]));
6055                    }
6056                    if matches!(op.as_str(), "and" | "or" | "both" | "neither") {
6057                        let left = try_eval_numeric(&children[0], env, options)?;
6058                        let right = try_eval_numeric(&children[2], env, options)?;
6059                        let value = env.apply_op(op, &[left, right]);
6060                        return Some(env.clamp(value));
6061                    }
6062                }
6063            }
6064            if let Node::Leaf(head) = &children[0] {
6065                if head != "=" && head != "!=" && env.has_op(head) {
6066                    let mut values = Vec::new();
6067                    for arg in &children[1..] {
6068                        values.push(try_eval_numeric(arg, env, options)?);
6069                    }
6070                    let value = env.apply_op(head, &values);
6071                    return Some(env.clamp(value));
6072                }
6073            }
6074            None
6075        }
6076    }
6077}
6078
6079fn equality_truth_value(
6080    left: &Node,
6081    right: &Node,
6082    left_term: &Node,
6083    right_term: &Node,
6084    env: &mut Env,
6085    options: ConvertOptions,
6086) -> f64 {
6087    if let Some(value) = lookup_assigned_infix(env, "=", left, right) {
6088        return env.clamp(value);
6089    }
6090    if !same_normalized_input(left, right, left_term, right_term) {
6091        if let Some(value) = lookup_assigned_infix(env, "=", left_term, right_term) {
6092            return env.clamp(value);
6093        }
6094    }
6095    if is_structurally_same(left_term, right_term) {
6096        return env.hi;
6097    }
6098    let left_num = try_eval_numeric(left_term, env, options);
6099    let right_num = try_eval_numeric(right_term, env, options);
6100    if let (Some(left_num), Some(right_num)) = (left_num, right_num) {
6101        if dec_round(left_num) == dec_round(right_num) {
6102            env.hi
6103        } else {
6104            env.lo
6105        }
6106    } else {
6107        env.lo
6108    }
6109}
6110
6111fn eval_equality_node(left: &Node, op: &str, right: &Node, env: &mut Env) -> EvalResult {
6112    let options = ConvertOptions::default();
6113    if let Some(value) = lookup_assigned_infix(env, op, left, right) {
6114        return EvalResult::Value(env.clamp(value));
6115    }
6116    let left_term = normalize_term(left, env, options);
6117    let right_term = normalize_term(right, env, options);
6118    if !same_normalized_input(left, right, &left_term, &right_term) {
6119        if let Some(value) = lookup_assigned_infix(env, op, &left_term, &right_term) {
6120            return EvalResult::Value(env.clamp(value));
6121        }
6122    }
6123    if op == "=" {
6124        let value = equality_truth_value(left, right, &left_term, &right_term, env, options);
6125        EvalResult::Value(env.clamp(value))
6126    } else {
6127        let eq = equality_truth_value(left, right, &left_term, &right_term, env, options);
6128        let value = env.apply_op("not", &[eq]);
6129        EvalResult::Value(env.clamp(value))
6130    }
6131}
6132
6133/// Decide whether two terms are definitionally equal under the current
6134/// environment using beta-normalization and explicit equality assignments.
6135pub fn is_convertible(left: &Node, right: &Node, env: &mut Env) -> bool {
6136    is_convertible_with_options(left, right, env, ConvertOptions::default())
6137}
6138
6139/// Variant of [`is_convertible`] with opt-in conversion features.
6140pub fn is_convertible_with_options(
6141    left: &Node,
6142    right: &Node,
6143    env: &mut Env,
6144    options: ConvertOptions,
6145) -> bool {
6146    if let Some(value) = lookup_assigned_infix(env, "=", left, right) {
6147        return env.clamp(value) == env.hi;
6148    }
6149    let left_term = normalize_term(left, env, options);
6150    let right_term = normalize_term(right, env, options);
6151    if !same_normalized_input(left, right, &left_term, &right_term) {
6152        if let Some(value) = lookup_assigned_infix(env, "=", &left_term, &right_term) {
6153            return env.clamp(value) == env.hi;
6154        }
6155    }
6156    is_structurally_same(&left_term, &right_term)
6157}
6158
6159fn eval_reduced_term(reduced: &Node, env: &mut Env) -> EvalResult {
6160    let term = normalize_term(reduced, env, ConvertOptions::default());
6161    if has_unresolved_free_variables(&term, env) {
6162        EvalResult::Term(term)
6163    } else {
6164        eval_node(&term, env)
6165    }
6166}
6167
6168fn context_has_name(env: &Env, name: &str) -> bool {
6169    if env.terms.contains(name)
6170        || env.types.contains_key(name)
6171        || env.lambdas.contains_key(name)
6172        || env.symbol_prob.contains_key(name)
6173        || env.ops.contains_key(name)
6174        || env.templates.contains_key(name)
6175    {
6176        return true;
6177    }
6178    let resolved = env.resolve_qualified(name);
6179    resolved != name
6180        && (env.terms.contains(&resolved)
6181            || env.types.contains_key(&resolved)
6182            || env.lambdas.contains_key(&resolved)
6183            || env.symbol_prob.contains_key(&resolved)
6184            || env.ops.contains_key(&resolved)
6185            || env.templates.contains_key(&resolved))
6186}
6187
6188fn eval_fresh(var_name: &str, body: &Node, env: &mut Env) -> EvalResult {
6189    if context_has_name(env, var_name) {
6190        panic!(
6191            "Freshness error: fresh variable \"{}\" already appears in context",
6192            var_name
6193        );
6194    }
6195    let had_term = env.terms.contains(var_name);
6196    let previous_type = env.types.get(var_name).cloned();
6197    let previous_lambda = env.lambdas.get(var_name).cloned();
6198    let previous_symbol = env.symbol_prob.get(var_name).copied();
6199    env.terms.insert(var_name.to_string());
6200    let result = catch_unwind(AssertUnwindSafe(|| eval_node(body, env)));
6201    if !had_term {
6202        env.terms.remove(var_name);
6203    }
6204    if let Some(value) = previous_type {
6205        env.types.insert(var_name.to_string(), value);
6206    } else {
6207        env.types.remove(var_name);
6208    }
6209    if let Some(value) = previous_lambda {
6210        env.lambdas.insert(var_name.to_string(), value);
6211    } else {
6212        env.lambdas.remove(var_name);
6213    }
6214    if let Some(value) = previous_symbol {
6215        env.symbol_prob.insert(var_name.to_string(), value);
6216    } else {
6217        env.symbol_prob.remove(var_name);
6218    }
6219    match result {
6220        Ok(value) => value,
6221        Err(payload) => std::panic::resume_unwind(payload),
6222    }
6223}
6224
6225// ========== Bidirectional Type Checker (issue #42) ==========
6226// Public API:
6227//     synth(term, env)                 -> SynthResult { typ, diagnostics }
6228//     check(term, expected_type, env)  -> CheckResult { ok, diagnostics }
6229//
6230// Mirrors the JavaScript `synth` / `check` helpers in `js/src/rml-links.mjs`.
6231//
6232// Synthesise mode walks the term and applies kernel rules for `(Type N)`,
6233// `(Pi ...)`, `(lambda ...)`, `(apply ...)`, `(subst ...)`, `(type of ...)`,
6234// and `(expr of T)`. Otherwise it falls back to the type recorded by
6235// `eval_node` in `env.types`.
6236//
6237// Check mode prefers a direct lambda-vs-Pi rule that opens the binder and
6238// recurses on the body; otherwise it switches modes by synthesising and
6239// comparing with definitional convertibility (`is_convertible`). Numeric
6240// literals accept any annotation — the kernel does not record number sorts
6241// directly, and equality with the expected type collapses through
6242// definitional convertibility downstream.
6243//
6244// Diagnostics use stable codes E020..E024 (see `docs/DIAGNOSTICS.md`).
6245
6246/// Result of a `synth` call: the synthesised type as an AST node (or `None`
6247/// when synthesis fails) plus any diagnostics emitted along the way.
6248#[derive(Debug, Clone, Default)]
6249pub struct SynthResult {
6250    pub typ: Option<Node>,
6251    pub diagnostics: Vec<Diagnostic>,
6252}
6253
6254/// Result of a `check` call: a boolean indicating whether the term checks
6255/// against the expected type, plus any diagnostics emitted along the way.
6256#[derive(Debug, Clone, Default)]
6257pub struct CheckResult {
6258    pub ok: bool,
6259    pub diagnostics: Vec<Diagnostic>,
6260}
6261
6262fn synth_span(env: &Env) -> Span {
6263    env.current_span.clone().unwrap_or_else(|| env.default_span.clone())
6264}
6265
6266fn type_key_to_node(type_key: &str) -> Node {
6267    let trimmed = type_key.trim();
6268    if trimmed.starts_with('(') {
6269        let toks = tokenize_one(trimmed);
6270        if let Ok(parsed) = parse_one(&toks) {
6271            return parsed;
6272        }
6273    }
6274    Node::Leaf(type_key.to_string())
6275}
6276
6277fn parse_term_input_str(s: &str) -> Node {
6278    let trimmed = s.trim();
6279    if trimmed.starts_with('(') {
6280        let toks = tokenize_one(trimmed);
6281        if let Ok(parsed) = parse_one(&toks) {
6282            return desugar_hoas(parsed);
6283        }
6284    }
6285    Node::Leaf(s.to_string())
6286}
6287
6288struct TypeBindingSnapshot {
6289    name: String,
6290    had_term: bool,
6291    previous_type: Option<String>,
6292}
6293
6294fn snapshot_type_binding(env: &Env, name: &str) -> TypeBindingSnapshot {
6295    TypeBindingSnapshot {
6296        name: name.to_string(),
6297        had_term: env.terms.contains(name),
6298        previous_type: env.types.get(name).cloned(),
6299    }
6300}
6301
6302fn extend_type_binding(env: &mut Env, name: &str, type_key: &str) {
6303    env.terms.insert(name.to_string());
6304    env.types.insert(name.to_string(), type_key.to_string());
6305}
6306
6307fn restore_type_binding(env: &mut Env, snap: TypeBindingSnapshot) {
6308    if !snap.had_term {
6309        env.terms.remove(&snap.name);
6310    }
6311    if let Some(value) = snap.previous_type {
6312        env.types.insert(snap.name, value);
6313    } else {
6314        env.types.remove(&snap.name);
6315    }
6316}
6317
6318// Prenex polymorphism (D9): `(forall A T)` is sugar for `(Pi (Type A) T)`.
6319// `A` is a bound type variable ranging over the universe `Type`. Expansion
6320// happens at the outermost layer only — nested quantifiers desugar lazily as
6321// the type checker recurses into the body.
6322fn is_forall_node(node: &Node) -> bool {
6323    if let Node::List(children) = node {
6324        if children.len() == 3 {
6325            if let (Node::Leaf(head), Node::Leaf(_)) = (&children[0], &children[1]) {
6326                return head == "forall";
6327            }
6328        }
6329    }
6330    false
6331}
6332
6333fn expand_forall(node: &Node) -> Node {
6334    if !is_forall_node(node) {
6335        return node.clone();
6336    }
6337    if let Node::List(children) = node {
6338        let var_name = match &children[1] {
6339            Node::Leaf(s) => s.clone(),
6340            _ => return node.clone(),
6341        };
6342        return Node::List(vec![
6343            Node::Leaf("Pi".to_string()),
6344            Node::List(vec![
6345                Node::Leaf("Type".to_string()),
6346                Node::Leaf(var_name),
6347            ]),
6348            children[2].clone(),
6349        ]);
6350    }
6351    node.clone()
6352}
6353
6354fn types_agree(a: &Node, b: &Node, env: &mut Env) -> bool {
6355    let a_n = expand_forall(a);
6356    let b_n = expand_forall(b);
6357    if is_structurally_same(&a_n, &b_n) {
6358        return true;
6359    }
6360    let result = catch_unwind(AssertUnwindSafe(|| is_convertible(&a_n, &b_n, env)));
6361    matches!(result, Ok(true))
6362}
6363
6364fn synth_leaf(name: &str, env: &mut Env) -> Option<Node> {
6365    if is_num(name) {
6366        return None;
6367    }
6368    let leaf = Node::Leaf(name.to_string());
6369    if let Some(recorded) = infer_type_key(&leaf, env) {
6370        return Some(type_key_to_node(&recorded));
6371    }
6372    let resolved = env.resolve_qualified(name);
6373    if resolved != name {
6374        if let Some(recorded) = env.types.get(&resolved).cloned() {
6375            return Some(type_key_to_node(&recorded));
6376        }
6377    }
6378    None
6379}
6380
6381fn synth_apply(children: &[Node], env: &mut Env, span: &Span, diagnostics: &mut Vec<Diagnostic>) -> Option<Node> {
6382    let head = &children[1];
6383    let arg = &children[2];
6384    let inner = synth(head, env);
6385    diagnostics.extend(inner.diagnostics);
6386    let fn_type = match inner.typ {
6387        Some(t) => t,
6388        None => {
6389            diagnostics.push(Diagnostic::new(
6390                "E020",
6391                format!(
6392                    "Cannot synthesize type of `{}` in `{}`",
6393                    key_of(head),
6394                    key_of(&Node::List(children.to_vec()))
6395                ),
6396                span.clone(),
6397            ));
6398            return None;
6399        }
6400    };
6401    // Prenex polymorphism (D9): `(forall A T)` desugars to `(Pi (Type A) T)`,
6402    // so type-application `(apply f Natural)` reduces by substituting `A := Natural`
6403    // in the body just like a regular Pi-type does.
6404    let fn_type = expand_forall(&fn_type);
6405    let pi_children = match &fn_type {
6406        Node::List(c) if c.len() == 3 && matches!(&c[0], Node::Leaf(s) if s == "Pi") => c.clone(),
6407        _ => {
6408            diagnostics.push(Diagnostic::new(
6409                "E022",
6410                format!(
6411                    "Application head `{}` has type `{}`, expected a Pi-type",
6412                    key_of(head),
6413                    key_of(&fn_type)
6414                ),
6415                span.clone(),
6416            ));
6417            return None;
6418        }
6419    };
6420    let (param_name, param_type_key) = match parse_binding(&pi_children[1]) {
6421        Some(b) => b,
6422        None => {
6423            diagnostics.push(Diagnostic::new(
6424                "E022",
6425                format!(
6426                    "Application head has malformed Pi binder `{}`",
6427                    key_of(&pi_children[1])
6428                ),
6429                span.clone(),
6430            ));
6431            return None;
6432        }
6433    };
6434    let domain_node = type_key_to_node(&param_type_key);
6435    let arg_check = check(arg, &domain_node, env);
6436    diagnostics.extend(arg_check.diagnostics);
6437    if !arg_check.ok {
6438        return None;
6439    }
6440    Some(subst(&pi_children[2], &param_name, arg))
6441}
6442
6443fn synth_lambda(children: &[Node], env: &mut Env, span: &Span, diagnostics: &mut Vec<Diagnostic>) -> Option<Node> {
6444    let (param_name, param_type_key) = match parse_binding(&children[1]) {
6445        Some(b) => b,
6446        None => {
6447            diagnostics.push(Diagnostic::new(
6448                "E024",
6449                format!("Lambda has malformed binder `{}`", key_of(&children[1])),
6450                span.clone(),
6451            ));
6452            return None;
6453        }
6454    };
6455    let snap = snapshot_type_binding(env, &param_name);
6456    extend_type_binding(env, &param_name, &param_type_key);
6457    let body_synth = synth(&children[2], env);
6458    restore_type_binding(env, snap);
6459    diagnostics.extend(body_synth.diagnostics);
6460    let body_type = body_synth.typ?;
6461    Some(Node::List(vec![
6462        Node::Leaf("Pi".to_string()),
6463        Node::List(vec![
6464            Node::Leaf(param_type_key),
6465            Node::Leaf(param_name),
6466        ]),
6467        body_type,
6468    ]))
6469}
6470
6471fn synth_of_membership(children: &[Node], env: &mut Env, _span: &Span, diagnostics: &mut Vec<Diagnostic>) -> Option<Node> {
6472    let result = check(&children[0], &children[2], env);
6473    diagnostics.extend(result.diagnostics);
6474    if !result.ok {
6475        return None;
6476    }
6477    Some(Node::List(vec![
6478        Node::Leaf("Type".to_string()),
6479        Node::Leaf("0".to_string()),
6480    ]))
6481}
6482
6483/// Synthesise the type of `term` under `env`.
6484///
6485/// On success, `SynthResult.typ` carries the inferred type as a `Node` AST.
6486/// On failure, `typ` is `None` and `diagnostics` carries one or more
6487/// `E020..E024` diagnostics describing the obstruction.
6488pub fn synth(term: &Node, env: &mut Env) -> SynthResult {
6489    let span = synth_span(env);
6490    let mut diagnostics: Vec<Diagnostic> = Vec::new();
6491
6492    match term {
6493        Node::Leaf(name) => {
6494            if let Some(t) = synth_leaf(name, env) {
6495                return SynthResult { typ: Some(t), diagnostics };
6496            }
6497            if !is_num(name) {
6498                diagnostics.push(Diagnostic::new(
6499                    "E020",
6500                    format!("Cannot synthesize type of symbol `{}`", name),
6501                    span,
6502                ));
6503            }
6504            SynthResult { typ: None, diagnostics }
6505        }
6506        Node::List(children) => {
6507            // (Type N) : (Type N+1)
6508            if children.len() == 2 {
6509                if let Node::Leaf(head) = &children[0] {
6510                    if head == "Type" {
6511                        if let Some(univ) = universe_type_key(term) {
6512                            return SynthResult {
6513                                typ: Some(type_key_to_node(&univ)),
6514                                diagnostics,
6515                            };
6516                        }
6517                        diagnostics.push(Diagnostic::new(
6518                            "E020",
6519                            format!(
6520                                "Universe `{}` has invalid level token `{}`",
6521                                key_of(term),
6522                                key_of(&children[1])
6523                            ),
6524                            span,
6525                        ));
6526                        return SynthResult { typ: None, diagnostics };
6527                    }
6528                }
6529            }
6530
6531            // (Prop) : (Type 1)
6532            if children.len() == 1 {
6533                if let Node::Leaf(head) = &children[0] {
6534                    if head == "Prop" {
6535                        return SynthResult {
6536                            typ: Some(Node::List(vec![
6537                                Node::Leaf("Type".to_string()),
6538                                Node::Leaf("1".to_string()),
6539                            ])),
6540                            diagnostics,
6541                        };
6542                    }
6543                }
6544            }
6545
6546            if children.len() == 3 {
6547                if let Node::Leaf(head) = &children[0] {
6548                    match head.as_str() {
6549                        "forall" => {
6550                            // (forall A T) : (Type 0) — prenex polymorphism (D9). `A` is bound
6551                            // as a type variable ranging over `Type`; the body `T` is the
6552                            // polymorphic type. Synthesise by recursing on the desugared form.
6553                            let expanded = expand_forall(term);
6554                            let inner = synth(&expanded, env);
6555                            diagnostics.extend(inner.diagnostics);
6556                            return SynthResult { typ: inner.typ, diagnostics };
6557                        }
6558                        "Pi" => {
6559                            if parse_binding(&children[1]).is_none() {
6560                                diagnostics.push(Diagnostic::new(
6561                                    "E024",
6562                                    format!("Pi has malformed binder `{}`", key_of(&children[1])),
6563                                    span,
6564                                ));
6565                                return SynthResult { typ: None, diagnostics };
6566                            }
6567                            return SynthResult {
6568                                typ: Some(Node::List(vec![
6569                                    Node::Leaf("Type".to_string()),
6570                                    Node::Leaf("0".to_string()),
6571                                ])),
6572                                diagnostics,
6573                            };
6574                        }
6575                        "lambda" => {
6576                            let t = synth_lambda(children, env, &span, &mut diagnostics);
6577                            return SynthResult { typ: t, diagnostics };
6578                        }
6579                        "apply" => {
6580                            let t = synth_apply(children, env, &span, &mut diagnostics);
6581                            return SynthResult { typ: t, diagnostics };
6582                        }
6583                        "type" => {
6584                            if let Node::Leaf(of_kw) = &children[1] {
6585                                if of_kw == "of" {
6586                                    let inner = synth(&children[2], env);
6587                                    diagnostics.extend(inner.diagnostics);
6588                                    if inner.typ.is_some() {
6589                                        return SynthResult {
6590                                            typ: Some(Node::List(vec![
6591                                                Node::Leaf("Type".to_string()),
6592                                                Node::Leaf("0".to_string()),
6593                                            ])),
6594                                            diagnostics,
6595                                        };
6596                                    }
6597                                    diagnostics.push(Diagnostic::new(
6598                                        "E020",
6599                                        format!(
6600                                            "Cannot synthesize type referenced by `{}`",
6601                                            key_of(term)
6602                                        ),
6603                                        span,
6604                                    ));
6605                                    return SynthResult { typ: None, diagnostics };
6606                                }
6607                            }
6608                        }
6609                        _ => {}
6610                    }
6611                }
6612                // (expr of T)
6613                if let Node::Leaf(of_kw) = &children[1] {
6614                    if of_kw == "of" {
6615                        let t = synth_of_membership(children, env, &span, &mut diagnostics);
6616                        return SynthResult { typ: t, diagnostics };
6617                    }
6618                }
6619            }
6620
6621            // (subst term x replacement)
6622            if children.len() == 4 {
6623                if let (Node::Leaf(head), Node::Leaf(name)) = (&children[0], &children[2]) {
6624                    if head == "subst" {
6625                        let reduced = subst(&children[1], name, &children[3]);
6626                        let inner = synth(&reduced, env);
6627                        diagnostics.extend(inner.diagnostics);
6628                        return SynthResult { typ: inner.typ, diagnostics };
6629                    }
6630                }
6631            }
6632
6633            // Fallback: types recorded by eval_node.
6634            if let Some(recorded) = infer_type_key(term, env) {
6635                return SynthResult {
6636                    typ: Some(type_key_to_node(&recorded)),
6637                    diagnostics,
6638                };
6639            }
6640
6641            diagnostics.push(Diagnostic::new(
6642                "E020",
6643                format!("Cannot synthesize type of `{}`", key_of(term)),
6644                span,
6645            ));
6646            SynthResult { typ: None, diagnostics }
6647        }
6648    }
6649}
6650
6651/// Check `term` against `expected_type` under `env`.
6652///
6653/// Returns `CheckResult { ok: true, diagnostics: [] }` on success.
6654/// On failure, `ok` is `false` and `diagnostics` carries one or more
6655/// `E020..E024` diagnostics describing the obstruction.
6656pub fn check(term: &Node, expected_type: &Node, env: &mut Env) -> CheckResult {
6657    let span = synth_span(env);
6658    let mut diagnostics: Vec<Diagnostic> = Vec::new();
6659
6660    // Prenex polymorphism (D9): `(forall A T)` is sugar for `(Pi (Type A) T)`.
6661    // Expand once here so the lambda-vs-Pi rule below applies uniformly.
6662    let expanded;
6663    let expected_type = if is_forall_node(expected_type) {
6664        expanded = expand_forall(expected_type);
6665        &expanded
6666    } else {
6667        expected_type
6668    };
6669
6670    // Direct rule: (lambda (A x) body) checked against (Pi (A' y) B).
6671    if let (Node::List(lc), Node::List(ec)) = (term, expected_type) {
6672        if lc.len() == 3 && ec.len() == 3 {
6673            let lambda_head = matches!(&lc[0], Node::Leaf(s) if s == "lambda");
6674            let pi_head = matches!(&ec[0], Node::Leaf(s) if s == "Pi");
6675            if lambda_head && pi_head {
6676                let lambda_binding = parse_binding(&lc[1]);
6677                let pi_binding = parse_binding(&ec[1]);
6678                if let (Some((lname, ltype)), Some((pname, ptype))) = (lambda_binding, pi_binding) {
6679                    let lparam_node = parse_term_input_str(&ltype);
6680                    let pparam_node = parse_term_input_str(&ptype);
6681                    if !types_agree(&lparam_node, &pparam_node, env) {
6682                        diagnostics.push(Diagnostic::new(
6683                            "E021",
6684                            format!(
6685                                "Lambda parameter type `{}` does not match Pi domain `{}`",
6686                                ltype, ptype
6687                            ),
6688                            span,
6689                        ));
6690                        return CheckResult { ok: false, diagnostics };
6691                    }
6692                    let codomain = subst(&ec[2], &pname, &Node::Leaf(lname.clone()));
6693                    let snap = snapshot_type_binding(env, &lname);
6694                    extend_type_binding(env, &lname, &ltype);
6695                    let body_result = check(&lc[2], &codomain, env);
6696                    restore_type_binding(env, snap);
6697                    diagnostics.extend(body_result.diagnostics);
6698                    return CheckResult {
6699                        ok: body_result.ok,
6700                        diagnostics,
6701                    };
6702                }
6703            }
6704        }
6705    }
6706
6707    // Lambda checked against non-Pi expected type.
6708    if let Node::List(lc) = term {
6709        if lc.len() == 3 && matches!(&lc[0], Node::Leaf(s) if s == "lambda") {
6710            let expected_is_pi = matches!(
6711                expected_type,
6712                Node::List(ec) if ec.len() == 3 && matches!(&ec[0], Node::Leaf(s) if s == "Pi")
6713            );
6714            if !expected_is_pi {
6715                diagnostics.push(Diagnostic::new(
6716                    "E023",
6717                    format!(
6718                        "Lambda `{}` cannot check against non-Pi type `{}`",
6719                        key_of(term),
6720                        key_of(expected_type)
6721                    ),
6722                    span,
6723                ));
6724                return CheckResult { ok: false, diagnostics };
6725            }
6726        }
6727    }
6728
6729    // Numeric literal: accept any non-empty annotation.
6730    if let Node::Leaf(name) = term {
6731        if is_num(name) {
6732            return CheckResult { ok: true, diagnostics };
6733        }
6734    }
6735
6736    // Default mode-switch: synthesise and compare with definitional equality.
6737    let synth_result = synth(term, env);
6738    diagnostics.extend(synth_result.diagnostics);
6739    let actual = match synth_result.typ {
6740        Some(t) => t,
6741        None => return CheckResult { ok: false, diagnostics },
6742    };
6743    let ok = types_agree(&actual, expected_type, env);
6744    if !ok {
6745        diagnostics.push(Diagnostic::new(
6746            "E021",
6747            format!(
6748                "Type mismatch: `{}` has type `{}`, expected `{}`",
6749                key_of(term),
6750                key_of(&actual),
6751                key_of(expected_type)
6752            ),
6753            span,
6754        ));
6755    }
6756    CheckResult { ok, diagnostics }
6757}
6758
6759// ========== Proof derivations (issue #35) ==========
6760// A derivation is a Node tree of the form `(by <rule> <subderivation>...)`.
6761// Building it on the same `Node` type as the AST means the existing
6762// `key_of` (print) and `parse_one(tokenize_one(...))` (parse) helpers give
6763// the round-trip property `parse(print(proof)) == proof` for free, without
6764// needing a separate proof format. Mirrors `buildProof` in
6765// `js/src/rml-links.mjs` so cross-runtime proofs match exactly.
6766//
6767// The walker is intentionally read-only — it never mutates the env beyond
6768// the lookups that `eval_node` would have performed during evaluation, so
6769// enabling proofs cannot change query results. Sub-derivations recurse
6770// through `build_proof` so every sub-expression carries its own witness
6771// rather than collapsing into the literal value.
6772fn wrap_proof(rule: &str, subs: Vec<Node>) -> Node {
6773    let mut out = Vec::with_capacity(subs.len() + 2);
6774    out.push(Node::Leaf("by".to_string()));
6775    out.push(Node::Leaf(rule.to_string()));
6776    out.extend(subs);
6777    Node::List(out)
6778}
6779
6780fn leaf(s: &str) -> Node {
6781    Node::Leaf(s.to_string())
6782}
6783
6784/// Strip an optional trailing `with proof` from a query body. Both
6785/// `(? expr with proof)` and `(? (expr) with proof)` are accepted. Mirrors
6786/// `_stripWithProof` in the JavaScript implementation.
6787fn strip_with_proof(parts: &[Node]) -> &[Node] {
6788    if parts.len() >= 3 {
6789        if let (Node::Leaf(w), Node::Leaf(p)) =
6790            (&parts[parts.len() - 2], &parts[parts.len() - 1])
6791        {
6792            if w == "with" && p == "proof" {
6793                return &parts[..parts.len() - 2];
6794            }
6795        }
6796    }
6797    parts
6798}
6799
6800/// Detect whether a top-level `(? ...)` form explicitly requested a proof
6801/// via the inline `with proof` keyword pair. Used to populate the per-query
6802/// proof slot even when the global `with_proofs` option is off. Mirrors
6803/// `_queryRequestsProof` in the JavaScript implementation.
6804fn query_requests_proof(node: &Node) -> bool {
6805    if let Node::List(children) = node {
6806        if let Some(Node::Leaf(head)) = children.first() {
6807            if head == "?" {
6808                let parts = &children[1..];
6809                if parts.len() >= 3 {
6810                    if let (Node::Leaf(w), Node::Leaf(p)) =
6811                        (&parts[parts.len() - 2], &parts[parts.len() - 1])
6812                    {
6813                        return w == "with" && p == "proof";
6814                    }
6815                }
6816            }
6817        }
6818    }
6819    false
6820}
6821
6822/// Read-only beta-normalization used by equality-layer classification.
6823/// Unlike `normalize_term`, this helper only handles the on-the-fly
6824/// `(apply (lambda (T x) body) arg)` redex shape and recurses into other
6825/// nodes structurally. That keeps it free of `&mut Env` so it can run from
6826/// the immutable `build_proof` walker without cloning the environment.
6827fn pure_beta_normalize(node: &Node) -> Node {
6828    if let Node::List(children) = node {
6829        if children.len() == 3 {
6830            if let Node::Leaf(head) = &children[0] {
6831                if head == "apply" {
6832                    let fn_n = pure_beta_normalize(&children[1]);
6833                    let arg = pure_beta_normalize(&children[2]);
6834                    if let Node::List(fn_children) = &fn_n {
6835                        if fn_children.len() == 3 {
6836                            if let Node::Leaf(fn_head) = &fn_children[0] {
6837                                if fn_head == "lambda" {
6838                                    if let Some((param, _)) =
6839                                        parse_binding(&fn_children[1])
6840                                    {
6841                                        let reduced =
6842                                            subst(&fn_children[2], &param, &arg);
6843                                        return pure_beta_normalize(&reduced);
6844                                    }
6845                                }
6846                            }
6847                        }
6848                    }
6849                    return Node::List(vec![Node::Leaf("apply".into()), fn_n, arg]);
6850                }
6851            }
6852        }
6853        let normalized: Vec<Node> = children.iter().map(pure_beta_normalize).collect();
6854        return Node::List(normalized);
6855    }
6856    node.clone()
6857}
6858
6859fn contains_lambda_or_apply(node: &Node) -> bool {
6860    if let Node::List(children) = node {
6861        if let Some(Node::Leaf(head)) = children.first() {
6862            if head == "lambda" || head == "apply" {
6863                return true;
6864            }
6865        }
6866        return children.iter().any(contains_lambda_or_apply);
6867    }
6868    false
6869}
6870
6871/// Equality-layer classification used by both `build_proof` and the
6872/// per-query provenance walker. Precedence (issue #97): assigned >
6873/// structural > definitional > numeric. Returns the rule string verbatim
6874/// so JS and Rust emit identical labels.
6875pub fn classify_equality_rule(l: &Node, r: &Node, op: &str, env: &Env) -> &'static str {
6876    let is_inequality = op == "!=";
6877    let k_prefix = key_of(&Node::List(vec![leaf("="), l.clone(), r.clone()]));
6878    let k_infix = key_of(&Node::List(vec![l.clone(), leaf("="), r.clone()]));
6879    if env.assign.contains_key(&k_prefix) || env.assign.contains_key(&k_infix) {
6880        return if is_inequality {
6881            "assigned-inequality"
6882        } else {
6883            "assigned-equality"
6884        };
6885    }
6886    if is_structurally_same(l, r) {
6887        return if is_inequality {
6888            "structural-inequality"
6889        } else {
6890            "structural-equality"
6891        };
6892    }
6893    // Definitional equality: if one side contains a lambda/apply and both
6894    // sides beta-normalize to structurally-identical terms, the equality
6895    // holds by reduction rather than by raw arithmetic.
6896    if contains_lambda_or_apply(l) || contains_lambda_or_apply(r) {
6897        let ln = pure_beta_normalize(l);
6898        let rn = pure_beta_normalize(r);
6899        if is_structurally_same(&ln, &rn) && !is_structurally_same(l, r) {
6900            return if is_inequality {
6901                "definitional-inequality"
6902            } else {
6903                "definitional-equality"
6904            };
6905        }
6906    }
6907    if is_inequality {
6908        "numeric-inequality"
6909    } else {
6910        "numeric-equality"
6911    }
6912}
6913
6914/// Strip an optional `with proof` suffix and then unwrap a singleton
6915/// container so `(? (a = b))` and `(? ((a = b)))` both yield `(a = b)`.
6916fn query_body_for_provenance(form: &Node) -> Option<Node> {
6917    if let Node::List(children) = form {
6918        if let Some(Node::Leaf(head)) = children.first() {
6919            if head == "?" {
6920                let stripped = strip_with_proof(&children[1..]);
6921                let mut body: Node = if stripped.len() == 1 {
6922                    stripped[0].clone()
6923                } else {
6924                    Node::List(stripped.to_vec())
6925                };
6926                loop {
6927                    match body {
6928                        Node::List(ref inner) if inner.len() == 1 => {
6929                            if matches!(&inner[0], Node::List(_)) {
6930                                body = inner[0].clone();
6931                            } else {
6932                                break;
6933                            }
6934                        }
6935                        _ => break,
6936                    }
6937                }
6938                return Some(body);
6939            }
6940        }
6941    }
6942    None
6943}
6944
6945/// Return the equality-layer rule for a query whose body is a direct
6946/// equality, or `None` for any other query shape. Composite queries like
6947/// `((a = true) and (b = true))` are intentionally returned as `None`: the
6948/// per-equality rules still appear in the proof witness, but the surface
6949/// provenance describes the query itself.
6950pub fn equality_provenance_for_query(form: &Node, env: &Env) -> Option<String> {
6951    let body = query_body_for_provenance(form)?;
6952    if let Node::List(children) = &body {
6953        if children.len() == 3 {
6954            if let Node::Leaf(op) = &children[1] {
6955                if op == "=" || op == "!=" {
6956                    return Some(
6957                        classify_equality_rule(&children[0], &children[2], op, env)
6958                            .to_string(),
6959                    );
6960                }
6961            }
6962        }
6963    }
6964    None
6965}
6966
6967/// Build a derivation tree witnessing how `node` reduces under `env`.
6968/// Returns a `Node::List` of the form `(by <rule> <subderivation>...)`.
6969///
6970/// The walker mirrors the structural cases of `eval_node`: definitions and
6971/// configuration directives become leaf witnesses, infix and prefix
6972/// operators become rule applications whose subderivations recurse through
6973/// `build_proof`, and equality picks `assigned-equality` /
6974/// `structural-equality` / `definitional-equality` / `numeric-equality`
6975/// (and the negated counterparts) based on the same lookups `eval_node`
6976/// performs — delegating to `classify_equality_rule` so both the proof
6977/// witness and the per-query provenance agree on which layer fired.
6978pub fn build_proof(node: &Node, env: &Env) -> Node {
6979    match node {
6980        // Numeric and bare-symbol leaves are axiomatic at this level.
6981        Node::Leaf(s) => {
6982            if is_num(s) {
6983                wrap_proof("literal", vec![leaf(s)])
6984            } else {
6985                wrap_proof("symbol", vec![leaf(s)])
6986            }
6987        }
6988        Node::List(children) => {
6989            // Definitions and operator redefs: (head: ...)
6990            if let Some(Node::Leaf(s)) = children.first() {
6991                if s.ends_with(':') {
6992                    return wrap_proof("definition", vec![node.clone()]);
6993                }
6994            }
6995
6996            // Assignment: ((expr) has probability p)
6997            if children.len() == 4 {
6998                if let (Node::Leaf(w1), Node::Leaf(w2), Node::Leaf(w3)) =
6999                    (&children[1], &children[2], &children[3])
7000                {
7001                    if w1 == "has" && w2 == "probability" && is_num(w3) {
7002                        return wrap_proof(
7003                            "assigned-probability",
7004                            vec![children[0].clone(), leaf(w3)],
7005                        );
7006                    }
7007                }
7008            }
7009
7010            // Range / valence configuration directives.
7011            if children.len() == 3 {
7012                if let (Node::Leaf(h), Node::Leaf(lo_s), Node::Leaf(hi_s)) =
7013                    (&children[0], &children[1], &children[2])
7014                {
7015                    if h == "range" && is_num(lo_s) && is_num(hi_s) {
7016                        return wrap_proof(
7017                            "configuration",
7018                            vec![leaf("range"), leaf(lo_s), leaf(hi_s)],
7019                        );
7020                    }
7021                }
7022            }
7023            if children.len() == 2 {
7024                if let (Node::Leaf(h), Node::Leaf(v)) = (&children[0], &children[1]) {
7025                    if h == "valence" && is_num(v) {
7026                        return wrap_proof(
7027                            "configuration",
7028                            vec![leaf("valence"), leaf(v)],
7029                        );
7030                    }
7031                }
7032            }
7033
7034            // Query: (? expr) and the per-query proof form (? expr with proof)
7035            if let Some(Node::Leaf(head)) = children.first() {
7036                if head == "?" {
7037                    let parts = &children[1..];
7038                    let inner = strip_with_proof(parts);
7039                    let target = if inner.len() == 1 {
7040                        inner[0].clone()
7041                    } else {
7042                        Node::List(inner.to_vec())
7043                    };
7044                    return wrap_proof("query", vec![build_proof(&target, env)]);
7045                }
7046            }
7047
7048            // Infix arithmetic: (A + B), (A - B), (A * B), (A / B)
7049            if children.len() == 3 {
7050                if let Node::Leaf(op_name) = &children[1] {
7051                    if matches!(op_name.as_str(), "+" | "-" | "*" | "/") {
7052                        let rule = match op_name.as_str() {
7053                            "+" => "sum",
7054                            "-" => "difference",
7055                            "*" => "product",
7056                            "/" => "quotient",
7057                            _ => unreachable!(),
7058                        };
7059                        return wrap_proof(
7060                            rule,
7061                            vec![build_proof(&children[0], env), build_proof(&children[2], env)],
7062                        );
7063                    }
7064                }
7065            }
7066
7067            // Infix AND/OR/BOTH/NEITHER
7068            if children.len() == 3 {
7069                if let Node::Leaf(op_name) = &children[1] {
7070                    if matches!(op_name.as_str(), "and" | "or" | "both" | "neither") {
7071                        return wrap_proof(
7072                            op_name,
7073                            vec![build_proof(&children[0], env), build_proof(&children[2], env)],
7074                        );
7075                    }
7076                }
7077            }
7078
7079            // Composite both/neither chains: (both A and B [and C ...]),
7080            // (neither A nor B [nor C ...]).
7081            if children.len() >= 4 && children.len() % 2 == 0 {
7082                if let Node::Leaf(head) = &children[0] {
7083                    if head == "both" || head == "neither" {
7084                        let sep = if head == "both" { "and" } else { "nor" };
7085                        let mut valid = true;
7086                        for i in (2..children.len()).step_by(2) {
7087                            if let Node::Leaf(s) = &children[i] {
7088                                if s != sep {
7089                                    valid = false;
7090                                    break;
7091                                }
7092                            } else {
7093                                valid = false;
7094                                break;
7095                            }
7096                        }
7097                        if valid {
7098                            let subs: Vec<Node> = (1..children.len())
7099                                .step_by(2)
7100                                .map(|i| build_proof(&children[i], env))
7101                                .collect();
7102                            return wrap_proof(head, subs);
7103                        }
7104                    }
7105                }
7106            }
7107
7108            // Infix equality / inequality: (L = R), (L != R)
7109            if children.len() == 3 {
7110                if let Node::Leaf(op_name) = &children[1] {
7111                    if op_name == "=" || op_name == "!=" {
7112                        let l = &children[0];
7113                        let r = &children[2];
7114                        let rule = classify_equality_rule(l, r, op_name, env);
7115                        // Sub-derivation of equality preserves the original
7116                        // operands as a link so the witness reads
7117                        // `(by structural-equality (a a))` per the issue.
7118                        let pair = Node::List(vec![l.clone(), r.clone()]);
7119                        return wrap_proof(rule, vec![pair]);
7120                    }
7121                }
7122            }
7123
7124            // ---------- Type system witnesses ----------
7125            if children.len() == 2 {
7126                if let (Node::Leaf(h), level) = (&children[0], &children[1]) {
7127                    if h == "Type" {
7128                        return wrap_proof("type-universe", vec![level.clone()]);
7129                    }
7130                }
7131            }
7132            if children.len() == 1 {
7133                if let Node::Leaf(h) = &children[0] {
7134                    if h == "Prop" {
7135                        return wrap_proof("prop", vec![]);
7136                    }
7137                }
7138            }
7139            if children.len() == 3 {
7140                if let Node::Leaf(h) = &children[0] {
7141                    if h == "Pi" {
7142                        return wrap_proof(
7143                            "pi-formation",
7144                            vec![children[1].clone(), children[2].clone()],
7145                        );
7146                    }
7147                    if h == "lambda" {
7148                        return wrap_proof(
7149                            "lambda-formation",
7150                            vec![children[1].clone(), children[2].clone()],
7151                        );
7152                    }
7153                    if h == "apply" {
7154                        return wrap_proof(
7155                            "beta-reduction",
7156                            vec![build_proof(&children[1], env), build_proof(&children[2], env)],
7157                        );
7158                    }
7159                }
7160            }
7161            // Normalization witnesses (issue #50, D4):
7162            //   `(whnf <expr>)` → `whnf-reduction`
7163            //   `(nf <expr>)` and `(normal-form <expr>)` → `nf-reduction`
7164            if children.len() == 2 {
7165                if let Node::Leaf(h) = &children[0] {
7166                    if h == "whnf" {
7167                        return wrap_proof("whnf-reduction", vec![children[1].clone()]);
7168                    }
7169                    if h == "nf" || h == "normal-form" {
7170                        return wrap_proof("nf-reduction", vec![children[1].clone()]);
7171                    }
7172                }
7173            }
7174            if children.len() == 4 {
7175                if let Node::Leaf(h) = &children[0] {
7176                    if h == "subst" {
7177                        return wrap_proof(
7178                            "substitution",
7179                            vec![
7180                                children[1].clone(),
7181                                children[2].clone(),
7182                                children[3].clone(),
7183                            ],
7184                        );
7185                    }
7186                    if h == "fresh" {
7187                        if let Node::Leaf(in_kw) = &children[2] {
7188                            if in_kw == "in" {
7189                                return wrap_proof(
7190                                    "fresh",
7191                                    vec![children[1].clone(), children[3].clone()],
7192                                );
7193                            }
7194                        }
7195                    }
7196                }
7197            }
7198            if children.len() == 3 {
7199                if let (Node::Leaf(h), Node::Leaf(m)) = (&children[0], &children[1]) {
7200                    if h == "type" && m == "of" {
7201                        return wrap_proof(
7202                            "type-query",
7203                            vec![children[2].clone()],
7204                        );
7205                    }
7206                }
7207                if let Node::Leaf(m) = &children[1] {
7208                    if m == "of" {
7209                        return wrap_proof(
7210                            "type-check",
7211                            vec![children[0].clone(), children[2].clone()],
7212                        );
7213                    }
7214                }
7215            }
7216
7217            // Prefix operator: (op X Y ...)
7218            if let Node::Leaf(head) = &children[0] {
7219                if env.has_op(head) {
7220                    let subs: Vec<Node> = children[1..]
7221                        .iter()
7222                        .map(|arg| build_proof(arg, env))
7223                        .collect();
7224                    return wrap_proof(head, subs);
7225                }
7226            }
7227
7228            // Fallback for unrecognised heads / named lambda applications.
7229            wrap_proof("reduce", vec![node.clone()])
7230        }
7231    }
7232}
7233
7234// ========== Tactic engine (issues #55 and #56) ==========
7235// Tactics are ordinary links that transform an explicit proof state. Keeping
7236// goals, local assumptions, and tactic history as `Node` values preserves the
7237// project invariant that proof steps are links.
7238
7239/// A single open proof goal plus its local hypothesis context.
7240#[derive(Debug, Clone, PartialEq)]
7241pub struct ProofGoal {
7242    pub goal: Node,
7243    pub context: Vec<Node>,
7244}
7245
7246impl ProofGoal {
7247    pub fn new(goal: Node) -> Self {
7248        Self {
7249            goal,
7250            context: Vec::new(),
7251        }
7252    }
7253}
7254
7255/// A tactic proof state: open goals and the successful tactic links applied so far.
7256#[derive(Debug, Clone, Default, PartialEq)]
7257pub struct ProofState {
7258    pub goals: Vec<ProofGoal>,
7259    pub proof: Vec<Node>,
7260}
7261
7262impl ProofState {
7263    pub fn from_goals(goals: Vec<Node>) -> Self {
7264        Self {
7265            goals: goals.into_iter().map(ProofGoal::new).collect(),
7266            proof: Vec::new(),
7267        }
7268    }
7269}
7270
7271/// Result of running tactics over a proof state.
7272#[derive(Debug, Clone, PartialEq)]
7273pub struct TacticRunResult {
7274    pub state: ProofState,
7275    pub diagnostics: Vec<Diagnostic>,
7276}
7277
7278const DEFAULT_SIMPLIFY_MAX_STEPS: usize = 100;
7279const DEFAULT_ATP_TIMEOUT_MS: u64 = 5000;
7280const DEFAULT_SMT_TIMEOUT_MS: u64 = 5000;
7281const ATP_PROVED_STATUSES: &[&str] = &["Theorem", "Unsatisfiable", "ContradictoryAxioms"];
7282const ATP_UNKNOWN_STATUSES: &[&str] = &["Unknown", "GaveUp"];
7283const ATP_TIMEOUT_STATUSES: &[&str] = &["Timeout", "ResourceOut"];
7284
7285/// Direction for applying an equality rewrite rule.
7286#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7287pub enum RewriteDirection {
7288    Forward,
7289    Backward,
7290}
7291
7292/// Which occurrence of the left-hand side to rewrite.
7293#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7294pub enum RewriteOccurrence {
7295    All,
7296    Index(usize),
7297}
7298
7299/// Options for a single rewrite pass.
7300#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7301pub struct RewriteOptions {
7302    pub direction: RewriteDirection,
7303    pub occurrence: RewriteOccurrence,
7304}
7305
7306impl Default for RewriteOptions {
7307    fn default() -> Self {
7308        Self {
7309            direction: RewriteDirection::Forward,
7310            occurrence: RewriteOccurrence::All,
7311        }
7312    }
7313}
7314
7315/// Result of a single rewrite pass.
7316#[derive(Debug, Clone, PartialEq)]
7317pub struct RewriteResult {
7318    pub node: Node,
7319    pub changed: bool,
7320    pub count: usize,
7321}
7322
7323/// Options for repeated simplification with a rewrite set.
7324#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7325pub struct SimplifyOptions {
7326    pub max_steps: usize,
7327}
7328
7329impl Default for SimplifyOptions {
7330    fn default() -> Self {
7331        Self {
7332            max_steps: DEFAULT_SIMPLIFY_MAX_STEPS,
7333        }
7334    }
7335}
7336
7337/// Result of simplification by repeated rewrite passes.
7338#[derive(Debug, Clone, PartialEq)]
7339pub struct SimplifyResult {
7340    pub node: Node,
7341    pub changed: bool,
7342    pub steps: usize,
7343}
7344
7345/// Configured external ATP invocation for the `(by atp)` tactic.
7346#[derive(Debug, Clone, PartialEq, Eq)]
7347pub struct AtpOptions {
7348    pub path: Option<String>,
7349    pub args: Vec<String>,
7350    pub name: Option<String>,
7351    pub timeout_ms: u64,
7352}
7353
7354impl Default for AtpOptions {
7355    fn default() -> Self {
7356        Self {
7357            path: None,
7358            args: Vec::new(),
7359            name: None,
7360            timeout_ms: DEFAULT_ATP_TIMEOUT_MS,
7361        }
7362    }
7363}
7364
7365/// High-level classification of a parsed SZS ATP status.
7366#[derive(Debug, Clone, PartialEq, Eq)]
7367pub enum AtpStatusKind {
7368    Proved,
7369    Unknown,
7370    Timeout,
7371    Failure,
7372}
7373
7374/// Parsed SZS status line from an ATP.
7375#[derive(Debug, Clone, PartialEq, Eq)]
7376pub struct AtpStatus {
7377    pub status: String,
7378    pub kind: AtpStatusKind,
7379}
7380
7381/// Options supplied to tactic execution.
7382#[derive(Debug, Clone, PartialEq)]
7383pub struct TacticOptions {
7384    pub rewrite_rules: Vec<Node>,
7385    pub simplify_max_steps: usize,
7386    pub atp: AtpOptions,
7387    pub smt_solver: Option<String>,
7388    pub smt_solver_args: Vec<String>,
7389    pub smt_timeout_ms: u64,
7390}
7391
7392impl Default for TacticOptions {
7393    fn default() -> Self {
7394        Self {
7395            rewrite_rules: Vec::new(),
7396            simplify_max_steps: DEFAULT_SIMPLIFY_MAX_STEPS,
7397            atp: AtpOptions::default(),
7398            smt_solver: std::env::var("RML_SMT_SOLVER").ok(),
7399            smt_solver_args: std::env::var("RML_SMT_ARGS")
7400                .ok()
7401                .map(|args| {
7402                    args.split_whitespace()
7403                        .map(|arg| arg.to_string())
7404                        .collect()
7405                })
7406                .unwrap_or_default(),
7407            smt_timeout_ms: std::env::var("RML_SMT_TIMEOUT_MS")
7408                .ok()
7409                .and_then(|raw| raw.parse::<u64>().ok())
7410                .unwrap_or(DEFAULT_SMT_TIMEOUT_MS),
7411        }
7412    }
7413}
7414
7415fn tactic_name(tactic: &Node) -> Option<&str> {
7416    match tactic {
7417        Node::Leaf(s) => Some(s.as_str()),
7418        Node::List(children) => match children.first() {
7419            Some(Node::Leaf(s)) => Some(s.as_str()),
7420            _ => None,
7421        },
7422    }
7423}
7424
7425fn tactic_args(tactic: &Node) -> &[Node] {
7426    match tactic {
7427        Node::List(children) if !children.is_empty() => &children[1..],
7428        _ => &[],
7429    }
7430}
7431
7432fn as_equality(node: &Node) -> Option<(&Node, &Node)> {
7433    if let Node::List(children) = node {
7434        if children.len() == 3 {
7435            if let Node::Leaf(op) = &children[1] {
7436                if op == "=" {
7437                    return Some((&children[0], &children[2]));
7438                }
7439            }
7440        }
7441    }
7442    None
7443}
7444
7445fn tactic_diagnostic(
7446    tactic: &Node,
7447    goal: Option<&ProofGoal>,
7448    reason: impl AsRef<str>,
7449) -> Diagnostic {
7450    let goal_text = goal
7451        .map(|g| key_of(&g.goal))
7452        .unwrap_or_else(|| "<none>".to_string());
7453    Diagnostic::new(
7454        "E039",
7455        format!(
7456            "Tactic {} failed: {}; current goal: {}",
7457            key_of(tactic),
7458            reason.as_ref(),
7459            goal_text
7460        ),
7461        Span::unknown(),
7462    )
7463}
7464
7465fn goal_with_context(current: &ProofGoal, goal: Node) -> ProofGoal {
7466    ProofGoal {
7467        goal,
7468        context: current.context.clone(),
7469    }
7470}
7471
7472fn replace_current_goal(
7473    state: &ProofState,
7474    replacement_goals: Vec<ProofGoal>,
7475    record_tactic: &Node,
7476) -> ProofState {
7477    let mut goals = replacement_goals;
7478    goals.extend(state.goals.iter().skip(1).cloned());
7479    let mut proof = state.proof.clone();
7480    proof.push(record_tactic.clone());
7481    ProofState { goals, proof }
7482}
7483
7484fn rewrite_diagnostic(message: impl Into<String>) -> Diagnostic {
7485    Diagnostic::new("E039", message, Span::unknown())
7486}
7487
7488#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7489enum SmtSort {
7490    Bool,
7491    Real,
7492}
7493
7494impl SmtSort {
7495    fn as_str(self) -> &'static str {
7496        match self {
7497            SmtSort::Bool => "Bool",
7498            SmtSort::Real => "Real",
7499        }
7500    }
7501}
7502
7503#[derive(Debug, Default)]
7504struct SmtContext {
7505    declarations: BTreeMap<String, SmtSort>,
7506}
7507
7508#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7509enum SmtStatus {
7510    Unsat,
7511    Sat,
7512    Unknown,
7513    Timeout,
7514    Error,
7515}
7516
7517#[derive(Debug, Clone, PartialEq, Eq)]
7518struct SmtRunResult {
7519    status: SmtStatus,
7520    reason: String,
7521}
7522
7523fn smt_escape_symbol(raw: &str) -> String {
7524    format!("|{}|", raw.replace('\\', "\\\\").replace('|', "\\|"))
7525}
7526
7527fn smt_declare(ctx: &mut SmtContext, raw: String, sort: SmtSort) -> Result<String, String> {
7528    if let Some(existing) = ctx.declarations.get(&raw) {
7529        if *existing != sort {
7530            return Err(format!(
7531                "SMT symbol {} is used as both {} and {}",
7532                raw,
7533                existing.as_str(),
7534                sort.as_str()
7535            ));
7536        }
7537    }
7538    ctx.declarations.insert(raw.clone(), sort);
7539    Ok(smt_escape_symbol(&raw))
7540}
7541
7542fn smt_number(raw: &str) -> String {
7543    if let Some(rest) = raw.strip_prefix('-') {
7544        format!("(- {})", rest)
7545    } else {
7546        raw.to_string()
7547    }
7548}
7549
7550fn smt_infix<'a>(node: &'a Node, operators: &[&str]) -> Option<&'a str> {
7551    let Node::List(children) = node else {
7552        return None;
7553    };
7554    if children.len() != 3 {
7555        return None;
7556    }
7557    let Node::Leaf(op) = &children[1] else {
7558        return None;
7559    };
7560    if operators.contains(&op.as_str()) {
7561        Some(op.as_str())
7562    } else {
7563        None
7564    }
7565}
7566
7567fn smt_is_boolish(node: &Node) -> bool {
7568    match node {
7569        Node::Leaf(s) => s == "true" || s == "false",
7570        Node::List(children) => {
7571            if children.is_empty() {
7572                return false;
7573            }
7574            if smt_infix(node, &["=", "!=", "and", "or", "=>", "implies"]).is_some() {
7575                return true;
7576            }
7577            matches!(
7578                &children[0],
7579                Node::Leaf(head)
7580                    if matches!(head.as_str(), "not" | "and" | "or" | "=>" | "implies")
7581            )
7582        }
7583    }
7584}
7585
7586fn smt_term(node: &Node, ctx: &mut SmtContext) -> Result<String, String> {
7587    match node {
7588        Node::Leaf(s) => {
7589            if is_num(s) {
7590                return Ok(smt_number(s));
7591            }
7592            if s == "true" || s == "false" {
7593                return Err(format!(
7594                    "SMT bridge cannot use Boolean constant {} as a Real term",
7595                    s
7596                ));
7597            }
7598            smt_declare(ctx, s.clone(), SmtSort::Real)
7599        }
7600        Node::List(children) => {
7601            if children.is_empty() {
7602                return Err(format!("SMT bridge cannot translate term {}", key_of(node)));
7603            }
7604            if let Some(op) = smt_infix(node, &["+", "-", "*", "/"]) {
7605                return Ok(format!(
7606                    "({} {} {})",
7607                    op,
7608                    smt_term(&children[0], ctx)?,
7609                    smt_term(&children[2], ctx)?
7610                ));
7611            }
7612            if let Node::Leaf(head) = &children[0] {
7613                if ["+", "-", "*", "/"].contains(&head.as_str()) && children.len() >= 3 {
7614                    let args = children[1..]
7615                        .iter()
7616                        .map(|arg| smt_term(arg, ctx))
7617                        .collect::<Result<Vec<_>, _>>()?;
7618                    return Ok(format!("({} {})", head, args.join(" ")));
7619                }
7620            }
7621            smt_declare(ctx, key_of(node), SmtSort::Real)
7622        }
7623    }
7624}
7625
7626fn smt_equality(left: &Node, right: &Node, ctx: &mut SmtContext) -> Result<String, String> {
7627    if smt_is_boolish(left) || smt_is_boolish(right) {
7628        return Ok(format!(
7629            "(= {} {})",
7630            smt_formula(left, ctx)?,
7631            smt_formula(right, ctx)?
7632        ));
7633    }
7634    Ok(format!(
7635        "(= {} {})",
7636        smt_term(left, ctx)?,
7637        smt_term(right, ctx)?
7638    ))
7639}
7640
7641fn smt_formula(node: &Node, ctx: &mut SmtContext) -> Result<String, String> {
7642    match node {
7643        Node::Leaf(s) => {
7644            if s == "true" {
7645                return Ok("true".to_string());
7646            }
7647            if s == "false" {
7648                return Ok("false".to_string());
7649            }
7650            if is_num(s) {
7651                return Err(format!(
7652                    "SMT bridge cannot use numeric literal {} as a Boolean formula",
7653                    s
7654                ));
7655            }
7656            smt_declare(ctx, s.clone(), SmtSort::Bool)
7657        }
7658        Node::List(children) => {
7659            if children.is_empty() {
7660                return Err(format!("SMT bridge cannot translate formula {}", key_of(node)));
7661            }
7662            match smt_infix(node, &["=", "!=", "and", "or", "=>", "implies"]) {
7663                Some("=") => return smt_equality(&children[0], &children[2], ctx),
7664                Some("!=") => {
7665                    return Ok(format!(
7666                        "(not {})",
7667                        smt_equality(&children[0], &children[2], ctx)?
7668                    ));
7669                }
7670                Some("and") | Some("or") => {
7671                    let op = smt_infix(node, &["and", "or"]).unwrap();
7672                    return Ok(format!(
7673                        "({} {} {})",
7674                        op,
7675                        smt_formula(&children[0], ctx)?,
7676                        smt_formula(&children[2], ctx)?
7677                    ));
7678                }
7679                Some("=>") | Some("implies") => {
7680                    return Ok(format!(
7681                        "(=> {} {})",
7682                        smt_formula(&children[0], ctx)?,
7683                        smt_formula(&children[2], ctx)?
7684                    ));
7685                }
7686                _ => {}
7687            }
7688
7689            if let Node::Leaf(head) = &children[0] {
7690                match head.as_str() {
7691                    "not" if children.len() == 2 => {
7692                        return Ok(format!("(not {})", smt_formula(&children[1], ctx)?));
7693                    }
7694                    "and" | "or" => {
7695                        if children.len() == 1 {
7696                            return Ok(if head == "and" {
7697                                "true".to_string()
7698                            } else {
7699                                "false".to_string()
7700                            });
7701                        }
7702                        let args = children[1..]
7703                            .iter()
7704                            .map(|arg| smt_formula(arg, ctx))
7705                            .collect::<Result<Vec<_>, _>>()?;
7706                        return Ok(format!("({} {})", head, args.join(" ")));
7707                    }
7708                    "=>" | "implies" if children.len() == 3 => {
7709                        return Ok(format!(
7710                            "(=> {} {})",
7711                            smt_formula(&children[1], ctx)?,
7712                            smt_formula(&children[2], ctx)?
7713                        ));
7714                    }
7715                    _ => {}
7716                }
7717            }
7718
7719            smt_declare(ctx, key_of(node), SmtSort::Bool)
7720        }
7721    }
7722}
7723
7724fn smt_lib_for_goal(goal: &Node) -> Result<String, String> {
7725    let mut ctx = SmtContext::default();
7726    let formula = smt_formula(goal, &mut ctx)?;
7727    let mut lines = Vec::new();
7728    for (name, sort) in ctx.declarations {
7729        lines.push(format!(
7730            "(declare-const {} {})",
7731            smt_escape_symbol(&name),
7732            sort.as_str()
7733        ));
7734    }
7735    lines.push(format!("(assert (not {}))", formula));
7736    lines.push("(check-sat)".to_string());
7737    lines.push("(exit)".to_string());
7738    lines.push(String::new());
7739    Ok(lines.join("\n"))
7740}
7741
7742fn smt_solver_proof_name(options: &TacticOptions) -> String {
7743    let Some(solver) = options.smt_solver.as_deref() else {
7744        return "unconfigured".to_string();
7745    };
7746    let base = Path::new(solver)
7747        .file_name()
7748        .and_then(|name| name.to_str())
7749        .unwrap_or(solver);
7750    let safe: String = base
7751        .chars()
7752        .map(|c| if c.is_whitespace() { '_' } else { c })
7753        .collect();
7754    if safe.is_empty() {
7755        "solver".to_string()
7756    } else {
7757        safe
7758    }
7759}
7760
7761fn smt_trusted_node(options: &TacticOptions) -> Node {
7762    Node::List(vec![
7763        leaf("by"),
7764        leaf("smt-trusted"),
7765        Node::Leaf(smt_solver_proof_name(options)),
7766    ])
7767}
7768
7769fn smt_process_summary(stdout: &[u8], stderr: &[u8]) -> String {
7770    let stderr_text = String::from_utf8_lossy(stderr);
7771    let stdout_text = String::from_utf8_lossy(stdout);
7772    let text = if stderr_text.trim().is_empty() {
7773        stdout_text.trim()
7774    } else {
7775        stderr_text.trim()
7776    };
7777    if text.is_empty() {
7778        return "<no output>".to_string();
7779    }
7780    let first = text.lines().next().unwrap_or(text);
7781    if first.chars().count() > 200 {
7782        format!("{}...", first.chars().take(200).collect::<String>())
7783    } else {
7784        first.to_string()
7785    }
7786}
7787
7788fn parse_smt_check_sat(stdout: &[u8], stderr: &[u8]) -> Option<SmtStatus> {
7789    let stdout_text = String::from_utf8_lossy(stdout);
7790    let stderr_text = String::from_utf8_lossy(stderr);
7791    for line in stdout_text.lines().chain(stderr_text.lines()) {
7792        match line.trim() {
7793            "unsat" => return Some(SmtStatus::Unsat),
7794            "sat" => return Some(SmtStatus::Sat),
7795            "unknown" => return Some(SmtStatus::Unknown),
7796            _ => {}
7797        }
7798    }
7799    None
7800}
7801
7802fn run_smt_solver(smt_lib: &str, options: &TacticOptions) -> SmtRunResult {
7803    let Some(solver) = options
7804        .smt_solver
7805        .as_deref()
7806        .filter(|solver| !solver.trim().is_empty())
7807    else {
7808        return SmtRunResult {
7809            status: SmtStatus::Error,
7810            reason: "SMT solver path is not configured".to_string(),
7811        };
7812    };
7813    let solver_name = smt_solver_proof_name(options);
7814    let mut child = match Command::new(solver)
7815        .args(&options.smt_solver_args)
7816        .stdin(Stdio::piped())
7817        .stdout(Stdio::piped())
7818        .stderr(Stdio::piped())
7819        .spawn()
7820    {
7821        Ok(child) => child,
7822        Err(err) => {
7823            return SmtRunResult {
7824                status: SmtStatus::Error,
7825                reason: format!("SMT solver {} failed to start: {}", solver_name, err),
7826            };
7827        }
7828    };
7829
7830    let mut stdin_error = None;
7831    if let Some(mut stdin) = child.stdin.take() {
7832        if let Err(err) = stdin.write_all(smt_lib.as_bytes()) {
7833            stdin_error = Some(err.to_string());
7834        }
7835    }
7836
7837    let started = Instant::now();
7838    let timeout = Duration::from_millis(options.smt_timeout_ms);
7839    loop {
7840        match child.try_wait() {
7841            Ok(Some(_)) => {
7842                let output = match child.wait_with_output() {
7843                    Ok(output) => output,
7844                    Err(err) => {
7845                        return SmtRunResult {
7846                            status: SmtStatus::Error,
7847                            reason: format!(
7848                                "SMT solver {} output collection failed: {}",
7849                                solver_name, err
7850                            ),
7851                        };
7852                    }
7853                };
7854                if !output.status.success() {
7855                    return SmtRunResult {
7856                        status: SmtStatus::Error,
7857                        reason: format!(
7858                            "SMT solver {} exited with status {}: {}",
7859                            solver_name,
7860                            output.status,
7861                            smt_process_summary(&output.stdout, &output.stderr)
7862                        ),
7863                    };
7864                }
7865                let Some(status) = parse_smt_check_sat(&output.stdout, &output.stderr) else {
7866                    let reason = stdin_error
7867                        .map(|err| {
7868                            format!(
7869                                "SMT solver {} did not accept SMT-LIB input: {}",
7870                                solver_name, err
7871                            )
7872                        })
7873                        .unwrap_or_else(|| {
7874                            format!(
7875                                "SMT solver {} did not return sat, unsat, or unknown",
7876                                solver_name
7877                            )
7878                        });
7879                    return SmtRunResult {
7880                        status: SmtStatus::Error,
7881                        reason,
7882                    };
7883                };
7884                return SmtRunResult {
7885                    status,
7886                    reason: format!(
7887                        "SMT solver {} returned {}",
7888                        solver_name,
7889                        match status {
7890                            SmtStatus::Unsat => "unsat",
7891                            SmtStatus::Sat => "sat",
7892                            SmtStatus::Unknown => "unknown",
7893                            SmtStatus::Timeout | SmtStatus::Error => unreachable!(),
7894                        }
7895                    ),
7896                };
7897            }
7898            Ok(None) => {
7899                if started.elapsed() >= timeout {
7900                    let _ = child.kill();
7901                    let _ = child.wait();
7902                    return SmtRunResult {
7903                        status: SmtStatus::Timeout,
7904                        reason: format!(
7905                            "SMT solver {} timed out after {} ms",
7906                            solver_name, options.smt_timeout_ms
7907                        ),
7908                    };
7909                }
7910                thread::sleep(Duration::from_millis(10));
7911            }
7912            Err(err) => {
7913                return SmtRunResult {
7914                    status: SmtStatus::Error,
7915                    reason: format!("SMT solver {} wait failed: {}", solver_name, err),
7916                };
7917            }
7918        }
7919    }
7920}
7921
7922fn tptp_identifier(raw: &str, role: &str) -> String {
7923    let mut cleaned: String = raw
7924        .chars()
7925        .map(|c| {
7926            if c.is_ascii_alphanumeric() || c == '_' {
7927                c
7928            } else {
7929                '_'
7930            }
7931        })
7932        .collect();
7933    if cleaned.is_empty() {
7934        cleaned = if role == "var" {
7935            "X".to_string()
7936        } else {
7937            "rml_symbol".to_string()
7938        };
7939    }
7940    if role == "var" {
7941        let mut chars = cleaned.chars();
7942        if let Some(first) = chars.next() {
7943            cleaned = first.to_ascii_uppercase().to_string() + chars.as_str();
7944        }
7945        if !cleaned
7946            .chars()
7947            .next()
7948            .map(|c| c.is_ascii_uppercase())
7949            .unwrap_or(false)
7950        {
7951            cleaned = format!("V_{}", cleaned);
7952        }
7953        return cleaned;
7954    }
7955    cleaned = cleaned.to_ascii_lowercase();
7956    if !cleaned
7957        .chars()
7958        .next()
7959        .map(|c| c.is_ascii_lowercase())
7960        .unwrap_or(false)
7961    {
7962        cleaned = format!("rml_{}", cleaned);
7963    }
7964    cleaned
7965}
7966
7967fn tptp_term(node: &Node, bound_vars: &HashSet<String>) -> Result<String, Diagnostic> {
7968    match node {
7969        Node::Leaf(raw) => {
7970            if bound_vars.contains(raw) {
7971                Ok(tptp_identifier(raw, "var"))
7972            } else if is_num(raw) {
7973                Ok(tptp_identifier(&format!("num_{}", raw), "term"))
7974            } else {
7975                Ok(tptp_identifier(raw, "term"))
7976            }
7977        }
7978        Node::List(children) if !children.is_empty() => {
7979            let Node::Leaf(head) = &children[0] else {
7980                return Err(rewrite_diagnostic(format!(
7981                    "TPTP export supports first-order terms only (got {})",
7982                    key_of(node)
7983                )));
7984            };
7985            let args = children[1..]
7986                .iter()
7987                .map(|arg| tptp_term(arg, bound_vars))
7988                .collect::<Result<Vec<_>, _>>()?
7989                .join(", ");
7990            Ok(format!("{}({})", tptp_identifier(head, "term"), args))
7991        }
7992        _ => Err(rewrite_diagnostic(format!(
7993            "TPTP export supports first-order terms only (got {})",
7994            key_of(node)
7995        ))),
7996    }
7997}
7998
7999fn infix_operands<'a>(node: &'a Node, op: &str) -> Option<Vec<&'a Node>> {
8000    let Node::List(children) = node else {
8001        return None;
8002    };
8003    if children.len() < 3 || children.len() % 2 == 0 {
8004        return None;
8005    }
8006    let mut operands = Vec::new();
8007    let mut index = 0;
8008    while index < children.len() {
8009        if index > 0 && !matches!(&children[index - 1], Node::Leaf(mid) if mid == op) {
8010            return None;
8011        }
8012        operands.push(&children[index]);
8013        index += 2;
8014    }
8015    Some(operands)
8016}
8017
8018fn tptp_join_formula(
8019    op: &str,
8020    operands: &[&Node],
8021    bound_vars: &HashSet<String>,
8022) -> Result<String, Diagnostic> {
8023    let parts = operands
8024        .iter()
8025        .map(|part| tptp_formula(part, bound_vars).map(|s| format!("({})", s)))
8026        .collect::<Result<Vec<_>, _>>()?;
8027    Ok(parts.join(&format!(" {} ", op)))
8028}
8029
8030fn quantifier_parts<'a>(node: &'a Node) -> Result<Option<(&'a str, String, &'a Node)>, Diagnostic> {
8031    let Node::List(children) = node else {
8032        return Ok(None);
8033    };
8034    if children.len() != 3 {
8035        return Ok(None);
8036    }
8037    let Node::Leaf(head) = &children[0] else {
8038        return Ok(None);
8039    };
8040    if head != "forall" && head != "exists" && head != "Pi" {
8041        return Ok(None);
8042    }
8043    let Some((variable, _)) = parse_binding(&children[1]) else {
8044        return Err(rewrite_diagnostic(format!(
8045            "TPTP export could not parse quantifier binder {}",
8046            key_of(&children[1])
8047        )));
8048    };
8049    let quantifier = if head == "exists" { "?" } else { "!" };
8050    Ok(Some((quantifier, variable, &children[2])))
8051}
8052
8053fn tptp_formula(node: &Node, bound_vars: &HashSet<String>) -> Result<String, Diagnostic> {
8054    match node {
8055        Node::Leaf(raw) => {
8056            if raw == "true" {
8057                return Ok("$true".to_string());
8058            }
8059            if raw == "false" {
8060                return Ok("$false".to_string());
8061            }
8062            if bound_vars.contains(raw) {
8063                return Ok(tptp_identifier(raw, "var"));
8064            }
8065            return Ok(tptp_identifier(raw, "pred"));
8066        }
8067        Node::List(children) if children.is_empty() => {
8068            return Err(rewrite_diagnostic(format!(
8069                "TPTP export supports first-order formulas only (got {})",
8070                key_of(node)
8071            )));
8072        }
8073        Node::List(_) => {}
8074    }
8075
8076    if let Some((quantifier, variable, body)) = quantifier_parts(node)? {
8077        let mut next_bound = bound_vars.clone();
8078        next_bound.insert(variable.clone());
8079        return Ok(format!(
8080            "{}[{}] : ({})",
8081            quantifier,
8082            tptp_identifier(&variable, "var"),
8083            tptp_formula(body, &next_bound)?
8084        ));
8085    }
8086
8087    if let Some((term, typ)) = type_ascription(node) {
8088        return Ok(format!(
8089            "{}({})",
8090            tptp_identifier(&key_of(typ), "pred"),
8091            tptp_term(term, bound_vars)?
8092        ));
8093    }
8094
8095    if let Some((left, right)) = as_equality(node) {
8096        return Ok(format!(
8097            "{} = {}",
8098            tptp_term(left, bound_vars)?,
8099            tptp_term(right, bound_vars)?
8100        ));
8101    }
8102    if let Node::List(children) = node {
8103        if children.len() == 3 && matches!(&children[1], Node::Leaf(op) if op == "!=") {
8104            return Ok(format!(
8105                "{} != {}",
8106                tptp_term(&children[0], bound_vars)?,
8107                tptp_term(&children[2], bound_vars)?
8108            ));
8109        }
8110    }
8111
8112    if let Some(operands) = infix_operands(node, "and") {
8113        return tptp_join_formula("&", &operands, bound_vars);
8114    }
8115    if let Some(operands) = infix_operands(node, "or") {
8116        return tptp_join_formula("|", &operands, bound_vars);
8117    }
8118    if let Some(operands) = infix_operands(node, "=>").or_else(|| infix_operands(node, "implies")) {
8119        if operands.len() == 2 {
8120            return tptp_join_formula("=>", &operands, bound_vars);
8121        }
8122    }
8123    if let Some(operands) = infix_operands(node, "<=>").or_else(|| infix_operands(node, "iff")) {
8124        if operands.len() == 2 {
8125            return tptp_join_formula("<=>", &operands, bound_vars);
8126        }
8127    }
8128
8129    let Node::List(children) = node else {
8130        unreachable!();
8131    };
8132    let Node::Leaf(head) = &children[0] else {
8133        return Err(rewrite_diagnostic(format!(
8134            "TPTP export supports first-order formulas only (got {})",
8135            key_of(node)
8136        )));
8137    };
8138    match head.as_str() {
8139        "not" if children.len() == 2 => {
8140            Ok(format!("~({})", tptp_formula(&children[1], bound_vars)?))
8141        }
8142        "and" if children.len() >= 2 => {
8143            let operands: Vec<&Node> = children[1..].iter().collect();
8144            tptp_join_formula("&", &operands, bound_vars)
8145        }
8146        "or" if children.len() >= 2 => {
8147            let operands: Vec<&Node> = children[1..].iter().collect();
8148            tptp_join_formula("|", &operands, bound_vars)
8149        }
8150        "=>" | "implies" if children.len() == 3 => {
8151            let operands: Vec<&Node> = children[1..].iter().collect();
8152            tptp_join_formula("=>", &operands, bound_vars)
8153        }
8154        "<=>" | "iff" if children.len() == 3 => {
8155            let operands: Vec<&Node> = children[1..].iter().collect();
8156            tptp_join_formula("<=>", &operands, bound_vars)
8157        }
8158        _ => {
8159            let predicate = tptp_identifier(head, "pred");
8160            if children.len() == 1 {
8161                return Ok(predicate);
8162            }
8163            let args = children[1..]
8164                .iter()
8165                .map(|arg| tptp_term(arg, bound_vars))
8166                .collect::<Result<Vec<_>, _>>()?
8167                .join(", ");
8168            Ok(format!("{}({})", predicate, args))
8169        }
8170    }
8171}
8172
8173/// Export a proof goal plus local context as a TPTP FOF problem.
8174pub fn goal_to_tptp(goal: &ProofGoal) -> Result<String, Diagnostic> {
8175    let bound_vars = HashSet::new();
8176    let mut lines = Vec::new();
8177    for (index, ctx) in goal.context.iter().enumerate() {
8178        lines.push(format!(
8179            "fof(rml_context_{}, axiom, ({})).",
8180            index + 1,
8181            tptp_formula(ctx, &bound_vars)?
8182        ));
8183    }
8184    lines.push(format!(
8185        "fof(rml_goal, conjecture, ({})).",
8186        tptp_formula(&goal.goal, &bound_vars)?
8187    ));
8188    Ok(format!("{}\n", lines.join("\n")))
8189}
8190
8191/// Parse the first SZS status line from ATP output.
8192pub fn parse_atp_status(output: &str) -> Option<AtpStatus> {
8193    let tokens: Vec<&str> = output.split_whitespace().collect();
8194    for window in tokens.windows(3) {
8195        if window[0] == "SZS" && window[1] == "status" {
8196            let status = window[2].to_string();
8197            let kind = if ATP_PROVED_STATUSES.contains(&window[2]) {
8198                AtpStatusKind::Proved
8199            } else if ATP_UNKNOWN_STATUSES.contains(&window[2]) {
8200                AtpStatusKind::Unknown
8201            } else if ATP_TIMEOUT_STATUSES.contains(&window[2]) {
8202                AtpStatusKind::Timeout
8203            } else {
8204                AtpStatusKind::Failure
8205            };
8206            return Some(AtpStatus { status, kind });
8207        }
8208    }
8209    None
8210}
8211
8212fn atp_solver_name(options: &AtpOptions) -> String {
8213    let raw = options
8214        .name
8215        .clone()
8216        .or_else(|| {
8217            options.path.as_ref().and_then(|p| {
8218                Path::new(p)
8219                    .file_name()
8220                    .and_then(|name| name.to_str())
8221                    .map(|name| name.to_string())
8222            })
8223        })
8224        .unwrap_or_else(|| "atp".to_string());
8225    let cleaned = raw
8226        .chars()
8227        .map(|c| {
8228            if c.is_whitespace() || c == '(' || c == ')' {
8229                '_'
8230            } else {
8231                c
8232            }
8233        })
8234        .collect::<String>();
8235    if cleaned.is_empty() {
8236        "atp".to_string()
8237    } else {
8238        cleaned
8239    }
8240}
8241
8242struct AtpRunSuccess {
8243    solver: String,
8244}
8245
8246fn read_atp_pipe<R>(mut pipe: R) -> JoinHandle<Result<Vec<u8>, String>>
8247where
8248    R: Read + Send + 'static,
8249{
8250    std::thread::spawn(move || {
8251        let mut bytes = Vec::new();
8252        pipe.read_to_end(&mut bytes)
8253            .map_err(|err| err.to_string())?;
8254        Ok(bytes)
8255    })
8256}
8257
8258fn collect_atp_pipe(
8259    handle: Option<JoinHandle<Result<Vec<u8>, String>>>,
8260    label: &str,
8261) -> Result<Vec<u8>, String> {
8262    match handle {
8263        Some(handle) => handle
8264            .join()
8265            .map_err(|_| format!("ATP {} reader failed", label))?,
8266        None => Ok(Vec::new()),
8267    }
8268}
8269
8270fn run_atp_process(tptp: &str, options: &AtpOptions) -> Result<AtpRunSuccess, String> {
8271    let Some(path) = options.path.as_ref().filter(|p| !p.is_empty()) else {
8272        return Err("ATP path is not configured".to_string());
8273    };
8274    if options.timeout_ms == 0 {
8275        return Err("ATP timeout must be a positive integer".to_string());
8276    }
8277    let mut child = Command::new(path)
8278        .args(&options.args)
8279        .stdin(Stdio::piped())
8280        .stdout(Stdio::piped())
8281        .stderr(Stdio::piped())
8282        .spawn()
8283        .map_err(|err| format!("ATP invocation failed: {}", err))?;
8284    let stdout_reader = child.stdout.take().map(read_atp_pipe);
8285    let stderr_reader = child.stderr.take().map(read_atp_pipe);
8286
8287    if let Some(mut stdin) = child.stdin.take() {
8288        stdin
8289            .write_all(tptp.as_bytes())
8290            .map_err(|err| format!("ATP invocation failed: {}", err))?;
8291    }
8292
8293    let deadline = Instant::now() + Duration::from_millis(options.timeout_ms);
8294    loop {
8295        match child.try_wait() {
8296            Ok(Some(_)) => break,
8297            Ok(None) => {
8298                if Instant::now() >= deadline {
8299                    let _ = child.kill();
8300                    let _ = child.wait();
8301                    let _ = collect_atp_pipe(stdout_reader, "stdout");
8302                    let _ = collect_atp_pipe(stderr_reader, "stderr");
8303                    return Err(format!("ATP timed out after {} ms", options.timeout_ms));
8304                }
8305                sleep(Duration::from_millis(5));
8306            }
8307            Err(err) => return Err(format!("ATP invocation failed: {}", err)),
8308        }
8309    }
8310
8311    let status = child
8312        .wait()
8313        .map_err(|err| format!("ATP invocation failed: {}", err))?;
8314    let stdout_bytes = collect_atp_pipe(stdout_reader, "stdout")?;
8315    let stderr_bytes = collect_atp_pipe(stderr_reader, "stderr")?;
8316    let stdout = String::from_utf8_lossy(&stdout_bytes);
8317    let stderr = String::from_utf8_lossy(&stderr_bytes);
8318    let combined = format!("{}\n{}", stdout, stderr);
8319
8320    if !status.success() {
8321        let detail = if !stderr.trim().is_empty() {
8322            stderr.trim().to_string()
8323        } else if !stdout.trim().is_empty() {
8324            stdout.trim().to_string()
8325        } else {
8326            status
8327                .code()
8328                .map(|code| format!("exit status {}", code))
8329                .unwrap_or_else(|| "terminated by signal".to_string())
8330        };
8331        return Err(format!("ATP exited with status {}: {}", status, detail));
8332    }
8333
8334    let Some(status) = parse_atp_status(&combined) else {
8335        return Err("ATP output did not contain an SZS status".to_string());
8336    };
8337    match status.kind {
8338        AtpStatusKind::Proved => Ok(AtpRunSuccess {
8339            solver: atp_solver_name(options),
8340        }),
8341        AtpStatusKind::Timeout | AtpStatusKind::Unknown => {
8342            Err(format!("ATP returned {}", status.status))
8343        }
8344        AtpStatusKind::Failure => Err(format!("ATP returned non-proving status {}", status.status)),
8345    }
8346}
8347
8348fn rewrite_sides(
8349    eq: &Node,
8350    direction: RewriteDirection,
8351) -> Result<(&Node, &Node), Diagnostic> {
8352    let Some((left, right)) = as_equality(eq) else {
8353        return Err(rewrite_diagnostic("rewrite expects an equality link"));
8354    };
8355    Ok(match direction {
8356        RewriteDirection::Forward => (left, right),
8357        RewriteDirection::Backward => (right, left),
8358    })
8359}
8360
8361fn rewrite_node(
8362    node: &Node,
8363    from: &Node,
8364    to: &Node,
8365    occurrence: RewriteOccurrence,
8366    seen: &mut usize,
8367    count: &mut usize,
8368) -> Node {
8369    if is_structurally_same(node, from) {
8370        *seen += 1;
8371        let selected = match occurrence {
8372            RewriteOccurrence::All => true,
8373            RewriteOccurrence::Index(index) => *seen == index,
8374        };
8375        if selected {
8376            *count += 1;
8377            return to.clone();
8378        }
8379    }
8380    match node {
8381        Node::Leaf(_) => node.clone(),
8382        Node::List(children) => Node::List(
8383            children
8384                .iter()
8385                .map(|child| rewrite_node(child, from, to, occurrence, seen, count))
8386                .collect(),
8387        ),
8388    }
8389}
8390
8391/// Rewrite `goal` once using equality `eq` and explicit options.
8392pub fn rewrite_with_options(
8393    goal: &Node,
8394    eq: &Node,
8395    options: RewriteOptions,
8396) -> Result<RewriteResult, Diagnostic> {
8397    let (from, to) = rewrite_sides(eq, options.direction)?;
8398    let mut seen = 0;
8399    let mut count = 0;
8400    let node = rewrite_node(
8401        goal,
8402        from,
8403        to,
8404        options.occurrence,
8405        &mut seen,
8406        &mut count,
8407    );
8408    Ok(RewriteResult {
8409        node,
8410        changed: count > 0,
8411        count,
8412    })
8413}
8414
8415/// Rewrite `goal` once using equality `eq` from left to right.
8416pub fn rewrite(goal: &Node, eq: &Node) -> Result<Node, Diagnostic> {
8417    rewrite_with_options(goal, eq, RewriteOptions::default()).map(|result| result.node)
8418}
8419
8420/// Repeatedly apply `rules` until no rule changes the term or the guard fires.
8421pub fn simplify_with_options(
8422    goal: &Node,
8423    rules: &[Node],
8424    options: SimplifyOptions,
8425) -> Result<SimplifyResult, Diagnostic> {
8426    let mut node = goal.clone();
8427    let mut changed = false;
8428    let mut steps = 0;
8429    loop {
8430        let mut applied = false;
8431        for rule in rules {
8432            let rewritten = rewrite_with_options(&node, rule, RewriteOptions::default())?;
8433            if !rewritten.changed {
8434                continue;
8435            }
8436            if steps >= options.max_steps {
8437                return Err(rewrite_diagnostic(format!(
8438                    "simplify termination guard reached after {} rewrite steps",
8439                    options.max_steps
8440                )));
8441            }
8442            node = rewritten.node;
8443            steps += 1;
8444            changed = true;
8445            applied = true;
8446            break;
8447        }
8448        if !applied {
8449            return Ok(SimplifyResult {
8450                node,
8451                changed,
8452                steps,
8453            });
8454        }
8455    }
8456}
8457
8458/// Repeatedly apply `rules` until no rule changes the term.
8459pub fn simplify(goal: &Node, rules: &[Node]) -> Result<Node, Diagnostic> {
8460    simplify_with_options(goal, rules, SimplifyOptions::default()).map(|result| result.node)
8461}
8462
8463fn type_ascription(node: &Node) -> Option<(&Node, &Node)> {
8464    if let Node::List(children) = node {
8465        if children.len() == 3 {
8466            if let Node::Leaf(mid) = &children[1] {
8467                if mid == "of" {
8468                    return Some((&children[0], &children[2]));
8469                }
8470            }
8471        }
8472    }
8473    None
8474}
8475
8476fn exact_closes_goal(arg: &Node, goal: &ProofGoal) -> bool {
8477    if is_structurally_same(arg, &goal.goal) {
8478        return true;
8479    }
8480    if let Some((_, typ)) = type_ascription(arg) {
8481        if is_structurally_same(typ, &goal.goal) {
8482            return true;
8483        }
8484    }
8485    goal.context.iter().any(|ctx| {
8486        if is_structurally_same(ctx, arg) && is_structurally_same(arg, &goal.goal) {
8487            return true;
8488        }
8489        if is_structurally_same(ctx, &goal.goal) && is_structurally_same(arg, &goal.goal) {
8490            return true;
8491        }
8492        if let Some((term, typ)) = type_ascription(ctx) {
8493            return is_structurally_same(term, arg) && is_structurally_same(typ, &goal.goal);
8494        }
8495        false
8496    })
8497}
8498
8499fn is_leaf(node: &Node, value: &str) -> bool {
8500    matches!(node, Node::Leaf(s) if s == value)
8501}
8502
8503fn parse_rewrite_direction(node: &Node) -> Option<RewriteDirection> {
8504    match node {
8505        Node::Leaf(s) if s == "->" => Some(RewriteDirection::Forward),
8506        Node::Leaf(s) if s == "<-" => Some(RewriteDirection::Backward),
8507        _ => None,
8508    }
8509}
8510
8511fn parse_rewrite_occurrence(node: &Node) -> Result<RewriteOccurrence, String> {
8512    let Node::Leaf(raw) = node else {
8513        return Err(format!(
8514            "rewrite occurrence must be \"all\", \"first\", or a positive integer (got {})",
8515            key_of(node)
8516        ));
8517    };
8518    if raw == "all" {
8519        return Ok(RewriteOccurrence::All);
8520    }
8521    if raw == "first" {
8522        return Ok(RewriteOccurrence::Index(1));
8523    }
8524    let Ok(index) = raw.parse::<usize>() else {
8525        return Err(format!(
8526            "rewrite occurrence must be \"all\", \"first\", or a positive integer (got {})",
8527            key_of(node)
8528        ));
8529    };
8530    if index == 0 {
8531        return Err(format!(
8532            "rewrite occurrence must be \"all\", \"first\", or a positive integer (got {})",
8533            key_of(node)
8534        ));
8535    }
8536    Ok(RewriteOccurrence::Index(index))
8537}
8538
8539struct ParsedRewriteTactic<'a> {
8540    eq: &'a Node,
8541    direction: RewriteDirection,
8542    occurrence: RewriteOccurrence,
8543}
8544
8545fn parse_rewrite_tactic(args: &[Node]) -> Result<ParsedRewriteTactic<'_>, String> {
8546    let mut index = 0;
8547    let mut direction = RewriteDirection::Forward;
8548    if let Some(next_direction) = args.first().and_then(parse_rewrite_direction) {
8549        direction = next_direction;
8550        index += 1;
8551    }
8552    if args.len() < index + 3
8553        || !is_leaf(&args[index + 1], "in")
8554        || !is_leaf(&args[index + 2], "goal")
8555    {
8556        return Err("rewrite expects `(rewrite [->|<-] (L = R) in goal [at N])`".to_string());
8557    }
8558    let eq = &args[index];
8559    index += 3;
8560    let mut occurrence = RewriteOccurrence::All;
8561    if index < args.len() {
8562        if !is_leaf(&args[index], "at") || index + 2 != args.len() {
8563            return Err("rewrite expects optional occurrence selector `at N`".to_string());
8564        }
8565        occurrence = parse_rewrite_occurrence(&args[index + 1])?;
8566    }
8567    Ok(ParsedRewriteTactic {
8568        eq,
8569        direction,
8570        occurrence,
8571    })
8572}
8573
8574fn rewrite_rules_from_node(node: &Node) -> Result<Vec<Node>, String> {
8575    if as_equality(node).is_some() {
8576        return Ok(vec![node.clone()]);
8577    }
8578    let Node::List(children) = node else {
8579        return Err(format!(
8580            "simplify expects equality rewrite rules (got {})",
8581            key_of(node)
8582        ));
8583    };
8584    let mut rules = Vec::with_capacity(children.len());
8585    for child in children {
8586        if as_equality(child).is_none() {
8587            return Err(format!(
8588                "simplify expects equality rewrite rules (got {})",
8589                key_of(child)
8590            ));
8591        }
8592        rules.push(child.clone());
8593    }
8594    Ok(rules)
8595}
8596
8597struct ParsedSimplifyTactic {
8598    rules: Option<Vec<Node>>,
8599    max_steps: Option<usize>,
8600}
8601
8602fn parse_simplify_tactic(args: &[Node]) -> Result<ParsedSimplifyTactic, String> {
8603    if args.len() < 2 || !is_leaf(&args[0], "in") || !is_leaf(&args[1], "goal") {
8604        return Err("simplify expects `(simplify in goal)`".to_string());
8605    }
8606    let mut index = 2;
8607    let mut rules = None;
8608    let mut max_steps = None;
8609    while index < args.len() {
8610        if is_leaf(&args[index], "using") && index + 1 < args.len() {
8611            rules = Some(rewrite_rules_from_node(&args[index + 1])?);
8612            index += 2;
8613            continue;
8614        }
8615        if (is_leaf(&args[index], "max") || is_leaf(&args[index], "limit"))
8616            && index + 1 < args.len()
8617        {
8618            let Node::Leaf(raw) = &args[index + 1] else {
8619                return Err("simplify max step count must be a non-negative integer".to_string());
8620            };
8621            let Ok(parsed) = raw.parse::<usize>() else {
8622                return Err("simplify max step count must be a non-negative integer".to_string());
8623            };
8624            max_steps = Some(parsed);
8625            index += 2;
8626            continue;
8627        }
8628        return Err("simplify expects optional `using <rules>` and `max <steps>` clauses".to_string());
8629    }
8630    Ok(ParsedSimplifyTactic { rules, max_steps })
8631}
8632
8633fn apply_tactic(
8634    state: &ProofState,
8635    tactic: &Node,
8636    record_tactic: &Node,
8637    tactic_options: &TacticOptions,
8638) -> Result<ProofState, Diagnostic> {
8639    let name = tactic_name(tactic);
8640    let args = tactic_args(tactic);
8641
8642    if name == Some("by") {
8643        if args.len() == 1 {
8644            return apply_tactic(state, &args[0], record_tactic, tactic_options);
8645        }
8646        if args.len() > 1 {
8647            return apply_tactic(state, &Node::List(args.to_vec()), record_tactic, tactic_options);
8648        }
8649        return Err(tactic_diagnostic(
8650            record_tactic,
8651            state.goals.first(),
8652            "`by` requires an inner tactic",
8653        ));
8654    }
8655
8656    let Some(current) = state.goals.first() else {
8657        return Err(tactic_diagnostic(record_tactic, None, "no open goals"));
8658    };
8659
8660    match name {
8661        Some("reflexivity") => {
8662            let Some((left, right)) = as_equality(&current.goal) else {
8663                return Err(tactic_diagnostic(
8664                    record_tactic,
8665                    Some(current),
8666                    "reflexivity expects an equality goal",
8667                ));
8668            };
8669            if !is_structurally_same(left, right) {
8670                return Err(tactic_diagnostic(
8671                    record_tactic,
8672                    Some(current),
8673                    "both sides are not structurally equal",
8674                ));
8675            }
8676            Ok(replace_current_goal(state, Vec::new(), record_tactic))
8677        }
8678        Some("symmetry") => {
8679            let Some((left, right)) = as_equality(&current.goal) else {
8680                return Err(tactic_diagnostic(
8681                    record_tactic,
8682                    Some(current),
8683                    "symmetry expects an equality goal",
8684                ));
8685            };
8686            Ok(replace_current_goal(
8687                state,
8688                vec![goal_with_context(
8689                    current,
8690                    Node::List(vec![right.clone(), leaf("="), left.clone()]),
8691                )],
8692                record_tactic,
8693            ))
8694        }
8695        Some("transitivity") => {
8696            let Some((left, right)) = as_equality(&current.goal) else {
8697                return Err(tactic_diagnostic(
8698                    record_tactic,
8699                    Some(current),
8700                    "transitivity expects an equality goal and one intermediate term",
8701                ));
8702            };
8703            if args.len() != 1 {
8704                return Err(tactic_diagnostic(
8705                    record_tactic,
8706                    Some(current),
8707                    "transitivity expects an equality goal and one intermediate term",
8708                ));
8709            }
8710            let mid = args[0].clone();
8711            Ok(replace_current_goal(
8712                state,
8713                vec![
8714                    goal_with_context(
8715                        current,
8716                        Node::List(vec![left.clone(), leaf("="), mid.clone()]),
8717                    ),
8718                    goal_with_context(
8719                        current,
8720                        Node::List(vec![mid, leaf("="), right.clone()]),
8721                    ),
8722                ],
8723                record_tactic,
8724            ))
8725        }
8726        Some("suppose") => {
8727            if args.len() != 1 {
8728                return Err(tactic_diagnostic(
8729                    record_tactic,
8730                    Some(current),
8731                    "suppose expects one hypothesis link",
8732                ));
8733            }
8734            let mut next = state.clone();
8735            next.goals[0].context.push(args[0].clone());
8736            next.proof.push(record_tactic.clone());
8737            Ok(next)
8738        }
8739        Some("introduce") => {
8740            if args.len() != 1 {
8741                return Err(tactic_diagnostic(
8742                    record_tactic,
8743                    Some(current),
8744                    "introduce expects one variable name",
8745                ));
8746            }
8747            let Node::Leaf(variable) = &args[0] else {
8748                return Err(tactic_diagnostic(
8749                    record_tactic,
8750                    Some(current),
8751                    "introduce expects one variable name",
8752                ));
8753            };
8754            let Node::List(goal_children) = &current.goal else {
8755                return Err(tactic_diagnostic(
8756                    record_tactic,
8757                    Some(current),
8758                    "introduce expects a Pi goal",
8759                ));
8760            };
8761            if goal_children.len() != 3 || !matches!(&goal_children[0], Node::Leaf(h) if h == "Pi") {
8762                return Err(tactic_diagnostic(
8763                    record_tactic,
8764                    Some(current),
8765                    "introduce expects a Pi goal",
8766                ));
8767            }
8768            let Some((param, param_type_key)) = parse_binding(&goal_children[1]) else {
8769                return Err(tactic_diagnostic(
8770                    record_tactic,
8771                    Some(current),
8772                    "introduce could not parse the Pi binder",
8773                ));
8774            };
8775            let body = subst(&goal_children[2], &param, &Node::Leaf(variable.clone()));
8776            let mut introduced = goal_with_context(current, body);
8777            introduced.context.push(Node::List(vec![
8778                Node::Leaf(variable.clone()),
8779                leaf("of"),
8780                type_key_to_node(&param_type_key),
8781            ]));
8782            Ok(replace_current_goal(
8783                state,
8784                vec![introduced],
8785                record_tactic,
8786            ))
8787        }
8788        Some("rewrite") => {
8789            let parsed = parse_rewrite_tactic(args)
8790                .map_err(|reason| tactic_diagnostic(record_tactic, Some(current), reason))?;
8791            let rewritten = rewrite_with_options(
8792                &current.goal,
8793                parsed.eq,
8794                RewriteOptions {
8795                    direction: parsed.direction,
8796                    occurrence: parsed.occurrence,
8797                },
8798            )
8799            .map_err(|diag| tactic_diagnostic(record_tactic, Some(current), diag.message))?;
8800            if !rewritten.changed {
8801                let (from, _) = rewrite_sides(parsed.eq, parsed.direction)
8802                    .map_err(|diag| tactic_diagnostic(record_tactic, Some(current), diag.message))?;
8803                return Err(tactic_diagnostic(
8804                    record_tactic,
8805                    Some(current),
8806                    format!("rewrite did not find {} in the current goal", key_of(from)),
8807                ));
8808            }
8809            Ok(replace_current_goal(
8810                state,
8811                vec![goal_with_context(current, rewritten.node)],
8812                record_tactic,
8813            ))
8814        }
8815        Some("simplify") => {
8816            let parsed = parse_simplify_tactic(args)
8817                .map_err(|reason| tactic_diagnostic(record_tactic, Some(current), reason))?;
8818            let rules = parsed
8819                .rules
8820                .as_deref()
8821                .unwrap_or_else(|| tactic_options.rewrite_rules.as_slice());
8822            if rules.is_empty() {
8823                return Err(tactic_diagnostic(
8824                    record_tactic,
8825                    Some(current),
8826                    "simplify expects at least one configured rewrite rule",
8827                ));
8828            }
8829            let max_steps = parsed
8830                .max_steps
8831                .unwrap_or(tactic_options.simplify_max_steps);
8832            let simplified = simplify_with_options(
8833                &current.goal,
8834                rules,
8835                SimplifyOptions { max_steps },
8836            )
8837            .map_err(|diag| tactic_diagnostic(record_tactic, Some(current), diag.message))?;
8838            Ok(replace_current_goal(
8839                state,
8840                vec![goal_with_context(current, simplified.node)],
8841                record_tactic,
8842            ))
8843        }
8844        Some("smt") => {
8845            if !args.is_empty() {
8846                return Err(tactic_diagnostic(
8847                    record_tactic,
8848                    Some(current),
8849                    "smt expects no arguments; configure the solver through tactic options",
8850                ));
8851            }
8852            let smt_lib = smt_lib_for_goal(&current.goal)
8853                .map_err(|reason| tactic_diagnostic(record_tactic, Some(current), reason))?;
8854            let checked = run_smt_solver(&smt_lib, tactic_options);
8855            if checked.status != SmtStatus::Unsat {
8856                return Err(tactic_diagnostic(
8857                    record_tactic,
8858                    Some(current),
8859                    checked.reason,
8860                ));
8861            }
8862            Ok(replace_current_goal(
8863                state,
8864                Vec::new(),
8865                &smt_trusted_node(tactic_options),
8866            ))
8867        }
8868        Some("atp") => {
8869            if !args.is_empty() {
8870                return Err(tactic_diagnostic(
8871                    record_tactic,
8872                    Some(current),
8873                    "atp expects no tactic arguments",
8874                ));
8875            }
8876            let tptp = goal_to_tptp(current)
8877                .map_err(|diag| tactic_diagnostic(record_tactic, Some(current), diag.message))?;
8878            let proved = run_atp_process(&tptp, &tactic_options.atp)
8879                .map_err(|reason| tactic_diagnostic(record_tactic, Some(current), reason))?;
8880            Ok(replace_current_goal(
8881                state,
8882                Vec::new(),
8883                &Node::List(vec![
8884                    leaf("by"),
8885                    leaf("atp-trusted"),
8886                    Node::Leaf(proved.solver),
8887                ]),
8888            ))
8889        }
8890        Some("exact") => {
8891            if args.len() != 1 {
8892                return Err(tactic_diagnostic(
8893                    record_tactic,
8894                    Some(current),
8895                    "exact expects one term or hypothesis",
8896                ));
8897            }
8898            if !exact_closes_goal(&args[0], current) {
8899                return Err(tactic_diagnostic(
8900                    record_tactic,
8901                    Some(current),
8902                    format!("{} does not prove the current goal", key_of(&args[0])),
8903                ));
8904            }
8905            Ok(replace_current_goal(state, Vec::new(), record_tactic))
8906        }
8907        Some("induction") => {
8908            if args.len() < 2 {
8909                return Err(tactic_diagnostic(
8910                    record_tactic,
8911                    Some(current),
8912                    "induction expects a variable and at least one case",
8913                ));
8914            }
8915            let Node::Leaf(variable) = &args[0] else {
8916                return Err(tactic_diagnostic(
8917                    record_tactic,
8918                    Some(current),
8919                    "induction expects a variable and at least one case",
8920                ));
8921            };
8922            let mut open_goals = Vec::new();
8923            let mut nested_proofs = Vec::new();
8924            for case_node in &args[1..] {
8925                let Node::List(case_children) = case_node else {
8926                    return Err(tactic_diagnostic(
8927                        record_tactic,
8928                        Some(current),
8929                        "induction cases must be `(case <pattern> <tactic>...)` links",
8930                    ));
8931                };
8932                if case_children.len() < 2
8933                    || !matches!(&case_children[0], Node::Leaf(h) if h == "case")
8934                {
8935                    return Err(tactic_diagnostic(
8936                        record_tactic,
8937                        Some(current),
8938                        "induction cases must be `(case <pattern> <tactic>...)` links",
8939                    ));
8940                }
8941                let pattern = &case_children[1];
8942                let case_goal =
8943                    goal_with_context(current, subst(&current.goal, variable, pattern));
8944                let case_tactics = &case_children[2..];
8945                if case_tactics.is_empty() {
8946                    open_goals.push(case_goal);
8947                    continue;
8948                }
8949                let nested = run_tactics_with_options(
8950                    ProofState {
8951                        goals: vec![case_goal],
8952                        proof: Vec::new(),
8953                    },
8954                    case_tactics,
8955                    tactic_options.clone(),
8956                );
8957                if let Some(diag) = nested.diagnostics.first() {
8958                    return Err(diag.clone());
8959                }
8960                open_goals.extend(nested.state.goals);
8961                nested_proofs.extend(nested.state.proof);
8962            }
8963            let mut goals = open_goals;
8964            goals.extend(state.goals.iter().skip(1).cloned());
8965            let mut proof = state.proof.clone();
8966            proof.push(record_tactic.clone());
8967            proof.extend(nested_proofs);
8968            Ok(ProofState { goals, proof })
8969        }
8970        Some(other) => Err(tactic_diagnostic(
8971            record_tactic,
8972            Some(current),
8973            format!("unknown tactic \"{}\"", other),
8974        )),
8975        None => Err(tactic_diagnostic(
8976            record_tactic,
8977            Some(current),
8978            "tactic head must be a symbol",
8979        )),
8980    }
8981}
8982
8983/// Parse a LiNo snippet into tactic links.
8984pub fn parse_tactic_links(text: &str) -> Vec<Node> {
8985    parse_lino(text)
8986        .iter()
8987        .filter(|link_str| {
8988            let s = link_str.trim();
8989            !(s.starts_with("(#") && s.chars().nth(2).map_or(false, |c| c.is_whitespace()))
8990        })
8991        .filter_map(|link_str| {
8992            let toks = tokenize_one(link_str);
8993            let toks = if toks.len() == 1 && toks[0] != "(" && toks[0] != ")" {
8994                vec!["(".to_string(), toks[0].clone(), ")".to_string()]
8995            } else {
8996                toks
8997            };
8998            parse_one(&toks).ok().map(desugar_hoas)
8999        })
9000        .collect()
9001}
9002
9003/// Apply link tactics with configured rewrite rules, stopping at the first failing tactic.
9004pub fn run_tactics_with_options(
9005    state: ProofState,
9006    tactics: &[Node],
9007    options: TacticOptions,
9008) -> TacticRunResult {
9009    let mut next = state;
9010    let mut diagnostics = Vec::new();
9011    for tactic in tactics {
9012        match apply_tactic(&next, tactic, tactic, &options) {
9013            Ok(applied) => next = applied,
9014            Err(diag) => {
9015                diagnostics.push(diag);
9016                break;
9017            }
9018        }
9019    }
9020    TacticRunResult {
9021        state: next,
9022        diagnostics,
9023    }
9024}
9025
9026/// Apply link tactics to a proof state, stopping at the first failing tactic.
9027pub fn run_tactics(state: ProofState, tactics: &[Node]) -> TacticRunResult {
9028    run_tactics_with_options(state, tactics, TacticOptions::default())
9029}
9030
9031// ---------- Mode declarations (issue #43, D15) ----------
9032// `(mode plus +input +input -output)` records the per-argument mode
9033// pattern for relation `plus`. `parse_mode_form` validates the shape and
9034// returns the normalised `(name, flags)` pair; `check_mode_at_call`
9035// inspects every call against any registered declaration. Both surface
9036// errors as panics with a recognisable prefix so the existing diagnostic
9037// dispatch in `decode_panic_payload` can map them to E030 / E031.
9038
9039fn parse_mode_form(children: &[Node]) -> Option<(String, Vec<ModeFlag>)> {
9040    // Caller already verified `children[0]` is the leaf `mode`.
9041    if children.len() < 2 {
9042        return None;
9043    }
9044    let name = match &children[1] {
9045        Node::Leaf(s) => s.clone(),
9046        _ => panic!("Mode declaration error: relation name must be a bare symbol"),
9047    };
9048    if children.len() < 3 {
9049        panic!(
9050            "Mode declaration error: declaration for \"{}\" must list at least one mode flag",
9051            name
9052        );
9053    }
9054    let mut flags = Vec::with_capacity(children.len() - 2);
9055    for child in &children[2..] {
9056        match child {
9057            Node::Leaf(token) => match ModeFlag::from_token(token) {
9058                Some(flag) => flags.push(flag),
9059                None => panic!(
9060                    "Mode declaration error: declaration for \"{}\": unknown flag \"{}\" (expected +input, -output, or *either)",
9061                    name, token
9062                ),
9063            },
9064            _ => panic!(
9065                "Mode declaration error: declaration for \"{}\" contains a non-token flag",
9066                name
9067            ),
9068        }
9069    }
9070    Some((name, flags))
9071}
9072
9073fn is_ground_for_mode(arg: &Node, env: &Env) -> bool {
9074    match arg {
9075        Node::Leaf(s) => {
9076            if is_num(s) {
9077                return true;
9078            }
9079            env_can_evaluate_name(env, s)
9080        }
9081        Node::List(_) => !has_unresolved_free_variables(arg, env),
9082    }
9083}
9084
9085fn check_mode_at_call(name: &str, args: &[Node], env: &Env) {
9086    let flags = match env.modes.get(name) {
9087        Some(f) => f.clone(),
9088        None => return,
9089    };
9090    if args.len() != flags.len() {
9091        panic!(
9092            "Mode mismatch: \"{}\" expected {} argument{}, got {}",
9093            name,
9094            flags.len(),
9095            if flags.len() == 1 { "" } else { "s" },
9096            args.len()
9097        );
9098    }
9099    for (i, flag) in flags.iter().enumerate() {
9100        if *flag == ModeFlag::In && !is_ground_for_mode(&args[i], env) {
9101            panic!(
9102                "Mode mismatch: \"{}\" argument {} (+input) is not ground",
9103                name,
9104                i + 1
9105            );
9106        }
9107    }
9108}
9109
9110// ---------- Relation declarations & totality (issue #44, D12) ----------
9111// Mirrors the JavaScript helpers in `js/src/rml-links.mjs`. The
9112// `(relation <name> <clause>...)` form stores the clause list per
9113// relation, and the single-rule shorthand
9114// `(relation <name> (<name> arg...) body)` is normalized to that clause
9115// shape. `(total <name>)` triggers `is_total`, and the same helper is
9116// exported for programmatic callers.
9117
9118fn is_relation_clause_head(node: &Node, name: &str) -> bool {
9119    match node {
9120        Node::List(items) if items.len() >= 2 => match &items[0] {
9121            Node::Leaf(head) => head == name,
9122            _ => false,
9123        },
9124        _ => false,
9125    }
9126}
9127
9128fn parse_relation_form(children: &[Node]) -> (String, Vec<Node>) {
9129    // Caller already verified `children[0]` is the leaf `relation`.
9130    if children.len() < 2 {
9131        panic!("Relation declaration error: relation name must be a bare symbol");
9132    }
9133    let name = match &children[1] {
9134        Node::Leaf(s) => s.clone(),
9135        _ => panic!("Relation declaration error: relation name must be a bare symbol"),
9136    };
9137    if children.len() < 3 {
9138        panic!(
9139            "Relation declaration error: declaration for \"{}\" must list at least one clause",
9140            name
9141        );
9142    }
9143    if children.len() == 4 {
9144        let pattern = &children[2];
9145        let body = &children[3];
9146        if is_relation_clause_head(pattern, &name) && !is_relation_clause_head(body, &name) {
9147            if let Node::List(items) = pattern {
9148                let mut clause = items.clone();
9149                clause.push(body.clone());
9150                return (name, vec![Node::List(clause)]);
9151            }
9152        }
9153    }
9154    let mut clauses = Vec::with_capacity(children.len() - 2);
9155    for (idx, clause) in children[2..].iter().enumerate() {
9156        match clause {
9157            Node::List(items) if items.len() >= 2 => match &items[0] {
9158                Node::Leaf(head) if *head == name => {
9159                    clauses.push(clause.clone());
9160                }
9161                _ => panic!(
9162                    "Relation declaration error: declaration for \"{}\": clause {} must be a list whose head is \"{}\"",
9163                    name,
9164                    idx + 1,
9165                    name
9166                ),
9167            },
9168            _ => panic!(
9169                "Relation declaration error: declaration for \"{}\": clause {} must be a list whose head is \"{}\"",
9170                name,
9171                idx + 1,
9172                name
9173            ),
9174        }
9175    }
9176    (name, clauses)
9177}
9178
9179fn is_strict_subterm(inner: &Node, outer: &Node) -> bool {
9180    if let Node::List(children) = outer {
9181        for child in children {
9182            if inner == child {
9183                return true;
9184            }
9185            if is_strict_subterm(inner, child) {
9186                return true;
9187            }
9188        }
9189    }
9190    false
9191}
9192
9193fn collect_recursive_calls(node: &Node, rel_name: &str, is_head: bool, out: &mut Vec<Node>) {
9194    if let Node::List(children) = node {
9195        if !is_head {
9196            if let Some(Node::Leaf(head)) = children.first() {
9197                if head == rel_name {
9198                    out.push(node.clone());
9199                }
9200            }
9201        }
9202        for (i, child) in children.iter().enumerate() {
9203            // Skip the head leaf — only descend into argument positions.
9204            if i == 0 {
9205                if let Node::Leaf(_) = child {
9206                    continue;
9207                }
9208            }
9209            collect_recursive_calls(child, rel_name, false, out);
9210        }
9211    }
9212}
9213
9214/// Per-clause / per-call totality diagnostic returned by [`is_total`].
9215#[derive(Debug, Clone, PartialEq, Eq)]
9216pub struct TotalityDiagnostic {
9217    pub code: String,
9218    pub message: String,
9219}
9220
9221/// Outcome of a totality check.
9222#[derive(Debug, Clone, PartialEq, Eq)]
9223pub struct TotalityResult {
9224    pub ok: bool,
9225    pub diagnostics: Vec<TotalityDiagnostic>,
9226}
9227
9228fn check_recursive_decrease(
9229    call: &Node,
9230    head_args: &[Node],
9231    flags: &[ModeFlag],
9232    rel_name: &str,
9233) -> Option<String> {
9234    let call_args: Vec<Node> = match call {
9235        Node::List(items) if !items.is_empty() => items[1..].to_vec(),
9236        _ => return Some(format!("recursive call `{}` has no arguments", key_of(call))),
9237    };
9238    let input_indices: Vec<usize> = flags
9239        .iter()
9240        .enumerate()
9241        .filter(|(_, f)| **f == ModeFlag::In)
9242        .map(|(i, _)| i)
9243        .collect();
9244
9245    let pairs: Vec<(&Node, &Node)> = if call_args.len() == flags.len() {
9246        input_indices
9247            .iter()
9248            .map(|&i| (&call_args[i], &head_args[i]))
9249            .collect()
9250    } else if call_args.len() == input_indices.len() {
9251        input_indices
9252            .iter()
9253            .enumerate()
9254            .map(|(j, &i)| (&call_args[j], &head_args[i]))
9255            .collect()
9256    } else {
9257        return Some(format!(
9258            "recursive call `{}` has {} argument{}, expected {} (or {} input{})",
9259            key_of(call),
9260            call_args.len(),
9261            if call_args.len() == 1 { "" } else { "s" },
9262            flags.len(),
9263            input_indices.len(),
9264            if input_indices.len() == 1 { "" } else { "s" },
9265        ));
9266    };
9267
9268    if input_indices.is_empty() {
9269        return Some(format!(
9270            "relation \"{}\" has no `+input` slot, so structural decrease is unverifiable",
9271            rel_name
9272        ));
9273    }
9274    for (call_arg, head_arg) in &pairs {
9275        if is_strict_subterm(call_arg, head_arg) {
9276            return None;
9277        }
9278    }
9279    let head_with_args = {
9280        let mut items = Vec::with_capacity(head_args.len() + 1);
9281        items.push(Node::Leaf(rel_name.to_string()));
9282        items.extend(head_args.iter().cloned());
9283        Node::List(items)
9284    };
9285    Some(format!(
9286        "recursive call `{}` does not structurally decrease any `+input` slot of `{}`",
9287        key_of(call),
9288        key_of(&head_with_args)
9289    ))
9290}
9291
9292/// Public totality checker. Returns a [`TotalityResult`] with structured
9293/// diagnostics; callers can either propagate them as-is or convert each
9294/// entry into a [`Diagnostic`] for the existing pipeline. The mirrored JS
9295/// helper is exported under the same name (`isTotal`) and produces an
9296/// equivalent shape so downstream tools see consistent output.
9297pub fn is_total(env: &Env, rel_name: &str) -> TotalityResult {
9298    let mut diagnostics: Vec<TotalityDiagnostic> = Vec::new();
9299    let flags = match env.modes.get(rel_name) {
9300        Some(f) => f.clone(),
9301        None => {
9302            diagnostics.push(TotalityDiagnostic {
9303                code: "E032".to_string(),
9304                message: format!(
9305                    "Totality check for \"{}\": no `(mode {} ...)` declaration found",
9306                    rel_name, rel_name
9307                ),
9308            });
9309            return TotalityResult {
9310                ok: false,
9311                diagnostics,
9312            };
9313        }
9314    };
9315    let clauses: Vec<Node> = match env.relations.get(rel_name) {
9316        Some(c) if !c.is_empty() => c.clone(),
9317        _ => {
9318            diagnostics.push(TotalityDiagnostic {
9319                code: "E032".to_string(),
9320                message: format!(
9321                    "Totality check for \"{}\": no `(relation {} ...)` clauses found",
9322                    rel_name, rel_name
9323                ),
9324            });
9325            return TotalityResult {
9326                ok: false,
9327                diagnostics,
9328            };
9329        }
9330    };
9331    for (ci, clause) in clauses.iter().enumerate() {
9332        let head_args: Vec<Node> = match clause {
9333            Node::List(items) if !items.is_empty() => items[1..].to_vec(),
9334            _ => continue,
9335        };
9336        if head_args.len() != flags.len() {
9337            diagnostics.push(TotalityDiagnostic {
9338                code: "E032".to_string(),
9339                message: format!(
9340                    "Totality check for \"{}\": clause {} `{}` has {} argument{}, mode declares {}",
9341                    rel_name,
9342                    ci + 1,
9343                    key_of(clause),
9344                    head_args.len(),
9345                    if head_args.len() == 1 { "" } else { "s" },
9346                    flags.len(),
9347                ),
9348            });
9349            continue;
9350        }
9351        let mut calls: Vec<Node> = Vec::new();
9352        collect_recursive_calls(clause, rel_name, true, &mut calls);
9353        for call in &calls {
9354            if let Some(reason) = check_recursive_decrease(call, &head_args, &flags, rel_name) {
9355                diagnostics.push(TotalityDiagnostic {
9356                    code: "E032".to_string(),
9357                    message: format!(
9358                        "Totality check for \"{}\": clause {} `{}` — {}",
9359                        rel_name,
9360                        ci + 1,
9361                        key_of(clause),
9362                        reason
9363                    ),
9364                });
9365            }
9366        }
9367    }
9368    TotalityResult {
9369        ok: diagnostics.is_empty(),
9370        diagnostics,
9371    }
9372}
9373
9374// ---------- Definitions & termination checking (issue #49, D13) ----------
9375// `(define <name> [(measure (lex <slot>...))] (case <pattern-args> <body>) ...)`
9376// records a recursive definition keyed by `<name>`. `is_terminating(env, name)`
9377// then verifies that every recursive call structurally decreases either the
9378// first argument (default) or the explicit lexicographic measure. Mirrors
9379// the JS export `isTerminating` and uses error code E035.
9380
9381/// Per-clause / per-call termination diagnostic returned by [`is_terminating`].
9382#[derive(Debug, Clone, PartialEq, Eq)]
9383pub struct TerminationDiagnostic {
9384    pub code: String,
9385    pub message: String,
9386}
9387
9388/// Outcome of a termination check.
9389#[derive(Debug, Clone, PartialEq, Eq)]
9390pub struct TerminationResult {
9391    pub ok: bool,
9392    pub diagnostics: Vec<TerminationDiagnostic>,
9393}
9394
9395fn parse_define_form(children: &[Node]) -> DefineDecl {
9396    // Caller already verified `children[0]` is the leaf `define`.
9397    if children.len() < 2 {
9398        panic!("Termination check error: Define declaration: name must be a bare symbol");
9399    }
9400    let name = match &children[1] {
9401        Node::Leaf(s) => s.clone(),
9402        _ => panic!("Termination check error: Define declaration: name must be a bare symbol"),
9403    };
9404    if children.len() < 3 {
9405        panic!(
9406            "Termination check error: Define declaration for \"{}\" must list at least one `(case ...)` clause",
9407            name
9408        );
9409    }
9410    let mut measure: Option<DefineMeasure> = None;
9411    let mut clauses: Vec<DefineClause> = Vec::new();
9412    for child in &children[2..] {
9413        match child {
9414            Node::List(items) if !items.is_empty() => match &items[0] {
9415                Node::Leaf(head) if head == "measure" => {
9416                    if measure.is_some() {
9417                        panic!(
9418                            "Termination check error: Define declaration for \"{}\": only one `(measure ...)` clause is allowed",
9419                            name
9420                        );
9421                    }
9422                    if items.len() != 2 {
9423                        panic!(
9424                            "Termination check error: Define declaration for \"{}\": `(measure ...)` body must be `(lex <slot>...)`",
9425                            name
9426                        );
9427                    }
9428                    let body = &items[1];
9429                    let lex_items = match body {
9430                        Node::List(b) if b.len() >= 2 => match &b[0] {
9431                            Node::Leaf(h) if h == "lex" => &b[1..],
9432                            _ => panic!(
9433                                "Termination check error: Define declaration for \"{}\": `(measure ...)` body must be `(lex <slot>...)`",
9434                                name
9435                            ),
9436                        },
9437                        _ => panic!(
9438                            "Termination check error: Define declaration for \"{}\": `(measure ...)` body must be `(lex <slot>...)`",
9439                            name
9440                        ),
9441                    };
9442                    let mut slots: Vec<usize> = Vec::with_capacity(lex_items.len());
9443                    for item in lex_items {
9444                        let raw = match item {
9445                            Node::Leaf(s) => s.clone(),
9446                            _ => panic!(
9447                                "Termination check error: Define declaration for \"{}\": measure slot must be a positive integer",
9448                                name
9449                            ),
9450                        };
9451                        let parsed: Result<usize, _> = raw.parse();
9452                        match parsed {
9453                            Ok(n) if n >= 1 => slots.push(n - 1),
9454                            _ => panic!(
9455                                "Termination check error: Define declaration for \"{}\": measure slot must be a positive integer",
9456                                name
9457                            ),
9458                        }
9459                    }
9460                    measure = Some(DefineMeasure::Lex(slots));
9461                }
9462                Node::Leaf(head) if head == "case" => {
9463                    if items.len() != 3 {
9464                        panic!(
9465                            "Termination check error: Define declaration for \"{}\": `(case <pattern-args> <body>)` clause must have exactly two children",
9466                            name
9467                        );
9468                    }
9469                    let pattern = match &items[1] {
9470                        Node::List(p) => p.clone(),
9471                        // The upstream `links-notation` parser collapses
9472                        // single-element parens (`(zero)` → `zero`), so a
9473                        // surface pattern with one argument arrives here as a
9474                        // `Leaf`. Treat it as the equivalent one-element list
9475                        // so `(case (zero) zero)` parses the same way it does
9476                        // in the JS implementation.
9477                        Node::Leaf(_) => vec![items[1].clone()],
9478                    };
9479                    clauses.push(DefineClause {
9480                        pattern,
9481                        body: items[2].clone(),
9482                    });
9483                }
9484                _ => panic!(
9485                    "Termination check error: Define declaration for \"{}\": unexpected clause `{}` (expected `(measure ...)` or `(case ...)`)",
9486                    name,
9487                    key_of(child)
9488                ),
9489            },
9490            _ => panic!(
9491                "Termination check error: Define declaration for \"{}\": unexpected clause `{}` (expected `(measure ...)` or `(case ...)`)",
9492                name,
9493                key_of(child)
9494            ),
9495        }
9496    }
9497    if clauses.is_empty() {
9498        panic!(
9499            "Termination check error: Define declaration for \"{}\" must list at least one `(case ...)` clause",
9500            name
9501        );
9502    }
9503    DefineDecl {
9504        name,
9505        measure,
9506        clauses,
9507    }
9508}
9509
9510fn check_define_decrease(
9511    call: &Node,
9512    pattern: &[Node],
9513    measure: &Option<DefineMeasure>,
9514    def_name: &str,
9515) -> Option<String> {
9516    let call_args: Vec<Node> = match call {
9517        Node::List(items) if !items.is_empty() => items[1..].to_vec(),
9518        _ => return Some(format!("recursive call `{}` has no arguments", key_of(call))),
9519    };
9520    if call_args.len() != pattern.len() {
9521        return Some(format!(
9522            "recursive call `{}` has {} argument{}, clause pattern declares {}",
9523            key_of(call),
9524            call_args.len(),
9525            if call_args.len() == 1 { "" } else { "s" },
9526            pattern.len(),
9527        ));
9528    }
9529    if let Some(DefineMeasure::Lex(slots)) = measure {
9530        for &slot in slots {
9531            if slot >= pattern.len() {
9532                return Some(format!(
9533                    "measure slot {} is out of range for {}-argument clause",
9534                    slot + 1,
9535                    pattern.len(),
9536                ));
9537            }
9538        }
9539        for &slot in slots {
9540            let call_arg = &call_args[slot];
9541            let pat_arg = &pattern[slot];
9542            if is_strict_subterm(call_arg, pat_arg) {
9543                return None;
9544            }
9545            if !is_node_equal(call_arg, pat_arg) {
9546                return Some(format!(
9547                    "recursive call `{}` does not lexicographically decrease the declared measure",
9548                    key_of(call),
9549                ));
9550            }
9551        }
9552        return Some(format!(
9553            "recursive call `{}` does not lexicographically decrease the declared measure",
9554            key_of(call),
9555        ));
9556    }
9557    if pattern.is_empty() {
9558        return Some(format!(
9559            "definition \"{}\" has no arguments, so structural decrease is unverifiable",
9560            def_name
9561        ));
9562    }
9563    if is_strict_subterm(&call_args[0], &pattern[0]) {
9564        return None;
9565    }
9566    let head_with_pattern = {
9567        let mut items = Vec::with_capacity(pattern.len() + 1);
9568        items.push(Node::Leaf(def_name.to_string()));
9569        items.extend(pattern.iter().cloned());
9570        Node::List(items)
9571    };
9572    Some(format!(
9573        "recursive call `{}` does not structurally decrease the first argument of `{}`",
9574        key_of(call),
9575        key_of(&head_with_pattern)
9576    ))
9577}
9578
9579fn is_node_equal(a: &Node, b: &Node) -> bool {
9580    a == b
9581}
9582
9583/// Public termination checker. Returns a [`TerminationResult`] with
9584/// structured diagnostics; callers can either propagate them as-is or
9585/// convert each entry into a [`Diagnostic`] for the existing pipeline. The
9586/// mirrored JS helper is exported under the same name (`isTerminating`).
9587pub fn is_terminating(env: &Env, def_name: &str) -> TerminationResult {
9588    let mut diagnostics: Vec<TerminationDiagnostic> = Vec::new();
9589    let decl = match env.definitions.get(def_name) {
9590        Some(d) => d.clone(),
9591        None => {
9592            diagnostics.push(TerminationDiagnostic {
9593                code: "E035".to_string(),
9594                message: format!(
9595                    "Termination check for \"{}\": no `(define {} ...)` declaration found",
9596                    def_name, def_name
9597                ),
9598            });
9599            return TerminationResult {
9600                ok: false,
9601                diagnostics,
9602            };
9603        }
9604    };
9605    for (ci, clause) in decl.clauses.iter().enumerate() {
9606        let mut calls: Vec<Node> = Vec::new();
9607        collect_recursive_calls(&clause.body, def_name, false, &mut calls);
9608        for call in &calls {
9609            if let Some(reason) =
9610                check_define_decrease(call, &clause.pattern, &decl.measure, def_name)
9611            {
9612                let case_node = Node::List(vec![
9613                    Node::Leaf("case".to_string()),
9614                    Node::List(clause.pattern.clone()),
9615                    clause.body.clone(),
9616                ]);
9617                diagnostics.push(TerminationDiagnostic {
9618                    code: "E035".to_string(),
9619                    message: format!(
9620                        "Termination check for \"{}\": clause {} `{}` — {}",
9621                        def_name,
9622                        ci + 1,
9623                        key_of(&case_node),
9624                        reason
9625                    ),
9626                });
9627            }
9628        }
9629    }
9630    TerminationResult {
9631        ok: diagnostics.is_empty(),
9632        diagnostics,
9633    }
9634}
9635
9636// ---------- Coverage checking (issue #46, D14) ----------
9637// Mirrors the JavaScript `isCovered` helper. For every `+input` slot of the
9638// named relation, the union of clause patterns at that slot must exhaust
9639// every constructor of the slot's inductive type. Wildcard variables
9640// (lowercase symbols not registered in the env) cover all constructors;
9641// slots whose inductive type cannot be inferred are skipped. A missing
9642// constructor produces an `E037` diagnostic with an example pattern.
9643
9644/// Structured coverage diagnostic mirroring [`TotalityDiagnostic`].
9645#[derive(Debug, Clone, PartialEq, Eq)]
9646pub struct CoverageDiagnostic {
9647    pub code: String,
9648    pub message: String,
9649}
9650
9651/// Outcome of a coverage check.
9652#[derive(Debug, Clone, PartialEq, Eq)]
9653pub struct CoverageResult {
9654    pub ok: bool,
9655    pub diagnostics: Vec<CoverageDiagnostic>,
9656}
9657
9658fn inductive_type_of_constructor(env: &Env, ctor_name: &str) -> Option<String> {
9659    for (type_name, decl) in &env.inductives {
9660        for ctor in &decl.constructors {
9661            if ctor.name == ctor_name {
9662                return Some(type_name.clone());
9663            }
9664        }
9665    }
9666    None
9667}
9668
9669fn is_wildcard_pattern(pat: &Node, env: &Env) -> bool {
9670    match pat {
9671        Node::Leaf(s) => {
9672            if is_num(s) {
9673                return false;
9674            }
9675            if non_variable_token(s) {
9676                return false;
9677            }
9678            inductive_type_of_constructor(env, s).is_none()
9679        }
9680        _ => false,
9681    }
9682}
9683
9684fn pattern_constructor_head(pat: &Node, env: &Env) -> Option<String> {
9685    match pat {
9686        Node::Leaf(s) => {
9687            if inductive_type_of_constructor(env, s).is_some() {
9688                Some(s.clone())
9689            } else {
9690                None
9691            }
9692        }
9693        Node::List(items) => {
9694            if let Some(Node::Leaf(head)) = items.first() {
9695                if inductive_type_of_constructor(env, head).is_some() {
9696                    return Some(head.clone());
9697                }
9698            }
9699            None
9700        }
9701    }
9702}
9703
9704fn infer_slot_type(env: &Env, clauses: &[Node], slot_index: usize) -> Option<String> {
9705    for clause in clauses {
9706        if let Node::List(items) = clause {
9707            if let Some(pat) = items.get(slot_index + 1) {
9708                if let Some(head) = pattern_constructor_head(pat, env) {
9709                    return inductive_type_of_constructor(env, &head);
9710                }
9711            }
9712        }
9713    }
9714    None
9715}
9716
9717fn example_constructor_pattern(ctor: &ConstructorDecl) -> String {
9718    if ctor.params.is_empty() {
9719        ctor.name.clone()
9720    } else {
9721        let placeholders = " _".repeat(ctor.params.len());
9722        format!("({}{})", ctor.name, placeholders)
9723    }
9724}
9725
9726/// Public coverage checker. Mirrors `isCovered` in the JavaScript
9727/// implementation and returns identical diagnostic shapes so external
9728/// tooling sees consistent output across runtimes.
9729pub fn is_covered(env: &Env, rel_name: &str) -> CoverageResult {
9730    let mut diagnostics: Vec<CoverageDiagnostic> = Vec::new();
9731    let flags = match env.modes.get(rel_name) {
9732        Some(f) => f.clone(),
9733        None => {
9734            diagnostics.push(CoverageDiagnostic {
9735                code: "E037".to_string(),
9736                message: format!(
9737                    "Coverage check for \"{}\": no `(mode {} ...)` declaration found",
9738                    rel_name, rel_name
9739                ),
9740            });
9741            return CoverageResult {
9742                ok: false,
9743                diagnostics,
9744            };
9745        }
9746    };
9747    let clauses: Vec<Node> = match env.relations.get(rel_name) {
9748        Some(c) if !c.is_empty() => c.clone(),
9749        _ => {
9750            diagnostics.push(CoverageDiagnostic {
9751                code: "E037".to_string(),
9752                message: format!(
9753                    "Coverage check for \"{}\": no `(relation {} ...)` clauses found",
9754                    rel_name, rel_name
9755                ),
9756            });
9757            return CoverageResult {
9758                ok: false,
9759                diagnostics,
9760            };
9761        }
9762    };
9763    for (i, flag) in flags.iter().enumerate() {
9764        if *flag != ModeFlag::In {
9765            continue;
9766        }
9767        let slot_patterns: Vec<Node> = clauses
9768            .iter()
9769            .filter_map(|c| match c {
9770                Node::List(items) => items.get(i + 1).cloned(),
9771                _ => None,
9772            })
9773            .collect();
9774        if slot_patterns.iter().any(|p| is_wildcard_pattern(p, env)) {
9775            continue;
9776        }
9777        let type_name = match infer_slot_type(env, &clauses, i) {
9778            Some(t) => t,
9779            None => continue,
9780        };
9781        let decl = match env.inductives.get(&type_name) {
9782            Some(d) => d.clone(),
9783            None => continue,
9784        };
9785        let mut covered: Vec<String> = Vec::new();
9786        for pat in &slot_patterns {
9787            if let Some(head) = pattern_constructor_head(pat, env) {
9788                if !covered.contains(&head) {
9789                    covered.push(head);
9790                }
9791            }
9792        }
9793        let missing: Vec<&ConstructorDecl> = decl
9794            .constructors
9795            .iter()
9796            .filter(|c| !covered.contains(&c.name))
9797            .collect();
9798        if missing.is_empty() {
9799            continue;
9800        }
9801        let examples: Vec<String> = missing
9802            .iter()
9803            .map(|c| example_constructor_pattern(c))
9804            .collect();
9805        let plural = if missing.len() == 1 { "" } else { "s" };
9806        diagnostics.push(CoverageDiagnostic {
9807            code: "E037".to_string(),
9808            message: format!(
9809                "Coverage check for \"{}\": +input slot {} (type \"{}\") missing case{} for constructor{} {}",
9810                rel_name,
9811                i + 1,
9812                type_name,
9813                plural,
9814                plural,
9815                examples.join(", ")
9816            ),
9817        });
9818    }
9819    CoverageResult {
9820        ok: diagnostics.is_empty(),
9821        diagnostics,
9822    }
9823}
9824
9825// ---------- World declarations (issue #54, D16) ----------
9826// `(world plus (Natural))` records that the relation `plus` may have
9827// arguments containing only the listed constants free (in addition to
9828// the relation's own argument variables and any locally-bound names).
9829// `parse_world_form` validates the shape and returns the normalised
9830// `(name, allowed_constants)` pair; `check_world_at_call` inspects every
9831// call against any registered declaration. Both surface errors as panics
9832// with a recognisable prefix so the existing diagnostic dispatch in
9833// `decode_panic_payload` can map them to E034.
9834
9835fn parse_world_form(children: &[Node]) -> Option<(String, Vec<String>)> {
9836    // Caller already verified `children[0]` is the leaf `world`.
9837    if children.len() < 2 {
9838        return None;
9839    }
9840    let name = match &children[1] {
9841        Node::Leaf(s) => s.clone(),
9842        _ => panic!("World declaration error: relation name must be a bare symbol"),
9843    };
9844    if children.len() != 3 {
9845        panic!(
9846            "World declaration error: declaration for \"{}\" must have shape `(world {} (<const>...))`",
9847            name, name
9848        );
9849    }
9850    let allowed: Vec<String> = match &children[2] {
9851        Node::List(items) => {
9852            let mut consts = Vec::with_capacity(items.len());
9853            for item in items {
9854                match item {
9855                    Node::Leaf(s) => consts.push(s.clone()),
9856                    _ => panic!(
9857                        "World declaration error: declaration for \"{}\": each allowed constant must be a bare symbol",
9858                        name
9859                    ),
9860                }
9861            }
9862            consts
9863        }
9864        // The LiNo parser collapses a single-element paren group such as
9865        // `(Natural)` into the bare leaf `Natural`, so accept a lone leaf
9866        // here as a one-constant allow-list.
9867        Node::Leaf(s) => vec![s.clone()],
9868    };
9869    Some((name, allowed))
9870}
9871
9872// Walk an argument expression and collect every free constant — i.e.
9873// every leaf symbol that is not numeric, not a reserved keyword, and is
9874// not bound by an enclosing `lambda`/`Pi`/`fresh` binder appearing
9875// inside the same argument. The collected names are matched against the
9876// world's `allowed` list to surface E033 violations.
9877fn collect_free_constants(node: &Node, bound: &mut HashSet<String>, out: &mut Vec<String>) {
9878    match node {
9879        Node::Leaf(s) => {
9880            if is_num(s) || non_variable_token(s) {
9881                return;
9882            }
9883            if bound.contains(s) {
9884                return;
9885            }
9886            if !out.contains(s) {
9887                out.push(s.clone());
9888            }
9889        }
9890        Node::List(items) => {
9891            // Recognise local binders so their bound name does not count
9892            // as a free constant inside the body.
9893            if items.len() >= 3 {
9894                if let Node::Leaf(head) = &items[0] {
9895                    if head == "lambda" || head == "Pi" {
9896                        if let Node::List(binder) = &items[1] {
9897                            if binder.len() == 2 {
9898                                if let Node::Leaf(var) = &binder[1] {
9899                                    let was_bound = bound.contains(var);
9900                                    if let Node::Leaf(ty) = &binder[0] {
9901                                        if !is_num(ty) && !non_variable_token(ty) && !bound.contains(ty) && !out.contains(ty) {
9902                                            out.push(ty.clone());
9903                                        }
9904                                    } else {
9905                                        collect_free_constants(&binder[0], bound, out);
9906                                    }
9907                                    bound.insert(var.clone());
9908                                    for child in &items[2..] {
9909                                        collect_free_constants(child, bound, out);
9910                                    }
9911                                    if !was_bound {
9912                                        bound.remove(var);
9913                                    }
9914                                    return;
9915                                }
9916                            }
9917                        }
9918                    }
9919                    if head == "fresh" && items.len() == 4 {
9920                        if let (Node::Leaf(var), Node::Leaf(in_kw)) = (&items[1], &items[2]) {
9921                            if in_kw == "in" {
9922                                let was_bound = bound.contains(var);
9923                                bound.insert(var.clone());
9924                                collect_free_constants(&items[3], bound, out);
9925                                if !was_bound {
9926                                    bound.remove(var);
9927                                }
9928                                return;
9929                            }
9930                        }
9931                    }
9932                }
9933            }
9934            for child in items {
9935                collect_free_constants(child, bound, out);
9936            }
9937        }
9938    }
9939}
9940
9941fn check_world_at_call(name: &str, args: &[Node], env: &Env) {
9942    let allowed = match env.worlds.get(name) {
9943        Some(a) => a.clone(),
9944        None => return,
9945    };
9946    // Treat the relation's own name and the declared allowed constants
9947    // as the world's vocabulary. Other free constants raise E033.
9948    let mut violations: Vec<String> = Vec::new();
9949    for arg in args {
9950        let mut bound: HashSet<String> = HashSet::new();
9951        let mut found: Vec<String> = Vec::new();
9952        collect_free_constants(arg, &mut bound, &mut found);
9953        for sym in found {
9954            if sym == name {
9955                continue;
9956            }
9957            if allowed.iter().any(|a| a == &sym) {
9958                continue;
9959            }
9960            // Names that are themselves declared in the world list of
9961            // any other relation are also treated as part of the
9962            // ambient vocabulary — only truly unknown free constants
9963            // should fail. We keep the check strict for now: only the
9964            // explicit allow-list and the relation's own name are OK.
9965            if !violations.contains(&sym) {
9966                violations.push(sym);
9967            }
9968        }
9969    }
9970    if !violations.is_empty() {
9971        let listed = violations
9972            .iter()
9973            .map(|s| format!("\"{}\"", s))
9974            .collect::<Vec<_>>()
9975            .join(", ");
9976        panic!(
9977            "World violation: \"{}\" argument contains free constant{} {} not in declared world",
9978            name,
9979            if violations.len() == 1 { "" } else { "s" },
9980            listed
9981        );
9982    }
9983}
9984
9985// ---------- Inductive declarations (issue #45, D10) ----------
9986// Mirrors the JavaScript helpers in `js/src/rml-links.mjs`. The
9987// `(inductive Name (constructor …) …)` form records an inductive
9988// datatype, installs every constructor, and synthesises the
9989// eliminator `Name-rec` with a dependent Pi-type. Errors panic with
9990// `Inductive declaration error:` so `decode_panic_payload` maps them
9991// to E033.
9992
9993fn is_pi_sig(node: &Node) -> bool {
9994    matches!(node, Node::List(items)
9995        if items.len() == 3
9996            && matches!(&items[0], Node::Leaf(h) if h == "Pi"))
9997}
9998
9999// Walk a `(Pi (A x) (Pi (B y) … R))` chain into binder pairs and the result.
10000fn flatten_pi(type_node: &Node) -> Option<(Vec<(String, Node)>, Node)> {
10001    let mut params: Vec<(String, Node)> = Vec::new();
10002    let mut current = type_node.clone();
10003    while is_pi_sig(&current) {
10004        let items = match &current {
10005            Node::List(items) => items.clone(),
10006            _ => return None,
10007        };
10008        let bindings = parse_bindings(&items[1])?;
10009        if bindings.is_empty() {
10010            return None;
10011        }
10012        for (name, type_str) in bindings {
10013            // parse_bindings returns the type as a string key — recover the
10014            // original type node from the binding form so a bare leaf stays
10015            // a leaf and a complex Pi-type round-trips structurally.
10016            let binding_node = &items[1];
10017            let type_node = recover_binding_type(binding_node, &name).unwrap_or(Node::Leaf(type_str));
10018            params.push((name, type_node));
10019        }
10020        current = items[2].clone();
10021    }
10022    Some((params, current))
10023}
10024
10025// Pull the type-side of a `(A x)` (or its parsed equivalents) back as a Node.
10026// `parse_bindings` flattens to a String type key, but for Pi-construction
10027// we need to preserve list shapes such as `(Pi (Natural _) (Type 0))`.
10028fn recover_binding_type(binding: &Node, param_name: &str) -> Option<Node> {
10029    match binding {
10030        Node::List(items) if items.len() == 2 => {
10031            if let Node::Leaf(name) = &items[1] {
10032                if name == param_name {
10033                    return Some(items[0].clone());
10034                }
10035            }
10036            if let Node::Leaf(name) = &items[0] {
10037                if name == param_name {
10038                    return Some(items[1].clone());
10039                }
10040            }
10041            None
10042        }
10043        _ => None,
10044    }
10045}
10046
10047// Build a chain of nested Pi nodes from a binder list and a final result.
10048fn build_pi(params: &[(String, Node)], result: Node) -> Node {
10049    let mut out = result;
10050    for (name, ty) in params.iter().rev() {
10051        out = Node::List(vec![
10052            Node::Leaf("Pi".to_string()),
10053            Node::List(vec![ty.clone(), Node::Leaf(name.clone())]),
10054            out,
10055        ]);
10056    }
10057    out
10058}
10059
10060fn parse_constructor_clause(clause: &Node, type_name: &str) -> ConstructorDecl {
10061    let items = match clause {
10062        Node::List(items) if items.len() == 2 => items,
10063        _ => panic!(
10064            "Inductive declaration error: each clause must be `(constructor <name>)` or `(constructor (<name> <pi-type>))`"
10065        ),
10066    };
10067    match &items[0] {
10068        Node::Leaf(h) if h == "constructor" => {}
10069        _ => panic!(
10070            "Inductive declaration error: each clause must be `(constructor <name>)` or `(constructor (<name> <pi-type>))`"
10071        ),
10072    }
10073    match &items[1] {
10074        Node::Leaf(name) => ConstructorDecl {
10075            name: name.clone(),
10076            params: Vec::new(),
10077            typ: Node::Leaf(type_name.to_string()),
10078        },
10079        Node::List(inner) if inner.len() == 2 => {
10080            let name = match &inner[0] {
10081                Node::Leaf(s) => s.clone(),
10082                _ => panic!(
10083                    "Inductive declaration error: malformed constructor clause `{}`",
10084                    key_of(clause)
10085                ),
10086            };
10087            if !is_pi_sig(&inner[1]) {
10088                panic!(
10089                    "Inductive declaration error: malformed constructor clause `{}`",
10090                    key_of(clause)
10091                );
10092            }
10093            let (params, result) = match flatten_pi(&inner[1]) {
10094                Some(parts) => parts,
10095                None => panic!(
10096                    "Inductive declaration error: constructor \"{}\" has malformed Pi-type `{}`",
10097                    name,
10098                    key_of(&inner[1])
10099                ),
10100            };
10101            match &result {
10102                Node::Leaf(r) if r == type_name => {}
10103                other => panic!(
10104                    "Inductive declaration error: constructor \"{}\" must return \"{}\" (got \"{}\")",
10105                    name,
10106                    type_name,
10107                    key_of(other)
10108                ),
10109            }
10110            ConstructorDecl {
10111                name,
10112                params,
10113                typ: inner[1].clone(),
10114            }
10115        }
10116        _ => panic!(
10117            "Inductive declaration error: malformed constructor clause `{}`",
10118            key_of(clause)
10119        ),
10120    }
10121}
10122
10123/// Parse an `(inductive Name (constructor …) …)` form into an
10124/// [`InductiveDecl`]. Panics with `Inductive declaration error:` on a
10125/// malformed declaration so the existing diagnostic dispatch maps it to
10126/// `E033`.
10127pub fn parse_inductive_form(node: &Node) -> Option<InductiveDecl> {
10128    let children = match node {
10129        Node::List(items) => items,
10130        _ => return None,
10131    };
10132    if children.is_empty() {
10133        return None;
10134    }
10135    match &children[0] {
10136        Node::Leaf(h) if h == "inductive" => {}
10137        _ => return None,
10138    }
10139    let name = match children.get(1) {
10140        Some(Node::Leaf(s)) => s.clone(),
10141        _ => panic!("Inductive declaration error: type name must be a bare symbol"),
10142    };
10143    if !name.chars().next().map_or(false, |c| c.is_ascii_uppercase()) {
10144        panic!(
10145            "Inductive declaration error: declaration for \"{}\": type name must start with an uppercase letter",
10146            name
10147        );
10148    }
10149    if children.len() < 3 {
10150        panic!(
10151            "Inductive declaration error: declaration for \"{}\" must list at least one constructor",
10152            name
10153        );
10154    }
10155    let mut constructors: Vec<ConstructorDecl> = Vec::new();
10156    let mut seen: HashSet<String> = HashSet::new();
10157    for clause in &children[2..] {
10158        let ctor = parse_constructor_clause(clause, &name);
10159        if seen.contains(&ctor.name) {
10160            panic!(
10161                "Inductive declaration error: declaration for \"{}\": constructor \"{}\" is declared more than once",
10162                name, ctor.name
10163            );
10164        }
10165        seen.insert(ctor.name.clone());
10166        constructors.push(ctor);
10167    }
10168    let elim_name = format!("{}-rec", name);
10169    let elim_type = build_eliminator_type(&name, &constructors);
10170    Some(InductiveDecl {
10171        name,
10172        constructors,
10173        elim_name,
10174        elim_type,
10175    })
10176}
10177
10178fn build_case_type(ctor: &ConstructorDecl, type_name: &str, motive_var: &str) -> Node {
10179    let mut rec_binders: Vec<(String, Node)> = Vec::new();
10180    for (pname, ptype) in &ctor.params {
10181        if let Node::Leaf(s) = ptype {
10182            if s == type_name {
10183                rec_binders.push((
10184                    format!("ih_{}", pname),
10185                    Node::List(vec![
10186                        Node::Leaf("apply".to_string()),
10187                        Node::Leaf(motive_var.to_string()),
10188                        Node::Leaf(pname.clone()),
10189                    ]),
10190                ));
10191            }
10192        }
10193    }
10194    let ctor_applied = if ctor.params.is_empty() {
10195        Node::Leaf(ctor.name.clone())
10196    } else {
10197        let mut items = vec![Node::Leaf(ctor.name.clone())];
10198        for (pname, _) in &ctor.params {
10199            items.push(Node::Leaf(pname.clone()));
10200        }
10201        Node::List(items)
10202    };
10203    let motive_on_target = Node::List(vec![
10204        Node::Leaf("apply".to_string()),
10205        Node::Leaf(motive_var.to_string()),
10206        ctor_applied,
10207    ]);
10208    let inner = build_pi(&rec_binders, motive_on_target);
10209    build_pi(&ctor.params, inner)
10210}
10211
10212/// Compose the dependent eliminator type for `Name-rec`, given the parsed
10213/// constructor list. The motive parameter binds the symbol `_motive`
10214/// throughout, and each constructor case parameter binds `case_<ctorName>`.
10215pub fn build_eliminator_type(type_name: &str, constructors: &[ConstructorDecl]) -> Node {
10216    let motive_var = "_motive";
10217    let motive_type = Node::List(vec![
10218        Node::Leaf("Pi".to_string()),
10219        Node::List(vec![
10220            Node::Leaf(type_name.to_string()),
10221            Node::Leaf("_".to_string()),
10222        ]),
10223        Node::List(vec![
10224            Node::Leaf("Type".to_string()),
10225            Node::Leaf("0".to_string()),
10226        ]),
10227    ]);
10228    let case_params: Vec<(String, Node)> = constructors
10229        .iter()
10230        .map(|c| (format!("case_{}", c.name), build_case_type(c, type_name, motive_var)))
10231        .collect();
10232    let target_var = "_target";
10233    let final_node = Node::List(vec![
10234        Node::Leaf("apply".to_string()),
10235        Node::Leaf(motive_var.to_string()),
10236        Node::Leaf(target_var.to_string()),
10237    ]);
10238    let inner = build_pi(
10239        &[(target_var.to_string(), Node::Leaf(type_name.to_string()))],
10240        final_node,
10241    );
10242    let with_cases = build_pi(&case_params, inner);
10243    build_pi(
10244        &[(motive_var.to_string(), motive_type)],
10245        with_cases,
10246    )
10247}
10248
10249/// Install an inductive declaration on the environment: register the type,
10250/// every constructor, and the generated eliminator together with its
10251/// dependent Pi-type. Mirrors `registerInductive` in the JavaScript kernel.
10252pub fn register_inductive(env: &mut Env, decl: InductiveDecl) {
10253    let store_type = env.qualify_name(&decl.name);
10254    env.terms.insert(store_type.clone());
10255    let type0 = Node::List(vec![
10256        Node::Leaf("Type".to_string()),
10257        Node::Leaf("0".to_string()),
10258    ]);
10259    env.set_type(&store_type, &key_of(&type0));
10260    eval_node(&type0, env);
10261
10262    for ctor in &decl.constructors {
10263        let store_name = env.qualify_name(&ctor.name);
10264        env.terms.insert(store_name.clone());
10265        env.set_type(&store_name, &key_of(&ctor.typ));
10266        if matches!(ctor.typ, Node::List(_)) {
10267            eval_node(&ctor.typ, env);
10268        }
10269    }
10270
10271    let store_elim = env.qualify_name(&decl.elim_name);
10272    env.terms.insert(store_elim.clone());
10273    env.set_type(&store_elim, &key_of(&decl.elim_type));
10274    eval_node(&decl.elim_type, env);
10275
10276    env.inductives.insert(decl.name.clone(), decl);
10277}
10278
10279// ---------- Coinductive declarations (issue #53, D11) ----------
10280// Mirrors the JavaScript helpers in `js/src/rml-links.mjs`. The
10281// `(coinductive Name (constructor …) …)` form records a coinductive
10282// datatype, installs every constructor, and synthesises a corecursor
10283// `Name-corec` with a dependent Pi-type following the standard
10284// coiteration principle. The declaration also enforces a syntactic
10285// productivity check: at least one constructor must take a recursive
10286// `Name` argument (otherwise no infinite value can ever be generated).
10287// Errors panic with `Coinductive declaration error:` so
10288// `decode_panic_payload` maps them to E036.
10289
10290fn parse_coinductive_constructor_clause(clause: &Node, type_name: &str) -> ConstructorDecl {
10291    let items = match clause {
10292        Node::List(items) if items.len() == 2 => items,
10293        _ => panic!(
10294            "Coinductive declaration error: each clause must be `(constructor <name>)` or `(constructor (<name> <pi-type>))`"
10295        ),
10296    };
10297    match &items[0] {
10298        Node::Leaf(h) if h == "constructor" => {}
10299        _ => panic!(
10300            "Coinductive declaration error: each clause must be `(constructor <name>)` or `(constructor (<name> <pi-type>))`"
10301        ),
10302    }
10303    match &items[1] {
10304        Node::Leaf(name) => ConstructorDecl {
10305            name: name.clone(),
10306            params: Vec::new(),
10307            typ: Node::Leaf(type_name.to_string()),
10308        },
10309        Node::List(inner) if inner.len() == 2 => {
10310            let name = match &inner[0] {
10311                Node::Leaf(s) => s.clone(),
10312                _ => panic!(
10313                    "Coinductive declaration error: malformed constructor clause `{}`",
10314                    key_of(clause)
10315                ),
10316            };
10317            if !is_pi_sig(&inner[1]) {
10318                panic!(
10319                    "Coinductive declaration error: malformed constructor clause `{}`",
10320                    key_of(clause)
10321                );
10322            }
10323            let (params, result) = match flatten_pi(&inner[1]) {
10324                Some(parts) => parts,
10325                None => panic!(
10326                    "Coinductive declaration error: constructor \"{}\" has malformed Pi-type `{}`",
10327                    name,
10328                    key_of(&inner[1])
10329                ),
10330            };
10331            match &result {
10332                Node::Leaf(r) if r == type_name => {}
10333                other => panic!(
10334                    "Coinductive declaration error: constructor \"{}\" must return \"{}\" (got \"{}\")",
10335                    name,
10336                    type_name,
10337                    key_of(other)
10338                ),
10339            }
10340            ConstructorDecl {
10341                name,
10342                params,
10343                typ: inner[1].clone(),
10344            }
10345        }
10346        _ => panic!(
10347            "Coinductive declaration error: malformed constructor clause `{}`",
10348            key_of(clause)
10349        ),
10350    }
10351}
10352
10353/// Walk a constructor's parameter list and return whether it has at least
10354/// one recursive `type_name` argument. Used by the productivity check.
10355fn ctor_has_recursive_param(ctor: &ConstructorDecl, type_name: &str) -> bool {
10356    ctor.params.iter().any(|(_, ty)| {
10357        if let Node::Leaf(s) = ty {
10358            s == type_name
10359        } else {
10360            false
10361        }
10362    })
10363}
10364
10365/// Parse a `(coinductive Name (constructor …) …)` form into a
10366/// [`CoinductiveDecl`]. Panics with `Coinductive declaration error:` on
10367/// a malformed or non-productive declaration so the existing diagnostic
10368/// dispatch maps it to `E036`.
10369pub fn parse_coinductive_form(node: &Node) -> Option<CoinductiveDecl> {
10370    let children = match node {
10371        Node::List(items) => items,
10372        _ => return None,
10373    };
10374    if children.is_empty() {
10375        return None;
10376    }
10377    match &children[0] {
10378        Node::Leaf(h) if h == "coinductive" => {}
10379        _ => return None,
10380    }
10381    let name = match children.get(1) {
10382        Some(Node::Leaf(s)) => s.clone(),
10383        _ => panic!("Coinductive declaration error: type name must be a bare symbol"),
10384    };
10385    if !name.chars().next().map_or(false, |c| c.is_ascii_uppercase()) {
10386        panic!(
10387            "Coinductive declaration error: declaration for \"{}\": type name must start with an uppercase letter",
10388            name
10389        );
10390    }
10391    if children.len() < 3 {
10392        panic!(
10393            "Coinductive declaration error: declaration for \"{}\" must list at least one constructor",
10394            name
10395        );
10396    }
10397    let mut constructors: Vec<ConstructorDecl> = Vec::new();
10398    let mut seen: HashSet<String> = HashSet::new();
10399    for clause in &children[2..] {
10400        let ctor = parse_coinductive_constructor_clause(clause, &name);
10401        if seen.contains(&ctor.name) {
10402            panic!(
10403                "Coinductive declaration error: declaration for \"{}\": constructor \"{}\" is declared more than once",
10404                name, ctor.name
10405            );
10406        }
10407        seen.insert(ctor.name.clone());
10408        constructors.push(ctor);
10409    }
10410    let any_recursive = constructors.iter().any(|c| ctor_has_recursive_param(c, &name));
10411    if !any_recursive {
10412        panic!(
10413            "Coinductive declaration error: declaration for \"{}\" is non-productive: at least one constructor must take a recursive \"{}\" argument",
10414            name, name
10415        );
10416    }
10417    let corec_name = format!("{}-corec", name);
10418    let corec_type = build_corecursor_type(&name, &constructors);
10419    Some(CoinductiveDecl {
10420        name,
10421        constructors,
10422        corec_name,
10423        corec_type,
10424    })
10425}
10426
10427fn build_corec_case_type(ctor: &ConstructorDecl, type_name: &str, state_var: &str) -> Node {
10428    let dual_params: Vec<(String, Node)> = ctor
10429        .params
10430        .iter()
10431        .map(|(pname, ptype)| {
10432            let new_type = match ptype {
10433                Node::Leaf(s) if s == type_name => Node::Leaf(state_var.to_string()),
10434                other => other.clone(),
10435            };
10436            (pname.clone(), new_type)
10437        })
10438        .collect();
10439    let inner = build_pi(&dual_params, Node::Leaf(type_name.to_string()));
10440    build_pi(
10441        &[(
10442            "_state".to_string(),
10443            Node::Leaf(state_var.to_string()),
10444        )],
10445        inner,
10446    )
10447}
10448
10449/// Compose the dependent corecursor type for `Name-corec`, given the parsed
10450/// constructor list. The state parameter binds the symbol `_state_type`
10451/// throughout, and each constructor case parameter binds `case_<ctorName>`.
10452pub fn build_corecursor_type(type_name: &str, constructors: &[ConstructorDecl]) -> Node {
10453    let state_var = "_state_type";
10454    let state_type = Node::List(vec![
10455        Node::Leaf("Type".to_string()),
10456        Node::Leaf("0".to_string()),
10457    ]);
10458    let case_params: Vec<(String, Node)> = constructors
10459        .iter()
10460        .map(|c| {
10461            (
10462                format!("case_{}", c.name),
10463                build_corec_case_type(c, type_name, state_var),
10464            )
10465        })
10466        .collect();
10467    let seed_var = "_seed";
10468    let final_node = Node::Leaf(type_name.to_string());
10469    let inner = build_pi(
10470        &[(seed_var.to_string(), Node::Leaf(state_var.to_string()))],
10471        final_node,
10472    );
10473    let with_cases = build_pi(&case_params, inner);
10474    build_pi(
10475        &[(state_var.to_string(), state_type)],
10476        with_cases,
10477    )
10478}
10479
10480/// Install a coinductive declaration on the environment: register the type,
10481/// every constructor, and the generated corecursor together with its
10482/// dependent Pi-type. Mirrors `registerCoinductive` in the JavaScript kernel.
10483pub fn register_coinductive(env: &mut Env, decl: CoinductiveDecl) {
10484    let store_type = env.qualify_name(&decl.name);
10485    env.terms.insert(store_type.clone());
10486    let type0 = Node::List(vec![
10487        Node::Leaf("Type".to_string()),
10488        Node::Leaf("0".to_string()),
10489    ]);
10490    env.set_type(&store_type, &key_of(&type0));
10491    eval_node(&type0, env);
10492
10493    for ctor in &decl.constructors {
10494        let store_name = env.qualify_name(&ctor.name);
10495        env.terms.insert(store_name.clone());
10496        env.set_type(&store_name, &key_of(&ctor.typ));
10497        if matches!(ctor.typ, Node::List(_)) {
10498            eval_node(&ctor.typ, env);
10499        }
10500    }
10501
10502    let store_corec = env.qualify_name(&decl.corec_name);
10503    env.terms.insert(store_corec.clone());
10504    env.set_type(&store_corec, &key_of(&decl.corec_type));
10505    eval_node(&decl.corec_type, env);
10506
10507    env.coinductives.insert(decl.name.clone(), decl);
10508}
10509
10510pub fn decide_automatic_sequence_theorem(name: &str) -> Option<AutomaticSequenceDecision> {
10511    if name == "thue-morse-cube-free" {
10512        return Some(AutomaticSequenceDecision {
10513            theorem: name.to_string(),
10514            value: true,
10515            method: "built-in Buchi emptiness certificate".to_string(),
10516            certificate: Node::List(vec![
10517                Node::Leaf("buchi-emptiness".to_string()),
10518                Node::Leaf("thue-morse".to_string()),
10519                Node::Leaf("cube-free".to_string()),
10520            ]),
10521        });
10522    }
10523    None
10524}
10525
10526pub fn automatic_sequences_domain_plugin(forms: &[Node], env: &mut Env) -> Result<(), String> {
10527    if forms.is_empty() {
10528        return Err("automatic-sequences domain requires at least one request".to_string());
10529    }
10530    let theorem_shape_error = "automatic-sequences entries must be `(theorem <name>)`".to_string();
10531    for form in forms {
10532        let theorem_name = match form {
10533            Node::List(children) if children.len() == 2 => {
10534                if let (Node::Leaf(head), Node::Leaf(name)) = (&children[0], &children[1]) {
10535                    if head == "theorem" {
10536                        name.clone()
10537                    } else {
10538                        return Err(theorem_shape_error.clone());
10539                    }
10540                } else {
10541                    return Err(theorem_shape_error.clone());
10542                }
10543            }
10544            _ => {
10545                return Err(theorem_shape_error.clone());
10546            }
10547        };
10548        let mut decision = decide_automatic_sequence_theorem(&theorem_name)
10549            .ok_or_else(|| format!("unknown automatic-sequences theorem \"{}\"", theorem_name))?;
10550        let store_name = env.qualify_name(&decision.theorem);
10551        let truth_value = if decision.value { env.hi } else { env.lo };
10552        env.terms.insert(store_name.clone());
10553        env.set_type(&store_name, "Theorem");
10554        env.set_symbol_prob(&store_name, truth_value);
10555        decision.theorem = store_name.clone();
10556        env.automatic_sequence_decisions
10557            .insert(store_name.clone(), decision);
10558        env.trace(
10559            "domain",
10560            format!("{} decided by automatic-sequences", store_name),
10561        );
10562    }
10563    Ok(())
10564}
10565
10566fn eval_domain_form(children: &[Node], env: &mut Env) -> EvalResult {
10567    if children.len() < 3 {
10568        panic!("Domain plugin error: Domain form must be `(domain <name> <request>...)`");
10569    }
10570    let plugin_name = match &children[1] {
10571        Node::Leaf(name) => name.clone(),
10572        _ => {
10573            panic!("Domain plugin error: Domain form must be `(domain <name> <request>...)`");
10574        }
10575    };
10576    let plugin = env.get_domain_plugin(&plugin_name).unwrap_or_else(|| {
10577        panic!(
10578            "Domain plugin error: Unknown domain plugin \"{}\"",
10579            plugin_name
10580        )
10581    });
10582    if let Err(message) = plugin(&children[2..], env) {
10583        panic!("Domain plugin error: {}", message);
10584    }
10585    EvalResult::Value(1.0)
10586}
10587
10588/// Evaluate an AST node in the given environment.
10589pub fn eval_node(node: &Node, env: &mut Env) -> EvalResult {
10590    // HOAS desugaring (issue #51, D7): rewrite `(forall (A x) body)` to
10591    // `(Pi (A x) body)` so callers passing AST nodes directly to `eval_node`
10592    // benefit from the same surface as `evaluate()` / `parse_term_input_str`.
10593    // The recursive walk also handles `forall` nested inside definition RHSs
10594    // such as `(succ: (forall (Natural n) Natural))`.
10595    let desugared;
10596    let node = if matches!(node, Node::List(_)) {
10597        desugared = desugar_hoas(node.clone());
10598        &desugared
10599    } else {
10600        node
10601    };
10602    match node {
10603        Node::Leaf(s) => {
10604            if is_num(s) {
10605                EvalResult::Value(env.to_num(s))
10606            } else {
10607                EvalResult::Value(env.get_symbol_prob(s))
10608            }
10609        }
10610        Node::List(children) => {
10611            if children.is_empty() {
10612                return EvalResult::Value(0.0);
10613            }
10614
10615            // Definitions & operator redefs: (head: ...) form
10616            if let Node::Leaf(ref s) = children[0] {
10617                if s.ends_with(':') {
10618                    let head = &s[..s.len() - 1];
10619                    return define_form(head, &children[1..], env);
10620                }
10621            }
10622
10623            // Note: (x : A) with spaces as a standalone colon separator is NOT supported.
10624            // Use (x: A) instead — the colon must be part of the link name.
10625
10626            // Mode declaration (issue #43, D15): (mode <name> +input -output ...)
10627            // Records the per-argument mode pattern for a relation. Validation
10628            // lives in `parse_mode_form`, which panics with `Mode declaration
10629            // error:` on a malformed declaration so `decode_panic_payload`
10630            // surfaces it as E030.
10631            if let Node::Leaf(ref head) = children[0] {
10632                if head == "mode" {
10633                    if let Some((name, flags)) = parse_mode_form(children) {
10634                        env.modes.insert(name, flags);
10635                        return EvalResult::Value(1.0);
10636                    }
10637                }
10638            }
10639
10640            // Relation declaration (issue #44, D12): (relation <name> <clause>...)
10641            // Stores the clause list keyed by relation name. `parse_relation_form`
10642            // panics with `Relation declaration error:` on a malformed
10643            // declaration so `decode_panic_payload` surfaces it as E032.
10644            if let Node::Leaf(ref head) = children[0] {
10645                if head == "relation" {
10646                    let (name, clauses) = parse_relation_form(children);
10647                    env.relations.insert(name, clauses);
10648                    return EvalResult::Value(1.0);
10649                }
10650            }
10651
10652            // Totality declaration (issue #44, D12): (total <name>) runs
10653            // `is_total` and surfaces the first diagnostic via the existing
10654            // panic-based dispatch (`Totality check error:` -> E032).
10655            if let Node::Leaf(ref head) = children[0] {
10656                if head == "total" {
10657                    if children.len() == 2 {
10658                        if let Node::Leaf(ref rel_name) = children[1] {
10659                            let result = is_total(env, rel_name);
10660                            if !result.ok {
10661                                if let Some(first) = result.diagnostics.first() {
10662                                    panic!("Totality check error: {}", first.message);
10663                                }
10664                            }
10665                            return EvalResult::Value(1.0);
10666                        }
10667                    }
10668                    panic!(
10669                        "Totality check error: Totality declaration must be `(total <relation-name>)`"
10670                    );
10671                }
10672            }
10673
10674            // Definition declaration (issue #49, D13):
10675            //   (define <name> [(measure (lex <slot>...))] (case <pat> <body>) ...)
10676            // Records the recursive definition on the env so termination can
10677            // be queried later. `parse_define_form` panics with
10678            // `Termination check error:` on a malformed declaration so
10679            // `decode_panic_payload` surfaces it as E035.
10680            if let Node::Leaf(ref head) = children[0] {
10681                if head == "define" {
10682                    let decl = parse_define_form(children);
10683                    env.definitions.insert(decl.name.clone(), decl);
10684                    return EvalResult::Value(1.0);
10685                }
10686            }
10687
10688            // Termination declaration (issue #49, D13): (terminating <name>)
10689            // runs `is_terminating` and surfaces the first diagnostic via
10690            // the existing panic-based dispatch (`Termination check error:`
10691            // -> E035).
10692            if let Node::Leaf(ref head) = children[0] {
10693                if head == "terminating" {
10694                    if children.len() == 2 {
10695                        if let Node::Leaf(ref def_name) = children[1] {
10696                            let result = is_terminating(env, def_name);
10697                            if !result.ok {
10698                                if let Some(first) = result.diagnostics.first() {
10699                                    panic!("Termination check error: {}", first.message);
10700                                }
10701                            }
10702                            return EvalResult::Value(1.0);
10703                        }
10704                    }
10705                    panic!(
10706                        "Termination check error: Termination declaration must be `(terminating <definition-name>)`"
10707                    );
10708                }
10709            }
10710
10711            // Coverage declaration (issue #46, D14): (coverage <name>) runs
10712            // `is_covered`. The first diagnostic becomes the panic so the
10713            // surrounding form gets a span; any extras land in
10714            // `shadow_diagnostics` so each missing slot reaches the user.
10715            if let Node::Leaf(ref head) = children[0] {
10716                if head == "coverage" {
10717                    if children.len() == 2 {
10718                        if let Node::Leaf(ref rel_name) = children[1] {
10719                            let result = is_covered(env, rel_name);
10720                            if !result.ok {
10721                                let span = env
10722                                    .current_span
10723                                    .clone()
10724                                    .unwrap_or_else(|| env.default_span.clone());
10725                                if result.diagnostics.len() > 1 {
10726                                    for d in result.diagnostics.iter().skip(1) {
10727                                        env.shadow_diagnostics.push(Diagnostic::new(
10728                                            &d.code,
10729                                            d.message.clone(),
10730                                            span.clone(),
10731                                        ));
10732                                    }
10733                                }
10734                                if let Some(first) = result.diagnostics.first() {
10735                                    panic!("Coverage check error: {}", first.message);
10736                                }
10737                            }
10738                            return EvalResult::Value(1.0);
10739                        }
10740                    }
10741                    panic!(
10742                        "Coverage check error: Coverage declaration must be `(coverage <relation-name>)`"
10743                    );
10744                }
10745            }
10746
10747            // World declaration (issue #54, D16): (world <name> (<const>...))
10748            // Records the allow-list of free constants permitted in arguments
10749            // of a relation. `parse_world_form` panics with `World declaration
10750            // error:` on a malformed declaration so `decode_panic_payload`
10751            // surfaces it as E034.
10752            if let Node::Leaf(ref head) = children[0] {
10753                if head == "world" {
10754                    if let Some((name, allowed)) = parse_world_form(children) {
10755                        env.worlds.insert(name, allowed);
10756                        return EvalResult::Value(1.0);
10757                    }
10758                }
10759            }
10760
10761            // Inductive declaration (issue #45, D10):
10762            //   (inductive Name (constructor c1) (constructor (c2 (Pi ...))) ...)
10763            // Stores the type, every constructor, and a generated `Name-rec`
10764            // eliminator on the env so they participate in `(of)`,
10765            // `(type of …)`, and the bidirectional checker.
10766            // `parse_inductive_form` panics with `Inductive declaration error:`
10767            // on a malformed declaration, which `decode_panic_payload` maps
10768            // to E033.
10769            if let Node::Leaf(ref head) = children[0] {
10770                if head == "inductive" {
10771                    if let Some(decl) = parse_inductive_form(node) {
10772                        register_inductive(env, decl);
10773                        return EvalResult::Value(1.0);
10774                    }
10775                }
10776            }
10777
10778            // Coinductive declaration (issue #53, D11):
10779            //   (coinductive Name (constructor c1) (constructor (c2 (Pi ...))) ...)
10780            // Stores the type, every constructor, and a generated
10781            // `Name-corec` corecursor on the env. The form additionally
10782            // enforces a syntactic productivity check: at least one
10783            // constructor must take a recursive argument so non-productive
10784            // types (which cannot generate any infinite values) are
10785            // rejected up front. `parse_coinductive_form` panics with
10786            // `Coinductive declaration error:` on a malformed or
10787            // non-productive declaration, which `decode_panic_payload`
10788            // maps to E035.
10789            if let Node::Leaf(ref head) = children[0] {
10790                if head == "coinductive" {
10791                    if let Some(decl) = parse_coinductive_form(node) {
10792                        register_coinductive(env, decl);
10793                        return EvalResult::Value(1.0);
10794                    }
10795                }
10796            }
10797
10798            // Domain plugin driver (issue #63): (domain <name> <request>...)
10799            // Dispatches the block body to a registered domain-specific
10800            // decision procedure. The default Env registers
10801            // `automatic-sequences`.
10802            if let Node::Leaf(ref head) = children[0] {
10803                if head == "domain" {
10804                    return eval_domain_form(children, env);
10805                }
10806            }
10807
10808            // Mode-mismatch check (issue #43, D15): a call `(name args...)`
10809            // whose head has a registered mode declaration must agree with the
10810            // declared flags. Run before head evaluation so the diagnostic
10811            // points at the call site rather than at a downstream reduction.
10812            if let Node::Leaf(ref head) = children[0] {
10813                if env.modes.contains_key(head) {
10814                    let head_owned = head.clone();
10815                    let args: Vec<Node> = children[1..].to_vec();
10816                    check_mode_at_call(&head_owned, &args, env);
10817                }
10818            }
10819
10820            // World-violation check (issue #54, D16): a call `(name args...)`
10821            // whose head has a registered world declaration must only contain
10822            // declared constants free in its arguments. Surface the first
10823            // offending free constant as E033.
10824            if let Node::Leaf(ref head) = children[0] {
10825                if env.worlds.contains_key(head) {
10826                    let head_owned = head.clone();
10827                    let args: Vec<Node> = children[1..].to_vec();
10828                    check_world_at_call(&head_owned, &args, env);
10829                }
10830            }
10831
10832            // Assignment: ((expr) has probability p)
10833            if children.len() == 4 {
10834                if let (Node::Leaf(ref w1), Node::Leaf(ref w2), Node::Leaf(ref w3)) =
10835                    (&children[1], &children[2], &children[3])
10836                {
10837                    if w1 == "has" && w2 == "probability" && is_num(w3) {
10838                        let p: f64 = w3.parse().unwrap_or(0.0);
10839                        // Carrier enforcement (issue #97, Section 2): if an
10840                        // enclosing `(with-foundation ...)` declared a strict
10841                        // carrier, the assigned value must belong to that
10842                        // carrier. Violations surface as E063 instead of
10843                        // being silently clamped. We panic with a
10844                        // "Carrier violation:" prefix so the surrounding
10845                        // catch_unwind translates it to a Diagnostic.
10846                        let clamped = env.clamp(p);
10847                        if let Some(msg) = env.check_carrier_value(clamped) {
10848                            panic!(
10849                                "Carrier violation: Probability assignment {} = {} violates active foundation carrier: {}",
10850                                key_of(&children[0]),
10851                                format_trace_value(clamped),
10852                                msg
10853                            );
10854                        }
10855                        env.set_expr_prob(&children[0], p);
10856                        let key = key_of(&children[0]);
10857                        env.trace(
10858                            "assign",
10859                            format!("{} ← {}", key, format_trace_value(clamped)),
10860                        );
10861                        return EvalResult::Value(env.to_num(w3));
10862                    }
10863                }
10864            }
10865
10866            // Range configuration: (range lo hi) prefix form
10867            if children.len() == 3 {
10868                if let Node::Leaf(ref first) = children[0] {
10869                    if first == "range" {
10870                        if let (Node::Leaf(ref lo_s), Node::Leaf(ref hi_s)) =
10871                            (&children[1], &children[2])
10872                        {
10873                            if is_num(lo_s) && is_num(hi_s) {
10874                                env.lo = lo_s.parse().unwrap_or(0.0);
10875                                env.hi = hi_s.parse().unwrap_or(1.0);
10876                                env.reinit_ops();
10877                                return EvalResult::Value(1.0);
10878                            }
10879                        }
10880                    }
10881                }
10882            }
10883
10884            // Valence configuration: (valence N) prefix form
10885            if children.len() == 2 {
10886                if let Node::Leaf(ref first) = children[0] {
10887                    if first == "valence" {
10888                        if let Node::Leaf(ref val_s) = children[1] {
10889                            if is_num(val_s) {
10890                                env.valence = val_s.parse::<f64>().unwrap_or(0.0) as u32;
10891                                return EvalResult::Value(1.0);
10892                            }
10893                        }
10894                    }
10895                }
10896            }
10897
10898            // Query: (? expr) or (? expr with proof)
10899            // The trailing `with proof` keyword pair is consumed here so it
10900            // does not interfere with evaluation; `evaluate_inner` looks at
10901            // the original form to decide whether to populate a proof slot.
10902            // The proof itself is built by `build_proof` after evaluation.
10903            if let Node::Leaf(ref first) = children[0] {
10904                if first == "?" {
10905                    let parts = strip_with_proof(&children[1..]);
10906                    let target: Node = if parts.len() == 1 {
10907                        parts[0].clone()
10908                    } else {
10909                        Node::List(parts.to_vec())
10910                    };
10911                    let result = eval_node(&target, env);
10912                    // If inner result is already a type query, pass it through
10913                    if result.is_type_query() {
10914                        return result;
10915                    }
10916                    if let EvalResult::Term(term) = result {
10917                        return EvalResult::TypeQuery(key_of(&term));
10918                    }
10919                    let v = result.as_f64();
10920                    return EvalResult::Query(env.clamp(v));
10921                }
10922            }
10923
10924            // Kernel substitution primitive: (subst term x replacement)
10925            if children.len() == 4 {
10926                if let (Node::Leaf(ref head), Node::Leaf(ref var_name)) =
10927                    (&children[0], &children[2])
10928                {
10929                    if head == "subst" {
10930                        let term = eval_term_node(node, env);
10931                        let _ = var_name;
10932                        return EvalResult::Term(term);
10933                    }
10934                }
10935            }
10936
10937            // Freshness binder: (fresh x in body)
10938            if children.len() == 4 {
10939                if let (Node::Leaf(ref head), Node::Leaf(ref var_name), Node::Leaf(ref in_kw)) =
10940                    (&children[0], &children[1], &children[2])
10941                {
10942                    if head == "fresh" && in_kw == "in" {
10943                        return eval_fresh(var_name, &children[3], env);
10944                    }
10945                }
10946            }
10947
10948            // Infix arithmetic: (A + B), (A - B), (A * B), (A / B)
10949            // Arithmetic uses raw numeric values (not clamped to the logic range)
10950            if children.len() == 3 {
10951                if let Node::Leaf(ref op_name) = children[1] {
10952                    if op_name == "+" || op_name == "-" || op_name == "*" || op_name == "/" {
10953                        let l = eval_arith(&children[0], env);
10954                        let r = eval_arith(&children[2], env);
10955                        return EvalResult::Value(env.apply_op(op_name, &[l, r]));
10956                    }
10957                }
10958            }
10959
10960            // Infix numeric comparisons: (A < B), (A <= B)
10961            if children.len() == 3 {
10962                if let Node::Leaf(ref op_name) = children[1] {
10963                    if op_name == "<" || op_name == "<=" {
10964                        let l = eval_arith(&children[0], env);
10965                        let r = eval_arith(&children[2], env);
10966                        return EvalResult::Value(env.clamp(env.apply_op(op_name, &[l, r])));
10967                    }
10968                }
10969            }
10970
10971            // Infix AND/OR/BOTH/NEITHER: ((A) and (B)) / ((A) or (B)) / ((A) both (B)) / ((A) neither (B))
10972            if children.len() == 3 {
10973                if let Node::Leaf(ref op_name) = children[1] {
10974                    if op_name == "and"
10975                        || op_name == "or"
10976                        || op_name == "both"
10977                        || op_name == "neither"
10978                    {
10979                        let l = eval_node(&children[0], env).as_f64();
10980                        let r = eval_node(&children[2], env).as_f64();
10981                        return EvalResult::Value(env.clamp(env.apply_op(op_name, &[l, r])));
10982                    }
10983                }
10984            }
10985
10986            // Composite natural language operators: (both A and B [and C ...]), (neither A nor B [nor C ...])
10987            if children.len() >= 4 && children.len() % 2 == 0 {
10988                if let Node::Leaf(ref head) = children[0] {
10989                    if head == "both" || head == "neither" {
10990                        let sep = if head == "both" { "and" } else { "nor" };
10991                        let mut valid = true;
10992                        for i in (2..children.len()).step_by(2) {
10993                            if let Node::Leaf(ref s) = children[i] {
10994                                if s != sep {
10995                                    valid = false;
10996                                    break;
10997                                }
10998                            } else {
10999                                valid = false;
11000                                break;
11001                            }
11002                        }
11003                        if valid {
11004                            let head_str = head.clone();
11005                            let vals: Vec<f64> = (1..children.len())
11006                                .step_by(2)
11007                                .map(|i| eval_node(&children[i], env).as_f64())
11008                                .collect();
11009                            return EvalResult::Value(env.clamp(env.apply_op(&head_str, &vals)));
11010                        }
11011                    }
11012                }
11013            }
11014
11015            // Infix equality/inequality: (L = R), (L != R)
11016            if children.len() == 3 {
11017                if let Node::Leaf(ref op_name) = children[1] {
11018                    if op_name == "=" {
11019                        return eval_equality_node(&children[0], "=", &children[2], env);
11020                    }
11021                    if op_name == "!=" {
11022                        return eval_equality_node(&children[0], "!=", &children[2], env);
11023                    }
11024                }
11025            }
11026
11027            // ---------- Type System: "everything is a link" ----------
11028
11029            // Type universe: (Type N)
11030            if children.len() == 2 {
11031                if let Node::Leaf(ref first) = children[0] {
11032                    if first == "Type" {
11033                        if let Node::Leaf(ref level_s) = children[1] {
11034                            if let Some(level) = parse_universe_level_token(level_s) {
11035                                if let Some(next_level) = level.checked_add(1) {
11036                                    let key = key_of(&Node::List(children.clone()));
11037                                    env.set_type(&key, &format!("(Type {})", next_level));
11038                                    return EvalResult::Value(1.0);
11039                                }
11040                            }
11041                        }
11042                    }
11043                }
11044            }
11045
11046            // Prop: (Prop) sugar for (Type 0)
11047            if children.len() == 1 {
11048                if let Node::Leaf(ref first) = children[0] {
11049                    if first == "Prop" {
11050                        env.set_type("(Prop)", "(Type 1)");
11051                        return EvalResult::Value(1.0);
11052                    }
11053                }
11054            }
11055
11056            // Dependent product (Pi-type): (Pi (x: A) B)
11057            if children.len() == 3 {
11058                if let Node::Leaf(ref first) = children[0] {
11059                    if first == "Pi" {
11060                        if let Some((param_name, param_type)) = parse_binding(&children[1]) {
11061                            env.terms.insert(param_name.clone());
11062                            env.set_type(&param_name, &param_type);
11063                            let key = key_of(&Node::List(children.clone()));
11064                            env.set_type(&key, "(Type 0)");
11065                        }
11066                        return EvalResult::Value(1.0);
11067                    }
11068                }
11069            }
11070
11071            // Lambda abstraction: (lambda (A x) body) or (lambda (x: A) body)
11072            // Also supports multi-param: (lambda (A x, B y) body)
11073            if children.len() == 3 {
11074                if let Node::Leaf(ref first) = children[0] {
11075                    if first == "lambda" {
11076                        if let Some(bindings) = parse_bindings(&children[1]) {
11077                            if !bindings.is_empty() {
11078                                let (ref param_name, ref param_type) = bindings[0];
11079                                env.terms.insert(param_name.clone());
11080                                env.set_type(param_name, param_type);
11081                                // Register additional bindings
11082                                for binding in &bindings[1..] {
11083                                    env.terms.insert(binding.0.clone());
11084                                    env.set_type(&binding.0, &binding.1);
11085                                }
11086                                let body_key = key_of(&children[2]);
11087                                let body_type = env
11088                                    .get_type(&body_key)
11089                                    .cloned()
11090                                    .unwrap_or_else(|| "unknown".to_string());
11091                                let key = key_of(&Node::List(children.clone()));
11092                                env.set_type(
11093                                    &key,
11094                                    &format!("(Pi ({} {}) {})", param_type, param_name, body_type),
11095                                );
11096                            }
11097                        }
11098                        return EvalResult::Value(1.0);
11099                    }
11100                }
11101            }
11102
11103            // Normalization drivers (issue #50, D4):
11104            //   (whnf <expr>)         — weak-head normal form
11105            //   (nf <expr>)           — full normal form
11106            //   (normal-form <expr>)  — alias for `nf`
11107            // Each driver returns an `EvalResult::Term` whose printed form
11108            // is the reduct. Malformed driver shapes panic with
11109            // `Normalization error:` so `decode_panic_payload` maps them
11110            // to E038.
11111            if let Node::Leaf(ref first) = children[0] {
11112                if first == "whnf" {
11113                    if children.len() == 2 {
11114                        let opts = ConvertOptions::default();
11115                        let reduced = whnf_term(&children[1], env, opts);
11116                        return EvalResult::Term(reduced);
11117                    }
11118                    panic!("Normalization error: Normalization form must be `(whnf <expr>)`");
11119                }
11120                if first == "nf" {
11121                    if children.len() == 2 {
11122                        let opts = ConvertOptions::default();
11123                        let normalized = normalize_term(&children[1], env, opts);
11124                        let flat = flatten_neutral_applies(&normalized, env);
11125                        return EvalResult::Term(flat);
11126                    }
11127                    panic!("Normalization error: Normalization form must be `(nf <expr>)`");
11128                }
11129                if first == "normal-form" {
11130                    if children.len() == 2 {
11131                        let opts = ConvertOptions::default();
11132                        let normalized = normalize_term(&children[1], env, opts);
11133                        let flat = flatten_neutral_applies(&normalized, env);
11134                        return EvalResult::Term(flat);
11135                    }
11136                    panic!("Normalization error: Normalization form must be `(normal-form <expr>)`");
11137                }
11138            }
11139
11140            // Application: (apply f x) — explicit application with beta-reduction
11141            if children.len() == 3 {
11142                if let Node::Leaf(ref first) = children[0] {
11143                    if first == "apply" {
11144                        let fn_node = &children[1];
11145                        let arg = &children[2];
11146
11147                        // Check if fn is a lambda: (lambda (A x) body)
11148                        if let Node::List(ref fn_children) = fn_node {
11149                            if fn_children.len() == 3 {
11150                                if let Node::Leaf(ref fn_head) = fn_children[0] {
11151                                    if fn_head == "lambda" {
11152                                        if let Some((param_name, _)) =
11153                                            parse_binding(&fn_children[1])
11154                                        {
11155                                            let body = &fn_children[2];
11156                                            let result = subst(body, &param_name, arg);
11157                                            return eval_reduced_term(&result, env);
11158                                        }
11159                                    }
11160                                }
11161                            }
11162                        }
11163
11164                        // Check if fn is a named lambda
11165                        if let Node::Leaf(ref fn_name) = fn_node {
11166                            if let Some(lambda) = env.get_lambda(fn_name).cloned() {
11167                                let result = subst(&lambda.body, &lambda.param, arg);
11168                                return eval_reduced_term(&result, env);
11169                            }
11170                        }
11171
11172                        // Otherwise evaluate both
11173                        let f_val = eval_node(fn_node, env).as_f64();
11174                        return EvalResult::Value(f_val);
11175                    }
11176                }
11177            }
11178
11179            // Type query: (type of expr) — returns the type of an expression
11180            // e.g. (? (type of x)) → returns the type string
11181            if children.len() == 3 {
11182                if let (Node::Leaf(ref first), Node::Leaf(ref mid)) = (&children[0], &children[1]) {
11183                    if first == "type" && mid == "of" {
11184                        let type_str = infer_type_key(&children[2], env)
11185                            .unwrap_or_else(|| "unknown".to_string());
11186                        return EvalResult::TypeQuery(type_str);
11187                    }
11188                }
11189            }
11190
11191            // Type check query: (expr of Type) — checks if expr has the given type
11192            // e.g. (? (x of Natural)) → returns 1 or 0
11193            if children.len() == 3 {
11194                if let Node::Leaf(ref mid) = children[1] {
11195                    if mid == "of" {
11196                        let expected_key = match &children[2] {
11197                            Node::Leaf(s) => s.clone(),
11198                            other => key_of(other),
11199                        };
11200                        if let Some(actual) = infer_type_key(&children[0], env) {
11201                            return EvalResult::Value(if actual == expected_key {
11202                                env.hi
11203                            } else {
11204                                env.lo
11205                            });
11206                        }
11207                        return EvalResult::Value(env.lo);
11208                    }
11209                }
11210            }
11211
11212            // Prefix: (not X), (and X Y ...), (or X Y ...)
11213            if let Node::Leaf(ref head) = children[0] {
11214                let head_str = head.clone();
11215                if (head_str == "=" || head_str == "!=") && children.len() == 3 {
11216                    return eval_equality_node(&children[1], &head_str, &children[2], env);
11217                }
11218                if env.has_op(&head_str) {
11219                    let vals: Vec<f64> = children[1..]
11220                        .iter()
11221                        .map(|a| eval_node(a, env).as_f64())
11222                        .collect();
11223                    return EvalResult::Value(env.clamp(env.apply_op(&head_str, &vals)));
11224                }
11225
11226                // Named lambda application: (name arg ...)
11227                if children.len() >= 2 {
11228                    if let Some(lambda) = env.get_lambda(&head_str).cloned() {
11229                        let result = subst(&lambda.body, &lambda.param, &children[1]);
11230                        if children.len() == 2 {
11231                            return eval_reduced_term(&result, env);
11232                        }
11233                        let mut next = vec![result];
11234                        next.extend_from_slice(&children[2..]);
11235                        return eval_reduced_term(&Node::List(next), env);
11236                    }
11237                }
11238            }
11239
11240            // Prefix application with an inline lambda head: ((lambda (A x) body) arg)
11241            if children.len() >= 2 {
11242                if let Node::List(head_children) = &children[0] {
11243                    if head_children.len() == 3 {
11244                        if let Node::Leaf(fn_head) = &head_children[0] {
11245                            if fn_head == "lambda" {
11246                                if let Some((param_name, _)) = parse_binding(&head_children[1]) {
11247                                    let result =
11248                                        subst(&head_children[2], &param_name, &children[1]);
11249                                    if children.len() == 2 {
11250                                        return eval_reduced_term(&result, env);
11251                                    }
11252                                    let mut next = vec![result];
11253                                    next.extend_from_slice(&children[2..]);
11254                                    return eval_reduced_term(&Node::List(next), env);
11255                                }
11256                            }
11257                        }
11258                    }
11259                }
11260            }
11261
11262            EvalResult::Value(0.0)
11263        }
11264    }
11265}
11266
11267/// Process definition forms: (head: rhs...)
11268fn define_form(head: &str, rhs: &[Node], env: &mut Env) -> EvalResult {
11269    // Configuration directives are file-level and never namespaced.
11270    // Range configuration: (range: lo hi)
11271    if head == "range" && rhs.len() == 2 {
11272        if let (Node::Leaf(ref lo_s), Node::Leaf(ref hi_s)) = (&rhs[0], &rhs[1]) {
11273            if is_num(lo_s) && is_num(hi_s) {
11274                env.lo = lo_s.parse().unwrap_or(0.0);
11275                env.hi = hi_s.parse().unwrap_or(1.0);
11276                env.reinit_ops();
11277                return EvalResult::Value(1.0);
11278            }
11279        }
11280    }
11281
11282    // Valence configuration: (valence: N)
11283    if head == "valence" && rhs.len() == 1 {
11284        if let Node::Leaf(ref val_s) = rhs[0] {
11285            if is_num(val_s) {
11286                env.valence = val_s.parse::<f64>().unwrap_or(0.0) as u32;
11287                return EvalResult::Value(1.0);
11288            }
11289        }
11290    }
11291
11292    // Bindings introduced inside `(namespace foo)` are stored under `foo.head`.
11293    // The syntactic head (e.g. `a` in `(a: a is a)`) is still used to match
11294    // patterns; only the storage key is qualified.
11295    let store_name = env.qualify_name(head);
11296    // Shadowing diagnostic (E008): if this name was already imported, warn.
11297    if store_name != head || env.namespace.is_none() {
11298        maybe_warn_shadow(env, &store_name);
11299    } else {
11300        maybe_warn_shadow(env, head);
11301    }
11302
11303    // Term definition: (a: a is a) → declare 'a' as a term
11304    if rhs.len() == 3 {
11305        if let (Node::Leaf(ref r0), Node::Leaf(ref r1), Node::Leaf(ref r2)) =
11306            (&rhs[0], &rhs[1], &rhs[2])
11307        {
11308            if r1 == "is" && r0 == head && r2 == head {
11309                env.terms.insert(store_name.clone());
11310                return EvalResult::Value(1.0);
11311            }
11312        }
11313    }
11314
11315    // Prefix type notation: (name: TypeName name) → typed self-referential declaration
11316    // e.g. (zero: Natural zero), (boolean: Type boolean), (true: Boolean true)
11317    if rhs.len() == 2 {
11318        if let Node::Leaf(ref last) = rhs[1] {
11319            if last == head {
11320                match &rhs[0] {
11321                    Node::Leaf(ref type_name)
11322                        if type_name.starts_with(|c: char| c.is_uppercase()) =>
11323                    {
11324                        env.terms.insert(store_name.clone());
11325                        env.types.insert(store_name.clone(), type_name.clone());
11326                        return EvalResult::Value(1.0);
11327                    }
11328                    Node::List(_) => {
11329                        env.terms.insert(store_name.clone());
11330                        let type_key = key_of(&rhs[0]);
11331                        env.types.insert(store_name.clone(), type_key);
11332                        eval_node(&rhs[0], env);
11333                        return EvalResult::Value(1.0);
11334                    }
11335                    _ => {}
11336                }
11337            }
11338        }
11339    }
11340
11341    // Optional symbol prior: (a: 0.7)
11342    if rhs.len() == 1 {
11343        if let Node::Leaf(ref val_s) = rhs[0] {
11344            if is_num(val_s) {
11345                let p: f64 = val_s.parse().unwrap_or(0.0);
11346                env.set_symbol_prob(&store_name, p);
11347                return EvalResult::Value(env.to_num(val_s));
11348            }
11349        }
11350    }
11351
11352    // Operator redefinitions
11353    let is_op_name = head == "="
11354        || head == "!="
11355        || head == "and"
11356        || head == "or"
11357        || head == "both"
11358        || head == "neither"
11359        || head == "not"
11360        || head == "is"
11361        || head == "?:"
11362        || head.contains('=')
11363        || head.contains('!');
11364
11365    if is_op_name {
11366        // Operator alias: `(not: not)` inside a namespace exports the existing
11367        // operator under the qualified name, e.g. `classical.not`.
11368        if rhs.len() == 1 {
11369            if let Node::Leaf(ref target) = rhs[0] {
11370                if let Some(op) = env.get_op(target.as_str()).cloned() {
11371                    env.define_op(&store_name, op);
11372                    env.trace("resolve", format!("({}: {})", store_name, target));
11373                    return EvalResult::Value(1.0);
11374                }
11375            }
11376        }
11377
11378        // Composition like: (!=: not =) or (=: =) (no-op)
11379        if rhs.len() == 2 {
11380            if let (Node::Leaf(ref outer), Node::Leaf(ref inner)) = (&rhs[0], &rhs[1]) {
11381                if env.has_op(outer.as_str()) && env.has_op(inner.as_str()) {
11382                    env.define_op(
11383                        &store_name,
11384                        Op::Compose {
11385                            outer: outer.clone(),
11386                            inner: inner.clone(),
11387                        },
11388                    );
11389                    env.trace("resolve", format!("({}: {} {})", store_name, outer, inner));
11390                    return EvalResult::Value(1.0);
11391                }
11392                // Mirror JS behavior: surface a diagnostic for the missing op.
11393                if !env.has_op(outer.as_str()) {
11394                    panic!("Unknown op: {}", outer);
11395                }
11396                if !env.has_op(inner.as_str()) {
11397                    panic!("Unknown op: {}", inner);
11398                }
11399            }
11400        }
11401
11402        // Aggregator selection: (and: avg|min|max|product|probabilistic_sum)
11403        if (head == "and" || head == "or" || head == "both" || head == "neither") && rhs.len() == 1
11404        {
11405            if let Node::Leaf(ref sel) = rhs[0] {
11406                if let Some(agg) = Aggregator::from_name(sel) {
11407                    env.define_op(&store_name, Op::Agg(agg));
11408                    env.trace("resolve", format!("({}: {})", store_name, sel));
11409                    return EvalResult::Value(1.0);
11410                } else {
11411                    panic!("Unknown aggregator \"{}\"", sel);
11412                }
11413            }
11414        }
11415    }
11416
11417    // Lambda definition: (name: lambda (A x) body)
11418    if rhs.len() >= 2 {
11419        if let Node::Leaf(ref first) = rhs[0] {
11420            if first == "lambda" && rhs.len() == 3 {
11421                if let Some((param_name, param_type)) = parse_binding(&rhs[1]) {
11422                    let body = rhs[2].clone();
11423                    env.terms.insert(store_name.clone());
11424                    let had_param_term = env.terms.contains(&param_name);
11425                    let previous_param_type = env.get_type(&param_name).cloned();
11426                    env.terms.insert(param_name.clone());
11427                    env.set_type(&param_name, &param_type);
11428                    let body_key = key_of(&body);
11429                    let body_type =
11430                        env.get_type(&body_key)
11431                            .cloned()
11432                            .unwrap_or_else(|| match &body {
11433                                Node::Leaf(s) => s.clone(),
11434                                other => key_of(other),
11435                            });
11436                    if !had_param_term {
11437                        env.terms.remove(&param_name);
11438                    }
11439                    if let Some(previous) = previous_param_type {
11440                        env.set_type(&param_name, &previous);
11441                    } else {
11442                        env.types.remove(&param_name);
11443                    }
11444                    env.set_type(
11445                        &store_name,
11446                        &format!("(Pi ({} {}) {})", param_type, param_name, body_type),
11447                    );
11448                    env.set_lambda(
11449                        &store_name,
11450                        Lambda {
11451                            param: param_name,
11452                            param_type,
11453                            body,
11454                        },
11455                    );
11456                    return EvalResult::Value(1.0);
11457                }
11458            }
11459        }
11460    }
11461
11462    // Typed declaration with complex type expression: (succ: (Pi (Natural n) Natural))
11463    // Only complex expressions (arrays/lists) are accepted as type annotations in single-element form.
11464    // Simple name type annotations like (x: Natural) are NOT supported — use (x: Natural x) prefix form instead.
11465    if rhs.len() == 1 {
11466        let is_op = head == "="
11467            || head == "!="
11468            || head == "and"
11469            || head == "or"
11470            || head == "both"
11471            || head == "neither"
11472            || head == "not"
11473            || head == "is"
11474            || head == "?:"
11475            || head.contains('=')
11476            || head.contains('!');
11477
11478        if !is_op {
11479            if let Node::List(_) = &rhs[0] {
11480                env.terms.insert(store_name.clone());
11481                let type_key = key_of(&rhs[0]);
11482                env.set_type(&store_name, &type_key);
11483                eval_node(&rhs[0], env);
11484                return EvalResult::Value(1.0);
11485            }
11486        }
11487    }
11488
11489    // Generic symbol alias like (x: y) just copies y's prior probability if any
11490    if rhs.len() == 1 {
11491        if let Node::Leaf(ref sym) = rhs[0] {
11492            let prob = env.get_symbol_prob(sym);
11493            env.set_symbol_prob(&store_name, prob);
11494            return EvalResult::Value(env.get_symbol_prob(&store_name));
11495        }
11496    }
11497
11498    // Else: ignore (keeps PoC minimal)
11499    EvalResult::Value(0.0)
11500}
11501
11502/// Emit a shadowing warning (E008) if the name being defined was previously
11503/// brought in via `(import ...)`. The import handler tracks names it added to
11504/// the environment in `env.imported`; the importing file's own definitions are
11505/// not in that set, so re-binding them locally never triggers the warning.
11506/// Diagnostics are appended to `env.shadow_diagnostics` and surfaced by the
11507/// outer `evaluate_inner` boundary alongside other diagnostics.
11508fn maybe_warn_shadow(env: &mut Env, name: &str) {
11509    // Resolve the name through alias mappings so a re-binding like `(cl.and: ...)`
11510    // matches the canonical imported key `classical.and`.
11511    let key = if env.imported.contains(name) {
11512        name.to_string()
11513    } else {
11514        let resolved = env.resolve_qualified(name);
11515        if resolved != name && env.imported.contains(&resolved) {
11516            resolved
11517        } else {
11518            return;
11519        }
11520    };
11521    // Only warn once per name to keep noise down; remove from imported so the
11522    // shadow only fires the first time it's rebinding.
11523    env.imported.remove(&key);
11524    let span = env
11525        .current_span
11526        .clone()
11527        .unwrap_or_else(|| env.default_span.clone());
11528    let diag = Diagnostic::new(
11529        "E008",
11530        format!("Definition of \"{}\" shadows an imported binding", name),
11531        span,
11532    );
11533    env.shadow_diagnostics.push(diag);
11534}
11535
11536// ========== Meta-expression Adapter ==========
11537
11538/// Selected interpretation supplied by a consumer such as meta-expression.
11539#[derive(Debug, Clone, PartialEq)]
11540pub struct Interpretation {
11541    pub kind: String,
11542    pub expression: Option<String>,
11543    pub summary: Option<String>,
11544    pub lino: Option<String>,
11545}
11546
11547impl Interpretation {
11548    pub fn arithmetic_equality(expression: &str) -> Self {
11549        Self {
11550            kind: "arithmetic-equality".to_string(),
11551            expression: Some(expression.to_string()),
11552            summary: None,
11553            lino: None,
11554        }
11555    }
11556
11557    pub fn arithmetic_question(expression: &str) -> Self {
11558        Self {
11559            kind: "arithmetic-question".to_string(),
11560            expression: Some(expression.to_string()),
11561            summary: None,
11562            lino: None,
11563        }
11564    }
11565
11566    pub fn real_world_claim(summary: &str) -> Self {
11567        Self {
11568            kind: "real-world-claim".to_string(),
11569            expression: None,
11570            summary: Some(summary.to_string()),
11571            lino: None,
11572        }
11573    }
11574
11575    pub fn lino(expression: &str) -> Self {
11576        Self {
11577            kind: "lino".to_string(),
11578            expression: None,
11579            summary: None,
11580            lino: Some(expression.to_string()),
11581        }
11582    }
11583}
11584
11585/// Explicit dependency record used to keep unsupported claims partial.
11586#[derive(Debug, Clone, PartialEq)]
11587pub struct Dependency {
11588    pub id: String,
11589    pub status: String,
11590    pub description: String,
11591}
11592
11593impl Dependency {
11594    pub fn missing(id: &str, description: &str) -> Self {
11595        Self {
11596            id: id.to_string(),
11597            status: "missing".to_string(),
11598            description: description.to_string(),
11599        }
11600    }
11601}
11602
11603/// Request object for `formalize_selected_interpretation`.
11604#[derive(Debug, Clone, PartialEq)]
11605pub struct FormalizationRequest {
11606    pub text: String,
11607    pub interpretation: Interpretation,
11608    pub formal_system: String,
11609    pub dependencies: Vec<Dependency>,
11610}
11611
11612/// A dependency-aware RML formalization.
11613#[derive(Debug, Clone, PartialEq)]
11614pub struct Formalization {
11615    pub source_text: String,
11616    pub interpretation: Interpretation,
11617    pub formal_system: String,
11618    pub dependencies: Vec<Dependency>,
11619    pub computable: bool,
11620    pub formalization_level: u8,
11621    pub unknowns: Vec<String>,
11622    pub value_kind: String,
11623    pub ast: Option<Node>,
11624    pub lino: Option<String>,
11625}
11626
11627/// Result value from evaluating a formalization.
11628#[derive(Debug, Clone, PartialEq)]
11629pub enum FormalizationResultValue {
11630    Number(f64),
11631    TruthValue(f64),
11632    Type(String),
11633    Partial(String),
11634}
11635
11636/// Evaluation result for the meta-expression adapter.
11637#[derive(Debug, Clone, PartialEq)]
11638pub struct FormalizationEvaluation {
11639    pub computable: bool,
11640    pub formalization_level: u8,
11641    pub unknowns: Vec<String>,
11642    pub result: FormalizationResultValue,
11643}
11644
11645fn normalize_question_expression(text: &str) -> String {
11646    let mut out = text.trim().trim_end_matches('?').trim().to_string();
11647    let lower = out.to_lowercase();
11648    if lower.starts_with("what is ") {
11649        out = out[8..].trim().to_string();
11650    }
11651    out
11652}
11653
11654fn split_top_level_equals(expression: &str) -> Option<(String, String)> {
11655    let mut depth: i32 = 0;
11656    let chars: Vec<char> = expression.chars().collect();
11657    for (i, c) in chars.iter().enumerate() {
11658        match c {
11659            '(' => depth += 1,
11660            ')' => depth -= 1,
11661            '=' if depth == 0 => {
11662                if i > 0 && chars[i - 1] == '!' {
11663                    continue;
11664                }
11665                if i + 1 < chars.len() && chars[i + 1] == '=' {
11666                    continue;
11667                }
11668                let left: String = chars[..i].iter().collect();
11669                let right: String = chars[i + 1..].iter().collect();
11670                return Some((left.trim().to_string(), right.trim().to_string()));
11671            }
11672            _ => {}
11673        }
11674    }
11675    None
11676}
11677
11678fn parse_expression_shape(expression: &str, unwrap_single: bool) -> Result<Node, String> {
11679    let trimmed = expression.trim();
11680    if trimmed.is_empty() {
11681        return Err("empty expression".to_string());
11682    }
11683    let source = if trimmed.starts_with('(') && trimmed.ends_with(')') {
11684        trimmed.to_string()
11685    } else {
11686        format!("({})", trimmed)
11687    };
11688    let mut ast = parse_one(&tokenize_one(&source))?;
11689    loop {
11690        match ast {
11691            Node::List(ref children) if children.len() == 1 => {
11692                if unwrap_single || matches!(&children[0], Node::List(_)) {
11693                    ast = children[0].clone();
11694                    continue;
11695                }
11696                return Ok(ast);
11697            }
11698            _ => return Ok(ast),
11699        }
11700    }
11701}
11702
11703fn unique_unknowns(unknowns: Vec<String>) -> Vec<String> {
11704    let mut out = Vec::new();
11705    for unknown in unknowns {
11706        if !out.contains(&unknown) {
11707            out.push(unknown);
11708        }
11709    }
11710    out
11711}
11712
11713fn partial_formalization(
11714    request: FormalizationRequest,
11715    unknowns: Vec<String>,
11716    formalization_level: u8,
11717) -> Formalization {
11718    Formalization {
11719        source_text: request.text,
11720        interpretation: request.interpretation,
11721        formal_system: request.formal_system,
11722        dependencies: request.dependencies,
11723        computable: false,
11724        formalization_level,
11725        unknowns: unique_unknowns(unknowns),
11726        value_kind: "partial".to_string(),
11727        ast: None,
11728        lino: None,
11729    }
11730}
11731
11732fn build_arithmetic_formalization(
11733    expression: &str,
11734    value_kind: &str,
11735) -> Result<(Node, String), String> {
11736    let ast = if value_kind == "truth-value" {
11737        if let Some((left, right)) = split_top_level_equals(expression) {
11738            Node::List(vec![
11739                parse_expression_shape(&left, true)?,
11740                Node::Leaf("=".to_string()),
11741                parse_expression_shape(&right, true)?,
11742            ])
11743        } else {
11744            parse_expression_shape(expression, true)?
11745        }
11746    } else {
11747        parse_expression_shape(expression, true)?
11748    };
11749    let lino = key_of(&ast);
11750    Ok((ast, lino))
11751}
11752
11753/// Convert an explicitly selected interpretation into an executable or partial RML formalization.
11754pub fn formalize_selected_interpretation(request: FormalizationRequest) -> Formalization {
11755    let kind = request.interpretation.kind.to_lowercase();
11756    let raw_expression = request
11757        .interpretation
11758        .expression
11759        .clone()
11760        .or_else(|| request.interpretation.lino.clone())
11761        .unwrap_or_else(|| normalize_question_expression(&request.text));
11762    let can_use_arithmetic = request.formal_system == "rml-arithmetic"
11763        || request.formal_system == "arithmetic"
11764        || kind.starts_with("arithmetic");
11765
11766    if can_use_arithmetic && !raw_expression.is_empty() {
11767        let value_kind =
11768            if kind.contains("equal") || split_top_level_equals(&raw_expression).is_some() {
11769                "truth-value"
11770            } else {
11771                "number"
11772            };
11773        match build_arithmetic_formalization(&raw_expression, value_kind) {
11774            Ok((ast, lino)) => Formalization {
11775                source_text: request.text,
11776                interpretation: request.interpretation,
11777                formal_system: request.formal_system,
11778                dependencies: request.dependencies,
11779                computable: true,
11780                formalization_level: 3,
11781                unknowns: vec![],
11782                value_kind: value_kind.to_string(),
11783                ast: Some(ast),
11784                lino: Some(lino),
11785            },
11786            Err(error) => partial_formalization(
11787                request,
11788                vec!["unsupported-arithmetic-shape".to_string(), error],
11789                1,
11790            ),
11791        }
11792    } else if request.interpretation.lino.is_some() && !raw_expression.is_empty() {
11793        match parse_expression_shape(&raw_expression, false) {
11794            Ok(ast) => {
11795                let lino = key_of(&ast);
11796                Formalization {
11797                    source_text: request.text,
11798                    interpretation: request.interpretation,
11799                    formal_system: request.formal_system,
11800                    dependencies: request.dependencies,
11801                    computable: true,
11802                    formalization_level: 3,
11803                    unknowns: vec![],
11804                    value_kind: if matches!(&ast, Node::List(children) if matches!(children.first(), Some(Node::Leaf(head)) if head == "?"))
11805                    {
11806                        "query".to_string()
11807                    } else {
11808                        "truth-value".to_string()
11809                    },
11810                    ast: Some(ast),
11811                    lino: Some(lino),
11812                }
11813            }
11814            Err(error) => partial_formalization(
11815                request,
11816                vec!["unsupported-lino-shape".to_string(), error],
11817                1,
11818            ),
11819        }
11820    } else {
11821        let mut unknowns = vec![
11822            "selected-subject".to_string(),
11823            "selected-relation".to_string(),
11824            "evidence-source".to_string(),
11825            "formal-shape".to_string(),
11826        ];
11827        for dependency in &request.dependencies {
11828            if dependency.status == "missing"
11829                || dependency.status == "unknown"
11830                || dependency.status == "partial"
11831            {
11832                unknowns.push(format!("dependency:{}", dependency.id));
11833            }
11834        }
11835        partial_formalization(request, unknowns, 2)
11836    }
11837}
11838
11839/// Evaluate a formalization when it has an executable RML AST.
11840pub fn evaluate_formalization(formalization: &Formalization) -> FormalizationEvaluation {
11841    let Some(ast) = formalization.ast.as_ref() else {
11842        return FormalizationEvaluation {
11843            computable: false,
11844            formalization_level: formalization.formalization_level,
11845            unknowns: formalization.unknowns.clone(),
11846            result: FormalizationResultValue::Partial("unknown".to_string()),
11847        };
11848    };
11849
11850    if !formalization.computable {
11851        return FormalizationEvaluation {
11852            computable: false,
11853            formalization_level: formalization.formalization_level,
11854            unknowns: formalization.unknowns.clone(),
11855            result: FormalizationResultValue::Partial("unknown".to_string()),
11856        };
11857    }
11858
11859    let mut env = Env::new(None);
11860    let evaluated = eval_node(ast, &mut env);
11861    let result = match formalization.value_kind.as_str() {
11862        "truth-value" => FormalizationResultValue::TruthValue(evaluated.as_f64()),
11863        "query" => match evaluated {
11864            EvalResult::TypeQuery(s) => FormalizationResultValue::Type(s),
11865            other => FormalizationResultValue::Number(other.as_f64()),
11866        },
11867        _ => FormalizationResultValue::Number(evaluated.as_f64()),
11868    };
11869
11870    FormalizationEvaluation {
11871        computable: true,
11872        formalization_level: formalization.formalization_level,
11873        unknowns: vec![],
11874        result,
11875    }
11876}
11877
11878// ========== Program extraction (issue #66) ==========
11879
11880/// Supported source-code generation targets for `extract_program`.
11881#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11882pub enum ExtractTarget {
11883    JavaScript,
11884    Rust,
11885}
11886
11887impl ExtractTarget {
11888    pub fn from_name(name: &str) -> Option<Self> {
11889        match name {
11890            "js" | "javascript" => Some(Self::JavaScript),
11891            "rust" | "rs" => Some(Self::Rust),
11892            _ => None,
11893        }
11894    }
11895}
11896
11897#[derive(Debug, Clone)]
11898struct ExtractLambda {
11899    name: String,
11900    params: Vec<String>,
11901    body: Node,
11902}
11903
11904#[derive(Debug, Clone)]
11905struct ExtractTest {
11906    left: Node,
11907    right: Node,
11908}
11909
11910#[derive(Debug, Clone)]
11911struct ExtractProgram {
11912    lambdas: Vec<ExtractLambda>,
11913    tests: Vec<ExtractTest>,
11914}
11915
11916struct ExtractContext<'a> {
11917    target: ExtractTarget,
11918    name_map: &'a HashMap<String, String>,
11919    locals: &'a HashMap<String, String>,
11920}
11921
11922fn extract_compile_error(message: impl Into<String>) -> String {
11923    message.into()
11924}
11925
11926fn extract_is_probability_assignment(node: &Node) -> bool {
11927    if let Node::List(children) = node {
11928        if children.len() == 4 {
11929            if let (Node::Leaf(w1), Node::Leaf(w2)) = (&children[1], &children[2]) {
11930                return w1 == "has" && w2 == "probability";
11931            }
11932        }
11933    }
11934    false
11935}
11936
11937fn extract_is_query_form(node: &Node) -> bool {
11938    matches!(node, Node::List(children) if matches!(children.first(), Some(Node::Leaf(head)) if head == "?"))
11939}
11940
11941fn extract_is_lambda_definition(node: &Node) -> bool {
11942    if let Node::List(children) = node {
11943        if children.len() >= 3 {
11944            return matches!(&children[0], Node::Leaf(head) if head.ends_with(':'))
11945                && matches!(&children[1], Node::Leaf(head) if head == "lambda")
11946                && matches!(&children[2], Node::List(_));
11947        }
11948    }
11949    false
11950}
11951
11952fn extract_is_type_only_form(node: &Node) -> bool {
11953    let children = match node {
11954        Node::List(children) => children,
11955        Node::Leaf(_) => return true,
11956    };
11957    if children.is_empty() {
11958        return true;
11959    }
11960    if matches!(&children[0], Node::Leaf(head) if head == "Type" || head == "Prop" || head == "Pi")
11961    {
11962        return true;
11963    }
11964    let head = match &children[0] {
11965        Node::Leaf(head) if head.ends_with(':') => &head[..head.len() - 1],
11966        _ => return false,
11967    };
11968    let rhs = &children[1..];
11969    if rhs.len() == 2 {
11970        if let Node::Leaf(last) = &rhs[1] {
11971            if last == head {
11972                return true;
11973            }
11974        }
11975    }
11976    if rhs.len() == 3 {
11977        if let (Node::Leaf(r0), Node::Leaf(r1), Node::Leaf(r2)) = (&rhs[0], &rhs[1], &rhs[2]) {
11978            if r0 == head && r1 == "is" && r2 == head {
11979                return true;
11980            }
11981        }
11982    }
11983    rhs.len() == 1 && matches!(&rhs[0], Node::List(_))
11984}
11985
11986fn extract_logic_token(token: &str) -> bool {
11987    matches!(
11988        token,
11989        "and" | "or" | "not" | "both" | "neither" | "has" | "probability"
11990    )
11991}
11992
11993fn extract_contains_logic(node: &Node) -> bool {
11994    match node {
11995        Node::Leaf(s) => extract_logic_token(s),
11996        Node::List(children) => {
11997            extract_is_probability_assignment(node) || children.iter().any(extract_contains_logic)
11998        }
11999    }
12000}
12001
12002fn extract_special_form(head: &str) -> bool {
12003    matches!(
12004        head,
12005        "range"
12006            | "valence"
12007            | "mode"
12008            | "relation"
12009            | "world"
12010            | "total"
12011            | "coverage"
12012            | "terminating"
12013            | "coinductive"
12014            | "template"
12015            | "import"
12016            | "namespace"
12017    )
12018}
12019
12020fn extract_parse_forms(text: &str) -> Result<Vec<Node>, String> {
12021    let mut forms = Vec::new();
12022    for link in parse_lino(text) {
12023        let trimmed = link.trim();
12024        if trimmed.starts_with("(#") && trimmed.chars().nth(2).map_or(false, |c| c.is_whitespace())
12025        {
12026            continue;
12027        }
12028        let toks = tokenize_one(&link);
12029        let node = parse_one(&toks).map_err(extract_compile_error)?;
12030        forms.push(desugar_hoas(node));
12031    }
12032    Ok(forms)
12033}
12034
12035fn extract_lambda_declaration(form: &Node) -> Result<ExtractLambda, String> {
12036    let children = match form {
12037        Node::List(children) => children,
12038        _ => return Err(extract_compile_error("Malformed lambda definition")),
12039    };
12040    let name = match &children[0] {
12041        Node::Leaf(head) if head.ends_with(':') => head[..head.len() - 1].to_string(),
12042        _ => return Err(extract_compile_error("Malformed lambda definition head")),
12043    };
12044    if children.len() != 4 {
12045        return Err(extract_compile_error(format!(
12046            "Cannot extract \"{}\": lambda definitions must have one body",
12047            name
12048        )));
12049    }
12050    let bindings = parse_bindings(&children[2]).ok_or_else(|| {
12051        extract_compile_error(format!(
12052            "Cannot extract \"{}\": malformed lambda binding",
12053            name
12054        ))
12055    })?;
12056    Ok(ExtractLambda {
12057        name,
12058        params: bindings.into_iter().map(|(param, _)| param).collect(),
12059        body: children[3].clone(),
12060    })
12061}
12062
12063fn extract_parse_query(form: &Node) -> Result<ExtractTest, String> {
12064    let children = match form {
12065        Node::List(children) => children,
12066        _ => {
12067            return Err(extract_compile_error(format!(
12068                "Cannot extract query \"{}\"",
12069                key_of(form)
12070            )))
12071        }
12072    };
12073    let parts = strip_with_proof(&children[1..]);
12074    let target = if parts.len() == 1 {
12075        parts[0].clone()
12076    } else {
12077        Node::List(parts.to_vec())
12078    };
12079    if let Node::List(target_children) = target {
12080        if target_children.len() == 3 {
12081            if matches!(&target_children[1], Node::Leaf(op) if op == "=") {
12082                return Ok(ExtractTest {
12083                    left: target_children[0].clone(),
12084                    right: target_children[2].clone(),
12085                });
12086            }
12087        }
12088    }
12089    Err(extract_compile_error(format!(
12090        "Cannot extract query \"{}\"; expected (? (<left> = <right>))",
12091        key_of(form)
12092    )))
12093}
12094
12095fn extract_parse_program(text: &str) -> Result<ExtractProgram, String> {
12096    let forms = extract_parse_forms(text)?;
12097    let mut lambdas = Vec::new();
12098    let mut tests = Vec::new();
12099    for form in forms {
12100        if extract_is_probability_assignment(&form) {
12101            return Err(extract_compile_error(
12102                "Cannot extract probability assignments",
12103            ));
12104        }
12105        if extract_is_lambda_definition(&form) {
12106            let lambda = extract_lambda_declaration(&form)?;
12107            if extract_contains_logic(&lambda.body) {
12108                return Err(extract_compile_error(format!(
12109                    "Cannot extract probabilistic or logical lambda \"{}\"",
12110                    lambda.name
12111                )));
12112            }
12113            lambdas.push(lambda);
12114            continue;
12115        }
12116        if extract_is_query_form(&form) {
12117            tests.push(extract_parse_query(&form)?);
12118            continue;
12119        }
12120        if let Node::List(children) = &form {
12121            if let Some(Node::Leaf(raw_head)) = children.first() {
12122                let head = raw_head.strip_suffix(':').unwrap_or(raw_head.as_str());
12123                if extract_special_form(head)
12124                    || matches!(head, "and" | "or" | "not" | "both" | "neither" | "=" | "!=")
12125                {
12126                    return Err(extract_compile_error(format!(
12127                        "Cannot extract unsupported form \"{}\"",
12128                        key_of(&form)
12129                    )));
12130                }
12131            }
12132        }
12133        if !extract_is_type_only_form(&form) {
12134            return Err(extract_compile_error(format!(
12135                "Cannot extract unsupported form \"{}\"",
12136                key_of(&form)
12137            )));
12138        }
12139    }
12140    if lambdas.is_empty() {
12141        return Err(extract_compile_error(
12142            "Cannot extract program: no lambda definitions found",
12143        ));
12144    }
12145    Ok(ExtractProgram { lambdas, tests })
12146}
12147
12148fn extract_identifier(name: &str, target: ExtractTarget, used: &mut HashSet<String>) -> String {
12149    let mut out: String = name
12150        .chars()
12151        .map(|c| {
12152            if c.is_ascii_alphanumeric() || c == '_' {
12153                c
12154            } else {
12155                '_'
12156            }
12157        })
12158        .collect();
12159    if out.is_empty() || out.chars().next().map_or(false, |c| c.is_ascii_digit()) {
12160        out.insert(0, '_');
12161    }
12162    let reserved = match target {
12163        ExtractTarget::JavaScript => matches!(
12164            out.as_str(),
12165            "await"
12166                | "break"
12167                | "case"
12168                | "catch"
12169                | "class"
12170                | "const"
12171                | "continue"
12172                | "debugger"
12173                | "default"
12174                | "delete"
12175                | "do"
12176                | "else"
12177                | "export"
12178                | "extends"
12179                | "finally"
12180                | "for"
12181                | "function"
12182                | "if"
12183                | "import"
12184                | "in"
12185                | "instanceof"
12186                | "let"
12187                | "new"
12188                | "return"
12189                | "super"
12190                | "switch"
12191                | "this"
12192                | "throw"
12193                | "try"
12194                | "typeof"
12195                | "var"
12196                | "void"
12197                | "while"
12198                | "with"
12199                | "yield"
12200        ),
12201        ExtractTarget::Rust => matches!(
12202            out.as_str(),
12203            "as" | "break"
12204                | "const"
12205                | "continue"
12206                | "crate"
12207                | "else"
12208                | "enum"
12209                | "extern"
12210                | "false"
12211                | "fn"
12212                | "for"
12213                | "if"
12214                | "impl"
12215                | "in"
12216                | "let"
12217                | "loop"
12218                | "match"
12219                | "mod"
12220                | "move"
12221                | "mut"
12222                | "pub"
12223                | "ref"
12224                | "return"
12225                | "self"
12226                | "Self"
12227                | "static"
12228                | "struct"
12229                | "super"
12230                | "trait"
12231                | "true"
12232                | "type"
12233                | "unsafe"
12234                | "use"
12235                | "where"
12236                | "while"
12237                | "async"
12238                | "await"
12239                | "dyn"
12240        ),
12241    };
12242    if reserved {
12243        out.push('_');
12244    }
12245    let base = out.clone();
12246    let mut i = 2usize;
12247    while used.contains(&out) {
12248        out = format!("{}_{}", base, i);
12249        i += 1;
12250    }
12251    used.insert(out.clone());
12252    out
12253}
12254
12255fn extract_name_map(names: &[String], target: ExtractTarget) -> HashMap<String, String> {
12256    let mut used = HashSet::new();
12257    let mut out = HashMap::new();
12258    for name in names {
12259        out.insert(name.clone(), extract_identifier(name, target, &mut used));
12260    }
12261    out
12262}
12263
12264fn extract_number_literal(token: &str, target: ExtractTarget) -> String {
12265    if target == ExtractTarget::Rust && token.parse::<i64>().is_ok() {
12266        format!("{}.0", token)
12267    } else {
12268        token.to_string()
12269    }
12270}
12271
12272fn extract_collect_apply_spine<'a>(node: &'a Node) -> (&'a Node, Vec<&'a Node>) {
12273    let mut args = Vec::new();
12274    let mut head = node;
12275    loop {
12276        match head {
12277            Node::List(children)
12278                if children.len() == 3
12279                    && matches!(&children[0], Node::Leaf(apply) if apply == "apply") =>
12280            {
12281                args.push(&children[2]);
12282                head = &children[1];
12283            }
12284            _ => break,
12285        }
12286    }
12287    args.reverse();
12288    (head, args)
12289}
12290
12291fn extract_compile_expr(node: &Node, ctx: &ExtractContext<'_>) -> Result<String, String> {
12292    match node {
12293        Node::Leaf(s) => {
12294            if is_num(s) {
12295                return Ok(extract_number_literal(s, ctx.target));
12296            }
12297            if let Some(local) = ctx.locals.get(s) {
12298                return Ok(local.clone());
12299            }
12300            if let Some(name) = ctx.name_map.get(s) {
12301                return Ok(name.clone());
12302            }
12303            Err(extract_compile_error(format!(
12304                "Cannot extract unresolved symbol \"{}\"",
12305                s
12306            )))
12307        }
12308        Node::List(children) => {
12309            if children.is_empty() {
12310                return Err(extract_compile_error(
12311                    "Cannot extract malformed expression \"()\"",
12312                ));
12313            }
12314            if extract_contains_logic(node) {
12315                return Err(extract_compile_error(format!(
12316                    "Cannot extract probabilistic or logical expression \"{}\"",
12317                    key_of(node)
12318                )));
12319            }
12320            if children.len() == 3 {
12321                if let Node::Leaf(op) = &children[1] {
12322                    if matches!(op.as_str(), "+" | "-" | "*" | "/") {
12323                        return Ok(format!(
12324                            "({} {} {})",
12325                            extract_compile_expr(&children[0], ctx)?,
12326                            op,
12327                            extract_compile_expr(&children[2], ctx)?
12328                        ));
12329                    }
12330                }
12331            }
12332            if children.len() == 3 && matches!(&children[0], Node::Leaf(head) if head == "apply") {
12333                let (head, args) = extract_collect_apply_spine(node);
12334                let fn_name = match head {
12335                    Node::Leaf(_) => extract_compile_expr(head, ctx)?,
12336                    _ => {
12337                        return Err(extract_compile_error(format!(
12338                            "Cannot extract higher-order application \"{}\"",
12339                            key_of(node)
12340                        )))
12341                    }
12342                };
12343                let compiled_args: Result<Vec<String>, String> = args
12344                    .iter()
12345                    .map(|arg| extract_compile_expr(arg, ctx))
12346                    .collect();
12347                return Ok(format!("{}({})", fn_name, compiled_args?.join(", ")));
12348            }
12349            if let Some(Node::Leaf(head)) = children.first() {
12350                if let Some(fn_name) = ctx.name_map.get(head) {
12351                    let compiled_args: Result<Vec<String>, String> = children[1..]
12352                        .iter()
12353                        .map(|arg| extract_compile_expr(arg, ctx))
12354                        .collect();
12355                    return Ok(format!("{}({})", fn_name, compiled_args?.join(", ")));
12356                }
12357            }
12358            Err(extract_compile_error(format!(
12359                "Cannot extract expression \"{}\"",
12360                key_of(node)
12361            )))
12362        }
12363    }
12364}
12365
12366fn compile_javascript_program(program: &ExtractProgram) -> Result<String, String> {
12367    let names: Vec<String> = program.lambdas.iter().map(|l| l.name.clone()).collect();
12368    let name_map = extract_name_map(&names, ExtractTarget::JavaScript);
12369    let mut lines = vec![
12370        "// Generated by rml extract js. Do not edit by hand.".to_string(),
12371        "import { pathToFileURL } from 'node:url';".to_string(),
12372        String::new(),
12373    ];
12374    for lambda in &program.lambdas {
12375        let mut used: HashSet<String> = name_map.values().cloned().collect();
12376        let mut locals = HashMap::new();
12377        for param in &lambda.params {
12378            locals.insert(
12379                param.clone(),
12380                extract_identifier(param, ExtractTarget::JavaScript, &mut used),
12381            );
12382        }
12383        let ctx = ExtractContext {
12384            target: ExtractTarget::JavaScript,
12385            name_map: &name_map,
12386            locals: &locals,
12387        };
12388        let params = lambda
12389            .params
12390            .iter()
12391            .map(|param| locals.get(param).cloned().unwrap_or_else(|| param.clone()))
12392            .collect::<Vec<_>>()
12393            .join(", ");
12394        lines.push(format!(
12395            "export function {}({}) {{",
12396            name_map.get(&lambda.name).unwrap(),
12397            params
12398        ));
12399        lines.push(format!(
12400            "  return {};",
12401            extract_compile_expr(&lambda.body, &ctx)?
12402        ));
12403        lines.push("}".to_string());
12404        lines.push(String::new());
12405    }
12406    lines.push("function __rmlApproxEq(left, right) {".to_string());
12407    lines.push("  return Object.is(left, right) || Math.abs(left - right) <= 1e-9;".to_string());
12408    lines.push("}".to_string());
12409    lines.push(String::new());
12410    lines.push("export function __runRmlExtractedTests() {".to_string());
12411    if program.tests.is_empty() {
12412        lines.push("  return true;".to_string());
12413    } else {
12414        for (idx, test) in program.tests.iter().enumerate() {
12415            let locals = HashMap::new();
12416            let ctx = ExtractContext {
12417                target: ExtractTarget::JavaScript,
12418                name_map: &name_map,
12419                locals: &locals,
12420            };
12421            lines.push(format!(
12422                "  if (!__rmlApproxEq({}, {})) {{",
12423                extract_compile_expr(&test.left, &ctx)?,
12424                extract_compile_expr(&test.right, &ctx)?
12425            ));
12426            lines.push(format!(
12427                "    throw new Error('RML extracted test {} failed');",
12428                idx + 1
12429            ));
12430            lines.push("  }".to_string());
12431        }
12432        lines.push("  return true;".to_string());
12433    }
12434    lines.push("}".to_string());
12435    lines.push(String::new());
12436    lines.push(
12437        "if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {"
12438            .to_string(),
12439    );
12440    lines.push("  __runRmlExtractedTests();".to_string());
12441    lines.push("}".to_string());
12442    lines.push(String::new());
12443    Ok(lines.join("\n"))
12444}
12445
12446fn compile_rust_program(program: &ExtractProgram) -> Result<String, String> {
12447    let names: Vec<String> = program.lambdas.iter().map(|l| l.name.clone()).collect();
12448    let name_map = extract_name_map(&names, ExtractTarget::Rust);
12449    let mut lines = vec![
12450        "// Generated by rml extract rust. Do not edit by hand.".to_string(),
12451        String::new(),
12452    ];
12453    for lambda in &program.lambdas {
12454        let mut used: HashSet<String> = name_map.values().cloned().collect();
12455        let mut locals = HashMap::new();
12456        for param in &lambda.params {
12457            locals.insert(
12458                param.clone(),
12459                extract_identifier(param, ExtractTarget::Rust, &mut used),
12460            );
12461        }
12462        let ctx = ExtractContext {
12463            target: ExtractTarget::Rust,
12464            name_map: &name_map,
12465            locals: &locals,
12466        };
12467        let params = lambda
12468            .params
12469            .iter()
12470            .map(|param| format!("{}: f64", locals.get(param).unwrap()))
12471            .collect::<Vec<_>>()
12472            .join(", ");
12473        lines.push(format!(
12474            "pub fn {}({}) -> f64 {{",
12475            name_map.get(&lambda.name).unwrap(),
12476            params
12477        ));
12478        lines.push(format!("    {}", extract_compile_expr(&lambda.body, &ctx)?));
12479        lines.push("}".to_string());
12480        lines.push(String::new());
12481    }
12482    if !program.tests.is_empty() {
12483        lines.push("#[cfg(test)]".to_string());
12484        lines.push("mod tests {".to_string());
12485        lines.push("    use super::*;".to_string());
12486        lines.push(String::new());
12487        lines.push("    fn rml_approx_eq(left: f64, right: f64) -> bool {".to_string());
12488        lines.push("        (left - right).abs() <= 1e-9".to_string());
12489        lines.push("    }".to_string());
12490        lines.push(String::new());
12491        for (idx, test) in program.tests.iter().enumerate() {
12492            let locals = HashMap::new();
12493            let ctx = ExtractContext {
12494                target: ExtractTarget::Rust,
12495                name_map: &name_map,
12496                locals: &locals,
12497            };
12498            lines.push("    #[test]".to_string());
12499            lines.push(format!("    fn rml_query_{}() {{", idx + 1));
12500            lines.push(format!(
12501                "        assert!(rml_approx_eq({}, {}), \"RML query {} failed\");",
12502                extract_compile_expr(&test.left, &ctx)?,
12503                extract_compile_expr(&test.right, &ctx)?,
12504                idx + 1
12505            ));
12506            lines.push("    }".to_string());
12507            lines.push(String::new());
12508        }
12509        lines.push("}".to_string());
12510        lines.push(String::new());
12511    }
12512    Ok(lines.join("\n"))
12513}
12514
12515/// Extract a typed, non-probabilistic lambda program to JavaScript or Rust.
12516///
12517/// The supported fragment erases RML type annotations, compiles named lambda
12518/// definitions to exported functions, compiles `apply` and arithmetic to
12519/// ordinary calls/expressions, and turns equality queries into generated
12520/// tests. Probabilistic assignments and logical/probabilistic operators are
12521/// rejected instead of being given misleading target-language semantics.
12522pub fn extract_program(text: &str, target: ExtractTarget) -> Result<String, String> {
12523    let parsed = extract_parse_program(text)?;
12524    match target {
12525        ExtractTarget::JavaScript => compile_javascript_program(&parsed),
12526        ExtractTarget::Rust => compile_rust_program(&parsed),
12527    }
12528}
12529
12530// ========== Runner ==========
12531
12532/// A result from running a query: a numeric value, a type string, a
12533/// foundation report, or a per-proof report (issue #97).
12534#[derive(Debug, Clone, PartialEq)]
12535pub enum RunResult {
12536    Num(f64),
12537    Type(String),
12538    Foundation(FoundationReport),
12539    Proof(ProofReport),
12540}
12541
12542/// Evaluate a complete LiNo knowledge base and return both results and any
12543/// diagnostics emitted by the parser, evaluator, or type checker.
12544///
12545/// Each diagnostic carries a code (`E001`, `E002`, ...), a message, and a
12546/// source span (1-based line/col).  See `docs/DIAGNOSTICS.md` for the
12547/// full code list.  Errors do not abort evaluation: independent forms
12548/// continue to be processed after a failing one.
12549pub fn evaluate(text: &str, file: Option<&str>, options: Option<EnvOptions>) -> EvaluateResult {
12550    evaluate_with_options(
12551        text,
12552        file,
12553        EvaluateOptions {
12554            env: options,
12555            ..EvaluateOptions::default()
12556        },
12557    )
12558}
12559
12560/// Like `evaluate`, but takes structured `EvaluateOptions`. When
12561/// `options.trace` is true the returned `EvaluateResult.trace` carries a
12562/// deterministic sequence of `TraceEvent` values (operator resolutions,
12563/// assignment lookups, top-level reductions) — one entry per event,
12564/// in source order.
12565pub fn evaluate_with_options(
12566    text: &str,
12567    file: Option<&str>,
12568    options: EvaluateOptions,
12569) -> EvaluateResult {
12570    let mut env = Env::new(options.env.clone());
12571    env.trace_enabled = options.trace;
12572    env.default_span = Span::new(file.map(|s| s.to_string()), 1, 1, 0);
12573    let mut ctx = ImportContext::default();
12574    evaluate_inner(text, file, &mut env, &options, &mut ctx)
12575}
12576
12577/// Variant of [`evaluate`] that runs against a caller-owned `Env` instead of
12578/// allocating a fresh one.  Used by the REPL to preserve state across inputs.
12579pub fn evaluate_with_env(text: &str, file: Option<&str>, env: &mut Env) -> EvaluateResult {
12580    let options = EvaluateOptions::default();
12581    let mut ctx = ImportContext::default();
12582    evaluate_inner(text, file, env, &options, &mut ctx)
12583}
12584
12585/// Read a file from disk and evaluate it, honouring `(import "...")` directives.
12586/// Mirrors `evaluate()` but takes a path on disk; relative imports inside the
12587/// file are resolved against the file's directory. A missing file is reported
12588/// as an `E007` diagnostic instead of an OS error.
12589pub fn evaluate_file(file_path: &str, options: EvaluateOptions) -> EvaluateResult {
12590    let resolved: PathBuf = match fs::canonicalize(file_path) {
12591        Ok(p) => p,
12592        Err(_) => Path::new(file_path).to_path_buf(),
12593    };
12594    let text = match fs::read_to_string(&resolved) {
12595        Ok(t) => t,
12596        Err(err) => {
12597            let diag = Diagnostic::new(
12598                "E007",
12599                format!("Failed to read \"{}\": {}", file_path, err),
12600                Span::new(Some(file_path.to_string()), 1, 1, 0),
12601            );
12602            return EvaluateResult {
12603                results: Vec::new(),
12604                diagnostics: vec![diag],
12605                trace: Vec::new(),
12606                proofs: Vec::new(),
12607                provenance: Vec::new(),
12608            };
12609        }
12610    };
12611    let mut env = Env::new(options.env.clone());
12612    env.trace_enabled = options.trace;
12613    let resolved_str = resolved.to_string_lossy().into_owned();
12614    env.default_span = Span::new(Some(resolved_str.clone()), 1, 1, 0);
12615    let mut ctx = ImportContext::default();
12616    ctx.stack.push(resolved.clone());
12617    ctx.loaded.insert(resolved.clone());
12618    evaluate_inner(&text, Some(&resolved_str), &mut env, &options, &mut ctx)
12619}
12620
12621/// Internal state threaded through nested `(import ...)` evaluations.
12622/// `stack` is the chain of files currently being loaded (for cycle detection);
12623/// `loaded` is the set of canonical paths already evaluated into the current
12624/// env (for diamond-import caching).
12625#[derive(Default)]
12626struct ImportContext {
12627    stack: Vec<PathBuf>,
12628    loaded: HashSet<PathBuf>,
12629}
12630
12631/// Strip surrounding ASCII quotes from a path string. The LiNo parser strips
12632/// `"..."` for most inputs but `'...'` may also appear when whitespace forced
12633/// a quote conversion; either form is accepted.
12634fn unquote_path(s: &str) -> &str {
12635    let bytes = s.as_bytes();
12636    if bytes.len() >= 2
12637        && (bytes[0] == b'"' || bytes[0] == b'\'')
12638        && bytes[bytes.len() - 1] == bytes[0]
12639    {
12640        &s[1..s.len() - 1]
12641    } else {
12642        s
12643    }
12644}
12645
12646/// Resolve an import target relative to the importing file's directory.
12647/// When `importing_file` is `None`, resolve relative to the current working
12648/// directory.
12649fn resolve_import_path(target: &str, importing_file: Option<&str>) -> PathBuf {
12650    let cleaned = unquote_path(target);
12651    let candidate = Path::new(cleaned);
12652    if candidate.is_absolute() {
12653        return candidate.to_path_buf();
12654    }
12655    let base_dir: PathBuf = if let Some(file) = importing_file {
12656        Path::new(file)
12657            .parent()
12658            .map(|p| p.to_path_buf())
12659            .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")))
12660    } else {
12661        std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
12662    };
12663    base_dir.join(candidate)
12664}
12665
12666/// Canonicalise an import path; falls back to the unresolved path when the
12667/// file does not exist (so missing-file diagnostics still carry a meaningful
12668/// path, and cycle keys stay consistent).
12669fn canonicalize_import(p: &Path) -> PathBuf {
12670    fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf())
12671}
12672
12673/// Process a top-level `(import <path>)` directive. Reads the imported file
12674/// and evaluates its contents against the same `env`, threading the import
12675/// context for cycle detection and caching. Returns a `Diagnostic` if the
12676/// import itself fails (cycle, missing file, bad target).
12677///
12678/// When `alias` is Some, the imported file's declared namespace (or the alias
12679/// itself if no namespace was declared) is registered as `aliases[alias] -> ns`
12680/// so qualified references like `(? (alias.foo))` resolve into that namespace.
12681fn handle_import(
12682    target_node: &Node,
12683    alias: Option<&str>,
12684    span: &Span,
12685    importing_file: Option<&str>,
12686    env: &mut Env,
12687    options: &EvaluateOptions,
12688    ctx: &mut ImportContext,
12689    diagnostics: &mut Vec<Diagnostic>,
12690) -> Option<Diagnostic> {
12691    let raw = match target_node {
12692        Node::Leaf(s) => s.clone(),
12693        _ => {
12694            return Some(Diagnostic::new(
12695                "E007",
12696                "Import target must be a string path",
12697                span.clone(),
12698            ));
12699        }
12700    };
12701    let cleaned = unquote_path(&raw);
12702    if cleaned.is_empty() {
12703        return Some(Diagnostic::new(
12704            "E007",
12705            "Import target must be a non-empty string path",
12706            span.clone(),
12707        ));
12708    }
12709
12710    // Validate alias collisions before reading the file.
12711    if let Some(a) = alias {
12712        if env.aliases.contains_key(a) || env.namespace.as_deref() == Some(a) {
12713            return Some(Diagnostic::new(
12714                "E009",
12715                format!(
12716                    "Import alias \"{}\" collides with an existing namespace or alias",
12717                    a
12718                ),
12719                span.clone(),
12720            ));
12721        }
12722    }
12723
12724    let unresolved = resolve_import_path(&raw, importing_file);
12725    let resolved = canonicalize_import(&unresolved);
12726
12727    if ctx.stack.iter().any(|p| p == &resolved) {
12728        let mut chain: Vec<String> = ctx
12729            .stack
12730            .iter()
12731            .map(|p| p.to_string_lossy().into_owned())
12732            .collect();
12733        chain.push(resolved.to_string_lossy().into_owned());
12734        return Some(Diagnostic::new(
12735            "E007",
12736            format!("Import cycle detected: {}", chain.join(" -> ")),
12737            span.clone(),
12738        ));
12739    }
12740
12741    if ctx.loaded.contains(&resolved) {
12742        // For cached re-imports, the imported namespace is already loaded
12743        // into the env. We only need to wire up the alias.
12744        if let Some(a) = alias {
12745            let recorded_ns = env
12746                .file_namespaces
12747                .get(&resolved)
12748                .cloned()
12749                .unwrap_or_else(|| a.to_string());
12750            env.aliases.insert(a.to_string(), recorded_ns);
12751        }
12752        if options.trace {
12753            env.trace_events.push(TraceEvent::new(
12754                "import",
12755                format!("{} (cached)", resolved.to_string_lossy()),
12756                span.clone(),
12757            ));
12758        }
12759        return None;
12760    }
12761
12762    let text = match fs::read_to_string(&resolved) {
12763        Ok(t) => t,
12764        Err(err) => {
12765            return Some(Diagnostic::new(
12766                "E007",
12767                format!("Failed to read import \"{}\": {}", cleaned, err),
12768                span.clone(),
12769            ));
12770        }
12771    };
12772
12773    ctx.loaded.insert(resolved.clone());
12774    ctx.stack.push(resolved.clone());
12775    if options.trace {
12776        env.trace_events.push(TraceEvent::new(
12777            "import",
12778            resolved.to_string_lossy().into_owned(),
12779            span.clone(),
12780        ));
12781    }
12782
12783    // Snapshot bindings so we can diff after the import to learn which names
12784    // were introduced by the imported file. Used to surface E008 when a later
12785    // top-level definition rebinds them.
12786    let before_ops: HashSet<String> = env.ops.keys().cloned().collect();
12787    let before_syms: HashSet<String> = env.symbol_prob.keys().cloned().collect();
12788    let before_terms: HashSet<String> = env.terms.iter().cloned().collect();
12789    let before_lambdas: HashSet<String> = env.lambdas.keys().cloned().collect();
12790    let before_templates: HashSet<String> = env.templates.keys().cloned().collect();
12791    let before_namespace = env.namespace.clone();
12792
12793    let resolved_str = resolved.to_string_lossy().into_owned();
12794    let inner = evaluate_inner(&text, Some(&resolved_str), env, options, ctx);
12795    ctx.stack.pop();
12796
12797    // The imported file may have declared its own (namespace ...) — capture it
12798    // before restoring the importing file's namespace so we can wire up the
12799    // alias and remember the file's namespace for cached re-imports.
12800    let imported_namespace = env.namespace.clone();
12801    env.namespace = before_namespace;
12802    if let Some(ns) = &imported_namespace {
12803        env.file_namespaces.insert(resolved.clone(), ns.clone());
12804    }
12805
12806    // Track which bindings the imported file added so a later top-level
12807    // definition that rebinds them surfaces an E008 shadowing warning.
12808    for k in env.ops.keys() {
12809        if !before_ops.contains(k) {
12810            env.imported.insert(k.clone());
12811        }
12812    }
12813    for k in env.symbol_prob.keys() {
12814        if !before_syms.contains(k) {
12815            env.imported.insert(k.clone());
12816        }
12817    }
12818    for k in env.terms.iter() {
12819        if !before_terms.contains(k) {
12820            env.imported.insert(k.clone());
12821        }
12822    }
12823    for k in env.lambdas.keys() {
12824        if !before_lambdas.contains(k) {
12825            env.imported.insert(k.clone());
12826        }
12827    }
12828    for k in env.templates.keys() {
12829        if !before_templates.contains(k) {
12830            env.imported.insert(k.clone());
12831        }
12832    }
12833
12834    // Wire up the alias once the imported file has finished evaluating. If the
12835    // imported file declared a namespace, alias maps to it; otherwise it maps
12836    // to the alias itself (so qualified refs `alias.x` resolve to `alias.x`).
12837    if let Some(a) = alias {
12838        let target_ns = imported_namespace.unwrap_or_else(|| a.to_string());
12839        env.aliases.insert(a.to_string(), target_ns);
12840    }
12841
12842    for diag in inner.diagnostics {
12843        diagnostics.push(diag);
12844    }
12845    // The inner evaluator drained env.trace_events into inner.trace; restore
12846    // them so the outer call surfaces them in source order.
12847    if options.trace {
12848        env.trace_events.extend(inner.trace);
12849    }
12850    None
12851}
12852
12853// Evaluate a single form inside a `(with-foundation ...)` body. Nested
12854// `(with-foundation ...)`, `(foundation ...)`, and `(foundation-report)`
12855// forms recurse through here so they behave the same way they would at
12856// the top level. Everything else is treated as a query expression.
12857fn eval_foundation_body_form(
12858    form: Node,
12859    span: &Span,
12860    env: &mut Env,
12861    diagnostics: &mut Vec<Diagnostic>,
12862    results: &mut Vec<RunResult>,
12863    proofs: &mut Option<Vec<Option<Node>>>,
12864    provenance: &mut Option<Vec<Option<String>>>,
12865    options: &EvaluateOptions,
12866) {
12867    let mut form = form;
12868    loop {
12869        match form {
12870            Node::List(ref children) if children.len() == 1 => {
12871                if let Node::List(_) = &children[0] {
12872                    form = children[0].clone();
12873                } else {
12874                    break;
12875                }
12876            }
12877            _ => break,
12878        }
12879    }
12880
12881    if let Node::List(children) = &form {
12882        if let Some(Node::Leaf(head)) = children.first() {
12883            if head == "with-foundation" {
12884                if children.len() < 2 {
12885                    diagnostics.push(Diagnostic::new(
12886                        "E062",
12887                        "with-foundation form must be `(with-foundation <name> <body>...)`",
12888                        span.clone(),
12889                    ));
12890                    return;
12891                }
12892                let fname = match &children[1] {
12893                    Node::Leaf(s) if !s.is_empty() => s.clone(),
12894                    _ => {
12895                        diagnostics.push(Diagnostic::new(
12896                            "E062",
12897                            "with-foundation requires a foundation name",
12898                            span.clone(),
12899                        ));
12900                        return;
12901                    }
12902                };
12903                if let Err(message) = env.enter_foundation(&fname) {
12904                    diagnostics.push(Diagnostic::new("E062", message, span.clone()));
12905                    return;
12906                }
12907                if options.trace {
12908                    env.trace_events.push(TraceEvent::new(
12909                        "with-foundation/enter",
12910                        fname.clone(),
12911                        span.clone(),
12912                    ));
12913                }
12914                let bodies: Vec<Node> = children[2..].to_vec();
12915                for body in bodies {
12916                    eval_foundation_body_form(
12917                        body,
12918                        span,
12919                        env,
12920                        diagnostics,
12921                        results,
12922                        proofs,
12923                        provenance,
12924                        options,
12925                    );
12926                }
12927                env.exit_foundation();
12928                if options.trace {
12929                    env.trace_events.push(TraceEvent::new(
12930                        "with-foundation/exit",
12931                        fname,
12932                        span.clone(),
12933                    ));
12934                }
12935                return;
12936            }
12937            if head == "foundation" {
12938                match parse_foundation_form(&form) {
12939                    Ok(foundation) => {
12940                        let name = foundation.name.clone();
12941                        if let Err(message) = env.register_foundation(foundation) {
12942                            diagnostics.push(Diagnostic::new("E061", message, span.clone()));
12943                        } else if options.trace {
12944                            env.trace_events.push(TraceEvent::new(
12945                                "foundation",
12946                                name,
12947                                span.clone(),
12948                            ));
12949                        }
12950                    }
12951                    Err(message) => {
12952                        diagnostics.push(Diagnostic::new("E061", message, span.clone()));
12953                    }
12954                }
12955                return;
12956            }
12957            if head == "root-construct" {
12958                match parse_root_construct_form(&form) {
12959                    Ok(descriptor) => {
12960                        let name = descriptor.name.clone();
12961                        if let Err(message) = env.register_root_construct(descriptor) {
12962                            diagnostics.push(Diagnostic::new("E060", message, span.clone()));
12963                        } else if options.trace {
12964                            env.trace_events.push(TraceEvent::new(
12965                                "root-construct",
12966                                name,
12967                                span.clone(),
12968                            ));
12969                        }
12970                    }
12971                    Err(message) => {
12972                        diagnostics.push(Diagnostic::new("E060", message, span.clone()));
12973                    }
12974                }
12975                return;
12976            }
12977            if head == "foundation-report" || head == "foundation-report?" {
12978                let report = env.foundation_report();
12979                if options.trace {
12980                    env.trace_events.push(TraceEvent::new(
12981                        "foundation-report",
12982                        report.active_foundation.clone(),
12983                        span.clone(),
12984                    ));
12985                }
12986                results.push(RunResult::Foundation(report));
12987                if let Some(p) = proofs.as_mut() {
12988                    p.push(None);
12989                }
12990                if let Some(pv) = provenance.as_mut() {
12991                    pv.push(None);
12992                }
12993                return;
12994            }
12995            if head == "rule" && is_proof_rule_shape(children) {
12996                match parse_rule_form(&form) {
12997                    Ok(rule) => {
12998                        let name = rule.name.clone();
12999                        env.register_proof_rule(rule);
13000                        if options.trace {
13001                            env.trace_events
13002                                .push(TraceEvent::new("rule", name, span.clone()));
13003                        }
13004                    }
13005                    Err(message) => {
13006                        diagnostics.push(Diagnostic::new("E064", message, span.clone()));
13007                    }
13008                }
13009                return;
13010            }
13011            if head == "assumption" || head == "axiom" {
13012                match parse_proof_assumption_form(&form) {
13013                    Ok(assumption) => {
13014                        let kind = assumption.kind.clone();
13015                        let name = assumption.name.clone();
13016                        env.register_proof_assumption(assumption);
13017                        if options.trace {
13018                            env.trace_events
13019                                .push(TraceEvent::new(&kind, name, span.clone()));
13020                        }
13021                    }
13022                    Err(message) => {
13023                        diagnostics.push(Diagnostic::new("E064", message, span.clone()));
13024                    }
13025                }
13026                return;
13027            }
13028            if head == "proof-object" {
13029                match parse_proof_object_form(&form) {
13030                    Ok(po) => {
13031                        let name = po.name.clone();
13032                        env.register_proof_object(po);
13033                        if options.trace {
13034                            env.trace_events.push(TraceEvent::new(
13035                                "proof-object",
13036                                name,
13037                                span.clone(),
13038                            ));
13039                        }
13040                    }
13041                    Err(message) => {
13042                        diagnostics.push(Diagnostic::new("E064", message, span.clone()));
13043                    }
13044                }
13045                return;
13046            }
13047            if head == "check-proof" {
13048                if children.len() != 2 {
13049                    diagnostics.push(Diagnostic::new(
13050                        "E064",
13051                        "(check-proof <name>) requires a proof-object name",
13052                        span.clone(),
13053                    ));
13054                    return;
13055                }
13056                let target = match &children[1] {
13057                    Node::Leaf(s) if !s.is_empty() => s.clone(),
13058                    _ => {
13059                        diagnostics.push(Diagnostic::new(
13060                            "E064",
13061                            "(check-proof <name>) requires a proof-object name",
13062                            span.clone(),
13063                        ));
13064                        return;
13065                    }
13066                };
13067                let verdict = check_proof_object(env, &target);
13068                let (value, error) = match verdict {
13069                    CheckProofVerdict::Ok(_) => (1.0_f64, None),
13070                    CheckProofVerdict::Err(msg) => (0.0_f64, Some(msg)),
13071                };
13072                results.push(RunResult::Num(value));
13073                if let Some(p) = proofs.as_mut() {
13074                    p.push(None);
13075                }
13076                if let Some(pv) = provenance.as_mut() {
13077                    pv.push(None);
13078                }
13079                if let Some(msg) = error {
13080                    diagnostics.push(Diagnostic::new("E064", msg, span.clone()));
13081                }
13082                if options.trace {
13083                    env.trace_events.push(TraceEvent::new(
13084                        "check-proof",
13085                        format!("{} → {}", target, if value == 1.0 { "ok" } else { "fail" }),
13086                        span.clone(),
13087                    ));
13088                }
13089                return;
13090            }
13091            if head == "proof-report" {
13092                if children.len() != 2 {
13093                    diagnostics.push(Diagnostic::new(
13094                        "E064",
13095                        "(proof-report <name>) requires a proof-object name",
13096                        span.clone(),
13097                    ));
13098                    return;
13099                }
13100                let target = match &children[1] {
13101                    Node::Leaf(s) if !s.is_empty() => s.clone(),
13102                    _ => {
13103                        diagnostics.push(Diagnostic::new(
13104                            "E064",
13105                            "(proof-report <name>) requires a proof-object name",
13106                            span.clone(),
13107                        ));
13108                        return;
13109                    }
13110                };
13111                let report = env.proof_report(&target);
13112                results.push(RunResult::Proof(report));
13113                if let Some(p) = proofs.as_mut() {
13114                    p.push(None);
13115                }
13116                if let Some(pv) = provenance.as_mut() {
13117                    pv.push(None);
13118                }
13119                if options.trace {
13120                    env.trace_events.push(TraceEvent::new(
13121                        "proof-report",
13122                        target,
13123                        span.clone(),
13124                    ));
13125                }
13126                return;
13127            }
13128            if head == "eval-nat" {
13129                if children.len() != 2 {
13130                    diagnostics.push(Diagnostic::new(
13131                        "E067",
13132                        "(eval-nat <term>) requires exactly one term argument",
13133                        span.clone(),
13134                    ));
13135                    return;
13136                }
13137                match eval_nat_term(env, &children[1]) {
13138                    Ok(result) => {
13139                        results.push(RunResult::Num(result.value));
13140                        if let Some(p) = proofs.as_mut() {
13141                            p.push(None);
13142                        }
13143                        if let Some(pv) = provenance.as_mut() {
13144                            pv.push(None);
13145                        }
13146                        if options.trace {
13147                            env.trace_events.push(TraceEvent::new(
13148                                "eval-nat",
13149                                format!(
13150                                    "{} -> normal-form {} -> {}; rules-used: {}; host-primitives-used: structural-matcher; renderer: nat-normal-form-to-host-number",
13151                                    key_of(&children[1]),
13152                                    key_of(&result.normal_form),
13153                                    format_trace_value(result.value),
13154                                    if result.steps.is_empty() {
13155                                        "<none>".to_string()
13156                                    } else {
13157                                        result.steps.join(", ")
13158                                    }
13159                                ),
13160                                span.clone(),
13161                            ));
13162                        }
13163                    }
13164                    Err(message) => {
13165                        diagnostics.push(Diagnostic::new("E067", message, span.clone()));
13166                    }
13167                }
13168                return;
13169            }
13170            if head == "strict-foundation" {
13171                match parse_strict_foundation_form(&form) {
13172                    Ok(decl) => {
13173                        env.strict_pure_links = true;
13174                        if options.trace {
13175                            env.trace_events.push(TraceEvent::new(
13176                                "strict-foundation",
13177                                decl.profile,
13178                                span.clone(),
13179                            ));
13180                        }
13181                    }
13182                    Err(message) => {
13183                        diagnostics.push(Diagnostic::new("E065", message, span.clone()));
13184                    }
13185                }
13186                return;
13187            }
13188            if head == "allow-host-primitive" {
13189                match parse_allow_host_primitive_form(&form) {
13190                    Ok(decl) => {
13191                        for name in &decl.names {
13192                            env.allowed_host_primitives.insert(name.clone());
13193                        }
13194                        if options.trace {
13195                            env.trace_events.push(TraceEvent::new(
13196                                "allow-host-primitive",
13197                                decl.names.join(" "),
13198                                span.clone(),
13199                            ));
13200                        }
13201                    }
13202                    Err(message) => {
13203                        diagnostics.push(Diagnostic::new("E065", message, span.clone()));
13204                    }
13205                }
13206                return;
13207            }
13208        }
13209    }
13210    if let Node::Leaf(head) = &form {
13211        if head == "foundation-report" || head == "foundation-report?" {
13212            let report = env.foundation_report();
13213            if options.trace {
13214                env.trace_events.push(TraceEvent::new(
13215                    "foundation-report",
13216                    report.active_foundation.clone(),
13217                    span.clone(),
13218                ));
13219            }
13220            results.push(RunResult::Foundation(report));
13221            if let Some(p) = proofs.as_mut() {
13222                p.push(None);
13223            }
13224            if let Some(pv) = provenance.as_mut() {
13225                pv.push(None);
13226            }
13227            return;
13228        }
13229    }
13230
13231    let inner_result = catch_unwind(AssertUnwindSafe(|| {
13232        let mut stack = Vec::new();
13233        let expanded = expand_templates(&form, env, &mut stack);
13234        let eval_res = eval_node(&expanded, env);
13235        (expanded, eval_res)
13236    }));
13237    match inner_result {
13238        Ok((expanded, eval_res)) => {
13239            let was_query = matches!(eval_res, EvalResult::Query(_) | EvalResult::TypeQuery(_));
13240            let query_value = if let EvalResult::Query(v) = &eval_res {
13241                Some(*v)
13242            } else {
13243                None
13244            };
13245            match eval_res {
13246                EvalResult::Query(v) => results.push(RunResult::Num(v)),
13247                EvalResult::TypeQuery(s) => results.push(RunResult::Type(s)),
13248                _ => {}
13249            }
13250            if was_query {
13251                if let Some(p) = proofs.as_mut() {
13252                    p.push(None);
13253                }
13254                let prov = equality_provenance_for_query(&expanded, env);
13255                record_provenance(
13256                    provenance,
13257                    results.len(),
13258                    prov,
13259                    env,
13260                    &expanded,
13261                    span,
13262                    options,
13263                );
13264                // Carrier enforcement (issue #97 Section 2): when the active
13265                // foundation strict-carrier is on, a numeric query result
13266                // outside the carrier produces an E063 diagnostic alongside
13267                // the value, so the trace stays explainable without losing
13268                // the result.
13269                if let Some(v) = query_value {
13270                    if let Some(msg) = env.check_carrier_value(v) {
13271                        diagnostics.push(Diagnostic::new(
13272                            "E063",
13273                            format!(
13274                                "Query result {} violates active foundation carrier: {}",
13275                                format_trace_value(v),
13276                                msg
13277                            ),
13278                            span.clone(),
13279                        ));
13280                    }
13281                }
13282                // Pure-links strict mode audit inside with-foundation bodies.
13283                if env.strict_pure_links {
13284                    if let Node::List(form_children) = &expanded {
13285                        if matches!(form_children.first(), Some(Node::Leaf(s)) if s == "?") {
13286                            let parts = &form_children[1..];
13287                            let inner = strip_with_proof(parts);
13288                            let target: Node = if inner.len() == 1 {
13289                                inner[0].clone()
13290                            } else {
13291                                Node::List(inner.to_vec())
13292                            };
13293                            let offenders = scan_pure_links_offenders(&target, env);
13294                            if !offenders.is_empty() {
13295                                diagnostics.push(Diagnostic::new(
13296                                    "E065",
13297                                    format!(
13298                                        "Query depends on host-primitive construct(s) under pure-links strict mode: {}",
13299                                        offenders.join(", ")
13300                                    ),
13301                                    span.clone(),
13302                                ));
13303                            }
13304                        }
13305                    }
13306                }
13307            }
13308        }
13309        Err(payload) => {
13310            let (code, message) = decode_panic_payload(&payload);
13311            diagnostics.push(Diagnostic::new(&code, message, span.clone()));
13312        }
13313    }
13314}
13315
13316/// Append a per-query provenance entry, lazily allocating the vector on the
13317/// first non-`None` rule (mirrors JS's `out.provenance` shape). When `rule`
13318/// is `Some`, also emits an `equality-layer` trace event so tracing tools
13319/// can attribute each classification to its source span.
13320fn record_provenance(
13321    provenance: &mut Option<Vec<Option<String>>>,
13322    results_len: usize,
13323    rule: Option<String>,
13324    env: &mut Env,
13325    _form: &Node,
13326    span: &Span,
13327    options: &EvaluateOptions,
13328) {
13329    if let Some(rule_name) = rule {
13330        if provenance.is_none() {
13331            let backfill = results_len.saturating_sub(1);
13332            *provenance = Some(vec![None; backfill]);
13333        }
13334        provenance.as_mut().unwrap().push(Some(rule_name.clone()));
13335        if options.trace {
13336            env.trace_events.push(TraceEvent::new(
13337                "equality-layer",
13338                rule_name,
13339                span.clone(),
13340            ));
13341        }
13342    } else if let Some(pv) = provenance.as_mut() {
13343        pv.push(None);
13344    }
13345}
13346
13347fn evaluate_inner(
13348    text: &str,
13349    file: Option<&str>,
13350    env: &mut Env,
13351    options: &EvaluateOptions,
13352    ctx: &mut ImportContext,
13353) -> EvaluateResult {
13354    let mut diagnostics: Vec<Diagnostic> = Vec::new();
13355    let extracted_literate = if is_literate_lino_path(file) {
13356        Some(extract_literate_lino(text))
13357    } else {
13358        None
13359    };
13360    let source_text = extracted_literate.as_deref().unwrap_or(text);
13361    let spans = compute_form_spans(source_text, file);
13362
13363    let (links, parse_errors) = parse_lino_with_errors(source_text);
13364    for parse_err in parse_errors {
13365        diagnostics.push(Diagnostic::new(
13366            "E006",
13367            format!("LiNo parse failure: {}", parse_err),
13368            Span::new(file.map(|s| s.to_string()), 1, 1, 0),
13369        ));
13370    }
13371    let forms: Vec<Node> = links
13372        .iter()
13373        .filter(|link_str| {
13374            let s = link_str.trim();
13375            !(s.starts_with("(#") && s.chars().nth(2).map_or(false, |c| c.is_whitespace()))
13376        })
13377        .filter_map(|link_str| {
13378            // The LiNo parser collapses single-token links like `(whnf)` to
13379            // the bare token `whnf` — no parens. Re-wrap as a single-element
13380            // list so downstream evaluators see the head as the form keyword
13381            // (mirrors the JS evaluator's `['whnf']` shape and lets the
13382            // normalization driver E038 fall-through fire).
13383            let toks = tokenize_one(link_str);
13384            let toks = if toks.len() == 1 && toks[0] != "(" && toks[0] != ")" {
13385                vec!["(".to_string(), toks[0].clone(), ")".to_string()]
13386            } else {
13387                toks
13388            };
13389            match parse_one(&toks) {
13390                Ok(node) => Some(desugar_hoas(node)),
13391                Err(msg) => {
13392                    diagnostics.push(Diagnostic::new(
13393                        "E002",
13394                        msg,
13395                        Span::new(file.map(|s| s.to_string()), 1, 1, 0),
13396                    ));
13397                    None
13398                }
13399            }
13400        })
13401        .collect();
13402
13403    let mut results: Vec<RunResult> = Vec::new();
13404
13405    // Proof collection (issue #35). When `options.with_proofs` is true the
13406    // global flag forces a derivation for every query; otherwise we lazily
13407    // allocate `proofs` on the first per-query `(? expr with proof)` opt-in
13408    // and backfill `None` for any prior bare queries so indices stay aligned
13409    // with `results`. When neither code path fires `proofs` stays empty and
13410    // is returned as `Vec::new()` — matching the plain `evaluate()` shape.
13411    let proofs_enabled = options.with_proofs;
13412    let mut proofs: Option<Vec<Option<Node>>> = if proofs_enabled {
13413        Some(Vec::new())
13414    } else {
13415        None
13416    };
13417
13418    // Equality-layer provenance (issue #97). Lazily allocated on the first
13419    // query that classifies into one of the four equality layers; prior
13420    // bare queries are backfilled with `None` so indices stay aligned with
13421    // `results`. When no equality query ever fires the vector stays empty
13422    // and the public field is returned as `Vec::new()`, matching the JS
13423    // `{results, diagnostics}` shape for legacy programs.
13424    let mut provenance: Option<Vec<Option<String>>> = None;
13425
13426    // Silence the default panic hook while we deliberately catch evaluator
13427    // panics — otherwise they'd leak to stderr alongside the diagnostics.
13428    let prev_hook = std::panic::take_hook();
13429    std::panic::set_hook(Box::new(|_| {}));
13430
13431    for (idx, form) in forms.into_iter().enumerate() {
13432        let mut form = form;
13433        loop {
13434            match form {
13435                Node::List(ref children) if children.len() == 1 => {
13436                    if let Node::List(_) = &children[0] {
13437                        form = children[0].clone();
13438                    } else {
13439                        break;
13440                    }
13441                }
13442                _ => break,
13443            }
13444        }
13445        let span = spans
13446            .get(idx)
13447            .cloned()
13448            .unwrap_or_else(|| Span::new(file.map(|s| s.to_string()), 1, 1, 0));
13449        env.current_span = Some(span.clone());
13450
13451        // Top-level (namespace <name>) directive — sets the active namespace
13452        // for all subsequent definitions in this file. The `(namespace foo)`
13453        // form is itself never namespaced. (issue #34)
13454        if let Node::List(children) = &form {
13455            if children.len() == 2 {
13456                if let (Node::Leaf(h), Node::Leaf(n)) = (&children[0], &children[1]) {
13457                    if h == "namespace" {
13458                        if n.is_empty() || n.contains('.') {
13459                            diagnostics.push(Diagnostic::new(
13460                                "E009",
13461                                format!("Invalid namespace name \"{}\"", n),
13462                                span.clone(),
13463                            ));
13464                        } else {
13465                            env.namespace = Some(n.clone());
13466                            if options.trace {
13467                                env.trace_events.push(TraceEvent::new(
13468                                    "namespace",
13469                                    n.clone(),
13470                                    span.clone(),
13471                                ));
13472                            }
13473                        }
13474                        continue;
13475                    }
13476                }
13477            }
13478        }
13479
13480        // Top-level (import <path>) and (import <path> as <alias>) directives —
13481        // handled before regular evaluation so they can recursively call
13482        // evaluate_inner against the same env while threading the import
13483        // context.
13484        if let Node::List(children) = &form {
13485            if let Some(Node::Leaf(head)) = children.first() {
13486                if head == "import" {
13487                    if children.len() == 2 {
13488                        let target = children[1].clone();
13489                        if let Some(diag) = handle_import(
13490                            &target,
13491                            None,
13492                            &span,
13493                            file,
13494                            env,
13495                            options,
13496                            ctx,
13497                            &mut diagnostics,
13498                        ) {
13499                            diagnostics.push(diag);
13500                        }
13501                        continue;
13502                    }
13503                    if children.len() == 4 {
13504                        if let (Node::Leaf(as_kw), Node::Leaf(alias_name)) =
13505                            (&children[2], &children[3])
13506                        {
13507                            if as_kw == "as" {
13508                                let target = children[1].clone();
13509                                if let Some(diag) = handle_import(
13510                                    &target,
13511                                    Some(alias_name),
13512                                    &span,
13513                                    file,
13514                                    env,
13515                                    options,
13516                                    ctx,
13517                                    &mut diagnostics,
13518                                ) {
13519                                    diagnostics.push(diag);
13520                                }
13521                                continue;
13522                            }
13523                        }
13524                    }
13525                }
13526            }
13527        }
13528
13529        // Top-level `(template (<name> <param>...) <body>)` declarations are
13530        // recorded on the environment and produce no result. Later regular
13531        // forms are expanded through this registry before evaluation.
13532        if let Node::List(children) = &form {
13533            if let Some(Node::Leaf(head)) = children.first() {
13534                if head == "template" {
13535                    match register_template_form(&form, env) {
13536                        Ok(name) => {
13537                            if options.trace {
13538                                env.trace_events.push(TraceEvent::new(
13539                                    "template",
13540                                    name,
13541                                    span.clone(),
13542                                ));
13543                            }
13544                        }
13545                        Err(message) => {
13546                            diagnostics.push(Diagnostic::new("E040", message, span.clone()));
13547                        }
13548                    }
13549                    continue;
13550                }
13551            }
13552        }
13553
13554        // Foundation / root-construct registry (issue #97). Data-only:
13555        // declarations record what the prover trusts but never change
13556        // host operator behaviour. `(with-foundation <name> body...)`
13557        // pushes a foundation tag for the duration of the body so the
13558        // trust report and audit can attribute reductions to it.
13559        if let Node::List(children) = &form {
13560            if let Some(Node::Leaf(head)) = children.first() {
13561                if head == "root-construct" {
13562                    match parse_root_construct_form(&form) {
13563                        Ok(descriptor) => {
13564                            let name = descriptor.name.clone();
13565                            if let Err(message) = env.register_root_construct(descriptor) {
13566                                diagnostics.push(Diagnostic::new("E060", message, span.clone()));
13567                            } else if options.trace {
13568                                env.trace_events.push(TraceEvent::new(
13569                                    "root-construct",
13570                                    name,
13571                                    span.clone(),
13572                                ));
13573                            }
13574                        }
13575                        Err(message) => {
13576                            diagnostics.push(Diagnostic::new("E060", message, span.clone()));
13577                        }
13578                    }
13579                    continue;
13580                }
13581                if head == "foundation" {
13582                    match parse_foundation_form(&form) {
13583                        Ok(foundation) => {
13584                            let name = foundation.name.clone();
13585                            if let Err(message) = env.register_foundation(foundation) {
13586                                diagnostics.push(Diagnostic::new("E061", message, span.clone()));
13587                            } else if options.trace {
13588                                env.trace_events.push(TraceEvent::new(
13589                                    "foundation",
13590                                    name,
13591                                    span.clone(),
13592                                ));
13593                            }
13594                        }
13595                        Err(message) => {
13596                            diagnostics.push(Diagnostic::new("E061", message, span.clone()));
13597                        }
13598                    }
13599                    continue;
13600                }
13601                if head == "with-foundation" {
13602                    if children.len() < 2 {
13603                        diagnostics.push(Diagnostic::new(
13604                            "E062",
13605                            "with-foundation form must be `(with-foundation <name> <body>...)`",
13606                            span.clone(),
13607                        ));
13608                        continue;
13609                    }
13610                    let fname = match &children[1] {
13611                        Node::Leaf(s) if !s.is_empty() => s.clone(),
13612                        _ => {
13613                            diagnostics.push(Diagnostic::new(
13614                                "E062",
13615                                "with-foundation requires a foundation name",
13616                                span.clone(),
13617                            ));
13618                            continue;
13619                        }
13620                    };
13621                    if let Err(message) = env.enter_foundation(&fname) {
13622                        diagnostics.push(Diagnostic::new("E062", message, span.clone()));
13623                        continue;
13624                    }
13625                    if options.trace {
13626                        env.trace_events.push(TraceEvent::new(
13627                            "with-foundation/enter",
13628                            fname.clone(),
13629                            span.clone(),
13630                        ));
13631                    }
13632                    let bodies: Vec<Node> = children[2..].to_vec();
13633                    for body in bodies {
13634                        eval_foundation_body_form(
13635                            body,
13636                            &span,
13637                            env,
13638                            &mut diagnostics,
13639                            &mut results,
13640                            &mut proofs,
13641                            &mut provenance,
13642                            options,
13643                        );
13644                    }
13645                    env.exit_foundation();
13646                    if options.trace {
13647                        env.trace_events.push(TraceEvent::new(
13648                            "with-foundation/exit",
13649                            fname,
13650                            span.clone(),
13651                        ));
13652                    }
13653                    continue;
13654                }
13655                if head == "foundation-report" || head == "foundation-report?" {
13656                    let report = env.foundation_report();
13657                    if options.trace {
13658                        env.trace_events.push(TraceEvent::new(
13659                            "foundation-report",
13660                            report.active_foundation.clone(),
13661                            span.clone(),
13662                        ));
13663                    }
13664                    results.push(RunResult::Foundation(report));
13665                    if let Some(p) = proofs.as_mut() {
13666                        p.push(None);
13667                    }
13668                    if let Some(pv) = provenance.as_mut() {
13669                        pv.push(None);
13670                    }
13671                    continue;
13672                }
13673                // Proof-object substrate (issue #97, Phase 3). The
13674                // `(rule <name> (premise ...)... (conclusion ...))` shape is
13675                // routed here only when every clause uses the
13676                // `premise`/`conclusion` keywords and at least one
13677                // `conclusion` is present, so existing self-bootstrap
13678                // grammars that use `(rule <name> (sequence ...) ...)` fall
13679                // through to the legacy data path unchanged.
13680                if head == "rule" && is_proof_rule_shape(children) {
13681                    match parse_rule_form(&form) {
13682                        Ok(rule) => {
13683                            let name = rule.name.clone();
13684                            env.register_proof_rule(rule);
13685                            if options.trace {
13686                                env.trace_events
13687                                    .push(TraceEvent::new("rule", name, span.clone()));
13688                            }
13689                        }
13690                        Err(message) => {
13691                            diagnostics.push(Diagnostic::new("E064", message, span.clone()));
13692                        }
13693                    }
13694                    continue;
13695                }
13696                if head == "assumption" || head == "axiom" {
13697                    match parse_proof_assumption_form(&form) {
13698                        Ok(assumption) => {
13699                            let kind = assumption.kind.clone();
13700                            let name = assumption.name.clone();
13701                            env.register_proof_assumption(assumption);
13702                            if options.trace {
13703                                env.trace_events
13704                                    .push(TraceEvent::new(&kind, name, span.clone()));
13705                            }
13706                        }
13707                        Err(message) => {
13708                            diagnostics.push(Diagnostic::new("E064", message, span.clone()));
13709                        }
13710                    }
13711                    continue;
13712                }
13713                if head == "proof-object" {
13714                    match parse_proof_object_form(&form) {
13715                        Ok(po) => {
13716                            let name = po.name.clone();
13717                            env.register_proof_object(po);
13718                            if options.trace {
13719                                env.trace_events.push(TraceEvent::new(
13720                                    "proof-object",
13721                                    name,
13722                                    span.clone(),
13723                                ));
13724                            }
13725                        }
13726                        Err(message) => {
13727                            diagnostics.push(Diagnostic::new("E064", message, span.clone()));
13728                        }
13729                    }
13730                    continue;
13731                }
13732                if head == "check-proof" {
13733                    if children.len() != 2 {
13734                        diagnostics.push(Diagnostic::new(
13735                            "E064",
13736                            "(check-proof <name>) requires a proof-object name",
13737                            span.clone(),
13738                        ));
13739                        continue;
13740                    }
13741                    let target = match &children[1] {
13742                        Node::Leaf(s) if !s.is_empty() => s.clone(),
13743                        _ => {
13744                            diagnostics.push(Diagnostic::new(
13745                                "E064",
13746                                "(check-proof <name>) requires a proof-object name",
13747                                span.clone(),
13748                            ));
13749                            continue;
13750                        }
13751                    };
13752                    let verdict = check_proof_object(env, &target);
13753                    let (value, error) = match verdict {
13754                        CheckProofVerdict::Ok(_) => (1.0_f64, None),
13755                        CheckProofVerdict::Err(msg) => (0.0_f64, Some(msg)),
13756                    };
13757                    results.push(RunResult::Num(value));
13758                    if let Some(p) = proofs.as_mut() {
13759                        p.push(None);
13760                    }
13761                    if let Some(pv) = provenance.as_mut() {
13762                        pv.push(None);
13763                    }
13764                    if let Some(msg) = error {
13765                        diagnostics.push(Diagnostic::new("E064", msg, span.clone()));
13766                    }
13767                    if options.trace {
13768                        env.trace_events.push(TraceEvent::new(
13769                            "check-proof",
13770                            format!("{} → {}", target, if value == 1.0 { "ok" } else { "fail" }),
13771                            span.clone(),
13772                        ));
13773                    }
13774                    continue;
13775                }
13776                if head == "proof-report" {
13777                    if children.len() != 2 {
13778                        diagnostics.push(Diagnostic::new(
13779                            "E064",
13780                            "(proof-report <name>) requires a proof-object name",
13781                            span.clone(),
13782                        ));
13783                        continue;
13784                    }
13785                    let target = match &children[1] {
13786                        Node::Leaf(s) if !s.is_empty() => s.clone(),
13787                        _ => {
13788                            diagnostics.push(Diagnostic::new(
13789                                "E064",
13790                                "(proof-report <name>) requires a proof-object name",
13791                                span.clone(),
13792                            ));
13793                            continue;
13794                        }
13795                    };
13796                    let report = env.proof_report(&target);
13797                    results.push(RunResult::Proof(report));
13798                    if let Some(p) = proofs.as_mut() {
13799                        p.push(None);
13800                    }
13801                    if let Some(pv) = provenance.as_mut() {
13802                        pv.push(None);
13803                    }
13804                    if options.trace {
13805                        env.trace_events.push(TraceEvent::new(
13806                            "proof-report",
13807                            target,
13808                            span.clone(),
13809                        ));
13810                    }
13811                    continue;
13812                }
13813                if head == "eval-nat" {
13814                    if children.len() != 2 {
13815                        diagnostics.push(Diagnostic::new(
13816                            "E067",
13817                            "(eval-nat <term>) requires exactly one term argument",
13818                            span.clone(),
13819                        ));
13820                        continue;
13821                    }
13822                    match eval_nat_term(env, &children[1]) {
13823                        Ok(result) => {
13824                            results.push(RunResult::Num(result.value));
13825                            if let Some(p) = proofs.as_mut() {
13826                                p.push(None);
13827                            }
13828                            if let Some(pv) = provenance.as_mut() {
13829                                pv.push(None);
13830                            }
13831                            if options.trace {
13832                                env.trace_events.push(TraceEvent::new(
13833                                    "eval-nat",
13834                                    format!(
13835                                        "{} -> normal-form {} -> {}; rules-used: {}; host-primitives-used: structural-matcher; renderer: nat-normal-form-to-host-number",
13836                                        key_of(&children[1]),
13837                                        key_of(&result.normal_form),
13838                                        format_trace_value(result.value),
13839                                        if result.steps.is_empty() {
13840                                            "<none>".to_string()
13841                                        } else {
13842                                            result.steps.join(", ")
13843                                        }
13844                                    ),
13845                                    span.clone(),
13846                                ));
13847                            }
13848                        }
13849                        Err(message) => {
13850                            diagnostics.push(Diagnostic::new("E067", message, span.clone()));
13851                        }
13852                    }
13853                    continue;
13854                }
13855                // Pure-links strict mode (issue #97, Phase 6).
13856                if head == "strict-foundation" {
13857                    match parse_strict_foundation_form(&form) {
13858                        Ok(decl) => {
13859                            env.strict_pure_links = true;
13860                            if options.trace {
13861                                env.trace_events.push(TraceEvent::new(
13862                                    "strict-foundation",
13863                                    decl.profile,
13864                                    span.clone(),
13865                                ));
13866                            }
13867                        }
13868                        Err(message) => {
13869                            diagnostics.push(Diagnostic::new("E065", message, span.clone()));
13870                        }
13871                    }
13872                    continue;
13873                }
13874                if head == "allow-host-primitive" {
13875                    match parse_allow_host_primitive_form(&form) {
13876                        Ok(decl) => {
13877                            for name in &decl.names {
13878                                env.allowed_host_primitives.insert(name.clone());
13879                            }
13880                            if options.trace {
13881                                env.trace_events.push(TraceEvent::new(
13882                                    "allow-host-primitive",
13883                                    decl.names.join(" "),
13884                                    span.clone(),
13885                                ));
13886                            }
13887                        }
13888                        Err(message) => {
13889                            diagnostics.push(Diagnostic::new("E065", message, span.clone()));
13890                        }
13891                    }
13892                    continue;
13893                }
13894            }
13895        }
13896
13897        let result = catch_unwind(AssertUnwindSafe(|| {
13898            let mut stack = Vec::new();
13899            let expanded_form = expand_templates(&form, env, &mut stack);
13900            let eval_res = eval_node(&expanded_form, env);
13901            (expanded_form, eval_res)
13902        }));
13903        match result {
13904            Ok((expanded_form, eval_res)) => {
13905                if options.trace {
13906                    let form_key = key_of(&expanded_form);
13907                    let summary = match &eval_res {
13908                        EvalResult::Query(v) => {
13909                            format!("{} → query {}", form_key, format_trace_value(*v))
13910                        }
13911                        EvalResult::TypeQuery(s) => {
13912                            format!("{} → type {}", form_key, s)
13913                        }
13914                        EvalResult::Value(v) => {
13915                            format!("{} → {}", form_key, format_trace_value(*v))
13916                        }
13917                        EvalResult::Term(term) => {
13918                            format!("{} → term {}", form_key, key_of(term))
13919                        }
13920                    };
13921                    env.trace_events
13922                        .push(TraceEvent::new("eval", summary, span.clone()));
13923                }
13924                let was_query = matches!(eval_res, EvalResult::Query(_) | EvalResult::TypeQuery(_));
13925                let query_value = if let EvalResult::Query(v) = &eval_res {
13926                    Some(*v)
13927                } else {
13928                    None
13929                };
13930                match eval_res {
13931                    EvalResult::Query(v) => results.push(RunResult::Num(v)),
13932                    EvalResult::TypeQuery(s) => results.push(RunResult::Type(s)),
13933                    _ => {}
13934                }
13935                if was_query {
13936                    let wants_proof = proofs_enabled || query_requests_proof(&expanded_form);
13937                    if wants_proof {
13938                        // Lazily allocate the proofs vec on first per-query
13939                        // opt-in so callers that never ask for proofs get
13940                        // an empty vec back. Backfill `None` for any prior
13941                        // bare queries so indices stay aligned with results.
13942                        if proofs.is_none() {
13943                            let backfill = results.len().saturating_sub(1);
13944                            proofs = Some(vec![None; backfill]);
13945                        }
13946                        // Strip the surrounding (? ...) so the proof attaches
13947                        // to the queried expression directly; this matches
13948                        // the issue example `(by structural-equality (a a))`
13949                        // rather than nesting under `(by query ...)`.
13950                        let proof_node = match &expanded_form {
13951                            Node::List(form_children)
13952                                if matches!(
13953                                    form_children.first(),
13954                                    Some(Node::Leaf(s)) if s == "?"
13955                                ) =>
13956                            {
13957                                let parts = &form_children[1..];
13958                                let inner = strip_with_proof(parts);
13959                                let target: Node = if inner.len() == 1 {
13960                                    inner[0].clone()
13961                                } else {
13962                                    Node::List(inner.to_vec())
13963                                };
13964                                build_proof(&target, env)
13965                            }
13966                            _ => build_proof(&expanded_form, env),
13967                        };
13968                        proofs.as_mut().unwrap().push(Some(proof_node));
13969                    } else if let Some(p) = proofs.as_mut() {
13970                        p.push(None);
13971                    }
13972                    let prov = equality_provenance_for_query(&expanded_form, env);
13973                    record_provenance(
13974                        &mut provenance,
13975                        results.len(),
13976                        prov,
13977                        env,
13978                        &expanded_form,
13979                        &span,
13980                        options,
13981                    );
13982                    // Carrier enforcement (issue #97 Section 2): also surface
13983                    // E063 at the top level so a `(with-foundation ...)` body
13984                    // that returns into a top-level query path still flags
13985                    // out-of-carrier results.
13986                    if let Some(v) = query_value {
13987                        if let Some(msg) = env.check_carrier_value(v) {
13988                            diagnostics.push(Diagnostic::new(
13989                                "E063",
13990                                format!(
13991                                    "Query result {} violates active foundation carrier: {}",
13992                                    format_trace_value(v),
13993                                    msg
13994                                ),
13995                                span.clone(),
13996                            ));
13997                        }
13998                    }
13999                    // Pure-links strict mode audit (issue #97 Phase 6). When
14000                    // `(strict-foundation pure-links)` is active, scan the
14001                    // queried form for operators registered as
14002                    // `host-primitive`/`host-derived` that have not been
14003                    // explicitly allow-listed via `(allow-host-primitive ...)`,
14004                    // and emit a single E065 listing them.
14005                    if env.strict_pure_links {
14006                        if let Node::List(form_children) = &expanded_form {
14007                            if matches!(form_children.first(), Some(Node::Leaf(s)) if s == "?") {
14008                                let parts = &form_children[1..];
14009                                let inner = strip_with_proof(parts);
14010                                let target: Node = if inner.len() == 1 {
14011                                    inner[0].clone()
14012                                } else {
14013                                    Node::List(inner.to_vec())
14014                                };
14015                                let offenders = scan_pure_links_offenders(&target, env);
14016                                if !offenders.is_empty() {
14017                                    diagnostics.push(Diagnostic::new(
14018                                        "E065",
14019                                        format!(
14020                                            "Query depends on host-primitive construct(s) under pure-links strict mode: {}",
14021                                            offenders.join(", ")
14022                                        ),
14023                                        span.clone(),
14024                                    ));
14025                                }
14026                            }
14027                        }
14028                    }
14029                }
14030            }
14031            Err(payload) => {
14032                let (code, message) = decode_panic_payload(&payload);
14033                diagnostics.push(Diagnostic::new(&code, message, span));
14034            }
14035        }
14036    }
14037
14038    env.current_span = None;
14039
14040    std::panic::set_hook(prev_hook);
14041
14042    // Surface any shadow diagnostics collected during this evaluation pass.
14043    // Drain them so a nested evaluate_inner (called from handle_import) does
14044    // not re-emit the same diagnostic at the outer boundary.
14045    if !env.shadow_diagnostics.is_empty() {
14046        let drained = std::mem::take(&mut env.shadow_diagnostics);
14047        for d in drained {
14048            diagnostics.push(d);
14049        }
14050    }
14051
14052    let trace = if options.trace {
14053        std::mem::take(&mut env.trace_events)
14054    } else {
14055        Vec::new()
14056    };
14057
14058    let provenance_vec = match provenance {
14059        Some(mut v) => {
14060            while v.len() < results.len() {
14061                v.push(None);
14062            }
14063            v
14064        }
14065        None => Vec::new(),
14066    };
14067
14068    EvaluateResult {
14069        results,
14070        diagnostics,
14071        trace,
14072        proofs: proofs.unwrap_or_default(),
14073        provenance: provenance_vec,
14074    }
14075}
14076
14077/// Map a panic payload to a diagnostic `(code, message)` pair.  Known panic
14078/// messages emitted by the evaluator are mapped to the canonical `E001`/etc.
14079/// codes; anything else falls back to `E000`.
14080fn decode_panic_payload(payload: &Box<dyn std::any::Any + Send>) -> (String, String) {
14081    let raw_msg: String = if let Some(s) = payload.downcast_ref::<&'static str>() {
14082        (*s).to_string()
14083    } else if let Some(s) = payload.downcast_ref::<String>() {
14084        s.clone()
14085    } else {
14086        "evaluation panicked".to_string()
14087    };
14088    if raw_msg.starts_with("Unknown op:") {
14089        ("E001".to_string(), raw_msg)
14090    } else if raw_msg.starts_with("Unknown aggregator") {
14091        ("E004".to_string(), raw_msg)
14092    } else if raw_msg.starts_with("Freshness error:") {
14093        (
14094            "E010".to_string(),
14095            raw_msg.replacen("Freshness error: ", "", 1),
14096        )
14097    } else if raw_msg.starts_with("Mode declaration error:") {
14098        (
14099            "E030".to_string(),
14100            raw_msg.replacen("Mode declaration error: ", "", 1),
14101        )
14102    } else if raw_msg.starts_with("Mode mismatch:") {
14103        (
14104            "E031".to_string(),
14105            raw_msg.replacen("Mode mismatch: ", "", 1),
14106        )
14107    } else if raw_msg.starts_with("Relation declaration error:") {
14108        (
14109            "E032".to_string(),
14110            raw_msg.replacen("Relation declaration error: ", "", 1),
14111        )
14112    } else if raw_msg.starts_with("Totality check error:") {
14113        (
14114            "E032".to_string(),
14115            raw_msg.replacen("Totality check error: ", "", 1),
14116        )
14117    } else if raw_msg.starts_with("Coverage check error:") {
14118        (
14119            "E037".to_string(),
14120            raw_msg.replacen("Coverage check error: ", "", 1),
14121        )
14122    } else if raw_msg.starts_with("World declaration error:") {
14123        (
14124            "E034".to_string(),
14125            raw_msg.replacen("World declaration error: ", "", 1),
14126        )
14127    } else if raw_msg.starts_with("World violation:") {
14128        (
14129            "E034".to_string(),
14130            raw_msg.replacen("World violation: ", "", 1),
14131        )
14132    } else if raw_msg.starts_with("Inductive declaration error:") {
14133        (
14134            "E033".to_string(),
14135            raw_msg.replacen("Inductive declaration error: ", "", 1),
14136        )
14137    } else if raw_msg.starts_with("Termination check error:") {
14138        (
14139            "E035".to_string(),
14140            raw_msg.replacen("Termination check error: ", "", 1),
14141        )
14142    } else if raw_msg.starts_with("Coinductive declaration error:") {
14143        (
14144            "E036".to_string(),
14145            raw_msg.replacen("Coinductive declaration error: ", "", 1),
14146        )
14147    } else if raw_msg.starts_with("Normalization error:") {
14148        (
14149            "E038".to_string(),
14150            raw_msg.replacen("Normalization error: ", "", 1),
14151        )
14152    } else if raw_msg.starts_with("Template expansion error:") {
14153        (
14154            "E040".to_string(),
14155            raw_msg.replacen("Template expansion error: ", "", 1),
14156        )
14157    } else if raw_msg.starts_with("Domain plugin error:") {
14158        (
14159            "E041".to_string(),
14160            raw_msg.replacen("Domain plugin error: ", "", 1),
14161        )
14162    } else if raw_msg.starts_with("Carrier violation:") {
14163        (
14164            "E063".to_string(),
14165            raw_msg.replacen("Carrier violation: ", "", 1),
14166        )
14167    } else {
14168        ("E000".to_string(), raw_msg)
14169    }
14170}
14171
14172/// Run a complete LiNo knowledge base and return query results (including type queries).
14173pub fn run_typed(text: &str, options: Option<EnvOptions>) -> Vec<RunResult> {
14174    evaluate(text, None, options).results
14175}
14176
14177/// Run a complete LiNo knowledge base and return query results.
14178pub fn run(text: &str, options: Option<EnvOptions>) -> Vec<f64> {
14179    run_typed(text, options)
14180        .into_iter()
14181        .filter_map(|result| match result {
14182            RunResult::Num(v) => Some(v),
14183            RunResult::Type(_) => None,
14184            RunResult::Foundation(_) => None,
14185            RunResult::Proof(_) => None,
14186        })
14187        .collect()
14188}
14189
14190// Tests are in the tests/ directory (integration tests).
14191// To run: cargo test
14192
14193pub mod repl;
14194pub mod check;
14195pub mod meta;
14196pub mod rocq;
14197
14198// Universal CST converters (issue #138).
14199pub mod cst;
14200pub mod cst_rust;
14201pub mod cst_js;
14202pub mod cst_lean;
14203pub mod cst_rocq;
14204pub mod cst_convert;