Add comprehensive JSON support via the Json module: - Parse JSON strings with Json.parse() returning Result<Json, String> - Stringify with Json.stringify() and Json.prettyPrint() - Extract values with Json.get(), getIndex(), asString(), asInt(), etc. - Build JSON with constructors: Json.null(), bool(), int(), string(), array(), object() - Query with Json.isNull() and Json.keys() Includes example at examples/json.lux demonstrating building, parsing, and extracting JSON data with file I/O integration. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
3405 lines
127 KiB
Rust
3405 lines
127 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 rand::Rng;
|
|
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,
|
|
|
|
// Schema Evolution
|
|
Versioned, // Create versioned value: versioned("TypeName", 1, value)
|
|
Migrate, // Migrate to version: migrate(versionedValue, targetVersion)
|
|
GetVersion, // Get version number: getVersion(versionedValue)
|
|
|
|
// Math operations
|
|
MathAbs,
|
|
MathMin,
|
|
MathMax,
|
|
MathSqrt,
|
|
MathPow,
|
|
MathFloor,
|
|
MathCeil,
|
|
MathRound,
|
|
|
|
// Additional List operations
|
|
ListIsEmpty,
|
|
ListFind,
|
|
ListAny,
|
|
ListAll,
|
|
ListTake,
|
|
ListDrop,
|
|
|
|
// Additional String operations
|
|
StringStartsWith,
|
|
StringEndsWith,
|
|
StringToUpper,
|
|
StringToLower,
|
|
StringSubstring,
|
|
|
|
// JSON operations
|
|
JsonParse,
|
|
JsonStringify,
|
|
JsonPrettyPrint,
|
|
JsonGet,
|
|
JsonGetIndex,
|
|
JsonAsString,
|
|
JsonAsNumber,
|
|
JsonAsInt,
|
|
JsonAsBool,
|
|
JsonAsArray,
|
|
JsonIsNull,
|
|
JsonKeys,
|
|
JsonNull,
|
|
JsonBool,
|
|
JsonNumber,
|
|
JsonInt,
|
|
JsonString,
|
|
JsonArray,
|
|
JsonObject,
|
|
}
|
|
|
|
/// 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>,
|
|
},
|
|
/// JSON value (for JSON parsing/manipulation)
|
|
Json(serde_json::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",
|
|
Value::Json(_) => "Json",
|
|
}
|
|
}
|
|
|
|
/// 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)
|
|
}
|
|
Value::Json(json) => write!(f, "{}", json),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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,
|
|
},
|
|
/// Resume from a handler - the value becomes the effect operation's return value
|
|
Resume(Value),
|
|
}
|
|
|
|
/// 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>>,
|
|
/// Built-in State effect storage (uses RefCell for interior mutability)
|
|
builtin_state: RefCell<Value>,
|
|
/// Built-in Reader effect value (uses RefCell for interior mutability)
|
|
builtin_reader: RefCell<Value>,
|
|
/// Depth of handler context (> 0 means we're inside a handler body where resume is valid)
|
|
in_handler_depth: usize,
|
|
}
|
|
|
|
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(),
|
|
builtin_state: RefCell::new(Value::Unit),
|
|
builtin_reader: RefCell::new(Value::Unit),
|
|
in_handler_depth: 0,
|
|
}
|
|
}
|
|
|
|
/// Set the initial value for the built-in State effect
|
|
pub fn set_state(&self, value: Value) {
|
|
*self.builtin_state.borrow_mut() = value;
|
|
}
|
|
|
|
/// Get the current value of the built-in State effect
|
|
pub fn get_state(&self) -> Value {
|
|
self.builtin_state.borrow().clone()
|
|
}
|
|
|
|
/// Set the value for the built-in Reader effect
|
|
pub fn set_reader(&self, value: Value) {
|
|
*self.builtin_reader.borrow_mut() = value;
|
|
}
|
|
|
|
/// Get the current value of the built-in Reader effect
|
|
pub fn get_reader(&self) -> Value {
|
|
self.builtin_reader.borrow().clone()
|
|
}
|
|
|
|
/// 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)),
|
|
(
|
|
"isEmpty".to_string(),
|
|
Value::Builtin(BuiltinFn::ListIsEmpty),
|
|
),
|
|
("find".to_string(), Value::Builtin(BuiltinFn::ListFind)),
|
|
("any".to_string(), Value::Builtin(BuiltinFn::ListAny)),
|
|
("all".to_string(), Value::Builtin(BuiltinFn::ListAll)),
|
|
("take".to_string(), Value::Builtin(BuiltinFn::ListTake)),
|
|
("drop".to_string(), Value::Builtin(BuiltinFn::ListDrop)),
|
|
]));
|
|
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)),
|
|
(
|
|
"startsWith".to_string(),
|
|
Value::Builtin(BuiltinFn::StringStartsWith),
|
|
),
|
|
(
|
|
"endsWith".to_string(),
|
|
Value::Builtin(BuiltinFn::StringEndsWith),
|
|
),
|
|
(
|
|
"toUpper".to_string(),
|
|
Value::Builtin(BuiltinFn::StringToUpper),
|
|
),
|
|
(
|
|
"toLower".to_string(),
|
|
Value::Builtin(BuiltinFn::StringToLower),
|
|
),
|
|
(
|
|
"substring".to_string(),
|
|
Value::Builtin(BuiltinFn::StringSubstring),
|
|
),
|
|
]));
|
|
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));
|
|
|
|
// Schema Evolution module
|
|
let schema_module = Value::Record(HashMap::from([
|
|
(
|
|
"versioned".to_string(),
|
|
Value::Builtin(BuiltinFn::Versioned),
|
|
),
|
|
("migrate".to_string(), Value::Builtin(BuiltinFn::Migrate)),
|
|
(
|
|
"getVersion".to_string(),
|
|
Value::Builtin(BuiltinFn::GetVersion),
|
|
),
|
|
]));
|
|
env.define("Schema", schema_module);
|
|
|
|
// Math module
|
|
let math_module = Value::Record(HashMap::from([
|
|
("abs".to_string(), Value::Builtin(BuiltinFn::MathAbs)),
|
|
("min".to_string(), Value::Builtin(BuiltinFn::MathMin)),
|
|
("max".to_string(), Value::Builtin(BuiltinFn::MathMax)),
|
|
("sqrt".to_string(), Value::Builtin(BuiltinFn::MathSqrt)),
|
|
("pow".to_string(), Value::Builtin(BuiltinFn::MathPow)),
|
|
("floor".to_string(), Value::Builtin(BuiltinFn::MathFloor)),
|
|
("ceil".to_string(), Value::Builtin(BuiltinFn::MathCeil)),
|
|
("round".to_string(), Value::Builtin(BuiltinFn::MathRound)),
|
|
]));
|
|
env.define("Math", math_module);
|
|
|
|
// JSON module
|
|
let json_module = Value::Record(HashMap::from([
|
|
("parse".to_string(), Value::Builtin(BuiltinFn::JsonParse)),
|
|
("stringify".to_string(), Value::Builtin(BuiltinFn::JsonStringify)),
|
|
("prettyPrint".to_string(), Value::Builtin(BuiltinFn::JsonPrettyPrint)),
|
|
("get".to_string(), Value::Builtin(BuiltinFn::JsonGet)),
|
|
("getIndex".to_string(), Value::Builtin(BuiltinFn::JsonGetIndex)),
|
|
("asString".to_string(), Value::Builtin(BuiltinFn::JsonAsString)),
|
|
("asNumber".to_string(), Value::Builtin(BuiltinFn::JsonAsNumber)),
|
|
("asInt".to_string(), Value::Builtin(BuiltinFn::JsonAsInt)),
|
|
("asBool".to_string(), Value::Builtin(BuiltinFn::JsonAsBool)),
|
|
("asArray".to_string(), Value::Builtin(BuiltinFn::JsonAsArray)),
|
|
("isNull".to_string(), Value::Builtin(BuiltinFn::JsonIsNull)),
|
|
("keys".to_string(), Value::Builtin(BuiltinFn::JsonKeys)),
|
|
("null".to_string(), Value::Builtin(BuiltinFn::JsonNull)),
|
|
("bool".to_string(), Value::Builtin(BuiltinFn::JsonBool)),
|
|
("number".to_string(), Value::Builtin(BuiltinFn::JsonNumber)),
|
|
("int".to_string(), Value::Builtin(BuiltinFn::JsonInt)),
|
|
("string".to_string(), Value::Builtin(BuiltinFn::JsonString)),
|
|
("array".to_string(), Value::Builtin(BuiltinFn::JsonArray)),
|
|
("object".to_string(), Value::Builtin(BuiltinFn::JsonObject)),
|
|
]));
|
|
env.define("Json", json_module);
|
|
}
|
|
|
|
/// 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)?;
|
|
}
|
|
EvalResult::Resume(v) => {
|
|
// Resume propagates up - return the value
|
|
return Ok(v);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 } => {
|
|
if self.in_handler_depth > 0 {
|
|
// We're inside a handler body - evaluate the value and return Resume
|
|
let val = self.eval_expr(value, env)?;
|
|
Ok(EvalResult::Resume(val))
|
|
} else {
|
|
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)?;
|
|
}
|
|
EvalResult::Resume(v) => return Ok(v),
|
|
}
|
|
}
|
|
}
|
|
|
|
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(),
|
|
)))
|
|
}
|
|
|
|
// Schema Evolution
|
|
BuiltinFn::Versioned => {
|
|
// versioned(typeName: String, version: Int, value: Any) -> Versioned
|
|
if args.len() != 3 {
|
|
return Err(err("Schema.versioned requires 3 arguments: typeName, version, value"));
|
|
}
|
|
let type_name = match &args[0] {
|
|
Value::String(s) => s.clone(),
|
|
_ => return Err(err("Schema.versioned: first argument must be a String")),
|
|
};
|
|
let version = match &args[1] {
|
|
Value::Int(n) => *n as u32,
|
|
_ => return Err(err("Schema.versioned: second argument must be an Int")),
|
|
};
|
|
Ok(EvalResult::Value(Value::Versioned {
|
|
type_name,
|
|
version,
|
|
value: Box::new(args[2].clone()),
|
|
}))
|
|
}
|
|
|
|
BuiltinFn::Migrate => {
|
|
// migrate(value: Versioned, targetVersion: Int) -> Versioned
|
|
if args.len() != 2 {
|
|
return Err(err("Schema.migrate requires 2 arguments: value, targetVersion"));
|
|
}
|
|
let target = match &args[1] {
|
|
Value::Int(n) => *n as u32,
|
|
_ => return Err(err("Schema.migrate: second argument must be an Int")),
|
|
};
|
|
match &args[0] {
|
|
Value::Versioned { type_name, version, value } => {
|
|
if *version == target {
|
|
// Same version, return as-is
|
|
Ok(EvalResult::Value(args[0].clone()))
|
|
} else if *version < target {
|
|
// Upgrade - for now just update version (no migration logic)
|
|
Ok(EvalResult::Value(Value::Versioned {
|
|
type_name: type_name.clone(),
|
|
version: target,
|
|
value: value.clone(),
|
|
}))
|
|
} else {
|
|
Err(err(&format!(
|
|
"Cannot downgrade from version {} to {}",
|
|
version, target
|
|
)))
|
|
}
|
|
}
|
|
_ => Err(err("Schema.migrate: first argument must be a Versioned value")),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::GetVersion => {
|
|
// getVersion(value: Versioned) -> Int
|
|
if args.len() != 1 {
|
|
return Err(err("Schema.getVersion requires 1 argument"));
|
|
}
|
|
match &args[0] {
|
|
Value::Versioned { version, .. } => {
|
|
Ok(EvalResult::Value(Value::Int(*version as i64)))
|
|
}
|
|
_ => Err(err("Schema.getVersion: argument must be a Versioned value")),
|
|
}
|
|
}
|
|
|
|
// Math operations
|
|
BuiltinFn::MathAbs => {
|
|
if args.len() != 1 {
|
|
return Err(err("Math.abs requires 1 argument"));
|
|
}
|
|
match &args[0] {
|
|
Value::Int(n) => Ok(EvalResult::Value(Value::Int(n.abs()))),
|
|
Value::Float(n) => Ok(EvalResult::Value(Value::Float(n.abs()))),
|
|
v => Err(err(&format!("Math.abs expects number, got {}", v.type_name()))),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::MathMin => {
|
|
let (a, b) = Self::expect_args_2::<i64, i64>(&args, "Math.min", span)
|
|
.or_else(|_| {
|
|
// Try floats
|
|
if args.len() == 2 {
|
|
match (&args[0], &args[1]) {
|
|
(Value::Float(a), Value::Float(b)) => {
|
|
return Ok((0i64, 0i64)); // Placeholder - we'll handle below
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
Err(err("Math.min requires 2 number arguments"))
|
|
})?;
|
|
// Check if they were floats
|
|
match (&args[0], &args[1]) {
|
|
(Value::Float(a), Value::Float(b)) => {
|
|
Ok(EvalResult::Value(Value::Float(a.min(*b))))
|
|
}
|
|
(Value::Int(a), Value::Int(b)) => {
|
|
Ok(EvalResult::Value(Value::Int((*a).min(*b))))
|
|
}
|
|
_ => Err(err("Math.min requires 2 number arguments")),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::MathMax => {
|
|
match (&args[0], &args[1]) {
|
|
(Value::Float(a), Value::Float(b)) => {
|
|
Ok(EvalResult::Value(Value::Float(a.max(*b))))
|
|
}
|
|
(Value::Int(a), Value::Int(b)) => {
|
|
Ok(EvalResult::Value(Value::Int((*a).max(*b))))
|
|
}
|
|
_ => Err(err("Math.max requires 2 number arguments")),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::MathSqrt => {
|
|
if args.len() != 1 {
|
|
return Err(err("Math.sqrt requires 1 argument"));
|
|
}
|
|
match &args[0] {
|
|
Value::Int(n) => Ok(EvalResult::Value(Value::Float((*n as f64).sqrt()))),
|
|
Value::Float(n) => Ok(EvalResult::Value(Value::Float(n.sqrt()))),
|
|
v => Err(err(&format!("Math.sqrt expects number, got {}", v.type_name()))),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::MathPow => {
|
|
if args.len() != 2 {
|
|
return Err(err("Math.pow requires 2 arguments: base, exponent"));
|
|
}
|
|
match (&args[0], &args[1]) {
|
|
(Value::Int(base), Value::Int(exp)) => {
|
|
if *exp >= 0 {
|
|
Ok(EvalResult::Value(Value::Int(base.pow(*exp as u32))))
|
|
} else {
|
|
Ok(EvalResult::Value(Value::Float((*base as f64).powi(*exp as i32))))
|
|
}
|
|
}
|
|
(Value::Float(base), Value::Int(exp)) => {
|
|
Ok(EvalResult::Value(Value::Float(base.powi(*exp as i32))))
|
|
}
|
|
(Value::Float(base), Value::Float(exp)) => {
|
|
Ok(EvalResult::Value(Value::Float(base.powf(*exp))))
|
|
}
|
|
(Value::Int(base), Value::Float(exp)) => {
|
|
Ok(EvalResult::Value(Value::Float((*base as f64).powf(*exp))))
|
|
}
|
|
_ => Err(err("Math.pow requires number arguments")),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::MathFloor => {
|
|
if args.len() != 1 {
|
|
return Err(err("Math.floor requires 1 argument"));
|
|
}
|
|
match &args[0] {
|
|
Value::Float(n) => Ok(EvalResult::Value(Value::Int(n.floor() as i64))),
|
|
Value::Int(n) => Ok(EvalResult::Value(Value::Int(*n))),
|
|
v => Err(err(&format!("Math.floor expects number, got {}", v.type_name()))),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::MathCeil => {
|
|
if args.len() != 1 {
|
|
return Err(err("Math.ceil requires 1 argument"));
|
|
}
|
|
match &args[0] {
|
|
Value::Float(n) => Ok(EvalResult::Value(Value::Int(n.ceil() as i64))),
|
|
Value::Int(n) => Ok(EvalResult::Value(Value::Int(*n))),
|
|
v => Err(err(&format!("Math.ceil expects number, got {}", v.type_name()))),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::MathRound => {
|
|
if args.len() != 1 {
|
|
return Err(err("Math.round requires 1 argument"));
|
|
}
|
|
match &args[0] {
|
|
Value::Float(n) => Ok(EvalResult::Value(Value::Int(n.round() as i64))),
|
|
Value::Int(n) => Ok(EvalResult::Value(Value::Int(*n))),
|
|
v => Err(err(&format!("Math.round expects number, got {}", v.type_name()))),
|
|
}
|
|
}
|
|
|
|
// Additional List operations
|
|
BuiltinFn::ListIsEmpty => {
|
|
let list = Self::expect_arg_1::<Vec<Value>>(&args, "List.isEmpty", span)?;
|
|
Ok(EvalResult::Value(Value::Bool(list.is_empty())))
|
|
}
|
|
|
|
BuiltinFn::ListFind => {
|
|
let (list, func) = Self::expect_args_2::<Vec<Value>, Value>(&args, "List.find", span)?;
|
|
for item in list {
|
|
let v = self.eval_call_to_value(func.clone(), vec![item.clone()], span)?;
|
|
match v {
|
|
Value::Bool(true) => {
|
|
return Ok(EvalResult::Value(Value::Constructor {
|
|
name: "Some".to_string(),
|
|
fields: vec![item],
|
|
}));
|
|
}
|
|
Value::Bool(false) => {}
|
|
_ => return Err(err("List.find predicate must return Bool")),
|
|
}
|
|
}
|
|
Ok(EvalResult::Value(Value::Constructor {
|
|
name: "None".to_string(),
|
|
fields: vec![],
|
|
}))
|
|
}
|
|
|
|
BuiltinFn::ListAny => {
|
|
let (list, func) = Self::expect_args_2::<Vec<Value>, Value>(&args, "List.any", span)?;
|
|
for item in list {
|
|
let v = self.eval_call_to_value(func.clone(), vec![item], span)?;
|
|
match v {
|
|
Value::Bool(true) => return Ok(EvalResult::Value(Value::Bool(true))),
|
|
Value::Bool(false) => {}
|
|
_ => return Err(err("List.any predicate must return Bool")),
|
|
}
|
|
}
|
|
Ok(EvalResult::Value(Value::Bool(false)))
|
|
}
|
|
|
|
BuiltinFn::ListAll => {
|
|
let (list, func) = Self::expect_args_2::<Vec<Value>, Value>(&args, "List.all", span)?;
|
|
for item in list {
|
|
let v = self.eval_call_to_value(func.clone(), vec![item], span)?;
|
|
match v {
|
|
Value::Bool(false) => return Ok(EvalResult::Value(Value::Bool(false))),
|
|
Value::Bool(true) => {}
|
|
_ => return Err(err("List.all predicate must return Bool")),
|
|
}
|
|
}
|
|
Ok(EvalResult::Value(Value::Bool(true)))
|
|
}
|
|
|
|
BuiltinFn::ListTake => {
|
|
let (list, n) = Self::expect_args_2::<Vec<Value>, i64>(&args, "List.take", span)?;
|
|
let n = n.max(0) as usize;
|
|
let result: Vec<Value> = list.into_iter().take(n).collect();
|
|
Ok(EvalResult::Value(Value::List(result)))
|
|
}
|
|
|
|
BuiltinFn::ListDrop => {
|
|
let (list, n) = Self::expect_args_2::<Vec<Value>, i64>(&args, "List.drop", span)?;
|
|
let n = n.max(0) as usize;
|
|
let result: Vec<Value> = list.into_iter().skip(n).collect();
|
|
Ok(EvalResult::Value(Value::List(result)))
|
|
}
|
|
|
|
// Additional String operations
|
|
BuiltinFn::StringStartsWith => {
|
|
let (s, prefix) = Self::expect_args_2::<String, String>(&args, "String.startsWith", span)?;
|
|
Ok(EvalResult::Value(Value::Bool(s.starts_with(&prefix))))
|
|
}
|
|
|
|
BuiltinFn::StringEndsWith => {
|
|
let (s, suffix) = Self::expect_args_2::<String, String>(&args, "String.endsWith", span)?;
|
|
Ok(EvalResult::Value(Value::Bool(s.ends_with(&suffix))))
|
|
}
|
|
|
|
BuiltinFn::StringToUpper => {
|
|
let s = Self::expect_arg_1::<String>(&args, "String.toUpper", span)?;
|
|
Ok(EvalResult::Value(Value::String(s.to_uppercase())))
|
|
}
|
|
|
|
BuiltinFn::StringToLower => {
|
|
let s = Self::expect_arg_1::<String>(&args, "String.toLower", span)?;
|
|
Ok(EvalResult::Value(Value::String(s.to_lowercase())))
|
|
}
|
|
|
|
BuiltinFn::StringSubstring => {
|
|
// String.substring(s, start, end) - end is exclusive
|
|
if args.len() != 3 {
|
|
return Err(err("String.substring requires 3 arguments: string, start, end"));
|
|
}
|
|
let s = match &args[0] {
|
|
Value::String(s) => s.clone(),
|
|
v => return Err(err(&format!("String.substring expects String, got {}", v.type_name()))),
|
|
};
|
|
let start = match &args[1] {
|
|
Value::Int(n) => (*n).max(0) as usize,
|
|
v => return Err(err(&format!("String.substring expects Int for start, got {}", v.type_name()))),
|
|
};
|
|
let end = match &args[2] {
|
|
Value::Int(n) => (*n).max(0) as usize,
|
|
v => return Err(err(&format!("String.substring expects Int for end, got {}", v.type_name()))),
|
|
};
|
|
let chars: Vec<char> = s.chars().collect();
|
|
let end = end.min(chars.len());
|
|
let start = start.min(end);
|
|
let result: String = chars[start..end].iter().collect();
|
|
Ok(EvalResult::Value(Value::String(result)))
|
|
}
|
|
|
|
// JSON operations
|
|
BuiltinFn::JsonParse => {
|
|
let s = Self::expect_arg_1::<String>(&args, "Json.parse", span)?;
|
|
match serde_json::from_str::<serde_json::Value>(&s) {
|
|
Ok(json) => Ok(EvalResult::Value(Value::Constructor {
|
|
name: "Ok".to_string(),
|
|
fields: vec![Value::Json(json)],
|
|
})),
|
|
Err(e) => Ok(EvalResult::Value(Value::Constructor {
|
|
name: "Err".to_string(),
|
|
fields: vec![Value::String(e.to_string())],
|
|
})),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::JsonStringify => {
|
|
let json = match &args[0] {
|
|
Value::Json(j) => j.clone(),
|
|
v => return Err(err(&format!("Json.stringify expects Json, got {}", v.type_name()))),
|
|
};
|
|
Ok(EvalResult::Value(Value::String(json.to_string())))
|
|
}
|
|
|
|
BuiltinFn::JsonPrettyPrint => {
|
|
let json = match &args[0] {
|
|
Value::Json(j) => j.clone(),
|
|
v => return Err(err(&format!("Json.prettyPrint expects Json, got {}", v.type_name()))),
|
|
};
|
|
match serde_json::to_string_pretty(&json) {
|
|
Ok(s) => Ok(EvalResult::Value(Value::String(s))),
|
|
Err(e) => Err(err(&format!("Json.prettyPrint error: {}", e))),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::JsonGet => {
|
|
// Json.get(json, key) -> Option<Json>
|
|
if args.len() != 2 {
|
|
return Err(err("Json.get requires 2 arguments: json, key"));
|
|
}
|
|
let json = match &args[0] {
|
|
Value::Json(j) => j,
|
|
v => return Err(err(&format!("Json.get expects Json, got {}", v.type_name()))),
|
|
};
|
|
let key = match &args[1] {
|
|
Value::String(s) => s.clone(),
|
|
v => return Err(err(&format!("Json.get expects String key, got {}", v.type_name()))),
|
|
};
|
|
match json.get(&key) {
|
|
Some(v) => Ok(EvalResult::Value(Value::Constructor {
|
|
name: "Some".to_string(),
|
|
fields: vec![Value::Json(v.clone())],
|
|
})),
|
|
None => Ok(EvalResult::Value(Value::Constructor {
|
|
name: "None".to_string(),
|
|
fields: vec![],
|
|
})),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::JsonGetIndex => {
|
|
// Json.getIndex(json, index) -> Option<Json>
|
|
if args.len() != 2 {
|
|
return Err(err("Json.getIndex requires 2 arguments: json, index"));
|
|
}
|
|
let json = match &args[0] {
|
|
Value::Json(j) => j,
|
|
v => return Err(err(&format!("Json.getIndex expects Json, got {}", v.type_name()))),
|
|
};
|
|
let idx = match &args[1] {
|
|
Value::Int(n) => *n as usize,
|
|
v => return Err(err(&format!("Json.getIndex expects Int index, got {}", v.type_name()))),
|
|
};
|
|
match json.get(idx) {
|
|
Some(v) => Ok(EvalResult::Value(Value::Constructor {
|
|
name: "Some".to_string(),
|
|
fields: vec![Value::Json(v.clone())],
|
|
})),
|
|
None => Ok(EvalResult::Value(Value::Constructor {
|
|
name: "None".to_string(),
|
|
fields: vec![],
|
|
})),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::JsonAsString => {
|
|
// Json.asString(json) -> Option<String>
|
|
let json = match &args[0] {
|
|
Value::Json(j) => j,
|
|
v => return Err(err(&format!("Json.asString expects Json, got {}", v.type_name()))),
|
|
};
|
|
match json.as_str() {
|
|
Some(s) => Ok(EvalResult::Value(Value::Constructor {
|
|
name: "Some".to_string(),
|
|
fields: vec![Value::String(s.to_string())],
|
|
})),
|
|
None => Ok(EvalResult::Value(Value::Constructor {
|
|
name: "None".to_string(),
|
|
fields: vec![],
|
|
})),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::JsonAsNumber => {
|
|
// Json.asNumber(json) -> Option<Float>
|
|
let json = match &args[0] {
|
|
Value::Json(j) => j,
|
|
v => return Err(err(&format!("Json.asNumber expects Json, got {}", v.type_name()))),
|
|
};
|
|
match json.as_f64() {
|
|
Some(n) => Ok(EvalResult::Value(Value::Constructor {
|
|
name: "Some".to_string(),
|
|
fields: vec![Value::Float(n)],
|
|
})),
|
|
None => Ok(EvalResult::Value(Value::Constructor {
|
|
name: "None".to_string(),
|
|
fields: vec![],
|
|
})),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::JsonAsInt => {
|
|
// Json.asInt(json) -> Option<Int>
|
|
let json = match &args[0] {
|
|
Value::Json(j) => j,
|
|
v => return Err(err(&format!("Json.asInt expects Json, got {}", v.type_name()))),
|
|
};
|
|
match json.as_i64() {
|
|
Some(n) => Ok(EvalResult::Value(Value::Constructor {
|
|
name: "Some".to_string(),
|
|
fields: vec![Value::Int(n)],
|
|
})),
|
|
None => Ok(EvalResult::Value(Value::Constructor {
|
|
name: "None".to_string(),
|
|
fields: vec![],
|
|
})),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::JsonAsBool => {
|
|
// Json.asBool(json) -> Option<Bool>
|
|
let json = match &args[0] {
|
|
Value::Json(j) => j,
|
|
v => return Err(err(&format!("Json.asBool expects Json, got {}", v.type_name()))),
|
|
};
|
|
match json.as_bool() {
|
|
Some(b) => Ok(EvalResult::Value(Value::Constructor {
|
|
name: "Some".to_string(),
|
|
fields: vec![Value::Bool(b)],
|
|
})),
|
|
None => Ok(EvalResult::Value(Value::Constructor {
|
|
name: "None".to_string(),
|
|
fields: vec![],
|
|
})),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::JsonAsArray => {
|
|
// Json.asArray(json) -> Option<List<Json>>
|
|
let json = match &args[0] {
|
|
Value::Json(j) => j,
|
|
v => return Err(err(&format!("Json.asArray expects Json, got {}", v.type_name()))),
|
|
};
|
|
match json.as_array() {
|
|
Some(arr) => {
|
|
let items: Vec<Value> = arr.iter().map(|v| Value::Json(v.clone())).collect();
|
|
Ok(EvalResult::Value(Value::Constructor {
|
|
name: "Some".to_string(),
|
|
fields: vec![Value::List(items)],
|
|
}))
|
|
}
|
|
None => Ok(EvalResult::Value(Value::Constructor {
|
|
name: "None".to_string(),
|
|
fields: vec![],
|
|
})),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::JsonIsNull => {
|
|
// Json.isNull(json) -> Bool
|
|
let json = match &args[0] {
|
|
Value::Json(j) => j,
|
|
v => return Err(err(&format!("Json.isNull expects Json, got {}", v.type_name()))),
|
|
};
|
|
Ok(EvalResult::Value(Value::Bool(json.is_null())))
|
|
}
|
|
|
|
BuiltinFn::JsonKeys => {
|
|
// Json.keys(json) -> Option<List<String>>
|
|
let json = match &args[0] {
|
|
Value::Json(j) => j,
|
|
v => return Err(err(&format!("Json.keys expects Json, got {}", v.type_name()))),
|
|
};
|
|
match json.as_object() {
|
|
Some(obj) => {
|
|
let keys: Vec<Value> = obj.keys().map(|k| Value::String(k.clone())).collect();
|
|
Ok(EvalResult::Value(Value::Constructor {
|
|
name: "Some".to_string(),
|
|
fields: vec![Value::List(keys)],
|
|
}))
|
|
}
|
|
None => Ok(EvalResult::Value(Value::Constructor {
|
|
name: "None".to_string(),
|
|
fields: vec![],
|
|
})),
|
|
}
|
|
}
|
|
|
|
// JSON constructors
|
|
BuiltinFn::JsonNull => {
|
|
Ok(EvalResult::Value(Value::Json(serde_json::Value::Null)))
|
|
}
|
|
|
|
BuiltinFn::JsonBool => {
|
|
let b = Self::expect_arg_1::<bool>(&args, "Json.bool", span)?;
|
|
Ok(EvalResult::Value(Value::Json(serde_json::Value::Bool(b))))
|
|
}
|
|
|
|
BuiltinFn::JsonNumber => {
|
|
let n = match &args[0] {
|
|
Value::Float(f) => serde_json::Number::from_f64(*f)
|
|
.ok_or_else(|| err("Invalid float for JSON"))?,
|
|
Value::Int(i) => serde_json::Number::from(*i),
|
|
v => return Err(err(&format!("Json.number expects Float or Int, got {}", v.type_name()))),
|
|
};
|
|
Ok(EvalResult::Value(Value::Json(serde_json::Value::Number(n))))
|
|
}
|
|
|
|
BuiltinFn::JsonInt => {
|
|
let n = Self::expect_arg_1::<i64>(&args, "Json.int", span)?;
|
|
Ok(EvalResult::Value(Value::Json(serde_json::Value::Number(serde_json::Number::from(n)))))
|
|
}
|
|
|
|
BuiltinFn::JsonString => {
|
|
let s = Self::expect_arg_1::<String>(&args, "Json.string", span)?;
|
|
Ok(EvalResult::Value(Value::Json(serde_json::Value::String(s))))
|
|
}
|
|
|
|
BuiltinFn::JsonArray => {
|
|
// Json.array(list: List<Json>) -> Json
|
|
let list = Self::expect_arg_1::<Vec<Value>>(&args, "Json.array", span)?;
|
|
let arr: Result<Vec<serde_json::Value>, RuntimeError> = list.into_iter().map(|v| {
|
|
match v {
|
|
Value::Json(j) => Ok(j),
|
|
_ => Err(err("Json.array expects List<Json>")),
|
|
}
|
|
}).collect();
|
|
Ok(EvalResult::Value(Value::Json(serde_json::Value::Array(arr?))))
|
|
}
|
|
|
|
BuiltinFn::JsonObject => {
|
|
// Json.object(entries: List<(String, Json)>) -> Json
|
|
let list = Self::expect_arg_1::<Vec<Value>>(&args, "Json.object", span)?;
|
|
let mut map = serde_json::Map::new();
|
|
for item in list {
|
|
match item {
|
|
Value::Tuple(fields) if fields.len() == 2 => {
|
|
let key = match &fields[0] {
|
|
Value::String(s) => s.clone(),
|
|
_ => return Err(err("Json.object expects (String, Json) tuples")),
|
|
};
|
|
let value = match &fields[1] {
|
|
Value::Json(j) => j.clone(),
|
|
_ => return Err(err("Json.object expects (String, Json) tuples")),
|
|
};
|
|
map.insert(key, value);
|
|
}
|
|
_ => return Err(err("Json.object expects List<(String, Json)>")),
|
|
}
|
|
}
|
|
Ok(EvalResult::Value(Value::Json(serde_json::Value::Object(map))))
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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());
|
|
}
|
|
}
|
|
// Enter handler context (enables resume)
|
|
self.in_handler_depth += 1;
|
|
let eval_result = self.eval_expr_inner(&body, &env);
|
|
self.in_handler_depth -= 1;
|
|
|
|
// Handle Resume result - use the resumed value as the effect's return value
|
|
match eval_result {
|
|
Ok(EvalResult::Resume(value)) => Ok(value),
|
|
Ok(EvalResult::Value(value)) => Ok(value),
|
|
Ok(EvalResult::Effect(req)) => {
|
|
// Handler body can perform effects - handle them
|
|
self.handle_effect(req)
|
|
}
|
|
Ok(EvalResult::TailCall { func, args, span }) => {
|
|
// Tail call in handler - evaluate it
|
|
self.eval_call_to_value(func, args, span)
|
|
}
|
|
Err(e) => Err(e),
|
|
}
|
|
} 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,
|
|
})
|
|
}
|
|
("State", "get") => {
|
|
Ok(self.builtin_state.borrow().clone())
|
|
}
|
|
("State", "put") => {
|
|
if let Some(value) = request.args.first() {
|
|
*self.builtin_state.borrow_mut() = value.clone();
|
|
Ok(Value::Unit)
|
|
} else {
|
|
Err(RuntimeError {
|
|
message: "State.put requires a value argument".to_string(),
|
|
span: None,
|
|
})
|
|
}
|
|
}
|
|
("Reader", "ask") => {
|
|
Ok(self.builtin_reader.borrow().clone())
|
|
}
|
|
("Random", "int") => {
|
|
let min = match request.args.first() {
|
|
Some(Value::Int(n)) => *n,
|
|
_ => 0,
|
|
};
|
|
let max = match request.args.get(1) {
|
|
Some(Value::Int(n)) => *n,
|
|
_ => i64::MAX,
|
|
};
|
|
let mut rng = rand::thread_rng();
|
|
let value = rng.gen_range(min..=max);
|
|
Ok(Value::Int(value))
|
|
}
|
|
("Random", "float") => {
|
|
let mut rng = rand::thread_rng();
|
|
let value: f64 = rng.gen();
|
|
Ok(Value::Float(value))
|
|
}
|
|
("Random", "bool") => {
|
|
let mut rng = rand::thread_rng();
|
|
let value: bool = rng.gen();
|
|
Ok(Value::Bool(value))
|
|
}
|
|
("Time", "now") => {
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
let duration = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap_or_default();
|
|
let millis = duration.as_millis() as i64;
|
|
Ok(Value::Int(millis))
|
|
}
|
|
("Time", "sleep") => {
|
|
use std::thread;
|
|
use std::time::Duration;
|
|
let ms = match request.args.first() {
|
|
Some(Value::Int(n)) => *n as u64,
|
|
_ => 0,
|
|
};
|
|
thread::sleep(Duration::from_millis(ms));
|
|
Ok(Value::Unit)
|
|
}
|
|
|
|
// ===== File Effect =====
|
|
("File", "read") => {
|
|
let path = match request.args.first() {
|
|
Some(Value::String(s)) => s.clone(),
|
|
_ => return Err(RuntimeError {
|
|
message: "File.read requires a string path".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
match std::fs::read_to_string(&path) {
|
|
Ok(content) => Ok(Value::String(content)),
|
|
Err(e) => Err(RuntimeError {
|
|
message: format!("Failed to read file '{}': {}", path, e),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
("File", "write") => {
|
|
let path = match request.args.first() {
|
|
Some(Value::String(s)) => s.clone(),
|
|
_ => return Err(RuntimeError {
|
|
message: "File.write requires a string path".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
let content = match request.args.get(1) {
|
|
Some(Value::String(s)) => s.clone(),
|
|
_ => return Err(RuntimeError {
|
|
message: "File.write requires string content".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
match std::fs::write(&path, &content) {
|
|
Ok(()) => Ok(Value::Unit),
|
|
Err(e) => Err(RuntimeError {
|
|
message: format!("Failed to write file '{}': {}", path, e),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
("File", "append") => {
|
|
use std::fs::OpenOptions;
|
|
use std::io::Write;
|
|
let path = match request.args.first() {
|
|
Some(Value::String(s)) => s.clone(),
|
|
_ => return Err(RuntimeError {
|
|
message: "File.append requires a string path".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
let content = match request.args.get(1) {
|
|
Some(Value::String(s)) => s.clone(),
|
|
_ => return Err(RuntimeError {
|
|
message: "File.append requires string content".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
match OpenOptions::new().create(true).append(true).open(&path) {
|
|
Ok(mut file) => {
|
|
file.write_all(content.as_bytes()).map_err(|e| RuntimeError {
|
|
message: format!("Failed to append to file '{}': {}", path, e),
|
|
span: None,
|
|
})?;
|
|
Ok(Value::Unit)
|
|
}
|
|
Err(e) => Err(RuntimeError {
|
|
message: format!("Failed to open file '{}': {}", path, e),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
("File", "exists") => {
|
|
let path = match request.args.first() {
|
|
Some(Value::String(s)) => s.clone(),
|
|
_ => return Err(RuntimeError {
|
|
message: "File.exists requires a string path".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
Ok(Value::Bool(std::path::Path::new(&path).exists()))
|
|
}
|
|
("File", "delete") => {
|
|
let path = match request.args.first() {
|
|
Some(Value::String(s)) => s.clone(),
|
|
_ => return Err(RuntimeError {
|
|
message: "File.delete requires a string path".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
match std::fs::remove_file(&path) {
|
|
Ok(()) => Ok(Value::Unit),
|
|
Err(e) => Err(RuntimeError {
|
|
message: format!("Failed to delete file '{}': {}", path, e),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
("File", "readDir") => {
|
|
let path = match request.args.first() {
|
|
Some(Value::String(s)) => s.clone(),
|
|
_ => return Err(RuntimeError {
|
|
message: "File.readDir requires a string path".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
match std::fs::read_dir(&path) {
|
|
Ok(entries) => {
|
|
let files: Vec<Value> = entries
|
|
.filter_map(|e| e.ok())
|
|
.map(|e| Value::String(e.file_name().to_string_lossy().to_string()))
|
|
.collect();
|
|
Ok(Value::List(files))
|
|
}
|
|
Err(e) => Err(RuntimeError {
|
|
message: format!("Failed to read directory '{}': {}", path, e),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
("File", "isDir") => {
|
|
let path = match request.args.first() {
|
|
Some(Value::String(s)) => s.clone(),
|
|
_ => return Err(RuntimeError {
|
|
message: "File.isDir requires a string path".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
Ok(Value::Bool(std::path::Path::new(&path).is_dir()))
|
|
}
|
|
("File", "mkdir") => {
|
|
let path = match request.args.first() {
|
|
Some(Value::String(s)) => s.clone(),
|
|
_ => return Err(RuntimeError {
|
|
message: "File.mkdir requires a string path".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
match std::fs::create_dir_all(&path) {
|
|
Ok(()) => Ok(Value::Unit),
|
|
Err(e) => Err(RuntimeError {
|
|
message: format!("Failed to create directory '{}': {}", path, e),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
// ===== Process Effect =====
|
|
("Process", "exec") => {
|
|
use std::process::Command;
|
|
let cmd = match request.args.first() {
|
|
Some(Value::String(s)) => s.clone(),
|
|
_ => return Err(RuntimeError {
|
|
message: "Process.exec requires a string command".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
match Command::new("sh").arg("-c").arg(&cmd).output() {
|
|
Ok(output) => {
|
|
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
|
Ok(Value::String(stdout))
|
|
}
|
|
Err(e) => Err(RuntimeError {
|
|
message: format!("Failed to execute command: {}", e),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
("Process", "execStatus") => {
|
|
use std::process::Command;
|
|
let cmd = match request.args.first() {
|
|
Some(Value::String(s)) => s.clone(),
|
|
_ => return Err(RuntimeError {
|
|
message: "Process.execStatus requires a string command".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
match Command::new("sh").arg("-c").arg(&cmd).status() {
|
|
Ok(status) => {
|
|
let code = status.code().unwrap_or(-1) as i64;
|
|
Ok(Value::Int(code))
|
|
}
|
|
Err(e) => Err(RuntimeError {
|
|
message: format!("Failed to execute command: {}", e),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
("Process", "env") => {
|
|
let var_name = match request.args.first() {
|
|
Some(Value::String(s)) => s.clone(),
|
|
_ => return Err(RuntimeError {
|
|
message: "Process.env requires a string name".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
match std::env::var(&var_name) {
|
|
Ok(value) => Ok(Value::Constructor {
|
|
name: "Some".to_string(),
|
|
fields: vec![Value::String(value)],
|
|
}),
|
|
Err(_) => Ok(Value::Constructor {
|
|
name: "None".to_string(),
|
|
fields: vec![],
|
|
}),
|
|
}
|
|
}
|
|
("Process", "args") => {
|
|
let args: Vec<Value> = std::env::args()
|
|
.skip(1) // Skip the program name
|
|
.map(Value::String)
|
|
.collect();
|
|
Ok(Value::List(args))
|
|
}
|
|
("Process", "exit") => {
|
|
let code = match request.args.first() {
|
|
Some(Value::Int(n)) => *n as i32,
|
|
_ => 0,
|
|
};
|
|
std::process::exit(code);
|
|
}
|
|
("Process", "cwd") => {
|
|
match std::env::current_dir() {
|
|
Ok(path) => Ok(Value::String(path.to_string_lossy().to_string())),
|
|
Err(e) => Err(RuntimeError {
|
|
message: format!("Failed to get current directory: {}", e),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
("Process", "setCwd") => {
|
|
let path = match request.args.first() {
|
|
Some(Value::String(s)) => s.clone(),
|
|
_ => return Err(RuntimeError {
|
|
message: "Process.setCwd requires a string path".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
match std::env::set_current_dir(&path) {
|
|
Ok(()) => Ok(Value::Unit),
|
|
Err(e) => Err(RuntimeError {
|
|
message: format!("Failed to change directory to '{}': {}", path, e),
|
|
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"),
|
|
}
|
|
}
|
|
}
|