Add tuple and map literals

This commit introduces support for tuple and map literals in the AST.
Tuples are now represented by `UntypedKind::Tuple` and maps by
`UntypedKind::Map`.
The `Binder` has been updated to correctly handle these new node types
and infer their types.
The `VM` now also supports evaluating tuple and map literals.
This commit is contained in:
Michael Schimmel
2026-02-17 13:07:17 +01:00
parent d55422272b
commit 9afde5a301
5 changed files with 92 additions and 177 deletions
+40 -2
View File
@@ -1,9 +1,9 @@
use std::collections::HashMap;
use std::collections::{HashMap, BTreeMap};
use std::rc::Rc;
use std::cell::RefCell;
use crate::ast::nodes::{Node, UntypedKind};
use crate::ast::compiler::bound_nodes::{BoundKind, Address};
use crate::ast::types::{Identity, StaticType};
use crate::ast::types::{Identity, StaticType, Value};
#[derive(Debug, Clone)]
struct LocalInfo {
@@ -229,6 +229,44 @@ impl Binder {
}
Ok(self.make_node(node.identity.clone(), BoundKind::Block { exprs: bound_exprs }, last_ty))
},
UntypedKind::Tuple { elements } => {
let mut bound_elems = Vec::new();
for e in elements {
bound_elems.push(self.bind(e)?);
}
// For now, tuple type is List(Any)
let ty = StaticType::List(Box::new(StaticType::Any));
Ok(self.make_node(node.identity.clone(), BoundKind::Tuple { elements: bound_elems }, ty))
},
UntypedKind::Map { entries } => {
let mut bound_entries = Vec::new();
let mut key_types = BTreeMap::new(); // For Record type inference if keys are constant keywords
for (k, v) in entries {
// Keys must be compile-time constants for now (for Record type),
// or at least we enforce keywords.
// But `bind` processes runtime expressions too.
// Let's bind both.
let bound_k = self.bind(k)?;
let bound_v = self.bind(v)?;
// If key is a constant keyword, we can build a static record type.
if let BoundKind::Constant(Value::Keyword(kw)) = &bound_k.kind {
key_types.insert(*kw, bound_v.ty.clone());
}
bound_entries.push((bound_k, bound_v));
}
// If all keys are known, we produce a Record type. Else Map(Any, Any) which we don't have yet.
// We default to Record(Any) if keys are dynamic (not supported well yet) or known.
// Since our parser enforced keywords, we assume we have a Record.
let ty = StaticType::Record(Rc::new(key_types));
Ok(self.make_node(node.identity.clone(), BoundKind::Map { entries: bound_entries }, ty))
},
UntypedKind::Extension(_) => {
Err("Custom extensions not supported in Binder yet".to_string())
+8
View File
@@ -51,6 +51,14 @@ pub enum BoundKind {
exprs: Vec<Node<BoundKind, StaticType>>,
},
// NEW
Tuple {
elements: Vec<Node<BoundKind, StaticType>>,
},
Map {
entries: Vec<(Node<BoundKind, StaticType>, Node<BoundKind, StaticType>)>,
},
// Extension points (need to be adapted for bound nodes if they use variables)
// For now, we assume extensions are self-contained or handled dynamically.
}
+6 -155
View File
@@ -1,6 +1,4 @@
use std::rc::Rc;
use std::cell::RefCell;
use std::collections::HashMap;
use std::fmt::Debug;
use crate::ast::types::{Identity, Value};
@@ -14,75 +12,9 @@ pub struct Node<K, T = ()> {
/// The base for custom node types (extensions)
pub trait CustomNode: Debug {
fn eval(&self, _ctx: &mut Context) -> Result<Value, String> {
Err("CustomNode eval not implemented".to_string())
}
fn display_name(&self) -> &'static str;
}
/// Dynamic Scope for the simple interpreter (Legacy / Macro expansion)
#[derive(Debug, Clone)]
pub struct Scope {
values: HashMap<String, Value>,
parent: Option<Rc<RefCell<Scope>>>,
}
impl Scope {
pub fn new(parent: Option<Rc<RefCell<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 assign(&mut self, name: &str, value: Value) -> Result<(), String> {
if self.values.contains_key(name) {
self.values.insert(name.to_string(), value);
return Ok(());
}
if let Some(parent) = &self.parent {
return parent.borrow_mut().assign(name, value);
}
Err(format!("Cannot assign to undefined variable '{}'", name))
}
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.borrow().resolve(name);
}
None
}
}
/// Evaluation Context (Scope management)
pub struct Context {
pub scope: Rc<RefCell<Scope>>,
}
impl Default for Context {
fn default() -> Self {
Self::new()
}
}
impl Context {
pub fn new() -> Self {
let root_scope = Scope::new(None);
// Registering stdlib directly in Scope for simple eval is skipped to keep it clean.
// If needed, we can re-add it, but VM is the primary execution engine.
Self {
scope: Rc::new(RefCell::new(root_scope)),
}
}
}
#[derive(Debug)]
pub enum UntypedKind {
Nop,
@@ -112,92 +44,11 @@ pub enum UntypedKind {
Block {
exprs: Vec<Node<UntypedKind>>,
},
Tuple {
elements: Vec<Node<UntypedKind>>,
},
Map {
entries: Vec<(Node<UntypedKind>, Node<UntypedKind>)>,
},
Extension(Box<dyn CustomNode>),
}
impl Node<UntypedKind> {
// This eval is a simple tree-walker, distinct from VM execution.
// It's useful for macros or constant folding.
pub fn eval(&self, ctx: &mut Context) -> Result<Value, String> {
match &self.kind {
UntypedKind::Nop => Ok(Value::Void),
UntypedKind::Constant(v) => Ok(v.clone()),
UntypedKind::Identifier(name) => {
let scope = ctx.scope.borrow();
scope.resolve(name)
.ok_or_else(|| format!("Undefined variable: '{}'", name))
},
UntypedKind::If { cond, then_br, else_br } => {
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 {
Ok(Value::Void)
}
},
UntypedKind::Def { name, value } => {
let val = value.eval(ctx)?;
let mut scope = ctx.scope.borrow_mut();
scope.define(name, val.clone());
Ok(val)
},
UntypedKind::Assign { target, value } => {
let val = value.eval(ctx)?;
if let UntypedKind::Identifier(name) = &target.kind {
let mut scope = ctx.scope.borrow_mut();
scope.assign(name, val.clone())?;
Ok(val)
} else {
Err("Assignment target must be an identifier".to_string())
}
},
UntypedKind::Block { exprs } => {
let mut last_val = Value::Void;
for expr in exprs {
last_val = expr.eval(ctx)?;
}
Ok(last_val)
},
UntypedKind::Lambda { params, body } => {
let captured_scope = ctx.scope.clone();
let params = params.clone();
let body = body.clone();
Ok(Value::Function(Rc::new(move |args| {
let mut new_scope = Scope::new(Some(captured_scope.clone()));
for (i, param) in params.iter().enumerate() {
if i < args.len() {
new_scope.define(param, args[i].clone());
} else {
new_scope.define(param, Value::Void);
}
}
let mut sub_ctx = Context { scope: Rc::new(RefCell::new(new_scope)) };
body.eval(&mut sub_ctx).unwrap_or(Value::Void)
})))
},
UntypedKind::Call { callee, args } => {
let func_val = callee.eval(ctx)?;
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(ctx),
}
}
}
+14 -17
View File
@@ -1,7 +1,7 @@
use std::rc::Rc;
use crate::ast::lexer::{Lexer, Token, TokenKind};
use crate::ast::types::{Identity, NodeIdentity, Value, Keyword};
use crate::ast::nodes::{Node, UntypedKind, Context};
use crate::ast::nodes::{Node, UntypedKind};
pub struct Parser<'a> {
lexer: Lexer<'a>,
@@ -202,29 +202,24 @@ impl<'a> Parser<'a> {
fn parse_vector_literal(&mut self) -> Result<Node<UntypedKind>, String> {
let token = self.advance()?;
let mut elements = Vec::new();
let mut temp_ctx = Context::new();
while *self.peek() != TokenKind::RightBracket && *self.peek() != TokenKind::EOF {
let expr = self.parse_expression()?;
match expr.eval(&mut temp_ctx) {
Ok(val) => elements.push(val),
Err(e) => return Err(format!("Vector literal error: {}", e)),
}
elements.push(expr);
}
self.expect(TokenKind::RightBracket)?;
Ok(Node {
identity: Rc::new(NodeIdentity { location: token.location }),
kind: UntypedKind::Constant(Value::List(Rc::new(elements))),
kind: UntypedKind::Tuple { elements },
ty: (),
})
}
fn parse_map_literal(&mut self) -> Result<Node<UntypedKind>, String> {
let token = self.advance()?;
let mut map = std::collections::HashMap::new();
let mut temp_ctx = Context::new();
let mut entries = Vec::new();
while *self.peek() != TokenKind::RightBrace {
if *self.peek() == TokenKind::EOF {
@@ -232,24 +227,26 @@ impl<'a> Parser<'a> {
}
let key_node = self.parse_expression()?;
let key = match key_node.eval(&mut temp_ctx)? {
Value::Keyword(k) => k,
_ => return Err("Map keys must be keywords".to_string()),
};
// We check for keyword kind here (syntactically) to avoid ambiguity, but
// strictly we could allow any expression and check at runtime.
// Delphi enforces keywords. We can do minimal check here.
match &key_node.kind {
UntypedKind::Constant(Value::Keyword(_)) => {},
_ => return Err("Map keys must be keywords (syntactically)".to_string()),
}
if *self.peek() == TokenKind::RightBrace {
return Err("Map literal must have even number of forms".to_string());
}
let val_node = self.parse_expression()?;
let val = val_node.eval(&mut temp_ctx)?;
map.insert(key, val);
entries.push((key_node, val_node));
}
self.expect(TokenKind::RightBrace)?;
Ok(Node {
identity: Rc::new(NodeIdentity { location: token.location }),
kind: UntypedKind::Constant(Value::Record(Rc::new(map))),
kind: UntypedKind::Map { entries },
ty: (),
})
}
+24 -3
View File
@@ -1,5 +1,6 @@
use std::rc::Rc;
use std::cell::RefCell;
use std::collections::HashMap;
use std::any::Any;
use crate::ast::compiler::bound_nodes::{Address, BoundKind};
use crate::ast::nodes::Node;
@@ -28,9 +29,6 @@ struct CallFrame {
pub struct VM {
stack: Vec<Value>,
// Globals are mutable shared state within the VM thread.
// However, if we move to frozen roots, this might change to a read-only structure.
// For now, mutable RefCell Vec is fine for single threaded execution.
globals: Rc<RefCell<Vec<Value>>>,
frames: Vec<CallFrame>,
}
@@ -151,6 +149,29 @@ impl VM {
},
_ => Err(format!("Attempt to call non-function: {}", func_val)),
}
},
BoundKind::Tuple { elements } => {
let mut vals = Vec::with_capacity(elements.len());
for e in elements {
vals.push(self.eval(e)?);
}
Ok(Value::List(Rc::new(vals)))
},
BoundKind::Map { entries } => {
let mut map = HashMap::new();
for (k, v) in entries {
let key = self.eval(k)?;
let val = self.eval(v)?;
if let Value::Keyword(kw) = key {
map.insert(kw, val);
} else {
return Err(format!("Map key must be keyword, got {}", key));
}
}
Ok(Value::Record(Rc::new(map)))
}
}
}