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:
+126
-29
@@ -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()),
|
||||
|
||||
Reference in New Issue
Block a user