Add scope and basic stdlib for evaluation

Introduces a `Scope` struct for dynamic scope management and a `Context`
struct to hold the current scope.
Implements a basic standard library with arithmetic operations and a
greater-than comparison.
Modifies `Node::eval` to handle identifiers by resolving them within the
current scope and `Call` nodes for function execution.
Updates the parser to use `Context::new()` for evaluating vector
elements, ensuring a fresh scope.
Enhances the main loop to handle potential runtime errors during
evaluation.
This commit is contained in:
Michael Schimmel
2026-02-17 00:33:58 +01:00
parent c05c74bb65
commit 647fc79d28
3 changed files with 145 additions and 20 deletions
+127 -12
View File
@@ -1,4 +1,5 @@
use std::sync::Arc;
use std::sync::{Arc, Mutex};
use std::collections::HashMap;
use std::fmt::Debug;
use crate::ast::types::{Identity, Value};
@@ -11,15 +12,111 @@ pub struct Node<K> {
/// The base for custom node types (extensions)
pub trait CustomNode: Debug + Send + Sync {
fn eval(&self, node: &Node<UntypedKind>, ctx: &mut Context) -> Value;
fn eval(&self, node: &Node<UntypedKind>, ctx: &mut Context) -> Result<Value, String>;
fn display_name(&self) -> &'static str;
}
/// Dynamic Scope for the interpreter (Temporary solution)
#[derive(Debug, Clone)]
pub struct Scope {
values: HashMap<String, Value>,
parent: Option<Arc<Mutex<Scope>>>,
}
impl Scope {
pub fn new(parent: Option<Arc<Mutex<Scope>>>) -> Self {
Self {
values: HashMap::new(),
parent,
}
}
pub fn define(&mut self, name: &str, value: Value) {
self.values.insert(name.to_string(), value);
}
pub fn resolve(&self, name: &str) -> Option<Value> {
if let Some(val) = self.values.get(name) {
return Some(val.clone());
}
if let Some(parent) = &self.parent {
return parent.lock().unwrap().resolve(name);
}
None
}
}
/// Evaluation Context (Scope management)
pub struct Context {
// Add variables, scope management here later
pub scope: Arc<Mutex<Scope>>,
}
impl Context {
pub fn new() -> Self {
let mut root_scope = Scope::new(None);
// Register standard library (built-ins)
register_stdlib(&mut root_scope);
Self {
scope: Arc::new(Mutex::new(root_scope)),
}
}
}
fn register_stdlib(scope: &mut Scope) {
// Helper macro to reduce boilerplate
macro_rules! bin_op {
($name:expr, $op:tt) => {
scope.define($name, Value::Function(Arc::new(|args| {
if args.len() < 2 { return Value::Void; } // Error handling later
// Fold allow multi-arg: (+ 1 2 3) -> 6
let mut acc = match &args[0] {
Value::Int(i) => *i as f64,
Value::Float(f) => *f,
_ => return Value::Void,
};
for arg in &args[1..] {
let val = match arg {
Value::Int(i) => *i as f64,
Value::Float(f) => *f,
_ => return Value::Void,
};
acc = acc $op val;
}
// Return Int if result is integer-like (simplified)
if acc.fract() == 0.0 {
Value::Int(acc as i64)
} else {
Value::Float(acc)
}
})));
};
}
bin_op!("+", +);
bin_op!("-", -);
bin_op!("*", *);
bin_op!("/", /);
// Logic
scope.define(">", Value::Function(Arc::new(|args| {
if args.len() != 2 { return Value::Void; }
let (v1, v2) = (&args[0], &args[1]);
match (v1, v2) {
(Value::Int(a), Value::Int(b)) => Value::Bool(a > b),
(Value::Float(a), Value::Float(b)) => Value::Bool(a > b),
(Value::Int(a), Value::Float(b)) => Value::Bool((*a as f64) > *b),
(Value::Float(a), Value::Int(b)) => Value::Bool(*a > (*b as f64)),
_ => Value::Bool(false),
}
})));
}
/// The core AST variant enum for performance and structure
#[derive(Debug)]
pub enum UntypedKind {
@@ -41,25 +138,43 @@ pub enum UntypedKind {
}
impl Node<UntypedKind> {
pub fn eval(&self, ctx: &mut Context) -> Value {
pub fn eval(&self, ctx: &mut Context) -> Result<Value, String> {
match &self.kind {
UntypedKind::Nop => Value::Void,
UntypedKind::Constant(v) => v.clone(),
UntypedKind::Identifier(_) => todo!("Lookup in Context"),
UntypedKind::Nop => Ok(Value::Void),
UntypedKind::Constant(v) => Ok(v.clone()),
UntypedKind::Identifier(name) => {
let scope = ctx.scope.lock().unwrap();
scope.resolve(name)
.ok_or_else(|| format!("Undefined variable: '{}'", name))
},
UntypedKind::If { cond, then_br, else_br } => {
if cond.eval(ctx).is_truthy() {
let cond_val = cond.eval(ctx)?;
if cond_val.is_truthy() {
then_br.eval(ctx)
} else if let Some(eb) = else_br {
eb.eval(ctx)
} else {
Value::Void
Ok(Value::Void)
}
}
UntypedKind::Call { callee, args } => {
let _func = callee.eval(ctx);
let _eval_args: Vec<Value> = args.iter().map(|a| a.eval(ctx)).collect();
todo!("Execute func with args")
let func_val = callee.eval(ctx)?;
// Evaluate all arguments first (strict evaluation)
let mut eval_args = Vec::with_capacity(args.len());
for arg in args {
eval_args.push(arg.eval(ctx)?);
}
match func_val {
Value::Function(f) => Ok(f(eval_args)),
_ => Err(format!("Attempt to call a non-function value: {}", func_val)),
}
}
UntypedKind::Extension(ext) => ext.eval(self, ctx),
}
}
+5 -1
View File
@@ -122,7 +122,11 @@ impl<'a> Parser<'a> {
while *self.peek() != TokenKind::RightBracket && *self.peek() != TokenKind::EOF {
// Vector elements in Myc are usually just constants in a list
let expr = self.parse_expression()?;
elements.push(expr.eval(&mut crate::ast::nodes::Context {})); // Simple direct eval for literals
// Simple direct eval for literals (Context::new creates fresh scope)
match expr.eval(&mut crate::ast::nodes::Context::new()) {
Ok(val) => elements.push(val),
Err(e) => return Err(format!("Error evaluating vector element: {}", e)),
}
}
self.expect(TokenKind::RightBracket)?;
+13 -7
View File
@@ -53,13 +53,19 @@ impl eframe::App for CompilerApp {
match parser.parse_expression() {
Ok(ast) => {
let mut context = Context {};
let result = ast.eval(&mut context);
self.output_log = format!(
"AST Parsed Successfully.\nResult: {}\n\nFinished at {:?}",
result,
std::time::SystemTime::now(),
);
let mut context = Context::new(); // Use new() which registers stdlib
match ast.eval(&mut context) {
Ok(result) => {
self.output_log = format!(
"AST Parsed & Evaluated Successfully.\nResult: {}\n\nFinished at {:?}",
result,
std::time::SystemTime::now(),
);
}
Err(e) => {
self.output_log = format!("Runtime Error: {}", e);
}
}
}
Err(e) => {
self.output_log = format!("Parser Error: {}", e);