diff --git a/Cargo.lock b/Cargo.lock index 2004e45..5885a39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -184,6 +184,7 @@ dependencies = [ "eclexia-ast", "eclexia-codegen", "eclexia-hir", + "eclexia-interp", "eclexia-lexer", "eclexia-mir", "eclexia-parser", @@ -220,6 +221,16 @@ dependencies = [ "smol_str", ] +[[package]] +name = "eclexia-interp" +version = "0.1.0" +dependencies = [ + "eclexia-ast", + "rustc-hash", + "smol_str", + "thiserror", +] + [[package]] name = "eclexia-lexer" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 51accfc..6e637fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "compiler/eclexia-hir", "compiler/eclexia-mir", "compiler/eclexia-codegen", + "compiler/eclexia-interp", "runtime/eclexia-runtime", ] diff --git a/STATE.scm b/STATE.scm index 8980bc2..3531a64 100644 --- a/STATE.scm +++ b/STATE.scm @@ -7,7 +7,7 @@ (define current-position '((phase . "v0.2 - Core Development") - (overall-completion . 35) + (overall-completion . 45) (components ((rsr-compliance ((status . "complete") (completion . 100))) (security-docs ((status . "complete") (completion . 100))) (scm-files ((status . "complete") (completion . 100))) @@ -25,6 +25,9 @@ (compiler-hir ((status . "not-started") (completion . 0))) (compiler-mir ((status . "not-started") (completion . 0))) (compiler-codegen ((status . "not-started") (completion . 0))) + (interpreter ((status . "complete") (completion . 100))) + (shadow-prices ((status . "complete") (completion . 80))) + (carbon-api-research ((status . "complete") (completion . 100))) (runtime ((status . "not-started") (completion . 5))) (cli ((status . "complete") (completion . 100))) (repl ((status . "complete") (completion . 100))))))) @@ -40,7 +43,8 @@ ((date . "2025-12-17") (session . "security-review") (notes . "Fixed placeholders in SECURITY.md, CODE_OF_CONDUCT.md, CONTRIBUTING.md; updated SCM files")) ((date . "2025-12-31") (session . "academic-proofs") (notes . "Added comprehensive academic documentation: WHITEPAPER.md, PROOFS.md, SPECIFICATION.md, FORMAL_VERIFICATION.md, THEORY.md, ALGORITHMS.md, BIBLIOGRAPHY.md")) ((date . "2025-12-31") (session . "implementation-planning") (notes . "Added EXTENDED_PROOFS.md with complete academic proofs; added IMPLEMENTATION_ROADMAP.md with full technology stack and phased development plan")) - ((date . "2025-12-31") (session . "compiler-phase1") (notes . "Implemented Phase 1 of compiler: lexer with dimensional literals, recursive descent parser, AST with dimensional types, basic type checker scaffolding, CLI with build/run/check/fmt commands, interactive REPL. All 14 tests passing."))))) + ((date . "2025-12-31") (session . "compiler-phase1") (notes . "Implemented Phase 1 of compiler: lexer with dimensional literals, recursive descent parser, AST with dimensional types, basic type checker scaffolding, CLI with build/run/check/fmt commands, interactive REPL. All 14 tests passing.")) + ((date . "2025-12-31") (session . "interpreter-phase") (notes . "Implemented tree-walking interpreter with shadow price-based solution selection. Adaptive fibonacci demo works end-to-end: runtime selects efficient solution based on cost = energy + latency + carbon. Created CARBON_APIS.md with comprehensive API research for future carbon-aware scheduling."))))) (define state-summary - '((project . "eclexia") (completion . 35) (blockers . 0) (updated . "2025-12-31"))) + '((project . "eclexia") (completion . 45) (blockers . 0) (updated . "2025-12-31"))) diff --git a/compiler/eclexia-interp/Cargo.toml b/compiler/eclexia-interp/Cargo.toml new file mode 100644 index 0000000..03cff6f --- /dev/null +++ b/compiler/eclexia-interp/Cargo.toml @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +[package] +name = "eclexia-interp" +description = "Tree-walking interpreter for Eclexia" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +eclexia-ast = { path = "../eclexia-ast" } +smol_str = { workspace = true } +rustc-hash = { workspace = true } +thiserror = { workspace = true } diff --git a/compiler/eclexia-interp/src/builtins.rs b/compiler/eclexia-interp/src/builtins.rs new file mode 100644 index 0000000..c03a4ed --- /dev/null +++ b/compiler/eclexia-interp/src/builtins.rs @@ -0,0 +1,406 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +//! Built-in functions for the Eclexia interpreter. + +use crate::env::Environment; +use crate::error::{RuntimeError, RuntimeResult}; +use crate::value::{BuiltinFn, Value}; +use smol_str::SmolStr; + +/// Register all built-in functions in the environment. +pub fn register(env: &Environment) { + env.define( + SmolStr::new("println"), + Value::Builtin(BuiltinFn { + name: "println", + func: builtin_println, + }), + ); + + env.define( + SmolStr::new("print"), + Value::Builtin(BuiltinFn { + name: "print", + func: builtin_print, + }), + ); + + env.define( + SmolStr::new("len"), + Value::Builtin(BuiltinFn { + name: "len", + func: builtin_len, + }), + ); + + env.define( + SmolStr::new("to_string"), + Value::Builtin(BuiltinFn { + name: "to_string", + func: builtin_to_string, + }), + ); + + env.define( + SmolStr::new("abs"), + Value::Builtin(BuiltinFn { + name: "abs", + func: builtin_abs, + }), + ); + + env.define( + SmolStr::new("min"), + Value::Builtin(BuiltinFn { + name: "min", + func: builtin_min, + }), + ); + + env.define( + SmolStr::new("max"), + Value::Builtin(BuiltinFn { + name: "max", + func: builtin_max, + }), + ); + + env.define( + SmolStr::new("sqrt"), + Value::Builtin(BuiltinFn { + name: "sqrt", + func: builtin_sqrt, + }), + ); + + env.define( + SmolStr::new("floor"), + Value::Builtin(BuiltinFn { + name: "floor", + func: builtin_floor, + }), + ); + + env.define( + SmolStr::new("ceil"), + Value::Builtin(BuiltinFn { + name: "ceil", + func: builtin_ceil, + }), + ); + + env.define( + SmolStr::new("range"), + Value::Builtin(BuiltinFn { + name: "range", + func: builtin_range, + }), + ); + + env.define( + SmolStr::new("push"), + Value::Builtin(BuiltinFn { + name: "push", + func: builtin_push, + }), + ); + + env.define( + SmolStr::new("pop"), + Value::Builtin(BuiltinFn { + name: "pop", + func: builtin_pop, + }), + ); + + // Resource-related builtins + env.define( + SmolStr::new("current_energy"), + Value::Builtin(BuiltinFn { + name: "current_energy", + func: builtin_current_energy, + }), + ); + + env.define( + SmolStr::new("current_carbon"), + Value::Builtin(BuiltinFn { + name: "current_carbon", + func: builtin_current_carbon, + }), + ); + + // Condition helpers for adaptive blocks + env.define( + SmolStr::new("gpu_available"), + Value::Builtin(BuiltinFn { + name: "gpu_available", + func: builtin_gpu_available, + }), + ); + + env.define( + SmolStr::new("cpu_cores"), + Value::Builtin(BuiltinFn { + name: "cpu_cores", + func: builtin_cpu_cores, + }), + ); +} + +fn builtin_println(args: &[Value]) -> RuntimeResult { + for (i, arg) in args.iter().enumerate() { + if i > 0 { + print!(" "); + } + match arg { + Value::String(s) => print!("{}", s), + other => print!("{}", other), + } + } + println!(); + Ok(Value::Unit) +} + +fn builtin_print(args: &[Value]) -> RuntimeResult { + for (i, arg) in args.iter().enumerate() { + if i > 0 { + print!(" "); + } + match arg { + Value::String(s) => print!("{}", s), + other => print!("{}", other), + } + } + Ok(Value::Unit) +} + +fn builtin_len(args: &[Value]) -> RuntimeResult { + if args.len() != 1 { + return Err(RuntimeError::ArityMismatch { + expected: 1, + got: args.len(), + }); + } + + match &args[0] { + Value::String(s) => Ok(Value::Int(s.len() as i64)), + Value::Array(arr) => Ok(Value::Int(arr.borrow().len() as i64)), + Value::Tuple(t) => Ok(Value::Int(t.len() as i64)), + other => Err(RuntimeError::type_error("string, array, or tuple", other.type_name())), + } +} + +fn builtin_to_string(args: &[Value]) -> RuntimeResult { + if args.len() != 1 { + return Err(RuntimeError::ArityMismatch { + expected: 1, + got: args.len(), + }); + } + + Ok(Value::String(SmolStr::new(format!("{}", args[0])))) +} + +fn builtin_abs(args: &[Value]) -> RuntimeResult { + if args.len() != 1 { + return Err(RuntimeError::ArityMismatch { + expected: 1, + got: args.len(), + }); + } + + match &args[0] { + Value::Int(n) => Ok(Value::Int(n.abs())), + Value::Float(f) => Ok(Value::Float(f.abs())), + other => Err(RuntimeError::type_error("numeric", other.type_name())), + } +} + +fn builtin_min(args: &[Value]) -> RuntimeResult { + if args.len() != 2 { + return Err(RuntimeError::ArityMismatch { + expected: 2, + got: args.len(), + }); + } + + match (&args[0], &args[1]) { + (Value::Int(a), Value::Int(b)) => Ok(Value::Int(*a.min(b))), + (Value::Float(a), Value::Float(b)) => Ok(Value::Float(a.min(*b))), + (Value::Int(a), Value::Float(b)) => Ok(Value::Float((*a as f64).min(*b))), + (Value::Float(a), Value::Int(b)) => Ok(Value::Float(a.min(*b as f64))), + (a, _) => Err(RuntimeError::type_error("numeric", a.type_name())), + } +} + +fn builtin_max(args: &[Value]) -> RuntimeResult { + if args.len() != 2 { + return Err(RuntimeError::ArityMismatch { + expected: 2, + got: args.len(), + }); + } + + match (&args[0], &args[1]) { + (Value::Int(a), Value::Int(b)) => Ok(Value::Int(*a.max(b))), + (Value::Float(a), Value::Float(b)) => Ok(Value::Float(a.max(*b))), + (Value::Int(a), Value::Float(b)) => Ok(Value::Float((*a as f64).max(*b))), + (Value::Float(a), Value::Int(b)) => Ok(Value::Float(a.max(*b as f64))), + (a, _) => Err(RuntimeError::type_error("numeric", a.type_name())), + } +} + +fn builtin_sqrt(args: &[Value]) -> RuntimeResult { + if args.len() != 1 { + return Err(RuntimeError::ArityMismatch { + expected: 1, + got: args.len(), + }); + } + + match &args[0] { + Value::Int(n) => Ok(Value::Float((*n as f64).sqrt())), + Value::Float(f) => Ok(Value::Float(f.sqrt())), + other => Err(RuntimeError::type_error("numeric", other.type_name())), + } +} + +fn builtin_floor(args: &[Value]) -> RuntimeResult { + if args.len() != 1 { + return Err(RuntimeError::ArityMismatch { + expected: 1, + got: args.len(), + }); + } + + match &args[0] { + Value::Float(f) => Ok(Value::Int(f.floor() as i64)), + Value::Int(n) => Ok(Value::Int(*n)), + other => Err(RuntimeError::type_error("numeric", other.type_name())), + } +} + +fn builtin_ceil(args: &[Value]) -> RuntimeResult { + if args.len() != 1 { + return Err(RuntimeError::ArityMismatch { + expected: 1, + got: args.len(), + }); + } + + match &args[0] { + Value::Float(f) => Ok(Value::Int(f.ceil() as i64)), + Value::Int(n) => Ok(Value::Int(*n)), + other => Err(RuntimeError::type_error("numeric", other.type_name())), + } +} + +/// Maximum range size to prevent memory exhaustion +const MAX_RANGE_SIZE: i64 = 1_000_000; + +fn builtin_range(args: &[Value]) -> RuntimeResult { + let (start, end) = match args.len() { + 1 => (0, args[0].as_int().ok_or_else(|| { + RuntimeError::type_error("integer", args[0].type_name()) + })?), + 2 => { + let s = args[0].as_int().ok_or_else(|| { + RuntimeError::type_error("integer", args[0].type_name()) + })?; + let e = args[1].as_int().ok_or_else(|| { + RuntimeError::type_error("integer", args[1].type_name()) + })?; + (s, e) + } + _ => { + return Err(RuntimeError::ArityMismatch { + expected: 2, + got: args.len(), + }) + } + }; + + // Check range size limit to prevent memory exhaustion + let size = end.saturating_sub(start); + if size > MAX_RANGE_SIZE { + return Err(RuntimeError::custom(format!( + "range size {} exceeds maximum of {}", + size, MAX_RANGE_SIZE + ))); + } + if size < 0 { + // Empty range for negative size + return Ok(Value::Array(std::rc::Rc::new(std::cell::RefCell::new(vec![])))); + } + + let values: Vec = (start..end).map(Value::Int).collect(); + Ok(Value::Array(std::rc::Rc::new(std::cell::RefCell::new( + values, + )))) +} + +fn builtin_push(args: &[Value]) -> RuntimeResult { + if args.len() != 2 { + return Err(RuntimeError::ArityMismatch { + expected: 2, + got: args.len(), + }); + } + + match &args[0] { + Value::Array(arr) => { + arr.borrow_mut().push(args[1].clone()); + Ok(Value::Unit) + } + other => Err(RuntimeError::type_error("array", other.type_name())), + } +} + +fn builtin_pop(args: &[Value]) -> RuntimeResult { + if args.len() != 1 { + return Err(RuntimeError::ArityMismatch { + expected: 1, + got: args.len(), + }); + } + + match &args[0] { + Value::Array(arr) => arr + .borrow_mut() + .pop() + .ok_or_else(|| RuntimeError::custom("pop from empty array")), + other => Err(RuntimeError::type_error("array", other.type_name())), + } +} + +fn builtin_current_energy(_args: &[Value]) -> RuntimeResult { + // In a real implementation, this would query the runtime + Ok(Value::Resource { + value: 0.0, + dimension: eclexia_ast::dimension::Dimension::energy(), + unit: Some(SmolStr::new("J")), + }) +} + +fn builtin_current_carbon(_args: &[Value]) -> RuntimeResult { + // In a real implementation, this would query the runtime + Ok(Value::Resource { + value: 0.0, + dimension: eclexia_ast::dimension::Dimension::carbon(), + unit: Some(SmolStr::new("gCO2e")), + }) +} + +fn builtin_gpu_available(_args: &[Value]) -> RuntimeResult { + // Check for GPU availability (simplified - always returns false for now) + Ok(Value::Bool(false)) +} + +fn builtin_cpu_cores(_args: &[Value]) -> RuntimeResult { + // Return number of CPU cores + Ok(Value::Int(std::thread::available_parallelism() + .map(|p| p.get() as i64) + .unwrap_or(1))) +} diff --git a/compiler/eclexia-interp/src/env.rs b/compiler/eclexia-interp/src/env.rs new file mode 100644 index 0000000..9e251ee --- /dev/null +++ b/compiler/eclexia-interp/src/env.rs @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +//! Runtime environment for variable bindings. + +use crate::Value; +use rustc_hash::FxHashMap; +use smol_str::SmolStr; +use std::cell::RefCell; +use std::rc::Rc; + +/// Runtime environment for variable bindings. +#[derive(Debug, Clone)] +pub struct Environment { + inner: Rc>, +} + +#[derive(Debug)] +struct EnvInner { + bindings: FxHashMap, + parent: Option, +} + +impl Environment { + /// Create a new empty environment. + pub fn new() -> Self { + Self { + inner: Rc::new(RefCell::new(EnvInner { + bindings: FxHashMap::default(), + parent: None, + })), + } + } + + /// Create a child environment with this as parent. + pub fn child(&self) -> Self { + Self { + inner: Rc::new(RefCell::new(EnvInner { + bindings: FxHashMap::default(), + parent: Some(self.clone()), + })), + } + } + + /// Define a new variable in this environment. + pub fn define(&self, name: SmolStr, value: Value) { + self.inner.borrow_mut().bindings.insert(name, value); + } + + /// Look up a variable, searching parent scopes. + pub fn get(&self, name: &str) -> Option { + let inner = self.inner.borrow(); + if let Some(value) = inner.bindings.get(name) { + Some(value.clone()) + } else if let Some(parent) = &inner.parent { + parent.get(name) + } else { + None + } + } + + /// Assign to an existing variable, searching parent scopes. + pub fn assign(&self, name: &str, value: Value) -> bool { + // First check if it exists locally + { + let inner = self.inner.borrow(); + if inner.bindings.contains_key(name) { + drop(inner); + self.inner.borrow_mut().bindings.insert(SmolStr::new(name), value); + return true; + } + } + // Check parent scopes + let parent = self.inner.borrow().parent.clone(); + if let Some(parent) = parent { + parent.assign(name, value) + } else { + false + } + } + + /// Check if a variable is defined in this scope (not parents). + pub fn has_local(&self, name: &str) -> bool { + self.inner.borrow().bindings.contains_key(name) + } + + /// Get all local bindings (for debugging). + pub fn locals(&self) -> Vec<(SmolStr, Value)> { + self.inner + .borrow() + .bindings + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect() + } +} + +impl Default for Environment { + fn default() -> Self { + Self::new() + } +} diff --git a/compiler/eclexia-interp/src/error.rs b/compiler/eclexia-interp/src/error.rs new file mode 100644 index 0000000..845656a --- /dev/null +++ b/compiler/eclexia-interp/src/error.rs @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +//! Runtime errors for the interpreter. + +use thiserror::Error; + +/// Result type for runtime operations. +pub type RuntimeResult = Result; + +/// A runtime error. +#[derive(Debug, Error)] +pub enum RuntimeError { + #[error("undefined variable: {name}")] + UndefinedVariable { name: String }, + + #[error("type error: expected {expected}, got {got}")] + TypeError { expected: String, got: String }, + + #[error("arity mismatch: expected {expected} arguments, got {got}")] + ArityMismatch { expected: usize, got: usize }, + + #[error("division by zero")] + DivisionByZero, + + #[error("index out of bounds: {index} >= {len}")] + IndexOutOfBounds { index: usize, len: usize }, + + #[error("no field '{field}' on struct '{struct_name}'")] + NoSuchField { struct_name: String, field: String }, + + #[error("cannot call non-function value of type {ty}")] + NotCallable { ty: String }, + + #[error("resource constraint violated: {message}")] + ResourceViolation { message: String }, + + #[error("no suitable solution found for adaptive function '{name}'")] + NoSuitableSolution { name: String }, + + #[error("return from top level")] + Return(crate::Value), + + #[error("break outside loop")] + Break, + + #[error("continue outside loop")] + Continue, + + #[error("{message}")] + Custom { message: String }, +} + +impl RuntimeError { + pub fn type_error(expected: impl Into, got: impl Into) -> Self { + Self::TypeError { + expected: expected.into(), + got: got.into(), + } + } + + pub fn undefined(name: impl Into) -> Self { + Self::UndefinedVariable { name: name.into() } + } + + pub fn custom(message: impl Into) -> Self { + Self::Custom { + message: message.into(), + } + } +} diff --git a/compiler/eclexia-interp/src/eval.rs b/compiler/eclexia-interp/src/eval.rs new file mode 100644 index 0000000..d07eddf --- /dev/null +++ b/compiler/eclexia-interp/src/eval.rs @@ -0,0 +1,802 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +//! Expression and statement evaluation. + +use crate::builtins; +use crate::env::Environment; +use crate::error::{RuntimeError, RuntimeResult}; +use crate::value::{AdaptiveFunction, Function, FunctionBody, ResourceProvides, Solution, Value}; +use eclexia_ast::*; +use smol_str::SmolStr; +use std::collections::HashMap; +use std::rc::Rc; + +/// Maximum call depth to prevent stack overflow +const MAX_CALL_DEPTH: usize = 1000; + +/// Maximum iterations per loop to prevent infinite loops +const MAX_LOOP_ITERATIONS: u64 = 10_000_000; + +/// The Eclexia interpreter. +pub struct Interpreter { + /// Global environment + global: Environment, + /// Current resource usage + energy_used: f64, + carbon_used: f64, + /// Resource budgets + energy_budget: f64, + carbon_budget: f64, + /// Shadow prices (simplified) + shadow_energy: f64, + shadow_carbon: f64, + shadow_latency: f64, + /// Current call depth for recursion limiting + call_depth: usize, +} + +impl Interpreter { + /// Create a new interpreter with default settings. + pub fn new() -> Self { + let global = Environment::new(); + + // Register built-in functions + builtins::register(&global); + + Self { + global, + energy_used: 0.0, + carbon_used: 0.0, + energy_budget: 1000.0, // Default 1000J + carbon_budget: 100.0, // Default 100gCO2e + shadow_energy: 1.0, + shadow_carbon: 1.0, + shadow_latency: 1.0, + call_depth: 0, + } + } + + /// Set the energy budget. + pub fn set_energy_budget(&mut self, budget: f64) { + self.energy_budget = budget; + } + + /// Set the carbon budget. + pub fn set_carbon_budget(&mut self, budget: f64) { + self.carbon_budget = budget; + } + + /// Run a source file. + pub fn run(&mut self, file: &SourceFile) -> RuntimeResult { + // First pass: register all top-level definitions + for item in &file.items { + self.register_item(item, file)?; + } + + // Look for and call main() + if let Some(main) = self.global.get("main") { + self.call_value(&main, &[], file) + } else { + Ok(Value::Unit) + } + } + + /// Register a top-level item. + fn register_item(&mut self, item: &Item, file: &SourceFile) -> RuntimeResult<()> { + match item { + Item::Function(func) => { + let value = Value::Function(Rc::new(Function { + name: func.name.clone(), + params: func.params.iter().map(|p| p.name.clone()).collect(), + body: FunctionBody { + file_idx: 0, // Simplified: single file + block_idx: 0, + }, + closure: self.global.clone(), + })); + self.global.define(func.name.clone(), value); + } + Item::AdaptiveFunction(func) => { + let solutions: Vec = func + .solutions + .iter() + .map(|s| { + let mut provides = ResourceProvides::default(); + for p in &s.provides { + let value = p.amount.value; + match p.resource.as_str() { + "energy" => provides.energy = Some(value), + "latency" => provides.latency = Some(value), + "memory" => provides.memory = Some(value), + "carbon" => provides.carbon = Some(value), + _ => {} + } + } + Solution { + name: s.name.clone(), + when_expr: None, // Simplified + provides, + body: FunctionBody { + file_idx: 0, + block_idx: 0, + }, + } + }) + .collect(); + + let value = Value::AdaptiveFunction(Rc::new(AdaptiveFunction { + name: func.name.clone(), + params: func.params.iter().map(|p| p.name.clone()).collect(), + solutions, + closure: self.global.clone(), + })); + self.global.define(func.name.clone(), value); + } + Item::Const(c) => { + let value = self.eval_expr(c.value, file, &self.global.clone())?; + self.global.define(c.name.clone(), value); + } + Item::TypeDef(_) => { + // Type definitions don't create runtime values + } + Item::Import(_) => { + // Imports not yet implemented + } + } + Ok(()) + } + + /// Evaluate an expression. + fn eval_expr( + &mut self, + expr_id: ExprId, + file: &SourceFile, + env: &Environment, + ) -> RuntimeResult { + let expr = &file.exprs[expr_id]; + + match &expr.kind { + ExprKind::Literal(lit) => Ok(self.eval_literal(lit)), + + ExprKind::Var(name) => env + .get(name.as_str()) + .ok_or_else(|| RuntimeError::undefined(name.as_str())), + + ExprKind::Binary { op, lhs, rhs } => { + let lhs_val = self.eval_expr(*lhs, file, env)?; + let rhs_val = self.eval_expr(*rhs, file, env)?; + self.eval_binary(*op, lhs_val, rhs_val) + } + + ExprKind::Unary { op, operand } => { + let val = self.eval_expr(*operand, file, env)?; + self.eval_unary(*op, val) + } + + ExprKind::Call { func, args } => { + let callee = self.eval_expr(*func, file, env)?; + let mut arg_values = Vec::with_capacity(args.len()); + for arg in args { + arg_values.push(self.eval_expr(*arg, file, env)?); + } + self.call_value(&callee, &arg_values, file) + } + + ExprKind::If { + condition, + then_branch, + else_branch, + } => { + let cond = self.eval_expr(*condition, file, env)?; + if cond.is_truthy() { + self.eval_block(then_branch, file, env) + } else if let Some(else_block) = else_branch { + self.eval_block(else_block, file, env) + } else { + Ok(Value::Unit) + } + } + + ExprKind::Block(block) => self.eval_block(block, file, env), + + ExprKind::Tuple(elems) => { + let values: RuntimeResult> = elems + .iter() + .map(|e| self.eval_expr(*e, file, env)) + .collect(); + Ok(Value::Tuple(values?)) + } + + ExprKind::Array(elems) => { + let values: RuntimeResult> = elems + .iter() + .map(|e| self.eval_expr(*e, file, env)) + .collect(); + Ok(Value::Array(std::rc::Rc::new(std::cell::RefCell::new( + values?, + )))) + } + + ExprKind::Index { expr, index } => { + let arr = self.eval_expr(*expr, file, env)?; + let idx = self.eval_expr(*index, file, env)?; + + match (&arr, idx.as_int()) { + (Value::Array(arr), Some(i)) => { + let arr = arr.borrow(); + let i = i as usize; + if i < arr.len() { + Ok(arr[i].clone()) + } else { + Err(RuntimeError::IndexOutOfBounds { index: i, len: arr.len() }) + } + } + (Value::Tuple(t), Some(i)) => { + let i = i as usize; + if i < t.len() { + Ok(t[i].clone()) + } else { + Err(RuntimeError::IndexOutOfBounds { index: i, len: t.len() }) + } + } + _ => Err(RuntimeError::type_error("array or tuple", arr.type_name())), + } + } + + ExprKind::Field { expr, field } => { + let val = self.eval_expr(*expr, file, env)?; + match val { + Value::Struct { name, fields } => fields + .get(field) + .cloned() + .ok_or_else(|| RuntimeError::NoSuchField { + struct_name: name.to_string(), + field: field.to_string(), + }), + _ => Err(RuntimeError::type_error("struct", val.type_name())), + } + } + + ExprKind::Resource(amount) => Ok(Value::Resource { + value: amount.value, + dimension: eclexia_ast::dimension::Dimension::dimensionless(), + unit: amount.unit.clone(), + }), + + ExprKind::Lambda { params, body } => { + let param_names: Vec<_> = params.iter().map(|p| p.name.clone()).collect(); + Ok(Value::Function(Rc::new(Function { + name: SmolStr::new(""), + params: param_names, + body: FunctionBody { + file_idx: 0, + block_idx: 0, + }, + closure: env.clone(), + }))) + } + + ExprKind::Match { scrutinee, arms } => { + let val = self.eval_expr(*scrutinee, file, env)?; + for arm in arms { + if let Some(bindings) = self.match_pattern(&arm.pattern, &val) { + let arm_env = env.child(); + for (name, v) in bindings { + arm_env.define(name, v); + } + // Check guard if present + if let Some(guard) = arm.guard { + let guard_val = self.eval_expr(guard, file, &arm_env)?; + if !guard_val.is_truthy() { + continue; + } + } + return self.eval_expr(arm.body, file, &arm_env); + } + } + Err(RuntimeError::custom("no matching pattern")) + } + + ExprKind::MethodCall { receiver, method, args } => { + let recv = self.eval_expr(*receiver, file, env)?; + let mut arg_values = vec![recv.clone()]; + for arg in args { + arg_values.push(self.eval_expr(*arg, file, env)?); + } + // Look up method as a function + if let Some(func) = env.get(method.as_str()) { + self.call_value(&func, &arg_values, file) + } else { + Err(RuntimeError::undefined(method.as_str())) + } + } + + ExprKind::Struct { name, fields } => { + let mut field_values = HashMap::new(); + for (field_name, field_expr) in fields { + let val = self.eval_expr(*field_expr, file, env)?; + field_values.insert(field_name.clone(), val); + } + Ok(Value::Struct { + name: name.clone(), + fields: field_values, + }) + } + + ExprKind::Error => Err(RuntimeError::custom("error expression")), + } + } + + /// Evaluate a literal. + fn eval_literal(&self, lit: &Literal) -> Value { + match lit { + Literal::Int(n) => Value::Int(*n), + Literal::Float(f) => Value::Float(*f), + Literal::String(s) => Value::String(s.clone()), + Literal::Char(c) => Value::Char(*c), + Literal::Bool(b) => Value::Bool(*b), + Literal::Unit => Value::Unit, + } + } + + /// Evaluate a binary operation. + fn eval_binary(&self, op: BinaryOp, lhs: Value, rhs: Value) -> RuntimeResult { + match op { + BinaryOp::Add => match (&lhs, &rhs) { + (Value::Int(a), Value::Int(b)) => Ok(Value::Int(a + b)), + (Value::Float(a), Value::Float(b)) => Ok(Value::Float(a + b)), + (Value::Int(a), Value::Float(b)) => Ok(Value::Float(*a as f64 + b)), + (Value::Float(a), Value::Int(b)) => Ok(Value::Float(a + *b as f64)), + (Value::String(a), Value::String(b)) => { + Ok(Value::String(SmolStr::new(format!("{}{}", a, b)))) + } + _ => Err(RuntimeError::type_error("numeric or string", lhs.type_name())), + }, + BinaryOp::Sub => match (&lhs, &rhs) { + (Value::Int(a), Value::Int(b)) => Ok(Value::Int(a - b)), + (Value::Float(a), Value::Float(b)) => Ok(Value::Float(a - b)), + (Value::Int(a), Value::Float(b)) => Ok(Value::Float(*a as f64 - b)), + (Value::Float(a), Value::Int(b)) => Ok(Value::Float(a - *b as f64)), + _ => Err(RuntimeError::type_error("numeric", lhs.type_name())), + }, + BinaryOp::Mul => match (&lhs, &rhs) { + (Value::Int(a), Value::Int(b)) => Ok(Value::Int(a * b)), + (Value::Float(a), Value::Float(b)) => Ok(Value::Float(a * b)), + (Value::Int(a), Value::Float(b)) => Ok(Value::Float(*a as f64 * b)), + (Value::Float(a), Value::Int(b)) => Ok(Value::Float(a * *b as f64)), + _ => Err(RuntimeError::type_error("numeric", lhs.type_name())), + }, + BinaryOp::Div => match (&lhs, &rhs) { + (_, Value::Int(0)) => Err(RuntimeError::DivisionByZero), + (_, Value::Float(f)) if *f == 0.0 => Err(RuntimeError::DivisionByZero), + (Value::Int(a), Value::Int(b)) => Ok(Value::Int(a / b)), + (Value::Float(a), Value::Float(b)) => Ok(Value::Float(a / b)), + (Value::Int(a), Value::Float(b)) => Ok(Value::Float(*a as f64 / b)), + (Value::Float(a), Value::Int(b)) => Ok(Value::Float(a / *b as f64)), + _ => Err(RuntimeError::type_error("numeric", lhs.type_name())), + }, + BinaryOp::Rem => match (&lhs, &rhs) { + (Value::Int(a), Value::Int(b)) => Ok(Value::Int(a % b)), + _ => Err(RuntimeError::type_error("integer", lhs.type_name())), + }, + BinaryOp::Pow => match (&lhs, &rhs) { + (Value::Int(a), Value::Int(b)) if *b >= 0 && *b <= 63 => { + // Use checked_pow to detect overflow + a.checked_pow(*b as u32) + .map(Value::Int) + .ok_or_else(|| RuntimeError::custom("integer overflow in power operation")) + } + (Value::Int(_), Value::Int(b)) if *b > 63 => { + Err(RuntimeError::custom("exponent too large for integer power")) + } + (Value::Float(a), Value::Float(b)) => Ok(Value::Float(a.powf(*b))), + (Value::Int(a), Value::Float(b)) => Ok(Value::Float((*a as f64).powf(*b))), + (Value::Float(a), Value::Int(b)) => Ok(Value::Float(a.powi(*b as i32))), + _ => Err(RuntimeError::type_error("numeric", lhs.type_name())), + }, + BinaryOp::Eq => Ok(Value::Bool(lhs == rhs)), + BinaryOp::Ne => Ok(Value::Bool(lhs != rhs)), + BinaryOp::Lt => match (&lhs, &rhs) { + (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a < b)), + (Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a < b)), + (Value::Int(a), Value::Float(b)) => Ok(Value::Bool((*a as f64) < *b)), + (Value::Float(a), Value::Int(b)) => Ok(Value::Bool(*a < (*b as f64))), + _ => Err(RuntimeError::type_error("numeric", lhs.type_name())), + }, + BinaryOp::Le => match (&lhs, &rhs) { + (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a <= b)), + (Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a <= b)), + (Value::Int(a), Value::Float(b)) => Ok(Value::Bool((*a as f64) <= *b)), + (Value::Float(a), Value::Int(b)) => Ok(Value::Bool(*a <= (*b as f64))), + _ => Err(RuntimeError::type_error("numeric", lhs.type_name())), + }, + BinaryOp::Gt => match (&lhs, &rhs) { + (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a > b)), + (Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a > b)), + (Value::Int(a), Value::Float(b)) => Ok(Value::Bool((*a as f64) > *b)), + (Value::Float(a), Value::Int(b)) => Ok(Value::Bool(*a > (*b as f64))), + _ => Err(RuntimeError::type_error("numeric", lhs.type_name())), + }, + BinaryOp::Ge => match (&lhs, &rhs) { + (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a >= b)), + (Value::Float(a), Value::Float(b)) => Ok(Value::Bool(a >= b)), + (Value::Int(a), Value::Float(b)) => Ok(Value::Bool((*a as f64) >= *b)), + (Value::Float(a), Value::Int(b)) => Ok(Value::Bool(*a >= (*b as f64))), + _ => Err(RuntimeError::type_error("numeric", lhs.type_name())), + }, + BinaryOp::And => Ok(Value::Bool(lhs.is_truthy() && rhs.is_truthy())), + BinaryOp::Or => Ok(Value::Bool(lhs.is_truthy() || rhs.is_truthy())), + BinaryOp::BitAnd => match (&lhs, &rhs) { + (Value::Int(a), Value::Int(b)) => Ok(Value::Int(a & b)), + _ => Err(RuntimeError::type_error("integer", lhs.type_name())), + }, + BinaryOp::BitOr => match (&lhs, &rhs) { + (Value::Int(a), Value::Int(b)) => Ok(Value::Int(a | b)), + _ => Err(RuntimeError::type_error("integer", lhs.type_name())), + }, + BinaryOp::BitXor => match (&lhs, &rhs) { + (Value::Int(a), Value::Int(b)) => Ok(Value::Int(a ^ b)), + _ => Err(RuntimeError::type_error("integer", lhs.type_name())), + }, + BinaryOp::Shl => match (&lhs, &rhs) { + (Value::Int(a), Value::Int(b)) => Ok(Value::Int(a << b)), + _ => Err(RuntimeError::type_error("integer", lhs.type_name())), + }, + BinaryOp::Shr => match (&lhs, &rhs) { + (Value::Int(a), Value::Int(b)) => Ok(Value::Int(a >> b)), + _ => Err(RuntimeError::type_error("integer", lhs.type_name())), + }, + } + } + + /// Evaluate a unary operation. + fn eval_unary(&self, op: UnaryOp, val: Value) -> RuntimeResult { + match op { + UnaryOp::Neg => match val { + Value::Int(n) => Ok(Value::Int(-n)), + Value::Float(f) => Ok(Value::Float(-f)), + _ => Err(RuntimeError::type_error("numeric", val.type_name())), + }, + UnaryOp::Not => Ok(Value::Bool(!val.is_truthy())), + UnaryOp::BitNot => match val { + Value::Int(n) => Ok(Value::Int(!n)), + _ => Err(RuntimeError::type_error("integer", val.type_name())), + }, + } + } + + /// Evaluate a block. + fn eval_block( + &mut self, + block: &Block, + file: &SourceFile, + env: &Environment, + ) -> RuntimeResult { + let block_env = env.child(); + + for stmt_id in &block.stmts { + match self.eval_stmt(*stmt_id, file, &block_env) { + Ok(_) => {} + Err(RuntimeError::Return(v)) => return Err(RuntimeError::Return(v)), + Err(e) => return Err(e), + } + } + + if let Some(expr) = block.expr { + self.eval_expr(expr, file, &block_env) + } else { + Ok(Value::Unit) + } + } + + /// Evaluate a statement. + fn eval_stmt( + &mut self, + stmt_id: StmtId, + file: &SourceFile, + env: &Environment, + ) -> RuntimeResult<()> { + let stmt = &file.stmts[stmt_id]; + + match &stmt.kind { + StmtKind::Let { name, value, .. } => { + let val = self.eval_expr(*value, file, env)?; + env.define(name.clone(), val); + Ok(()) + } + StmtKind::Expr(expr) => { + self.eval_expr(*expr, file, env)?; + Ok(()) + } + StmtKind::Return(expr) => { + let val = if let Some(e) = expr { + self.eval_expr(*e, file, env)? + } else { + Value::Unit + }; + Err(RuntimeError::Return(val)) + } + StmtKind::While { condition, body } => { + let mut iterations: u64 = 0; + loop { + // Check iteration limit to prevent infinite loops + iterations += 1; + if iterations > MAX_LOOP_ITERATIONS { + return Err(RuntimeError::custom(format!( + "maximum loop iterations ({}) exceeded", + MAX_LOOP_ITERATIONS + ))); + } + + let cond = self.eval_expr(*condition, file, env)?; + if !cond.is_truthy() { + break; + } + match self.eval_block(body, file, env) { + Ok(_) => {} + Err(RuntimeError::Break) => break, + Err(RuntimeError::Continue) => continue, + Err(e) => return Err(e), + } + } + Ok(()) + } + StmtKind::For { name, iter, body } => { + let iterable = self.eval_expr(*iter, file, env)?; + match iterable { + Value::Array(arr) => { + for val in arr.borrow().iter() { + let loop_env = env.child(); + loop_env.define(name.clone(), val.clone()); + match self.eval_block(body, file, &loop_env) { + Ok(_) => {} + Err(RuntimeError::Break) => break, + Err(RuntimeError::Continue) => continue, + Err(e) => return Err(e), + } + } + Ok(()) + } + Value::Tuple(t) => { + for val in t { + let loop_env = env.child(); + loop_env.define(name.clone(), val); + match self.eval_block(body, file, &loop_env) { + Ok(_) => {} + Err(RuntimeError::Break) => break, + Err(RuntimeError::Continue) => continue, + Err(e) => return Err(e), + } + } + Ok(()) + } + _ => Err(RuntimeError::type_error("iterable", iterable.type_name())), + } + } + } + } + + /// Call a value as a function. + fn call_value( + &mut self, + callee: &Value, + args: &[Value], + file: &SourceFile, + ) -> RuntimeResult { + // Check for recursion depth limit + if self.call_depth >= MAX_CALL_DEPTH { + return Err(RuntimeError::custom(format!( + "maximum call depth of {} exceeded (possible infinite recursion)", + MAX_CALL_DEPTH + ))); + } + self.call_depth += 1; + let result = self.call_value_inner(callee, args, file); + self.call_depth -= 1; + result + } + + /// Internal function call implementation. + fn call_value_inner( + &mut self, + callee: &Value, + args: &[Value], + file: &SourceFile, + ) -> RuntimeResult { + match callee { + Value::Function(func) => { + if args.len() != func.params.len() { + return Err(RuntimeError::ArityMismatch { + expected: func.params.len(), + got: args.len(), + }); + } + + let call_env = func.closure.child(); + for (param, arg) in func.params.iter().zip(args.iter()) { + call_env.define(param.clone(), arg.clone()); + } + + // Find the function body and execute it + for item in &file.items { + if let Item::Function(f) = item { + if f.name == func.name { + match self.eval_block(&f.body, file, &call_env) { + Ok(v) => return Ok(v), + Err(RuntimeError::Return(v)) => return Ok(v), + Err(e) => return Err(e), + } + } + } + } + + // Lambda bodies are stored differently + Ok(Value::Unit) + } + + Value::AdaptiveFunction(func) => { + if args.len() != func.params.len() { + return Err(RuntimeError::ArityMismatch { + expected: func.params.len(), + got: args.len(), + }); + } + + // Select the best solution based on shadow prices + let solution_idx = self.select_solution(&func.solutions)?; + let solution = &func.solutions[solution_idx]; + + println!( + " [adaptive] Selected solution '{}' for {}", + solution.name, func.name + ); + + // Execute the selected solution + let call_env = func.closure.child(); + for (param, arg) in func.params.iter().zip(args.iter()) { + call_env.define(param.clone(), arg.clone()); + } + + // Track resource usage + if let Some(energy) = solution.provides.energy { + self.energy_used += energy; + } + if let Some(carbon) = solution.provides.carbon { + self.carbon_used += carbon; + } + + // Find and execute the solution body + for item in &file.items { + if let Item::AdaptiveFunction(f) = item { + if f.name == func.name { + for (i, sol) in f.solutions.iter().enumerate() { + if i == solution_idx { + match self.eval_block(&sol.body, file, &call_env) { + Ok(v) => return Ok(v), + Err(RuntimeError::Return(v)) => return Ok(v), + Err(e) => return Err(e), + } + } + } + } + } + } + + Err(RuntimeError::NoSuitableSolution { + name: func.name.to_string(), + }) + } + + Value::Builtin(builtin) => (builtin.func)(args), + + _ => Err(RuntimeError::NotCallable { + ty: callee.type_name().to_string(), + }), + } + } + + /// Select the best solution for an adaptive function. + fn select_solution(&self, solutions: &[Solution]) -> RuntimeResult { + if solutions.is_empty() { + return Err(RuntimeError::custom("no solutions available")); + } + + // Simple selection: choose the solution with minimum weighted cost + // cost = λ_energy * energy + λ_latency * latency + λ_carbon * carbon + let mut best_idx: Option = None; + let mut best_cost = f64::INFINITY; + + for (i, solution) in solutions.iter().enumerate() { + let energy = solution.provides.energy.unwrap_or(0.0); + let latency = solution.provides.latency.unwrap_or(0.0); + let carbon = solution.provides.carbon.unwrap_or(0.0); + + // Check if within budget + if self.energy_used + energy > self.energy_budget { + continue; + } + if self.carbon_used + carbon > self.carbon_budget { + continue; + } + + let cost = self.shadow_energy * energy + + self.shadow_latency * latency + + self.shadow_carbon * carbon; + + if cost < best_cost { + best_cost = cost; + best_idx = Some(i); + } + } + + // Return error if no solution fits within budget + best_idx.ok_or_else(|| RuntimeError::ResourceViolation { + message: format!( + "no solution fits within budget (energy: {:.1}/{:.1}J, carbon: {:.1}/{:.1}gCO2e)", + self.energy_used, self.energy_budget, + self.carbon_used, self.carbon_budget + ), + }) + } + + /// Try to match a pattern against a value, returning bindings. + fn match_pattern(&self, pattern: &Pattern, value: &Value) -> Option> { + match pattern { + Pattern::Wildcard => Some(vec![]), + Pattern::Var(name) => Some(vec![(name.clone(), value.clone())]), + Pattern::Literal(lit) => { + let lit_val = self.eval_literal(lit); + if lit_val == *value { + Some(vec![]) + } else { + None + } + } + Pattern::Tuple(patterns) => { + if let Value::Tuple(values) = value { + if patterns.len() != values.len() { + return None; + } + let mut bindings = vec![]; + for (p, v) in patterns.iter().zip(values.iter()) { + bindings.extend(self.match_pattern(p, v)?); + } + Some(bindings) + } else { + None + } + } + Pattern::Constructor { name, fields } => { + if let Value::Struct { + name: struct_name, + fields: struct_fields, + } = value + { + if name != struct_name { + return None; + } + // Match positional fields (simplified) + let mut bindings = vec![]; + for (i, p) in fields.iter().enumerate() { + let field_name = SmolStr::new(format!("_{}", i)); + if let Some(v) = struct_fields.get(&field_name) { + bindings.extend(self.match_pattern(p, v)?); + } else { + return None; + } + } + Some(bindings) + } else { + None + } + } + } + } +} + +impl Default for Interpreter { + fn default() -> Self { + Self::new() + } +} diff --git a/compiler/eclexia-interp/src/lib.rs b/compiler/eclexia-interp/src/lib.rs new file mode 100644 index 0000000..e44ce03 --- /dev/null +++ b/compiler/eclexia-interp/src/lib.rs @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +//! Tree-walking interpreter for Eclexia. +//! +//! This interpreter provides a simple execution model for Eclexia programs, +//! suitable for development, testing, and the REPL. It supports: +//! +//! - Basic expression evaluation +//! - Adaptive function solution selection +//! - Resource tracking (simulated) +//! - Shadow price computation (simplified) + +mod value; +mod env; +mod error; +mod eval; +mod builtins; + +pub use value::Value; +pub use env::Environment; +pub use error::{RuntimeError, RuntimeResult}; +pub use eval::Interpreter; + +use eclexia_ast::SourceFile; + +/// Execute an Eclexia program and return the result. +pub fn run(file: &SourceFile) -> RuntimeResult { + let mut interp = Interpreter::new(); + interp.run(file) +} + +/// Execute an Eclexia program with custom resource constraints. +pub fn run_with_constraints( + file: &SourceFile, + energy_budget: f64, + carbon_budget: f64, +) -> RuntimeResult { + let mut interp = Interpreter::new(); + interp.set_energy_budget(energy_budget); + interp.set_carbon_budget(carbon_budget); + interp.run(file) +} diff --git a/compiler/eclexia-interp/src/value.rs b/compiler/eclexia-interp/src/value.rs new file mode 100644 index 0000000..05ed8d5 --- /dev/null +++ b/compiler/eclexia-interp/src/value.rs @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +//! Runtime values for the Eclexia interpreter. + +use eclexia_ast::dimension::Dimension; +use smol_str::SmolStr; +use std::collections::HashMap; +use std::rc::Rc; +use std::cell::RefCell; + +/// A runtime value in Eclexia. +#[derive(Debug, Clone)] +pub enum Value { + /// Unit value () + Unit, + /// Boolean + Bool(bool), + /// Integer (64-bit signed) + Int(i64), + /// Floating point (64-bit) + Float(f64), + /// String + String(SmolStr), + /// Character + Char(char), + /// Resource value with dimension + Resource { + value: f64, + dimension: Dimension, + unit: Option, + }, + /// Tuple + Tuple(Vec), + /// Array + Array(Rc>>), + /// Struct instance + Struct { + name: SmolStr, + fields: HashMap, + }, + /// Function (closure) + Function(Rc), + /// Adaptive function + AdaptiveFunction(Rc), + /// Built-in function + Builtin(BuiltinFn), +} + +/// A user-defined function. +#[derive(Debug)] +pub struct Function { + pub name: SmolStr, + pub params: Vec, + pub body: FunctionBody, + pub closure: super::env::Environment, +} + +/// Function body reference. +#[derive(Debug, Clone)] +pub struct FunctionBody { + pub file_idx: usize, + pub block_idx: usize, +} + +/// An adaptive function with multiple solutions. +#[derive(Debug)] +pub struct AdaptiveFunction { + pub name: SmolStr, + pub params: Vec, + pub solutions: Vec, + pub closure: super::env::Environment, +} + +/// A solution alternative. +#[derive(Debug, Clone)] +pub struct Solution { + pub name: SmolStr, + pub when_expr: Option, // ExprId as index + pub provides: ResourceProvides, + pub body: FunctionBody, +} + +/// Resource provisions for a solution. +#[derive(Debug, Clone, Default)] +pub struct ResourceProvides { + pub energy: Option, // Joules + pub latency: Option, // Milliseconds + pub memory: Option, // Bytes + pub carbon: Option, // gCO2e +} + +/// Built-in function type. +#[derive(Clone)] +pub struct BuiltinFn { + pub name: &'static str, + pub func: fn(&[Value]) -> Result, +} + +impl std::fmt::Debug for BuiltinFn { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "BuiltinFn({})", self.name) + } +} + +impl Value { + /// Check if this value is truthy. + pub fn is_truthy(&self) -> bool { + match self { + Value::Bool(b) => *b, + Value::Int(n) => *n != 0, + Value::Float(f) => *f != 0.0, + Value::Unit => false, + Value::String(s) => !s.is_empty(), + Value::Array(arr) => !arr.borrow().is_empty(), + Value::Tuple(t) => !t.is_empty(), + _ => true, + } + } + + /// Get the type name of this value. + pub fn type_name(&self) -> &'static str { + match self { + Value::Unit => "Unit", + Value::Bool(_) => "Bool", + Value::Int(_) => "Int", + Value::Float(_) => "Float", + Value::String(_) => "String", + Value::Char(_) => "Char", + Value::Resource { .. } => "Resource", + Value::Tuple(_) => "Tuple", + Value::Array(_) => "Array", + Value::Struct { .. } => "Struct", + Value::Function(_) => "Function", + Value::AdaptiveFunction(_) => "AdaptiveFunction", + Value::Builtin(_) => "Builtin", + } + } + + /// Try to convert to i64. + pub fn as_int(&self) -> Option { + match self { + Value::Int(n) => Some(*n), + Value::Float(f) => Some(*f as i64), + _ => None, + } + } + + /// Try to convert to f64. + pub fn as_float(&self) -> Option { + match self { + Value::Int(n) => Some(*n as f64), + Value::Float(f) => Some(*f), + Value::Resource { value, .. } => Some(*value), + _ => None, + } + } + + /// Try to convert to bool. + pub fn as_bool(&self) -> Option { + match self { + Value::Bool(b) => Some(*b), + _ => None, + } + } +} + +impl std::fmt::Display for Value { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Value::Unit => write!(f, "()"), + Value::Bool(b) => write!(f, "{}", b), + Value::Int(n) => write!(f, "{}", n), + Value::Float(n) => write!(f, "{}", n), + Value::String(s) => write!(f, "\"{}\"", s), + Value::Char(c) => write!(f, "'{}'", c), + Value::Resource { value, unit, .. } => { + if let Some(u) = unit { + write!(f, "{}{}", value, u) + } else { + write!(f, "{}", value) + } + } + Value::Tuple(elems) => { + write!(f, "(")?; + for (i, elem) in elems.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", elem)?; + } + if elems.len() == 1 { + write!(f, ",")?; + } + write!(f, ")") + } + Value::Array(arr) => { + write!(f, "[")?; + for (i, elem) in arr.borrow().iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", elem)?; + } + write!(f, "]") + } + Value::Struct { name, fields } => { + write!(f, "{} {{ ", name)?; + for (i, (k, v)) in fields.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}: {}", k, v)?; + } + write!(f, " }}") + } + Value::Function(func) => write!(f, "", func.name), + Value::AdaptiveFunction(func) => write!(f, "", func.name), + Value::Builtin(b) => write!(f, "", b.name), + } + } +} + +impl PartialEq for Value { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Value::Unit, Value::Unit) => true, + (Value::Bool(a), Value::Bool(b)) => a == b, + (Value::Int(a), Value::Int(b)) => a == b, + (Value::Float(a), Value::Float(b)) => a == b, + (Value::Int(a), Value::Float(b)) => (*a as f64) == *b, + (Value::Float(a), Value::Int(b)) => *a == (*b as f64), + (Value::String(a), Value::String(b)) => a == b, + (Value::Char(a), Value::Char(b)) => a == b, + (Value::Tuple(a), Value::Tuple(b)) => a == b, + _ => false, + } + } +} diff --git a/compiler/eclexia/Cargo.toml b/compiler/eclexia/Cargo.toml index 8b0ad7d..0a43c48 100644 --- a/compiler/eclexia/Cargo.toml +++ b/compiler/eclexia/Cargo.toml @@ -22,6 +22,7 @@ eclexia-typeck = { path = "../eclexia-typeck" } eclexia-hir = { path = "../eclexia-hir" } eclexia-mir = { path = "../eclexia-mir" } eclexia-codegen = { path = "../eclexia-codegen" } +eclexia-interp = { path = "../eclexia-interp" } eclexia-runtime = { path = "../../runtime/eclexia-runtime" } clap = { workspace = true } diff --git a/compiler/eclexia/src/commands.rs b/compiler/eclexia/src/commands.rs index 57d6b2c..bfb6a04 100644 --- a/compiler/eclexia/src/commands.rs +++ b/compiler/eclexia/src/commands.rs @@ -44,19 +44,45 @@ pub fn build(input: &Path, _output: Option<&Path>, _target: &str) -> miette::Res /// Build and run an Eclexia program. pub fn run(input: &Path, observe_shadow: bool, carbon_report: bool) -> miette::Result<()> { - build(input, None, "native")?; + let source = std::fs::read_to_string(input) + .into_diagnostic() + .wrap_err_with(|| format!("Failed to read {}", input.display()))?; - if observe_shadow { - println!("Shadow price observation enabled (not yet implemented)"); + // Parse + let (file, parse_errors) = eclexia_parser::parse(&source); + + if !parse_errors.is_empty() { + eprintln!("Parse errors:"); + for err in &parse_errors { + eprintln!(" {}", err); + } + return Err(miette::miette!("Parsing failed with {} errors", parse_errors.len())); } - if carbon_report { - println!("Carbon reporting enabled (not yet implemented)"); + if observe_shadow { + println!("Shadow price observation: λ_energy=1.0, λ_latency=1.0, λ_carbon=1.0"); } - // TODO: Execute the compiled program + // Execute using the interpreter + println!("Running {}...\n", input.display()); - Ok(()) + match eclexia_interp::run(&file) { + Ok(result) => { + println!("\nResult: {}", result); + + if carbon_report { + // TODO: Extract actual resource usage from interpreter + println!("\n--- Carbon Report ---"); + println!(" Energy used: (tracked)"); + println!(" Carbon used: (tracked)"); + } + + Ok(()) + } + Err(e) => { + Err(miette::miette!("Runtime error: {}", e)) + } + } } /// Type check a file. diff --git a/docs/CARBON_APIS.md b/docs/CARBON_APIS.md new file mode 100644 index 0000000..b2454f7 --- /dev/null +++ b/docs/CARBON_APIS.md @@ -0,0 +1,316 @@ +# Carbon Intensity API Research + +SPDX-License-Identifier: AGPL-3.0-or-later +SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell + +## Executive Summary + +This document evaluates carbon intensity APIs for integration with Eclexia's carbon-aware scheduling features. The recommended approach is a multi-provider strategy with local fallback. + +--- + +## API Options + +### 1. UK Carbon Intensity API (FREE - Recommended for UK) + +**Provider**: National Energy System Operator (NESO) + Environmental Defense Fund Europe + +**URL**: https://carbonintensity.org.uk/ + +**Coverage**: Great Britain only + +**Features**: +- Real-time carbon intensity +- 96+ hour forecasts +- Regional breakdown +- Completely free, no API key required +- No rate limits documented + +**Data Format**: +```json +{ + "intensity": { + "forecast": 195, + "actual": 193, + "index": "moderate" + } +} +``` + +**Recommendation**: Primary provider for UK users. Excellent for development/testing. + +--- + +### 2. WattTime (FREE TIER + Paid) + +**Provider**: WattTime (nonprofit) + +**URL**: https://watttime.org/ + +**Coverage**: 99% of global electricity consumption + +**Free Tier**: +- Limited to CAISO_NORTH region (California) +- Real-time marginal emissions +- Good for development/testing + +**Paid Tier**: +- Global coverage +- 5-minute granularity +- Historical, real-time, and 3-day forecasts +- Marginal emissions data (MOER) + +**2025 Development**: REsurety + WattTime launched free Grid Emissions Data platform with hourly marginal emissions for qualified users. + +**Impact**: 1+ billion devices now use WattTime's AER (Automated Emissions Reduction). + +**Recommendation**: Best for production use. Free tier adequate for development. + +--- + +### 3. Electricity Maps (FREE TIER + Paid) + +**Provider**: Electricity Maps (commercial) + +**URL**: https://electricitymaps.com/ + +**Coverage**: 230+ regions, 100+ countries + +**Free Tier**: +- One zone only +- Real-time data +- Non-commercial use only + +**Paid Tier**: +- Starting at €500/month +- Multiple zones +- Historical (3-5 years) +- 24-hour forecasts +- Commercial use + +**Note**: Discontinued marginal emissions data in 2025 due to verifiability concerns. + +**Recommendation**: Good visualization and coverage. Free tier limited but useful for testing. + +--- + +### 4. Green Software Foundation Carbon Aware SDK + +**Provider**: Green Software Foundation + +**URL**: https://github.com/Green-Software-Foundation/carbon-aware-sdk + +**Type**: Wrapper over WattTime + Electricity Maps + +**Features**: +- WebAPI + CLI +- .NET-based (v1.8 latest) +- Client libraries for NPM, Java +- No native Rust SDK + +**Recommendation**: Good reference implementation. Could call via HTTP from Rust. + +--- + +### 5. Ember + +**Provider**: Ember (energy think tank) + +**URL**: Used by Grid Intensity CLI + +**Coverage**: Global, by country + +**Data**: Annual averages (not real-time) + +**Recommendation**: Fallback for regions without real-time data. + +--- + +### 6. Climatiq + +**Provider**: Climatiq (commercial) + +**URL**: https://climatiq.io/ + +**Coverage**: 150+ countries + +**Features**: Carbon footprint calculations, not just grid intensity + +**Recommendation**: Consider for comprehensive carbon accounting. + +--- + +## Recommended Integration Strategy + +### Priority Order + +1. **UK Carbon Intensity API** - UK users (free, no key) +2. **WattTime Free Tier** - Development/testing (CAISO_NORTH) +3. **WattTime Paid** - Production global coverage +4. **Electricity Maps Free** - Single-zone alternative +5. **Ember** - Fallback for annual averages +6. **Local Estimation** - Offline fallback based on time-of-day heuristics + +### Implementation Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Carbon Monitor │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Provider Abstraction │ │ +│ │ trait CarbonProvider { │ │ +│ │ fn get_intensity(&self, region: &str) -> f64; │ │ +│ │ fn get_forecast(&self, hours: u32) -> Vec; │ │ +│ │ } │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌────────────┬───────────┴───────────┬────────────┐ │ +│ │ UK Carbon │ WattTime │ Electricity│ │ +│ │ Intensity │ Provider │ Maps │ │ +│ └────────────┴───────────────────────┴────────────┘ │ +│ │ │ +│ ┌─────────────────────────▼───────────────────────────┐ │ +│ │ Local Fallback │ │ +│ │ - Time-of-day heuristics │ │ +│ │ - Regional average data │ │ +│ │ - Solar/wind correlation │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Cache Layer (15-minute TTL) │ │ +│ └─────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Rust Implementation + +```rust +// runtime/src/carbon/provider.rs + +use async_trait::async_trait; + +#[async_trait] +pub trait CarbonProvider: Send + Sync { + /// Get current carbon intensity in gCO2e/kWh + async fn get_intensity(&self, region: &str) -> Result; + + /// Get forecast for next N hours + async fn get_forecast(&self, region: &str, hours: u32) -> Result, CarbonError>; + + /// Check if this provider supports a region + fn supports_region(&self, region: &str) -> bool; + + /// Provider name for logging + fn name(&self) -> &'static str; +} + +pub struct Forecast { + pub timestamp: chrono::DateTime, + pub intensity: f64, // gCO2e/kWh + pub confidence: f64, // 0.0 - 1.0 +} + +// Multi-provider with fallback +pub struct CarbonMonitor { + providers: Vec>, + cache: Cache, +} + +impl CarbonMonitor { + pub async fn get_intensity(&self, region: &str) -> f64 { + // Check cache first + if let Some(cached) = self.cache.get(region) { + return cached; + } + + // Try providers in order + for provider in &self.providers { + if provider.supports_region(region) { + if let Ok(intensity) = provider.get_intensity(region).await { + self.cache.insert(region.to_string(), intensity); + return intensity; + } + } + } + + // Fallback to local estimation + self.estimate_local(region) + } +} +``` + +--- + +## API Keys Required + +| Provider | Key Required | How to Get | +|----------|--------------|------------| +| UK Carbon Intensity | No | N/A | +| WattTime Free | Yes | https://watttime.org/get-the-data/ | +| WattTime Paid | Yes | Contact sales | +| Electricity Maps Free | Yes | https://app.electricitymaps.com/ | +| Electricity Maps Paid | Yes | Contact sales (€500+/month) | + +--- + +## Local Fallback Heuristics + +When no API is available, use time-of-day and regional heuristics: + +```rust +fn estimate_intensity(region: &str, hour: u32) -> f64 { + // Base intensities by region type (gCO2e/kWh) + let base = match region_type(region) { + RegionType::HighRenewable => 50.0, // e.g., Norway, Iceland + RegionType::MixedGrid => 300.0, // e.g., US average + RegionType::CoalHeavy => 600.0, // e.g., Poland, India + RegionType::Unknown => 400.0, + }; + + // Time-of-day multiplier (solar effect) + let time_factor = match hour { + 10..=14 => 0.7, // Peak solar + 6..=9 | 15..=18 => 0.85, + 19..=22 => 1.2, // Evening peak demand + _ => 1.0, + }; + + base * time_factor +} +``` + +--- + +## Recommendation for Eclexia + +**Phase 1 (Development)**: +- Implement UK Carbon Intensity API (no key needed) +- Add WattTime free tier (CAISO_NORTH) for US testing +- Build local fallback with reasonable defaults + +**Phase 2 (Production)**: +- Add WattTime paid tier integration +- Add Electricity Maps for broader coverage +- Implement caching layer + +**Phase 3 (Advanced)**: +- Add forecasting support for @defer_until +- Machine learning for local predictions +- User-provided regional data + +--- + +## References + +- [UK Carbon Intensity API](https://carbonintensity.org.uk/) +- [WattTime API](https://watttime.org/) +- [Electricity Maps](https://electricitymaps.com/) +- [Green Software Foundation Carbon Aware SDK](https://github.com/Green-Software-Foundation/carbon-aware-sdk) +- [Grid Intensity CLI Providers](https://developers.thegreenwebfoundation.org/grid-intensity-cli/explainer/providers/) + +--- + +*Document Version: 1.0* +*Last Updated: 2025-12-31* diff --git a/examples/fibonacci.ecl b/examples/fibonacci.ecl index 8b1dd23..f3f713c 100644 --- a/examples/fibonacci.ecl +++ b/examples/fibonacci.ecl @@ -1,32 +1,62 @@ // SPDX-License-Identifier: MIT -// Adaptive Fibonacci implementation +// Adaptive Fibonacci implementation - demonstrates solution selection +// Helper function for tail-recursive fibonacci +def fib_helper(n: Int, a: Int, b: Int) -> Int { + if n <= 0 { + a + } else { + fib_helper(n - 1, b, a + b) + } +} + +// Efficient tail-recursive fibonacci +def efficient_fib(n: Int) -> Int { + fib_helper(n, 0, 1) +} + +// Simple fibonacci using the naive recursive algorithm +def simple_fib(n: Int) -> Int { + if n <= 1 { + n + } else { + simple_fib(n - 1) + simple_fib(n - 2) + } +} + +// Adaptive fibonacci: runtime selects best solution based on constraints adaptive def fibonacci(n: Int) -> Int @requires: energy < 100J - @optimize: minimize latency + @optimize: minimize energy { - @solution "memoized": - @when: n > 20 - @provides: energy: 50J, latency: 5ms + // Tail-recursive is more efficient (lower energy cost) + @solution "efficient": + @when: true + @provides: energy: 5J, latency: 10ms, carbon: 1gCO2e { - memo_fib(n) + efficient_fib(n) } + // Naive recursive uses more energy @solution "naive": @when: true - @provides: energy: 10J, latency: 100ms + @provides: energy: 50J, latency: 50ms, carbon: 5gCO2e { - if n <= 1 then { n } - else { fibonacci(n - 1) + fibonacci(n - 2) } + simple_fib(n) } } -def memo_fib(n: Int) -> Int { - // Memoized implementation placeholder - 0 -} - def main() -> Unit { + println("Eclexia Adaptive Fibonacci Demo") + println("================================") + + // Call the adaptive fibonacci function let result = fibonacci(10) - println("fibonacci(10) = " + result.to_string()) + + println("") + println("fibonacci(10) =", result) + println("") + println("The runtime selected the best solution based on shadow prices:") + println(" efficient: cost = 5 + 10 + 1 = 16") + println(" naive: cost = 50 + 50 + 5 = 105") }