Skip to main content

logicaffeine_compile/
ui_bridge.rs

1//! UI bridge for web interface integration.
2//!
3//! This module provides high-level, UI-friendly wrappers around the compilation
4//! pipeline, returning structured results suitable for display in the browser.
5//!
6//! # Key Functions
7//!
8//! | Function | Purpose |
9//! |----------|---------|
10//! | [`compile_for_ui`] | Compile LOGOS to FOL (UI format) |
11//! | [`compile_for_proof`] | Compile and search for proofs |
12//! | [`compile_theorem_for_ui`] | Compile theorems with derivation trees |
13//! | [`verify_theorem`] | Verify a theorem is provable |
14//! | [`interpret_for_ui`] | Run imperative code and return output |
15//! | [`generate_rust_code`] | Generate Rust source (requires `codegen` feature) |
16//!
17//! # Result Types
18//!
19//! All functions return serializable result types:
20//! - [`CompileResult`] - Tokens, FOL, AST, errors
21//! - [`ProofCompileResult`] - FOL with proof search results
22//! - [`TheoremCompileResult`] - Theorem verification with derivation tree
23//! - [`InterpreterResult`] - Program output lines and errors
24//!
25//! # Token Categories
26//!
27//! The [`TokenCategory`] enum provides syntactic highlighting categories:
28//! Quantifier, Noun, Verb, Adjective, Connective, Determiner, etc.
29
30use serde::{Deserialize, Serialize};
31use std::collections::HashSet;
32
33use logicaffeine_base::{Arena, Interner};
34use logicaffeine_language::{
35    ast::{self, LogicExpr, Term},
36    analysis::DiscoveryPass,
37    arena_ctx::AstContext,
38    compile::{compile_forest, compile_forest_with_options},
39    drs,
40    error::socratic_explanation,
41    lexer::Lexer,
42    mwe,
43    parser::Parser,
44    pragmatics,
45    registry::SymbolRegistry,
46    semantics,
47    token::TokenType,
48    CompileOptions, OutputFormat, ParseError,
49};
50use logicaffeine_proof::{DerivationTree, ProofExpr, ProofTerm};
51
52// Re-export interpreter result from our interpreter module
53pub use crate::interpreter::InterpreterResult;
54
55// ═══════════════════════════════════════════════════════════════════
56// Token Visualization
57// ═══════════════════════════════════════════════════════════════════
58
59/// Syntactic category of a token for UI highlighting.
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
61pub enum TokenCategory {
62    /// Universal/existential quantifiers: every, some, no, most
63    Quantifier,
64    /// Common nouns: dog, cat, person
65    Noun,
66    /// Main verbs: runs, loves, gives
67    Verb,
68    /// Adjective modifiers: tall, happy, red
69    Adjective,
70    /// Logical connectives: and, or, not, if
71    Connective,
72    /// Articles and determiners: a, an, the
73    Determiner,
74    /// Prepositional words: in, on, to, with
75    Preposition,
76    /// Personal and relative pronouns: he, she, who
77    Pronoun,
78    /// Modal auxiliaries: can, must, might
79    Modal,
80    /// Sentence-ending punctuation
81    Punctuation,
82    /// Proper names (capitalized)
83    Proper,
84    /// Uncategorized tokens
85    Other,
86}
87
88/// Token information for UI display with position and category.
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct TokenInfo {
91    /// Byte offset of token start in source string.
92    pub start: usize,
93    /// Byte offset of token end (exclusive) in source string.
94    pub end: usize,
95    /// The actual text of the token.
96    pub text: String,
97    /// Syntactic category for highlighting.
98    pub category: TokenCategory,
99}
100
101fn categorize_token(kind: &TokenType, _interner: &Interner) -> TokenCategory {
102    match kind {
103        TokenType::All | TokenType::Some | TokenType::No | TokenType::Any
104        | TokenType::Most | TokenType::Few | TokenType::Many
105        | TokenType::Cardinal(_) | TokenType::AtLeast(_) | TokenType::AtMost(_) => TokenCategory::Quantifier,
106        TokenType::Noun(_) => TokenCategory::Noun,
107        TokenType::Verb { .. } => TokenCategory::Verb,
108        TokenType::Adjective(_) | TokenType::NonIntersectiveAdjective(_) => TokenCategory::Adjective,
109        TokenType::And | TokenType::Or | TokenType::Not | TokenType::If | TokenType::Then
110        | TokenType::Iff | TokenType::Because => TokenCategory::Connective,
111        TokenType::Article(_) => TokenCategory::Determiner,
112        TokenType::Preposition(_) => TokenCategory::Preposition,
113        TokenType::Pronoun { .. } => TokenCategory::Pronoun,
114        TokenType::Must | TokenType::Can | TokenType::Should | TokenType::Shall
115        | TokenType::Would | TokenType::Could | TokenType::May | TokenType::Cannot
116        | TokenType::Might => TokenCategory::Modal,
117        TokenType::Period | TokenType::Comma => TokenCategory::Punctuation,
118        TokenType::ProperName(_) => TokenCategory::Proper,
119        _ => TokenCategory::Other,
120    }
121}
122
123/// Tokenizes input text and returns token information for UI display.
124///
125/// Each token includes its byte position, text content, and syntactic
126/// category for syntax highlighting in the browser interface.
127pub fn tokenize_for_ui(input: &str) -> Vec<TokenInfo> {
128    let mut interner = Interner::new();
129    let mut lexer = Lexer::new(input, &mut interner);
130    let tokens = lexer.tokenize();
131
132    tokens.iter().map(|t| TokenInfo {
133        start: t.span.start,
134        end: t.span.end,
135        text: input[t.span.start..t.span.end].to_string(),
136        category: categorize_token(&t.kind, &interner),
137    }).collect()
138}
139
140// ═══════════════════════════════════════════════════════════════════
141// AST Visualization
142// ═══════════════════════════════════════════════════════════════════
143
144/// AST node for tree visualization in the UI.
145#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
146pub struct AstNode {
147    /// Display label for this node (e.g., "∀x", "Run(x)").
148    pub label: String,
149    /// Node type for styling (e.g., "quantifier", "predicate", "connective").
150    pub node_type: String,
151    /// Child nodes in the AST.
152    pub children: Vec<AstNode>,
153}
154
155impl AstNode {
156    pub fn leaf(label: &str, node_type: &str) -> Self {
157        AstNode {
158            label: label.to_string(),
159            node_type: node_type.to_string(),
160            children: Vec::new(),
161        }
162    }
163
164    pub fn with_children(label: &str, node_type: &str, children: Vec<AstNode>) -> Self {
165        AstNode {
166            label: label.to_string(),
167            node_type: node_type.to_string(),
168            children,
169        }
170    }
171}
172
173/// Converts a logic expression to an AST node for tree visualization.
174///
175/// Recursively builds a tree structure with labeled nodes suitable for
176/// rendering in the UI. Each node includes a display label, node type
177/// for styling, and child nodes.
178pub fn expr_to_ast_node(expr: &LogicExpr, interner: &Interner) -> AstNode {
179    match expr {
180        LogicExpr::Predicate { name, args, .. } => {
181            let name_str = interner.resolve(*name);
182            let arg_nodes: Vec<AstNode> = args.iter()
183                .map(|t| term_to_ast_node(t, interner))
184                .collect();
185            AstNode::with_children(
186                &format!("{}({})", name_str, args.len()),
187                "predicate",
188                arg_nodes,
189            )
190        }
191        LogicExpr::Quantifier { kind, variable, body, .. } => {
192            let var_str = interner.resolve(*variable);
193            let symbol = match kind {
194                ast::QuantifierKind::Universal => "∀",
195                ast::QuantifierKind::Existential => "∃",
196                ast::QuantifierKind::Most => "MOST",
197                ast::QuantifierKind::Few => "FEW",
198                ast::QuantifierKind::Many => "MANY",
199                ast::QuantifierKind::Cardinal(n) => return AstNode::with_children(
200                    &format!("∃={}{}", n, var_str),
201                    "quantifier",
202                    vec![expr_to_ast_node(body, interner)],
203                ),
204                ast::QuantifierKind::AtLeast(n) => return AstNode::with_children(
205                    &format!("∃≥{}{}", n, var_str),
206                    "quantifier",
207                    vec![expr_to_ast_node(body, interner)],
208                ),
209                ast::QuantifierKind::AtMost(n) => return AstNode::with_children(
210                    &format!("∃≤{}{}", n, var_str),
211                    "quantifier",
212                    vec![expr_to_ast_node(body, interner)],
213                ),
214                ast::QuantifierKind::Generic => "GEN",
215            };
216            AstNode::with_children(
217                &format!("{}{}", symbol, var_str),
218                "quantifier",
219                vec![expr_to_ast_node(body, interner)],
220            )
221        }
222        LogicExpr::BinaryOp { left, op, right } => {
223            let op_str = match op {
224                TokenType::And => "∧",
225                TokenType::Or => "∨",
226                TokenType::If | TokenType::Then => "→",
227                TokenType::Iff => "↔",
228                _ => "?",
229            };
230            AstNode::with_children(
231                op_str,
232                "binary_op",
233                vec![
234                    expr_to_ast_node(left, interner),
235                    expr_to_ast_node(right, interner),
236                ],
237            )
238        }
239        LogicExpr::UnaryOp { op, operand } => {
240            let op_str = match op {
241                TokenType::Not => "¬",
242                _ => "?",
243            };
244            AstNode::with_children(
245                op_str,
246                "unary_op",
247                vec![expr_to_ast_node(operand, interner)],
248            )
249        }
250        LogicExpr::Identity { left, right } => {
251            AstNode::with_children(
252                "=",
253                "identity",
254                vec![
255                    term_to_ast_node(left, interner),
256                    term_to_ast_node(right, interner),
257                ],
258            )
259        }
260        LogicExpr::Modal { vector, operand } => {
261            AstNode::with_children(
262                &format!("□{:?}", vector.domain),
263                "modal",
264                vec![expr_to_ast_node(operand, interner)],
265            )
266        }
267        LogicExpr::Lambda { variable, body } => {
268            let var_str = interner.resolve(*variable);
269            AstNode::with_children(
270                &format!("λ{}", var_str),
271                "lambda",
272                vec![expr_to_ast_node(body, interner)],
273            )
274        }
275        LogicExpr::SpeechAct { performer, act_type, content } => {
276            AstNode::with_children(
277                &format!(
278                    "{}!{}",
279                    interner.resolve(*act_type),
280                    interner.resolve(*performer)
281                ),
282                "speech_act",
283                vec![expr_to_ast_node(content, interner)],
284            )
285        }
286        _ => AstNode::leaf(&format!("{:?}", expr), "other"),
287    }
288}
289
290fn term_to_ast_node(term: &Term, interner: &Interner) -> AstNode {
291    match term {
292        Term::Constant(sym) => AstNode::leaf(interner.resolve(*sym), "constant"),
293        Term::Variable(sym) => AstNode::leaf(interner.resolve(*sym), "variable"),
294        Term::Function(name, args) => {
295            let name_str = interner.resolve(*name);
296            let arg_nodes: Vec<AstNode> = args.iter()
297                .map(|t| term_to_ast_node(t, interner))
298                .collect();
299            AstNode::with_children(&format!("{}()", name_str), "function", arg_nodes)
300        }
301        Term::Group(terms) => {
302            let term_nodes: Vec<AstNode> = terms.iter()
303                .map(|t| term_to_ast_node(t, interner))
304                .collect();
305            AstNode::with_children("⊕", "group", term_nodes)
306        }
307        _ => AstNode::leaf(&format!("{:?}", term), "term"),
308    }
309}
310
311// ═══════════════════════════════════════════════════════════════════
312// Compilation Results
313// ═══════════════════════════════════════════════════════════════════
314
315#[derive(Debug, Clone, Serialize, Deserialize)]
316/// Result of compiling English input to FOL with UI metadata.
317pub struct CompileResult {
318    /// Primary FOL output (Unicode format), if compilation succeeded.
319    pub logic: Option<String>,
320    /// Simplified FOL with modals stripped (for verification).
321    pub simple_logic: Option<String>,
322    /// Kripke semantics output with explicit world quantification.
323    pub kripke_logic: Option<String>,
324    /// AST tree representation for visualization.
325    pub ast: Option<AstNode>,
326    /// All scope readings in Unicode format.
327    pub readings: Vec<String>,
328    /// All scope readings in simplified format.
329    pub simple_readings: Vec<String>,
330    /// All scope readings in Kripke format.
331    pub kripke_readings: Vec<String>,
332    /// Tokenization with categories for syntax highlighting.
333    pub tokens: Vec<TokenInfo>,
334    /// Parse/compile error message, if any.
335    pub error: Option<String>,
336}
337
338/// Compile English input to FOL with full UI metadata.
339pub fn compile_for_ui(input: &str) -> CompileResult {
340    let tokens = tokenize_for_ui(input);
341    let readings = compile_forest(input);
342
343    // Generate Simple readings (modals stripped) - deduplicated
344    let simple_readings: Vec<String> = {
345        let raw = compile_forest_with_options(input, CompileOptions { format: OutputFormat::SimpleFOL, pragmatic: false });
346        let mut seen = HashSet::new();
347        raw.into_iter().filter(|r| seen.insert(r.clone())).collect()
348    };
349
350    // Generate Kripke readings with explicit world quantification
351    let kripke_readings = compile_forest_with_options(input, CompileOptions { format: OutputFormat::Kripke, pragmatic: false });
352
353    let mut interner = Interner::new();
354    let mut lexer = Lexer::new(input, &mut interner);
355    let lex_tokens = lexer.tokenize();
356
357    let mwe_trie = mwe::build_mwe_trie();
358    let lex_tokens = mwe::apply_mwe_pipeline(lex_tokens, &mwe_trie, &mut interner);
359
360    // Pass 1: Discovery
361    let type_registry = {
362        let mut discovery = DiscoveryPass::new(&lex_tokens, &mut interner);
363        discovery.run()
364    };
365
366    let expr_arena = Arena::new();
367    let term_arena = Arena::new();
368    let np_arena = Arena::new();
369    let sym_arena = Arena::new();
370    let role_arena = Arena::new();
371    let pp_arena = Arena::new();
372
373    let ctx = AstContext::new(
374        &expr_arena,
375        &term_arena,
376        &np_arena,
377        &sym_arena,
378        &role_arena,
379        &pp_arena,
380    );
381
382    // Pass 2: Parse
383    let mut world_state = drs::WorldState::new();
384    let mut parser = Parser::new(lex_tokens, &mut world_state, &mut interner, ctx, type_registry);
385
386    match parser.parse() {
387        Ok(ast) => {
388            let ast = semantics::apply_axioms(ast, ctx.exprs, ctx.terms, &mut interner);
389            let ast = pragmatics::apply_pragmatics(ast, ctx.exprs, &interner);
390            let ast_node = expr_to_ast_node(ast, &interner);
391            let mut registry = SymbolRegistry::new();
392            let logic = ast.transpile_discourse(&mut registry, &interner, OutputFormat::Unicode);
393            let simple_logic = ast.transpile_discourse(&mut registry, &interner, OutputFormat::SimpleFOL);
394
395            let kripke_ast = semantics::apply_kripke_lowering(ast, ctx.exprs, ctx.terms, &mut interner);
396            let kripke_logic = kripke_ast.transpile_discourse(&mut registry, &interner, OutputFormat::Kripke);
397
398            CompileResult {
399                logic: Some(logic),
400                simple_logic: Some(simple_logic),
401                kripke_logic: Some(kripke_logic),
402                ast: Some(ast_node),
403                readings,
404                simple_readings,
405                kripke_readings,
406                tokens,
407                error: None,
408            }
409        }
410        Err(e) => {
411            let advice = socratic_explanation(&e, &interner);
412            CompileResult {
413                logic: None,
414                simple_logic: None,
415                kripke_logic: None,
416                ast: None,
417                readings: Vec::new(),
418                simple_readings: Vec::new(),
419                kripke_readings: Vec::new(),
420                tokens,
421                error: Some(advice),
422            }
423        }
424    }
425}
426
427// ═══════════════════════════════════════════════════════════════════
428// Proof Integration
429// ═══════════════════════════════════════════════════════════════════
430
431/// Result of compiling English to a proof expression.
432///
433/// Used by the proof engine to search for derivations. Contains the
434/// converted proof expression, the simplified FOL string representation,
435/// and any compilation error.
436#[derive(Debug, Clone)]
437pub struct ProofCompileResult {
438    /// The compiled proof expression, or `None` on error.
439    pub proof_expr: Option<ProofExpr>,
440    /// Simplified FOL string representation for display.
441    pub logic_string: Option<String>,
442    /// Error message if compilation failed.
443    pub error: Option<String>,
444}
445
446/// Compile English input to ProofExpr for the proof engine.
447pub fn compile_for_proof(input: &str) -> ProofCompileResult {
448    use logicaffeine_language::proof_convert::logic_expr_to_proof_expr;
449
450    let mut interner = Interner::new();
451    let mut lexer = Lexer::new(input, &mut interner);
452    let lex_tokens = lexer.tokenize();
453
454    let mwe_trie = mwe::build_mwe_trie();
455    let lex_tokens = mwe::apply_mwe_pipeline(lex_tokens, &mwe_trie, &mut interner);
456
457    let type_registry = {
458        let mut discovery = DiscoveryPass::new(&lex_tokens, &mut interner);
459        discovery.run()
460    };
461
462    let expr_arena = Arena::new();
463    let term_arena = Arena::new();
464    let np_arena = Arena::new();
465    let sym_arena = Arena::new();
466    let role_arena = Arena::new();
467    let pp_arena = Arena::new();
468
469    let ctx = AstContext::new(
470        &expr_arena,
471        &term_arena,
472        &np_arena,
473        &sym_arena,
474        &role_arena,
475        &pp_arena,
476    );
477
478    let mut world_state = drs::WorldState::new();
479    let mut parser = Parser::new(lex_tokens, &mut world_state, &mut interner, ctx, type_registry);
480
481    match parser.parse() {
482        Ok(ast) => {
483            let ast = semantics::apply_axioms(ast, ctx.exprs, ctx.terms, &mut interner);
484            let ast = pragmatics::apply_pragmatics(ast, ctx.exprs, &interner);
485
486            let mut registry = SymbolRegistry::new();
487            let logic_string = ast.transpile(&mut registry, &interner, OutputFormat::SimpleFOL);
488            let proof_expr = logic_expr_to_proof_expr(ast, &interner);
489
490            ProofCompileResult {
491                proof_expr: Some(proof_expr),
492                logic_string: Some(logic_string),
493                error: None,
494            }
495        }
496        Err(e) => {
497            let advice = socratic_explanation(&e, &interner);
498            ProofCompileResult {
499                proof_expr: None,
500                logic_string: None,
501                error: Some(advice),
502            }
503        }
504    }
505}
506
507/// Result of compiling and verifying a theorem block.
508///
509/// Contains the parsed theorem structure (name, premises, goal) along
510/// with the proof derivation tree if automatic proof search succeeded.
511#[derive(Debug, Clone)]
512pub struct TheoremCompileResult {
513    /// The theorem's declared name.
514    pub name: String,
515    /// Compiled premise expressions (axioms).
516    pub premises: Vec<ProofExpr>,
517    /// The goal expression to prove, or `None` on parse error.
518    pub goal: Option<ProofExpr>,
519    /// Simplified FOL string of the goal for display.
520    pub goal_string: Option<String>,
521    /// Derivation tree from backward chaining, if proof found.
522    pub derivation: Option<DerivationTree>,
523    /// True iff the derivation was certified AND kernel type-checked.
524    /// A derivation alone (`derivation.is_some()`) never implies this.
525    pub verified: bool,
526    /// Where verification broke (certification or type-check), if it did.
527    pub verification_error: Option<String>,
528    /// For a wh-question goal ("Who is in Florida?"), the certified witness(es); `None`
529    /// when the goal is a closed proposition.
530    pub answer: Option<Vec<String>>,
531    /// For a recognized finite-domain grid, the cells the certified prover could fill —
532    /// attached whenever the premises take the grid form, no flag required.
533    pub grid: Option<SolvedGrid>,
534    /// Error message if compilation or proof failed.
535    pub error: Option<String>,
536}
537
538/// A solved (or partially solved) logic grid: the row entities plus one column per
539/// declared category closure. Every filled cell is an entailment the no-Z3 certified
540/// prover (CDCL+RUP / kernel) closed, so the whole table is independently checkable.
541#[derive(Debug, Clone, PartialEq)]
542pub struct SolvedGrid {
543    /// Display header for the identity column — the row sort (e.g. "Trip").
544    pub row_label: String,
545    /// The row entities in declaration order (e.g. Alpha, Beta, Gamma, Delta).
546    pub rows: Vec<String>,
547    /// One column per category closure (year, state, friend, activity, …).
548    pub columns: Vec<GridColumn>,
549}
550
551/// One category column of a [`SolvedGrid`].
552#[derive(Debug, Clone, PartialEq)]
553pub struct GridColumn {
554    /// Category header (the value sort, e.g. "Year"), best-effort from the declarations.
555    pub label: String,
556    /// The category's domain values in closure order.
557    pub values: Vec<String>,
558    /// The determined value for each row (parallel to [`SolvedGrid::rows`]), or `None`
559    /// where the prover could not force a unique value.
560    pub cells: Vec<Option<String>>,
561}
562
563/// A parsed theorem block: premises and goal as owned (arena-free) `ProofExpr`s, the goal's
564/// display string, and whether the strategy is `Auto`. Factored out of
565/// [`compile_theorem_for_ui`] so the grounded grid problem can feed every trust tier and the
566/// benchmark, not just the kernel path.
567struct ParsedTheorem {
568    name: String,
569    premises: Vec<ProofExpr>,
570    goal: ProofExpr,
571    goal_string: String,
572    is_auto: bool,
573    /// An explicit `Proof:` tactic script (English-esque vernacular), if the block
574    /// gave one instead of `Auto`. Run by the tactic framework, kernel-certified.
575    script: Option<String>,
576    /// Optional names for the premises (`Given (h): …`), parallel to `premises`, so a
577    /// script can refer to a premise as `h` rather than `hp0`.
578    premise_names: Vec<Option<String>>,
579}
580
581fn parse_theorem(input: &str) -> Result<ParsedTheorem, String> {
582    use logicaffeine_language::proof_convert::logic_expr_to_proof_expr;
583
584    let mut interner = Interner::new();
585    let mut lexer = Lexer::new(input, &mut interner);
586    let tokens = lexer.tokenize();
587
588    let mwe_trie = mwe::build_mwe_trie();
589    let tokens = mwe::apply_mwe_pipeline(tokens, &mwe_trie, &mut interner);
590
591    let type_registry = {
592        let mut discovery = DiscoveryPass::new(&tokens, &mut interner);
593        discovery.run()
594    };
595
596    let expr_arena = Arena::new();
597    let term_arena = Arena::new();
598    let np_arena = Arena::new();
599    let sym_arena = Arena::new();
600    let role_arena = Arena::new();
601    let pp_arena = Arena::new();
602
603    let ctx = AstContext::new(
604        &expr_arena,
605        &term_arena,
606        &np_arena,
607        &sym_arena,
608        &role_arena,
609        &pp_arena,
610    );
611
612    let mut world_state = drs::WorldState::new();
613    let mut parser = Parser::new(tokens, &mut world_state, &mut interner, ctx, type_registry);
614
615    let statements = parser
616        .parse_program()
617        .map_err(|e| format!("Parse error: {:?}", e))?;
618
619    let theorem = statements
620        .iter()
621        .find_map(|stmt| if let ast::Stmt::Theorem(t) = stmt { Some(t) } else { None })
622        .ok_or_else(|| "No theorem block found".to_string())?;
623
624    let premises: Vec<ProofExpr> = theorem
625        .premises
626        .iter()
627        .map(|p| logic_expr_to_proof_expr(p, &interner))
628        .collect();
629    let goal = logic_expr_to_proof_expr(theorem.goal, &interner);
630
631    let mut registry = SymbolRegistry::new();
632    let goal_string = theorem.goal.transpile(&mut registry, &interner, OutputFormat::SimpleFOL);
633
634    let script = match &theorem.strategy {
635        ast::theorem::ProofStrategy::Script(s) => Some(s.clone()),
636        _ => None,
637    };
638
639    Ok(ParsedTheorem {
640        name: theorem.name.clone(),
641        premises,
642        goal,
643        goal_string,
644        is_auto: matches!(theorem.strategy, ast::theorem::ProofStrategy::Auto),
645        script,
646        premise_names: theorem.premise_names.clone(),
647    })
648}
649
650/// A single theorem's verdict within a compiled formal development.
651#[derive(Debug, Clone)]
652pub struct TheoryTheoremResult {
653    pub name: String,
654    pub verified: bool,
655    pub error: Option<String>,
656}
657
658/// The result of compiling a program's formal development — its `## Axiom` declarations
659/// (a shared premise base) plus the theorems of its `## Theory` block, each discharged in
660/// citation order and kernel-certified by the multi-theorem driver.
661#[derive(Debug, Clone)]
662pub struct TheoryCompileResult {
663    pub theory_name: Option<String>,
664    pub axiom_count: usize,
665    pub theorems: Vec<TheoryTheoremResult>,
666    pub parse_error: Option<String>,
667}
668
669impl TheoryCompileResult {
670    /// Whether every theorem in the development was kernel-certified (and it parsed).
671    pub fn all_verified(&self) -> bool {
672        self.parse_error.is_none()
673            && !self.theorems.is_empty()
674            && self.theorems.iter().all(|t| t.verified)
675    }
676}
677
678/// Compile a program's formal development end to end: collect every `## Axiom` (a shared
679/// premise base) and every `## Theory` block's `Axiom`/`Theorem` declarations, then prove
680/// the theorems in citation order, each kernel-certified by the multi-theorem driver. This
681/// is the seam that turns a Tarski-style source file into a verified development.
682pub fn compile_theory_for_ui(input: &str) -> TheoryCompileResult {
683    use logicaffeine_proof::development::parse_development;
684    use logicaffeine_proof::formula::parse_formula;
685    use logicaffeine_proof::verify::{prove_library_with_axioms, LibraryTheorem};
686
687    // Parse the program with the same wiring as `parse_theorem`.
688    let mut interner = Interner::new();
689    let mut lexer = Lexer::new(input, &mut interner);
690    let tokens = lexer.tokenize();
691    let mwe_trie = mwe::build_mwe_trie();
692    let tokens = mwe::apply_mwe_pipeline(tokens, &mwe_trie, &mut interner);
693    let type_registry = {
694        let mut discovery = DiscoveryPass::new(&tokens, &mut interner);
695        discovery.run()
696    };
697    let expr_arena = Arena::new();
698    let term_arena = Arena::new();
699    let np_arena = Arena::new();
700    let sym_arena = Arena::new();
701    let role_arena = Arena::new();
702    let pp_arena = Arena::new();
703    let ctx = AstContext::new(
704        &expr_arena,
705        &term_arena,
706        &np_arena,
707        &sym_arena,
708        &role_arena,
709        &pp_arena,
710    );
711    let mut world_state = drs::WorldState::new();
712    let mut parser = Parser::new(tokens, &mut world_state, &mut interner, ctx, type_registry);
713    let statements = match parser.parse_program() {
714        Ok(s) => s,
715        Err(e) => {
716            return TheoryCompileResult {
717                theory_name: None,
718                axiom_count: 0,
719                theorems: Vec::new(),
720                parse_error: Some(format!("Parse error: {:?}", e)),
721            }
722        }
723    };
724
725    // Collect the development: standalone `## Axiom` blocks + `## Theory` bodies.
726    let mut axioms: Vec<ProofExpr> = Vec::new();
727    let mut theorems: Vec<LibraryTheorem> = Vec::new();
728    let mut theory_name: Option<String> = None;
729
730    for stmt in &statements {
731        match stmt {
732            ast::Stmt::Axiom(a) => match parse_formula(&a.formula) {
733                Ok(expr) => axioms.push(expr),
734                Err(e) => {
735                    return TheoryCompileResult {
736                        theory_name,
737                        axiom_count: axioms.len(),
738                        theorems: Vec::new(),
739                        parse_error: Some(format!("Axiom '{}': {}", a.name, e)),
740                    }
741                }
742            },
743            ast::Stmt::Theory(t) => {
744                if theory_name.is_none() {
745                    theory_name = Some(t.name.clone());
746                }
747                if !t.body.trim().is_empty() {
748                    match parse_development(&t.body) {
749                        Ok(dev) => {
750                            axioms.extend(dev.axiom_exprs());
751                            theorems.extend(dev.theorems);
752                        }
753                        Err(e) => {
754                            return TheoryCompileResult {
755                                theory_name,
756                                axiom_count: axioms.len(),
757                                theorems: Vec::new(),
758                                parse_error: Some(format!("Theory '{}': {}", t.name, e)),
759                            }
760                        }
761                    }
762                }
763            }
764            _ => {}
765        }
766    }
767
768    let results = prove_library_with_axioms(&axioms, &theorems);
769    let theorem_results = theorems
770        .iter()
771        .zip(results)
772        .map(|(t, r)| TheoryTheoremResult {
773            name: t.name.clone(),
774            verified: r.verified,
775            error: r.verification_error,
776        })
777        .collect();
778
779    TheoryCompileResult {
780        theory_name,
781        axiom_count: axioms.len(),
782        theorems: theorem_results,
783        parse_error: None,
784    }
785}
786
787/// The GROUNDED, quantifier-free grid problem `(solver_input, goal)` for an `Auto` grid
788/// theorem — the EXACT problem [`compile_theorem_for_ui`] solves, exposed so the trust tiers
789/// (Untrusted CDCL / RUP / Kernel) and the benchmark all run the same input. `None` if the
790/// theorem is not an `Auto` finite-domain grid.
791pub fn grounded_grid_problem(input: &str) -> Option<(Vec<ProofExpr>, ProofExpr)> {
792    let parsed = parse_theorem(input).ok()?;
793    if !parsed.is_auto || !looks_like_grid(&parsed.premises) {
794        return None;
795    }
796    let solver_input = prepare_premises_opts(&parsed.premises, true);
797    let g = erase_tense(&parsed.goal);
798    Some((solver_input, g))
799}
800
801/// Compile a theorem block for UI display.
802pub fn compile_theorem_for_ui(input: &str) -> TheoremCompileResult {
803    let parsed = match parse_theorem(input) {
804        Ok(p) => p,
805        Err(e) => {
806            return TheoremCompileResult {
807                name: String::new(),
808                premises: Vec::new(),
809                goal: None,
810                goal_string: None,
811                derivation: None,
812                verified: false,
813                verification_error: None,
814                answer: None,
815                grid: None,
816                error: Some(e),
817            };
818        }
819    };
820    let ParsedTheorem { name, premises, goal, goal_string, is_auto, script, premise_names } =
821        parsed;
822
823    // The easter egg: when the premises take the grid form (declared bijection +
824    // disjunctive closures), fill every cell the certified prover can force — no flag,
825    // the structure alone triggers it. Attached to every grid result so the studio can
826    // render the solved table alongside the headline goal.
827    let grid = if is_auto && looks_like_grid(&premises) {
828        solve_grid_from_premises(&premises, input)
829    } else {
830        None
831    };
832
833    let (derivation, verified, verification_error, answer) =
834        if is_auto {
835            // A wh-question goal ("Who is in Florida?") is an open ∃-form; the closed-goal
836            // prover cannot discharge it and grinds a deep, futile search. Recognize the
837            // form and ANSWER it by enumerating witnesses through the same certified no-Z3
838            // path the puzzle uses — each candidate proved (or refuted) in ~1ms.
839            if let ProofExpr::Exists { variable, body } = &goal {
840                let witnesses = answer_wh(&premises, variable, body);
841                let verified = !witnesses.is_empty();
842                let verr = (!verified)
843                    .then(|| "no individual in the domain satisfies the question".to_string());
844                (None, verified, verr, Some(witnesses))
845            } else {
846            // A finite-domain GRID (a declared closure / "exactly one") is proved over
847            // its GROUNDED, quantifier-free form, so the certified kernel can close it
848            // in the browser (no Z3). An OPEN syllogism (Socrates: "every man is
849            // mortal") is left untouched, keeping its `UniversalInst`/`ModusPonens`
850            // trace. Both remain kernel-certified.
851            let outcome = if looks_like_grid(&premises) {
852                let g = erase_tense(&goal);
853                // The incremental certified grid solver (trail propagation + DPLL) emits
854                // a DerivationTree the kernel re-checks; it is faster and avoids the
855                // general prover's re-saturation, and is the only path given the
856                // FUNCTIONALITY exclusions. It is strictly additive: only a VERIFIED
857                // solver tree is used, otherwise we fall back to the bounded backward
858                // chainer (without functionality), so the solver can never regress.
859                let solver_input = prepare_premises_opts(&premises, true);
860                let solve = || {
861                    let trace = std::env::var("LOGOS_TRACE").is_ok();
862                    let t_solve = trace.then(std::time::Instant::now);
863                    let tree = logicaffeine_proof::grid_solver::grid_prove(&solver_input, &g);
864                    if let Some(t_solve) = t_solve {
865                        let n = tree.as_ref().map(count_tree_nodes).unwrap_or(0);
866                        eprintln!("[grid] solve+emit {:.2?} ({} tree nodes)", t_solve.elapsed(), n);
867                    }
868                    let t_cert = trace.then(std::time::Instant::now);
869                    let solved = tree
870                        .map(|tree| logicaffeine_proof::verify::check_derivation(&solver_input, &g, tree))
871                        .filter(|vp| vp.verified);
872                    if let Some(t_cert) = t_cert {
873                        eprintln!("[grid] kernel-certify {:.2?} (verified={})", t_cert.elapsed(), solved.is_some());
874                    }
875                    match solved {
876                        Some(vp) => vp,
877                        None => {
878                            // The grid solver produced no certified tree. Before the
879                            // expensive bounded chainer, ask the fast certified RUP tier
880                            // whether the goal is even entailed — a non-entailed goal is
881                            // decided in ~1ms instead of grinding a deep, futile search.
882                            // (RUP runs the SAME `solver_input`, so a NotEntailed verdict is
883                            // sound: the goal is not entailed by the premises.)
884                            if matches!(
885                                logicaffeine_proof::rup::entails_certified(&solver_input, &g),
886                                Some(logicaffeine_proof::rup::Verdict::NotEntailed)
887                            ) {
888                                return logicaffeine_proof::verify::VerifiedProof {
889                                    derivation: None,
890                                    proof_term: None,
891                                    kernel_ctx: Default::default(),
892                                    verified: false,
893                                    verification_error: Some(
894                                        "goal is not entailed (RUP-certified)".to_string(),
895                                    ),
896                                };
897                            }
898                            let grounded = prepare_premises(&premises);
899                            logicaffeine_proof::verify::prove_certify_check_bounded(&grounded, &g, 60)
900                        }
901                    }
902                };
903                // A full multi-category grid's certified derivation recurses deeply
904                // (case-analysis over every interdependent clue), so run the solve on a
905                // generous stack — well past the default 8 MiB — on native targets.
906                #[cfg(not(target_arch = "wasm32"))]
907                {
908                    std::thread::scope(|s| {
909                        std::thread::Builder::new()
910                            .stack_size(512 * 1024 * 1024)
911                            .spawn_scoped(s, solve)
912                            .expect("spawn grid-solve thread")
913                            .join()
914                            .expect("grid-solve thread panicked")
915                    })
916                }
917                #[cfg(target_arch = "wasm32")]
918                {
919                    solve()
920                }
921            } else {
922                logicaffeine_proof::verify::prove_certify_check(&premises, &goal)
923            };
924            (outcome.derivation, outcome.verified, outcome.verification_error, None)
925            }
926        } else if let Some(src) = &script {
927            // An explicit `Proof:` tactic script (English-esque vernacular): run it
928            // through the tactic framework on a generous stack, kernel-certified.
929            use logicaffeine_proof::tactic::ProofState;
930            let run = || {
931                let mut st =
932                    ProofState::start_with_names(premises.clone(), &premise_names, goal.clone());
933                match st.run_script(src) {
934                    Ok(_) => match st.qed() {
935                        Ok(vp) => (vp.derivation, vp.verified, vp.verification_error),
936                        Err(e) => (None, false, Some(format!("{e:?}"))),
937                    },
938                    Err(e) => (None, false, Some(e.to_string())),
939                }
940            };
941            #[cfg(not(target_arch = "wasm32"))]
942            let (derivation, verified, verr) = std::thread::scope(|s| {
943                std::thread::Builder::new()
944                    .stack_size(256 * 1024 * 1024)
945                    .spawn_scoped(s, run)
946                    .expect("spawn proof-script thread")
947                    .join()
948                    .expect("proof-script thread panicked")
949            });
950            #[cfg(target_arch = "wasm32")]
951            let (derivation, verified, verr) = run();
952            (derivation, verified, verr, None)
953        } else {
954            (None, false, None, None)
955        };
956
957    TheoremCompileResult {
958        name,
959        premises,
960        goal: Some(goal),
961        goal_string: Some(goal_string),
962        derivation,
963        verified,
964        verification_error,
965        answer,
966        grid,
967        error: None,
968    }
969}
970
971fn count_tree_nodes(t: &logicaffeine_proof::DerivationTree) -> usize {
972    1 + t.premises.iter().map(count_tree_nodes).sum::<usize>()
973}
974
975// ═══════════════════════════════════════════════════════════════════
976// Code Generation
977// ═══════════════════════════════════════════════════════════════════
978
979/// Generate Rust code from LOGOS source — the Studio Code path. Mixed-document
980/// aware: a source interleaving imperative code with Coq-style math
981/// (`Definition`/`## Theorem:`/…) extracts the math into a bundled `mod proven` that
982/// the imperative half calls into. Pure imperative source is a no-op partition.
983#[cfg(feature = "codegen")]
984pub fn generate_rust_code(source: &str) -> Result<String, ParseError> {
985    let (imperative_src, math_src) = partition_mixed(source);
986    let proven = math_src.as_deref().and_then(mixed_proven_module);
987    generate_rust_code_with_proven(&imperative_src, proven.as_deref())
988}
989
990/// [`generate_rust_code`] with an already-extracted proven module bundled in.
991#[cfg(feature = "codegen")]
992pub fn generate_rust_code_with_proven(source: &str, proven: Option<&str>) -> Result<String, ParseError> {
993    use logicaffeine_language::ast::stmt::{Stmt, Expr, TypeExpr};
994
995    let mut interner = Interner::new();
996    let mut lexer = Lexer::new(source, &mut interner);
997    let tokens = lexer.tokenize();
998
999    let mwe_trie = mwe::build_mwe_trie();
1000    let tokens = mwe::apply_mwe_pipeline(tokens, &mwe_trie, &mut interner);
1001
1002    let (type_registry, policy_registry) = {
1003        let mut discovery = DiscoveryPass::new(&tokens, &mut interner);
1004        let result = discovery.run_full();
1005        (result.types, result.policies)
1006    };
1007    let codegen_registry = type_registry.clone();
1008    let codegen_policies = policy_registry.clone();
1009
1010    let mut world_state = drs::WorldState::new();
1011    let expr_arena = Arena::new();
1012    let term_arena = Arena::new();
1013    let np_arena = Arena::new();
1014    let sym_arena = Arena::new();
1015    let role_arena = Arena::new();
1016    let pp_arena = Arena::new();
1017    let stmt_arena: Arena<Stmt> = Arena::new();
1018    let imperative_expr_arena: Arena<Expr> = Arena::new();
1019    let type_expr_arena: Arena<TypeExpr> = Arena::new();
1020
1021    let ast_ctx = AstContext::with_types(
1022        &expr_arena,
1023        &term_arena,
1024        &np_arena,
1025        &sym_arena,
1026        &role_arena,
1027        &pp_arena,
1028        &stmt_arena,
1029        &imperative_expr_arena,
1030        &type_expr_arena,
1031    );
1032
1033    let mut parser = Parser::new(tokens, &mut world_state, &mut interner, ast_ctx, type_registry);
1034    let stmts = parser.parse_program()?;
1035
1036    let type_env = crate::analysis::types::TypeEnv::infer_program(&stmts, &interner, &codegen_registry);
1037    let rust_code = crate::codegen::codegen_program_with_proven(&stmts, &codegen_registry, &codegen_policies, &interner, &type_env, &crate::optimization::OptimizationConfig::from_env(), "proven", proven);
1038    Ok(rust_code)
1039}
1040
1041// ═══════════════════════════════════════════════════════════════════
1042// Interpreter (async)
1043// ═══════════════════════════════════════════════════════════════════
1044
1045/// Interpret LOGOS imperative code and return output lines.
1046///
1047/// Same engine dispatch as [`interpret_for_ui_sync`]: the bytecode VM runs
1048/// synchronous programs (with the tree-walker as the debug shadow oracle);
1049/// programs needing async (file I/O, sleep, mount) run on the tree-walker's
1050/// async executor.
1051/// The first Send/escape-analysis violation in `stmts`, packaged as a ready
1052/// rejection — or `None` if the program respects the message-passing + CRDT
1053/// memory model. This is the static concurrency gate (Phase 4): a program whose
1054/// concurrent branches share non-CRDT mutable state is refused *before* any tier
1055/// runs, so neither the VM nor the tree-walker executes a data race.
1056fn send_escape_rejection(stmts: &[logicaffeine_language::ast::stmt::Stmt]) -> Option<InterpreterResult> {
1057    crate::concurrency::check_send_escape(stmts)
1058        .first()
1059        .map(|d| InterpreterResult { lines: Vec::new(), error: Some(d.message.clone()) })
1060}
1061
1062/// Reject a dimension-incoherent program (`2 meters + 1 gram`) BEFORE it runs, so the interpreter
1063/// tier matches the AOT compiler: a dimension mismatch is an analysis error on every tier, surfaced
1064/// before any output is produced — not a mid-execution failure. Mirrors `send_escape_rejection`.
1065fn send_dimension_rejection(
1066    stmts: &[logicaffeine_language::ast::stmt::Stmt],
1067    interner: &Interner,
1068) -> Option<InterpreterResult> {
1069    crate::analysis::dimension_check::DimensionChecker::new(interner)
1070        .check_program(stmts)
1071        .err()
1072        .map(|e| InterpreterResult { lines: Vec::new(), error: Some(e.message) })
1073}
1074
1075pub async fn interpret_for_ui(input: &str) -> InterpreterResult {
1076    interpret_for_ui_with_args(input, &[]).await
1077}
1078
1079/// Like [`interpret_for_ui`], but supplies the program's argument vector to the
1080/// `args()` system native. `program_args` is the full argv (index 0 is the
1081/// program name), matching the compiled binary's `env::args()`.
1082pub async fn interpret_for_ui_with_args(
1083    input: &str,
1084    program_args: &[String],
1085) -> InterpreterResult {
1086    // The synchronous dispatcher covers every non-async program; for async
1087    // ones it uses block_on, which would nest inside this future — so handle
1088    // the async case here with a real await.
1089    let needs_async = with_parsed_program(input, |parsed, _| match parsed {
1090        Ok((stmts, _, _)) => crate::interpreter::needs_async(stmts),
1091        Err(_) => false,
1092    });
1093    if !needs_async {
1094        return interpret_for_ui_sync_with_args(input, program_args);
1095    }
1096
1097    use logicaffeine_language::ast::stmt::{Stmt, Expr, TypeExpr};
1098
1099    let mut interner = Interner::new();
1100    let mut lexer = Lexer::new(input, &mut interner);
1101    let tokens = lexer.tokenize();
1102
1103    let mwe_trie = mwe::build_mwe_trie();
1104    let tokens = mwe::apply_mwe_pipeline(tokens, &mwe_trie, &mut interner);
1105
1106    let (type_registry, policy_registry) = {
1107        let mut discovery = DiscoveryPass::new(&tokens, &mut interner);
1108        let result = discovery.run_full();
1109        (result.types, result.policies)
1110    };
1111
1112    let expr_arena = Arena::new();
1113    let term_arena = Arena::new();
1114    let np_arena = Arena::new();
1115    let sym_arena = Arena::new();
1116    let role_arena = Arena::new();
1117    let pp_arena = Arena::new();
1118    let stmt_arena: Arena<Stmt> = Arena::new();
1119    let imperative_expr_arena: Arena<Expr> = Arena::new();
1120    let type_expr_arena: Arena<TypeExpr> = Arena::new();
1121
1122    let ctx = AstContext::with_types(
1123        &expr_arena,
1124        &term_arena,
1125        &np_arena,
1126        &sym_arena,
1127        &role_arena,
1128        &pp_arena,
1129        &stmt_arena,
1130        &imperative_expr_arena,
1131        &type_expr_arena,
1132    );
1133
1134    let mut world_state = drs::WorldState::new();
1135    let type_registry_for_interp = type_registry.clone();
1136    let mut parser = Parser::new(tokens, &mut world_state, &mut interner, ctx, type_registry);
1137
1138    match parser.parse_program() {
1139        Ok(stmts) => {
1140            if let Some(rejection) = send_escape_rejection(&stmts) {
1141                return rejection;
1142            }
1143            // A program that uses channels/tasks AND needs async (e.g. networking) must run
1144            // on the cooperative scheduler — `interp.run` alone installs none, so a channel op
1145            // would panic "outside a scheduler context". The async drive loop services both
1146            // the channel ops and the network awaits (the latter over the host reactor). A
1147            // pure-channel program (no async) never reaches here; it takes the sync path.
1148            if crate::concurrency::uses_scheduler(&stmts) {
1149                return run_program_concurrent_streaming(
1150                    &stmts,
1151                    &type_registry_for_interp,
1152                    policy_registry,
1153                    &interner,
1154                    program_args,
1155                    None,
1156                    None,
1157                    None,
1158                    0,
1159                )
1160                .await;
1161            }
1162            let mut interp = crate::interpreter::Interpreter::new(&interner)
1163                .with_type_registry(&type_registry_for_interp)
1164                .with_policies(policy_registry)
1165                .with_program_args(program_args.to_vec());
1166            match interp.run(&stmts).await {
1167                Ok(()) => InterpreterResult {
1168                    lines: interp.output,
1169                    error: None,
1170                },
1171                Err(e) => InterpreterResult {
1172                    lines: interp.output,
1173                    error: Some(e),
1174                },
1175            }
1176        }
1177        Err(e) => {
1178            let advice = socratic_explanation(&e, &interner);
1179            InterpreterResult {
1180                lines: vec![],
1181                error: Some(advice),
1182            }
1183        }
1184    }
1185}
1186
1187/// The execution front-end shared by every engine: lex → MWE → discovery →
1188/// parse. The tree-walker and the bytecode VM MUST both come through here so
1189/// they see the identical token stream, type registry, and policies — a
1190/// differential test that parses differently per engine is comparing two
1191/// different programs.
1192///
1193/// Parsed statements borrow stack-local arenas, so they are handed to the
1194/// closure rather than returned. A parse failure is delivered as
1195/// `Err(socratic advice text)` — also identical for both engines.
1196pub fn with_parsed_program<R>(
1197    input: &str,
1198    f: impl for<'a> FnOnce(
1199        Result<
1200            (
1201                &'a [logicaffeine_language::ast::stmt::Stmt<'a>],
1202                &'a logicaffeine_language::analysis::TypeRegistry,
1203                logicaffeine_language::analysis::PolicyRegistry,
1204            ),
1205            String,
1206        >,
1207        &'a Interner,
1208    ) -> R,
1209) -> R {
1210    use logicaffeine_language::ast::stmt::{Expr, Stmt, TypeExpr};
1211
1212    // A bare script (no `##` headers, imperative opening) runs as `## Main`.
1213    let implicit = implicit_main(input);
1214    let input = implicit.as_deref().unwrap_or(input);
1215
1216    // Phase 10: auto-prepend the stdlib modules the program references (no-op when
1217    // it uses no stdlib vocabulary, so such programs stay byte-identical).
1218    let prelude_src = crate::loader::apply_prelude(input);
1219    let input = prelude_src.as_ref();
1220
1221    let mut interner = Interner::new();
1222    let mut lexer = Lexer::new(input, &mut interner);
1223    let tokens = lexer.tokenize();
1224
1225    let mwe_trie = mwe::build_mwe_trie();
1226    let tokens = mwe::apply_mwe_pipeline(tokens, &mwe_trie, &mut interner);
1227
1228    let (type_registry, policy_registry) = {
1229        let mut discovery = DiscoveryPass::new(&tokens, &mut interner);
1230        let result = discovery.run_full();
1231        (result.types, result.policies)
1232    };
1233
1234    let expr_arena = Arena::new();
1235    let term_arena = Arena::new();
1236    let np_arena = Arena::new();
1237    let sym_arena = Arena::new();
1238    let role_arena = Arena::new();
1239    let pp_arena = Arena::new();
1240    let stmt_arena: Arena<Stmt> = Arena::new();
1241    let imperative_expr_arena: Arena<Expr> = Arena::new();
1242    let type_expr_arena: Arena<TypeExpr> = Arena::new();
1243
1244    let ctx = AstContext::with_types(
1245        &expr_arena,
1246        &term_arena,
1247        &np_arena,
1248        &sym_arena,
1249        &role_arena,
1250        &pp_arena,
1251        &stmt_arena,
1252        &imperative_expr_arena,
1253        &type_expr_arena,
1254    );
1255
1256    let mut world_state = drs::WorldState::new();
1257    let type_registry_for_engines = type_registry.clone();
1258    let (parsed, opt_flags) = {
1259        let mut parser = Parser::new(tokens, &mut world_state, &mut interner, ctx, type_registry);
1260        let stmts = parser.parse_program();
1261        let flags = parser.program_opt_flags();
1262        (stmts, flags)
1263    };
1264
1265    match parsed {
1266        Ok(stmts) => {
1267            // Strength-reduce accumulator recursion to a constant-stack `while`
1268            // loop so the VM and tree-walker match the AOT (and never hit the
1269            // call-depth limit on `Return n + f(n-1)`-shaped recursion).
1270            // Type-directed division: rewrite `Divide → ExactDivide` in Rational
1271            // contexts (default stays floor), then accumulator-TCO the resolved AST.
1272            // The constant Rational fold honors the `Opt::Comptime` toggle (env +
1273            // in-source `## No comptime`), consistent with the AOT and tiered paths.
1274            let mut run_cfg =
1275                crate::optimization::OptimizationConfig::from_env().merged(&opt_flags);
1276            run_cfg.normalize();
1277            let resolved = crate::resolve_division::resolve_divisions(
1278                &stmts,
1279                &stmt_arena,
1280                &imperative_expr_arena,
1281                &interner,
1282                run_cfg.is_on(crate::optimization::Opt::Comptime),
1283            );
1284            let pre = resolved.unwrap_or(stmts.as_slice());
1285            match crate::tail_call::rewrite_accumulators(
1286                pre,
1287                &stmt_arena,
1288                &imperative_expr_arena,
1289                &mut interner,
1290            ) {
1291                Some(rw) => f(Ok((rw, &type_registry_for_engines, policy_registry)), &interner),
1292                None => f(Ok((pre, &type_registry_for_engines, policy_registry)), &interner),
1293            }
1294        }
1295        Err(e) => {
1296            let advice = socratic_explanation(&e, &interner);
1297            f(Err(advice), &interner)
1298        }
1299    }
1300}
1301
1302/// [`with_parsed_program`], but the statements pass through the RUN-PATH
1303/// optimizer (EXODIA D1: the Futamura residual — PE, GVN, LICM, closed-form,
1304/// deforestation, interval analysis, DCE) before reaching the closure. The
1305/// live `largo run` engines consume this; the differential seams stay on the
1306/// raw variant so optimizer correctness is itself differentially gated
1307/// (optimized-VM vs raw-tree-walker).
1308pub fn with_optimized_program<R>(
1309    input: &str,
1310    f: impl for<'a> FnOnce(
1311        Result<
1312            (
1313                &'a [logicaffeine_language::ast::stmt::Stmt<'a>],
1314                &'a logicaffeine_language::analysis::TypeRegistry,
1315                logicaffeine_language::analysis::PolicyRegistry,
1316            ),
1317            String,
1318        >,
1319        &'a Interner,
1320    ) -> R,
1321) -> R {
1322    // The run-path upfront tier is the env-configured mode (default Eager → T3, i.e.
1323    // today's behavior, bit-for-bit). `LOGOS_TIER_PROFILE`/`LOGOS_FORCE_TIER` select
1324    // Tiered/Baseline for the benchmark A/B and the T_optimize measurement (§12.1).
1325    let tier = crate::optimization::HotswapConfig::from_env().run_tier();
1326    with_optimized_program_tiered(input, tier, f)
1327}
1328
1329/// [`with_optimized_program`] gated by an explicit hotness `tier` (HOTSWAP §4): the
1330/// statements pass through [`crate::optimize::optimize_for_run_tiered`] at `tier`.
1331/// `Tier::T3` reproduces `with_optimized_program` exactly; `Tier::T0` skips the
1332/// optimizer (the baseline tier). The accumulator→loop TCO rewrite is a LANGUAGE
1333/// SEMANTIC, not an optimization, so it runs at every tier — deep recursion stays
1334/// constant-stack regardless of the optimization tier.
1335pub fn with_optimized_program_tiered<R>(
1336    input: &str,
1337    tier: crate::optimization::Tier,
1338    f: impl for<'a> FnOnce(
1339        Result<
1340            (
1341                &'a [logicaffeine_language::ast::stmt::Stmt<'a>],
1342                &'a logicaffeine_language::analysis::TypeRegistry,
1343                logicaffeine_language::analysis::PolicyRegistry,
1344            ),
1345            String,
1346        >,
1347        &'a Interner,
1348    ) -> R,
1349) -> R {
1350    use logicaffeine_language::ast::stmt::{Expr, Stmt, TypeExpr};
1351
1352    // A bare script (no `##` headers, imperative opening) runs as `## Main`.
1353    let implicit = implicit_main(input);
1354    let input = implicit.as_deref().unwrap_or(input);
1355
1356    // Phase 10: auto-prepend the stdlib modules the program references (no-op when
1357    // it uses no stdlib vocabulary, so such programs stay byte-identical).
1358    let prelude_src = crate::loader::apply_prelude(input);
1359    let input = prelude_src.as_ref();
1360
1361    let mut interner = Interner::new();
1362    let mut lexer = Lexer::new(input, &mut interner);
1363    let tokens = lexer.tokenize();
1364
1365    let mwe_trie = mwe::build_mwe_trie();
1366    let tokens = mwe::apply_mwe_pipeline(tokens, &mwe_trie, &mut interner);
1367
1368    let (type_registry, policy_registry) = {
1369        let mut discovery = DiscoveryPass::new(&tokens, &mut interner);
1370        let result = discovery.run_full();
1371        (result.types, result.policies)
1372    };
1373
1374    let expr_arena = Arena::new();
1375    let term_arena = Arena::new();
1376    let np_arena = Arena::new();
1377    let sym_arena = Arena::new();
1378    let role_arena = Arena::new();
1379    let pp_arena = Arena::new();
1380    let stmt_arena: Arena<Stmt> = Arena::new();
1381    let imperative_expr_arena: Arena<Expr> = Arena::new();
1382    let type_expr_arena: Arena<TypeExpr> = Arena::new();
1383
1384    let ctx = AstContext::with_types(
1385        &expr_arena,
1386        &term_arena,
1387        &np_arena,
1388        &sym_arena,
1389        &role_arena,
1390        &pp_arena,
1391        &stmt_arena,
1392        &imperative_expr_arena,
1393        &type_expr_arena,
1394    );
1395
1396    let mut world_state = drs::WorldState::new();
1397    let type_registry_for_engines = type_registry.clone();
1398    let (parsed, opt_flags, tier_pins) = {
1399        let mut parser = Parser::new(tokens, &mut world_state, &mut interner, ctx, type_registry);
1400        let stmts = parser.parse_program();
1401        let flags = parser.program_opt_flags();
1402        let pins = parser.program_tier_pins();
1403        (stmts, flags, pins)
1404    };
1405
1406    match parsed {
1407        Ok(stmts) => {
1408            // Respect file-level `## No <opt>` decorators on the run path too, so
1409            // optimization toggles behave consistently with the AOT compile.
1410            let mut run_cfg =
1411                crate::optimization::OptimizationConfig::from_env().merged(&opt_flags);
1412            run_cfg.normalize();
1413            // Tiered-optimizer pins: ambient env (`LOGOS_TIER_PIN`) overlaid by the
1414            // program's in-source `## Tier` decorators (the decorator wins).
1415            let mut hotswap = crate::optimization::HotswapConfig::from_env();
1416            hotswap.pins.overlay(&tier_pins);
1417            // Type-directed division (Divide → ExactDivide in Rational contexts) runs
1418            // BEFORE optimization so the optimizer sees ExactDivide (opaque, never
1419            // floor-folded), not a bare `7 / 2` it would fold to `3`.
1420            let resolved = crate::resolve_division::resolve_divisions(
1421                &stmts,
1422                &stmt_arena,
1423                &imperative_expr_arena,
1424                &interner,
1425                run_cfg.is_on(crate::optimization::Opt::Comptime),
1426            );
1427            let pre: Vec<_> = match resolved {
1428                Some(rw) => rw.to_vec(),
1429                None => stmts,
1430            };
1431            let optimized = crate::optimize::optimize_for_run_tiered(
1432                pre,
1433                &imperative_expr_arena,
1434                &stmt_arena,
1435                &mut interner,
1436                &run_cfg,
1437                &hotswap,
1438                tier,
1439            );
1440            // Accumulator recursion → constant-stack loop (matches the AOT and
1441            // the raw-parse engines).
1442            match crate::tail_call::rewrite_accumulators(
1443                &optimized,
1444                &stmt_arena,
1445                &imperative_expr_arena,
1446                &mut interner,
1447            ) {
1448                Some(rw) => f(Ok((rw, &type_registry_for_engines, policy_registry)), &interner),
1449                None => f(Ok((&optimized, &type_registry_for_engines, policy_registry)), &interner),
1450            }
1451        }
1452        Err(e) => {
1453            let advice = socratic_explanation(&e, &interner);
1454            f(Err(advice), &interner)
1455        }
1456    }
1457}
1458
1459/// Like [`with_optimized_program`], but the program runs through the
1460/// ARCHITECT pipeline (`optimize_program_v2`): equality saturation with
1461/// kernel-certified rewrites in place of GVN + LICM + closed-form. The
1462/// differential gates in `phase_exodia_architect.rs` hold this path to the
1463/// raw tree-walker's outcomes.
1464pub fn with_v2_optimized_program<R>(
1465    input: &str,
1466    f: impl for<'a> FnOnce(
1467        Result<
1468            (
1469                &'a [logicaffeine_language::ast::stmt::Stmt<'a>],
1470                &'a logicaffeine_language::analysis::TypeRegistry,
1471                logicaffeine_language::analysis::PolicyRegistry,
1472            ),
1473            String,
1474        >,
1475        &'a Interner,
1476    ) -> R,
1477) -> R {
1478    use logicaffeine_language::ast::stmt::{Expr, Stmt, TypeExpr};
1479
1480    // A bare script (no `##` headers, imperative opening) runs as `## Main`.
1481    let implicit = implicit_main(input);
1482    let input = implicit.as_deref().unwrap_or(input);
1483
1484    // Phase 10: auto-prepend the stdlib modules the program references (no-op when
1485    // it uses no stdlib vocabulary, so such programs stay byte-identical).
1486    let prelude_src = crate::loader::apply_prelude(input);
1487    let input = prelude_src.as_ref();
1488
1489    let mut interner = Interner::new();
1490    let mut lexer = Lexer::new(input, &mut interner);
1491    let tokens = lexer.tokenize();
1492
1493    let mwe_trie = mwe::build_mwe_trie();
1494    let tokens = mwe::apply_mwe_pipeline(tokens, &mwe_trie, &mut interner);
1495
1496    let (type_registry, policy_registry) = {
1497        let mut discovery = DiscoveryPass::new(&tokens, &mut interner);
1498        let result = discovery.run_full();
1499        (result.types, result.policies)
1500    };
1501
1502    let expr_arena = Arena::new();
1503    let term_arena = Arena::new();
1504    let np_arena = Arena::new();
1505    let sym_arena = Arena::new();
1506    let role_arena = Arena::new();
1507    let pp_arena = Arena::new();
1508    let stmt_arena: Arena<Stmt> = Arena::new();
1509    let imperative_expr_arena: Arena<Expr> = Arena::new();
1510    let type_expr_arena: Arena<TypeExpr> = Arena::new();
1511
1512    let ctx = AstContext::with_types(
1513        &expr_arena,
1514        &term_arena,
1515        &np_arena,
1516        &sym_arena,
1517        &role_arena,
1518        &pp_arena,
1519        &stmt_arena,
1520        &imperative_expr_arena,
1521        &type_expr_arena,
1522    );
1523
1524    let mut world_state = drs::WorldState::new();
1525    let type_registry_for_engines = type_registry.clone();
1526    let parsed = {
1527        let mut parser = Parser::new(tokens, &mut world_state, &mut interner, ctx, type_registry);
1528        parser.parse_program()
1529    };
1530
1531    match parsed {
1532        Ok(stmts) => {
1533            let optimized = crate::optimize::optimize_program(
1534                stmts,
1535                &imperative_expr_arena,
1536                &stmt_arena,
1537                &mut interner,
1538                &crate::optimization::OptimizationConfig::from_env(),
1539            );
1540            f(Ok((&optimized, &type_registry_for_engines, policy_registry)), &interner)
1541        }
1542        Err(e) => {
1543            let advice = socratic_explanation(&e, &interner);
1544            f(Err(advice), &interner)
1545        }
1546    }
1547}
1548
1549/// Interpret LOGOS imperative code synchronously (no async runtime needed).
1550///
1551/// The bytecode VM is the LIVE engine for the synchronous path (which is also
1552/// the browser/WASM path). The tree-walker remains fully wired as: the async
1553/// path (file I/O, sleep, mount), the shadow oracle in debug builds (every
1554/// program the corpus runs is differentially checked against it), and the
1555/// fallback for anything the VM compiler rejects.
1556pub fn interpret_for_ui_sync(input: &str) -> InterpreterResult {
1557    interpret_for_ui_sync_with_args(input, &[])
1558}
1559
1560#[cfg(not(target_arch = "wasm32"))]
1561thread_local! {
1562    /// Compiled-native functions to install into the next VM run on this thread
1563    /// (HOTSWAP §Axis-3). `largo run` loads a `.logos-native` bundle into this; the VM
1564    /// seam below drains it and installs each via `install_aot_native`, so a `native`-
1565    /// annotated function dispatches to `rustc -O3` machine code from its first call.
1566    /// Empty for every other caller — no AOT, behavior unchanged.
1567    static PENDING_AOT: std::cell::RefCell<Vec<(String, Box<dyn crate::vm::NativeFn>)>> =
1568        const { std::cell::RefCell::new(Vec::new()) };
1569}
1570
1571/// Queue compiled-native functions for the next VM run on this thread (HOTSWAP §Axis-3).
1572/// Consumed by the next `interpret_for_ui_*` VM run.
1573#[cfg(not(target_arch = "wasm32"))]
1574pub fn set_pending_aot_natives(natives: Vec<(String, Box<dyn crate::vm::NativeFn>)>) {
1575    PENDING_AOT.with(|p| *p.borrow_mut() = natives);
1576}
1577
1578/// Drain the pending compiled-native functions and install them into `vm`, resolving
1579/// each by name to its function index. No-op when none are queued.
1580#[cfg(not(target_arch = "wasm32"))]
1581fn install_pending_aot_natives(
1582    vm: &mut crate::vm::Vm,
1583    program: &crate::vm::CompiledProgram,
1584    interner: &Interner,
1585) {
1586    let pending = PENDING_AOT.with(|p| std::mem::take(&mut *p.borrow_mut()));
1587    for (name, nf) in pending {
1588        if let Some(fi) = program
1589            .fn_index
1590            .iter()
1591            .find(|(s, _)| interner.resolve(**s) == name)
1592            .map(|(_, i)| *i as usize)
1593        {
1594            vm.install_aot_native(fi, nf);
1595            if std::env::var_os("LOGOS_ENGINE_TRACE").is_some() {
1596                eprintln!("logos-engine: aot-native installed for '{name}'");
1597            }
1598        }
1599    }
1600}
1601
1602/// Like [`interpret_for_ui_sync`], but supplies the program's argument vector
1603/// to the `args()` system native. `program_args` is the full argv (index 0 is
1604/// the program name), matching the compiled binary's `env::args()` — this is
1605/// what `largo run --interpret N` forwards so an args-driven `main.lg` runs the
1606/// same on the VM/JIT as on the native binary.
1607pub fn interpret_for_ui_sync_with_args(input: &str, program_args: &[String]) -> InterpreterResult {
1608    // Diagnostic for benchmarking: with LOGOS_ENGINE_TRACE set, report which
1609    // engine actually ran a program to stderr, so a silent tree-walker fallback
1610    // is never mistaken for the VM+JIT.
1611    let trace = |engine: &str| {
1612        if std::env::var_os("LOGOS_ENGINE_TRACE").is_some() {
1613            eprintln!("logos-engine: {engine}");
1614        }
1615    };
1616    // The LIVE path runs the Futamura residual: both the VM and the debug
1617    // shadow oracle receive the SAME optimized program (two engines, one
1618    // program — the optimizer is differentially gated elsewhere against the
1619    // raw tree-walker).
1620    with_optimized_program(input, |parsed, interner| match parsed {
1621        Ok((stmts, type_registry, policies)) => {
1622            if let Some(rejection) = send_escape_rejection(stmts) {
1623                return rejection;
1624            }
1625            if let Some(rejection) = send_dimension_rejection(stmts, interner) {
1626                return rejection;
1627            }
1628            if crate::interpreter::needs_async(stmts) || crate::concurrency::uses_scheduler(stmts) {
1629                trace("treewalker (async)");
1630                return run_treewalker(stmts, type_registry, policies, interner, true, program_args);
1631            }
1632            // Oracle range analysis (M9) over the EXACT snapshot being
1633            // compiled → bounds-check elimination in the JIT.
1634            let oracle = crate::optimize::oracle_analyze_with(stmts, interner);
1635            match crate::vm::Compiler::compile_with_oracle(
1636                stmts,
1637                interner,
1638                Some(type_registry),
1639                Some(oracle),
1640            ) {
1641                Ok(program) => {
1642                    trace("vm+jit");
1643                    let mut vm = crate::vm::Vm::new(&program)
1644                        .with_policy_ctx(&policies, interner)
1645                        .with_program_args(program_args.to_vec());
1646                    if let Some(tier) = crate::vm::installed_native_tier() {
1647                        vm = vm.with_native_tier(tier);
1648                    }
1649                    // Compiled-native tier (HOTSWAP §Axis-3): install any AOT functions
1650                    // the run was given, so they dispatch to rustc -O3 machine code.
1651                    #[cfg(not(target_arch = "wasm32"))]
1652                    install_pending_aot_natives(&mut vm, &program, interner);
1653                    let error = vm.run().err();
1654                    let result = InterpreterResult { lines: vm.into_lines(), error };
1655
1656                    // Debug-build shadow oracle: the SAME program runs on the
1657                    // tree-walker and the full outcome must match — this turns
1658                    // the entire existing test corpus into a differential
1659                    // suite. (Skipped on wasm to keep dev builds light.)
1660                    #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
1661                    {
1662                        let shadow = run_treewalker(
1663                            stmts,
1664                            type_registry,
1665                            policies.clone(),
1666                            interner,
1667                            false,
1668                            program_args,
1669                        );
1670                        assert_eq!(
1671                            (&result.lines, &result.error),
1672                            (&shadow.lines, &shadow.error),
1673                            "VM diverged from the tree-walker oracle for:\n{input}"
1674                        );
1675                    }
1676                    result
1677                }
1678                // The VM compiler rejects only constructs outside the parser's
1679                // reach; run them on the tree-walker rather than failing.
1680                Err(_) => {
1681                    trace("treewalker (vm-reject)");
1682                    run_treewalker(stmts, type_registry, policies, interner, false, program_args)
1683                }
1684            }
1685        }
1686        Err(advice) => InterpreterResult { lines: vec![], error: Some(advice) },
1687    })
1688}
1689
1690/// Baseline-tier interpret for the interactive UI (the Studio) — the cold-start
1691/// engine. Runs on the bytecode VM with NO run-path optimizer
1692/// (`with_parsed_program`, not `with_optimized_program`) and NO oracle
1693/// (`compile_with_types`, not `compile_with_oracle`), so there is no
1694/// optimize/analysis latency before execution. `largo run` and the benchmarks
1695/// keep [`interpret_for_ui`], which optimizes ahead of execution for peak
1696/// throughput. Output is identical — the VM is differentially gated against the
1697/// tree-walker on the SAME parsed statements (see the debug shadow-oracle assert
1698/// in [`interpret_for_ui_baseline_sync_with_args`]); only the startup latency
1699/// differs. This is the baseline half of the EXODIA tier split; promoting hot
1700/// code to the optimized path is a later step.
1701pub async fn interpret_for_ui_baseline(input: &str) -> InterpreterResult {
1702    interpret_for_ui_baseline_with_args(input, &[]).await
1703}
1704
1705/// Like [`interpret_for_ui_baseline`], but supplies the program's argument
1706/// vector to the `args()` system native.
1707pub async fn interpret_for_ui_baseline_with_args(
1708    input: &str,
1709    program_args: &[String],
1710) -> InterpreterResult {
1711    // Async programs (file I/O, sleep, mount) run on the tree-walker's async
1712    // executor regardless of tier — the VM is sync-only — so that case is
1713    // identical to the optimized entry (and must `await` rather than nest a
1714    // `block_on`); delegate it.
1715    let needs_async = with_parsed_program(input, |parsed, _| match parsed {
1716        Ok((stmts, _, _)) => crate::interpreter::needs_async(stmts),
1717        Err(_) => false,
1718    });
1719    if needs_async {
1720        return interpret_for_ui_with_args(input, program_args).await;
1721    }
1722    interpret_for_ui_baseline_sync_with_args(input, program_args)
1723}
1724
1725/// The synchronous baseline core: parse (UNoptimized) → bytecode VM (no oracle)
1726/// → run. Mirrors [`interpret_for_ui_sync_with_args`] exactly, minus
1727/// `optimize_for_run` and the oracle range analysis.
1728pub fn interpret_for_ui_baseline_sync_with_args(
1729    input: &str,
1730    program_args: &[String],
1731) -> InterpreterResult {
1732    let trace = |engine: &str| {
1733        if std::env::var_os("LOGOS_ENGINE_TRACE").is_some() {
1734            eprintln!("logos-engine: {engine}");
1735        }
1736    };
1737    with_parsed_program(input, |parsed, interner| match parsed {
1738        Ok((stmts, type_registry, policies)) => {
1739            if let Some(rejection) = send_escape_rejection(stmts) {
1740                return rejection;
1741            }
1742            if let Some(rejection) = send_dimension_rejection(stmts, interner) {
1743                return rejection;
1744            }
1745            if crate::interpreter::needs_async(stmts) || crate::concurrency::uses_scheduler(stmts) {
1746                trace("treewalker (async)");
1747                return run_treewalker(stmts, type_registry, policies, interner, true, program_args);
1748            }
1749            match crate::vm::Compiler::compile_with_types(stmts, interner, Some(type_registry)) {
1750                Ok(program) => {
1751                    trace("vm (baseline)");
1752                    let mut vm = crate::vm::Vm::new(&program)
1753                        .with_policy_ctx(&policies, interner)
1754                        .with_program_args(program_args.to_vec());
1755                    if let Some(tier) = crate::vm::installed_native_tier() {
1756                        vm = vm.with_native_tier(tier);
1757                    }
1758                    // Compiled-native tier (HOTSWAP §Axis-3): install any AOT functions
1759                    // the run was given, so they dispatch to rustc -O3 machine code.
1760                    #[cfg(not(target_arch = "wasm32"))]
1761                    install_pending_aot_natives(&mut vm, &program, interner);
1762                    let error = vm.run().err();
1763                    let result = InterpreterResult { lines: vm.into_lines(), error };
1764
1765                    // The same debug differential net as the optimized path: the
1766                    // baseline VM and the tree-walker must agree on the SAME
1767                    // (unoptimized) statements. (Skipped on wasm to keep dev
1768                    // builds light.)
1769                    #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
1770                    {
1771                        let shadow = run_treewalker(
1772                            stmts,
1773                            type_registry,
1774                            policies.clone(),
1775                            interner,
1776                            false,
1777                            program_args,
1778                        );
1779                        assert_eq!(
1780                            (&result.lines, &result.error),
1781                            (&shadow.lines, &shadow.error),
1782                            "baseline VM diverged from the tree-walker oracle for:\n{input}"
1783                        );
1784                    }
1785                    result
1786                }
1787                // The VM compiler rejects only constructs outside the parser's
1788                // reach; run them on the tree-walker rather than failing.
1789                Err(_) => {
1790                    trace("treewalker (vm-reject)");
1791                    run_treewalker(stmts, type_registry, policies, interner, false, program_args)
1792                }
1793            }
1794        }
1795        Err(advice) => InterpreterResult { lines: vec![], error: Some(advice) },
1796    })
1797}
1798
1799/// Run a parsed program on the TREE-WALKER (the oracle engine). `force_async`
1800/// selects the async executor; otherwise the sync path is used.
1801/// Run a concurrent program on the deterministic scheduler: spawn the main block
1802/// as a task (which may spawn more), drive the scheduler to quiescence, and
1803/// collect the output merged through a shared callback.
1804fn run_program_concurrent<'a>(
1805    stmts: &'a [logicaffeine_language::ast::stmt::Stmt<'a>],
1806    type_registry: &logicaffeine_language::analysis::TypeRegistry,
1807    policies: logicaffeine_language::analysis::PolicyRegistry,
1808    interner: &'a Interner,
1809    program_args: &[String],
1810    vfs: Option<std::sync::Arc<dyn logicaffeine_system::fs::Vfs>>,
1811    stream: Option<crate::interpreter::OutputCallback>,
1812    seed: u64,
1813) -> InterpreterResult {
1814    use crate::concurrency::bridge::YieldState;
1815    use crate::concurrency::driver::InterpreterTask;
1816    use logicaffeine_runtime::{run_with_seed, RunOutcome, SchedSeed, SchedulerConfig};
1817    use std::cell::RefCell;
1818    use std::rc::Rc;
1819
1820    let output_sink: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
1821    let sink = output_sink.clone();
1822    // Collect every task's output for the final result, and — when streaming —
1823    // forward each line to the live callback as it is produced (the Studio's
1824    // real-time display).
1825    let callback: crate::interpreter::OutputCallback =
1826        Rc::new(RefCell::new(move |line: String| {
1827            if let Some(s) = &stream {
1828                (s.borrow_mut())(line.clone());
1829            }
1830            sink.borrow_mut().push(line);
1831        }));
1832    let err_sink: crate::concurrency::driver::ErrSink = Rc::new(RefCell::new(None));
1833
1834    let mut main = crate::interpreter::Interpreter::new(interner)
1835        .with_type_registry(type_registry)
1836        .with_policies(policies)
1837        .with_program_args(program_args.to_vec())
1838        .with_output_callback(callback);
1839    if let Some(v) = vfs {
1840        main = main.with_vfs(v);
1841    }
1842    let main_ys = Rc::new(RefCell::new(YieldState::new()));
1843    main.install_yield_state(main_ys.clone());
1844
1845    let main_fut = Box::pin(async move { main.run(stmts).await });
1846    let main_task = InterpreterTask::new(main_fut, main_ys, Some(err_sink.clone()));
1847
1848    let (outcome, _trace) =
1849        run_with_seed(SchedulerConfig::default(), SchedSeed(seed), move |sched| {
1850            sched.spawn_main(Box::new(main_task));
1851        });
1852
1853    let mut error = err_sink.borrow().clone();
1854    if error.is_none() {
1855        match outcome {
1856            RunOutcome::Deadlock => {
1857                error = Some("deadlock: every task is blocked waiting".to_string());
1858            }
1859            // The synchronous scheduler has no reactor; a program that needs network I/O
1860            // must run on the async drive loop (it routes there via `needs_async`). Reaching
1861            // here means it was driven on the wrong tier — surface it instead of half-running.
1862            RunOutcome::WaitingForIo => {
1863                error = Some(
1864                    "networking requires the async runtime; this program was run on the \
1865                     synchronous scheduler"
1866                        .to_string(),
1867                );
1868            }
1869            RunOutcome::Done(_) => {}
1870        }
1871    }
1872    let lines = output_sink.borrow().clone();
1873    InterpreterResult { lines, error }
1874}
1875
1876/// A snapshot sink for the Studio's Tasks/Channels strip — invoked between scheduler
1877/// slices with the live task/channel state.
1878pub type ObserverCallback = std::rc::Rc<std::cell::RefCell<dyn FnMut(logicaffeine_runtime::SchedSnapshot)>>;
1879
1880/// Yield a macrotask so the host event loop (Dioxus) can repaint between scheduler
1881/// slices. On wasm this is a real `setTimeout(0)`; on native there is no event loop to
1882/// yield to, so it returns immediately and the drive loop continues — the output is
1883/// identical, only the interleaving with repaints differs.
1884async fn yield_macrotask() {
1885    #[cfg(target_arch = "wasm32")]
1886    {
1887        gloo_timers::future::TimeoutFuture::new(0).await;
1888    }
1889}
1890
1891/// Yield control back to the host async runtime once, so its reactor can advance a pending
1892/// network future before the scheduler re-polls the task that awaits it. Runtime-agnostic
1893/// (no tokio dependency): a future that wakes itself and returns `Pending` exactly once — the
1894/// same shape as `tokio::task::yield_now`, which lets the current-thread runtime poll its I/O
1895/// driver between turns. On wasm a `setTimeout(0)` lets the event loop fire the socket
1896/// callbacks. This is how a [`logicaffeine_runtime::RunOutcome::WaitingForIo`] is serviced.
1897async fn yield_to_reactor() {
1898    #[cfg(target_arch = "wasm32")]
1899    {
1900        gloo_timers::future::TimeoutFuture::new(0).await;
1901    }
1902    #[cfg(not(target_arch = "wasm32"))]
1903    {
1904        use std::future::Future;
1905        use std::pin::Pin;
1906        use std::task::{Context, Poll};
1907        struct YieldOnce(bool);
1908        impl Future for YieldOnce {
1909            type Output = ();
1910            fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
1911                if self.0 {
1912                    Poll::Ready(())
1913                } else {
1914                    self.0 = true;
1915                    cx.waker().wake_by_ref();
1916                    Poll::Pending
1917                }
1918            }
1919        }
1920        YieldOnce(false).await;
1921    }
1922}
1923
1924/// The browser-facing async driver for concurrent programs. Identical semantics to
1925/// [`run_program_concurrent`] (same deterministic scheduler, same seed, same output), but
1926/// it advances the scheduler in bounded slices and yields a macrotask between them so the
1927/// Studio repaints instead of freezing — and emits a [`logicaffeine_runtime::SchedSnapshot`]
1928/// to `observer` after each slice to drive the Tasks/Channels strip.
1929#[allow(clippy::too_many_arguments)]
1930async fn run_program_concurrent_streaming<'a>(
1931    stmts: &'a [logicaffeine_language::ast::stmt::Stmt<'a>],
1932    type_registry: &logicaffeine_language::analysis::TypeRegistry,
1933    policies: logicaffeine_language::analysis::PolicyRegistry,
1934    interner: &'a Interner,
1935    program_args: &[String],
1936    vfs: Option<std::sync::Arc<dyn logicaffeine_system::fs::Vfs>>,
1937    stream: Option<crate::interpreter::OutputCallback>,
1938    observer: Option<ObserverCallback>,
1939    seed: u64,
1940) -> InterpreterResult {
1941    use crate::concurrency::bridge::YieldState;
1942    use crate::concurrency::driver::InterpreterTask;
1943    use logicaffeine_runtime::{Chooser, RunOutcome, SchedSeed, Scheduler, SchedulerConfig};
1944    use std::cell::RefCell;
1945    use std::rc::Rc;
1946
1947    // The slice budget: how many scheduler steps to run before yielding a macrotask. Large
1948    // enough that a quiescent program finishes in one slice; small enough that a busy one
1949    // still repaints. It does not affect output (the scheduler is deterministic).
1950    const SLICE_STEPS: usize = 256;
1951
1952    let output_sink: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
1953    let sink = output_sink.clone();
1954    let callback: crate::interpreter::OutputCallback = Rc::new(RefCell::new(move |line: String| {
1955        if let Some(s) = &stream {
1956            (s.borrow_mut())(line.clone());
1957        }
1958        sink.borrow_mut().push(line);
1959    }));
1960    let err_sink: crate::concurrency::driver::ErrSink = Rc::new(RefCell::new(None));
1961
1962    let mut main = crate::interpreter::Interpreter::new(interner)
1963        .with_type_registry(type_registry)
1964        .with_policies(policies)
1965        .with_program_args(program_args.to_vec())
1966        .with_output_callback(callback);
1967    if let Some(v) = vfs {
1968        main = main.with_vfs(v);
1969    }
1970    let main_ys = Rc::new(RefCell::new(YieldState::new()));
1971    main.install_yield_state(main_ys.clone());
1972
1973    let main_fut = Box::pin(async move { main.run(stmts).await });
1974    let main_task = InterpreterTask::new(main_fut, main_ys, Some(err_sink.clone()));
1975
1976    let mut sched = Scheduler::new(SchedulerConfig::default(), Chooser::record(SchedSeed(seed)));
1977    sched.spawn_main(Box::new(main_task));
1978
1979    let outcome = loop {
1980        match sched.run_slice(SLICE_STEPS) {
1981            Some(RunOutcome::WaitingForIo) => {
1982                // A task is awaiting external I/O (a network op). Let the host reactor make
1983                // progress, then re-poll the parked tasks — they re-drive their network
1984                // futures, which now observe completion.
1985                if let Some(ob) = &observer {
1986                    (ob.borrow_mut())(sched.snapshot());
1987                }
1988                yield_to_reactor().await;
1989                sched.wake_io();
1990            }
1991            Some(o) => break o,
1992            None => {
1993                if let Some(ob) = &observer {
1994                    (ob.borrow_mut())(sched.snapshot());
1995                }
1996                yield_macrotask().await;
1997            }
1998        }
1999    };
2000
2001    let mut error = err_sink.borrow().clone();
2002    if error.is_none() {
2003        if let RunOutcome::Deadlock = outcome {
2004            error = Some("deadlock: every task is blocked waiting".to_string());
2005        }
2006    }
2007    let lines = output_sink.borrow().clone();
2008    InterpreterResult { lines, error }
2009}
2010
2011/// Parse `input` and run its concurrent program on the **tree-walker** scheduler
2012/// under an explicit `seed` — the seeded sibling of the default
2013/// `run_program_concurrent` entry, used by the cross-tier seeded differential.
2014pub fn run_treewalker_concurrent_seeded(input: &str, seed: u64) -> InterpreterResult {
2015    with_parsed_program(input, |parsed, interner| match parsed {
2016        Ok((stmts, type_registry, policies)) => {
2017            run_program_concurrent(stmts, type_registry, policies, interner, &[], None, None, seed)
2018        }
2019        Err(advice) => InterpreterResult { lines: vec![], error: Some(advice) },
2020    })
2021}
2022
2023/// Run a concurrent program on the **bytecode VM** under the deterministic
2024/// scheduler (T11): compile to opcodes, then drive the main `Vm` (and any
2025/// spawned per-task VMs) through `run_until_block` via a `VmTask`. This is the
2026/// VM analog of [`run_program_concurrent`]; the default routing still sends
2027/// concurrent programs to the tree-walker, so this is the explicit VM entry used
2028/// to exercise and (Phase 5c) differentially compare the VM concurrency tier.
2029pub fn run_vm_concurrent(input: &str) -> InterpreterResult {
2030    run_vm_concurrent_seeded(input, 0)
2031}
2032
2033/// [`run_vm_concurrent`] under an explicit scheduler `seed` — the VM sibling of
2034/// [`run_treewalker_concurrent_seeded`] for the cross-tier seeded differential.
2035pub fn run_vm_concurrent_seeded(input: &str, seed: u64) -> InterpreterResult {
2036    use crate::concurrency::vm_driver::VmTask;
2037    use logicaffeine_runtime::{run_with_seed, RunOutcome, SchedSeed, SchedulerConfig};
2038    use std::cell::RefCell;
2039    use std::rc::Rc;
2040
2041    with_parsed_program(input, |parsed, interner| match parsed {
2042        Ok((stmts, type_registry, policies)) => {
2043            let program = match crate::vm::Compiler::compile_with_types(
2044                stmts,
2045                interner,
2046                Some(type_registry),
2047            ) {
2048                Ok(p) => p,
2049                Err(e) => return InterpreterResult { lines: vec![], error: Some(e) },
2050            };
2051            let output: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
2052            let err_sink: crate::concurrency::driver::ErrSink = Rc::new(RefCell::new(None));
2053            let mut vm = crate::vm::Vm::new(&program).with_policy_ctx(&policies, interner);
2054            // Task bodies tier exactly like the main program: a hot integer loop
2055            // inside a task JIT-compiles. Concurrency ops are JIT-ineligible (not
2056            // integer ops ⇒ never region-selected), so a tiered region is
2057            // yield-free and a task only ever suspends on the bytecode path.
2058            // Spawned children inherit this tier via `spawn_task_vm`.
2059            #[cfg(not(target_arch = "wasm32"))]
2060            if let Some(tier) = crate::vm::installed_native_tier() {
2061                vm = vm.with_native_tier(tier);
2062            }
2063            let main_task = VmTask::new(vm, output.clone(), Some(err_sink.clone()));
2064            let (outcome, _trace) =
2065                run_with_seed(SchedulerConfig::default(), SchedSeed(seed), move |sched| {
2066                    sched.spawn_main(Box::new(main_task));
2067                });
2068            let mut error = err_sink.borrow().clone();
2069            if error.is_none() {
2070                if let RunOutcome::Deadlock = outcome {
2071                    error = Some("deadlock: every task is blocked waiting".to_string());
2072                }
2073            }
2074            let lines = output.borrow().clone();
2075            InterpreterResult { lines, error }
2076        }
2077        Err(advice) => InterpreterResult { lines: vec![], error: Some(advice) },
2078    })
2079}
2080
2081/// One cooperative poll interval for the VM net runner — a short async sleep so the relay delivers
2082/// (and, in the browser, the event loop runs) between `Await` polls. Mirrors the tree-walker's.
2083async fn vm_net_poll_tick() {
2084    #[cfg(not(target_arch = "wasm32"))]
2085    logicaffeine_system::tokio::time::sleep(std::time::Duration::from_millis(2)).await;
2086    #[cfg(target_arch = "wasm32")]
2087    gloo_timers::future::TimeoutFuture::new(2).await;
2088}
2089
2090/// A peer value → its relay topic (mirrors the tree-walker's `Interpreter::peer_topic_of`).
2091fn vm_peer_topic_of(value: &crate::interpreter::RuntimeValue) -> Result<String, String> {
2092    use crate::interpreter::RuntimeValue;
2093    match value {
2094        RuntimeValue::Peer(topic) => Ok((**topic).clone()),
2095        RuntimeValue::Text(s) => Ok(logicaffeine_system::addr::canonical_topic(s)),
2096        other => Err(format!(
2097            "Send/Await expects a PeerAgent or address string, got {}",
2098            other.type_name()
2099        )),
2100    }
2101}
2102
2103/// Service one suspended VM networking request through the shared [`NetInbox`] — the SAME inbox the
2104/// tree-walker uses, so the wire encode/decode is identical and a `Send`/`Await` produces the same
2105/// value on both tiers. `Connect`/`Listen` do the async relay I/O; `Send`/`Stream` encode + publish;
2106/// `Await` drains-and-takes (yielding between polls) until the peer's message arrives.
2107async fn service_vm_net_block(
2108    vm: &mut crate::vm::Vm<'_>,
2109    netbox: &mut crate::concurrency::net_inbox::NetInbox,
2110    req: crate::vm::VmBlock,
2111) -> Result<(), String> {
2112    use crate::concurrency::marshal::{self};
2113    use crate::interpreter::RuntimeValue;
2114    use crate::vm::{Value, VmBlock};
2115    match req {
2116        VmBlock::NetConnect(url_payload) => {
2117            let raw = marshal::rebuild(url_payload).to_display_string();
2118            let url = logicaffeine_system::addr::multiaddr_to_ws_url(&raw).map_err(|e| {
2119                format!("Connect address '{raw}' is not a ws:// URL or supported multiaddr: {e}")
2120            })?;
2121            // OFFLINE mode: `Connect` is a single-node local no-op (mirrors the tree-walker's offline
2122            // `Connect`), so the VM net oracle agrees with it. A relay-connected driver dials for real.
2123            if !crate::concurrency::net_inbox::net_is_offline() {
2124                let net = logicaffeine_system::net::Net::connect(&url)
2125                    .await
2126                    .map_err(|e| format!("Connect to relay '{url}' failed: {e}"))?;
2127                netbox.net = Some(net);
2128            }
2129            vm.deliver_resume(Value::nothing());
2130            Ok(())
2131        }
2132        VmBlock::NetListen(topic_payload) => {
2133            let raw = marshal::rebuild(topic_payload).to_display_string();
2134            let topic = logicaffeine_system::addr::canonical_topic(&raw);
2135            let hs_topic = crate::concurrency::net_inbox::handshake_topic_for(&topic);
2136            // LOCAL/OFFLINE mode (no relay): declare the inbox identity locally, skip the relay
2137            // subscribe — mirrors the tree-walker's offline `Listen` so both tiers agree.
2138            if let Some(net) = netbox.net.as_mut() {
2139                net.subscribe(&topic).await?;
2140                // Also receive peers' capability handshakes on our dedicated handshake topic (kept off
2141                // the data topic so they never interleave with messages).
2142                net.subscribe(&hs_topic).await?;
2143            }
2144            netbox.inbox = Some(std::rc::Rc::new(topic));
2145            vm.deliver_resume(Value::nothing());
2146            Ok(())
2147        }
2148        VmBlock::NetSend(to_payload, msg_payload) => {
2149            let topic = vm_peer_topic_of(&marshal::rebuild(to_payload))?;
2150            // Advertise our surface to this peer on first contact — on its HANDSHAKE topic, never the
2151            // data topic — so it can negotiate back. Absorbed by the peer's drain, invisible to data.
2152            if let Some(hs) = netbox.first_contact_handshake(&topic) {
2153                netbox.publish(&crate::concurrency::net_inbox::handshake_topic_for(&topic), hs)?;
2154            }
2155            let value = marshal::rebuild(msg_payload);
2156            let from = netbox.inbox.as_ref().map(|t| t.to_string()).unwrap_or_default();
2157            // The negotiated maximal crush — byte-identical to the tree-walker's plain send (both call
2158            // `encode_negotiated` with the same program registry). Type-id fires only on a matching
2159            // peer epoch, so a raw / different-program peer still gets the plain encoding.
2160            let bytes = netbox.encode_negotiated(&from, &value, &topic, netbox.my_registry.clone())?;
2161            netbox.publish(&topic, bytes)?;
2162            vm.deliver_resume(Value::nothing());
2163            Ok(())
2164        }
2165        VmBlock::NetStream(to_payload, values_payload) => {
2166            let topic = vm_peer_topic_of(&marshal::rebuild(to_payload))?;
2167            if let Some(hs) = netbox.first_contact_handshake(&topic) {
2168                netbox.publish(&crate::concurrency::net_inbox::handshake_topic_for(&topic), hs)?;
2169            }
2170            let list = marshal::rebuild(values_payload);
2171            let items = match &list {
2172                RuntimeValue::List(rc) => rc.borrow().to_values(),
2173                other => vec![other.clone()],
2174            };
2175            let from = netbox.inbox.as_ref().map(|t| t.to_string()).unwrap_or_default();
2176            let blob = marshal::frame_stream_message(&from, &items)?;
2177            netbox.publish(&topic, blob)?;
2178            vm.deliver_resume(Value::nothing());
2179            Ok(())
2180        }
2181        VmBlock::NetAwait(from_payload, stream) => {
2182            let want = vm_peer_topic_of(&marshal::rebuild(from_payload))?;
2183            if netbox.inbox.is_none() {
2184                return Err("Await requires a prior Listen to establish an inbox".to_string());
2185            }
2186            // OFFLINE (no relay): drain our own loopback outbox (the `Send … to <self>` delivered
2187            // locally) rather than requiring a relay — the VM net oracle mirrors the tree-walker.
2188            loop {
2189                let got = if stream {
2190                    netbox.try_take_stream(&want)
2191                } else {
2192                    netbox.try_take_message(&want, false)
2193                };
2194                if let Some(v) = got {
2195                    vm.deliver_resume(Value::from_runtime(v));
2196                    return Ok(());
2197                }
2198                // No type-id registry: the VM net runner sends self-describing (no `shared` dial),
2199                // so an empty registry decodes everything correctly.
2200                netbox.drain(netbox.my_registry.clone());
2201                let got = if stream {
2202                    netbox.try_take_stream(&want)
2203                } else {
2204                    netbox.try_take_message(&want, false)
2205                };
2206                if let Some(v) = got {
2207                    vm.deliver_resume(Value::from_runtime(v));
2208                    return Ok(());
2209                }
2210                vm_net_poll_tick().await;
2211            }
2212        }
2213        VmBlock::NetMakePeer(addr_payload) => {
2214            let raw = marshal::rebuild(addr_payload).to_display_string();
2215            let topic = logicaffeine_system::addr::canonical_topic(&raw);
2216            vm.deliver_resume(Value::from_runtime(RuntimeValue::Peer(std::rc::Rc::new(topic))));
2217            Ok(())
2218        }
2219        VmBlock::NetSync(topic_payload, current_payload) => {
2220            let topic = marshal::rebuild(topic_payload).to_display_string();
2221            let current = marshal::rebuild(current_payload);
2222            // Encode the counter (Int) or counter-struct as the relay wire form; `None` ⇒ nothing
2223            // to publish yet. Then merge whatever has arrived since the last sync point.
2224            let publish_bytes = crate::semantics::arith::crdt_to_wire(&current);
2225            // LOCAL/OFFLINE mode: single-node sync is a no-op — the local CRDT value stands (mirrors
2226            // the tree-walker's offline `Sync`). A relay-connected node publishes + merges.
2227            let merged = if let Some(net) = netbox.net.as_mut() {
2228                net.subscribe(&topic).await?;
2229                if let Some(bytes) = publish_bytes {
2230                    net.publish(&topic, bytes)?;
2231                }
2232                let incoming = net.drain();
2233                let mut merged = current;
2234                for (_t, data) in incoming {
2235                    merged = crate::semantics::arith::crdt_merge_wire(merged, &data);
2236                }
2237                merged
2238            } else {
2239                current
2240            };
2241            vm.deliver_resume(Value::from_runtime(merged));
2242            Ok(())
2243        }
2244        _ => Err("run_vm_net_async services only peer-networking blocks; this program mixes \
2245                  channels/tasks with networking, which the VM net runner does not yet drive"
2246            .to_string()),
2247    }
2248}
2249
2250/// Run a peer-networking program on the BYTECODE VM (single task, no channels): drive the resumable
2251/// VM and service each `VmBlock::Net*` through the shared `NetInbox` — the SAME inbox the
2252/// tree-walker uses — so `Connect`/`Listen`/`Send`/`Stream`/`Await` run byte-identically on both
2253/// tiers. The VM analog of the tree-walker's networking path; the cross-tier lock proves they agree.
2254pub async fn run_vm_net_async(input: &str) -> InterpreterResult {
2255    use logicaffeine_language::ast::stmt::{Expr, Stmt, TypeExpr};
2256
2257    let mut interner = Interner::new();
2258    let mut lexer = Lexer::new(input, &mut interner);
2259    let tokens = lexer.tokenize();
2260    let mwe_trie = mwe::build_mwe_trie();
2261    let tokens = mwe::apply_mwe_pipeline(tokens, &mwe_trie, &mut interner);
2262    let (type_registry, policy_registry) = {
2263        let mut discovery = DiscoveryPass::new(&tokens, &mut interner);
2264        let result = discovery.run_full();
2265        (result.types, result.policies)
2266    };
2267    let expr_arena = Arena::new();
2268    let term_arena = Arena::new();
2269    let np_arena = Arena::new();
2270    let sym_arena = Arena::new();
2271    let role_arena = Arena::new();
2272    let pp_arena = Arena::new();
2273    let stmt_arena: Arena<Stmt> = Arena::new();
2274    let imperative_expr_arena: Arena<Expr> = Arena::new();
2275    let type_expr_arena: Arena<TypeExpr> = Arena::new();
2276    let ctx = AstContext::with_types(
2277        &expr_arena,
2278        &term_arena,
2279        &np_arena,
2280        &sym_arena,
2281        &role_arena,
2282        &pp_arena,
2283        &stmt_arena,
2284        &imperative_expr_arena,
2285        &type_expr_arena,
2286    );
2287    let mut world_state = drs::WorldState::new();
2288    let type_registry_for_vm = type_registry.clone();
2289    let mut parser = Parser::new(tokens, &mut world_state, &mut interner, ctx, type_registry);
2290    let stmts = match parser.parse_program() {
2291        Ok(s) => s,
2292        Err(e) => return InterpreterResult { lines: vec![], error: Some(format!("{e:?}")) },
2293    };
2294    let program =
2295        match crate::vm::Compiler::compile_with_types(&stmts, &interner, Some(&type_registry_for_vm)) {
2296            Ok(p) => p,
2297            Err(e) => return InterpreterResult { lines: vec![], error: Some(e) },
2298        };
2299    let mut vm = crate::vm::Vm::new(&program).with_policy_ctx(&policy_registry, &interner);
2300    let mut netbox = crate::concurrency::net_inbox::NetInbox::new();
2301    // Build the wire type registry from the analysis types — IDENTICALLY to the tree-walker's
2302    // `build_wire_type_registry` (both derive from the same analysis table) — so two same-program peers
2303    // advertise matching registry epochs and may elide type NAMES from the wire (type-id).
2304    {
2305        let mut structs: Vec<(String, Vec<String>)> = Vec::new();
2306        let mut enums: Vec<(String, Vec<String>)> = Vec::new();
2307        for (name_sym, type_def) in type_registry_for_vm.iter_types() {
2308            let type_name = interner.resolve(*name_sym).to_string();
2309            match type_def {
2310                crate::analysis::registry::TypeDef::Struct { fields, .. } => {
2311                    structs.push((type_name, fields.iter().map(|f| interner.resolve(f.name).to_string()).collect()));
2312                }
2313                crate::analysis::registry::TypeDef::Enum { variants, .. } => {
2314                    enums.push((type_name, variants.iter().map(|v| interner.resolve(v.name).to_string()).collect()));
2315                }
2316                _ => {}
2317            }
2318        }
2319        netbox.set_registry(
2320            crate::concurrency::marshal::WireTypeRegistry::new(structs).with_enums(enums),
2321        );
2322    }
2323    let mut lines: Vec<String> = Vec::new();
2324    let mut error = None;
2325    loop {
2326        let step = vm.run_until_block();
2327        lines.extend(vm.drain_lines());
2328        match step {
2329            Ok(crate::vm::VmStep::Done(_)) => break,
2330            Ok(crate::vm::VmStep::Blocked) => {
2331                let req = match vm.take_pending() {
2332                    Some(r) => r,
2333                    None => break,
2334                };
2335                if let Err(e) = service_vm_net_block(&mut vm, &mut netbox, req).await {
2336                    error = Some(e);
2337                    break;
2338                }
2339            }
2340            Ok(crate::vm::VmStep::Paused) => break,
2341            Err(e) => {
2342                error = Some(e);
2343                break;
2344            }
2345        }
2346    }
2347    InterpreterResult { lines, error }
2348}
2349
2350/// [`run_vm_concurrent_seeded`] under the **work-stealing M:N driver**: `workers`
2351/// OS-thread workers poll task bodies in parallel while one coordinator owns the
2352/// scheduler and applies channel ops + flushes output in deterministic pick
2353/// order. The observable result is byte-identical to the cooperative driver at the
2354/// same seed (`diff_cooperative_eq_workstealing_seeded`) — the difference is that
2355/// task bodies genuinely run on multiple cores.
2356///
2357/// The executor uses scoped threads, so each worker *borrows* the one shared,
2358/// immutable program (+ policies + interner) — no clone, no leak. Only a `Send`
2359/// `SpawnDesc` crosses a worker boundary; the worker rebuilds the `!Send` task
2360/// body locally from it.
2361#[cfg(not(target_arch = "wasm32"))]
2362pub fn run_vm_workstealing_seeded(input: &str, seed: u64, workers: usize) -> InterpreterResult {
2363    use crate::concurrency::vm_driver::VmTask;
2364    use logicaffeine_runtime::{
2365        run_workstealing_seeded, RunOutcome, SchedSeed, SchedulerConfig, SpawnDesc, Task,
2366    };
2367
2368    with_parsed_program(input, |parsed, interner| match parsed {
2369        Ok((stmts, type_registry, policies)) => {
2370            let program = match crate::vm::Compiler::compile_with_types(
2371                stmts,
2372                interner,
2373                Some(type_registry),
2374            ) {
2375                Ok(p) => p,
2376                Err(e) => return InterpreterResult { lines: vec![], error: Some(e) },
2377            };
2378            // Build a worker-local task body from a `Send` descriptor. Workers run
2379            // the bytecode path: the native JIT tier is per-thread state (its code
2380            // cache is `!Sync`), so no shared tier is installed across workers.
2381            // Tiering is output-preserving, so this stays byte-identical to the
2382            // cooperative (tiered) driver; per-worker JIT is a future optimization.
2383            // The parallelism is the task bodies running on separate cores, which
2384            // the bytecode interpreter already delivers. The main task runs the
2385            // top-level program (`Vm::new` positions there); a spawned task is
2386            // positioned at its function via `setup_task`.
2387            let build = |desc: SpawnDesc| -> Box<dyn Task<'_> + '_> {
2388                let mut vm = crate::vm::Vm::new(&program).with_policy_ctx(&policies, interner);
2389                if !desc.is_main {
2390                    vm.setup_task(desc.func, &desc.args);
2391                }
2392                Box::new(VmTask::work_stealing(vm, None))
2393            };
2394            let main = SpawnDesc { func: 0, args: vec![], priority: 0, is_main: true };
2395            let config = SchedulerConfig::default().with_workers(workers.max(1));
2396            let result = run_workstealing_seeded(config, SchedSeed(seed), main, build);
2397            let error = match result.outcome {
2398                RunOutcome::Deadlock => {
2399                    Some("deadlock: every task is blocked waiting".to_string())
2400                }
2401                _ => None,
2402            };
2403            InterpreterResult { lines: result.output, error }
2404        }
2405        Err(advice) => InterpreterResult { lines: vec![], error: Some(advice) },
2406    })
2407}
2408
2409/// Run a program on the synchronous tree-walker and return its global
2410/// bindings as sorted `(name, type, value)` rows — the REPL's `:vars`
2411/// inspection. Returns `None` for programs the sync walker can't host
2412/// (async/concurrent) or that fail to parse or run.
2413pub fn repl_global_bindings(input: &str, program_args: &[String]) -> Option<Vec<(String, String, String)>> {
2414    with_parsed_program(input, |parsed, interner| match parsed {
2415        Ok((stmts, type_registry, policies)) => {
2416            if crate::interpreter::needs_async(stmts) || crate::concurrency::uses_scheduler(stmts) {
2417                return None;
2418            }
2419            let mut interp = crate::interpreter::Interpreter::new(interner)
2420                .with_type_registry(type_registry)
2421                .with_policies(policies)
2422                .with_program_args(program_args.to_vec());
2423            interp.run_sync(stmts).ok()?;
2424            Some(interp.global_bindings())
2425        }
2426        Err(_) => None,
2427    })
2428}
2429
2430pub(crate) fn run_treewalker<'a>(
2431    stmts: &'a [logicaffeine_language::ast::stmt::Stmt<'a>],
2432    type_registry: &logicaffeine_language::analysis::TypeRegistry,
2433    policies: logicaffeine_language::analysis::PolicyRegistry,
2434    interner: &'a Interner,
2435    force_async: bool,
2436    program_args: &[String],
2437) -> InterpreterResult {
2438    if crate::concurrency::uses_scheduler(stmts) {
2439        return run_program_concurrent(
2440            stmts, type_registry, policies, interner, program_args, None, None, 0,
2441        );
2442    }
2443    let mut interp = crate::interpreter::Interpreter::new(interner)
2444        .with_type_registry(type_registry)
2445        .with_policies(policies)
2446        .with_program_args(program_args.to_vec());
2447    let run_result = if force_async {
2448        futures::executor::block_on(interp.run(stmts))
2449    } else {
2450        interp.run_sync(stmts)
2451    };
2452    match run_result {
2453        Ok(()) => InterpreterResult { lines: interp.output, error: None },
2454        Err(e) => InterpreterResult { lines: interp.output, error: Some(e) },
2455    }
2456}
2457
2458/// Interpret LOGOS imperative code with streaming output.
2459///
2460/// The `on_output` callback is called each time `Show` executes, allowing
2461/// real-time output display like a REPL. The callback receives the output line.
2462///
2463/// # Example
2464/// ```no_run
2465/// use std::rc::Rc;
2466/// use std::cell::RefCell;
2467/// # use logicaffeine_compile::interpret_streaming;
2468///
2469/// # fn main() {}
2470/// # async fn example() {
2471/// # let source = "## Main\nShow \"hello\".";
2472/// let lines = Rc::new(RefCell::new(Vec::new()));
2473/// let lines_clone = lines.clone();
2474///
2475/// interpret_streaming(source, Rc::new(RefCell::new(move |line: String| {
2476///     lines_clone.borrow_mut().push(line);
2477/// }))).await;
2478/// # }
2479/// ```
2480pub async fn interpret_streaming<F>(input: &str, on_output: std::rc::Rc<std::cell::RefCell<F>>) -> InterpreterResult
2481where
2482    F: FnMut(String) + 'static,
2483{
2484    interpret_streaming_with_vfs(input, on_output, None).await
2485}
2486
2487/// Like [`interpret_streaming`], but routes the interpreter's file I/O
2488/// (`Write`/`Read`/`Mount`) to `vfs`. The browser Studio passes its `WebVfs`
2489/// here so the standard-library I/O vocabulary works against OPFS/IndexedDB; a
2490/// `None` vfs leaves file I/O reporting "VFS not initialized" as before. The
2491/// clone-per-task model means concurrent tasks inherit the same VFS handle.
2492pub async fn interpret_streaming_with_vfs<F>(
2493    input: &str,
2494    on_output: std::rc::Rc<std::cell::RefCell<F>>,
2495    vfs: Option<std::sync::Arc<dyn logicaffeine_system::fs::Vfs>>,
2496) -> InterpreterResult
2497where
2498    F: FnMut(String) + 'static,
2499{
2500    interpret_streaming_impl(input, on_output, vfs, None).await
2501}
2502
2503/// Like [`interpret_streaming_with_vfs`], but also emits a [`logicaffeine_runtime::SchedSnapshot`]
2504/// to `observer` after each scheduler slice — the Studio's Tasks/Channels strip subscribes
2505/// here to show a concurrent program's live task and channel state as it runs.
2506pub async fn interpret_streaming_with_vfs_observer<F>(
2507    input: &str,
2508    on_output: std::rc::Rc<std::cell::RefCell<F>>,
2509    vfs: Option<std::sync::Arc<dyn logicaffeine_system::fs::Vfs>>,
2510    observer: ObserverCallback,
2511) -> InterpreterResult
2512where
2513    F: FnMut(String) + 'static,
2514{
2515    interpret_streaming_impl(input, on_output, vfs, Some(observer)).await
2516}
2517
2518async fn interpret_streaming_impl<F>(
2519    input: &str,
2520    on_output: std::rc::Rc<std::cell::RefCell<F>>,
2521    vfs: Option<std::sync::Arc<dyn logicaffeine_system::fs::Vfs>>,
2522    observer: Option<ObserverCallback>,
2523) -> InterpreterResult
2524where
2525    F: FnMut(String) + 'static,
2526{
2527    use logicaffeine_language::ast::stmt::{Stmt, Expr, TypeExpr};
2528    use crate::interpreter::OutputCallback;
2529
2530    let mut interner = Interner::new();
2531    let mut lexer = Lexer::new(input, &mut interner);
2532    let tokens = lexer.tokenize();
2533
2534    let mwe_trie = mwe::build_mwe_trie();
2535    let tokens = mwe::apply_mwe_pipeline(tokens, &mwe_trie, &mut interner);
2536
2537    let (type_registry, policy_registry) = {
2538        let mut discovery = DiscoveryPass::new(&tokens, &mut interner);
2539        let result = discovery.run_full();
2540        (result.types, result.policies)
2541    };
2542
2543    let expr_arena = Arena::new();
2544    let term_arena = Arena::new();
2545    let np_arena = Arena::new();
2546    let sym_arena = Arena::new();
2547    let role_arena = Arena::new();
2548    let pp_arena = Arena::new();
2549    let stmt_arena: Arena<Stmt> = Arena::new();
2550    let imperative_expr_arena: Arena<Expr> = Arena::new();
2551    let type_expr_arena: Arena<TypeExpr> = Arena::new();
2552
2553    let ctx = AstContext::with_types(
2554        &expr_arena,
2555        &term_arena,
2556        &np_arena,
2557        &sym_arena,
2558        &role_arena,
2559        &pp_arena,
2560        &stmt_arena,
2561        &imperative_expr_arena,
2562        &type_expr_arena,
2563    );
2564
2565    let mut world_state = drs::WorldState::new();
2566    let type_registry_for_interp = type_registry.clone();
2567    let mut parser = Parser::new(tokens, &mut world_state, &mut interner, ctx, type_registry);
2568
2569    match parser.parse_program() {
2570        Ok(stmts) => {
2571            if let Some(rejection) = send_escape_rejection(&stmts) {
2572                return rejection;
2573            }
2574            // Create the callback wrapper that calls the user's callback
2575            let callback: OutputCallback = std::rc::Rc::new(std::cell::RefCell::new(move |line: String| {
2576                (on_output.borrow_mut())(line);
2577            }));
2578
2579            // Concurrent programs run on the deterministic scheduler — the browser
2580            // concurrency path — driven in slices that yield a macrotask between them so
2581            // the UI repaints, streaming each line as it is produced and emitting a
2582            // snapshot to the observer (if any). (Without this, a concurrency op would
2583            // `yield_request` with no scheduler installed and panic.)
2584            if crate::concurrency::uses_scheduler(&stmts) {
2585                return run_program_concurrent_streaming(
2586                    &stmts,
2587                    &type_registry_for_interp,
2588                    policy_registry,
2589                    &interner,
2590                    &[],
2591                    vfs,
2592                    Some(callback),
2593                    observer,
2594                    0,
2595                )
2596                .await;
2597            }
2598
2599            let mut interp = crate::interpreter::Interpreter::new(&interner)
2600                .with_type_registry(&type_registry_for_interp)
2601                .with_policies(policy_registry)
2602                .with_output_callback(callback);
2603            if let Some(v) = vfs {
2604                interp = interp.with_vfs(v);
2605            }
2606
2607            match interp.run(&stmts).await {
2608                Ok(()) => InterpreterResult {
2609                    lines: interp.output,
2610                    error: None,
2611                },
2612                Err(e) => InterpreterResult {
2613                    lines: interp.output,
2614                    error: Some(e),
2615                },
2616            }
2617        }
2618        Err(e) => {
2619            let advice = socratic_explanation(&e, &interner);
2620            InterpreterResult {
2621                lines: vec![],
2622                error: Some(advice),
2623            }
2624        }
2625    }
2626}
2627
2628// ═══════════════════════════════════════════════════════════════════
2629// Theorem Verification (Kernel-certified)
2630// ═══════════════════════════════════════════════════════════════════
2631
2632use logicaffeine_language::ast::Stmt;
2633use logicaffeine_language::proof_convert::logic_expr_to_proof_expr;
2634use crate::kernel;
2635
2636/// Phase 78: Verify a theorem with full kernel certification.
2637///
2638/// Pipeline:
2639/// 1. Parse theorem block
2640/// 2. Extract symbols and build kernel context
2641/// 3. Run proof engine
2642/// 4. Certify derivation tree to kernel term
2643/// 5. Type-check the term
2644/// 6. Return (proof_term, context)
2645pub fn verify_theorem(input: &str) -> Result<(kernel::Term, kernel::Context), ParseError> {
2646    let (proof_exprs, goal_expr, definitions) = theorem_proof_exprs_with_defs(input)?;
2647
2648    // === STEPS 3-6: Prove → certify → type-check (the one canonical pipeline) ===
2649    let outcome = logicaffeine_proof::verify::prove_certify_check_with_defs(
2650        &proof_exprs,
2651        &goal_expr,
2652        &definitions,
2653    );
2654    if outcome.verified {
2655        Ok((
2656            outcome
2657                .proof_term
2658                .expect("a verified outcome always carries a proof term"),
2659            outcome.kernel_ctx,
2660        ))
2661    } else {
2662        Err(ParseError {
2663            kind: logicaffeine_language::error::ParseErrorKind::Custom(
2664                outcome
2665                    .verification_error
2666                    .unwrap_or_else(|| "Theorem verification failed".to_string()),
2667            ),
2668            span: logicaffeine_language::token::Span::default(),
2669        })
2670    }
2671}
2672
2673/// An English theorem proved, with its FOL and a human-readable PROOF TRACE — the
2674/// "English in → proof out, see the trace" path. Parsing yields the premises and
2675/// goal as FOL; the kernel-certified backward chainer yields the derivation, which
2676/// renders as an indented step-by-step proof.
2677#[derive(Debug, Clone)]
2678pub struct TheoremTrace {
2679    /// Whether the goal was proved (kernel-certified).
2680    pub verified: bool,
2681    /// The premises, rendered as FOL — what the English compiled to.
2682    pub premises: Vec<String>,
2683    /// The goal, rendered as FOL.
2684    pub goal: String,
2685    /// The derivation rendered as an indented proof trace (`└─ [Rule] conclusion`
2686    /// per step). `None` when no derivation was found.
2687    pub trace: Option<String>,
2688    /// The verification error, when proving failed.
2689    pub error: Option<String>,
2690}
2691
2692/// Prove an English `## Theorem` block and return its FOL plus a rendered PROOF
2693/// TRACE. Unlike [`verify_theorem`] (which returns the raw kernel term), this
2694/// surfaces the derivation tree so the proof steps are visible.
2695pub fn prove_theorem_trace(input: &str) -> Result<TheoremTrace, ParseError> {
2696    let (proof_exprs, goal_expr, definitions) = theorem_proof_exprs_with_defs(input)?;
2697    let outcome = logicaffeine_proof::verify::prove_certify_check_with_defs(
2698        &proof_exprs,
2699        &goal_expr,
2700        &definitions,
2701    );
2702    Ok(TheoremTrace {
2703        verified: outcome.verified,
2704        premises: proof_exprs.iter().map(|p| p.to_string()).collect(),
2705        goal: goal_expr.to_string(),
2706        trace: outcome.derivation.as_ref().map(|d| d.display_tree()),
2707        error: outcome.verification_error,
2708    })
2709}
2710
2711// ═══════════════════════════════════════════════════════════════════
2712// Proof / Math → Rust extraction (the Curry-Howard "Forge", UI-facing)
2713// ═══════════════════════════════════════════════════════════════════
2714
2715/// Names the user defined — present in `ctx` but absent from a fresh kernel
2716/// context (i.e. excluding the StandardLibrary baseline). Inductives are listed
2717/// before definitions so the extracted module reads top-down; StandardLibrary
2718/// items are pulled in by [`extract_math_rust`] only when transitively needed.
2719fn user_defined_entries(ctx: &kernel::Context) -> Vec<String> {
2720    let baseline = kernel::interface::Repl::new();
2721    let base = baseline.context();
2722    let mut base_names: HashSet<String> = HashSet::new();
2723    for (name, _) in base.iter_inductives() {
2724        base_names.insert(name.to_string());
2725    }
2726    for (name, _, _) in base.iter_definitions() {
2727        base_names.insert(name.to_string());
2728    }
2729
2730    let mut entries = Vec::new();
2731    for (name, _) in ctx.iter_inductives() {
2732        // A user inductive extracts as an enum only if every constructor field is
2733        // a concrete Rust type. Dependent/indexed families (e.g. `Eq<Nat,n,…>`,
2734        // value-indexed proof types) and opaque/primitive types are skipped.
2735        if !base_names.contains(name) && inductive_is_emittable(ctx, name) {
2736            entries.push(name.to_string());
2737        }
2738    }
2739    for (name, ty, _) in ctx.iter_definitions() {
2740        // Only data-typed definitions whose body references nothing inextractable
2741        // (no Prop proofs, no axioms/tactics like `syn_diag`/`try_auto`) extract to
2742        // runnable Rust — so we never emit Rust that references undefined symbols.
2743        if !base_names.contains(name)
2744            && type_is_emittable(ctx, ty, &[])
2745            && crate::extraction::is_extractable(ctx, name)
2746        {
2747            entries.push(name.to_string());
2748        }
2749    }
2750    // Deterministic order (iter_* walk HashMaps with per-instance random seeds).
2751    entries.sort();
2752    entries.dedup();
2753    entries
2754}
2755
2756/// Whether a kernel type extracts to a concrete Rust type: a mapped primitive, a
2757/// user inductive with emittable constructors (→ enum), a declared type parameter
2758/// (`generics`), or a function/application built from those. `Sort` (Prop/Type),
2759/// logical/opaque types, and undeclared type variables are not.
2760fn type_is_emittable(ctx: &kernel::Context, ty: &kernel::Term, generics: &[String]) -> bool {
2761    use kernel::Term;
2762    match ty {
2763        Term::Global(name) => {
2764            crate::extraction::primitive_rust_type(name).is_some()
2765                || (ctx.is_inductive(name)
2766                    && !ctx.get_constructors(name).is_empty()
2767                    && !crate::extraction::is_logical_type(name))
2768        }
2769        // A type variable is only a real Rust type if it's a declared generic of
2770        // the enclosing inductive — never in a bare definition type.
2771        Term::Var(v) => generics.iter().any(|g| g == v),
2772        Term::Pi { param_type, body_type, .. } => {
2773            type_is_emittable(ctx, param_type, generics) && type_is_emittable(ctx, body_type, generics)
2774        }
2775        Term::App(f, a) => {
2776            type_is_emittable(ctx, f, generics) && type_is_emittable(ctx, a, generics)
2777        }
2778        _ => false,
2779    }
2780}
2781
2782/// The leading `: Type` parameters of an inductive (its Rust generics).
2783fn inductive_generics(ctx: &kernel::Context, ind: &str) -> Vec<String> {
2784    use kernel::Term;
2785    let mut names = Vec::new();
2786    let mut cur = match ctx.get_global(ind) {
2787        Some(t) => t,
2788        None => return names,
2789    };
2790    while let Term::Pi { param, param_type, body_type } = cur {
2791        if matches!(param_type.as_ref(), Term::Sort(_)) {
2792            names.push(param.clone());
2793            cur = body_type;
2794        } else {
2795            break;
2796        }
2797    }
2798    names
2799}
2800
2801/// A user inductive is emittable iff it has constructors and every constructor
2802/// *field* (after the leading type parameters) is itself an emittable type.
2803fn inductive_is_emittable(ctx: &kernel::Context, ind: &str) -> bool {
2804    use kernel::Term;
2805    let ctors = ctx.get_constructors(ind);
2806    if ctors.is_empty() || crate::extraction::is_logical_type(ind) {
2807        return false;
2808    }
2809    let generics = inductive_generics(ctx, ind);
2810    for (_, ty) in &ctors {
2811        let mut cur = *ty;
2812        for _ in 0..generics.len() {
2813            if let Term::Pi { body_type, .. } = cur {
2814                cur = body_type;
2815            } else {
2816                break;
2817            }
2818        }
2819        while let Term::Pi { param_type, body_type, .. } = cur {
2820            if !type_is_emittable(ctx, param_type, &generics) {
2821                return false;
2822            }
2823            cur = body_type;
2824        }
2825    }
2826    true
2827}
2828
2829/// Extract every user-defined inductive and definition in `ctx` into one Rust
2830/// module — the "compile my math to Rust" path for the Math studio. Shared and
2831/// StandardLibrary dependencies are pulled in only when a user definition
2832/// transitively needs them, and are emitted exactly once.
2833pub fn extract_math_rust(ctx: &kernel::Context) -> Result<String, String> {
2834    let entries = user_defined_entries(ctx);
2835    if entries.is_empty() {
2836        return Ok("// nothing defined yet — add a Definition or Inductive".to_string());
2837    }
2838    let module = extract_math_module(ctx)?;
2839    let checks: Vec<(String, Vec<kernel::Term>)> =
2840        property_checks(ctx).into_iter().map(|(n, _, p)| (n, p)).collect();
2841    // The "compiled mathematical object" runs and proves itself.
2842    Ok(format!("{module}{}", math_demo_main(ctx, &entries, &checks)))
2843}
2844
2845/// Extract every user-defined inductive/definition in `ctx` into one Rust module —
2846/// types, functions, and `check_*` property fns from proven theorems — WITHOUT a
2847/// demo `main`. This is the linkable artifact bundled into an imperative program's
2848/// `mod proven`; [`extract_math_rust`] wraps it with a self-verifying `main` for the
2849/// standalone Math compile.
2850pub fn extract_math_module(ctx: &kernel::Context) -> Result<String, String> {
2851    let entries = user_defined_entries(ctx);
2852    if entries.is_empty() {
2853        return Ok("// nothing defined yet — add a Definition or Inductive".to_string());
2854    }
2855    let refs: Vec<&str> = entries.iter().map(|s| s.as_str()).collect();
2856    let mut module = crate::extraction::extract_programs(ctx, &refs).map_err(|e| e.to_string())?;
2857    // Proven theorems → runnable property checks over the extracted functions
2858    // (e.g. `∀n. add Zero n = n` → `fn check_…(n) -> bool { add(Zero, n) == n }`).
2859    // Appended in sorted order so the module is byte-identical across recompiles.
2860    let mut check_names: std::collections::HashSet<String> = std::collections::HashSet::new();
2861    for (name, check_fn, _) in property_checks(ctx) {
2862        module.push_str(&check_fn);
2863        check_names.insert(name);
2864    }
2865    // Honest notes for proof-irrelevant theorems (Gödel, consistency, `True`, …): a
2866    // definition whose TYPE is a proposition is a *proof*, and by Curry-Howard a proof
2867    // of a Prop has no computational content — so it has no runnable form. The
2868    // constructive dependencies it uses are still extracted above; the note explains
2869    // why the theorem itself isn't a function. Sorted for deterministic output.
2870    let base = baseline_names();
2871    let mut notes: Vec<String> = Vec::new();
2872    for (name, ty, _) in ctx.iter_definitions() {
2873        if base.contains(name) || check_names.contains(name) {
2874            continue;
2875        }
2876        if def_type_is_proposition(ty) {
2877            notes.push(format!(
2878                "// note: `{name}` is a proof of a proposition — proof-irrelevant (no \
2879                 computational content), so it has no runnable form; any constructive \
2880                 definitions it relies on are extracted above.\n"
2881            ));
2882        }
2883    }
2884    notes.sort();
2885    for n in notes {
2886        module.push_str(&n);
2887    }
2888    Ok(module)
2889}
2890
2891/// Whether a definition's type is a proposition — i.e. the definition is a *proof*
2892/// (proof-irrelevant), not computational data. Peels any `∀` (Pi) binders, then
2893/// checks whether the head of the result is a logical/proof type (`Eq`/`And`/`True`/
2894/// `Syntax`/`Derivation`/…). Used to emit honest notes for theorems with no runnable form.
2895fn def_type_is_proposition(ty: &kernel::Term) -> bool {
2896    use kernel::Term;
2897    let mut cur = ty;
2898    while let Term::Pi { body_type, .. } = cur {
2899        cur = body_type;
2900    }
2901    let mut head = cur;
2902    while let Term::App(f, _) = head {
2903        head = f;
2904    }
2905    matches!(head, Term::Global(n) if crate::extraction::is_logical_type(n))
2906}
2907
2908/// Proven theorems → runnable property checks: `(name, check_fn_source, param_types)`,
2909/// sorted by name for deterministic emission. A theorem yields a check iff
2910/// [`crate::extraction::emit_property_check`] produces a runnable predicate over the
2911/// extracted functions (it quantifies only over data types).
2912fn property_checks(ctx: &kernel::Context) -> Vec<(String, String, Vec<kernel::Term>)> {
2913    let base = baseline_names();
2914    let mut checks: Vec<(String, String, Vec<kernel::Term>)> = Vec::new();
2915    for (name, ty, _) in ctx.iter_definitions() {
2916        if base.contains(name) {
2917            continue;
2918        }
2919        if let Some(check_fn) = crate::extraction::emit_property_check(ctx, name, ty) {
2920            checks.push((name.to_string(), check_fn, pi_param_types(ty)));
2921        }
2922    }
2923    checks.sort_by(|a, b| a.0.cmp(&b.0));
2924    checks
2925}
2926
2927/// Names present in a fresh `Repl::new()` (the StandardLibrary baseline).
2928fn baseline_names() -> std::collections::HashSet<String> {
2929    let baseline = kernel::interface::Repl::new();
2930    let base = baseline.context();
2931    let mut names = std::collections::HashSet::new();
2932    for (n, _) in base.iter_inductives() {
2933        names.insert(n.to_string());
2934    }
2935    for (n, _, _) in base.iter_definitions() {
2936        names.insert(n.to_string());
2937    }
2938    names
2939}
2940
2941/// Whether a term applies the `div`/`mod` arithmetic builtins (whose extracted Rust
2942/// `/`/`%` panics on a zero divisor) — used to skip them in the self-verifying demo.
2943fn term_uses_div_or_mod(term: &kernel::Term) -> bool {
2944    use kernel::Term;
2945    match term {
2946        Term::Global(n) => n == "div" || n == "mod",
2947        Term::App(f, a) => term_uses_div_or_mod(f) || term_uses_div_or_mod(a),
2948        Term::Lambda { param_type, body, .. } => {
2949            term_uses_div_or_mod(param_type) || term_uses_div_or_mod(body)
2950        }
2951        Term::Pi { param_type, body_type, .. } => {
2952            term_uses_div_or_mod(param_type) || term_uses_div_or_mod(body_type)
2953        }
2954        Term::Fix { body, .. } => term_uses_div_or_mod(body),
2955        Term::Match { discriminant, motive, cases } => {
2956            term_uses_div_or_mod(discriminant)
2957                || term_uses_div_or_mod(motive)
2958                || cases.iter().any(term_uses_div_or_mod)
2959        }
2960        _ => false,
2961    }
2962}
2963
2964/// A self-verifying demo `main`: each value/function result is `assert_eq!`d
2965/// against `kernel::normalize` (the kernel's evaluator) and printed; each proven
2966/// theorem's property check is run on a sample and asserted.
2967fn math_demo_main(
2968    ctx: &kernel::Context,
2969    entries: &[String],
2970    checks: &[(String, Vec<kernel::Term>)],
2971) -> String {
2972    use kernel::Term;
2973    let mut lines = Vec::new();
2974    for name in entries {
2975        let Some(body) = ctx.get_definition_body(name) else {
2976            continue; // an inductive, not a definition
2977        };
2978        // Skip exercising a def that uses `div`/`mod`: the demo samples Int as 0, and
2979        // `n / n` / `n % n` at n=0 is a compile-time divide-by-zero panic (and the
2980        // kernel leaves div-by-0 stuck, diverging from Rust's panic). The function is
2981        // still EXTRACTED — just not run in the self-verifying demo.
2982        if term_uses_div_or_mod(body) {
2983            continue;
2984        }
2985        if matches!(body, Term::Lambda { .. } | Term::Fix { .. }) {
2986            // A function: apply it to kernel-built sample arguments.
2987            let Some(ty) = ctx.get_definition_type(name) else { continue };
2988            let params = pi_param_types(ty);
2989            let samples: Option<Vec<Term>> =
2990                params.iter().map(|p| sample_value(ctx, p)).collect();
2991            let Some(samples) = samples else { continue }; // can't sample → skip
2992            if samples.is_empty() {
2993                continue;
2994            }
2995            let args_rust: Vec<String> =
2996                samples.iter().map(|s| crate::extraction::emit_value(ctx, s)).collect();
2997            let mut app = Term::Global(name.clone());
2998            for s in &samples {
2999                app = Term::App(Box::new(app), Box::new(s.clone()));
3000            }
3001            let expected = crate::extraction::emit_value(ctx, &kernel::normalize(ctx, &app));
3002            let call = format!("{}({})", name, args_rust.join(", "));
3003            lines.push(format!("    assert_eq!({call}, {expected});"));
3004            lines.push(format!("    println!(\"{name}(..) = {{:?}}\", {call});"));
3005        } else {
3006            // A value: evaluate and self-verify.
3007            let expected = crate::extraction::emit_value(ctx, &kernel::normalize(ctx, body));
3008            lines.push(format!("    assert_eq!({name}(), {expected});"));
3009            lines.push(format!("    println!(\"{name} = {{:?}}\", {name}());"));
3010        }
3011    }
3012    // Run each proven theorem's property check on a sample and assert it holds.
3013    for (name, param_types) in checks {
3014        let samples: Option<Vec<kernel::Term>> =
3015            param_types.iter().map(|p| sample_value(ctx, p)).collect();
3016        let Some(samples) = samples else { continue };
3017        let args: Vec<String> =
3018            samples.iter().map(|s| crate::extraction::emit_value(ctx, s)).collect();
3019        let call = format!("check_{}({})", name, args.join(", "));
3020        lines.push(format!("    assert!({call}, \"theorem {name} failed on sample\");"));
3021        lines.push(format!("    println!(\"\\u{{2713}} {name} holds (checked on a sample)\");"));
3022    }
3023    if lines.is_empty() {
3024        return "\nfn main() {}\n".to_string();
3025    }
3026    format!("\nfn main() {{\n{}\n}}\n", lines.join("\n"))
3027}
3028
3029/// The parameter types of a (possibly curried) function type `A -> B -> C`.
3030fn pi_param_types(ty: &kernel::Term) -> Vec<kernel::Term> {
3031    use kernel::Term;
3032    let mut params = Vec::new();
3033    let mut cur = ty;
3034    while let Term::Pi { param_type, body_type, .. } = cur {
3035        params.push((**param_type).clone());
3036        cur = body_type;
3037    }
3038    params
3039}
3040
3041/// A small sample value of a type, for the demo (a primitive zero, or the first
3042/// nullary constructor of an inductive). `None` if we can't build one cheaply.
3043fn sample_value(ctx: &kernel::Context, ty: &kernel::Term) -> Option<kernel::Term> {
3044    use kernel::{Literal, Term};
3045    match ty {
3046        Term::Global(name) => match name.as_str() {
3047            "Int" => Some(Term::Lit(Literal::Int(0))),
3048            "Float" => Some(Term::Lit(Literal::Float(0.0))),
3049            "Text" => Some(Term::Lit(Literal::Text(String::new()))),
3050            // First nullary constructor of a user inductive (Zero, Yes, MNil, …).
3051            _ if ctx.is_inductive(name) => ctx
3052                .get_constructors(name)
3053                .into_iter()
3054                .find(|(_, cty)| !matches!(cty, Term::Pi { .. }))
3055                .map(|(cname, _)| Term::Global(cname.to_string())),
3056            _ => None,
3057        },
3058        _ => None,
3059    }
3060}
3061
3062/// Compile a Math-mode SOURCE program (the editor text) to a Rust module: split
3063/// it into vernacular statements, run them into a fresh kernel, then extract every
3064/// user-defined inductive/definition. This is the exact pipeline the Studio's
3065/// Math "🦀 Compile" button drives, exposed as one function so it is testable.
3066pub fn extract_math_rust_from_source(input: &str) -> String {
3067    let mut repl = kernel::interface::Repl::new();
3068    // Batch execution groups mutually-recursive inductives declared as separate
3069    // statements into one mutual block (see `Repl::execute_batch`); errors on individual
3070    // statements (e.g. `Check`/`Eval`) are ignored — only the resulting
3071    // definitions/inductives matter for extraction.
3072    let stmts = parse_math_statements(input);
3073    let _ = repl.execute_batch(&stmts);
3074    match extract_math_rust(repl.context()) {
3075        Ok(rust) => rust,
3076        Err(e) => format!("// extraction error: {e}"),
3077    }
3078}
3079
3080/// Like [`extract_math_rust_from_source`], but produces the main-less module (the
3081/// linkable artifact bundled into an imperative program's `mod proven`).
3082pub fn extract_math_module_from_source(input: &str) -> String {
3083    let mut repl = kernel::interface::Repl::new();
3084    // Batch execution groups mutually-recursive inductives (`Tree`↔`Forest`) declared as
3085    // separate statements into one mutual block, so a forward reference between them
3086    // registers instead of failing its universe check. A source with no forward
3087    // references behaves exactly as the per-statement loop did.
3088    let stmts = parse_math_statements(input);
3089    let _ = repl.execute_batch(&stmts);
3090    match extract_math_module(repl.context()) {
3091        Ok(rust) => rust,
3092        Err(e) => format!("// extraction error: {e}"),
3093    }
3094}
3095
3096/// Partition a possibly-MIXED document into its imperative and math streams.
3097///
3098/// A mixed document interleaves imperative LOGOS (`## To`/`## Main`/statements) with
3099/// Coq-style math (`Definition`/`Inductive`/`Axiom`/`Theorem`/`Lemma`/`Fixpoint`) and
3100/// literate `## Theorem:`/`## Lemma:` blocks. The math blocks feed the Forge (extracted
3101/// into `mod proven`); the imperative blocks compile normally and call into it.
3102///
3103/// Returns `(imperative_src, Some(math_src))` when math is present, or
3104/// `(source.to_string(), None)` for a pure imperative program — so that path is a
3105/// guaranteed no-op (byte-identical compile). In `imperative_src` the math lines are
3106/// BLANKED (kept as empty lines), not deleted, so imperative line numbers — and thus
3107/// error spans — stay aligned with the original source.
3108///
3109/// `## To`/`## A X is one of` stay imperative (only the Coq keywords and `## Theorem:`/
3110/// `## Lemma:` route to math), so this never steals an imperative function or enum.
3111/// IMPLICIT MAIN: a source with no `##` headers whose first non-blank line
3112/// reads as an imperative statement runs as its own `## Main` body — a bare
3113/// script no longer parses as logic prose and runs to empty output.
3114/// Natural-language documents (no statement-keyword opening) are untouched.
3115/// Returns the wrapped source, or `None` when the input is not a bare script.
3116pub fn implicit_main(source: &str) -> Option<String> {
3117    if source.lines().any(|l| l.trim_start().starts_with("##")) {
3118        return None;
3119    }
3120    let first = source.lines().find(|l| !l.trim().is_empty())?;
3121    const STATEMENT_HEADS: &[&str] = &[
3122        "Let ", "Show ", "Set ", "If ", "While ", "Repeat ", "Push ",
3123        "Pop ", "Add ", "Remove ", "Call ", "Return ", "Increase ",
3124        "Decrease ", "Break", "Inspect ", "Assert ",
3125    ];
3126    let t = first.trim_start();
3127    if STATEMENT_HEADS.iter().any(|h| t.starts_with(h)) {
3128        Some(format!("## Main\n{}", source))
3129    } else {
3130        None
3131    }
3132}
3133
3134pub fn partition_mixed(source: &str) -> (String, Option<String>) {
3135    if let Some(wrapped) = implicit_main(source) {
3136        return (wrapped, None);
3137    }
3138    let lines: Vec<&str> = source.lines().collect();
3139    let mut is_math = vec![false; lines.len()];
3140    let mut i = 0;
3141    let mut any = false;
3142    while i < lines.len() {
3143        let t = lines[i].trim();
3144        if !is_math_block_start(t) {
3145            i += 1;
3146            continue;
3147        }
3148        any = true;
3149        if t.starts_with("## Theorem:") || t.starts_with("## Lemma:") {
3150            // Literate theorem: header + indented / `Statement:` / `Proof:` lines.
3151            is_math[i] = true;
3152            i += 1;
3153            while i < lines.len() {
3154                let nt = lines[i].trim();
3155                let indented = lines[i].starts_with(' ') || lines[i].starts_with('\t');
3156                if nt.is_empty() {
3157                    is_math[i] = true;
3158                    i += 1;
3159                    continue;
3160                }
3161                if indented || nt.starts_with("Statement:") || nt.starts_with("Proof:") {
3162                    is_math[i] = true;
3163                    i += 1;
3164                    if nt.starts_with("Proof:") && nt.ends_with('.') {
3165                        break;
3166                    }
3167                } else {
3168                    break;
3169                }
3170            }
3171        } else {
3172            // Coq-style statement: accumulate until a line ending with `.`. Defensive:
3173            // a `## ` block header (e.g. `## Main`, `## To`) ends the math block even
3174            // if the terminating `.` is missing/unterminated, so a malformed Definition
3175            // never swallows the imperative code that follows it.
3176            let mut ended = t.ends_with('.');
3177            is_math[i] = true;
3178            i += 1;
3179            while !ended && i < lines.len() {
3180                if lines[i].trim_start().starts_with("## ") {
3181                    break;
3182                }
3183                is_math[i] = true;
3184                ended = lines[i].trim().ends_with('.');
3185                i += 1;
3186            }
3187        }
3188    }
3189    if !any {
3190        return (source.to_string(), None);
3191    }
3192    let imp: Vec<&str> = lines
3193        .iter()
3194        .enumerate()
3195        .map(|(j, l)| if is_math[j] { "" } else { *l })
3196        .collect();
3197    let math: Vec<&str> = lines
3198        .iter()
3199        .enumerate()
3200        .filter(|(j, _)| is_math[*j])
3201        .map(|(_, l)| *l)
3202        .collect();
3203    (imp.join("\n"), Some(math.join("\n")))
3204}
3205
3206fn is_math_block_start(trimmed: &str) -> bool {
3207    const COQ: [&str; 6] = [
3208        "Definition ", "Inductive ", "Axiom ", "Theorem ", "Lemma ", "Fixpoint ",
3209    ];
3210    COQ.iter().any(|k| trimmed.starts_with(k))
3211        || trimmed.starts_with("## Theorem:")
3212        || trimmed.starts_with("## Lemma:")
3213}
3214
3215/// Extract a mixed document's math stream into a bundleable `mod proven` body, or
3216/// `None` if it has no public items to call into (e.g. only vacuous proofs / parse
3217/// errors) — so we never emit a `use proven::*;` over an empty module.
3218///
3219/// Unlike standalone math extraction, this ALSO emits a `Showable` impl for each
3220/// non-generic proven enum, so imperative `Show <proven value>` works. `Showable`
3221/// lives in `logicaffeine_system` (in scope inside `mod proven` via `use super::*;`),
3222/// which is ONLY linked when bundled into an imperative program — hence it is added
3223/// here, not in `extract_math_module` (whose output must also compile standalone, dep-free).
3224pub(crate) fn mixed_proven_module(math_src: &str) -> Option<String> {
3225    let mut repl = kernel::interface::Repl::new();
3226    // Batch execution groups mutually-recursive inductives (see `Repl::execute_batch`).
3227    let stmts = parse_math_statements(math_src);
3228    let _ = repl.execute_batch(&stmts);
3229    let ctx = repl.context();
3230    let mut rust = match extract_math_module(ctx) {
3231        Ok(r) => r,
3232        Err(_) => return None,
3233    };
3234    if !(rust.contains("pub fn") || rust.contains("pub enum") || rust.contains("pub struct")) {
3235        return None;
3236    }
3237    // Show-verb integration: bridge each non-generic proven enum to the runtime
3238    // `Showable` trait (via Debug). Sorted for deterministic output.
3239    let base = baseline_names();
3240    let mut inds: Vec<String> = Vec::new();
3241    for (name, _) in ctx.iter_inductives() {
3242        if !base.contains(name)
3243            && inductive_is_emittable(ctx, name)
3244            && inductive_generics(ctx, name).is_empty()
3245        {
3246            inds.push(name.to_string());
3247        }
3248    }
3249    inds.sort();
3250    for ind in inds {
3251        rust.push_str(&format!(
3252            "impl Showable for {ind} {{ fn format_show(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {{ write!(f, \"{{:?}}\", self) }} }}\n"
3253        ));
3254    }
3255    Some(rust)
3256}
3257
3258/// Split a Math-mode program into complete vernacular statements.
3259///
3260/// Handles both Coq-style (period-terminated) and the Literate forms
3261/// (`## To …` functions, `## Theorem:` blocks, `A X is either …` inductives).
3262pub fn parse_math_statements(code: &str) -> Vec<String> {
3263    let mut statements = Vec::new();
3264    let lines: Vec<&str> = code.lines().collect();
3265    let mut i = 0;
3266
3267    while i < lines.len() {
3268        let line = lines[i];
3269        let trimmed = line.trim();
3270
3271        // Skip empty lines and comments
3272        if trimmed.is_empty() || trimmed.starts_with("--") {
3273            i += 1;
3274            continue;
3275        }
3276
3277        // Literate function definition: "## To ..."
3278        if trimmed.starts_with("## To ") {
3279            let mut block = String::new();
3280            block.push_str(trimmed);
3281            i += 1;
3282
3283            while i < lines.len() {
3284                let next_line = lines[i];
3285                let next_trimmed = next_line.trim();
3286
3287                if next_trimmed.is_empty() {
3288                    i += 1;
3289                    continue;
3290                }
3291                if next_trimmed.starts_with("--") {
3292                    i += 1;
3293                    continue;
3294                }
3295
3296                let is_indented = next_line.starts_with(' ') || next_line.starts_with('\t');
3297                let is_continuation = next_trimmed.starts_with("Consider ")
3298                    || next_trimmed.starts_with("When ")
3299                    || next_trimmed.starts_with("Yield ");
3300
3301                if is_indented || is_continuation {
3302                    block.push(' ');
3303                    block.push_str(next_trimmed);
3304                    i += 1;
3305                } else {
3306                    break;
3307                }
3308            }
3309
3310            statements.push(block);
3311            continue;
3312        }
3313
3314        // Literate theorem: "## Theorem: ..." (header + Statement: + Proof:)
3315        if trimmed.starts_with("## Theorem:") {
3316            let mut block = String::new();
3317            block.push_str(trimmed);
3318            i += 1;
3319
3320            while i < lines.len() {
3321                let next_line = lines[i];
3322                let next_trimmed = next_line.trim();
3323
3324                if next_trimmed.is_empty() {
3325                    i += 1;
3326                    continue;
3327                }
3328                if next_trimmed.starts_with("--") {
3329                    i += 1;
3330                    continue;
3331                }
3332
3333                let is_indented = next_line.starts_with(' ') || next_line.starts_with('\t');
3334                let is_theorem_part = next_trimmed.starts_with("Statement:")
3335                    || next_trimmed.starts_with("Proof:");
3336
3337                if is_indented || is_theorem_part {
3338                    block.push('\n');
3339                    block.push_str(next_line);
3340                    i += 1;
3341                    if next_trimmed.starts_with("Proof:") && next_trimmed.ends_with('.') {
3342                        break;
3343                    }
3344                } else {
3345                    break;
3346                }
3347            }
3348
3349            statements.push(block);
3350            continue;
3351        }
3352
3353        // Literate inductive: "A X is either..." / "An X is either..."
3354        if (trimmed.starts_with("A ") || trimmed.starts_with("An ")) && trimmed.contains(" is either")
3355        {
3356            if trimmed.ends_with('.') && !trimmed.trim_end_matches('.').ends_with(':') {
3357                statements.push(trimmed.to_string());
3358                i += 1;
3359                continue;
3360            }
3361
3362            let mut block = String::new();
3363            block.push_str(trimmed);
3364            i += 1;
3365
3366            while i < lines.len() {
3367                let next_line = lines[i];
3368                let next_trimmed = next_line.trim();
3369
3370                if next_trimmed.is_empty() {
3371                    i += 1;
3372                    continue;
3373                }
3374                if next_trimmed.starts_with("--") {
3375                    i += 1;
3376                    continue;
3377                }
3378
3379                let is_indented = next_line.starts_with(' ') || next_line.starts_with('\t');
3380                let looks_like_variant = next_trimmed.starts_with("a ")
3381                    || next_trimmed
3382                        .chars()
3383                        .next()
3384                        .map(|c| c.is_uppercase())
3385                        .unwrap_or(false);
3386
3387                if is_indented
3388                    || (looks_like_variant
3389                        && !next_trimmed.starts_with("A ")
3390                        && !next_trimmed.starts_with("An "))
3391                {
3392                    if !block.ends_with(':') {
3393                        block.push_str(" or ");
3394                    } else {
3395                        block.push(' ');
3396                    }
3397                    block.push_str(next_trimmed.trim_end_matches('.'));
3398                    i += 1;
3399                } else {
3400                    break;
3401                }
3402            }
3403
3404            if !block.ends_with('.') {
3405                block.push('.');
3406            }
3407            statements.push(block);
3408            continue;
3409        }
3410
3411        // Traditional Coq-style: accumulate until period
3412        let mut current_stmt = String::new();
3413        while i < lines.len() {
3414            let line = lines[i];
3415            let trimmed = line.trim();
3416
3417            if trimmed.is_empty() || trimmed.starts_with("--") {
3418                i += 1;
3419                continue;
3420            }
3421
3422            if !current_stmt.is_empty() {
3423                current_stmt.push(' ');
3424            }
3425            current_stmt.push_str(trimmed);
3426            i += 1;
3427
3428            if trimmed.ends_with('.') {
3429                break;
3430            }
3431        }
3432
3433        if !current_stmt.is_empty() {
3434            statements.push(current_stmt);
3435        }
3436    }
3437
3438    statements
3439}
3440
3441/// Compile a Logic-mode input to runnable Rust.
3442///
3443/// A `## Theorem:` block becomes a **model-checker** for `premises ⊨ goal`; a
3444/// plain sentence becomes a model-checker for that formula — a self-contained
3445/// `Model` + `holds(&Model) -> bool` + demo `main` you can compile and run
3446/// ([`crate::extraction::fol_model`]). The finite-domain *puzzle* (Simon grid) is
3447/// the exception: it would run the full solver synchronously and freeze the page,
3448/// so it bails with a note (use Execute to solve it).
3449pub fn extract_logic_rust(input: &str) -> Result<String, String> {
3450    extract_logic_impl(input, true)
3451}
3452
3453/// Like [`extract_logic_rust`], but emits NO demo `main` — the linkable form bundled
3454/// into an imperative program's `mod proven` (which has its own `main`). Produces the
3455/// `World` + `holds` (+ `Monitor`) library items only.
3456pub fn extract_logic_module(input: &str) -> Result<String, String> {
3457    extract_logic_impl(input, false)
3458}
3459
3460fn extract_logic_impl(input: &str, emit_main: bool) -> Result<String, String> {
3461    use crate::extraction::fol_model::{fol_to_model_checker, fol_to_model_checker_module};
3462    let emit = |premises: &[logicaffeine_proof::ProofExpr],
3463                goal: &logicaffeine_proof::ProofExpr,
3464                english: &str,
3465                fol: &str| {
3466        if emit_main {
3467            fol_to_model_checker(premises, goal, english, fol)
3468        } else {
3469            fol_to_model_checker_module(premises, goal, english, fol)
3470        }
3471    };
3472
3473    // A `## Theorem:` block: model-check `premises ⊨ goal` — unless it is a grid
3474    // puzzle (bail before the solver runs, see above).
3475    if let Ok((premises, goal)) = theorem_proof_exprs(input) {
3476        if looks_like_grid(&premises) {
3477            return Ok("// this is a finite-domain puzzle — run it with Execute. \
3478                Compiling it to Rust would run the full solver synchronously."
3479                .to_string());
3480        }
3481        let fol = if premises.is_empty() {
3482            goal.to_string()
3483        } else {
3484            format!(
3485                "{} ⊢ {}",
3486                premises.iter().map(|p| p.to_string()).collect::<Vec<_>>().join(", "),
3487                goal
3488            )
3489        };
3490        return Ok(emit(&premises, &goal, input, &fol));
3491    }
3492
3493    // A plain sentence: model-check the single formula.
3494    let proof = compile_for_proof(input);
3495    if let Some(expr) = proof.proof_expr {
3496        let fol = proof.logic_string.clone().unwrap_or_else(|| expr.to_string());
3497        return Ok(emit(&[], &expr, input, &fol));
3498    }
3499
3500    Ok("// could not parse this input into a logical formula to compile".to_string())
3501}
3502
3503/// Ask the Z3 oracle whether a theorem's premises semantically entail its goal.
3504///
3505/// Same `## Theorem:` block format as [`verify_theorem`], but the answer is an
3506/// [`SmtVerdict`](logicaffeine_proof::oracle::SmtVerdict) over the standard
3507/// translation (modal frame axioms, similarity relations, lattice axioms) —
3508/// **not** a kernel-certified proof. Use this for entailment questions the
3509/// monotonic kernel cannot express (modality, counterfactuals, mereology).
3510/// Assemble the lexicon-derived [`SmtTheory`](logicaffeine_proof::oracle::SmtTheory)
3511/// for a theorem: every mentioned predicate the lexicon tags as a MASS noun
3512/// becomes a cumulative (Link-lattice) predicate.
3513#[cfg(feature = "verification")]
3514fn smt_theory_for(
3515    premises: &[ProofExpr],
3516    goal: Option<&ProofExpr>,
3517) -> logicaffeine_proof::oracle::SmtTheory {
3518    let mut exprs: Vec<ProofExpr> = premises.to_vec();
3519    if let Some(g) = goal {
3520        exprs.push(g.clone());
3521    }
3522    let cumulative_predicates = logicaffeine_proof::oracle::predicate_names(&exprs)
3523        .into_iter()
3524        .filter(|name| logicaffeine_language::lexicon::is_mass_noun(name))
3525        .collect();
3526    logicaffeine_proof::oracle::SmtTheory {
3527        cumulative_predicates,
3528    }
3529}
3530
3531#[cfg(feature = "verification")]
3532pub fn check_theorem_smt(
3533    input: &str,
3534) -> Result<logicaffeine_proof::oracle::SmtVerdict, ParseError> {
3535    let (proof_exprs, goal_expr) = theorem_proof_exprs(input)?;
3536    let theory = smt_theory_for(&proof_exprs, Some(&goal_expr));
3537    Ok(logicaffeine_proof::oracle::oracle_entails_with_theory(
3538        &proof_exprs,
3539        &goal_expr,
3540        &theory,
3541    ))
3542}
3543
3544/// Ask the Z3 oracle whether a theorem's premises are jointly satisfiable
3545/// (the goal is parsed but ignored).
3546///
3547/// Every non-entailment claim in the test suite pairs with this check so an
3548/// inconsistent premise set cannot fake a `NotEntailed` via vacuity.
3549#[cfg(feature = "verification")]
3550pub fn check_theorem_premises_consistent(
3551    input: &str,
3552) -> Result<logicaffeine_proof::oracle::SmtConsistency, ParseError> {
3553    let (proof_exprs, _goal) = theorem_proof_exprs(input)?;
3554    let theory = smt_theory_for(&proof_exprs, None);
3555    Ok(logicaffeine_proof::oracle::oracle_consistent_with_theory(
3556        &proof_exprs,
3557        &theory,
3558    ))
3559}
3560
3561/// Ask the defeasible (non-monotonic) layer whether a theorem's premises
3562/// defeasibly entail its goal: generics and implicatures license cancellable
3563/// inferences via circumscription over per-rule abnormality predicates.
3564///
3565/// The verdict is **not kernel-certified** and is strictly weaker than
3566/// classical entailment — a defeated default reads as `NotEntailed` while the
3567/// premise set stays [`SmtConsistency::Consistent`].
3568#[cfg(feature = "verification")]
3569pub fn check_theorem_defeasible(
3570    input: &str,
3571) -> Result<logicaffeine_proof::oracle::SmtVerdict, ParseError> {
3572    let (proof_exprs, goal, defaults, _definitions) = theorem_problem(input, true)?;
3573    let theory = smt_theory_for(&proof_exprs, Some(&goal));
3574    Ok(crate::defeasible::defeasible_entails(
3575        &proof_exprs,
3576        &goal,
3577        &defaults,
3578        &theory,
3579    ))
3580}
3581
3582/// Consistency under the defeasible layer: defaults defeated by exceptions
3583/// must NOT read as contradictions.
3584#[cfg(feature = "verification")]
3585pub fn check_theorem_defeasible_consistent(
3586    input: &str,
3587) -> Result<logicaffeine_proof::oracle::SmtConsistency, ParseError> {
3588    let (proof_exprs, _goal, defaults, _definitions) = theorem_problem(input, true)?;
3589    let theory = smt_theory_for(&proof_exprs, None);
3590    Ok(crate::defeasible::defeasible_consistent(
3591        &proof_exprs,
3592        &defaults,
3593        &theory,
3594    ))
3595}
3596
3597/// Parse a `## Theorem:` block and convert its premises and goal to
3598/// [`ProofExpr`]s — the shared front half of [`verify_theorem`] and
3599/// `check_theorem_smt`.
3600/// Parse a `## Theorem` document and convert its premises and goal to
3601/// [`ProofExpr`]. Public so a puzzle solver can obtain the parsed-FOL premises
3602/// (the Given clues/declarations) and feed them to the entailment oracle.
3603pub fn theorem_proof_exprs(input: &str) -> Result<(Vec<ProofExpr>, ProofExpr), ParseError> {
3604    let (premises, goal, _defaults, _definitions) = theorem_problem(input, false)?;
3605    Ok((premises, goal))
3606}
3607
3608/// Like [`theorem_proof_exprs`] but also returns the document's `## Define`
3609/// blocks (Rung 0a) lowered to proof-layer definitions, so the prover can
3610/// δ-unfold them. This is the entry the kernel-certified theorem path uses.
3611pub fn theorem_proof_exprs_with_defs(
3612    input: &str,
3613) -> Result<
3614    (
3615        Vec<ProofExpr>,
3616        ProofExpr,
3617        Vec<logicaffeine_proof::verify::Definition>,
3618    ),
3619    ParseError,
3620> {
3621    let (premises, goal, _defaults, definitions) = theorem_problem(input, false)?;
3622    Ok((premises, goal, definitions))
3623}
3624
3625/// The `uses` dependency graph (Rung 0b) for an English document: its `## Define`
3626/// blocks plus the theorem's premises and goal, lowered and analyzed. Each node
3627/// is a definition or the theorem; each edge is a `uses`. This is the structure
3628/// a `mathscrapes` node/edge compiles into.
3629pub fn theorem_dependency_graph(
3630    input: &str,
3631) -> Result<logicaffeine_proof::verify::DependencyGraph, ParseError> {
3632    let (premises, goal, definitions) = theorem_proof_exprs_with_defs(input)?;
3633    Ok(logicaffeine_proof::verify::dependency_graph(
3634        &definitions,
3635        &premises,
3636        &goal,
3637    ))
3638}
3639
3640/// ANSWER a wh-question theorem ("Given: … Prove: Who is a lawyer?"). A wh-goal
3641/// converts to ∃x.φ(x); the ANSWER is the witness. We find it the same way the
3642/// puzzle is meant to be solved — by the GENERAL kernel-certified prover, applied
3643/// per candidate: enumerate the domain individuals named in the premises and prove
3644/// each candidate goal φ(c); the c's that prove are the answer (usually one, for a
3645/// puzzle with a unique solution). No question-specific reasoning — every step is
3646/// the same `prove_certify_check` that proves Socrates.
3647pub fn answer_question(input: &str) -> Result<Vec<String>, ParseError> {
3648    let (premises, goal) = theorem_proof_exprs(input)?;
3649    match &goal {
3650        ProofExpr::Exists { variable, body } => Ok(answer_wh(&premises, variable, body)),
3651        _ => Err(ParseError {
3652            kind: logicaffeine_language::error::ParseErrorKind::Custom(
3653                "Prove goal is not a question (expected a wh-question ∃-form)".to_string(),
3654            ),
3655            span: logicaffeine_language::token::Span::default(),
3656        }),
3657    }
3658}
3659
3660/// The witnesses of a wh-question `∃var. body(var)`: enumerate the domain individuals
3661/// named in the premises and keep those `c` for which `body(c)` is entailed by the
3662/// certified no-Z3 path. Shared by [`answer_question`] and the studio entry
3663/// [`compile_theorem_for_ui`], so a wh-goal never reaches the closed-goal search.
3664fn answer_wh(premises: &[ProofExpr], var: &str, body: &ProofExpr) -> Vec<String> {
3665    let mut candidates: Vec<String> = Vec::new();
3666    for p in premises {
3667        collect_constants(p, &mut candidates);
3668    }
3669    candidates.sort();
3670    candidates.dedup();
3671    // Prepare the premise set ONCE (tense-erase + finite-domain grounding) — it is
3672    // identical for every candidate cell, so doing it per candidate would re-ground the
3673    // whole grid N times. This is the optimization that keeps bigger puzzles tractable.
3674    let trace = std::env::var("LOGOS_TRACE").is_ok();
3675    let t0 = trace.then(std::time::Instant::now);
3676    let prepared = prepare_premises(premises);
3677    if let Some(t0) = t0 {
3678        eprintln!(
3679            "[answer] {} premises → prepared ({} clauses) in {:.2?}; {} candidates",
3680            premises.len(),
3681            prepared.len(),
3682            t0.elapsed(),
3683            candidates.len()
3684        );
3685    }
3686    let mut answers = Vec::new();
3687    for c in &candidates {
3688        let tc = trace.then(std::time::Instant::now);
3689        let candidate_goal =
3690            logicaffeine_language::proof_convert::instantiate_var_with_constant(body, var, c);
3691        let ok = candidate_entailed_prepared(&prepared, &candidate_goal);
3692        if let Some(tc) = tc {
3693            eprintln!("[answer]   {:<14} {} ({:.2?})", c, ok, tc.elapsed());
3694        }
3695        if ok {
3696            answers.push(c.clone());
3697        }
3698    }
3699    if let Some(t0) = t0 {
3700        eprintln!("[answer] total {:.2?} → {:?}", t0.elapsed(), answers);
3701    }
3702    answers
3703}
3704
3705/// SOLVE the whole grid for the studio (the form-recognized easter egg): re-parse the
3706/// theorem and, if it is a finite-domain grid, fill every cell the certified prover can
3707/// force. `None` for a non-grid theorem.
3708pub fn solve_grid(input: &str) -> Option<SolvedGrid> {
3709    let parsed = parse_theorem(input).ok()?;
3710    if !looks_like_grid(&parsed.premises) {
3711        return None;
3712    }
3713    solve_grid_from_premises(&parsed.premises, input)
3714}
3715
3716/// One category closure `∀x(RowSort(x) → d₁ ∨ … ∨ dₙ)`: the bound variable, its row
3717/// sort, and each disjunct paired with the value it contributes.
3718struct GridClosure {
3719    var: String,
3720    row_sort: String,
3721    disjuncts: Vec<(String, ProofExpr)>,
3722}
3723
3724/// Split a (possibly nested) disjunction into its leaf disjuncts.
3725fn flatten_or<'a>(e: &'a ProofExpr, out: &mut Vec<&'a ProofExpr>) {
3726    match e {
3727        ProofExpr::Or(l, r) => {
3728            flatten_or(l, out);
3729            flatten_or(r, out);
3730        }
3731        other => out.push(other),
3732    }
3733}
3734
3735/// The unary SORT predicate guarding `var` (`Trip(x)` → "trip"), if the antecedent is
3736/// one — possibly under conjunction.
3737fn antecedent_sort(e: &ProofExpr, var: &str) -> Option<String> {
3738    let is_var = |t: &ProofTerm| matches!(t, ProofTerm::Variable(v) | ProofTerm::BoundVarRef(v) if v == var);
3739    match e {
3740        ProofExpr::Predicate { name, args, .. } if args.len() == 1 && is_var(&args[0]) => {
3741            Some(name.clone())
3742        }
3743        ProofExpr::And(l, r) => antecedent_sort(l, var).or_else(|| antecedent_sort(r, var)),
3744        _ => None,
3745    }
3746}
3747
3748/// The value a disjunct contributes for `var`: the constant argument of a binary relation
3749/// (`in(x, Florida)` → "Florida") or the predicate name of a unary one (`cycling(x)` →
3750/// "cycling"). `None` if the disjunct is not a simple predicate over `var`.
3751fn disjunct_value(d: &ProofExpr, var: &str) -> Option<String> {
3752    let is_var = |t: &ProofTerm| matches!(t, ProofTerm::Variable(v) | ProofTerm::BoundVarRef(v) if v == var);
3753    match d {
3754        ProofExpr::Predicate { name, args, .. } => match args.as_slice() {
3755            [a] if is_var(a) => Some(name.clone()),
3756            [a, ProofTerm::Constant(c)] if is_var(a) => Some(c.clone()),
3757            [ProofTerm::Constant(c), a] if is_var(a) => Some(c.clone()),
3758            _ => None,
3759        },
3760        _ => None,
3761    }
3762}
3763
3764/// Extract one [`GridClosure`] per disjunctive-closure premise (`∀x(Trip(x) → A ∨ B ∨
3765/// …)`). These closures ARE the grid's columns — the antecedent names the rows, the
3766/// disjuncts name the cells.
3767fn extract_grid_closures(premises: &[ProofExpr]) -> Vec<GridClosure> {
3768    fn from_forall(e: &ProofExpr, out: &mut Vec<GridClosure>) {
3769        if let ProofExpr::ForAll { variable, body } = e {
3770            match body.as_ref() {
3771                ProofExpr::Implies(ante, cons) => {
3772                    let mut leaves = Vec::new();
3773                    flatten_or(cons, &mut leaves);
3774                    let disjuncts: Vec<(String, ProofExpr)> = leaves
3775                        .iter()
3776                        .filter_map(|d| disjunct_value(d, variable).map(|v| (v, (*d).clone())))
3777                        .collect();
3778                    if let Some(row_sort) = antecedent_sort(ante, variable) {
3779                        // Only a closure whose every disjunct is a clean predicate over the
3780                        // row var defines a column (guards against a stray non-grid ∀).
3781                        if !disjuncts.is_empty() && disjuncts.len() == leaves.len() {
3782                            out.push(GridClosure { var: variable.clone(), row_sort, disjuncts });
3783                        }
3784                    }
3785                }
3786                ProofExpr::ForAll { .. } => from_forall(body, out),
3787                _ => {}
3788            }
3789        }
3790    }
3791    let mut out = Vec::new();
3792    for p in premises {
3793        from_forall(&erase_tense(p), &mut out);
3794    }
3795    out
3796}
3797
3798/// Title-case a lowercased sort name for a column/row header ("trip" → "Trip").
3799fn title_case(s: &str) -> String {
3800    let mut chars = s.chars();
3801    match chars.next() {
3802        Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
3803        None => String::new(),
3804    }
3805}
3806
3807/// A human header for a category column: the declared sort whose domain covers the
3808/// column's values (case-insensitive), else a positional fallback.
3809fn grid_column_label(
3810    values: &[String],
3811    sorts: &std::collections::HashMap<String, Vec<ProofTerm>>,
3812    idx: usize,
3813) -> String {
3814    let want: Vec<String> = values.iter().map(|v| v.to_lowercase()).collect();
3815    let mut keys: Vec<&String> = sorts.keys().collect();
3816    keys.sort();
3817    for k in keys {
3818        let dom: std::collections::HashSet<String> = sorts[k]
3819            .iter()
3820            .filter_map(|t| match t {
3821                ProofTerm::Constant(c) => Some(c.to_lowercase()),
3822                _ => None,
3823            })
3824            .collect();
3825        if !dom.is_empty() && want.iter().all(|v| dom.contains(v)) {
3826            return title_case(k);
3827        }
3828    }
3829    format!("Category {}", idx + 1)
3830}
3831
3832/// A coarse stem that bridges a gerund surface form and the base form it normalizes to
3833/// (`"Cycling"` and `"Cycle"` → `"cycl"`), so a grid value can be matched back to the word
3834/// the user actually wrote. Strips a trailing `-ing` then a trailing `-e`; leaves
3835/// already-base words (constants, years, names) effectively unchanged.
3836fn category_stem(s: &str) -> String {
3837    let mut w = s.to_lowercase();
3838    if w.len() > 5 && w.ends_with("ing") {
3839        w.truncate(w.len() - 3);
3840    }
3841    if w.len() > 3 && w.ends_with('e') {
3842        w.truncate(w.len() - 1);
3843    }
3844    w
3845}
3846
3847/// Map each grid value back to the SURFACE word the user wrote. Category values
3848/// normalize to a base form in the FOL (a relation's constant like `Florida` survives, but
3849/// a gerund activity becomes `Cycle`); the original word ("cycling") lives only in the
3850/// input text. Key the input words by stem and recover them. First occurrence wins, so the
3851/// declaration's spelling is preferred.
3852fn surface_form_map(input: &str) -> std::collections::HashMap<String, String> {
3853    let mut map = std::collections::HashMap::new();
3854    for word in input.split(|c: char| !c.is_alphanumeric()) {
3855        if !word.is_empty() {
3856            map.entry(category_stem(word)).or_insert_with(|| word.to_string());
3857        }
3858    }
3859    map
3860}
3861
3862/// Fill the grid from already-parsed premises: rows are the row sort's domain, columns
3863/// are the disjunctive closures, and each cell is the disjunct value the certified no-Z3
3864/// prover entails for that row (or `None` if undetermined). Same engine `answer_question`
3865/// uses per cell, swept across the whole table.
3866fn solve_grid_from_premises(premises: &[ProofExpr], input: &str) -> Option<SolvedGrid> {
3867    let closures = extract_grid_closures(premises);
3868    if closures.is_empty() {
3869        return None;
3870    }
3871    let untensed: Vec<ProofExpr> = premises.iter().map(erase_tense).collect();
3872    let sorts = logicaffeine_proof::grounding::sort_domains(&untensed);
3873    let row_sort = closures[0].row_sort.clone();
3874    let rows: Vec<String> = sorts
3875        .get(&row_sort)
3876        .map(|dom| {
3877            dom.iter()
3878                .filter_map(|t| match t {
3879                    ProofTerm::Constant(c) => Some(c.clone()),
3880                    _ => None,
3881                })
3882                .collect()
3883        })
3884        .unwrap_or_default();
3885    if rows.is_empty() {
3886        return None;
3887    }
3888    // Ground ONCE, with functionality, exactly as the fast certified grid path does — the
3889    // exclusion lemmas let RUP decide each non-entailed candidate in ~1ms.
3890    let prepared = prepare_premises_opts(premises, true);
3891    let surface = surface_form_map(input);
3892    let display = |v: &str| surface.get(&category_stem(v)).cloned().unwrap_or_else(|| v.to_string());
3893    let mut columns = Vec::new();
3894    for clo in &closures {
3895        if clo.row_sort != row_sort {
3896            continue;
3897        }
3898        let values: Vec<String> = clo.disjuncts.iter().map(|(v, _)| display(v)).collect();
3899        let mut cells = Vec::with_capacity(rows.len());
3900        for r in &rows {
3901            let mut found = None;
3902            for (label, dj) in &clo.disjuncts {
3903                let atom = erase_tense(
3904                    &logicaffeine_language::proof_convert::instantiate_var_with_constant(
3905                        dj, &clo.var, r,
3906                    ),
3907                );
3908                if candidate_entailed_prepared(&prepared, &atom) {
3909                    found = Some(display(label));
3910                    break;
3911                }
3912            }
3913            cells.push(found);
3914        }
3915        let label = grid_column_label(&values, &sorts, columns.len());
3916        columns.push(GridColumn { label, values, cells });
3917    }
3918    Some(SolvedGrid { row_label: title_case(&row_sort), rows, columns })
3919}
3920
3921/// Prepare the premise set for the per-candidate solve, ONCE.
3922///
3923/// A logic grid is FINITE and STATIC, so we (1) ERASE TENSE — a grid is one static
3924/// scenario; the past-tense clue wrappers ("one WAS in Connecticut") carry no
3925/// information — and (2) GROUND the finite domain SORT-AWARE: each guarded quantifier
3926/// expands over its sort's declared domain (∀x(Trip(x)→…) over the four trips, not the
3927/// whole universe). The result is quantifier-free (decidable — neither our kernel nor
3928/// Z3 can instantiate ∀/∃ forever) and identical for every cell, so the per-cell
3929/// entailment check below sees a finite, decidable problem.
3930///
3931/// This is GENERAL over any declared finite-domain grid: the domains come from the
3932/// English declarations the parser produced (`sort_domains`), never from puzzle
3933/// knowledge. Grounding the KERNEL path (not just the Z3 path) is what lets the
3934/// certified prover close a full multi-category grid in the browser.
3935/// Does this premise set describe a finite-domain GRID — a declared bijection with a
3936/// closure (`∀x(Sort(x) → A ∨ B ∨ …)`) or an "exactly one" — as opposed to an open
3937/// syllogism? Only grids are grounded before proving (so Socrates keeps its
3938/// `UniversalInst` trace). Structural, never keyed to a particular puzzle.
3939fn looks_like_grid(premises: &[ProofExpr]) -> bool {
3940    fn has_disjunctive_closure(e: &ProofExpr) -> bool {
3941        match e {
3942            ProofExpr::ForAll { body, .. } => match body.as_ref() {
3943                ProofExpr::Implies(_, c) => matches!(c.as_ref(), ProofExpr::Or(..)),
3944                ProofExpr::ForAll { .. } => has_disjunctive_closure(body),
3945                _ => false,
3946            },
3947            ProofExpr::Temporal { body, .. } => has_disjunctive_closure(body),
3948            _ => false,
3949        }
3950    }
3951    !logicaffeine_proof::grounding::at_most_one_lemmas(premises).is_empty()
3952        || premises.iter().any(has_disjunctive_closure)
3953}
3954
3955fn prepare_premises(premises: &[ProofExpr]) -> Vec<ProofExpr> {
3956    prepare_premises_opts(premises, false)
3957}
3958
3959/// `prepare_premises`, optionally adding the bijection's FUNCTIONALITY half
3960/// (`∀x(Li → ¬Lj)`: a row takes at most one value per category). Functionality is
3961/// given ONLY to the incremental grid solver — it propagates the exclusions cheaply —
3962/// never to the re-saturating backward chainer, which it would slow to a crawl.
3963fn prepare_premises_opts(premises: &[ProofExpr], with_functionality: bool) -> Vec<ProofExpr> {
3964    let mut untensed: Vec<ProofExpr> = premises.iter().map(erase_tense).collect();
3965    // "Exactly one φ" ⟹ pairwise "at most one φ": the existence form grounds to a
3966    // disjunction the certified kernel cannot use directly; its entailed pairwise
3967    // uniqueness grounds to the conjunctive exclusion rules unit propagation runs on.
3968    let lemmas = logicaffeine_proof::grounding::at_most_one_lemmas(&untensed);
3969    untensed.extend(lemmas);
3970    // Definite-description clues ("the Florida trip is the hunting trip", "the trip with
3971    // Yvonne wasn't in Kentucky") name a unique row by a singleton value; rewrite them
3972    // to row-indexed implications the solver propagates, instead of an existential whose
3973    // grounding explodes to a row-disjunction.
3974    let defns = logicaffeine_proof::grounding::definite_property_implications(&untensed);
3975    untensed.extend(defns);
3976    if with_functionality {
3977        let func = logicaffeine_proof::grounding::functionality_lemmas(&untensed);
3978        untensed.extend(func);
3979        // HIDDEN SINGLE: the column closures (`⋁_r In(r, v)`) — the existence half of each
3980        // "exactly one" that `compile` otherwise drops. With functionality they let the
3981        // solver force a value's last remaining row, deepening the ROOT fixpoint so more
3982        // cells take the linear prove_var exit instead of search.
3983        let cols = logicaffeine_proof::grounding::column_closure_lemmas(&untensed);
3984        untensed.extend(cols);
3985    }
3986    let fallback = logicaffeine_proof::grounding::domain_constants(&untensed);
3987    let mut sorts = logicaffeine_proof::grounding::sort_domains(&untensed);
3988    bind_occasion_synonyms_to_row_domain(&untensed, &mut sorts);
3989    let grounded: Vec<ProofExpr> = untensed
3990        .iter()
3991        .map(|p| logicaffeine_proof::grounding::ground_sorted(p, &sorts, &fallback))
3992        .collect();
3993    // Discharge the now-true sort facts (`Trip(Alpha)`, …) so propagation runs on pure
3994    // value-literals: a closure becomes a bare disjunction, an at-most-one a one-step
3995    // exclusion rule. This is what keeps the full-grid search tractable.
3996    let discharged = logicaffeine_proof::grounding::discharge_unary_facts(&grounded);
3997    // Fold trivial reflexive identities: a grounded `∃x∃y` of-pair's diagonal
3998    // (`x = y = c`) carries `¬(c = c) = False`, so that disjunct drops.
3999    logicaffeine_proof::grounding::simplify_trivial_identities(&discharged)
4000}
4001
4002/// Does the premise set entail the candidate answer goal?
4003///
4004/// Without `verification`: the KERNEL-CERTIFIED prover over the GROUNDED, tense-erased
4005/// grid. Grounding made the problem quantifier-free, so the kernel's case-split +
4006/// reductio strategies close each cell with a real proof term — no Z3, runs in WASM.
4007/// The bounded depth makes an unentailed cell fail fast rather than search to the hilt.
4008#[cfg(not(feature = "verification"))]
4009fn candidate_entailed_prepared(prepared: &[ProofExpr], goal: &ProofExpr) -> bool {
4010    let g = erase_tense(goal);
4011    // FAST certified tier first: the CDCL+RUP engine decides the grounded grid in ~1ms with
4012    // an independently-checked proof (and answers a NON-entailed candidate just as fast,
4013    // where the kernel path degenerates to a deep futile search). Fall back to the bounded
4014    // kernel chainer only when the problem isn't purely propositional (RUP returns `None`).
4015    match logicaffeine_proof::rup::entails_certified(prepared, &g) {
4016        Some(logicaffeine_proof::rup::Verdict::Entailed) => return true,
4017        Some(logicaffeine_proof::rup::Verdict::NotEntailed) => return false,
4018        None => {}
4019    }
4020    logicaffeine_proof::verify::prove_certify_check_bounded(prepared, &g, 100).verified
4021}
4022
4023/// OCCASION soft-typing for the finite-domain solve.
4024///
4025/// In a logic grid the row entity is described by several OCCASION-sort head nouns
4026/// — "the hunting VACATION", "the Florida TRIP", "the 2004 HOLIDAY" — where the
4027/// modifier does the referring and the head is a soft type ([`Sort::is_occasion`],
4028/// the same principle [`drs::DiscourseModel::resolve_definite_by_modifier`] uses for
4029/// coreference). Only ONE of these head nouns is declared with a domain
4030/// (`Trip(Alpha)…`); the synonyms carry no domain, so an occasion guard like
4031/// `Vacation(x)` would ground over the WHOLE universe and a phantom non-row constant
4032/// (`Florida`, `2003`, …) could satisfy a synonym-headed clue. Point every
4033/// occasion-sorted guard at the shared row domain (the union of the declared
4034/// occasion-sort domains).
4035///
4036/// This is NOT global synonymy — no `Vacation ↔ Trip` axiom is asserted and "a work
4037/// trip" never collapses; the binding fires only for occasion-SORTED head predicates
4038/// inside the grounded solve, leaving ordinary parsing untouched.
4039fn bind_occasion_synonyms_to_row_domain(
4040    premises: &[ProofExpr],
4041    sorts: &mut std::collections::HashMap<String, Vec<logicaffeine_proof::ProofTerm>>,
4042) {
4043    use logicaffeine_language::lexicon::lookup_sort;
4044    use logicaffeine_proof::ProofTerm;
4045    let is_occasion = |n: &str| lookup_sort(n).map_or(false, |s| s.is_occasion());
4046
4047    // The shared row domain: the union of every declared occasion-sort domain. In a
4048    // grid there is one row sort with a domain (`Trip`); its synonyms have none.
4049    let mut row_domain: Vec<ProofTerm> = Vec::new();
4050    for (name, dom) in sorts.iter() {
4051        if is_occasion(name) {
4052            for c in dom {
4053                if !row_domain.contains(c) {
4054                    row_domain.push(c.clone());
4055                }
4056            }
4057        }
4058    }
4059    if row_domain.is_empty() {
4060        return;
4061    }
4062
4063    // Every occasion-sorted guard that appears in the premises but lacks its own
4064    // declared domain inherits the row domain.
4065    let mut names = HashSet::new();
4066    for p in premises {
4067        collect_unary_predicate_names(p, &mut names);
4068    }
4069    for name in names {
4070        if is_occasion(&name) {
4071            sorts.entry(name).or_insert_with(|| row_domain.clone());
4072        }
4073    }
4074}
4075
4076/// Collect every UNARY predicate name appearing anywhere in `e` — the candidate sort
4077/// guards the occasion-binding inspects.
4078fn collect_unary_predicate_names(e: &ProofExpr, out: &mut HashSet<String>) {
4079    match e {
4080        ProofExpr::Predicate { name, args, .. } if args.len() == 1 => {
4081            out.insert(name.clone());
4082        }
4083        ProofExpr::And(l, r)
4084        | ProofExpr::Or(l, r)
4085        | ProofExpr::Implies(l, r)
4086        | ProofExpr::Iff(l, r) => {
4087            collect_unary_predicate_names(l, out);
4088            collect_unary_predicate_names(r, out);
4089        }
4090        ProofExpr::Counterfactual { antecedent, consequent } => {
4091            collect_unary_predicate_names(antecedent, out);
4092            collect_unary_predicate_names(consequent, out);
4093        }
4094        ProofExpr::Not(x)
4095        | ProofExpr::ForAll { body: x, .. }
4096        | ProofExpr::Exists { body: x, .. }
4097        | ProofExpr::Temporal { body: x, .. }
4098        | ProofExpr::Modal { body: x, .. } => collect_unary_predicate_names(x, out),
4099        _ => {}
4100    }
4101}
4102
4103#[cfg(feature = "verification")]
4104fn candidate_entailed_prepared(prepared: &[ProofExpr], goal: &ProofExpr) -> bool {
4105    // The candidate goal is a ground predicate (no quantifiers) — just erase tense.
4106    let g = erase_tense(goal);
4107    // FAST certified tier first: the CDCL+RUP engine decides the grounded grid in ~1ms with
4108    // an independently-checked proof. Only if it can't encode the problem do we fall back to
4109    // the bounded kernel chainer, then Z3.
4110    match logicaffeine_proof::rup::entails_certified(prepared, &g) {
4111        Some(logicaffeine_proof::rup::Verdict::Entailed) => return true,
4112        Some(logicaffeine_proof::rup::Verdict::NotEntailed) => return false,
4113        None => {}
4114    }
4115    if logicaffeine_proof::verify::prove_certify_check_bounded(prepared, &g, 40).verified {
4116        return true;
4117    }
4118    matches!(
4119        logicaffeine_proof::oracle::oracle_entails(prepared, &g),
4120        logicaffeine_proof::oracle::SmtVerdict::Entailed
4121    )
4122}
4123
4124/// Strip `Temporal` wrappers throughout a [`ProofExpr`] — a logic grid is one
4125/// static scenario, so tense carries no information and would otherwise block the
4126/// oracle from forcing values across past-tense clues.
4127fn erase_tense(e: &ProofExpr) -> ProofExpr {
4128    match e {
4129        ProofExpr::Temporal { body, .. } => erase_tense(body),
4130        ProofExpr::And(l, r) => {
4131            ProofExpr::And(Box::new(erase_tense(l)), Box::new(erase_tense(r)))
4132        }
4133        ProofExpr::Or(l, r) => ProofExpr::Or(Box::new(erase_tense(l)), Box::new(erase_tense(r))),
4134        ProofExpr::Implies(l, r) => {
4135            ProofExpr::Implies(Box::new(erase_tense(l)), Box::new(erase_tense(r)))
4136        }
4137        ProofExpr::Iff(l, r) => ProofExpr::Iff(Box::new(erase_tense(l)), Box::new(erase_tense(r))),
4138        ProofExpr::Not(x) => ProofExpr::Not(Box::new(erase_tense(x))),
4139        ProofExpr::ForAll { variable, body } => ProofExpr::ForAll {
4140            variable: variable.clone(),
4141            body: Box::new(erase_tense(body)),
4142        },
4143        ProofExpr::Exists { variable, body } => ProofExpr::Exists {
4144            variable: variable.clone(),
4145            body: Box::new(erase_tense(body)),
4146        },
4147        other => other.clone(),
4148    }
4149}
4150
4151fn collect_constants(e: &ProofExpr, out: &mut Vec<String>) {
4152    use logicaffeine_proof::ProofTerm;
4153    fn term(t: &ProofTerm, out: &mut Vec<String>) {
4154        match t {
4155            ProofTerm::Constant(s) => out.push(s.clone()),
4156            ProofTerm::Function(_, args) | ProofTerm::Group(args) => {
4157                args.iter().for_each(|a| term(a, out))
4158            }
4159            _ => {}
4160        }
4161    }
4162    match e {
4163        ProofExpr::Predicate { args, .. } => args.iter().for_each(|a| term(a, out)),
4164        ProofExpr::Identity(a, b) => {
4165            term(a, out);
4166            term(b, out);
4167        }
4168        ProofExpr::And(l, r)
4169        | ProofExpr::Or(l, r)
4170        | ProofExpr::Implies(l, r)
4171        | ProofExpr::Iff(l, r) => {
4172            collect_constants(l, out);
4173            collect_constants(r, out);
4174        }
4175        ProofExpr::Not(x) => collect_constants(x, out),
4176        ProofExpr::ForAll { body, .. }
4177        | ProofExpr::Exists { body, .. }
4178        | ProofExpr::Temporal { body, .. } => collect_constants(body, out),
4179        ProofExpr::Term(t) => term(t, out),
4180        _ => {}
4181    }
4182}
4183
4184/// Lower a `## Define` block (Rung 0a) to a proof-layer definition: the LHS
4185/// predicate supplies the (normalized) definiendum name and parameter symbols,
4186/// the RHS is the definiens. Both sides pass through the SAME
4187/// `logic_expr_to_proof_expr` + interner as theorem premises and goals, so the
4188/// definiendum name and parameters match the predicate occurrences the prover
4189/// sees. Returns `None` if the LHS is not a predicate application.
4190fn lower_definition(
4191    def: &logicaffeine_language::ast::DefinitionBlock,
4192    interner: &Interner,
4193) -> Option<logicaffeine_proof::verify::Definition> {
4194    use logicaffeine_language::proof_convert::logic_expr_to_proof_expr;
4195    let (name, params) = match logic_expr_to_proof_expr(def.definiendum, interner) {
4196        ProofExpr::Predicate { name, args, .. } => {
4197            let params = args
4198                .iter()
4199                .filter_map(|t| match t {
4200                    ProofTerm::Constant(n) | ProofTerm::Variable(n) => Some(n.clone()),
4201                    _ => None,
4202                })
4203                .collect();
4204            (name, params)
4205        }
4206        _ => return None,
4207    };
4208    let definiens = logic_expr_to_proof_expr(def.definiens, interner);
4209    Some(logicaffeine_proof::verify::Definition {
4210        name,
4211        params,
4212        definiens,
4213    })
4214}
4215
4216/// [`theorem_proof_exprs`] with optional DEFEASIBLE conversion: premises keep
4217/// their generics/implicatures as abnormality-guarded defaults (returned for
4218/// the circumscription pass); the goal always converts strictly. Also returns
4219/// the document's `## Define` blocks lowered to proof-layer definitions (Rung 0a).
4220fn theorem_problem(
4221    input: &str,
4222    defeasible: bool,
4223) -> Result<
4224    (
4225        Vec<ProofExpr>,
4226        ProofExpr,
4227        Vec<logicaffeine_language::proof_convert::DefaultRule>,
4228        Vec<logicaffeine_proof::verify::Definition>,
4229    ),
4230    ParseError,
4231> {
4232    // === STEP 1: Parse ===
4233    let mut interner = Interner::new();
4234    let mut lexer = Lexer::new(input, &mut interner);
4235    let tokens = lexer.tokenize();
4236
4237    let mwe_trie = mwe::build_mwe_trie();
4238    let tokens = mwe::apply_mwe_pipeline(tokens, &mwe_trie, &mut interner);
4239
4240    let type_registry = {
4241        let mut discovery = DiscoveryPass::new(&tokens, &mut interner);
4242        discovery.run()
4243    };
4244
4245    let expr_arena = Arena::new();
4246    let term_arena = Arena::new();
4247    let np_arena = Arena::new();
4248    let sym_arena = Arena::new();
4249    let role_arena = Arena::new();
4250    let pp_arena = Arena::new();
4251
4252    let ctx = AstContext::new(
4253        &expr_arena,
4254        &term_arena,
4255        &np_arena,
4256        &sym_arena,
4257        &role_arena,
4258        &pp_arena,
4259    );
4260
4261    let mut world_state = drs::WorldState::new();
4262    let mut parser = Parser::new(tokens, &mut world_state, &mut interner, ctx, type_registry);
4263    if defeasible {
4264        // The defeasible layer reasons over the pragmatic channel too:
4265        // scalar implicatures become guarded defaults.
4266        parser.set_pragmatic_mode(true);
4267    }
4268    let statements = parser.parse_program()?;
4269
4270    let theorem = statements
4271        .iter()
4272        .find_map(|stmt| {
4273            if let Stmt::Theorem(t) = stmt {
4274                Some(t)
4275            } else {
4276                None
4277            }
4278        })
4279        .ok_or_else(|| ParseError {
4280            kind: logicaffeine_language::error::ParseErrorKind::Custom("No theorem block found in input".to_string()),
4281            span: logicaffeine_language::token::Span::default(),
4282        })?;
4283
4284    // === STEP 2: Convert premises and goal to ProofExpr ===
4285    let mut defaults = Vec::new();
4286    let proof_exprs: Vec<ProofExpr> = theorem
4287        .premises
4288        .iter()
4289        .map(|premise| {
4290            if defeasible {
4291                logicaffeine_language::proof_convert::logic_expr_to_proof_expr_defeasible(
4292                    premise,
4293                    &interner,
4294                    &mut defaults,
4295                )
4296            } else {
4297                logic_expr_to_proof_expr(premise, &interner)
4298            }
4299        })
4300        .collect();
4301    let goal_expr = logic_expr_to_proof_expr(theorem.goal, &interner);
4302
4303    // Rung 0a: collect every `## Define` block in the document, lowered to a
4304    // proof-layer definition the prover can δ-unfold.
4305    let definitions: Vec<logicaffeine_proof::verify::Definition> = statements
4306        .iter()
4307        .filter_map(|stmt| match stmt {
4308            Stmt::Definition(d) => lower_definition(d, &interner),
4309            _ => None,
4310        })
4311        .collect();
4312
4313    Ok((proof_exprs, goal_expr, defaults, definitions))
4314}