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
+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()),