Skip to main content

synoema_eval/
lib.rs

1//! # synoema-eval
2//! Tree-walking interpreter for the Synoema programming language.
3//!
4//! Implements strict (eager) evaluation following the big-step
5//! operational semantics from the Language Reference §5.
6
7pub mod value;
8pub mod eval;
9
10pub use value::{Value, Env};
11pub use eval::{Evaluator, EvalError};
12
13use synoema_diagnostic::{Diagnostic, codes};
14use synoema_types::{TypeError, TypeErrorKind};
15
16// ── Error conversion ────────────────────────────────────
17
18fn parse_err(e: impl std::fmt::Display) -> Diagnostic {
19    Diagnostic::error(codes::PARSE_UNEXPECTED_TOKEN, format!("{}", e))
20}
21
22fn type_err(e: TypeError) -> Diagnostic {
23    let code = match &e.kind {
24        TypeErrorKind::Mismatch { .. } => codes::TYPE_MISMATCH,
25        TypeErrorKind::InfiniteType { .. } => codes::TYPE_INFINITE,
26        TypeErrorKind::Unbound { .. } => codes::TYPE_UNBOUND_VAR,
27        TypeErrorKind::UnboundType { .. } => codes::TYPE_UNBOUND_TYPE,
28        TypeErrorKind::ArityMismatch { .. } => codes::TYPE_ARITY,
29        TypeErrorKind::PatternMismatch { .. } => codes::TYPE_PATTERN,
30        TypeErrorKind::Other(_) => codes::TYPE_OTHER,
31    };
32    let mut diag = Diagnostic::error(code, format!("{}", e));
33    // Attach structured notes for type mismatches
34    if let TypeErrorKind::Mismatch { expected, found } = &e.kind {
35        diag = diag
36            .with_note(format!("expected: {}", expected))
37            .with_note(format!("found: {}", found));
38    }
39    diag.maybe_span(e.span)
40}
41
42fn eval_err(e: EvalError) -> Diagnostic {
43    let msg = &e.message;
44    let code = if msg.contains("Undefined") || msg.contains("undefined") {
45        codes::EVAL_UNDEFINED
46    } else if msg.contains("No matching pattern") || msg.contains("no matching") {
47        codes::EVAL_NO_MATCH
48    } else if msg.contains("Division by zero") || msg.contains("division by zero") {
49        codes::EVAL_DIV_ZERO
50    } else if msg.contains("readline") || msg.contains("IO") {
51        codes::EVAL_IO
52    } else {
53        codes::EVAL_TYPE
54    };
55    Diagnostic::error(code, e.message)
56}
57
58// ── Public API ──────────────────────────────────────────
59
60/// Parse, type-check, and evaluate a Synoema program.
61/// Returns the final environment.
62pub fn run(source: &str) -> Result<Env, Diagnostic> {
63    let program = synoema_parser::parse(source)
64        .map_err(|e| parse_err(&e))?;
65    let program = synoema_types::resolve_modules(program);
66    let _types = synoema_types::typecheck(source)
67        .map_err(type_err)?;
68    let mut evaluator = Evaluator::new();
69    evaluator.eval_program(&program)
70        .map_err(eval_err)
71}
72
73/// Parse, type-check, evaluate, and return a specific function's result
74/// when called with no arguments (a constant or nullary function).
75///
76/// Phase 10.1: runs in a 64 MB stack thread so deeply-recursive programs
77/// (like euler1 with 999 recursive calls) don't stack-overflow.
78pub fn eval_main(source: &str) -> Result<(Value, Vec<String>), Diagnostic> {
79    let source = source.to_string();
80    std::thread::Builder::new()
81        .stack_size(64 * 1024 * 1024) // 64 MB — handles ~50 000 levels of recursion
82        .spawn(move || eval_main_inner(&source))
83        .expect("Failed to spawn eval thread")
84        .join()
85        .expect("Eval thread panicked")
86}
87
88fn eval_main_inner(source: &str) -> Result<(Value, Vec<String>), Diagnostic> {
89    let program = synoema_parser::parse(source)
90        .map_err(|e| parse_err(&e))?;
91    let program = synoema_types::resolve_modules(program);
92    let _types = synoema_types::typecheck(source)
93        .map_err(type_err)?;
94    let mut evaluator = Evaluator::new();
95    let env = evaluator.eval_program(&program)
96        .map_err(eval_err)?;
97
98    // Look for 'main' or the last defined function
99    let main_name = program.decls.iter().rev()
100        .find_map(|d| match d {
101            synoema_parser::Decl::Func { name, .. } => Some(name.clone()),
102            _ => None,
103        })
104        .ok_or_else(|| Diagnostic::error(codes::EVAL_UNDEFINED, "No function defined"))?;
105
106    let val = env.lookup(&main_name)
107        .cloned()
108        .ok_or_else(|| Diagnostic::error(codes::EVAL_UNDEFINED, format!("Function '{}' not found", main_name)))?;
109
110    // If it's a zero-arg function (constant), evaluate its body
111    let result = match &val {
112        Value::Func { equations, .. } if equations.iter().all(|eq| eq.pats.is_empty()) => {
113            let eq = &equations[0];
114            evaluator.eval(&env, &eq.body)
115                .map_err(eval_err)?
116        }
117        other => other.clone(),
118    };
119
120    Ok((result, evaluator.output))
121}
122
123/// Quick eval: parse + eval an expression (for REPL), skip typechecking
124pub fn eval_expr(source: &str) -> Result<Value, Diagnostic> {
125    // Wrap as function definition for the parser
126    let wrapped = format!("__expr = {}", source);
127    let program = synoema_parser::parse(&wrapped)
128        .map_err(|e| parse_err(&e))?;
129    let mut evaluator = Evaluator::new();
130    let env = evaluator.eval_program(&program)
131        .map_err(eval_err)?;
132
133    match env.lookup("__expr") {
134        Some(Value::Func { equations, .. }) if !equations.is_empty() => {
135            let eq = &equations[0];
136            if eq.pats.is_empty() {
137                evaluator.eval(&env, &eq.body)
138                    .map_err(eval_err)
139            } else {
140                Ok(env.lookup("__expr").unwrap().clone())
141            }
142        }
143        Some(v) => Ok(v.clone()),
144        None => Err(Diagnostic::error(codes::EVAL_UNDEFINED, "Expression not found")),
145    }
146}
147
148#[cfg(test)]
149mod tests;