Add missing List operations requested by ergon game engine project: - findIndex(list, predicate) -> Option<Int> - zip(list1, list2) -> List<(A, B)> - flatten(listOfLists) -> List<A> - contains(list, element) -> Bool Resolves ergon porting blocker #4. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5757 lines
227 KiB
Rust
5757 lines
227 KiB
Rust
//! Tree-walking interpreter for the Lux language with algebraic effects
|
|
|
|
#![allow(dead_code, unused_variables)]
|
|
|
|
use crate::ast::*;
|
|
use crate::diagnostics::{Diagnostic, ErrorCode, Severity};
|
|
use rand::Rng;
|
|
use postgres::{Client as PgClient, NoTls};
|
|
use rusqlite::Connection;
|
|
use std::cell::RefCell;
|
|
use std::collections::HashMap;
|
|
use std::fmt;
|
|
use std::rc::Rc;
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
/// 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,
|
|
ListForEach,
|
|
ListSort,
|
|
ListSortBy,
|
|
|
|
// String operations
|
|
StringSplit,
|
|
StringJoin,
|
|
StringTrim,
|
|
StringContains,
|
|
StringReplace,
|
|
StringLength,
|
|
StringChars,
|
|
StringLines,
|
|
StringParseInt,
|
|
StringParseFloat,
|
|
|
|
// 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,
|
|
MathSin,
|
|
MathCos,
|
|
MathAtan2,
|
|
|
|
// Additional List operations
|
|
ListIsEmpty,
|
|
ListFind,
|
|
ListFindIndex,
|
|
ListAny,
|
|
ListAll,
|
|
ListTake,
|
|
ListDrop,
|
|
ListZip,
|
|
ListFlatten,
|
|
ListContains,
|
|
|
|
// Additional String operations
|
|
StringStartsWith,
|
|
StringEndsWith,
|
|
StringToUpper,
|
|
StringToLower,
|
|
StringSubstring,
|
|
StringFromChar,
|
|
StringCharAt,
|
|
StringIndexOf,
|
|
StringLastIndexOf,
|
|
StringRepeat,
|
|
|
|
// Int/Float operations
|
|
IntToString,
|
|
IntToFloat,
|
|
FloatToString,
|
|
FloatToInt,
|
|
|
|
// JSON operations
|
|
JsonParse,
|
|
JsonStringify,
|
|
JsonPrettyPrint,
|
|
JsonGet,
|
|
JsonGetIndex,
|
|
JsonAsString,
|
|
JsonAsNumber,
|
|
JsonAsInt,
|
|
JsonAsBool,
|
|
JsonAsArray,
|
|
JsonIsNull,
|
|
JsonKeys,
|
|
JsonNull,
|
|
JsonBool,
|
|
JsonNumber,
|
|
JsonInt,
|
|
JsonString,
|
|
JsonArray,
|
|
JsonObject,
|
|
|
|
// Map operations
|
|
MapNew,
|
|
MapSet,
|
|
MapGet,
|
|
MapContains,
|
|
MapRemove,
|
|
MapKeys,
|
|
MapValues,
|
|
MapSize,
|
|
MapIsEmpty,
|
|
MapFromList,
|
|
MapToList,
|
|
MapMerge,
|
|
}
|
|
|
|
/// 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>),
|
|
Map(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::Map(_) => "Map",
|
|
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,
|
|
}
|
|
}
|
|
|
|
/// Compare two values for equality (for testing)
|
|
/// Returns true if the values are structurally equal
|
|
pub fn values_equal(a: &Value, b: &Value) -> bool {
|
|
match (a, b) {
|
|
(Value::Int(x), Value::Int(y)) => x == y,
|
|
(Value::Float(x), Value::Float(y)) => (x - y).abs() < f64::EPSILON,
|
|
(Value::Bool(x), Value::Bool(y)) => x == y,
|
|
(Value::String(x), Value::String(y)) => x == y,
|
|
(Value::Char(x), Value::Char(y)) => x == y,
|
|
(Value::Unit, Value::Unit) => true,
|
|
(Value::List(xs), Value::List(ys)) => {
|
|
xs.len() == ys.len() && xs.iter().zip(ys.iter()).all(|(x, y)| Value::values_equal(x, y))
|
|
}
|
|
(Value::Tuple(xs), Value::Tuple(ys)) => {
|
|
xs.len() == ys.len() && xs.iter().zip(ys.iter()).all(|(x, y)| Value::values_equal(x, y))
|
|
}
|
|
(Value::Record(xs), Value::Record(ys)) => {
|
|
xs.len() == ys.len() && xs.iter().all(|(k, v)| {
|
|
ys.get(k).map(|yv| Value::values_equal(v, yv)).unwrap_or(false)
|
|
})
|
|
}
|
|
(Value::Map(xs), Value::Map(ys)) => {
|
|
xs.len() == ys.len() && xs.iter().all(|(k, v)| {
|
|
ys.get(k).map(|yv| Value::values_equal(v, yv)).unwrap_or(false)
|
|
})
|
|
}
|
|
(Value::Constructor { name: n1, fields: f1 }, Value::Constructor { name: n2, fields: f2 }) => {
|
|
n1 == n2 && f1.len() == f2.len() && f1.iter().zip(f2.iter()).all(|(x, y)| Value::values_equal(x, y))
|
|
}
|
|
(Value::Versioned { type_name: t1, version: v1, value: val1 },
|
|
Value::Versioned { type_name: t2, version: v2, value: val2 }) => {
|
|
t1 == t2 && v1 == v2 && Value::values_equal(val1, val2)
|
|
}
|
|
(Value::Json(j1), Value::Json(j2)) => j1 == j2,
|
|
// Functions and handlers cannot be compared for equality
|
|
_ => false,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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 HashMap<String, Value> {
|
|
const TYPE_NAME: &'static str = "Map";
|
|
fn try_from_value(value: &Value) -> Option<Self> {
|
|
match value {
|
|
Value::Map(m) => Some(m.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::Map(entries) => {
|
|
write!(f, "Map {{")?;
|
|
let mut sorted: Vec<_> = entries.iter().collect();
|
|
sorted.sort_by_key(|(k, _)| (*k).clone());
|
|
for (i, (key, value)) in sorted.iter().enumerate() {
|
|
if i > 0 {
|
|
write!(f, ", ")?;
|
|
}
|
|
write!(f, "\"{}\": {}", key, 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 (code, title, hints) = categorize_runtime_error(&self.message);
|
|
|
|
Diagnostic {
|
|
severity: Severity::Error,
|
|
code,
|
|
title,
|
|
message: self.message.clone(),
|
|
span: self.span.unwrap_or_default(),
|
|
hints,
|
|
expected_type: None,
|
|
actual_type: None,
|
|
secondary_spans: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Categorize runtime errors to provide better titles, hints, and error codes
|
|
fn categorize_runtime_error(message: &str) -> (Option<ErrorCode>, String, Vec<String>) {
|
|
let message_lower = message.to_lowercase();
|
|
|
|
if message_lower.contains("undefined variable") {
|
|
(
|
|
Some(ErrorCode::E0301),
|
|
"Undefined Variable".to_string(),
|
|
vec!["Make sure the variable is defined and in scope.".to_string()],
|
|
)
|
|
} else if message_lower.contains("undefined function") || message_lower.contains("function not found") {
|
|
(
|
|
Some(ErrorCode::E0302),
|
|
"Undefined Function".to_string(),
|
|
vec!["Make sure the function is defined and in scope.".to_string()],
|
|
)
|
|
} else if message_lower.contains("undefined") || message_lower.contains("not found") {
|
|
(
|
|
Some(ErrorCode::E0301),
|
|
"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") {
|
|
(
|
|
None, // Runtime error, not a type error
|
|
"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") {
|
|
(
|
|
Some(ErrorCode::E0201),
|
|
"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") {
|
|
(
|
|
Some(ErrorCode::E0401),
|
|
"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") {
|
|
(
|
|
Some(ErrorCode::E0501),
|
|
"Non-exhaustive Pattern".to_string(),
|
|
vec!["Add more patterns to cover all possible cases.".to_string()],
|
|
)
|
|
} else if message_lower.contains("argument") {
|
|
(
|
|
Some(ErrorCode::E0209),
|
|
"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") {
|
|
(
|
|
None, // Runtime error
|
|
"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 {
|
|
(None, "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 (kept for nested handler semantics)
|
|
handler_stack: Vec<Rc<HandlerValue>>,
|
|
/// Evidence map for O(1) handler lookup (evidence passing optimization)
|
|
/// Maps effect name -> handler, updated when entering/exiting run blocks
|
|
evidence: HashMap<String, 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,
|
|
/// HTTP server state (using Arc<Mutex> for thread-safety with tiny_http)
|
|
http_server: Arc<Mutex<Option<tiny_http::Server>>>,
|
|
/// Current HTTP request being handled (stored for respond operation)
|
|
current_http_request: Arc<Mutex<Option<tiny_http::Request>>>,
|
|
/// Test results for the Test effect
|
|
test_results: RefCell<TestResults>,
|
|
/// SQL database connections (connection ID -> Connection)
|
|
sql_connections: RefCell<HashMap<i64, Connection>>,
|
|
/// Next SQL connection ID
|
|
next_sql_conn_id: RefCell<i64>,
|
|
/// PostgreSQL database connections (connection ID -> Client)
|
|
pg_connections: RefCell<HashMap<i64, PgClient>>,
|
|
/// Next PostgreSQL connection ID
|
|
next_pg_conn_id: RefCell<i64>,
|
|
/// Concurrent tasks: task_id -> (thunk_value, result_option, is_cancelled)
|
|
concurrent_tasks: RefCell<HashMap<i64, (Value, Option<Value>, bool)>>,
|
|
/// Next task ID
|
|
next_task_id: RefCell<i64>,
|
|
/// Channels: channel_id -> (queue, is_closed)
|
|
channels: RefCell<HashMap<i64, (Vec<Value>, bool)>>,
|
|
/// Next channel ID
|
|
next_channel_id: RefCell<i64>,
|
|
}
|
|
|
|
/// Results from running tests
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct TestResults {
|
|
pub passed: usize,
|
|
pub failed: usize,
|
|
pub failures: Vec<TestFailure>,
|
|
}
|
|
|
|
/// A single test failure
|
|
#[derive(Debug, Clone)]
|
|
pub struct TestFailure {
|
|
pub message: String,
|
|
pub expected: Option<String>,
|
|
pub actual: Option<String>,
|
|
}
|
|
|
|
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(),
|
|
evidence: HashMap::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,
|
|
http_server: Arc::new(Mutex::new(None)),
|
|
current_http_request: Arc::new(Mutex::new(None)),
|
|
test_results: RefCell::new(TestResults::default()),
|
|
sql_connections: RefCell::new(HashMap::new()),
|
|
next_sql_conn_id: RefCell::new(1),
|
|
pg_connections: RefCell::new(HashMap::new()),
|
|
next_pg_conn_id: RefCell::new(1),
|
|
concurrent_tasks: RefCell::new(HashMap::new()),
|
|
next_task_id: RefCell::new(1),
|
|
channels: RefCell::new(HashMap::new()),
|
|
next_channel_id: RefCell::new(1),
|
|
}
|
|
}
|
|
|
|
/// Get the test results
|
|
pub fn get_test_results(&self) -> TestResults {
|
|
self.test_results.borrow().clone()
|
|
}
|
|
|
|
/// Reset test results for a new test run
|
|
pub fn reset_test_results(&self) {
|
|
*self.test_results.borrow_mut() = TestResults::default();
|
|
}
|
|
|
|
/// 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);
|
|
}
|
|
|
|
/// Register auto-generated migrations from the typechecker
|
|
/// These are migrations that were automatically generated for auto-migratable schema changes
|
|
pub fn register_auto_migrations(
|
|
&mut self,
|
|
auto_migrations: &std::collections::HashMap<String, std::collections::HashMap<u32, Expr>>,
|
|
) {
|
|
for (type_name, version_migrations) in auto_migrations {
|
|
for (from_version, body) in version_migrations {
|
|
// Only register if no migration already exists
|
|
let already_has = self
|
|
.migrations
|
|
.get(type_name)
|
|
.map(|m| m.contains_key(from_version))
|
|
.unwrap_or(false);
|
|
|
|
if !already_has {
|
|
let stored = StoredMigration {
|
|
body: body.clone(),
|
|
env: self.global_env.clone(),
|
|
};
|
|
self.register_migration(type_name, *from_version, stored);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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)),
|
|
("findIndex".to_string(), Value::Builtin(BuiltinFn::ListFindIndex)),
|
|
("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)),
|
|
("zip".to_string(), Value::Builtin(BuiltinFn::ListZip)),
|
|
("flatten".to_string(), Value::Builtin(BuiltinFn::ListFlatten)),
|
|
("contains".to_string(), Value::Builtin(BuiltinFn::ListContains)),
|
|
(
|
|
"forEach".to_string(),
|
|
Value::Builtin(BuiltinFn::ListForEach),
|
|
),
|
|
("sort".to_string(), Value::Builtin(BuiltinFn::ListSort)),
|
|
(
|
|
"sortBy".to_string(),
|
|
Value::Builtin(BuiltinFn::ListSortBy),
|
|
),
|
|
]));
|
|
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),
|
|
),
|
|
(
|
|
"fromChar".to_string(),
|
|
Value::Builtin(BuiltinFn::StringFromChar),
|
|
),
|
|
(
|
|
"parseInt".to_string(),
|
|
Value::Builtin(BuiltinFn::StringParseInt),
|
|
),
|
|
(
|
|
"parseFloat".to_string(),
|
|
Value::Builtin(BuiltinFn::StringParseFloat),
|
|
),
|
|
(
|
|
"charAt".to_string(),
|
|
Value::Builtin(BuiltinFn::StringCharAt),
|
|
),
|
|
(
|
|
"indexOf".to_string(),
|
|
Value::Builtin(BuiltinFn::StringIndexOf),
|
|
),
|
|
(
|
|
"lastIndexOf".to_string(),
|
|
Value::Builtin(BuiltinFn::StringLastIndexOf),
|
|
),
|
|
(
|
|
"repeat".to_string(),
|
|
Value::Builtin(BuiltinFn::StringRepeat),
|
|
),
|
|
]));
|
|
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)),
|
|
("sin".to_string(), Value::Builtin(BuiltinFn::MathSin)),
|
|
("cos".to_string(), Value::Builtin(BuiltinFn::MathCos)),
|
|
("atan2".to_string(), Value::Builtin(BuiltinFn::MathAtan2)),
|
|
]));
|
|
env.define("Math", math_module);
|
|
|
|
// Int module
|
|
let int_module = Value::Record(HashMap::from([
|
|
("toString".to_string(), Value::Builtin(BuiltinFn::IntToString)),
|
|
("toFloat".to_string(), Value::Builtin(BuiltinFn::IntToFloat)),
|
|
]));
|
|
env.define("Int", int_module);
|
|
|
|
// Float module
|
|
let float_module = Value::Record(HashMap::from([
|
|
("toString".to_string(), Value::Builtin(BuiltinFn::FloatToString)),
|
|
("toInt".to_string(), Value::Builtin(BuiltinFn::FloatToInt)),
|
|
]));
|
|
env.define("Float", float_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);
|
|
|
|
// Map module
|
|
let map_module = Value::Record(HashMap::from([
|
|
("new".to_string(), Value::Builtin(BuiltinFn::MapNew)),
|
|
("set".to_string(), Value::Builtin(BuiltinFn::MapSet)),
|
|
("get".to_string(), Value::Builtin(BuiltinFn::MapGet)),
|
|
("contains".to_string(), Value::Builtin(BuiltinFn::MapContains)),
|
|
("remove".to_string(), Value::Builtin(BuiltinFn::MapRemove)),
|
|
("keys".to_string(), Value::Builtin(BuiltinFn::MapKeys)),
|
|
("values".to_string(), Value::Builtin(BuiltinFn::MapValues)),
|
|
("size".to_string(), Value::Builtin(BuiltinFn::MapSize)),
|
|
("isEmpty".to_string(), Value::Builtin(BuiltinFn::MapIsEmpty)),
|
|
("fromList".to_string(), Value::Builtin(BuiltinFn::MapFromList)),
|
|
("toList".to_string(), Value::Builtin(BuiltinFn::MapToList)),
|
|
("merge".to_string(), Value::Builtin(BuiltinFn::MapMerge)),
|
|
]));
|
|
env.define("Map", map_module);
|
|
}
|
|
|
|
/// Execute a program
|
|
pub fn run(&mut self, program: &Program) -> Result<Value, RuntimeError> {
|
|
let mut last_value = Value::Unit;
|
|
let mut has_main_let = false;
|
|
|
|
for decl in &program.declarations {
|
|
// Track if there's a top-level `let main = ...`
|
|
if let Declaration::Let(let_decl) = decl {
|
|
if let_decl.name.name == "main" {
|
|
has_main_let = true;
|
|
}
|
|
}
|
|
last_value = self.eval_declaration(decl)?;
|
|
}
|
|
|
|
// Auto-invoke main if it was defined as a let binding with a function value
|
|
if has_main_let {
|
|
if let Some(main_val) = self.global_env.get("main") {
|
|
if let Value::Function(ref closure) = main_val {
|
|
if closure.params.is_empty() {
|
|
let span = Span { start: 0, end: 0 };
|
|
let mut result = self.eval_call(main_val.clone(), vec![], span)?;
|
|
// Trampoline loop
|
|
loop {
|
|
match result {
|
|
EvalResult::Value(v) => {
|
|
last_value = v;
|
|
break;
|
|
}
|
|
EvalResult::Effect(req) => {
|
|
last_value = self.handle_effect(req)?;
|
|
break;
|
|
}
|
|
EvalResult::TailCall { func, args, span } => {
|
|
result = self.eval_call(func, args, span)?;
|
|
}
|
|
EvalResult::Resume(v) => {
|
|
last_value = v;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
// Register migrations for versioned types
|
|
for migration in &type_decl.migrations {
|
|
let stored = StoredMigration {
|
|
body: migration.body.clone(),
|
|
env: self.global_env.clone(),
|
|
};
|
|
self.register_migration(
|
|
&type_decl.name.name,
|
|
migration.from_version.number,
|
|
stored,
|
|
);
|
|
}
|
|
|
|
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::TupleIndex {
|
|
object,
|
|
index,
|
|
span,
|
|
} => {
|
|
let obj_val = self.eval_expr(object, env)?;
|
|
match obj_val {
|
|
Value::Tuple(elements) => {
|
|
if *index < elements.len() {
|
|
Ok(EvalResult::Value(elements[*index].clone()))
|
|
} else {
|
|
Err(RuntimeError {
|
|
message: format!(
|
|
"Tuple index {} out of bounds for tuple with {} elements",
|
|
index,
|
|
elements.len()
|
|
),
|
|
span: Some(*span),
|
|
})
|
|
}
|
|
}
|
|
_ => Err(RuntimeError {
|
|
message: format!("Cannot use tuple index 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 {
|
|
spread, fields, ..
|
|
} => {
|
|
let mut record = HashMap::new();
|
|
|
|
// If there's a spread, evaluate it and start with its fields
|
|
if let Some(spread_expr) = spread {
|
|
let spread_val = self.eval_expr(spread_expr, env)?;
|
|
if let Value::Record(spread_fields) = spread_val {
|
|
record = spread_fields;
|
|
} else {
|
|
return Err(RuntimeError {
|
|
message: format!(
|
|
"Spread expression must evaluate to a record, got {}",
|
|
spread_val.type_name()
|
|
),
|
|
span: Some(expr.span()),
|
|
});
|
|
}
|
|
}
|
|
|
|
// Override with explicit fields
|
|
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::Concat => match (left, right) {
|
|
(Value::String(a), Value::String(b)) => Ok(Value::String(a + &b)),
|
|
(Value::List(a), Value::List(b)) => {
|
|
let mut result = a;
|
|
result.extend(b);
|
|
Ok(Value::List(result))
|
|
}
|
|
(l, r) => Err(RuntimeError {
|
|
message: format!("Cannot concatenate {} 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)),
|
|
(Value::Char(a), Value::Char(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)),
|
|
(Value::Char(a), Value::Char(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)),
|
|
(Value::Char(a), Value::Char(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)),
|
|
(Value::Char(a), Value::Char(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(req) => {
|
|
// Handle the effect and continue
|
|
let handled = self.handle_effect(req)?;
|
|
return Ok(handled);
|
|
}
|
|
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)))
|
|
}
|
|
|
|
BuiltinFn::StringParseInt => {
|
|
let s = Self::expect_arg_1::<String>(&args, "String.parseInt", span)?;
|
|
let trimmed = s.trim();
|
|
match trimmed.parse::<i64>() {
|
|
Ok(n) => Ok(EvalResult::Value(Value::Constructor {
|
|
name: "Some".to_string(),
|
|
fields: vec![Value::Int(n)],
|
|
})),
|
|
Err(_) => Ok(EvalResult::Value(Value::Constructor {
|
|
name: "None".to_string(),
|
|
fields: vec![],
|
|
})),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::StringParseFloat => {
|
|
let s = Self::expect_arg_1::<String>(&args, "String.parseFloat", span)?;
|
|
let trimmed = s.trim();
|
|
match trimmed.parse::<f64>() {
|
|
Ok(f) => Ok(EvalResult::Value(Value::Constructor {
|
|
name: "Some".to_string(),
|
|
fields: vec![Value::Float(f)],
|
|
})),
|
|
Err(_) => Ok(EvalResult::Value(Value::Constructor {
|
|
name: "None".to_string(),
|
|
fields: vec![],
|
|
})),
|
|
}
|
|
}
|
|
|
|
// 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::IntToString => {
|
|
if args.len() != 1 {
|
|
return Err(err("Int.toString requires 1 argument"));
|
|
}
|
|
match &args[0] {
|
|
Value::Int(n) => Ok(EvalResult::Value(Value::String(format!("{}", n)))),
|
|
v => Ok(EvalResult::Value(Value::String(format!("{}", v)))),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::FloatToString => {
|
|
if args.len() != 1 {
|
|
return Err(err("Float.toString requires 1 argument"));
|
|
}
|
|
match &args[0] {
|
|
Value::Float(f) => Ok(EvalResult::Value(Value::String(format!("{}", f)))),
|
|
v => Ok(EvalResult::Value(Value::String(format!("{}", v)))),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::IntToFloat => {
|
|
if args.len() != 1 {
|
|
return Err(err("Int.toFloat requires 1 argument"));
|
|
}
|
|
match &args[0] {
|
|
Value::Int(n) => Ok(EvalResult::Value(Value::Float(*n as f64))),
|
|
v => Err(err(&format!("Int.toFloat expects Int, got {}", v.type_name()))),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::FloatToInt => {
|
|
if args.len() != 1 {
|
|
return Err(err("Float.toInt requires 1 argument"));
|
|
}
|
|
match &args[0] {
|
|
Value::Float(f) => Ok(EvalResult::Value(Value::Int(*f as i64))),
|
|
v => Err(err(&format!("Float.toInt expects Float, got {}", v.type_name()))),
|
|
}
|
|
}
|
|
|
|
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")),
|
|
};
|
|
// Use migrate_value which executes registered migrations
|
|
let migrated = self.migrate_value(args[0].clone(), target)?;
|
|
Ok(EvalResult::Value(migrated))
|
|
}
|
|
|
|
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()))),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::MathSin => {
|
|
if args.len() != 1 {
|
|
return Err(err("Math.sin requires 1 argument"));
|
|
}
|
|
match &args[0] {
|
|
Value::Float(n) => Ok(EvalResult::Value(Value::Float(n.sin()))),
|
|
Value::Int(n) => Ok(EvalResult::Value(Value::Float((*n as f64).sin()))),
|
|
v => Err(err(&format!("Math.sin expects number, got {}", v.type_name()))),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::MathCos => {
|
|
if args.len() != 1 {
|
|
return Err(err("Math.cos requires 1 argument"));
|
|
}
|
|
match &args[0] {
|
|
Value::Float(n) => Ok(EvalResult::Value(Value::Float(n.cos()))),
|
|
Value::Int(n) => Ok(EvalResult::Value(Value::Float((*n as f64).cos()))),
|
|
v => Err(err(&format!("Math.cos expects number, got {}", v.type_name()))),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::MathAtan2 => {
|
|
if args.len() != 2 {
|
|
return Err(err("Math.atan2 requires 2 arguments: y, x"));
|
|
}
|
|
let y = match &args[0] {
|
|
Value::Float(n) => *n,
|
|
Value::Int(n) => *n as f64,
|
|
v => return Err(err(&format!("Math.atan2 expects number, got {}", v.type_name()))),
|
|
};
|
|
let x = match &args[1] {
|
|
Value::Float(n) => *n,
|
|
Value::Int(n) => *n as f64,
|
|
v => return Err(err(&format!("Math.atan2 expects number, got {}", v.type_name()))),
|
|
};
|
|
Ok(EvalResult::Value(Value::Float(y.atan2(x))))
|
|
}
|
|
|
|
// 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::ListFindIndex => {
|
|
let (list, func) = Self::expect_args_2::<Vec<Value>, Value>(&args, "List.findIndex", span)?;
|
|
for (i, item) in list.iter().enumerate() {
|
|
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![Value::Int(i as i64)],
|
|
}));
|
|
}
|
|
Value::Bool(false) => {}
|
|
_ => return Err(err("List.findIndex predicate must return Bool")),
|
|
}
|
|
}
|
|
Ok(EvalResult::Value(Value::Constructor {
|
|
name: "None".to_string(),
|
|
fields: vec![],
|
|
}))
|
|
}
|
|
|
|
BuiltinFn::ListZip => {
|
|
let (list1, list2) = Self::expect_args_2::<Vec<Value>, Vec<Value>>(&args, "List.zip", span)?;
|
|
let result: Vec<Value> = list1
|
|
.into_iter()
|
|
.zip(list2.into_iter())
|
|
.map(|(a, b)| Value::Tuple(vec![a, b]))
|
|
.collect();
|
|
Ok(EvalResult::Value(Value::List(result)))
|
|
}
|
|
|
|
BuiltinFn::ListFlatten => {
|
|
let list = Self::expect_arg_1::<Vec<Value>>(&args, "List.flatten", span)?;
|
|
let mut result = Vec::new();
|
|
for item in list {
|
|
match item {
|
|
Value::List(inner) => result.extend(inner),
|
|
other => result.push(other),
|
|
}
|
|
}
|
|
Ok(EvalResult::Value(Value::List(result)))
|
|
}
|
|
|
|
BuiltinFn::ListContains => {
|
|
let (list, target) = Self::expect_args_2::<Vec<Value>, Value>(&args, "List.contains", span)?;
|
|
let found = list.iter().any(|item| Value::values_equal(item, &target));
|
|
Ok(EvalResult::Value(Value::Bool(found)))
|
|
}
|
|
|
|
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)))
|
|
}
|
|
|
|
BuiltinFn::ListForEach => {
|
|
// List.forEach(list, fn(item) => { effectful code })
|
|
// Unlike map, forEach doesn't collect results - it just runs effects
|
|
let (list, func) =
|
|
Self::expect_args_2::<Vec<Value>, Value>(&args, "List.forEach", span)?;
|
|
for item in list {
|
|
// Call the function for each item, ignoring the result
|
|
self.eval_call_to_value(func.clone(), vec![item], span)?;
|
|
}
|
|
Ok(EvalResult::Value(Value::Unit))
|
|
}
|
|
|
|
BuiltinFn::ListSort => {
|
|
// List.sort(list) - sort using natural ordering (Int, Float, String, Bool)
|
|
let mut list =
|
|
Self::expect_arg_1::<Vec<Value>>(&args, "List.sort", span)?;
|
|
list.sort_by(|a, b| Self::compare_values(a, b));
|
|
Ok(EvalResult::Value(Value::List(list)))
|
|
}
|
|
|
|
BuiltinFn::ListSortBy => {
|
|
// List.sortBy(list, fn(a, b) => Int) - sort with custom comparator
|
|
// Comparator returns negative (a < b), 0 (a == b), or positive (a > b)
|
|
let (list, func) =
|
|
Self::expect_args_2::<Vec<Value>, Value>(&args, "List.sortBy", span)?;
|
|
let mut indexed: Vec<(usize, Value)> =
|
|
list.into_iter().enumerate().collect();
|
|
let mut err: Option<RuntimeError> = None;
|
|
let func_ref = &func;
|
|
let self_ptr = self as *mut Self;
|
|
indexed.sort_by(|a, b| {
|
|
if err.is_some() {
|
|
return std::cmp::Ordering::Equal;
|
|
}
|
|
// Safety: we're in a single-threaded context and the closure
|
|
// needs mutable access to call eval_call_to_value
|
|
let interp = unsafe { &mut *self_ptr };
|
|
match interp.eval_call_to_value(
|
|
func_ref.clone(),
|
|
vec![a.1.clone(), b.1.clone()],
|
|
span,
|
|
) {
|
|
Ok(Value::Int(n)) => {
|
|
if n < 0 {
|
|
std::cmp::Ordering::Less
|
|
} else if n > 0 {
|
|
std::cmp::Ordering::Greater
|
|
} else {
|
|
std::cmp::Ordering::Equal
|
|
}
|
|
}
|
|
Ok(_) => {
|
|
err = Some(RuntimeError {
|
|
message: "List.sortBy comparator must return Int"
|
|
.to_string(),
|
|
span: Some(span),
|
|
});
|
|
std::cmp::Ordering::Equal
|
|
}
|
|
Err(e) => {
|
|
err = Some(e);
|
|
std::cmp::Ordering::Equal
|
|
}
|
|
}
|
|
});
|
|
if let Some(e) = err {
|
|
return Err(e);
|
|
}
|
|
let result: Vec<Value> =
|
|
indexed.into_iter().map(|(_, v)| v).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)))
|
|
}
|
|
|
|
BuiltinFn::StringFromChar => {
|
|
if args.len() != 1 {
|
|
return Err(err("String.fromChar requires 1 argument: char"));
|
|
}
|
|
let c = match &args[0] {
|
|
Value::Char(c) => *c,
|
|
v => return Err(err(&format!("String.fromChar expects Char, got {}", v.type_name()))),
|
|
};
|
|
Ok(EvalResult::Value(Value::String(c.to_string())))
|
|
}
|
|
|
|
BuiltinFn::StringCharAt => {
|
|
if args.len() != 2 {
|
|
return Err(err("String.charAt requires 2 arguments: string, index"));
|
|
}
|
|
let s = match &args[0] {
|
|
Value::String(s) => s.clone(),
|
|
v => return Err(err(&format!("String.charAt expects String, got {}", v.type_name()))),
|
|
};
|
|
let idx = match &args[1] {
|
|
Value::Int(n) => *n as usize,
|
|
v => return Err(err(&format!("String.charAt expects Int for index, got {}", v.type_name()))),
|
|
};
|
|
let chars: Vec<char> = s.chars().collect();
|
|
if idx < chars.len() {
|
|
Ok(EvalResult::Value(Value::String(chars[idx].to_string())))
|
|
} else {
|
|
Ok(EvalResult::Value(Value::String(String::new())))
|
|
}
|
|
}
|
|
|
|
BuiltinFn::StringIndexOf => {
|
|
if args.len() != 2 {
|
|
return Err(err("String.indexOf requires 2 arguments: string, substring"));
|
|
}
|
|
let s = match &args[0] {
|
|
Value::String(s) => s.clone(),
|
|
v => return Err(err(&format!("String.indexOf expects String, got {}", v.type_name()))),
|
|
};
|
|
let sub = match &args[1] {
|
|
Value::String(s) => s.clone(),
|
|
v => return Err(err(&format!("String.indexOf expects String for substring, got {}", v.type_name()))),
|
|
};
|
|
match s.find(&sub) {
|
|
Some(idx) => Ok(EvalResult::Value(Value::Constructor {
|
|
name: "Some".to_string(),
|
|
fields: vec![Value::Int(idx as i64)],
|
|
})),
|
|
None => Ok(EvalResult::Value(Value::Constructor {
|
|
name: "None".to_string(),
|
|
fields: vec![],
|
|
})),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::StringLastIndexOf => {
|
|
if args.len() != 2 {
|
|
return Err(err("String.lastIndexOf requires 2 arguments: string, substring"));
|
|
}
|
|
let s = match &args[0] {
|
|
Value::String(s) => s.clone(),
|
|
v => return Err(err(&format!("String.lastIndexOf expects String, got {}", v.type_name()))),
|
|
};
|
|
let sub = match &args[1] {
|
|
Value::String(s) => s.clone(),
|
|
v => return Err(err(&format!("String.lastIndexOf expects String for substring, got {}", v.type_name()))),
|
|
};
|
|
match s.rfind(&sub) {
|
|
Some(idx) => Ok(EvalResult::Value(Value::Constructor {
|
|
name: "Some".to_string(),
|
|
fields: vec![Value::Int(idx as i64)],
|
|
})),
|
|
None => Ok(EvalResult::Value(Value::Constructor {
|
|
name: "None".to_string(),
|
|
fields: vec![],
|
|
})),
|
|
}
|
|
}
|
|
|
|
BuiltinFn::StringRepeat => {
|
|
if args.len() != 2 {
|
|
return Err(err("String.repeat requires 2 arguments: string, count"));
|
|
}
|
|
let s = match &args[0] {
|
|
Value::String(s) => s.clone(),
|
|
v => return Err(err(&format!("String.repeat expects String, got {}", v.type_name()))),
|
|
};
|
|
let count = match &args[1] {
|
|
Value::Int(n) => (*n).max(0) as usize,
|
|
v => return Err(err(&format!("String.repeat expects Int for count, got {}", v.type_name()))),
|
|
};
|
|
Ok(EvalResult::Value(Value::String(s.repeat(count))))
|
|
}
|
|
|
|
// 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))))
|
|
}
|
|
|
|
// Map operations
|
|
BuiltinFn::MapNew => {
|
|
Ok(EvalResult::Value(Value::Map(HashMap::new())))
|
|
}
|
|
|
|
BuiltinFn::MapSet => {
|
|
if args.len() != 3 {
|
|
return Err(err("Map.set requires 3 arguments: map, key, value"));
|
|
}
|
|
let mut map = match &args[0] {
|
|
Value::Map(m) => m.clone(),
|
|
v => return Err(err(&format!("Map.set expects Map as first argument, got {}", v.type_name()))),
|
|
};
|
|
let key = match &args[1] {
|
|
Value::String(s) => s.clone(),
|
|
v => return Err(err(&format!("Map.set expects String key, got {}", v.type_name()))),
|
|
};
|
|
map.insert(key, args[2].clone());
|
|
Ok(EvalResult::Value(Value::Map(map)))
|
|
}
|
|
|
|
BuiltinFn::MapGet => {
|
|
let (map, key) = Self::expect_args_2::<HashMap<String, Value>, String>(&args, "Map.get", span)?;
|
|
match map.get(&key) {
|
|
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::MapContains => {
|
|
let (map, key) = Self::expect_args_2::<HashMap<String, Value>, String>(&args, "Map.contains", span)?;
|
|
Ok(EvalResult::Value(Value::Bool(map.contains_key(&key))))
|
|
}
|
|
|
|
BuiltinFn::MapRemove => {
|
|
let (mut map, key) = Self::expect_args_2::<HashMap<String, Value>, String>(&args, "Map.remove", span)?;
|
|
map.remove(&key);
|
|
Ok(EvalResult::Value(Value::Map(map)))
|
|
}
|
|
|
|
BuiltinFn::MapKeys => {
|
|
let map = Self::expect_arg_1::<HashMap<String, Value>>(&args, "Map.keys", span)?;
|
|
let mut keys: Vec<String> = map.keys().cloned().collect();
|
|
keys.sort();
|
|
Ok(EvalResult::Value(Value::List(
|
|
keys.into_iter().map(Value::String).collect(),
|
|
)))
|
|
}
|
|
|
|
BuiltinFn::MapValues => {
|
|
let map = Self::expect_arg_1::<HashMap<String, Value>>(&args, "Map.values", span)?;
|
|
let mut entries: Vec<(String, Value)> = map.into_iter().collect();
|
|
entries.sort_by(|(a, _), (b, _)| a.cmp(b));
|
|
Ok(EvalResult::Value(Value::List(
|
|
entries.into_iter().map(|(_, v)| v).collect(),
|
|
)))
|
|
}
|
|
|
|
BuiltinFn::MapSize => {
|
|
let map = Self::expect_arg_1::<HashMap<String, Value>>(&args, "Map.size", span)?;
|
|
Ok(EvalResult::Value(Value::Int(map.len() as i64)))
|
|
}
|
|
|
|
BuiltinFn::MapIsEmpty => {
|
|
let map = Self::expect_arg_1::<HashMap<String, Value>>(&args, "Map.isEmpty", span)?;
|
|
Ok(EvalResult::Value(Value::Bool(map.is_empty())))
|
|
}
|
|
|
|
BuiltinFn::MapFromList => {
|
|
let list = Self::expect_arg_1::<Vec<Value>>(&args, "Map.fromList", span)?;
|
|
let mut map = HashMap::new();
|
|
for item in list {
|
|
match item {
|
|
Value::Tuple(fields) if fields.len() == 2 => {
|
|
let key = match &fields[0] {
|
|
Value::String(s) => s.clone(),
|
|
v => return Err(err(&format!("Map.fromList expects (String, V) tuples, got {} key", v.type_name()))),
|
|
};
|
|
map.insert(key, fields[1].clone());
|
|
}
|
|
_ => return Err(err("Map.fromList expects List<(String, V)>")),
|
|
}
|
|
}
|
|
Ok(EvalResult::Value(Value::Map(map)))
|
|
}
|
|
|
|
BuiltinFn::MapToList => {
|
|
let map = Self::expect_arg_1::<HashMap<String, Value>>(&args, "Map.toList", span)?;
|
|
let mut entries: Vec<(String, Value)> = map.into_iter().collect();
|
|
entries.sort_by(|(a, _), (b, _)| a.cmp(b));
|
|
Ok(EvalResult::Value(Value::List(
|
|
entries
|
|
.into_iter()
|
|
.map(|(k, v)| Value::Tuple(vec![Value::String(k), v]))
|
|
.collect(),
|
|
)))
|
|
}
|
|
|
|
BuiltinFn::MapMerge => {
|
|
if args.len() != 2 {
|
|
return Err(err("Map.merge requires 2 arguments: map1, map2"));
|
|
}
|
|
let mut map1 = match &args[0] {
|
|
Value::Map(m) => m.clone(),
|
|
v => return Err(err(&format!("Map.merge expects Map as first argument, got {}", v.type_name()))),
|
|
};
|
|
let map2 = match &args[1] {
|
|
Value::Map(m) => m.clone(),
|
|
v => return Err(err(&format!("Map.merge expects Map as second argument, got {}", v.type_name()))),
|
|
};
|
|
for (k, v) in map2 {
|
|
map1.insert(k, v);
|
|
}
|
|
Ok(EvalResult::Value(Value::Map(map1)))
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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),
|
|
})
|
|
}
|
|
|
|
/// Compare two values for natural ordering (used by List.sort)
|
|
fn compare_values(a: &Value, b: &Value) -> std::cmp::Ordering {
|
|
match (a, b) {
|
|
(Value::Int(x), Value::Int(y)) => x.cmp(y),
|
|
(Value::Float(x), Value::Float(y)) => x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal),
|
|
(Value::String(x), Value::String(y)) => x.cmp(y),
|
|
(Value::Bool(x), Value::Bool(y)) => x.cmp(y),
|
|
(Value::Char(x), Value::Char(y)) => x.cmp(y),
|
|
_ => std::cmp::Ordering::Equal,
|
|
}
|
|
}
|
|
|
|
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::Record(a), Value::Record(b)) => {
|
|
a.len() == b.len() && a.iter().all(|(k, v)| {
|
|
b.get(k).map(|bv| self.values_equal(v, bv)).unwrap_or(false)
|
|
})
|
|
}
|
|
(Value::Map(a), Value::Map(b)) => {
|
|
a.len() == b.len() && a.iter().all(|(k, v)| {
|
|
b.get(k).map(|bv| self.values_equal(v, bv)).unwrap_or(false)
|
|
})
|
|
}
|
|
(
|
|
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))
|
|
}
|
|
(Value::Json(a), Value::Json(b)) => a == b,
|
|
_ => 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 onto stack (for nested handler semantics)
|
|
for h in &handler_values {
|
|
self.handler_stack.push(Rc::clone(h));
|
|
}
|
|
|
|
// Update evidence map for O(1) lookup (evidence passing optimization)
|
|
// Save previous evidence values for restoration
|
|
let mut previous_evidence: Vec<(String, Option<Rc<HandlerValue>>)> = Vec::new();
|
|
for h in &handler_values {
|
|
let effect_name = h.effect.clone();
|
|
let prev = self.evidence.insert(effect_name.clone(), Rc::clone(h));
|
|
previous_evidence.push((effect_name, prev));
|
|
}
|
|
|
|
// Evaluate expression
|
|
let result = self.eval_expr_inner(expr, env);
|
|
|
|
// Restore previous evidence (for proper nested handler support)
|
|
for (effect_name, prev) in previous_evidence.into_iter().rev() {
|
|
match prev {
|
|
Some(h) => {
|
|
self.evidence.insert(effect_name, h);
|
|
}
|
|
None => {
|
|
self.evidence.remove(&effect_name);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pop handlers from stack
|
|
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 using evidence map (O(1) lookup via evidence passing)
|
|
// This replaces the previous O(n) handler_stack search
|
|
let handler_data: Option<(Env, crate::ast::Expr, Vec<Ident>)> = self
|
|
.evidence
|
|
.get(&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") | ("Console", "readLine") => {
|
|
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()))
|
|
}
|
|
("Console", "readInt") => {
|
|
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,
|
|
})?;
|
|
let trimmed = input.trim();
|
|
match trimmed.parse::<i64>() {
|
|
Ok(n) => Ok(Value::Int(n)),
|
|
Err(_) => Err(RuntimeError {
|
|
message: format!("Invalid integer: '{}'", trimmed),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
("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,
|
|
}),
|
|
}
|
|
}
|
|
|
|
("File", "copy") => {
|
|
let source = match request.args.first() {
|
|
Some(Value::String(s)) => s.clone(),
|
|
_ => return Err(RuntimeError {
|
|
message: "File.copy requires a string source path".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
let dest = match request.args.get(1) {
|
|
Some(Value::String(s)) => s.clone(),
|
|
_ => return Err(RuntimeError {
|
|
message: "File.copy requires a string destination path".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
match std::fs::copy(&source, &dest) {
|
|
Ok(_) => Ok(Value::Unit),
|
|
Err(e) => Err(RuntimeError {
|
|
message: format!("Failed to copy '{}' to '{}': {}", source, dest, e),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
("File", "glob") => {
|
|
let pattern = match request.args.first() {
|
|
Some(Value::String(s)) => s.clone(),
|
|
_ => return Err(RuntimeError {
|
|
message: "File.glob requires a string pattern".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
match glob::glob(&pattern) {
|
|
Ok(paths) => {
|
|
let entries: Vec<Value> = paths
|
|
.filter_map(|entry| entry.ok())
|
|
.map(|path| Value::String(path.to_string_lossy().to_string()))
|
|
.collect();
|
|
Ok(Value::List(entries))
|
|
}
|
|
Err(e) => Err(RuntimeError {
|
|
message: format!("Invalid glob pattern '{}': {}", pattern, e),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
// ===== File Effect (safe Result-returning variants) =====
|
|
("File", "tryRead") => {
|
|
let path = match request.args.first() {
|
|
Some(Value::String(s)) => s.clone(),
|
|
_ => return Err(RuntimeError {
|
|
message: "File.tryRead requires a string path".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
match std::fs::read_to_string(&path) {
|
|
Ok(content) => Ok(Value::Constructor {
|
|
name: "Ok".to_string(),
|
|
fields: vec![Value::String(content)],
|
|
}),
|
|
Err(e) => Ok(Value::Constructor {
|
|
name: "Err".to_string(),
|
|
fields: vec![Value::String(format!("Failed to read file '{}': {}", path, e))],
|
|
}),
|
|
}
|
|
}
|
|
("File", "tryWrite") => {
|
|
let path = match request.args.first() {
|
|
Some(Value::String(s)) => s.clone(),
|
|
_ => return Err(RuntimeError {
|
|
message: "File.tryWrite 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.tryWrite requires string content".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
match std::fs::write(&path, &content) {
|
|
Ok(()) => Ok(Value::Constructor {
|
|
name: "Ok".to_string(),
|
|
fields: vec![Value::Unit],
|
|
}),
|
|
Err(e) => Ok(Value::Constructor {
|
|
name: "Err".to_string(),
|
|
fields: vec![Value::String(format!("Failed to write file '{}': {}", path, e))],
|
|
}),
|
|
}
|
|
}
|
|
("File", "tryDelete") => {
|
|
let path = match request.args.first() {
|
|
Some(Value::String(s)) => s.clone(),
|
|
_ => return Err(RuntimeError {
|
|
message: "File.tryDelete requires a string path".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
match std::fs::remove_file(&path) {
|
|
Ok(()) => Ok(Value::Constructor {
|
|
name: "Ok".to_string(),
|
|
fields: vec![Value::Unit],
|
|
}),
|
|
Err(e) => Ok(Value::Constructor {
|
|
name: "Err".to_string(),
|
|
fields: vec![Value::String(format!("Failed to delete file '{}': {}", path, e))],
|
|
}),
|
|
}
|
|
}
|
|
|
|
// ===== 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,
|
|
}),
|
|
}
|
|
}
|
|
|
|
// ===== Http Effect =====
|
|
("Http", "get") => {
|
|
let url = match request.args.first() {
|
|
Some(Value::String(s)) => s.clone(),
|
|
_ => return Err(RuntimeError {
|
|
message: "Http.get requires a URL string".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
self.http_request("GET", &url, None)
|
|
}
|
|
("Http", "post") => {
|
|
let (url, body) = match (request.args.get(0), request.args.get(1)) {
|
|
(Some(Value::String(u)), Some(Value::String(b))) => (u.clone(), b.clone()),
|
|
_ => return Err(RuntimeError {
|
|
message: "Http.post requires URL and body strings".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
self.http_request("POST", &url, Some(&body))
|
|
}
|
|
("Http", "postJson") => {
|
|
let (url, json) = match (request.args.get(0), request.args.get(1)) {
|
|
(Some(Value::String(u)), Some(Value::Json(j))) => (u.clone(), j.clone()),
|
|
_ => return Err(RuntimeError {
|
|
message: "Http.postJson requires URL string and Json value".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
self.http_request_json("POST", &url, &json)
|
|
}
|
|
("Http", "put") => {
|
|
let (url, body) = match (request.args.get(0), request.args.get(1)) {
|
|
(Some(Value::String(u)), Some(Value::String(b))) => (u.clone(), b.clone()),
|
|
_ => return Err(RuntimeError {
|
|
message: "Http.put requires URL and body strings".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
self.http_request("PUT", &url, Some(&body))
|
|
}
|
|
("Http", "delete") => {
|
|
let url = match request.args.first() {
|
|
Some(Value::String(s)) => s.clone(),
|
|
_ => return Err(RuntimeError {
|
|
message: "Http.delete requires a URL string".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
self.http_request("DELETE", &url, None)
|
|
}
|
|
("Http", "setHeader") => {
|
|
// Headers would need to be stored in interpreter state
|
|
// For now, this is a no-op placeholder
|
|
Ok(Value::Unit)
|
|
}
|
|
("Http", "setTimeout") => {
|
|
// Timeout would need to be stored in interpreter state
|
|
// For now, this is a no-op placeholder
|
|
Ok(Value::Unit)
|
|
}
|
|
|
|
// ===== HttpServer Effect =====
|
|
("HttpServer", "listen") => {
|
|
let port = match request.args.first() {
|
|
Some(Value::Int(p)) => *p as u16,
|
|
_ => return Err(RuntimeError {
|
|
message: "HttpServer.listen requires an integer port".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
let addr = format!("0.0.0.0:{}", port);
|
|
match tiny_http::Server::http(&addr) {
|
|
Ok(server) => {
|
|
*self.http_server.lock().unwrap() = Some(server);
|
|
Ok(Value::Unit)
|
|
}
|
|
Err(e) => Err(RuntimeError {
|
|
message: format!("Failed to start HTTP server on port {}: {}", port, e),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
("HttpServer", "accept") => {
|
|
let server_guard = self.http_server.lock().unwrap();
|
|
let server = match server_guard.as_ref() {
|
|
Some(s) => s,
|
|
None => return Err(RuntimeError {
|
|
message: "HttpServer.accept: No server is listening. Call HttpServer.listen first.".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
// Block until a request arrives
|
|
match server.recv() {
|
|
Ok(mut request) => {
|
|
// Extract request info
|
|
let method = request.method().to_string();
|
|
let path = request.url().to_string();
|
|
|
|
// Read body
|
|
let mut body = String::new();
|
|
let _ = std::io::Read::read_to_string(&mut request.as_reader(), &mut body);
|
|
|
|
// Extract headers
|
|
let headers: Vec<Value> = request.headers()
|
|
.iter()
|
|
.map(|h| Value::Tuple(vec![
|
|
Value::String(h.field.as_str().to_string()),
|
|
Value::String(h.value.as_str().to_string()),
|
|
]))
|
|
.collect();
|
|
|
|
// Store the request for respond operation
|
|
drop(server_guard); // Release lock before storing request
|
|
*self.current_http_request.lock().unwrap() = Some(request);
|
|
|
|
// Return request as a record
|
|
Ok(Value::Record(HashMap::from([
|
|
("method".to_string(), Value::String(method)),
|
|
("path".to_string(), Value::String(path)),
|
|
("body".to_string(), Value::String(body)),
|
|
("headers".to_string(), Value::List(headers)),
|
|
])))
|
|
}
|
|
Err(e) => Err(RuntimeError {
|
|
message: format!("HttpServer.accept failed: {}", e),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
("HttpServer", "respond") => {
|
|
let (status, body) = match (request.args.get(0), request.args.get(1)) {
|
|
(Some(Value::Int(s)), Some(Value::String(b))) => (*s as u16, b.clone()),
|
|
_ => return Err(RuntimeError {
|
|
message: "HttpServer.respond requires status (Int) and body (String)".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
let mut req_guard = self.current_http_request.lock().unwrap();
|
|
match req_guard.take() {
|
|
Some(http_request) => {
|
|
let status_code = tiny_http::StatusCode(status);
|
|
let response = tiny_http::Response::from_string(body)
|
|
.with_status_code(status_code);
|
|
if let Err(e) = http_request.respond(response) {
|
|
return Err(RuntimeError {
|
|
message: format!("Failed to send HTTP response: {}", e),
|
|
span: None,
|
|
});
|
|
}
|
|
Ok(Value::Unit)
|
|
}
|
|
None => Err(RuntimeError {
|
|
message: "HttpServer.respond: No pending request to respond to".to_string(),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
("HttpServer", "respondWithHeaders") => {
|
|
let (status, body, headers) = match (request.args.get(0), request.args.get(1), request.args.get(2)) {
|
|
(Some(Value::Int(s)), Some(Value::String(b)), Some(Value::List(h))) => {
|
|
(*s as u16, b.clone(), h.clone())
|
|
}
|
|
_ => return Err(RuntimeError {
|
|
message: "HttpServer.respondWithHeaders requires status (Int), body (String), and headers (List)".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
let mut req_guard = self.current_http_request.lock().unwrap();
|
|
match req_guard.take() {
|
|
Some(http_request) => {
|
|
let status_code = tiny_http::StatusCode(status);
|
|
let mut response = tiny_http::Response::from_string(body)
|
|
.with_status_code(status_code);
|
|
|
|
// Add custom headers
|
|
for header_val in headers {
|
|
if let Value::Tuple(pair) = header_val {
|
|
if let (Some(Value::String(name)), Some(Value::String(value))) =
|
|
(pair.get(0), pair.get(1))
|
|
{
|
|
if let Ok(header) = tiny_http::Header::from_bytes(
|
|
name.as_bytes(),
|
|
value.as_bytes(),
|
|
) {
|
|
response = response.with_header(header);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Err(e) = http_request.respond(response) {
|
|
return Err(RuntimeError {
|
|
message: format!("Failed to send HTTP response: {}", e),
|
|
span: None,
|
|
});
|
|
}
|
|
Ok(Value::Unit)
|
|
}
|
|
None => Err(RuntimeError {
|
|
message: "HttpServer.respondWithHeaders: No pending request to respond to".to_string(),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
("HttpServer", "stop") => {
|
|
// Drop the server to stop listening
|
|
*self.http_server.lock().unwrap() = None;
|
|
*self.current_http_request.lock().unwrap() = None;
|
|
Ok(Value::Unit)
|
|
}
|
|
|
|
// Test effect for testing framework
|
|
("Test", "assert") => {
|
|
let condition = match request.args.first() {
|
|
Some(Value::Bool(b)) => *b,
|
|
_ => false,
|
|
};
|
|
let message = match request.args.get(1) {
|
|
Some(Value::String(s)) => s.clone(),
|
|
_ => "Assertion failed".to_string(),
|
|
};
|
|
|
|
if condition {
|
|
self.test_results.borrow_mut().passed += 1;
|
|
} else {
|
|
self.test_results.borrow_mut().failed += 1;
|
|
self.test_results.borrow_mut().failures.push(TestFailure {
|
|
message,
|
|
expected: None,
|
|
actual: None,
|
|
});
|
|
}
|
|
Ok(Value::Unit)
|
|
}
|
|
("Test", "assertEqual") => {
|
|
let expected = request.args.first().cloned().unwrap_or(Value::Unit);
|
|
let actual = request.args.get(1).cloned().unwrap_or(Value::Unit);
|
|
|
|
if Value::values_equal(&expected, &actual) {
|
|
self.test_results.borrow_mut().passed += 1;
|
|
} else {
|
|
self.test_results.borrow_mut().failed += 1;
|
|
self.test_results.borrow_mut().failures.push(TestFailure {
|
|
message: "Values not equal".to_string(),
|
|
expected: Some(format!("{}", expected)),
|
|
actual: Some(format!("{}", actual)),
|
|
});
|
|
}
|
|
Ok(Value::Unit)
|
|
}
|
|
("Test", "assertEqualMsg") => {
|
|
let expected = request.args.first().cloned().unwrap_or(Value::Unit);
|
|
let actual = request.args.get(1).cloned().unwrap_or(Value::Unit);
|
|
let label = match request.args.get(2) {
|
|
Some(Value::String(s)) => s.clone(),
|
|
_ => "Values not equal".to_string(),
|
|
};
|
|
|
|
if Value::values_equal(&expected, &actual) {
|
|
self.test_results.borrow_mut().passed += 1;
|
|
} else {
|
|
self.test_results.borrow_mut().failed += 1;
|
|
self.test_results.borrow_mut().failures.push(TestFailure {
|
|
message: label,
|
|
expected: Some(format!("{}", expected)),
|
|
actual: Some(format!("{}", actual)),
|
|
});
|
|
}
|
|
Ok(Value::Unit)
|
|
}
|
|
("Test", "assertNotEqual") => {
|
|
let a = request.args.first().cloned().unwrap_or(Value::Unit);
|
|
let b = request.args.get(1).cloned().unwrap_or(Value::Unit);
|
|
|
|
if !Value::values_equal(&a, &b) {
|
|
self.test_results.borrow_mut().passed += 1;
|
|
} else {
|
|
self.test_results.borrow_mut().failed += 1;
|
|
self.test_results.borrow_mut().failures.push(TestFailure {
|
|
message: "Values should not be equal".to_string(),
|
|
expected: Some(format!("not {}", a)),
|
|
actual: Some(format!("{}", b)),
|
|
});
|
|
}
|
|
Ok(Value::Unit)
|
|
}
|
|
("Test", "assertTrue") => {
|
|
let condition = match request.args.first() {
|
|
Some(Value::Bool(b)) => *b,
|
|
_ => false,
|
|
};
|
|
|
|
if condition {
|
|
self.test_results.borrow_mut().passed += 1;
|
|
} else {
|
|
self.test_results.borrow_mut().failed += 1;
|
|
self.test_results.borrow_mut().failures.push(TestFailure {
|
|
message: "Expected true".to_string(),
|
|
expected: Some("true".to_string()),
|
|
actual: Some("false".to_string()),
|
|
});
|
|
}
|
|
Ok(Value::Unit)
|
|
}
|
|
("Test", "assertFalse") => {
|
|
let condition = match request.args.first() {
|
|
Some(Value::Bool(b)) => *b,
|
|
_ => true,
|
|
};
|
|
|
|
if !condition {
|
|
self.test_results.borrow_mut().passed += 1;
|
|
} else {
|
|
self.test_results.borrow_mut().failed += 1;
|
|
self.test_results.borrow_mut().failures.push(TestFailure {
|
|
message: "Expected false".to_string(),
|
|
expected: Some("false".to_string()),
|
|
actual: Some("true".to_string()),
|
|
});
|
|
}
|
|
Ok(Value::Unit)
|
|
}
|
|
("Test", "fail") => {
|
|
let message = match request.args.first() {
|
|
Some(Value::String(s)) => s.clone(),
|
|
_ => "Test failed".to_string(),
|
|
};
|
|
self.test_results.borrow_mut().failed += 1;
|
|
self.test_results.borrow_mut().failures.push(TestFailure {
|
|
message,
|
|
expected: None,
|
|
actual: None,
|
|
});
|
|
Ok(Value::Unit)
|
|
}
|
|
|
|
// ===== Sql Effect =====
|
|
("Sql", "open") => {
|
|
let path = match request.args.first() {
|
|
Some(Value::String(s)) => s.clone(),
|
|
_ => return Err(RuntimeError {
|
|
message: "Sql.open requires a path string".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
match Connection::open(&path) {
|
|
Ok(conn) => {
|
|
let id = *self.next_sql_conn_id.borrow();
|
|
*self.next_sql_conn_id.borrow_mut() += 1;
|
|
self.sql_connections.borrow_mut().insert(id, conn);
|
|
Ok(Value::Int(id))
|
|
}
|
|
Err(e) => Err(RuntimeError {
|
|
message: format!("Sql.open failed: {}", e),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
("Sql", "openMemory") => {
|
|
match Connection::open_in_memory() {
|
|
Ok(conn) => {
|
|
let id = *self.next_sql_conn_id.borrow();
|
|
*self.next_sql_conn_id.borrow_mut() += 1;
|
|
self.sql_connections.borrow_mut().insert(id, conn);
|
|
Ok(Value::Int(id))
|
|
}
|
|
Err(e) => Err(RuntimeError {
|
|
message: format!("Sql.openMemory failed: {}", e),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
("Sql", "close") => {
|
|
let conn_id = match request.args.first() {
|
|
Some(Value::Int(id)) => *id,
|
|
_ => return Err(RuntimeError {
|
|
message: "Sql.close requires a connection ID".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
if self.sql_connections.borrow_mut().remove(&conn_id).is_some() {
|
|
Ok(Value::Unit)
|
|
} else {
|
|
Err(RuntimeError {
|
|
message: format!("Sql.close: invalid connection ID {}", conn_id),
|
|
span: None,
|
|
})
|
|
}
|
|
}
|
|
("Sql", "execute") => {
|
|
let (conn_id, sql) = match (request.args.get(0), request.args.get(1)) {
|
|
(Some(Value::Int(id)), Some(Value::String(s))) => (*id, s.clone()),
|
|
_ => return Err(RuntimeError {
|
|
message: "Sql.execute requires connection ID and SQL string".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
let conns = self.sql_connections.borrow();
|
|
let conn = match conns.get(&conn_id) {
|
|
Some(c) => c,
|
|
None => return Err(RuntimeError {
|
|
message: format!("Sql.execute: invalid connection ID {}", conn_id),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
match conn.execute(&sql, []) {
|
|
Ok(rows_affected) => Ok(Value::Int(rows_affected as i64)),
|
|
Err(e) => Err(RuntimeError {
|
|
message: format!("Sql.execute failed: {}", e),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
("Sql", "query") => {
|
|
let (conn_id, sql) = match (request.args.get(0), request.args.get(1)) {
|
|
(Some(Value::Int(id)), Some(Value::String(s))) => (*id, s.clone()),
|
|
_ => return Err(RuntimeError {
|
|
message: "Sql.query requires connection ID and SQL string".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
let conns = self.sql_connections.borrow();
|
|
let conn = match conns.get(&conn_id) {
|
|
Some(c) => c,
|
|
None => return Err(RuntimeError {
|
|
message: format!("Sql.query: invalid connection ID {}", conn_id),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
let mut stmt = match conn.prepare(&sql) {
|
|
Ok(s) => s,
|
|
Err(e) => return Err(RuntimeError {
|
|
message: format!("Sql.query prepare failed: {}", e),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
let column_names: Vec<String> = stmt.column_names().iter().map(|s| s.to_string()).collect();
|
|
let column_count = column_names.len();
|
|
|
|
let rows: Result<Vec<Value>, _> = stmt.query_map([], |row| {
|
|
let mut record = HashMap::new();
|
|
for (i, col_name) in column_names.iter().enumerate() {
|
|
let value = match row.get_ref(i) {
|
|
Ok(rusqlite::types::ValueRef::Null) => Value::Constructor {
|
|
name: "None".to_string(),
|
|
fields: vec![],
|
|
},
|
|
Ok(rusqlite::types::ValueRef::Integer(n)) => Value::Int(n),
|
|
Ok(rusqlite::types::ValueRef::Real(f)) => Value::Float(f),
|
|
Ok(rusqlite::types::ValueRef::Text(s)) => {
|
|
Value::String(String::from_utf8_lossy(s).to_string())
|
|
}
|
|
Ok(rusqlite::types::ValueRef::Blob(b)) => {
|
|
Value::String(format!("<blob {} bytes>", b.len()))
|
|
}
|
|
Err(_) => Value::String("<error>".to_string()),
|
|
};
|
|
record.insert(col_name.clone(), value);
|
|
}
|
|
Ok(Value::Record(record))
|
|
}).and_then(|rows| rows.collect());
|
|
|
|
match rows {
|
|
Ok(r) => Ok(Value::List(r)),
|
|
Err(e) => Err(RuntimeError {
|
|
message: format!("Sql.query failed: {}", e),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
("Sql", "queryOne") => {
|
|
let (conn_id, sql) = match (request.args.get(0), request.args.get(1)) {
|
|
(Some(Value::Int(id)), Some(Value::String(s))) => (*id, s.clone()),
|
|
_ => return Err(RuntimeError {
|
|
message: "Sql.queryOne requires connection ID and SQL string".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
let conns = self.sql_connections.borrow();
|
|
let conn = match conns.get(&conn_id) {
|
|
Some(c) => c,
|
|
None => return Err(RuntimeError {
|
|
message: format!("Sql.queryOne: invalid connection ID {}", conn_id),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
let mut stmt = match conn.prepare(&sql) {
|
|
Ok(s) => s,
|
|
Err(e) => return Err(RuntimeError {
|
|
message: format!("Sql.queryOne prepare failed: {}", e),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
let column_names: Vec<String> = stmt.column_names().iter().map(|s| s.to_string()).collect();
|
|
|
|
let result = stmt.query_row([], |row| {
|
|
let mut record = HashMap::new();
|
|
for (i, col_name) in column_names.iter().enumerate() {
|
|
let value = match row.get_ref(i) {
|
|
Ok(rusqlite::types::ValueRef::Null) => Value::Constructor {
|
|
name: "None".to_string(),
|
|
fields: vec![],
|
|
},
|
|
Ok(rusqlite::types::ValueRef::Integer(n)) => Value::Int(n),
|
|
Ok(rusqlite::types::ValueRef::Real(f)) => Value::Float(f),
|
|
Ok(rusqlite::types::ValueRef::Text(s)) => {
|
|
Value::String(String::from_utf8_lossy(s).to_string())
|
|
}
|
|
Ok(rusqlite::types::ValueRef::Blob(b)) => {
|
|
Value::String(format!("<blob {} bytes>", b.len()))
|
|
}
|
|
Err(_) => Value::String("<error>".to_string()),
|
|
};
|
|
record.insert(col_name.clone(), value);
|
|
}
|
|
Ok(Value::Record(record))
|
|
});
|
|
|
|
match result {
|
|
Ok(row) => Ok(Value::Constructor {
|
|
name: "Some".to_string(),
|
|
fields: vec![row],
|
|
}),
|
|
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(Value::Constructor {
|
|
name: "None".to_string(),
|
|
fields: vec![],
|
|
}),
|
|
Err(e) => Err(RuntimeError {
|
|
message: format!("Sql.queryOne failed: {}", e),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
("Sql", "beginTx") => {
|
|
let conn_id = match request.args.first() {
|
|
Some(Value::Int(id)) => *id,
|
|
_ => return Err(RuntimeError {
|
|
message: "Sql.beginTx requires a connection ID".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
let conns = self.sql_connections.borrow();
|
|
let conn = match conns.get(&conn_id) {
|
|
Some(c) => c,
|
|
None => return Err(RuntimeError {
|
|
message: format!("Sql.beginTx: invalid connection ID {}", conn_id),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
match conn.execute("BEGIN TRANSACTION", []) {
|
|
Ok(_) => Ok(Value::Unit),
|
|
Err(e) => Err(RuntimeError {
|
|
message: format!("Sql.beginTx failed: {}", e),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
("Sql", "commit") => {
|
|
let conn_id = match request.args.first() {
|
|
Some(Value::Int(id)) => *id,
|
|
_ => return Err(RuntimeError {
|
|
message: "Sql.commit requires a connection ID".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
let conns = self.sql_connections.borrow();
|
|
let conn = match conns.get(&conn_id) {
|
|
Some(c) => c,
|
|
None => return Err(RuntimeError {
|
|
message: format!("Sql.commit: invalid connection ID {}", conn_id),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
match conn.execute("COMMIT", []) {
|
|
Ok(_) => Ok(Value::Unit),
|
|
Err(e) => Err(RuntimeError {
|
|
message: format!("Sql.commit failed: {}", e),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
("Sql", "rollback") => {
|
|
let conn_id = match request.args.first() {
|
|
Some(Value::Int(id)) => *id,
|
|
_ => return Err(RuntimeError {
|
|
message: "Sql.rollback requires a connection ID".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
let conns = self.sql_connections.borrow();
|
|
let conn = match conns.get(&conn_id) {
|
|
Some(c) => c,
|
|
None => return Err(RuntimeError {
|
|
message: format!("Sql.rollback: invalid connection ID {}", conn_id),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
match conn.execute("ROLLBACK", []) {
|
|
Ok(_) => Ok(Value::Unit),
|
|
Err(e) => Err(RuntimeError {
|
|
message: format!("Sql.rollback failed: {}", e),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// PostgreSQL Effect
|
|
// ============================================================
|
|
|
|
("Postgres", "connect") => {
|
|
let conn_str = match request.args.first() {
|
|
Some(Value::String(s)) => s.clone(),
|
|
_ => return Err(RuntimeError {
|
|
message: "Postgres.connect requires a connection string".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
match PgClient::connect(&conn_str, NoTls) {
|
|
Ok(client) => {
|
|
let id = *self.next_pg_conn_id.borrow();
|
|
*self.next_pg_conn_id.borrow_mut() += 1;
|
|
self.pg_connections.borrow_mut().insert(id, client);
|
|
Ok(Value::Int(id))
|
|
}
|
|
Err(e) => Err(RuntimeError {
|
|
message: format!("Postgres.connect failed: {}", e),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
("Postgres", "close") => {
|
|
let conn_id = match request.args.first() {
|
|
Some(Value::Int(id)) => *id,
|
|
_ => return Err(RuntimeError {
|
|
message: "Postgres.close requires a connection ID".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
if self.pg_connections.borrow_mut().remove(&conn_id).is_some() {
|
|
Ok(Value::Unit)
|
|
} else {
|
|
Err(RuntimeError {
|
|
message: format!("Postgres.close: invalid connection ID {}", conn_id),
|
|
span: None,
|
|
})
|
|
}
|
|
}
|
|
|
|
("Postgres", "execute") => {
|
|
let (conn_id, sql) = match (request.args.get(0), request.args.get(1)) {
|
|
(Some(Value::Int(id)), Some(Value::String(s))) => (*id, s.clone()),
|
|
_ => return Err(RuntimeError {
|
|
message: "Postgres.execute requires connection ID and SQL string".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
let mut conns = self.pg_connections.borrow_mut();
|
|
let conn = match conns.get_mut(&conn_id) {
|
|
Some(c) => c,
|
|
None => return Err(RuntimeError {
|
|
message: format!("Postgres.execute: invalid connection ID {}", conn_id),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
match conn.execute(&sql, &[]) {
|
|
Ok(rows) => Ok(Value::Int(rows as i64)),
|
|
Err(e) => Err(RuntimeError {
|
|
message: format!("Postgres.execute failed: {}", e),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
("Postgres", "query") => {
|
|
let (conn_id, sql) = match (request.args.get(0), request.args.get(1)) {
|
|
(Some(Value::Int(id)), Some(Value::String(s))) => (*id, s.clone()),
|
|
_ => return Err(RuntimeError {
|
|
message: "Postgres.query requires connection ID and SQL string".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
let mut conns = self.pg_connections.borrow_mut();
|
|
let conn = match conns.get_mut(&conn_id) {
|
|
Some(c) => c,
|
|
None => return Err(RuntimeError {
|
|
message: format!("Postgres.query: invalid connection ID {}", conn_id),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
match conn.query(&sql, &[]) {
|
|
Ok(rows) => {
|
|
let mut results = Vec::new();
|
|
for row in rows {
|
|
let mut record = HashMap::new();
|
|
for (i, col) in row.columns().iter().enumerate() {
|
|
let col_name = col.name().to_string();
|
|
let value: Value = match col.type_().name() {
|
|
"int4" | "int8" | "int2" => {
|
|
let v: Option<i64> = row.get(i);
|
|
match v {
|
|
Some(n) => Value::Int(n),
|
|
None => Value::Constructor { name: "None".to_string(), fields: vec![] },
|
|
}
|
|
}
|
|
"float4" | "float8" => {
|
|
let v: Option<f64> = row.get(i);
|
|
match v {
|
|
Some(n) => Value::Float(n),
|
|
None => Value::Constructor { name: "None".to_string(), fields: vec![] },
|
|
}
|
|
}
|
|
"bool" => {
|
|
let v: Option<bool> = row.get(i);
|
|
match v {
|
|
Some(b) => Value::Bool(b),
|
|
None => Value::Constructor { name: "None".to_string(), fields: vec![] },
|
|
}
|
|
}
|
|
"text" | "varchar" | "char" | "bpchar" | "name" => {
|
|
let v: Option<String> = row.get(i);
|
|
match v {
|
|
Some(s) => Value::String(s),
|
|
None => Value::Constructor { name: "None".to_string(), fields: vec![] },
|
|
}
|
|
}
|
|
_ => {
|
|
// Try to get as string for other types
|
|
let v: Option<String> = row.try_get(i).ok().flatten();
|
|
match v {
|
|
Some(s) => Value::String(s),
|
|
None => Value::Constructor { name: "None".to_string(), fields: vec![] },
|
|
}
|
|
}
|
|
};
|
|
record.insert(col_name, value);
|
|
}
|
|
results.push(Value::Record(record));
|
|
}
|
|
Ok(Value::List(results))
|
|
}
|
|
Err(e) => Err(RuntimeError {
|
|
message: format!("Postgres.query failed: {}", e),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
("Postgres", "queryOne") => {
|
|
let (conn_id, sql) = match (request.args.get(0), request.args.get(1)) {
|
|
(Some(Value::Int(id)), Some(Value::String(s))) => (*id, s.clone()),
|
|
_ => return Err(RuntimeError {
|
|
message: "Postgres.queryOne requires connection ID and SQL string".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
let mut conns = self.pg_connections.borrow_mut();
|
|
let conn = match conns.get_mut(&conn_id) {
|
|
Some(c) => c,
|
|
None => return Err(RuntimeError {
|
|
message: format!("Postgres.queryOne: invalid connection ID {}", conn_id),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
match conn.query_opt(&sql, &[]) {
|
|
Ok(Some(row)) => {
|
|
let mut record = HashMap::new();
|
|
for (i, col) in row.columns().iter().enumerate() {
|
|
let col_name = col.name().to_string();
|
|
let value: Value = match col.type_().name() {
|
|
"int4" | "int8" | "int2" => {
|
|
let v: Option<i64> = row.get(i);
|
|
match v {
|
|
Some(n) => Value::Int(n),
|
|
None => Value::Constructor { name: "None".to_string(), fields: vec![] },
|
|
}
|
|
}
|
|
"float4" | "float8" => {
|
|
let v: Option<f64> = row.get(i);
|
|
match v {
|
|
Some(n) => Value::Float(n),
|
|
None => Value::Constructor { name: "None".to_string(), fields: vec![] },
|
|
}
|
|
}
|
|
"bool" => {
|
|
let v: Option<bool> = row.get(i);
|
|
match v {
|
|
Some(b) => Value::Bool(b),
|
|
None => Value::Constructor { name: "None".to_string(), fields: vec![] },
|
|
}
|
|
}
|
|
"text" | "varchar" | "char" | "bpchar" | "name" => {
|
|
let v: Option<String> = row.get(i);
|
|
match v {
|
|
Some(s) => Value::String(s),
|
|
None => Value::Constructor { name: "None".to_string(), fields: vec![] },
|
|
}
|
|
}
|
|
_ => {
|
|
let v: Option<String> = row.try_get(i).ok().flatten();
|
|
match v {
|
|
Some(s) => Value::String(s),
|
|
None => Value::Constructor { name: "None".to_string(), fields: vec![] },
|
|
}
|
|
}
|
|
};
|
|
record.insert(col_name, value);
|
|
}
|
|
Ok(Value::Constructor {
|
|
name: "Some".to_string(),
|
|
fields: vec![Value::Record(record)],
|
|
})
|
|
}
|
|
Ok(None) => Ok(Value::Constructor {
|
|
name: "None".to_string(),
|
|
fields: vec![],
|
|
}),
|
|
Err(e) => Err(RuntimeError {
|
|
message: format!("Postgres.queryOne failed: {}", e),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
("Postgres", "beginTx") => {
|
|
let conn_id = match request.args.first() {
|
|
Some(Value::Int(id)) => *id,
|
|
_ => return Err(RuntimeError {
|
|
message: "Postgres.beginTx requires a connection ID".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
let mut conns = self.pg_connections.borrow_mut();
|
|
let conn = match conns.get_mut(&conn_id) {
|
|
Some(c) => c,
|
|
None => return Err(RuntimeError {
|
|
message: format!("Postgres.beginTx: invalid connection ID {}", conn_id),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
match conn.execute("BEGIN", &[]) {
|
|
Ok(_) => Ok(Value::Unit),
|
|
Err(e) => Err(RuntimeError {
|
|
message: format!("Postgres.beginTx failed: {}", e),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
("Postgres", "commit") => {
|
|
let conn_id = match request.args.first() {
|
|
Some(Value::Int(id)) => *id,
|
|
_ => return Err(RuntimeError {
|
|
message: "Postgres.commit requires a connection ID".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
let mut conns = self.pg_connections.borrow_mut();
|
|
let conn = match conns.get_mut(&conn_id) {
|
|
Some(c) => c,
|
|
None => return Err(RuntimeError {
|
|
message: format!("Postgres.commit: invalid connection ID {}", conn_id),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
match conn.execute("COMMIT", &[]) {
|
|
Ok(_) => Ok(Value::Unit),
|
|
Err(e) => Err(RuntimeError {
|
|
message: format!("Postgres.commit failed: {}", e),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
("Postgres", "rollback") => {
|
|
let conn_id = match request.args.first() {
|
|
Some(Value::Int(id)) => *id,
|
|
_ => return Err(RuntimeError {
|
|
message: "Postgres.rollback requires a connection ID".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
let mut conns = self.pg_connections.borrow_mut();
|
|
let conn = match conns.get_mut(&conn_id) {
|
|
Some(c) => c,
|
|
None => return Err(RuntimeError {
|
|
message: format!("Postgres.rollback: invalid connection ID {}", conn_id),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
match conn.execute("ROLLBACK", &[]) {
|
|
Ok(_) => Ok(Value::Unit),
|
|
Err(e) => Err(RuntimeError {
|
|
message: format!("Postgres.rollback failed: {}", e),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
// ===== Concurrent Effect =====
|
|
("Concurrent", "spawn") => {
|
|
// For now, spawn just stores the thunk - it will be evaluated on await
|
|
// In a real implementation, this would start a thread/fiber
|
|
let thunk = match request.args.first() {
|
|
Some(v) => v.clone(),
|
|
_ => return Err(RuntimeError {
|
|
message: "Concurrent.spawn requires a thunk argument".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
let task_id = *self.next_task_id.borrow();
|
|
*self.next_task_id.borrow_mut() += 1;
|
|
|
|
// Store task: (thunk, None for result, not cancelled)
|
|
self.concurrent_tasks.borrow_mut().insert(task_id, (thunk, None, false));
|
|
|
|
Ok(Value::Int(task_id))
|
|
}
|
|
("Concurrent", "await") => {
|
|
let task_id = match request.args.first() {
|
|
Some(Value::Int(id)) => *id,
|
|
_ => return Err(RuntimeError {
|
|
message: "Concurrent.await requires a task ID".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
// Check if already computed or cancelled
|
|
let task_info = {
|
|
let tasks = self.concurrent_tasks.borrow();
|
|
tasks.get(&task_id).cloned()
|
|
};
|
|
|
|
match task_info {
|
|
Some((_, Some(result), _)) => Ok(result),
|
|
Some((_, _, true)) => Err(RuntimeError {
|
|
message: format!("Task {} was cancelled", task_id),
|
|
span: None,
|
|
}),
|
|
Some((thunk, None, false)) => {
|
|
// For cooperative concurrency, we just need to signal
|
|
// that we're waiting on this task
|
|
// Return the thunk to be evaluated by the caller
|
|
// This is a simplification - real async would use fibers
|
|
Ok(thunk)
|
|
}
|
|
None => Err(RuntimeError {
|
|
message: format!("Unknown task ID: {}", task_id),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
("Concurrent", "yield") => {
|
|
// In cooperative concurrency, yield allows other tasks to run
|
|
// For now, this is a no-op in our single-threaded model
|
|
Ok(Value::Unit)
|
|
}
|
|
("Concurrent", "sleep") => {
|
|
// Non-blocking sleep (delegates to thread sleep for now)
|
|
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)
|
|
}
|
|
("Concurrent", "cancel") => {
|
|
let task_id = match request.args.first() {
|
|
Some(Value::Int(id)) => *id,
|
|
_ => return Err(RuntimeError {
|
|
message: "Concurrent.cancel requires a task ID".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
let mut tasks = self.concurrent_tasks.borrow_mut();
|
|
if let Some((thunk, result, _)) = tasks.get(&task_id).cloned() {
|
|
tasks.insert(task_id, (thunk, result, true));
|
|
Ok(Value::Bool(true))
|
|
} else {
|
|
Ok(Value::Bool(false))
|
|
}
|
|
}
|
|
("Concurrent", "isRunning") => {
|
|
let task_id = match request.args.first() {
|
|
Some(Value::Int(id)) => *id,
|
|
_ => return Err(RuntimeError {
|
|
message: "Concurrent.isRunning requires a task ID".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
let tasks = self.concurrent_tasks.borrow();
|
|
let is_running = match tasks.get(&task_id) {
|
|
Some((_, None, false)) => true, // Not completed and not cancelled
|
|
_ => false,
|
|
};
|
|
Ok(Value::Bool(is_running))
|
|
}
|
|
("Concurrent", "taskCount") => {
|
|
let tasks = self.concurrent_tasks.borrow();
|
|
let count = tasks.iter()
|
|
.filter(|(_, (_, result, cancelled))| result.is_none() && !cancelled)
|
|
.count();
|
|
Ok(Value::Int(count as i64))
|
|
}
|
|
|
|
// ===== Channel Effect =====
|
|
("Channel", "create") => {
|
|
let channel_id = *self.next_channel_id.borrow();
|
|
*self.next_channel_id.borrow_mut() += 1;
|
|
|
|
// Create empty channel queue, not closed
|
|
self.channels.borrow_mut().insert(channel_id, (Vec::new(), false));
|
|
|
|
Ok(Value::Int(channel_id))
|
|
}
|
|
("Channel", "send") => {
|
|
let channel_id = match request.args.first() {
|
|
Some(Value::Int(id)) => *id,
|
|
_ => return Err(RuntimeError {
|
|
message: "Channel.send requires a channel ID".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
let value = match request.args.get(1) {
|
|
Some(v) => v.clone(),
|
|
_ => return Err(RuntimeError {
|
|
message: "Channel.send requires a value".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
let mut channels = self.channels.borrow_mut();
|
|
match channels.get_mut(&channel_id) {
|
|
Some((queue, false)) => {
|
|
queue.push(value);
|
|
Ok(Value::Unit)
|
|
}
|
|
Some((_, true)) => Err(RuntimeError {
|
|
message: format!("Channel {} is closed", channel_id),
|
|
span: None,
|
|
}),
|
|
None => Err(RuntimeError {
|
|
message: format!("Unknown channel ID: {}", channel_id),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
("Channel", "receive") => {
|
|
let channel_id = match request.args.first() {
|
|
Some(Value::Int(id)) => *id,
|
|
_ => return Err(RuntimeError {
|
|
message: "Channel.receive requires a channel ID".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
let mut channels = self.channels.borrow_mut();
|
|
match channels.get_mut(&channel_id) {
|
|
Some((queue, _)) if !queue.is_empty() => {
|
|
Ok(queue.remove(0))
|
|
}
|
|
Some((_, true)) => Err(RuntimeError {
|
|
message: format!("Channel {} is closed and empty", channel_id),
|
|
span: None,
|
|
}),
|
|
Some((_, false)) => Err(RuntimeError {
|
|
message: format!("Channel {} is empty (blocking receive not supported yet)", channel_id),
|
|
span: None,
|
|
}),
|
|
None => Err(RuntimeError {
|
|
message: format!("Unknown channel ID: {}", channel_id),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
("Channel", "tryReceive") => {
|
|
let channel_id = match request.args.first() {
|
|
Some(Value::Int(id)) => *id,
|
|
_ => return Err(RuntimeError {
|
|
message: "Channel.tryReceive requires a channel ID".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
let mut channels = self.channels.borrow_mut();
|
|
match channels.get_mut(&channel_id) {
|
|
Some((queue, _)) if !queue.is_empty() => {
|
|
Ok(Value::Constructor {
|
|
name: "Some".to_string(),
|
|
fields: vec![queue.remove(0)],
|
|
})
|
|
}
|
|
Some(_) => {
|
|
Ok(Value::Constructor {
|
|
name: "None".to_string(),
|
|
fields: vec![],
|
|
})
|
|
}
|
|
None => Err(RuntimeError {
|
|
message: format!("Unknown channel ID: {}", channel_id),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
("Channel", "close") => {
|
|
let channel_id = match request.args.first() {
|
|
Some(Value::Int(id)) => *id,
|
|
_ => return Err(RuntimeError {
|
|
message: "Channel.close requires a channel ID".to_string(),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
let mut channels = self.channels.borrow_mut();
|
|
if let Some((queue, closed)) = channels.get_mut(&channel_id) {
|
|
*closed = true;
|
|
Ok(Value::Unit)
|
|
} else {
|
|
Err(RuntimeError {
|
|
message: format!("Unknown channel ID: {}", channel_id),
|
|
span: None,
|
|
})
|
|
}
|
|
}
|
|
|
|
_ => Err(RuntimeError {
|
|
message: format!(
|
|
"Unhandled effect operation: {}.{}",
|
|
request.effect, request.operation
|
|
),
|
|
span: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
/// Helper for HTTP requests
|
|
fn http_request(&self, method: &str, url: &str, body: Option<&str>) -> Result<Value, RuntimeError> {
|
|
use reqwest::blocking::Client;
|
|
|
|
let client = Client::new();
|
|
let request = match method {
|
|
"GET" => client.get(url),
|
|
"POST" => {
|
|
let req = client.post(url);
|
|
if let Some(b) = body {
|
|
req.header("Content-Type", "text/plain").body(b.to_string())
|
|
} else {
|
|
req
|
|
}
|
|
}
|
|
"PUT" => {
|
|
let req = client.put(url);
|
|
if let Some(b) = body {
|
|
req.header("Content-Type", "text/plain").body(b.to_string())
|
|
} else {
|
|
req
|
|
}
|
|
}
|
|
"DELETE" => client.delete(url),
|
|
_ => return Err(RuntimeError {
|
|
message: format!("Unsupported HTTP method: {}", method),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
match request.send() {
|
|
Ok(response) => {
|
|
let status = response.status().as_u16() as i64;
|
|
let headers: Vec<Value> = response.headers()
|
|
.iter()
|
|
.map(|(name, value)| {
|
|
Value::Tuple(vec![
|
|
Value::String(name.to_string()),
|
|
Value::String(value.to_str().unwrap_or("").to_string()),
|
|
])
|
|
})
|
|
.collect();
|
|
let body = response.text().unwrap_or_default();
|
|
|
|
let response_record = Value::Record(HashMap::from([
|
|
("status".to_string(), Value::Int(status)),
|
|
("body".to_string(), Value::String(body)),
|
|
("headers".to_string(), Value::List(headers)),
|
|
]));
|
|
|
|
Ok(Value::Constructor {
|
|
name: "Ok".to_string(),
|
|
fields: vec![response_record],
|
|
})
|
|
}
|
|
Err(e) => {
|
|
Ok(Value::Constructor {
|
|
name: "Err".to_string(),
|
|
fields: vec![Value::String(e.to_string())],
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Helper for HTTP requests with JSON body
|
|
fn http_request_json(&self, method: &str, url: &str, json: &serde_json::Value) -> Result<Value, RuntimeError> {
|
|
use reqwest::blocking::Client;
|
|
|
|
let client = Client::new();
|
|
let request = match method {
|
|
"POST" => client.post(url).json(json),
|
|
"PUT" => client.put(url).json(json),
|
|
_ => return Err(RuntimeError {
|
|
message: format!("Unsupported HTTP method for JSON: {}", method),
|
|
span: None,
|
|
}),
|
|
};
|
|
|
|
match request.send() {
|
|
Ok(response) => {
|
|
let status = response.status().as_u16() as i64;
|
|
let headers: Vec<Value> = response.headers()
|
|
.iter()
|
|
.map(|(name, value)| {
|
|
Value::Tuple(vec![
|
|
Value::String(name.to_string()),
|
|
Value::String(value.to_str().unwrap_or("").to_string()),
|
|
])
|
|
})
|
|
.collect();
|
|
let body = response.text().unwrap_or_default();
|
|
|
|
let response_record = Value::Record(HashMap::from([
|
|
("status".to_string(), Value::Int(status)),
|
|
("body".to_string(), Value::String(body)),
|
|
("headers".to_string(), Value::List(headers)),
|
|
]));
|
|
|
|
Ok(Value::Constructor {
|
|
name: "Ok".to_string(),
|
|
fields: vec![response_record],
|
|
})
|
|
}
|
|
Err(e) => {
|
|
Ok(Value::Constructor {
|
|
name: "Err".to_string(),
|
|
fields: vec![Value::String(e.to_string())],
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 {
|
|
spread: None,
|
|
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"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_migration_from_type_declaration() {
|
|
use crate::parser::Parser;
|
|
|
|
// Test that migrations defined in type declarations are registered and executed
|
|
let source = r#"
|
|
type User @v2 {
|
|
name: String,
|
|
email: String,
|
|
|
|
from @v1 = { name: old.name, email: "default@example.com" }
|
|
}
|
|
|
|
// Create a v1 user using Schema.versioned
|
|
let v1_user = Schema.versioned("User", 1, { name: "Alice" })
|
|
|
|
// Migrate to v2 - should use the declared migration
|
|
let v2_user = Schema.migrate(v1_user, 2)
|
|
|
|
// Get the migrated value
|
|
let version = Schema.getVersion(v2_user)
|
|
"#;
|
|
|
|
let program = Parser::parse_source(source).expect("parse failed");
|
|
let mut interp = Interpreter::new();
|
|
let result = interp.run(&program);
|
|
|
|
assert!(result.is_ok(), "Interpreter failed: {:?}", result);
|
|
let result_value = result.unwrap();
|
|
|
|
// The last expression should be the version number
|
|
assert_eq!(format!("{}", result_value), "2");
|
|
}
|
|
|
|
#[test]
|
|
fn test_migration_chain() {
|
|
use crate::parser::Parser;
|
|
|
|
// Test that migrations chain correctly: v1 -> v2 -> v3
|
|
let source = r#"
|
|
type Config @v3 {
|
|
host: String,
|
|
port: Int,
|
|
secure: Bool,
|
|
|
|
from @v2 = { host: old.host, port: old.port, secure: true },
|
|
from @v1 = { host: old.host, port: 8080 }
|
|
}
|
|
|
|
// Create v1 config
|
|
let v1_config = Schema.versioned("Config", 1, { host: "localhost" })
|
|
|
|
// Migrate directly to v3 - should go v1 -> v2 -> v3
|
|
let v3_config = Schema.migrate(v1_config, 3)
|
|
let version = Schema.getVersion(v3_config)
|
|
"#;
|
|
|
|
let program = Parser::parse_source(source).expect("parse failed");
|
|
let mut interp = Interpreter::new();
|
|
let result = interp.run(&program);
|
|
|
|
assert!(result.is_ok(), "Interpreter failed: {:?}", result);
|
|
assert_eq!(format!("{}", result.unwrap()), "3");
|
|
}
|
|
}
|