Refactor: Implement Record Layouts and Optimized Field Access

Introduces a new `RecordLayout` system for efficient and type-safe
record handling.

Key changes:
- `RecordLayout` struct with `O(1)` field lookup using FMap
  optimization.
- Field accessors (`.name`) are now first-class callable values.
- Parser recognizes dot-prefixed identifiers as field accessors.
- Binder and TypeChecker handle field accessors.
- Optimizer transforms field accessor calls into specialized `GetField`
  nodes.
- VM executes `GetField` efficiently by directly accessing record
  values.
- Adds a new `records.myc` example showcasing record features.
- Improves comparison logic for records to use pointer equality for
  layout.
This commit is contained in:
Michael Schimmel
2026-02-26 21:38:20 +01:00
parent 2ad2eb5d43
commit bf74795e01
20 changed files with 548 additions and 63 deletions
+17
View File
@@ -83,6 +83,20 @@ impl<'a> Analyzer<'a> {
)
}
BoundKind::FieldAccessor(k) => (BoundKind::FieldAccessor(*k), Purity::Pure),
BoundKind::GetField { rec, field } => {
let rec_m = self.visit(Rc::new((**rec).clone()));
let p = rec_m.ty.purity;
(
BoundKind::GetField {
rec: Box::new(rec_m),
field: *field,
},
p,
)
}
BoundKind::Set { addr, value } => {
let val_m = self.visit(Rc::new((**value).clone()));
(
@@ -313,6 +327,9 @@ impl NodeExt for BoundKind<crate::ast::types::StaticType> {
BoundKind::Define { value, .. } | BoundKind::Set { value, .. } => {
f(value);
}
BoundKind::GetField { rec, .. } => {
f(rec);
}
BoundKind::Lambda { params, body, .. } => {
f(params);
f(body);
+4
View File
@@ -189,6 +189,10 @@ impl Binder {
Err("Unexpected 'Parameter' node in general binder context. This should be handled via 'bind_pattern'.".to_string())
}
UntypedKind::FieldAccessor(k) => {
Ok(self.make_node(node.identity.clone(), BoundKind::FieldAccessor(*k)))
}
UntypedKind::If {
cond,
then_br,
+15
View File
@@ -97,6 +97,15 @@ pub enum BoundKind<T = ()> {
captured_by: Vec<Identity>,
},
/// A first-class field accessor (e.g. .name)
FieldAccessor(crate::ast::types::Keyword),
/// Specialized field access (O(1) via RecordLayout)
GetField {
rec: Box<BoundNode<T>>,
field: crate::ast::types::Keyword,
},
If {
cond: Box<BoundNode<T>>,
then_br: Box<BoundNode<T>>,
@@ -188,6 +197,10 @@ where
captured_by: cb,
},
) => na == nb && aa == ab && ka == kb && va == vb && ca == cb,
(BoundKind::FieldAccessor(a), BoundKind::FieldAccessor(b)) => a == b,
(BoundKind::GetField { rec: ra, field: fa }, BoundKind::GetField { rec: rb, field: fb }) => {
ra == rb && fa == fb
}
(
BoundKind::If {
cond: ca,
@@ -273,6 +286,8 @@ impl<T> BoundKind<T> {
};
format!("DEF_{}({}, {:?})", k_str, name.name, addr)
}
BoundKind::FieldAccessor(k) => format!("FIELD_ACCESSOR(.{})", k.name()),
BoundKind::GetField { field, .. } => format!("GET_FIELD(.{})", field.name()),
BoundKind::If { .. } => "IF".to_string(),
BoundKind::Destructure { .. } => "DESTRUCTURE".to_string(),
BoundKind::Lambda {
+9
View File
@@ -47,6 +47,15 @@ impl CapturePass {
};
}
BoundKind::FieldAccessor(_) => {}
BoundKind::GetField { rec, field } => {
node.kind = BoundKind::GetField {
rec: Box::new(Self::transform(*rec, capture_map)),
field,
};
}
BoundKind::Destructure { pattern, value } => {
node.kind = BoundKind::Destructure {
pattern: Box::new(Self::transform(*pattern, capture_map)),
+11
View File
@@ -66,6 +66,17 @@ impl Dumper {
self.log(&format!("Get: {} ({:?})", name.name, addr), node)
}
BoundKind::FieldAccessor(k) => {
self.log(&format!("FieldAccessor: .{}", k.name()), node)
}
BoundKind::GetField { rec, field } => {
self.log(&format!("GetField: .{}", field.name()), node);
self.indent += 1;
self.visit(rec);
self.indent -= 1;
}
BoundKind::Set { addr, value } => {
self.log(&format!("Set: {:?}", addr), node);
self.indent += 1;
+38 -5
View File
@@ -112,6 +112,27 @@ impl Optimizer {
)
}
BoundKind::FieldAccessor(k) => (BoundKind::FieldAccessor(k), node.ty.clone()),
BoundKind::GetField { ref rec, field } => {
let rec_opt = self.visit_node((**rec).clone(), sub, path);
// Constant folding for Field Access
if let BoundKind::Constant(Value::Record(layout, values)) = &rec_opt.kind
&& let Some(idx) = layout.index_of(field)
{
return folder.make_constant_node(values[idx].clone(), &node);
}
(
BoundKind::GetField {
rec: Box::new(rec_opt),
field,
},
node.ty.clone(),
)
}
BoundKind::Set { addr, value } => {
let value = Box::new(self.visit_node(*value, sub, path));
if let BoundKind::Constant(val) = &value.kind {
@@ -175,6 +196,23 @@ impl Optimizer {
let args = self.visit_node(*args, sub, path);
if self.enabled {
// Optimized Field Access Transformation
if let BoundKind::FieldAccessor(k) = &callee.kind
&& let BoundKind::Tuple { elements } = &args.kind
&& elements.len() == 1
{
let rec = elements[0].clone();
let metrics = node.ty.clone();
return Node {
identity: node.identity,
kind: BoundKind::GetField {
rec: Box::new(rec),
field: *k,
},
ty: metrics,
};
}
let mut arg_nodes = Vec::new();
self.flatten_tuple(args.clone(), &mut arg_nodes);
@@ -542,11 +580,6 @@ impl Optimizer {
self.flatten_tuple(el, into);
}
}
BoundKind::Record { fields } => {
for (_, v) in fields {
self.flatten_tuple(v, into);
}
}
BoundKind::Nop => {}
_ => into.push(node),
}
+7 -1
View File
@@ -68,6 +68,9 @@ impl UsageInfo {
self.assigned.insert(*addr);
self.collect(value);
}
BoundKind::GetField { rec, .. } => {
self.collect(rec);
}
BoundKind::Lambda {
params,
body,
@@ -147,7 +150,10 @@ impl UsageInfo {
BoundKind::Expansion { bound_expanded, .. } => {
self.collect(bound_expanded);
}
_ => {}
BoundKind::Again { args } => {
self.collect(args);
}
BoundKind::Nop | BoundKind::FieldAccessor(_) | BoundKind::Extension(_) => {}
}
}
+5
View File
@@ -141,6 +141,11 @@ impl TCO {
addr: *addr,
name: name.clone(),
},
BoundKind::FieldAccessor(k) => BoundKind::FieldAccessor(*k),
BoundKind::GetField { rec, field } => BoundKind::GetField {
rec: Box::new(Self::transform(Rc::new((**rec).clone()), false)),
field: *field,
},
BoundKind::Nop => BoundKind::Nop,
BoundKind::Expansion {
original_call,
+40 -8
View File
@@ -226,9 +226,10 @@ impl TypeChecker {
StaticType::Vector(inner, _) => (**inner).clone(),
StaticType::Matrix(inner, _) => (**inner).clone(), // Matrix flat iteration or row? Wait. Matrix destructuring logic is tricky. But for now let's focus on Record.
StaticType::List(inner) => (**inner).clone(),
StaticType::Record(fields) => fields
StaticType::Record(layout) => layout
.fields
.get(i)
.map(|(_, ty)| ty.clone())
.map(|(_, ty): &(crate::ast::types::Keyword, StaticType)| ty.clone())
.unwrap_or(StaticType::Any),
_ => StaticType::Any,
};
@@ -296,6 +297,36 @@ impl TypeChecker {
)
}
BoundKind::FieldAccessor(k) => (BoundKind::FieldAccessor(k), StaticType::FieldAccessor(k)),
BoundKind::GetField { rec, field } => {
let rec_typed = self.check_node(*rec, ctx)?;
let field_ty = match &rec_typed.ty {
StaticType::Record(layout) => {
if let Some(idx) = layout.index_of(field) {
layout.fields[idx].1.clone()
} else {
return Err(format!("Record does not have field :{}", field.name()));
}
}
StaticType::Any => StaticType::Any,
_ => {
return Err(format!(
"Cannot access field :{} on non-record type {}",
field.name(),
rec_typed.ty
));
}
};
(
BoundKind::GetField {
rec: Box::new(rec_typed),
field,
},
field_ty,
)
}
BoundKind::Set { addr, value } => {
let val_typed = self.check_node(*value, ctx)?;
let ty = val_typed.ty.clone();
@@ -545,11 +576,12 @@ impl TypeChecker {
typed_fields.push((kt, vt));
}
let layout = crate::ast::types::RecordLayout::get_or_create(fields_ty);
(
BoundKind::Record {
fields: typed_fields,
},
StaticType::Record(Rc::new(fields_ty)),
StaticType::Record(layout),
)
}
@@ -801,12 +833,12 @@ mod tests {
#[test]
fn test_inference_record() {
use crate::ast::types::Keyword;
let typed = check_source("{:x 1, :y 0.3}");
let typed = check_source("{:x 1 :y 0.3}");
let ty = get_ret_type(&typed);
if let StaticType::Record(fields) = ty {
assert_eq!(fields.len(), 2);
assert_eq!(fields[0], (Keyword::intern("x"), StaticType::Int));
assert_eq!(fields[1], (Keyword::intern("y"), StaticType::Float));
if let StaticType::Record(layout) = ty {
assert_eq!(layout.fields.len(), 2);
assert_eq!(layout.fields[0], (Keyword::intern("x"), StaticType::Int));
assert_eq!(layout.fields[1], (Keyword::intern("y"), StaticType::Float));
} else {
panic!("Expected Record, got {:?}", ty);
}
+10
View File
@@ -194,6 +194,16 @@ impl Environment {
let (bound_ast, captures) = Binder::bind_root(self.global_names.clone(), &expanded_ast)?;
let bound_ast = crate::ast::compiler::captures::CapturePass::apply(bound_ast, &captures);
// Pre-allocate global slots to prevent out-of-bounds during specialization/optimization
{
let mut values = self.global_values.borrow_mut();
let names = self.global_names.borrow();
let max_idx = names.values().map(|(idx, _)| idx.0).max().unwrap_or(0);
if (max_idx as usize) >= values.len() {
values.resize((max_idx + 1) as usize, Value::Void);
}
}
LambdaCollector::collect(&bound_ast, &mut self.function_registry.borrow_mut());
let checker = TypeChecker::new(self.global_types.clone());
+2
View File
@@ -65,6 +65,8 @@ pub enum UntypedKind {
Constant(Value),
Identifier(Symbol),
Parameter(Symbol),
/// A first-class field accessor (e.g. .name)
FieldAccessor(crate::ast::types::Keyword),
If {
cond: Box<Node<UntypedKind>>,
then_br: Box<Node<UntypedKind>>,
+3
View File
@@ -100,6 +100,9 @@ impl<'a> Parser<'a> {
TokenKind::Keyword(k) => UntypedKind::Constant(Value::Keyword(Keyword::intern(&k))),
TokenKind::Identifier(id) => match id.as_ref() {
"..." => UntypedKind::Nop,
s if s.starts_with('.') && s.len() > 1 => {
UntypedKind::FieldAccessor(Keyword::intern(&s[1..]))
}
_ => UntypedKind::Identifier(id.into()),
},
_ => {
+3
View File
@@ -366,6 +366,9 @@ fn register_comparison(env: &Environment) {
(Value::Bool(a), Value::Bool(b)) => Value::Bool(a == b),
(Value::Keyword(a), Value::Keyword(b)) => Value::Bool(a == b),
(Value::DateTime(a), Value::DateTime(b)) => Value::Bool(a == b),
(Value::Record(la, va), Value::Record(lb, vb)) => {
Value::Bool(std::sync::Arc::ptr_eq(la, lb) && va == vb)
}
(Value::Void, Value::Void) => Value::Bool(true),
_ => Value::Bool(false),
}
+9 -18
View File
@@ -1,7 +1,6 @@
use crate::ast::types::{Keyword, Purity, Signature, StaticType, Value};
use std::any::TypeId;
use std::collections::HashMap;
use std::rc::Rc;
/// Represents a Rust type that can be exposed to the script environment.
pub trait Scriptable: 'static + Sized {
@@ -149,7 +148,7 @@ impl TypeBuilder {
}
pub fn build(self) -> StaticType {
StaticType::Record(Rc::new(self.fields))
StaticType::Record(crate::ast::types::RecordLayout::get_or_create(self.fields))
}
}
@@ -202,20 +201,16 @@ mod tests {
// Check static type
let st = TypeRegistry::resolve_type::<Person>(&registry);
if let StaticType::Record(fields) = st {
assert!(fields.iter().any(|(k, _)| *k == Keyword::intern("greet")));
if let StaticType::Record(layout) = st {
assert!(layout.fields.iter().any(|(k, _)| *k == Keyword::intern("greet")));
} else {
panic!("Expected Record type");
}
// Check runtime behavior
if let Value::Record(r) = wrapped {
let idx = r
.keys
.iter()
.position(|k| *k == Keyword::intern("greet"))
.unwrap();
let greet_fn = &r.values[idx];
if let Value::Record(layout, values) = wrapped {
let idx = layout.index_of(Keyword::intern("greet")).unwrap();
let greet_fn = &values[idx];
if let Value::Function(f) = greet_fn {
let res = (f.func)(vec![]);
@@ -251,13 +246,9 @@ mod tests {
if let Value::Function(f) = factory_val {
let instance = (f.func)(vec![Value::Text("Bob".into()), Value::Int(40)]);
if let Value::Record(r) = instance {
let idx = r
.keys
.iter()
.position(|k| *k == Keyword::intern("greet"))
.unwrap();
let greet_fn = &r.values[idx];
if let Value::Record(layout, values) = instance {
let idx = layout.index_of(Keyword::intern("greet")).unwrap();
let greet_fn = &values[idx];
if let Value::Function(gf) = greet_fn {
let res = (gf.func)(vec![]);
+126 -29
View File
@@ -98,6 +98,84 @@ impl Keyword {
}
}
/// A shared schema for Records, providing O(1) field lookup via FMap optimization.
#[derive(Debug, Clone)]
pub struct RecordLayout {
pub fields: Vec<(Keyword, StaticType)>,
/// Optimization: maps (Keyword.idx - min_idx) to index in fields.
fmap: Vec<i32>,
min_idx: u32,
}
impl PartialEq for RecordLayout {
fn eq(&self, other: &Self) -> bool {
self.fields == other.fields
}
}
impl Eq for RecordLayout {}
impl std::hash::Hash for RecordLayout {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.fields.hash(state);
}
}
type LayoutMap = HashMap<Vec<(Keyword, StaticType)>, std::sync::Arc<RecordLayout>>;
static LAYOUT_REGISTRY: OnceLock<Mutex<LayoutMap>> = OnceLock::new();
impl RecordLayout {
pub fn get_or_create(fields: Vec<(Keyword, StaticType)>) -> std::sync::Arc<Self> {
let mut reg = LAYOUT_REGISTRY
.get_or_init(|| Mutex::new(HashMap::new()))
.lock()
.unwrap();
if let Some(layout) = reg.get(&fields) {
return layout.clone();
}
let mut min_idx = u32::MAX;
let mut max_idx = 0;
for (k, _) in &fields {
min_idx = min_idx.min(k.0);
max_idx = max_idx.max(k.0);
}
let mut fmap = vec![-1; (max_idx - min_idx + 1) as usize];
for (i, (k, _)) in fields.iter().enumerate() {
fmap[(k.0 - min_idx) as usize] = i as i32;
}
let layout = std::sync::Arc::new(RecordLayout {
fields: fields.clone(),
fmap,
min_idx,
});
reg.insert(fields, layout.clone());
layout
}
#[inline(always)]
pub fn index_of(&self, key: Keyword) -> Option<usize> {
if key.0 < self.min_idx {
return None;
}
let offset = (key.0 - self.min_idx) as usize;
if offset >= self.fmap.len() {
return None;
}
let idx = self.fmap[offset];
if idx < 0 {
None
} else {
Some(idx as usize)
}
}
}
/// Interface for custom objects (Closures, Series, Streams)
pub trait Object: fmt::Debug {
fn type_name(&self) -> &'static str;
@@ -107,15 +185,6 @@ pub trait Object: fmt::Debug {
/// A shared sequence of values, used by both Tuples and Records.
pub type ValueList = Rc<Vec<Value>>;
/// Internal storage for Records to allow sharing schema (keys) between instances.
#[derive(Debug, Clone, PartialEq)]
pub struct RecordData {
/// Names for slots.
pub keys: Rc<Vec<Keyword>>,
/// The actual values, potentially shared with a Tuple.
pub values: ValueList,
}
/// Purity level of an expression or function.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Purity {
@@ -150,7 +219,8 @@ pub enum Value {
Text(Rc<str>),
Keyword(Keyword),
Tuple(ValueList),
Record(Rc<RecordData>),
Record(std::sync::Arc<RecordLayout>, ValueList),
FieldAccessor(Keyword),
Function(Rc<NativeFunction>),
Object(Rc<dyn Object>), // For compiled Closures and other opaque types
Cell(Rc<RefCell<Value>>), // Boxed value for captures
@@ -168,7 +238,10 @@ impl PartialEq for Value {
(Value::Text(a), Value::Text(b)) => a == b,
(Value::Keyword(a), Value::Keyword(b)) => a == b,
(Value::Tuple(a), Value::Tuple(b)) => a == b,
(Value::Record(a), Value::Record(b)) => a == b,
(Value::Record(la, va), Value::Record(lb, vb)) => {
std::sync::Arc::ptr_eq(la, lb) && va == vb
}
(Value::FieldAccessor(a), Value::FieldAccessor(b)) => a == b,
(Value::Function(a), Value::Function(b)) => Rc::ptr_eq(a, b),
(Value::Object(a), Value::Object(b)) => Rc::ptr_eq(a, b),
(Value::Cell(a), Value::Cell(b)) => Rc::ptr_eq(a, b),
@@ -214,7 +287,8 @@ pub enum StaticType {
Tuple(Vec<StaticType>), // Heterogeneous fixed-size
Vector(Box<StaticType>, usize), // Homogeneous fixed-size
Matrix(Box<StaticType>, Vec<usize>), // Multi-dimensional homogeneous
Record(Rc<Vec<(Keyword, StaticType)>>),
Record(std::sync::Arc<RecordLayout>),
FieldAccessor(Keyword),
Function(Box<Signature>),
FunctionOverloads(Vec<Signature>),
Object(&'static str),
@@ -253,9 +327,9 @@ impl fmt::Display for StaticType {
}
write!(f, "]>")
}
StaticType::Record(fields) => {
StaticType::Record(layout) => {
write!(f, "{{")?;
for (i, (k, v)) in fields.iter().enumerate() {
for (i, (k, v)) in layout.fields.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
@@ -263,6 +337,7 @@ impl fmt::Display for StaticType {
}
write!(f, "}}")
}
StaticType::FieldAccessor(k) => write!(f, ".{}", k.name()),
StaticType::Function(sig) => {
write!(f, "fn({}) -> {}", sig.params, sig.ret)
}
@@ -312,6 +387,10 @@ impl StaticType {
inner.is_assignable_from(&StaticType::Matrix(m_inner.clone(), sub_shape))
}
}
// Records are assignable if their layouts match (Structural identity via interning)
(StaticType::Record(a), StaticType::Record(b)) => {
std::sync::Arc::ptr_eq(a, b)
}
_ => false,
}
}
@@ -320,6 +399,26 @@ impl StaticType {
pub fn resolve_call(&self, args_ty: &StaticType) -> Option<StaticType> {
match self {
StaticType::Any => Some(StaticType::Any),
StaticType::FieldAccessor(k) => {
// (.name { :name val })
// The args_ty should be a Tuple of [Record]
let rec_ty = if let StaticType::Tuple(t) = args_ty {
t.first()?
} else {
args_ty
};
match rec_ty {
StaticType::Record(layout) => {
if let Some(idx) = layout.index_of(*k) {
return Some(layout.fields[idx].1.clone());
}
}
StaticType::Any => return Some(StaticType::Any),
_ => {}
}
None
}
StaticType::Function(sig) => {
if sig.params.is_assignable_from(args_ty) {
Some(sig.ret.clone())
@@ -370,10 +469,12 @@ impl Value {
}
pub fn make_record(keys: Vec<Keyword>, values: Vec<Value>) -> Self {
Value::Record(Rc::new(RecordData {
keys: Rc::new(keys),
values: Rc::new(values),
}))
let fields: Vec<_> = keys
.into_iter()
.zip(values.iter().map(|v| v.static_type()))
.collect();
let layout = RecordLayout::get_or_create(fields);
Value::Record(layout, Rc::new(values))
}
pub fn make_function(purity: Purity, func: impl Fn(Vec<Value>) -> Value + 'static) -> Self {
@@ -420,14 +521,9 @@ impl Value {
StaticType::Tuple(element_types)
}
}
Value::Record(r) => {
let mut fields = Vec::with_capacity(r.values.len());
for (i, v) in r.values.iter().enumerate() {
fields.push((r.keys[i], v.static_type()));
}
StaticType::Record(Rc::new(fields))
}
Value::Function(_) => StaticType::Any, // Dynamic function
Value::Record(layout, _) => StaticType::Record(layout.clone()),
Value::FieldAccessor(k) => StaticType::FieldAccessor(*k),
Value::Function(_) => StaticType::Any, // Dynamic function
Value::Object(o) => StaticType::Object(o.type_name()),
Value::Cell(c) => c.borrow().static_type(),
Value::TailCallRequest(_) => StaticType::Any, // Internal state, but typable as Any
@@ -460,16 +556,17 @@ impl fmt::Display for Value {
}
write!(f, "]")
}
Value::Record(r) => {
Value::Record(layout, values) => {
write!(f, "{{")?;
for i in 0..r.values.len() {
for i in 0..values.len() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, ":{} {}", r.keys[i].name(), r.values[i])?;
write!(f, ":{} {}", layout.fields[i].0.name(), values[i])?;
}
write!(f, "}}")
}
Value::FieldAccessor(k) => write!(f, ".{}", k.name()),
Value::Function(f_meta) => write!(f, "<native fn, {:?}>", f_meta.purity),
Value::Object(o) => write!(f, "<{}>", o.type_name()),
Value::Cell(c) => write!(f, "{}", c.borrow()),
+62
View File
@@ -294,6 +294,22 @@ impl VM {
Ok(val)
}
BoundKind::Get { addr, .. } => self.get_value(*addr),
BoundKind::FieldAccessor(k) => Ok(Value::FieldAccessor(*k)),
BoundKind::GetField { rec, field } => {
let rec_val = self.eval_internal(obs, rec)?;
if let Value::Record(layout, values) = rec_val {
if let Some(idx) = layout.index_of(*field) {
Ok(values[idx].clone())
} else {
Err(format!("Record does not have field :{}", field.name()))
}
} else {
Err(format!("Attempt to access field on non-record: {}", rec_val))
}
}
BoundKind::Set { addr, value } => {
let val = self.eval_internal(obs, value)?;
self.set_value(*addr, val.clone())?;
@@ -368,6 +384,29 @@ impl VM {
return Ok(Value::TailCallRequest(Box::new((obj, arg_vals))));
}
Value::Function(f) => return Ok((f.func)(arg_vals)),
Value::FieldAccessor(k) => {
if arg_vals.len() != 1 {
return Err(format!(
"Field accessor .{} expects exactly 1 argument, got {}",
k.name(),
arg_vals.len()
));
}
let rec = &arg_vals[0];
if let Value::Record(layout, values) = rec {
if let Some(idx) = layout.index_of(k) {
return Ok(values[idx].clone());
} else {
return Err(format!("Record does not have field :{}", k.name()));
}
} else {
return Err(format!(
"Field accessor .{} expects a record, got {}",
k.name(),
rec
));
}
}
_ => {
return Err(format!(
"Tail call target is not a function: {}",
@@ -380,6 +419,29 @@ impl VM {
loop {
match func_val {
Value::Function(f) => break Ok((f.func)(arg_vals)),
Value::FieldAccessor(k) => {
if arg_vals.len() != 1 {
break Err(format!(
"Field accessor .{} expects exactly 1 argument, got {}",
k.name(),
arg_vals.len()
));
}
let rec = &arg_vals[0];
if let Value::Record(layout, values) = rec {
if let Some(idx) = layout.index_of(k) {
break Ok(values[idx].clone());
} else {
break Err(format!("Record does not have field :{}", k.name()));
}
} else {
break Err(format!(
"Field accessor .{} expects a record, got {}",
k.name(),
rec
));
}
}
Value::Object(obj) => {
if let Some(closure) = obj.as_any().downcast_ref::<Closure>() {
let old_stack_top = self.stack.len();