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);
}