Skip to main content

ternlang_test/
lib.rs

1// SPDX-License-Identifier: LGPL-3.0-or-later
2// Ternlang — RFI-IRFOS Ternary Intelligence Stack
3// Copyright (C) 2026 RFI-IRFOS
4// Open-core compiler. See LICENSE-LGPL in the repository root.
5
6//! ternlang-test — test framework for `.tern` programs.
7//!
8//! Runs source strings through the full pipeline (parse → stdlib resolve →
9//! semantic check → codegen → BET VM) and asserts on the outcome.
10//!
11//! # Quick start
12//! ```rust,no_run
13//! # use ternlang_test::{TernTestCase, TernExpected, assert_tern};
14//! # fn main() {
15//! assert_tern!(TernTestCase {
16//!     name: "hold is the zero state",
17//!     source: "fn main() -> trit { return 0; }",
18//!     expected: TernExpected::Trit(0),
19//! });
20//! # }
21//! ```
22
23use ternlang_core::{
24    Parser, SemanticAnalyzer, BytecodeEmitter, StdlibLoader, BetVm,
25    vm::Value,
26    trit::Trit,
27};
28
29// ─── Test case definition ─────────────────────────────────────────────────────
30
31/// What a test expects as its outcome.
32#[derive(Debug, Clone)]
33pub enum TernExpected {
34    /// The program's return value should equal this trit (-1, 0, or +1).
35    Trit(i8),
36    /// The program should fail to parse (any parse error satisfies this).
37    ParseError,
38    /// The program should pass parsing but fail semantic analysis.
39    SemanticError,
40}
41
42/// A single ternlang test case.
43pub struct TernTestCase {
44    pub name: &'static str,
45    /// Complete `.tern` source. Must contain a `fn main() -> trit { }`.
46    pub source: &'static str,
47    pub expected: TernExpected,
48}
49
50// ─── Result type ─────────────────────────────────────────────────────────────
51
52/// Outcome of running a `TernTestCase`.
53#[derive(Debug)]
54pub struct TernTestResult {
55    pub name: &'static str,
56    pub passed: bool,
57    pub actual_trit: Option<i8>,
58    pub message: String,
59}
60
61impl TernTestResult {
62    fn pass(name: &'static str, trit: Option<i8>) -> Self {
63        Self { name, passed: true, actual_trit: trit, message: "ok".into() }
64    }
65
66    fn fail(name: &'static str, msg: impl Into<String>) -> Self {
67        Self { name, passed: false, actual_trit: None, message: msg.into() }
68    }
69
70    fn fail_trit(name: &'static str, actual: i8, msg: impl Into<String>) -> Self {
71        Self { name, passed: false, actual_trit: Some(actual), message: msg.into() }
72    }
73}
74
75// ─── Runner ──────────────────────────────────────────────────────────────────
76
77/// Run a single test case through the complete ternlang pipeline and return
78/// a result indicating pass/fail with a diagnostic message.
79pub fn run_tern_test(case: &TernTestCase) -> TernTestResult {
80    // 1. Parse
81    let mut parser = Parser::new(case.source);
82    let prog = match parser.parse_program() {
83        Ok(p) => p,
84        Err(e) => {
85            let msg = format!("{}", e);
86            let passed = matches!(case.expected, TernExpected::ParseError);
87            return if passed {
88                TernTestResult::pass(case.name, None)
89            } else {
90                TernTestResult::fail(case.name, format!("Parse error (unexpected): {msg}"))
91            };
92        }
93    };
94    if matches!(case.expected, TernExpected::ParseError) {
95        return TernTestResult::fail(case.name, "expected a parse error but program parsed successfully");
96    }
97
98    // 2. Stdlib resolve
99    let mut prog = prog;
100    StdlibLoader::resolve(&mut prog);
101
102    // 3. Semantic analysis
103    let mut checker = SemanticAnalyzer::new();
104    if let Err(e) = checker.check_program(&prog) {
105        let msg = format!("{}", e);
106        let passed = matches!(case.expected, TernExpected::SemanticError);
107        return if passed {
108            TernTestResult::pass(case.name, None)
109        } else {
110            TernTestResult::fail(case.name, format!("Semantic error (unexpected): {msg}"))
111        };
112    }
113    if matches!(case.expected, TernExpected::SemanticError) {
114        return TernTestResult::fail(case.name, "expected a semantic error but program passed analysis");
115    }
116
117    // 4. Codegen
118    let mut emitter = BytecodeEmitter::new();
119    emitter.emit_program(&prog);
120    // Emit a TCALL to main so the entry TJMP lands on the actual invocation.
121    emitter.emit_entry_call("main");
122    let code = emitter.finalize();
123
124    // 5. VM execution
125    let mut vm = BetVm::new(code);
126    match vm.run() {
127        Err(e) => TernTestResult::fail(case.name, format!("VM error: {e}")),
128        Ok(()) => {
129            // Result is the top of the stack after execution.
130            let trit_val: i8 = match vm.peek_stack() {
131                Some(Value::Trit(Trit::Affirm))  =>  1,
132                Some(Value::Trit(Trit::Tend))    =>  0,
133                Some(Value::Trit(Trit::Reject))  => -1,
134                Some(other) => {
135                    return TernTestResult::fail(
136                        case.name,
137                        format!("VM returned non-trit value: {other:?}"),
138                    );
139                }
140                None => {
141                    return TernTestResult::fail(case.name, "VM stack is empty after execution");
142                }
143            };
144
145            match &case.expected {
146                TernExpected::Trit(expected) => {
147                    if trit_val == *expected {
148                        TernTestResult::pass(case.name, Some(trit_val))
149                    } else {
150                        TernTestResult::fail_trit(
151                            case.name,
152                            trit_val,
153                            format!("expected trit={expected}, got trit={trit_val}"),
154                        )
155                    }
156                }
157                // Already handled above
158                TernExpected::ParseError | TernExpected::SemanticError => unreachable!(),
159            }
160        }
161    }
162}
163
164// ─── Assertion macro ──────────────────────────────────────────────────────────
165
166/// Run a `TernTestCase` and panic with a readable message if it fails.
167///
168/// ```rust,no_run
169/// # use ternlang_test::{TernTestCase, TernExpected, assert_tern};
170/// # fn main() {
171/// assert_tern!(TernTestCase {
172///     name: "hold is zero",
173///     source: "fn main() -> trit { return 0; }",
174///     expected: TernExpected::Trit(0),
175/// });
176/// # }
177/// ```
178#[macro_export]
179macro_rules! assert_tern {
180    ($case:expr) => {{
181        let result = $crate::run_tern_test(&$case);
182        assert!(
183            result.passed,
184            "\n[TERN-TEST] '{}' failed\n  → {}\n",
185            result.name,
186            result.message,
187        );
188        result
189    }};
190}
191
192// ─── Built-in test suite ──────────────────────────────────────────────────────
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    fn run(name: &'static str, source: &'static str, expected: TernExpected) -> TernTestResult {
199        run_tern_test(&TernTestCase { name, source, expected })
200    }
201
202    // ── Trit literals ─────────────────────────────────────────────────────────
203
204    #[test]
205    fn trit_pos_literal() {
206        let r = run("trit +1", "fn main() -> trit { return 1; }", TernExpected::Trit(1));
207        assert!(r.passed, "{}", r.message);
208    }
209
210    #[test]
211    fn trit_zero_literal() {
212        let r = run("trit 0", "fn main() -> trit { return 0; }", TernExpected::Trit(0));
213        assert!(r.passed, "{}", r.message);
214    }
215
216    #[test]
217    fn trit_neg_literal() {
218        let r = run("trit -1", "fn main() -> trit { return -1; }", TernExpected::Trit(-1));
219        assert!(r.passed, "{}", r.message);
220    }
221
222    // ── Consensus ─────────────────────────────────────────────────────────────
223
224    #[test]
225    fn consensus_pos_and_zero() {
226        // balanced ternary: 1 + 0 = 1
227        let r = run(
228            "consensus(+1, 0)=+1",
229            "fn main() -> trit { return consensus(1, 0); }",
230            TernExpected::Trit(1),
231        );
232        assert!(r.passed, "{}", r.message);
233    }
234
235    #[test]
236    fn consensus_conflict_holds() {
237        // balanced ternary: 1 + (-1) = 0
238        let r = run(
239            "consensus(+1,-1)=0",
240            "fn main() -> trit { return consensus(1, -1); }",
241            TernExpected::Trit(0),
242        );
243        assert!(r.passed, "{}", r.message);
244    }
245
246    // ── Error propagation (`?`) ───────────────────────────────────────────────
247
248    #[test]
249    fn propagate_passes_through_pos() {
250        let r = run(
251            "propagate pass-through on +1",
252            r#"
253fn check() -> trit { return 1; }
254fn main() -> trit { return check()?; }
255"#,
256            TernExpected::Trit(1),
257        );
258        assert!(r.passed, "{}", r.message);
259    }
260
261    #[test]
262    fn propagate_early_returns_on_neg() {
263        // check() returns -1 → main() should return -1 via propagation
264        let r = run(
265            "propagate early return on -1",
266            r#"
267fn check() -> trit { return -1; }
268fn main() -> trit {
269    let x: trit = check()?;
270    return 1;
271}
272"#,
273            TernExpected::Trit(-1),
274        );
275        assert!(r.passed, "{}", r.message);
276    }
277
278    // ── Module system ─────────────────────────────────────────────────────────
279
280    #[test]
281    fn stdlib_trit_resolves() {
282        let r = run(
283            "std::trit resolves",
284            r#"
285fn main() -> trit {
286    use std::trit;
287    return abs(-1);
288}
289"#,
290            TernExpected::Trit(1),
291        );
292        assert!(r.passed, "{}", r.message);
293    }
294
295    // ── Error messages ────────────────────────────────────────────────────────
296
297    #[test]
298    fn non_exhaustive_match_is_parse_error() {
299        let r = run(
300            "non-exhaustive match",
301            r#"
302fn main() -> trit {
303    let x: trit = 1;
304    match x { 1 => { return 1; } 0 => { return 0; } }
305}
306"#,
307            TernExpected::ParseError,
308        );
309        assert!(r.passed, "{}", r.message);
310    }
311
312    #[test]
313    fn undefined_variable_is_semantic_error() {
314        let r = run(
315            "undefined variable",
316            "fn main() -> trit { return ghost; }",
317            TernExpected::SemanticError,
318        );
319        assert!(r.passed, "{}", r.message);
320    }
321}