Add support for string interpolation with {expr} syntax:
"Hello, {name}!" becomes "Hello, " + toString(name) + "!"
Lexer changes:
- Add StringPart enum (Literal/Expr) and InterpolatedString token
- Detect {expr} in strings and capture expression text
- Support escaped braces with \{ and \}
Parser changes:
- Add desugar_interpolated_string() to convert to concatenation
- Automatically wrap expressions in toString() calls
Interpreter changes:
- Fix toString() to not add quotes around strings
Tests added:
- 4 lexer tests for interpolation tokenization
- 4 integration tests for full interpolation pipeline
Add examples/interpolation.lux demonstrating:
- Variable interpolation
- Number interpolation (auto toString)
- Expression interpolation ({a + b})
- Escaped braces
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2333 lines
82 KiB
Rust
2333 lines
82 KiB
Rust
//! Tree-walking interpreter for the Lux language with algebraic effects
|
|
|
|
#![allow(dead_code, unused_variables)]
|
|
|
|
use crate::ast::*;
|
|
use crate::diagnostics::{Diagnostic, Severity};
|
|
use std::cell::RefCell;
|
|
use std::collections::HashMap;
|
|
use std::fmt;
|
|
use std::rc::Rc;
|
|
|
|
/// Built-in function identifier
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum BuiltinFn {
|
|
// List operations
|
|
ListMap,
|
|
ListFilter,
|
|
ListFold,
|
|
ListHead,
|
|
ListTail,
|
|
ListConcat,
|
|
ListReverse,
|
|
ListLength,
|
|
ListGet,
|
|
ListRange,
|
|
|
|
// String operations
|
|
StringSplit,
|
|
StringJoin,
|
|
StringTrim,
|
|
StringContains,
|
|
StringReplace,
|
|
StringLength,
|
|
StringChars,
|
|
StringLines,
|
|
|
|
// Option operations
|
|
OptionMap,
|
|
OptionFlatMap,
|
|
OptionGetOrElse,
|
|
OptionIsSome,
|
|
OptionIsNone,
|
|
|
|
// Result operations
|
|
ResultMap,
|
|
ResultFlatMap,
|
|
ResultGetOrElse,
|
|
ResultIsOk,
|
|
ResultIsErr,
|
|
|
|
// Utility
|
|
Print,
|
|
ToString,
|
|
TypeOf,
|
|
}
|
|
|
|
/// Runtime value
|
|
#[derive(Debug, Clone)]
|
|
pub enum Value {
|
|
Int(i64),
|
|
Float(f64),
|
|
Bool(bool),
|
|
String(String),
|
|
Char(char),
|
|
Unit,
|
|
List(Vec<Value>),
|
|
Tuple(Vec<Value>),
|
|
Record(HashMap<String, Value>),
|
|
Function(Rc<Closure>),
|
|
Handler(Rc<HandlerValue>),
|
|
/// Built-in function
|
|
Builtin(BuiltinFn),
|
|
/// Constructor value (for ADTs)
|
|
Constructor {
|
|
name: String,
|
|
fields: Vec<Value>,
|
|
},
|
|
/// Versioned value (for schema evolution)
|
|
Versioned {
|
|
type_name: String,
|
|
version: u32,
|
|
value: Box<Value>,
|
|
},
|
|
}
|
|
|
|
impl Value {
|
|
pub fn type_name(&self) -> &'static str {
|
|
match self {
|
|
Value::Int(_) => "Int",
|
|
Value::Float(_) => "Float",
|
|
Value::Bool(_) => "Bool",
|
|
Value::String(_) => "String",
|
|
Value::Char(_) => "Char",
|
|
Value::Unit => "Unit",
|
|
Value::List(_) => "List",
|
|
Value::Tuple(_) => "Tuple",
|
|
Value::Record(_) => "Record",
|
|
Value::Function(_) => "Function",
|
|
Value::Handler(_) => "Handler",
|
|
Value::Builtin(_) => "Function",
|
|
Value::Constructor { .. } => "Constructor",
|
|
Value::Versioned { .. } => "Versioned",
|
|
}
|
|
}
|
|
|
|
/// Unwrap a versioned value to get the inner value
|
|
pub fn unwrap_versioned(&self) -> &Value {
|
|
match self {
|
|
Value::Versioned { value, .. } => value.unwrap_versioned(),
|
|
other => other,
|
|
}
|
|
}
|
|
|
|
/// Get version info if this is a versioned value
|
|
pub fn version_info(&self) -> Option<(String, u32)> {
|
|
match self {
|
|
Value::Versioned {
|
|
type_name, version, ..
|
|
} => Some((type_name.clone(), *version)),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Trait for extracting typed values from Value
|
|
trait TryFromValue: Sized {
|
|
const TYPE_NAME: &'static str;
|
|
fn try_from_value(value: &Value) -> Option<Self>;
|
|
}
|
|
|
|
impl TryFromValue for i64 {
|
|
const TYPE_NAME: &'static str = "Int";
|
|
fn try_from_value(value: &Value) -> Option<Self> {
|
|
match value {
|
|
Value::Int(n) => Some(*n),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TryFromValue for f64 {
|
|
const TYPE_NAME: &'static str = "Float";
|
|
fn try_from_value(value: &Value) -> Option<Self> {
|
|
match value {
|
|
Value::Float(n) => Some(*n),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TryFromValue for String {
|
|
const TYPE_NAME: &'static str = "String";
|
|
fn try_from_value(value: &Value) -> Option<Self> {
|
|
match value {
|
|
Value::String(s) => Some(s.clone()),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TryFromValue for bool {
|
|
const TYPE_NAME: &'static str = "Bool";
|
|
fn try_from_value(value: &Value) -> Option<Self> {
|
|
match value {
|
|
Value::Bool(b) => Some(*b),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TryFromValue for Vec<Value> {
|
|
const TYPE_NAME: &'static str = "List";
|
|
fn try_from_value(value: &Value) -> Option<Self> {
|
|
match value {
|
|
Value::List(l) => Some(l.clone()),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TryFromValue for Value {
|
|
const TYPE_NAME: &'static str = "any";
|
|
fn try_from_value(value: &Value) -> Option<Self> {
|
|
Some(value.clone())
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Value {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Value::Int(n) => write!(f, "{}", n),
|
|
Value::Float(n) => write!(f, "{}", n),
|
|
Value::Bool(b) => write!(f, "{}", b),
|
|
Value::String(s) => write!(f, "\"{}\"", s),
|
|
Value::Char(c) => write!(f, "'{}'", c),
|
|
Value::Unit => write!(f, "()"),
|
|
Value::List(elements) => {
|
|
write!(f, "[")?;
|
|
for (i, e) in elements.iter().enumerate() {
|
|
if i > 0 {
|
|
write!(f, ", ")?;
|
|
}
|
|
write!(f, "{}", e)?;
|
|
}
|
|
write!(f, "]")
|
|
}
|
|
Value::Tuple(elements) => {
|
|
write!(f, "(")?;
|
|
for (i, e) in elements.iter().enumerate() {
|
|
if i > 0 {
|
|
write!(f, ", ")?;
|
|
}
|
|
write!(f, "{}", e)?;
|
|
}
|
|
write!(f, ")")
|
|
}
|
|
Value::Record(fields) => {
|
|
write!(f, "{{ ")?;
|
|
for (i, (name, value)) in fields.iter().enumerate() {
|
|
if i > 0 {
|
|
write!(f, ", ")?;
|
|
}
|
|
write!(f, "{}: {}", name, value)?;
|
|
}
|
|
write!(f, " }}")
|
|
}
|
|
Value::Function(_) => write!(f, "<function>"),
|
|
Value::Builtin(b) => write!(f, "<builtin:{:?}>", b),
|
|
Value::Handler(_) => write!(f, "<handler>"),
|
|
Value::Constructor { name, fields } => {
|
|
if fields.is_empty() {
|
|
write!(f, "{}", name)
|
|
} else {
|
|
write!(f, "{}(", name)?;
|
|
for (i, field) in fields.iter().enumerate() {
|
|
if i > 0 {
|
|
write!(f, ", ")?;
|
|
}
|
|
write!(f, "{}", field)?;
|
|
}
|
|
write!(f, ")")
|
|
}
|
|
}
|
|
Value::Versioned {
|
|
type_name,
|
|
version,
|
|
value,
|
|
} => {
|
|
write!(f, "{} @v{}", value, version)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Function closure
|
|
#[derive(Debug)]
|
|
pub struct Closure {
|
|
pub params: Vec<String>,
|
|
pub body: Expr,
|
|
pub env: Env,
|
|
}
|
|
|
|
/// Handler value
|
|
#[derive(Debug)]
|
|
pub struct HandlerValue {
|
|
pub effect: String,
|
|
pub implementations: HashMap<String, HandlerImpl>,
|
|
pub env: Env,
|
|
}
|
|
|
|
/// Environment (lexical scope)
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct Env {
|
|
bindings: Rc<RefCell<HashMap<String, Value>>>,
|
|
parent: Option<Box<Env>>,
|
|
}
|
|
|
|
impl Env {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
bindings: Rc::new(RefCell::new(HashMap::new())),
|
|
parent: None,
|
|
}
|
|
}
|
|
|
|
pub fn extend(&self) -> Self {
|
|
Self {
|
|
bindings: Rc::new(RefCell::new(HashMap::new())),
|
|
parent: Some(Box::new(self.clone())),
|
|
}
|
|
}
|
|
|
|
pub fn define(&self, name: impl Into<String>, value: Value) {
|
|
self.bindings.borrow_mut().insert(name.into(), value);
|
|
}
|
|
|
|
pub fn get(&self, name: &str) -> Option<Value> {
|
|
if let Some(value) = self.bindings.borrow().get(name) {
|
|
return Some(value.clone());
|
|
}
|
|
if let Some(ref parent) = self.parent {
|
|
return parent.get(name);
|
|
}
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Runtime error
|
|
#[derive(Debug, Clone)]
|
|
pub struct RuntimeError {
|
|
pub message: String,
|
|
pub span: Option<Span>,
|
|
}
|
|
|
|
impl fmt::Display for RuntimeError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
if let Some(span) = self.span {
|
|
write!(
|
|
f,
|
|
"Runtime error at {}-{}: {}",
|
|
span.start, span.end, self.message
|
|
)
|
|
} else {
|
|
write!(f, "Runtime error: {}", self.message)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for RuntimeError {}
|
|
|
|
impl RuntimeError {
|
|
/// Convert to a rich diagnostic for Elm-style error display
|
|
pub fn to_diagnostic(&self) -> Diagnostic {
|
|
let (title, hints) = categorize_runtime_error(&self.message);
|
|
|
|
Diagnostic {
|
|
severity: Severity::Error,
|
|
title,
|
|
message: self.message.clone(),
|
|
span: self.span.unwrap_or_default(),
|
|
hints,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Categorize runtime errors to provide better titles and hints
|
|
fn categorize_runtime_error(message: &str) -> (String, Vec<String>) {
|
|
let message_lower = message.to_lowercase();
|
|
|
|
if message_lower.contains("undefined") || message_lower.contains("not found") {
|
|
(
|
|
"Undefined Reference".to_string(),
|
|
vec!["Make sure the name is defined and in scope.".to_string()],
|
|
)
|
|
} else if message_lower.contains("division by zero") || message_lower.contains("divide by zero") {
|
|
(
|
|
"Division by Zero".to_string(),
|
|
vec![
|
|
"Check that the divisor is not zero before dividing.".to_string(),
|
|
"Consider using a guard or match to handle this case.".to_string(),
|
|
],
|
|
)
|
|
} else if message_lower.contains("type") && message_lower.contains("mismatch") {
|
|
(
|
|
"Type Mismatch".to_string(),
|
|
vec!["The value has a different type than expected.".to_string()],
|
|
)
|
|
} else if message_lower.contains("effect") && message_lower.contains("unhandled") {
|
|
(
|
|
"Unhandled Effect".to_string(),
|
|
vec![
|
|
"This effect must be handled before the program can continue.".to_string(),
|
|
"Wrap this code in a 'handle' expression.".to_string(),
|
|
],
|
|
)
|
|
} else if message_lower.contains("pattern") && message_lower.contains("match") {
|
|
(
|
|
"Non-exhaustive Pattern".to_string(),
|
|
vec!["Add more patterns to cover all possible cases.".to_string()],
|
|
)
|
|
} else if message_lower.contains("argument") {
|
|
(
|
|
"Wrong Arguments".to_string(),
|
|
vec!["Check the number and types of arguments provided.".to_string()],
|
|
)
|
|
} else if message_lower.contains("index") || message_lower.contains("bounds") {
|
|
(
|
|
"Index Out of Bounds".to_string(),
|
|
vec![
|
|
"The index is outside the valid range.".to_string(),
|
|
"Check the length of the collection before accessing.".to_string(),
|
|
],
|
|
)
|
|
} else {
|
|
("Runtime Error".to_string(), vec![])
|
|
}
|
|
}
|
|
|
|
/// Effect operation request
|
|
#[derive(Debug, Clone)]
|
|
pub struct EffectRequest {
|
|
pub effect: String,
|
|
pub operation: String,
|
|
pub args: Vec<Value>,
|
|
pub continuation: Continuation,
|
|
}
|
|
|
|
/// Continuation (captured rest of computation)
|
|
#[derive(Debug, Clone)]
|
|
pub struct Continuation {
|
|
// For simplicity, we'll use a callback-based approach
|
|
// In a real implementation, this would capture the stack
|
|
id: usize,
|
|
}
|
|
|
|
static NEXT_CONT_ID: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
|
|
|
|
impl Continuation {
|
|
fn new() -> Self {
|
|
Self {
|
|
id: NEXT_CONT_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Result of evaluation (either a value, effect request, or tail call)
|
|
pub enum EvalResult {
|
|
Value(Value),
|
|
Effect(EffectRequest),
|
|
/// Tail call optimization: instead of recursing, return the call to be trampolined
|
|
TailCall {
|
|
func: Value,
|
|
args: Vec<Value>,
|
|
span: Span,
|
|
},
|
|
}
|
|
|
|
/// Effect trace entry for debugging
|
|
#[derive(Debug, Clone)]
|
|
pub struct EffectTrace {
|
|
pub effect: String,
|
|
pub operation: String,
|
|
pub args: Vec<Value>,
|
|
pub result: Option<Value>,
|
|
pub timestamp_us: u128,
|
|
}
|
|
|
|
/// The interpreter
|
|
/// A stored migration function
|
|
#[derive(Clone)]
|
|
pub struct StoredMigration {
|
|
/// The expression to evaluate for migration
|
|
pub body: Expr,
|
|
/// Environment captured when the migration was defined
|
|
pub env: Env,
|
|
}
|
|
|
|
impl std::fmt::Debug for StoredMigration {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("StoredMigration").finish()
|
|
}
|
|
}
|
|
|
|
pub struct Interpreter {
|
|
global_env: Env,
|
|
/// Stack of active effect handlers
|
|
handler_stack: Vec<Rc<HandlerValue>>,
|
|
/// Stored continuations for resumption
|
|
continuations: HashMap<usize, Box<dyn FnOnce(Value) -> Result<EvalResult, RuntimeError>>>,
|
|
/// Effect tracing for debugging
|
|
pub trace_effects: bool,
|
|
/// Collected effect traces
|
|
pub effect_traces: Vec<EffectTrace>,
|
|
/// Start time for timestamps
|
|
start_time: std::time::Instant,
|
|
/// Migration registry: type_name -> (from_version -> to_version -> migration)
|
|
migrations: HashMap<String, HashMap<u32, StoredMigration>>,
|
|
}
|
|
|
|
impl Interpreter {
|
|
pub fn new() -> Self {
|
|
let global_env = Env::new();
|
|
|
|
// Add built-in functions
|
|
Self::add_builtins(&global_env);
|
|
|
|
Self {
|
|
global_env,
|
|
handler_stack: Vec::new(),
|
|
continuations: HashMap::new(),
|
|
trace_effects: false,
|
|
effect_traces: Vec::new(),
|
|
start_time: std::time::Instant::now(),
|
|
migrations: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
/// Enable effect tracing for debugging
|
|
pub fn enable_tracing(&mut self) {
|
|
self.trace_effects = true;
|
|
self.effect_traces.clear();
|
|
self.start_time = std::time::Instant::now();
|
|
}
|
|
|
|
/// Get all effect traces
|
|
pub fn get_traces(&self) -> &[EffectTrace] {
|
|
&self.effect_traces
|
|
}
|
|
|
|
/// Print effect traces in a readable format
|
|
pub fn print_traces(&self) {
|
|
println!("\n── EFFECT TRACE ──────────────────────────────────────");
|
|
for trace in &self.effect_traces {
|
|
let time_ms = trace.timestamp_us as f64 / 1000.0;
|
|
let args_str: Vec<String> = trace.args.iter().map(|a| format!("{}", a)).collect();
|
|
let result_str = trace
|
|
.result
|
|
.as_ref()
|
|
.map(|r| format!(" → {}", r))
|
|
.unwrap_or_default();
|
|
println!(
|
|
"[{:8.3}ms] {}.{}({}){}",
|
|
time_ms,
|
|
trace.effect,
|
|
trace.operation,
|
|
args_str.join(", "),
|
|
result_str
|
|
);
|
|
}
|
|
println!("──────────────────────────────────────────────────────\n");
|
|
}
|
|
|
|
/// Register a migration for a versioned type
|
|
pub fn register_migration(
|
|
&mut self,
|
|
type_name: &str,
|
|
from_version: u32,
|
|
migration: StoredMigration,
|
|
) {
|
|
self.migrations
|
|
.entry(type_name.to_string())
|
|
.or_default()
|
|
.insert(from_version, migration);
|
|
}
|
|
|
|
/// Create a versioned value
|
|
pub fn create_versioned(&self, type_name: &str, version: u32, value: Value) -> Value {
|
|
Value::Versioned {
|
|
type_name: type_name.to_string(),
|
|
version,
|
|
value: Box::new(value),
|
|
}
|
|
}
|
|
|
|
/// Migrate a versioned value to a target version
|
|
pub fn migrate_value(
|
|
&mut self,
|
|
value: Value,
|
|
target_version: u32,
|
|
) -> Result<Value, RuntimeError> {
|
|
let (type_name, current_version, inner_value) = match value {
|
|
Value::Versioned {
|
|
type_name,
|
|
version,
|
|
value,
|
|
} => (type_name, version, *value),
|
|
other => return Ok(other), // Non-versioned values don't need migration
|
|
};
|
|
|
|
if current_version == target_version {
|
|
return Ok(Value::Versioned {
|
|
type_name,
|
|
version: target_version,
|
|
value: Box::new(inner_value),
|
|
});
|
|
}
|
|
|
|
if current_version > target_version {
|
|
return Err(RuntimeError {
|
|
message: format!(
|
|
"Cannot downgrade {} from @v{} to @v{}",
|
|
type_name, current_version, target_version
|
|
),
|
|
span: None,
|
|
});
|
|
}
|
|
|
|
// Migrate step by step: v1 -> v2 -> v3 -> ... -> target
|
|
let mut current_value = inner_value;
|
|
let mut current_ver = current_version;
|
|
|
|
while current_ver < target_version {
|
|
let next_ver = current_ver + 1;
|
|
|
|
// Look up the migration
|
|
let migration = self
|
|
.migrations
|
|
.get(&type_name)
|
|
.and_then(|m| m.get(¤t_ver))
|
|
.cloned();
|
|
|
|
match migration {
|
|
Some(stored_migration) => {
|
|
// Execute the migration
|
|
let migration_env = stored_migration.env.clone();
|
|
migration_env.define("old", current_value.clone());
|
|
|
|
current_value = self.eval_expr(&stored_migration.body, &migration_env)?;
|
|
current_ver = next_ver;
|
|
}
|
|
None => {
|
|
// No explicit migration - try auto-migration (just pass through)
|
|
// In a full implementation, we'd check compatibility here
|
|
current_ver = next_ver;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(Value::Versioned {
|
|
type_name,
|
|
version: target_version,
|
|
value: Box::new(current_value),
|
|
})
|
|
}
|
|
|
|
fn add_builtins(env: &Env) {
|
|
// Option constructors
|
|
env.define(
|
|
"None",
|
|
Value::Constructor {
|
|
name: "None".to_string(),
|
|
fields: Vec::new(),
|
|
},
|
|
);
|
|
env.define(
|
|
"Some",
|
|
Value::Constructor {
|
|
name: "Some".to_string(),
|
|
fields: Vec::new(), // Will accumulate args when called
|
|
},
|
|
);
|
|
|
|
// Result constructors
|
|
env.define(
|
|
"Ok",
|
|
Value::Constructor {
|
|
name: "Ok".to_string(),
|
|
fields: Vec::new(),
|
|
},
|
|
);
|
|
env.define(
|
|
"Err",
|
|
Value::Constructor {
|
|
name: "Err".to_string(),
|
|
fields: Vec::new(),
|
|
},
|
|
);
|
|
|
|
// List module (as a record with function fields)
|
|
let list_module = Value::Record(HashMap::from([
|
|
("map".to_string(), Value::Builtin(BuiltinFn::ListMap)),
|
|
("filter".to_string(), Value::Builtin(BuiltinFn::ListFilter)),
|
|
("fold".to_string(), Value::Builtin(BuiltinFn::ListFold)),
|
|
("head".to_string(), Value::Builtin(BuiltinFn::ListHead)),
|
|
("tail".to_string(), Value::Builtin(BuiltinFn::ListTail)),
|
|
("concat".to_string(), Value::Builtin(BuiltinFn::ListConcat)),
|
|
(
|
|
"reverse".to_string(),
|
|
Value::Builtin(BuiltinFn::ListReverse),
|
|
),
|
|
("length".to_string(), Value::Builtin(BuiltinFn::ListLength)),
|
|
("get".to_string(), Value::Builtin(BuiltinFn::ListGet)),
|
|
("range".to_string(), Value::Builtin(BuiltinFn::ListRange)),
|
|
]));
|
|
env.define("List", list_module);
|
|
|
|
// String module
|
|
let string_module = Value::Record(HashMap::from([
|
|
("split".to_string(), Value::Builtin(BuiltinFn::StringSplit)),
|
|
("join".to_string(), Value::Builtin(BuiltinFn::StringJoin)),
|
|
("trim".to_string(), Value::Builtin(BuiltinFn::StringTrim)),
|
|
(
|
|
"contains".to_string(),
|
|
Value::Builtin(BuiltinFn::StringContains),
|
|
),
|
|
(
|
|
"replace".to_string(),
|
|
Value::Builtin(BuiltinFn::StringReplace),
|
|
),
|
|
(
|
|
"length".to_string(),
|
|
Value::Builtin(BuiltinFn::StringLength),
|
|
),
|
|
("chars".to_string(), Value::Builtin(BuiltinFn::StringChars)),
|
|
("lines".to_string(), Value::Builtin(BuiltinFn::StringLines)),
|
|
]));
|
|
env.define("String", string_module);
|
|
|
|
// Option module (functions, not constructors)
|
|
let option_module = Value::Record(HashMap::from([
|
|
("map".to_string(), Value::Builtin(BuiltinFn::OptionMap)),
|
|
(
|
|
"flatMap".to_string(),
|
|
Value::Builtin(BuiltinFn::OptionFlatMap),
|
|
),
|
|
(
|
|
"getOrElse".to_string(),
|
|
Value::Builtin(BuiltinFn::OptionGetOrElse),
|
|
),
|
|
(
|
|
"isSome".to_string(),
|
|
Value::Builtin(BuiltinFn::OptionIsSome),
|
|
),
|
|
(
|
|
"isNone".to_string(),
|
|
Value::Builtin(BuiltinFn::OptionIsNone),
|
|
),
|
|
]));
|
|
env.define("Option", option_module);
|
|
|
|
// Result module
|
|
let result_module = Value::Record(HashMap::from([
|
|
("map".to_string(), Value::Builtin(BuiltinFn::ResultMap)),
|
|
(
|
|
"flatMap".to_string(),
|
|
Value::Builtin(BuiltinFn::ResultFlatMap),
|
|
),
|
|
(
|
|
"getOrElse".to_string(),
|
|
Value::Builtin(BuiltinFn::ResultGetOrElse),
|
|
),
|
|
("isOk".to_string(), Value::Builtin(BuiltinFn::ResultIsOk)),
|
|
("isErr".to_string(), Value::Builtin(BuiltinFn::ResultIsErr)),
|
|
]));
|
|
env.define("Result", result_module);
|
|
|
|
// Utility functions
|
|
env.define("print", Value::Builtin(BuiltinFn::Print));
|
|
env.define("toString", Value::Builtin(BuiltinFn::ToString));
|
|
env.define("typeOf", Value::Builtin(BuiltinFn::TypeOf));
|
|
}
|
|
|
|
/// Execute a program
|
|
pub fn run(&mut self, program: &Program) -> Result<Value, RuntimeError> {
|
|
let mut last_value = Value::Unit;
|
|
|
|
for decl in &program.declarations {
|
|
last_value = self.eval_declaration(decl)?;
|
|
}
|
|
|
|
Ok(last_value)
|
|
}
|
|
|
|
/// Execute a program with module support
|
|
pub fn run_with_modules(
|
|
&mut self,
|
|
program: &Program,
|
|
loader: &crate::modules::ModuleLoader,
|
|
) -> Result<Value, RuntimeError> {
|
|
// Process imports first
|
|
self.load_imports(&program.imports, loader)?;
|
|
|
|
// Then run the declarations
|
|
self.run(program)
|
|
}
|
|
|
|
/// Load imports into the environment
|
|
pub fn load_imports(
|
|
&mut self,
|
|
imports: &[ImportDecl],
|
|
loader: &crate::modules::ModuleLoader,
|
|
) -> Result<(), RuntimeError> {
|
|
use crate::modules::ImportKind;
|
|
|
|
let resolved = loader.resolve_imports(imports).map_err(|e| RuntimeError {
|
|
message: e.message,
|
|
span: None,
|
|
})?;
|
|
|
|
for (name, import) in resolved {
|
|
match import.kind {
|
|
ImportKind::Module => {
|
|
// Import as a module object - create a record with all exports
|
|
let module =
|
|
loader
|
|
.get_module(&import.module_path)
|
|
.ok_or_else(|| RuntimeError {
|
|
message: format!("Module '{}' not found", import.module_path),
|
|
span: None,
|
|
})?;
|
|
|
|
// Create a temporary interpreter to evaluate the module
|
|
// Clone the module.program to avoid borrow issues
|
|
let program = module.program.clone();
|
|
let exports = module.exports.clone();
|
|
|
|
let mut module_interp = Interpreter::new();
|
|
module_interp.run_with_modules(&program, loader)?;
|
|
|
|
// Collect all public values into a record
|
|
let mut module_record = HashMap::new();
|
|
for export_name in &exports {
|
|
if let Some(value) = module_interp.global_env.get(export_name) {
|
|
module_record.insert(export_name.clone(), value);
|
|
}
|
|
}
|
|
|
|
self.global_env.define(&name, Value::Record(module_record));
|
|
}
|
|
ImportKind::Direct => {
|
|
// Import a specific name directly
|
|
let module =
|
|
loader
|
|
.get_module(&import.module_path)
|
|
.ok_or_else(|| RuntimeError {
|
|
message: format!("Module '{}' not found", import.module_path),
|
|
span: None,
|
|
})?;
|
|
|
|
// Clone the module data to avoid borrow issues
|
|
let program = module.program.clone();
|
|
let import_name = import.name.clone();
|
|
let module_path = import.module_path.clone();
|
|
|
|
// Evaluate the module to get the value
|
|
let mut module_interp = Interpreter::new();
|
|
module_interp.run_with_modules(&program, loader)?;
|
|
|
|
if let Some(value) = module_interp.global_env.get(&import_name) {
|
|
self.global_env.define(&name, value);
|
|
} else {
|
|
return Err(RuntimeError {
|
|
message: format!(
|
|
"'{}' not found in module '{}'",
|
|
import_name, module_path
|
|
),
|
|
span: None,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Evaluate a declaration
|
|
fn eval_declaration(&mut self, decl: &Declaration) -> Result<Value, RuntimeError> {
|
|
match decl {
|
|
Declaration::Function(func) => {
|
|
let closure = Closure {
|
|
params: func.params.iter().map(|p| p.name.name.clone()).collect(),
|
|
body: func.body.clone(),
|
|
env: self.global_env.clone(),
|
|
};
|
|
let value = Value::Function(Rc::new(closure));
|
|
self.global_env.define(&func.name.name, value.clone());
|
|
Ok(value)
|
|
}
|
|
|
|
Declaration::Let(let_decl) => {
|
|
let value = self.eval_expr(&let_decl.value, &self.global_env.clone())?;
|
|
self.global_env.define(&let_decl.name.name, value.clone());
|
|
Ok(value)
|
|
}
|
|
|
|
Declaration::Handler(handler) => {
|
|
let mut implementations = HashMap::new();
|
|
for impl_ in &handler.implementations {
|
|
implementations.insert(impl_.op_name.name.clone(), impl_.clone());
|
|
}
|
|
|
|
let handler_value = HandlerValue {
|
|
effect: handler.effect.name.clone(),
|
|
implementations,
|
|
env: self.global_env.clone(),
|
|
};
|
|
|
|
let value = Value::Handler(Rc::new(handler_value));
|
|
self.global_env.define(&handler.name.name, value.clone());
|
|
Ok(value)
|
|
}
|
|
|
|
Declaration::Type(type_decl) => {
|
|
// Register ADT constructors if this is an enum type
|
|
if let crate::ast::TypeDef::Enum(variants) = &type_decl.definition {
|
|
for variant in variants {
|
|
let constructor = Value::Constructor {
|
|
name: variant.name.name.clone(),
|
|
fields: Vec::new(),
|
|
};
|
|
self.global_env.define(&variant.name.name, constructor);
|
|
}
|
|
}
|
|
Ok(Value::Unit)
|
|
}
|
|
|
|
Declaration::Effect(_) | Declaration::Trait(_) | Declaration::Impl(_) => {
|
|
// These are compile-time only
|
|
Ok(Value::Unit)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Evaluate an expression with tail call optimization (trampoline)
|
|
fn eval_expr(&mut self, expr: &Expr, env: &Env) -> Result<Value, RuntimeError> {
|
|
let mut result = self.eval_expr_inner(expr, env)?;
|
|
|
|
// Trampoline loop for tail call optimization
|
|
loop {
|
|
match result {
|
|
EvalResult::Value(v) => return Ok(v),
|
|
EvalResult::Effect(req) => {
|
|
// Handle the effect
|
|
return self.handle_effect(req);
|
|
}
|
|
EvalResult::TailCall { func, args, span } => {
|
|
// Continue the tail call without growing the stack
|
|
result = self.eval_call(func, args, span)?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn eval_expr_inner(&mut self, expr: &Expr, env: &Env) -> Result<EvalResult, RuntimeError> {
|
|
self.eval_expr_tail(expr, env, false)
|
|
}
|
|
|
|
/// Evaluate an expression, with tail position tracking for TCO
|
|
fn eval_expr_tail(&mut self, expr: &Expr, env: &Env, tail: bool) -> Result<EvalResult, RuntimeError> {
|
|
match expr {
|
|
Expr::Literal(lit) => Ok(EvalResult::Value(self.eval_literal(lit))),
|
|
|
|
Expr::Var(ident) => match env.get(&ident.name) {
|
|
Some(value) => Ok(EvalResult::Value(value)),
|
|
None => Err(RuntimeError {
|
|
message: format!("Undefined variable: {}", ident.name),
|
|
span: Some(ident.span),
|
|
}),
|
|
},
|
|
|
|
Expr::BinaryOp {
|
|
op,
|
|
left,
|
|
right,
|
|
span,
|
|
} => {
|
|
let left_val = self.eval_expr(left, env)?;
|
|
let right_val = self.eval_expr(right, env)?;
|
|
Ok(EvalResult::Value(
|
|
self.eval_binary_op(*op, left_val, right_val, *span)?,
|
|
))
|
|
}
|
|
|
|
Expr::UnaryOp { op, operand, span } => {
|
|
let val = self.eval_expr(operand, env)?;
|
|
Ok(EvalResult::Value(self.eval_unary_op(*op, val, *span)?))
|
|
}
|
|
|
|
Expr::Call { func, args, span } => {
|
|
let func_val = self.eval_expr(func, env)?;
|
|
let arg_vals: Vec<Value> = args
|
|
.iter()
|
|
.map(|a| self.eval_expr(a, env))
|
|
.collect::<Result<_, _>>()?;
|
|
|
|
// If we're in tail position, return TailCall for trampoline
|
|
if tail {
|
|
Ok(EvalResult::TailCall {
|
|
func: func_val,
|
|
args: arg_vals,
|
|
span: *span,
|
|
})
|
|
} else {
|
|
self.eval_call(func_val, arg_vals, *span)
|
|
}
|
|
}
|
|
|
|
Expr::EffectOp {
|
|
effect,
|
|
operation,
|
|
args,
|
|
span,
|
|
} => {
|
|
// Check if this is a module call instead of an effect operation
|
|
// This includes stdlib modules (List, String, etc.) and user-imported modules
|
|
if let Some(module_val) = env.get(&effect.name) {
|
|
if let Value::Record(fields) = module_val {
|
|
if let Some(func) = fields.get(&operation.name) {
|
|
let arg_vals: Vec<Value> = args
|
|
.iter()
|
|
.map(|a| self.eval_expr(a, env))
|
|
.collect::<Result<_, _>>()?;
|
|
return self.eval_call(func.clone(), arg_vals, *span);
|
|
} else {
|
|
return Err(RuntimeError {
|
|
message: format!(
|
|
"Module '{}' has no member '{}'",
|
|
effect.name, operation.name
|
|
),
|
|
span: Some(*span),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
let arg_vals: Vec<Value> = args
|
|
.iter()
|
|
.map(|a| self.eval_expr(a, env))
|
|
.collect::<Result<_, _>>()?;
|
|
|
|
// Create effect request
|
|
let request = EffectRequest {
|
|
effect: effect.name.clone(),
|
|
operation: operation.name.clone(),
|
|
args: arg_vals,
|
|
continuation: Continuation::new(),
|
|
};
|
|
|
|
Ok(EvalResult::Effect(request))
|
|
}
|
|
|
|
Expr::Field {
|
|
object,
|
|
field,
|
|
span,
|
|
} => {
|
|
let obj_val = self.eval_expr(object, env)?;
|
|
match obj_val {
|
|
Value::Record(fields) => match fields.get(&field.name) {
|
|
Some(v) => Ok(EvalResult::Value(v.clone())),
|
|
None => Err(RuntimeError {
|
|
message: format!("Record has no field '{}'", field.name),
|
|
span: Some(*span),
|
|
}),
|
|
},
|
|
_ => Err(RuntimeError {
|
|
message: format!("Cannot access field on {}", obj_val.type_name()),
|
|
span: Some(*span),
|
|
}),
|
|
}
|
|
}
|
|
|
|
Expr::Lambda { params, body, .. } => {
|
|
let closure = Closure {
|
|
params: params.iter().map(|p| p.name.name.clone()).collect(),
|
|
body: (**body).clone(),
|
|
env: env.clone(),
|
|
};
|
|
Ok(EvalResult::Value(Value::Function(Rc::new(closure))))
|
|
}
|
|
|
|
Expr::Let {
|
|
name, value, body, ..
|
|
} => {
|
|
let val = self.eval_expr(value, env)?;
|
|
let new_env = env.extend();
|
|
new_env.define(&name.name, val);
|
|
// Body of let is in tail position if the let itself is
|
|
self.eval_expr_tail(body, &new_env, tail)
|
|
}
|
|
|
|
Expr::If {
|
|
condition,
|
|
then_branch,
|
|
else_branch,
|
|
span,
|
|
} => {
|
|
let cond_val = self.eval_expr(condition, env)?;
|
|
match cond_val {
|
|
// Branches are in tail position if the if itself is
|
|
Value::Bool(true) => self.eval_expr_tail(then_branch, env, tail),
|
|
Value::Bool(false) => self.eval_expr_tail(else_branch, env, tail),
|
|
_ => Err(RuntimeError {
|
|
message: format!("If condition must be Bool, got {}", cond_val.type_name()),
|
|
span: Some(*span),
|
|
}),
|
|
}
|
|
}
|
|
|
|
Expr::Match {
|
|
scrutinee,
|
|
arms,
|
|
span,
|
|
} => {
|
|
let val = self.eval_expr(scrutinee, env)?;
|
|
// Match arms are in tail position if the match itself is
|
|
self.eval_match(val, arms, env, *span, tail)
|
|
}
|
|
|
|
Expr::Block {
|
|
statements, result, ..
|
|
} => {
|
|
let block_env = env.extend();
|
|
for stmt in statements {
|
|
match stmt {
|
|
Statement::Expr(e) => {
|
|
self.eval_expr(e, &block_env)?;
|
|
}
|
|
Statement::Let { name, value, .. } => {
|
|
let val = self.eval_expr(value, &block_env)?;
|
|
block_env.define(&name.name, val);
|
|
}
|
|
}
|
|
}
|
|
// Block result is in tail position if the block itself is
|
|
self.eval_expr_tail(result, &block_env, tail)
|
|
}
|
|
|
|
Expr::Record { fields, .. } => {
|
|
let mut record = HashMap::new();
|
|
for (name, expr) in fields {
|
|
let val = self.eval_expr(expr, env)?;
|
|
record.insert(name.name.clone(), val);
|
|
}
|
|
Ok(EvalResult::Value(Value::Record(record)))
|
|
}
|
|
|
|
Expr::Tuple { elements, .. } => {
|
|
let vals: Vec<Value> = elements
|
|
.iter()
|
|
.map(|e| self.eval_expr(e, env))
|
|
.collect::<Result<_, _>>()?;
|
|
Ok(EvalResult::Value(Value::Tuple(vals)))
|
|
}
|
|
|
|
Expr::List { elements, .. } => {
|
|
let vals: Vec<Value> = elements
|
|
.iter()
|
|
.map(|e| self.eval_expr(e, env))
|
|
.collect::<Result<_, _>>()?;
|
|
Ok(EvalResult::Value(Value::List(vals)))
|
|
}
|
|
|
|
Expr::Run {
|
|
expr,
|
|
handlers,
|
|
span,
|
|
} => self.eval_run(expr, handlers, env, *span),
|
|
|
|
Expr::Resume { value, span } => Err(RuntimeError {
|
|
message: "Resume called outside of handler".to_string(),
|
|
span: Some(*span),
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn eval_literal(&self, lit: &Literal) -> Value {
|
|
match &lit.kind {
|
|
LiteralKind::Int(n) => Value::Int(*n),
|
|
LiteralKind::Float(f) => Value::Float(*f),
|
|
LiteralKind::String(s) => Value::String(s.clone()),
|
|
LiteralKind::Char(c) => Value::Char(*c),
|
|
LiteralKind::Bool(b) => Value::Bool(*b),
|
|
LiteralKind::Unit => Value::Unit,
|
|
}
|
|
}
|
|
|
|
fn eval_binary_op(
|
|
&mut self,
|
|
op: BinaryOp,
|
|
left: Value,
|
|
right: Value,
|
|
span: Span,
|
|
) -> Result<Value, RuntimeError> {
|
|
match op {
|
|
BinaryOp::Add => match (left, right) {
|
|
(Value::Int(a), Value::Int(b)) => Ok(Value::Int(a + b)),
|
|
(Value::Float(a), Value::Float(b)) => Ok(Value::Float(a + b)),
|
|
(Value::String(a), Value::String(b)) => Ok(Value::String(a + &b)),
|
|
(l, r) => Err(RuntimeError {
|
|
message: format!("Cannot add {} and {}", l.type_name(), r.type_name()),
|
|
span: Some(span),
|
|
}),
|
|
},
|
|
BinaryOp::Sub => match (left, right) {
|
|
(Value::Int(a), Value::Int(b)) => Ok(Value::Int(a - b)),
|
|
(Value::Float(a), Value::Float(b)) => Ok(Value::Float(a - b)),
|
|
(l, r) => Err(RuntimeError {
|
|
message: format!("Cannot subtract {} and {}", l.type_name(), r.type_name()),
|
|
span: Some(span),
|
|
}),
|
|
},
|
|
BinaryOp::Mul => match (left, right) {
|
|
(Value::Int(a), Value::Int(b)) => Ok(Value::Int(a * b)),
|
|
(Value::Float(a), Value::Float(b)) => Ok(Value::Float(a * b)),
|
|
(l, r) => Err(RuntimeError {
|
|
message: format!("Cannot multiply {} and {}", l.type_name(), r.type_name()),
|
|
span: Some(span),
|
|
}),
|
|
},
|
|
BinaryOp::Div => match (left, right) {
|
|
(Value::Int(a), Value::Int(b)) => {
|
|
if b == 0 {
|
|
Err(RuntimeError {
|
|
message: "Division by zero".to_string(),
|
|
span: Some(span),
|
|
})
|
|
} else {
|
|
Ok(Value::Int(a / b))
|
|
}
|
|
}
|
|
(Value::Float(a), Value::Float(b)) => Ok(Value::Float(a / b)),
|
|
(l, r) => Err(RuntimeError {
|
|
message: format!("Cannot divide {} and {}", l.type_name(), r.type_name()),
|
|
span: Some(span),
|
|
}),
|
|
},
|
|
BinaryOp::Mod => match (left, right) {
|
|
(Value::Int(a), Value::Int(b)) => {
|
|
if b == 0 {
|
|
Err(RuntimeError {
|
|
message: "Modulo by zero".to_string(),
|
|
span: Some(span),
|
|
})
|
|
} else {
|
|
Ok(Value::Int(a % b))
|
|
}
|
|
}
|
|
(l, r) => Err(RuntimeError {
|
|
message: format!("Cannot modulo {} and {}", l.type_name(), r.type_name()),
|
|
span: Some(span),
|
|
}),
|
|
},
|
|
BinaryOp::Eq => Ok(Value::Bool(self.values_equal(&left, &right))),
|
|
BinaryOp::Ne => Ok(Value::Bool(!self.values_equal(&left, &right))),
|
|
BinaryOp::Lt => match (left, right) {
|
|
(Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a < b)),
|
|
(Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a < b)),
|
|
(Value::String(a), Value::String(b)) => Ok(Value::Bool(a < b)),
|
|
(l, r) => Err(RuntimeError {
|
|
message: format!("Cannot compare {} and {}", l.type_name(), r.type_name()),
|
|
span: Some(span),
|
|
}),
|
|
},
|
|
BinaryOp::Le => match (left, right) {
|
|
(Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a <= b)),
|
|
(Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a <= b)),
|
|
(Value::String(a), Value::String(b)) => Ok(Value::Bool(a <= b)),
|
|
(l, r) => Err(RuntimeError {
|
|
message: format!("Cannot compare {} and {}", l.type_name(), r.type_name()),
|
|
span: Some(span),
|
|
}),
|
|
},
|
|
BinaryOp::Gt => match (left, right) {
|
|
(Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a > b)),
|
|
(Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a > b)),
|
|
(Value::String(a), Value::String(b)) => Ok(Value::Bool(a > b)),
|
|
(l, r) => Err(RuntimeError {
|
|
message: format!("Cannot compare {} and {}", l.type_name(), r.type_name()),
|
|
span: Some(span),
|
|
}),
|
|
},
|
|
BinaryOp::Ge => match (left, right) {
|
|
(Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a >= b)),
|
|
(Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a >= b)),
|
|
(Value::String(a), Value::String(b)) => Ok(Value::Bool(a >= b)),
|
|
(l, r) => Err(RuntimeError {
|
|
message: format!("Cannot compare {} and {}", l.type_name(), r.type_name()),
|
|
span: Some(span),
|
|
}),
|
|
},
|
|
BinaryOp::And => match (left, right) {
|
|
(Value::Bool(a), Value::Bool(b)) => Ok(Value::Bool(a && b)),
|
|
(l, r) => Err(RuntimeError {
|
|
message: format!("Cannot 'and' {} and {}", l.type_name(), r.type_name()),
|
|
span: Some(span),
|
|
}),
|
|
},
|
|
BinaryOp::Or => match (left, right) {
|
|
(Value::Bool(a), Value::Bool(b)) => Ok(Value::Bool(a || b)),
|
|
(l, r) => Err(RuntimeError {
|
|
message: format!("Cannot 'or' {} and {}", l.type_name(), r.type_name()),
|
|
span: Some(span),
|
|
}),
|
|
},
|
|
BinaryOp::Pipe => {
|
|
// a |> f means f(a)
|
|
self.eval_call_to_value(right, vec![left], span)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn eval_unary_op(&self, op: UnaryOp, val: Value, span: Span) -> Result<Value, RuntimeError> {
|
|
match op {
|
|
UnaryOp::Neg => match val {
|
|
Value::Int(n) => Ok(Value::Int(-n)),
|
|
Value::Float(f) => Ok(Value::Float(-f)),
|
|
v => Err(RuntimeError {
|
|
message: format!("Cannot negate {}", v.type_name()),
|
|
span: Some(span),
|
|
}),
|
|
},
|
|
UnaryOp::Not => match val {
|
|
Value::Bool(b) => Ok(Value::Bool(!b)),
|
|
v => Err(RuntimeError {
|
|
message: format!("Cannot negate {}", v.type_name()),
|
|
span: Some(span),
|
|
}),
|
|
},
|
|
}
|
|
}
|
|
|
|
fn eval_call(
|
|
&mut self,
|
|
func: Value,
|
|
args: Vec<Value>,
|
|
span: Span,
|
|
) -> Result<EvalResult, RuntimeError> {
|
|
match func {
|
|
Value::Function(closure) => {
|
|
if closure.params.len() != args.len() {
|
|
return Err(RuntimeError {
|
|
message: format!(
|
|
"Function expects {} arguments, got {}",
|
|
closure.params.len(),
|
|
args.len()
|
|
),
|
|
span: Some(span),
|
|
});
|
|
}
|
|
|
|
let call_env = closure.env.extend();
|
|
for (param, arg) in closure.params.iter().zip(args) {
|
|
call_env.define(param, arg);
|
|
}
|
|
|
|
// Evaluate body in tail position for TCO
|
|
self.eval_expr_tail(&closure.body, &call_env, true)
|
|
}
|
|
Value::Constructor { name, fields } => {
|
|
// Constructor application
|
|
let mut new_fields = fields;
|
|
new_fields.extend(args);
|
|
Ok(EvalResult::Value(Value::Constructor {
|
|
name,
|
|
fields: new_fields,
|
|
}))
|
|
}
|
|
Value::Builtin(builtin) => self.eval_builtin(builtin, args, span),
|
|
v => Err(RuntimeError {
|
|
message: format!("Cannot call {}", v.type_name()),
|
|
span: Some(span),
|
|
}),
|
|
}
|
|
}
|
|
|
|
/// Fully evaluate a call, handling any tail calls via trampoline.
|
|
/// Used by builtins that need to call user functions and get a value back.
|
|
fn eval_call_to_value(
|
|
&mut self,
|
|
func: Value,
|
|
args: Vec<Value>,
|
|
span: Span,
|
|
) -> Result<Value, RuntimeError> {
|
|
let mut result = self.eval_call(func, args, span)?;
|
|
loop {
|
|
match result {
|
|
EvalResult::Value(v) => return Ok(v),
|
|
EvalResult::Effect(_) => {
|
|
return Err(RuntimeError {
|
|
message: "Effect in callback not supported".to_string(),
|
|
span: Some(span),
|
|
});
|
|
}
|
|
EvalResult::TailCall { func, args, span } => {
|
|
result = self.eval_call(func, args, span)?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn eval_builtin(
|
|
&mut self,
|
|
builtin: BuiltinFn,
|
|
args: Vec<Value>,
|
|
span: Span,
|
|
) -> Result<EvalResult, RuntimeError> {
|
|
let err = |msg: &str| RuntimeError {
|
|
message: msg.to_string(),
|
|
span: Some(span),
|
|
};
|
|
|
|
match builtin {
|
|
// List operations
|
|
BuiltinFn::ListMap => {
|
|
let (list, func) =
|
|
Self::expect_args_2::<Vec<Value>, Value>(&args, "List.map", span)?;
|
|
let mut result = Vec::with_capacity(list.len());
|
|
for item in list {
|
|
let v = self.eval_call_to_value(func.clone(), vec![item], span)?;
|
|
result.push(v);
|
|
}
|
|
Ok(EvalResult::Value(Value::List(result)))
|
|
}
|
|
|
|
BuiltinFn::ListFilter => {
|
|
let (list, func) =
|
|
Self::expect_args_2::<Vec<Value>, Value>(&args, "List.filter", span)?;
|
|
let mut result = Vec::new();
|
|
for item in list {
|
|
let v = self.eval_call_to_value(func.clone(), vec![item.clone()], span)?;
|
|
match v {
|
|
Value::Bool(true) => result.push(item),
|
|
Value::Bool(false) => {}
|
|
_ => {
|
|
return Err(err(&format!(
|
|
"List.filter predicate must return Bool, got {}",
|
|
v.type_name()
|
|
)))
|
|
}
|
|
}
|
|
}
|
|
Ok(EvalResult::Value(Value::List(result)))
|
|
}
|
|
|
|
BuiltinFn::ListFold => {
|
|
// List.fold(list, initial, fn(acc, item) => ...)
|
|
if args.len() != 3 {
|
|
return Err(err(
|
|
"List.fold requires 3 arguments: list, initial, reducer",
|
|
));
|
|
}
|
|
let list = match &args[0] {
|
|
Value::List(l) => l.clone(),
|
|
v => {
|
|
return Err(err(&format!(
|
|
"List.fold expects List as first argument, got {}",
|
|
v.type_name()
|
|
)))
|
|
}
|
|
};
|
|
let mut acc = args[1].clone();
|
|
let func = args[2].clone();
|
|
|
|
for item in list {
|
|
acc = self.eval_call_to_value(func.clone(), vec![acc, item], span)?;
|
|
}
|
|
Ok(EvalResult::Value(acc))
|
|
}
|
|
|
|
BuiltinFn::ListHead => {
|
|
let list = Self::expect_arg_1::<Vec<Value>>(&args, "List.head", span)?;
|
|
match list.first() {
|
|
Some(v) => Ok(EvalResult::Value(Value::Constructor {
|
|
name: "Some".to_string(),
|
|
fields: vec![v.clone()],
|
|
})),
|
|
None => Ok(EvalResult::Value(Value::Constructor {
|
|
name: "None".to_string(),
|
|
fields: vec![],
|
|
})),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::ListTail => {
|
|
let list = Self::expect_arg_1::<Vec<Value>>(&args, "List.tail", span)?;
|
|
if list.is_empty() {
|
|
Ok(EvalResult::Value(Value::Constructor {
|
|
name: "None".to_string(),
|
|
fields: vec![],
|
|
}))
|
|
} else {
|
|
Ok(EvalResult::Value(Value::Constructor {
|
|
name: "Some".to_string(),
|
|
fields: vec![Value::List(list[1..].to_vec())],
|
|
}))
|
|
}
|
|
}
|
|
|
|
BuiltinFn::ListConcat => {
|
|
let (list1, list2) =
|
|
Self::expect_args_2::<Vec<Value>, Vec<Value>>(&args, "List.concat", span)?;
|
|
let mut result = list1;
|
|
result.extend(list2);
|
|
Ok(EvalResult::Value(Value::List(result)))
|
|
}
|
|
|
|
BuiltinFn::ListReverse => {
|
|
let mut list = Self::expect_arg_1::<Vec<Value>>(&args, "List.reverse", span)?;
|
|
list.reverse();
|
|
Ok(EvalResult::Value(Value::List(list)))
|
|
}
|
|
|
|
BuiltinFn::ListLength => {
|
|
let list = Self::expect_arg_1::<Vec<Value>>(&args, "List.length", span)?;
|
|
Ok(EvalResult::Value(Value::Int(list.len() as i64)))
|
|
}
|
|
|
|
BuiltinFn::ListGet => {
|
|
let (list, idx) = Self::expect_args_2::<Vec<Value>, i64>(&args, "List.get", span)?;
|
|
if idx < 0 || idx as usize >= list.len() {
|
|
Ok(EvalResult::Value(Value::Constructor {
|
|
name: "None".to_string(),
|
|
fields: vec![],
|
|
}))
|
|
} else {
|
|
Ok(EvalResult::Value(Value::Constructor {
|
|
name: "Some".to_string(),
|
|
fields: vec![list[idx as usize].clone()],
|
|
}))
|
|
}
|
|
}
|
|
|
|
BuiltinFn::ListRange => {
|
|
let (start, end) = Self::expect_args_2::<i64, i64>(&args, "List.range", span)?;
|
|
let list: Vec<Value> = (start..end).map(Value::Int).collect();
|
|
Ok(EvalResult::Value(Value::List(list)))
|
|
}
|
|
|
|
// String operations
|
|
BuiltinFn::StringSplit => {
|
|
let (s, delim) =
|
|
Self::expect_args_2::<String, String>(&args, "String.split", span)?;
|
|
let parts: Vec<Value> = s
|
|
.split(&delim)
|
|
.map(|p| Value::String(p.to_string()))
|
|
.collect();
|
|
Ok(EvalResult::Value(Value::List(parts)))
|
|
}
|
|
|
|
BuiltinFn::StringJoin => {
|
|
let (list, sep) =
|
|
Self::expect_args_2::<Vec<Value>, String>(&args, "String.join", span)?;
|
|
let strings: Result<Vec<String>, _> = list
|
|
.iter()
|
|
.map(|v| match v {
|
|
Value::String(s) => Ok(s.clone()),
|
|
_ => Err(err("String.join requires list of strings")),
|
|
})
|
|
.collect();
|
|
Ok(EvalResult::Value(Value::String(strings?.join(&sep))))
|
|
}
|
|
|
|
BuiltinFn::StringTrim => {
|
|
let s = Self::expect_arg_1::<String>(&args, "String.trim", span)?;
|
|
Ok(EvalResult::Value(Value::String(s.trim().to_string())))
|
|
}
|
|
|
|
BuiltinFn::StringContains => {
|
|
let (s, needle) =
|
|
Self::expect_args_2::<String, String>(&args, "String.contains", span)?;
|
|
Ok(EvalResult::Value(Value::Bool(s.contains(&needle))))
|
|
}
|
|
|
|
BuiltinFn::StringReplace => {
|
|
if args.len() != 3 {
|
|
return Err(err("String.replace requires 3 arguments: string, from, to"));
|
|
}
|
|
let s = match &args[0] {
|
|
Value::String(s) => s.clone(),
|
|
v => {
|
|
return Err(err(&format!(
|
|
"String.replace expects String, got {}",
|
|
v.type_name()
|
|
)))
|
|
}
|
|
};
|
|
let from = match &args[1] {
|
|
Value::String(s) => s.clone(),
|
|
v => {
|
|
return Err(err(&format!(
|
|
"String.replace expects String, got {}",
|
|
v.type_name()
|
|
)))
|
|
}
|
|
};
|
|
let to = match &args[2] {
|
|
Value::String(s) => s.clone(),
|
|
v => {
|
|
return Err(err(&format!(
|
|
"String.replace expects String, got {}",
|
|
v.type_name()
|
|
)))
|
|
}
|
|
};
|
|
Ok(EvalResult::Value(Value::String(s.replace(&from, &to))))
|
|
}
|
|
|
|
BuiltinFn::StringLength => {
|
|
let s = Self::expect_arg_1::<String>(&args, "String.length", span)?;
|
|
Ok(EvalResult::Value(Value::Int(s.len() as i64)))
|
|
}
|
|
|
|
BuiltinFn::StringChars => {
|
|
let s = Self::expect_arg_1::<String>(&args, "String.chars", span)?;
|
|
let chars: Vec<Value> = s.chars().map(Value::Char).collect();
|
|
Ok(EvalResult::Value(Value::List(chars)))
|
|
}
|
|
|
|
BuiltinFn::StringLines => {
|
|
let s = Self::expect_arg_1::<String>(&args, "String.lines", span)?;
|
|
let lines: Vec<Value> = s.lines().map(|l| Value::String(l.to_string())).collect();
|
|
Ok(EvalResult::Value(Value::List(lines)))
|
|
}
|
|
|
|
// Option operations
|
|
BuiltinFn::OptionMap => {
|
|
let (opt, func) = Self::expect_args_2::<Value, Value>(&args, "Option.map", span)?;
|
|
match opt {
|
|
Value::Constructor { name, fields } if name == "Some" && !fields.is_empty() => {
|
|
let v = self.eval_call_to_value(func, vec![fields[0].clone()], span)?;
|
|
Ok(EvalResult::Value(Value::Constructor {
|
|
name: "Some".to_string(),
|
|
fields: vec![v],
|
|
}))
|
|
}
|
|
Value::Constructor { name, .. } if name == "None" => {
|
|
Ok(EvalResult::Value(Value::Constructor {
|
|
name: "None".to_string(),
|
|
fields: vec![],
|
|
}))
|
|
}
|
|
v => Err(err(&format!(
|
|
"Option.map expects Option, got {}",
|
|
v.type_name()
|
|
))),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::OptionFlatMap => {
|
|
let (opt, func) =
|
|
Self::expect_args_2::<Value, Value>(&args, "Option.flatMap", span)?;
|
|
match opt {
|
|
Value::Constructor { name, fields } if name == "Some" && !fields.is_empty() => {
|
|
let v = self.eval_call_to_value(func, vec![fields[0].clone()], span)?;
|
|
Ok(EvalResult::Value(v))
|
|
}
|
|
Value::Constructor { name, .. } if name == "None" => {
|
|
Ok(EvalResult::Value(Value::Constructor {
|
|
name: "None".to_string(),
|
|
fields: vec![],
|
|
}))
|
|
}
|
|
v => Err(err(&format!(
|
|
"Option.flatMap expects Option, got {}",
|
|
v.type_name()
|
|
))),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::OptionGetOrElse => {
|
|
let (opt, default) =
|
|
Self::expect_args_2::<Value, Value>(&args, "Option.getOrElse", span)?;
|
|
match opt {
|
|
Value::Constructor { name, fields } if name == "Some" && !fields.is_empty() => {
|
|
Ok(EvalResult::Value(fields[0].clone()))
|
|
}
|
|
Value::Constructor { name, .. } if name == "None" => {
|
|
Ok(EvalResult::Value(default))
|
|
}
|
|
v => Err(err(&format!(
|
|
"Option.getOrElse expects Option, got {}",
|
|
v.type_name()
|
|
))),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::OptionIsSome => {
|
|
let opt = Self::expect_arg_1::<Value>(&args, "Option.isSome", span)?;
|
|
match opt {
|
|
Value::Constructor { name, .. } if name == "Some" => {
|
|
Ok(EvalResult::Value(Value::Bool(true)))
|
|
}
|
|
Value::Constructor { name, .. } if name == "None" => {
|
|
Ok(EvalResult::Value(Value::Bool(false)))
|
|
}
|
|
v => Err(err(&format!(
|
|
"Option.isSome expects Option, got {}",
|
|
v.type_name()
|
|
))),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::OptionIsNone => {
|
|
let opt = Self::expect_arg_1::<Value>(&args, "Option.isNone", span)?;
|
|
match opt {
|
|
Value::Constructor { name, .. } if name == "None" => {
|
|
Ok(EvalResult::Value(Value::Bool(true)))
|
|
}
|
|
Value::Constructor { name, .. } if name == "Some" => {
|
|
Ok(EvalResult::Value(Value::Bool(false)))
|
|
}
|
|
v => Err(err(&format!(
|
|
"Option.isNone expects Option, got {}",
|
|
v.type_name()
|
|
))),
|
|
}
|
|
}
|
|
|
|
// Result operations
|
|
BuiltinFn::ResultMap => {
|
|
let (res, func) = Self::expect_args_2::<Value, Value>(&args, "Result.map", span)?;
|
|
match res {
|
|
Value::Constructor { name, fields } if name == "Ok" && !fields.is_empty() => {
|
|
let v = self.eval_call_to_value(func, vec![fields[0].clone()], span)?;
|
|
Ok(EvalResult::Value(Value::Constructor {
|
|
name: "Ok".to_string(),
|
|
fields: vec![v],
|
|
}))
|
|
}
|
|
Value::Constructor { name, fields } if name == "Err" => {
|
|
Ok(EvalResult::Value(Value::Constructor {
|
|
name: "Err".to_string(),
|
|
fields,
|
|
}))
|
|
}
|
|
v => Err(err(&format!(
|
|
"Result.map expects Result, got {}",
|
|
v.type_name()
|
|
))),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::ResultFlatMap => {
|
|
let (res, func) =
|
|
Self::expect_args_2::<Value, Value>(&args, "Result.flatMap", span)?;
|
|
match res {
|
|
Value::Constructor { name, fields } if name == "Ok" && !fields.is_empty() => {
|
|
let v = self.eval_call_to_value(func, vec![fields[0].clone()], span)?;
|
|
Ok(EvalResult::Value(v))
|
|
}
|
|
Value::Constructor { name, fields } if name == "Err" => {
|
|
Ok(EvalResult::Value(Value::Constructor {
|
|
name: "Err".to_string(),
|
|
fields,
|
|
}))
|
|
}
|
|
v => Err(err(&format!(
|
|
"Result.flatMap expects Result, got {}",
|
|
v.type_name()
|
|
))),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::ResultGetOrElse => {
|
|
let (res, default) =
|
|
Self::expect_args_2::<Value, Value>(&args, "Result.getOrElse", span)?;
|
|
match res {
|
|
Value::Constructor { name, fields } if name == "Ok" && !fields.is_empty() => {
|
|
Ok(EvalResult::Value(fields[0].clone()))
|
|
}
|
|
Value::Constructor { name, .. } if name == "Err" => {
|
|
Ok(EvalResult::Value(default))
|
|
}
|
|
v => Err(err(&format!(
|
|
"Result.getOrElse expects Result, got {}",
|
|
v.type_name()
|
|
))),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::ResultIsOk => {
|
|
let res = Self::expect_arg_1::<Value>(&args, "Result.isOk", span)?;
|
|
match res {
|
|
Value::Constructor { name, .. } if name == "Ok" => {
|
|
Ok(EvalResult::Value(Value::Bool(true)))
|
|
}
|
|
Value::Constructor { name, .. } if name == "Err" => {
|
|
Ok(EvalResult::Value(Value::Bool(false)))
|
|
}
|
|
v => Err(err(&format!(
|
|
"Result.isOk expects Result, got {}",
|
|
v.type_name()
|
|
))),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::ResultIsErr => {
|
|
let res = Self::expect_arg_1::<Value>(&args, "Result.isErr", span)?;
|
|
match res {
|
|
Value::Constructor { name, .. } if name == "Err" => {
|
|
Ok(EvalResult::Value(Value::Bool(true)))
|
|
}
|
|
Value::Constructor { name, .. } if name == "Ok" => {
|
|
Ok(EvalResult::Value(Value::Bool(false)))
|
|
}
|
|
v => Err(err(&format!(
|
|
"Result.isErr expects Result, got {}",
|
|
v.type_name()
|
|
))),
|
|
}
|
|
}
|
|
|
|
// Utility functions
|
|
BuiltinFn::Print => {
|
|
for arg in &args {
|
|
match arg {
|
|
Value::String(s) => print!("{}", s),
|
|
v => print!("{}", v),
|
|
}
|
|
}
|
|
println!();
|
|
Ok(EvalResult::Value(Value::Unit))
|
|
}
|
|
|
|
BuiltinFn::ToString => {
|
|
if args.len() != 1 {
|
|
return Err(err("toString requires 1 argument"));
|
|
}
|
|
// For strings, return the string itself (no quotes)
|
|
// For other values, use Display formatting
|
|
let result = match &args[0] {
|
|
Value::String(s) => s.clone(),
|
|
v => format!("{}", v),
|
|
};
|
|
Ok(EvalResult::Value(Value::String(result)))
|
|
}
|
|
|
|
BuiltinFn::TypeOf => {
|
|
if args.len() != 1 {
|
|
return Err(err("typeOf requires 1 argument"));
|
|
}
|
|
Ok(EvalResult::Value(Value::String(
|
|
args[0].type_name().to_string(),
|
|
)))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper functions for extracting typed arguments
|
|
fn expect_arg_1<T>(args: &[Value], name: &str, span: Span) -> Result<T, RuntimeError>
|
|
where
|
|
T: TryFromValue,
|
|
{
|
|
if args.len() != 1 {
|
|
return Err(RuntimeError {
|
|
message: format!("{} requires 1 argument, got {}", name, args.len()),
|
|
span: Some(span),
|
|
});
|
|
}
|
|
T::try_from_value(&args[0]).ok_or_else(|| RuntimeError {
|
|
message: format!("{} expects {} as argument", name, T::TYPE_NAME),
|
|
span: Some(span),
|
|
})
|
|
}
|
|
|
|
fn expect_args_2<T, U>(args: &[Value], name: &str, span: Span) -> Result<(T, U), RuntimeError>
|
|
where
|
|
T: TryFromValue,
|
|
U: TryFromValue,
|
|
{
|
|
if args.len() != 2 {
|
|
return Err(RuntimeError {
|
|
message: format!("{} requires 2 arguments, got {}", name, args.len()),
|
|
span: Some(span),
|
|
});
|
|
}
|
|
let a = T::try_from_value(&args[0]).ok_or_else(|| RuntimeError {
|
|
message: format!("{} expects {} as first argument", name, T::TYPE_NAME),
|
|
span: Some(span),
|
|
})?;
|
|
let b = U::try_from_value(&args[1]).ok_or_else(|| RuntimeError {
|
|
message: format!("{} expects {} as second argument", name, U::TYPE_NAME),
|
|
span: Some(span),
|
|
})?;
|
|
Ok((a, b))
|
|
}
|
|
|
|
fn eval_match(
|
|
&mut self,
|
|
val: Value,
|
|
arms: &[MatchArm],
|
|
env: &Env,
|
|
span: Span,
|
|
tail: bool,
|
|
) -> Result<EvalResult, RuntimeError> {
|
|
for arm in arms {
|
|
if let Some(bindings) = self.match_pattern(&arm.pattern, &val) {
|
|
let match_env = env.extend();
|
|
for (name, value) in bindings {
|
|
match_env.define(name, value);
|
|
}
|
|
|
|
// Check guard if present
|
|
if let Some(ref guard) = arm.guard {
|
|
let guard_val = self.eval_expr(guard, &match_env)?;
|
|
match guard_val {
|
|
Value::Bool(true) => {}
|
|
Value::Bool(false) => continue,
|
|
_ => {
|
|
return Err(RuntimeError {
|
|
message: "Match guard must be Bool".to_string(),
|
|
span: Some(arm.span),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Match arm body is in tail position if the match itself is
|
|
return self.eval_expr_tail(&arm.body, &match_env, tail);
|
|
}
|
|
}
|
|
|
|
Err(RuntimeError {
|
|
message: "No matching pattern".to_string(),
|
|
span: Some(span),
|
|
})
|
|
}
|
|
|
|
fn match_pattern(&self, pattern: &Pattern, value: &Value) -> Option<Vec<(String, Value)>> {
|
|
match pattern {
|
|
Pattern::Wildcard(_) => Some(Vec::new()),
|
|
|
|
Pattern::Var(ident) => Some(vec![(ident.name.clone(), value.clone())]),
|
|
|
|
Pattern::Literal(lit) => {
|
|
let lit_val = self.eval_literal(lit);
|
|
if self.values_equal(&lit_val, value) {
|
|
Some(Vec::new())
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
Pattern::Constructor { name, fields, .. } => match value {
|
|
Value::Constructor {
|
|
name: val_name,
|
|
fields: val_fields,
|
|
} => {
|
|
if name.name != *val_name {
|
|
return None;
|
|
}
|
|
if fields.len() != val_fields.len() {
|
|
return None;
|
|
}
|
|
let mut bindings = Vec::new();
|
|
for (pat, val) in fields.iter().zip(val_fields) {
|
|
bindings.extend(self.match_pattern(pat, val)?);
|
|
}
|
|
Some(bindings)
|
|
}
|
|
_ => None,
|
|
},
|
|
|
|
Pattern::Tuple { elements, .. } => match value {
|
|
Value::Tuple(vals) => {
|
|
if elements.len() != vals.len() {
|
|
return None;
|
|
}
|
|
let mut bindings = Vec::new();
|
|
for (pat, val) in elements.iter().zip(vals) {
|
|
bindings.extend(self.match_pattern(pat, val)?);
|
|
}
|
|
Some(bindings)
|
|
}
|
|
_ => None,
|
|
},
|
|
|
|
Pattern::Record { fields, .. } => match value {
|
|
Value::Record(val_fields) => {
|
|
let mut bindings = Vec::new();
|
|
for (name, pat) in fields {
|
|
let val = val_fields.get(&name.name)?;
|
|
bindings.extend(self.match_pattern(pat, val)?);
|
|
}
|
|
Some(bindings)
|
|
}
|
|
_ => None,
|
|
},
|
|
}
|
|
}
|
|
|
|
fn values_equal(&self, a: &Value, b: &Value) -> bool {
|
|
match (a, b) {
|
|
(Value::Int(a), Value::Int(b)) => a == b,
|
|
(Value::Float(a), Value::Float(b)) => a == b,
|
|
(Value::Bool(a), Value::Bool(b)) => a == b,
|
|
(Value::String(a), Value::String(b)) => a == b,
|
|
(Value::Char(a), Value::Char(b)) => a == b,
|
|
(Value::Unit, Value::Unit) => true,
|
|
(Value::List(a), Value::List(b)) => {
|
|
a.len() == b.len() && a.iter().zip(b).all(|(x, y)| self.values_equal(x, y))
|
|
}
|
|
(Value::Tuple(a), Value::Tuple(b)) => {
|
|
a.len() == b.len() && a.iter().zip(b).all(|(x, y)| self.values_equal(x, y))
|
|
}
|
|
(
|
|
Value::Constructor {
|
|
name: n1,
|
|
fields: f1,
|
|
},
|
|
Value::Constructor {
|
|
name: n2,
|
|
fields: f2,
|
|
},
|
|
) => {
|
|
n1 == n2
|
|
&& f1.len() == f2.len()
|
|
&& f1.iter().zip(f2).all(|(x, y)| self.values_equal(x, y))
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
fn eval_run(
|
|
&mut self,
|
|
expr: &Expr,
|
|
handlers: &[(Ident, Expr)],
|
|
env: &Env,
|
|
span: Span,
|
|
) -> Result<EvalResult, RuntimeError> {
|
|
// Evaluate handlers and push onto stack
|
|
let mut handler_values = Vec::new();
|
|
for (effect_name, handler_expr) in handlers {
|
|
let handler_val = self.eval_expr(handler_expr, env)?;
|
|
match handler_val {
|
|
Value::Handler(h) => {
|
|
if h.effect != effect_name.name {
|
|
return Err(RuntimeError {
|
|
message: format!(
|
|
"Handler for effect '{}' assigned to '{}'",
|
|
h.effect, effect_name.name
|
|
),
|
|
span: Some(span),
|
|
});
|
|
}
|
|
handler_values.push(h);
|
|
}
|
|
_ => {
|
|
return Err(RuntimeError {
|
|
message: format!(
|
|
"Expected handler for effect '{}', got {}",
|
|
effect_name.name,
|
|
handler_val.type_name()
|
|
),
|
|
span: Some(span),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Push handlers
|
|
for h in &handler_values {
|
|
self.handler_stack.push(Rc::clone(h));
|
|
}
|
|
|
|
// Evaluate expression
|
|
let result = self.eval_expr_inner(expr, env);
|
|
|
|
// Pop handlers
|
|
for _ in &handler_values {
|
|
self.handler_stack.pop();
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
fn handle_effect(&mut self, request: EffectRequest) -> Result<Value, RuntimeError> {
|
|
let trace_enabled = self.trace_effects;
|
|
let timestamp = if trace_enabled {
|
|
self.start_time.elapsed().as_micros()
|
|
} else {
|
|
0
|
|
};
|
|
|
|
// Find a handler for this effect - clone what we need to avoid borrow issues
|
|
let handler_data: Option<(Env, crate::ast::Expr, Vec<Ident>)> = self
|
|
.handler_stack
|
|
.iter()
|
|
.rev()
|
|
.find(|h| h.effect == request.effect)
|
|
.and_then(|handler| {
|
|
handler
|
|
.implementations
|
|
.get(&request.operation)
|
|
.map(|impl_| {
|
|
(
|
|
handler.env.clone(),
|
|
impl_.body.clone(),
|
|
impl_.params.clone(),
|
|
)
|
|
})
|
|
});
|
|
|
|
let result = if let Some((handler_env, body, params)) = handler_data {
|
|
let env = handler_env.extend();
|
|
for (i, param) in params.iter().enumerate() {
|
|
if i < request.args.len() {
|
|
env.define(¶m.name, request.args[i].clone());
|
|
}
|
|
}
|
|
self.eval_expr(&body, &env)
|
|
} else {
|
|
// No handler found - check for built-in effects
|
|
self.handle_builtin_effect(&request)
|
|
};
|
|
|
|
// Record trace if enabled
|
|
if trace_enabled {
|
|
self.effect_traces.push(EffectTrace {
|
|
effect: request.effect.clone(),
|
|
operation: request.operation.clone(),
|
|
args: request.args.clone(),
|
|
result: result.as_ref().ok().cloned(),
|
|
timestamp_us: timestamp,
|
|
});
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
fn handle_builtin_effect(&self, request: &EffectRequest) -> Result<Value, RuntimeError> {
|
|
match (request.effect.as_str(), request.operation.as_str()) {
|
|
("Console", "print") => {
|
|
if let Some(Value::String(s)) = request.args.first() {
|
|
println!("{}", s);
|
|
Ok(Value::Unit)
|
|
} else if let Some(v) = request.args.first() {
|
|
println!("{}", v);
|
|
Ok(Value::Unit)
|
|
} else {
|
|
Ok(Value::Unit)
|
|
}
|
|
}
|
|
("Console", "read") => {
|
|
let mut input = String::new();
|
|
std::io::stdin()
|
|
.read_line(&mut input)
|
|
.map_err(|e| RuntimeError {
|
|
message: format!("Failed to read input: {}", e),
|
|
span: None,
|
|
})?;
|
|
Ok(Value::String(input.trim().to_string()))
|
|
}
|
|
("Fail", "fail") => {
|
|
let msg = request
|
|
.args
|
|
.first()
|
|
.map(|v| format!("{}", v))
|
|
.unwrap_or_else(|| "Unknown error".to_string());
|
|
Err(RuntimeError {
|
|
message: msg,
|
|
span: None,
|
|
})
|
|
}
|
|
_ => Err(RuntimeError {
|
|
message: format!(
|
|
"Unhandled effect operation: {}.{}",
|
|
request.effect, request.operation
|
|
),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for Interpreter {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_create_versioned() {
|
|
let interp = Interpreter::new();
|
|
let record = Value::Record(
|
|
[("name".to_string(), Value::String("Alice".to_string()))]
|
|
.into_iter()
|
|
.collect(),
|
|
);
|
|
|
|
let versioned = interp.create_versioned("User", 1, record);
|
|
|
|
match versioned {
|
|
Value::Versioned {
|
|
type_name,
|
|
version,
|
|
value,
|
|
} => {
|
|
assert_eq!(type_name, "User");
|
|
assert_eq!(version, 1);
|
|
match *value {
|
|
Value::Record(fields) => match fields.get("name") {
|
|
Some(Value::String(s)) => assert_eq!(s, "Alice"),
|
|
_ => panic!("Expected name field with String value"),
|
|
},
|
|
_ => panic!("Expected Record value"),
|
|
}
|
|
}
|
|
_ => panic!("Expected Versioned value"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_migrate_non_versioned_passthrough() {
|
|
let mut interp = Interpreter::new();
|
|
let value = Value::Int(42);
|
|
|
|
let result = interp.migrate_value(value, 2).unwrap();
|
|
|
|
match result {
|
|
Value::Int(n) => assert_eq!(n, 42),
|
|
_ => panic!("Expected Int value to pass through unchanged"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_migrate_same_version() {
|
|
let mut interp = Interpreter::new();
|
|
let versioned = Value::Versioned {
|
|
type_name: "User".to_string(),
|
|
version: 2,
|
|
value: Box::new(Value::String("test".to_string())),
|
|
};
|
|
|
|
let result = interp.migrate_value(versioned, 2).unwrap();
|
|
|
|
match result {
|
|
Value::Versioned { version, .. } => assert_eq!(version, 2),
|
|
_ => panic!("Expected Versioned value"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_migrate_downgrade_error() {
|
|
let mut interp = Interpreter::new();
|
|
let versioned = Value::Versioned {
|
|
type_name: "User".to_string(),
|
|
version: 3,
|
|
value: Box::new(Value::String("test".to_string())),
|
|
};
|
|
|
|
let result = interp.migrate_value(versioned, 2);
|
|
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().message.contains("Cannot downgrade"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_migrate_with_auto_migration() {
|
|
let mut interp = Interpreter::new();
|
|
// No explicit migration registered - should auto-migrate
|
|
let versioned = Value::Versioned {
|
|
type_name: "User".to_string(),
|
|
version: 1,
|
|
value: Box::new(Value::String("test".to_string())),
|
|
};
|
|
|
|
let result = interp.migrate_value(versioned, 3).unwrap();
|
|
|
|
match result {
|
|
Value::Versioned { version, value, .. } => {
|
|
assert_eq!(version, 3);
|
|
// Value should be unchanged for auto-migration
|
|
match *value {
|
|
Value::String(s) => assert_eq!(s, "test"),
|
|
_ => panic!("Expected String value"),
|
|
}
|
|
}
|
|
_ => panic!("Expected Versioned value"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_register_and_execute_migration() {
|
|
let mut interp = Interpreter::new();
|
|
|
|
// Create a simple migration that adds a field
|
|
// Migration: old.name -> { name: old.name, email: "unknown" }
|
|
let migration_body = Expr::Record {
|
|
fields: vec![
|
|
(
|
|
Ident::new("name", Span::default()),
|
|
Expr::Field {
|
|
object: Box::new(Expr::Var(Ident::new("old", Span::default()))),
|
|
field: Ident::new("name", Span::default()),
|
|
span: Span::default(),
|
|
},
|
|
),
|
|
(
|
|
Ident::new("email", Span::default()),
|
|
Expr::Literal(Literal {
|
|
kind: LiteralKind::String("unknown@example.com".to_string()),
|
|
span: Span::default(),
|
|
}),
|
|
),
|
|
],
|
|
span: Span::default(),
|
|
};
|
|
|
|
let stored_migration = StoredMigration {
|
|
body: migration_body,
|
|
env: Env::new(),
|
|
};
|
|
|
|
interp.register_migration("User", 1, stored_migration);
|
|
|
|
// Create a v1 value
|
|
let v1_user = Value::Versioned {
|
|
type_name: "User".to_string(),
|
|
version: 1,
|
|
value: Box::new(Value::Record(
|
|
[("name".to_string(), Value::String("Alice".to_string()))]
|
|
.into_iter()
|
|
.collect(),
|
|
)),
|
|
};
|
|
|
|
// Migrate to v2
|
|
let result = interp.migrate_value(v1_user, 2).unwrap();
|
|
|
|
match result {
|
|
Value::Versioned {
|
|
type_name,
|
|
version,
|
|
value,
|
|
} => {
|
|
assert_eq!(type_name, "User");
|
|
assert_eq!(version, 2);
|
|
match *value {
|
|
Value::Record(fields) => {
|
|
match fields.get("name") {
|
|
Some(Value::String(s)) => assert_eq!(s, "Alice"),
|
|
_ => panic!("Expected name field with String value"),
|
|
}
|
|
match fields.get("email") {
|
|
Some(Value::String(s)) => assert_eq!(s, "unknown@example.com"),
|
|
_ => panic!("Expected email field with String value"),
|
|
}
|
|
}
|
|
_ => panic!("Expected Record value"),
|
|
}
|
|
}
|
|
_ => panic!("Expected Versioned value"),
|
|
}
|
|
}
|
|
}
|