use std::fmt::Write as _;
use crate::GcRef;
use crate::descriptor::{BuiltinTypeId, DynamicHasher, FormatSink, Tracer, TypeDescriptor};
#[repr(C)]
pub struct RecordField {
pub name: &'static str,
pub descriptor: *const TypeDescriptor,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[repr(C)]
pub enum SchemaIdentity {
Anonymous,
Nominal(&'static str),
}
impl SchemaIdentity {
pub(crate) fn order_key(self) -> (u8, &'static str) {
match self {
SchemaIdentity::Anonymous => (0, ""),
SchemaIdentity::Nominal(name) => (1, name),
}
}
}
#[repr(C)]
pub struct RecordSchema {
pub identity: SchemaIdentity,
pub fields: &'static [RecordField],
}
impl RecordSchema {
pub fn arity(&self) -> usize {
self.fields.len()
}
fn descriptor_at(&self, i: usize, value: GcRef) -> &'static TypeDescriptor {
match self.fields.get(i).map(|f| f.descriptor) {
Some(d) if !d.is_null() => {
unsafe { &*d }
}
_ => value.descriptor(),
}
}
#[must_use]
pub fn same_type(&self, other: &RecordSchema) -> bool {
if self.identity != other.identity {
return false;
}
self.fields.len() == other.fields.len()
&& self
.fields
.iter()
.zip(other.fields.iter())
.all(|(a, b)| a.name == b.name && std::ptr::eq(a.descriptor, b.descriptor))
}
}
#[repr(C)]
pub struct RecordPayload {
pub schema: *const RecordSchema,
pub items: Vec<GcRef>,
}
unsafe fn record_trace(payload: *mut u8, tracer: &mut dyn Tracer) {
let p = unsafe { &*(payload as *const RecordPayload) };
for item in p.items.iter() {
tracer.trace(*item);
}
}
unsafe fn record_drop(payload: *mut u8) {
unsafe { std::ptr::drop_in_place(payload as *mut RecordPayload) };
}
unsafe fn record_format(payload: *const u8, out: &mut FormatSink<'_>) {
let p = unsafe { &*(payload as *const RecordPayload) };
let schema = unsafe { &*p.schema };
let _ = out.write_str("{ ");
for (i, item) in p.items.iter().enumerate() {
if i > 0 {
let _ = out.write_str(", ");
}
let field = &schema.fields[i];
let _ = out.write_str(field.name);
let _ = out.write_str(": ");
let elem_desc = unsafe { &*field.descriptor };
unsafe { (elem_desc.format)(item.payload::<u8>() as *const u8, out) };
}
let _ = out.write_str(" }");
}
unsafe fn record_equals(a: *const u8, b: *const u8) -> bool {
let pa = unsafe { &*(a as *const RecordPayload) };
let pb = unsafe { &*(b as *const RecordPayload) };
if pa.schema.is_null() || pb.schema.is_null() {
return false;
}
if !unsafe { (*pa.schema).same_type(&*pb.schema) } {
return false;
}
if pa.items.len() != pb.items.len() {
return false;
}
let schema = unsafe { &*pa.schema };
for (i, (x, y)) in pa.items.iter().zip(pb.items.iter()).enumerate() {
let Some(eq) = unsafe { &*schema.fields[i].descriptor }.equals else {
return false;
};
let xe = x.payload::<u8>() as *const u8;
let ye = y.payload::<u8>() as *const u8;
if !unsafe { eq(xe, ye) } {
return false;
}
}
true
}
unsafe fn record_hash(payload: *const u8, hasher: &mut dyn DynamicHasher) {
let p = unsafe { &*(payload as *const RecordPayload) };
let schema = unsafe { &*p.schema };
match schema.identity {
SchemaIdentity::Anonymous => hasher.write_bytes(b"anon"),
SchemaIdentity::Nominal(name) => {
hasher.write_bytes(b"nom");
hasher.write_bytes(name.as_bytes());
}
}
hasher.write_bytes(&(p.items.len() as u64).to_le_bytes());
for (i, item) in p.items.iter().enumerate() {
hasher.write_bytes(schema.fields[i].name.as_bytes());
let field_desc = unsafe { &*schema.fields[i].descriptor };
hasher.write_bytes(&field_desc.id().to_u32().to_le_bytes());
let Some(hash_field) = field_desc.hash else {
return;
};
let elem_payload = item.payload::<u8>() as *const u8;
unsafe { hash_field(elem_payload, hasher) };
}
}
unsafe fn record_compare(a: *const u8, b: *const u8) -> std::cmp::Ordering {
use std::cmp::Ordering;
let pa = unsafe { &*(a as *const RecordPayload) };
let pb = unsafe { &*(b as *const RecordPayload) };
match (pa.schema.is_null(), pb.schema.is_null()) {
(true, true) => return Ordering::Equal,
(true, false) => return Ordering::Less,
(false, true) => return Ordering::Greater,
(false, false) => {}
}
let (schema_a, schema_b) = unsafe { (&*pa.schema, &*pb.schema) };
match schema_a
.identity
.order_key()
.cmp(&schema_b.identity.order_key())
{
Ordering::Equal => {}
other => return other,
}
match pa.items.len().cmp(&pb.items.len()) {
Ordering::Equal => {}
other => return other,
}
for (i, (x, y)) in pa.items.iter().zip(pb.items.iter()).enumerate() {
let (na, nb) = (schema_a.fields[i].name, schema_b.fields[i].name);
match na.cmp(nb) {
Ordering::Equal => {}
other => return other,
}
let dx = schema_a.descriptor_at(i, *x);
let dy = schema_b.descriptor_at(i, *y);
match unsafe { crate::ordering::slot_cmp(*x, *y, dx, dy) } {
Ordering::Equal => {}
other => return other,
}
}
Ordering::Equal
}
pub static RECORD: TypeDescriptor = TypeDescriptor::builtin::<RecordPayload>(
BuiltinTypeId::Record,
"Record",
record_trace,
record_drop,
record_format,
Some(record_equals),
Some(record_hash),
Some(record_compare),
)
.with_owned_bytes(record_owned_bytes);
pub struct Direction {
pub name: &'static str,
pub dx: i64,
pub dy: i64,
}
pub static AROUND4_DIRECTIONS: &[Direction] = &[
Direction {
name: "up",
dx: 0,
dy: -1,
},
Direction {
name: "left",
dx: -1,
dy: 0,
},
Direction {
name: "right",
dx: 1,
dy: 0,
},
Direction {
name: "down",
dx: 0,
dy: 1,
},
];
pub static AROUND8_DIRECTIONS: &[Direction] = &[
Direction {
name: "up_left",
dx: -1,
dy: -1,
},
Direction {
name: "up",
dx: 0,
dy: -1,
},
Direction {
name: "up_right",
dx: 1,
dy: -1,
},
Direction {
name: "left",
dx: -1,
dy: 0,
},
Direction {
name: "right",
dx: 1,
dy: 0,
},
Direction {
name: "down_left",
dx: -1,
dy: 1,
},
Direction {
name: "down",
dx: 0,
dy: 1,
},
Direction {
name: "down_right",
dx: 1,
dy: 1,
},
];
fn leak_around_schema(
name: &'static str,
directions: &'static [Direction],
) -> &'static RecordSchema {
let fields: Vec<RecordField> = directions
.iter()
.map(|d| RecordField {
name: d.name,
descriptor: &crate::enums::ENUM,
})
.collect();
Box::leak(Box::new(RecordSchema {
identity: SchemaIdentity::Nominal(name),
fields: Box::leak(fields.into_boxed_slice()),
}))
}
#[must_use]
pub fn around4_schema() -> &'static RecordSchema {
use std::sync::OnceLock;
struct SyncPtr(&'static RecordSchema);
unsafe impl Send for SyncPtr {}
unsafe impl Sync for SyncPtr {}
static AROUND4: OnceLock<SyncPtr> = OnceLock::new();
AROUND4
.get_or_init(|| SyncPtr(leak_around_schema("Around4", AROUND4_DIRECTIONS)))
.0
}
#[must_use]
pub fn around8_schema() -> &'static RecordSchema {
use std::sync::OnceLock;
struct SyncPtr(&'static RecordSchema);
unsafe impl Send for SyncPtr {}
unsafe impl Sync for SyncPtr {}
static AROUND8: OnceLock<SyncPtr> = OnceLock::new();
AROUND8
.get_or_init(|| SyncPtr(leak_around_schema("Around8", AROUND8_DIRECTIONS)))
.0
}
unsafe fn record_owned_bytes(payload: *const u8) -> usize {
let p = unsafe { &*(payload as *const RecordPayload) };
p.items.capacity() * std::mem::size_of::<GcRef>()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn record_descriptor_reports_capabilities() {
assert!(RECORD.is_equatable());
assert!(RECORD.is_hashable());
assert_eq!(RECORD.name, "Record");
assert_eq!(RECORD.as_builtin(), Some(BuiltinTypeId::Record));
}
#[test]
fn grid_descriptor_reports_capabilities() {
assert!(crate::collections::GRID.is_equatable());
assert!(crate::collections::GRID.is_hashable());
assert_eq!(crate::collections::GRID.name, "Grid");
assert_eq!(
crate::collections::GRID.as_builtin(),
Some(BuiltinTypeId::Grid)
);
}
fn leak_schema(identity: SchemaIdentity, names: &[&'static str]) -> &'static RecordSchema {
let fields: Vec<RecordField> = names
.iter()
.map(|name| RecordField {
name,
descriptor: &crate::scalars::INT,
})
.collect();
Box::leak(Box::new(RecordSchema {
identity,
fields: Box::leak(fields.into_boxed_slice()),
}))
}
fn record_of(
ctx: &mut crate::RuntimeContext,
schema: &'static RecordSchema,
values: &[i64],
) -> GcRef {
let r = unsafe { crate::abi::praxis_alloc_record(ctx, schema) };
for (i, v) in values.iter().enumerate() {
let boxed = unsafe { crate::abi::praxis_alloc_int(ctx, *v) };
unsafe { crate::abi::praxis_record_set_field(ctx, r, i as u32, boxed) };
}
r
}
fn equal(a: GcRef, b: GcRef) -> bool {
unsafe {
record_equals(
a.payload::<u8>() as *const u8,
b.payload::<u8>() as *const u8,
)
}
}
fn hash_of(r: GcRef) -> u64 {
let mut h = crate::descriptor::StructHasher::new();
unsafe { record_hash(r.payload::<u8>() as *const u8, &mut h) };
h.finish()
}
#[test]
fn anonymous_records_of_one_shape_are_equal_across_schema_allocations() {
let mut rt = crate::Runtime::new();
let mut ctx = rt.context();
let first = leak_schema(SchemaIdentity::Anonymous, &["x", "y"]);
let second = leak_schema(SchemaIdentity::Anonymous, &["x", "y"]);
assert!(
!std::ptr::eq(first, second),
"the two schemas must really be distinct allocations"
);
let a = record_of(&mut ctx, first, &[1, 2]);
let b = record_of(&mut ctx, second, &[1, 2]);
assert!(equal(a, b));
assert_eq!(hash_of(a), hash_of(b), "equal records must hash equally");
let c = record_of(&mut ctx, second, &[1, 3]);
assert!(!equal(a, c));
}
#[test]
fn record_compare_is_identity_then_fields() {
let mut rt = crate::Runtime::new();
let mut ctx = rt.context();
let cmp = |a: GcRef, b: GcRef| unsafe {
record_compare(
a.payload::<u8>() as *const u8,
b.payload::<u8>() as *const u8,
)
};
let first = leak_schema(SchemaIdentity::Anonymous, &["x", "y"]);
let second = leak_schema(SchemaIdentity::Anonymous, &["x", "y"]);
assert!(!std::ptr::eq(first, second));
let a = record_of(&mut ctx, first, &[1, 2]);
let same = record_of(&mut ctx, second, &[1, 2]);
assert_eq!(
cmp(a, same),
std::cmp::Ordering::Equal,
"one shape, one value"
);
let bigger = record_of(&mut ctx, second, &[1, 10]);
let smaller = record_of(&mut ctx, second, &[1, 2]);
assert_eq!(cmp(smaller, bigger), std::cmp::Ordering::Less);
let point = leak_schema(SchemaIdentity::Nominal("Point"), &["x", "y"]);
let p = record_of(&mut ctx, point, &[0, 0]);
assert_eq!(cmp(a, p), std::cmp::Ordering::Less);
assert_eq!(cmp(p, a), std::cmp::Ordering::Greater);
}
#[test]
fn nominal_records_of_different_types_are_never_equal() {
let mut rt = crate::Runtime::new();
let mut ctx = rt.context();
let point = leak_schema(SchemaIdentity::Nominal("Point"), &["x", "y"]);
let vector = leak_schema(SchemaIdentity::Nominal("Vector"), &["x", "y"]);
let anon = leak_schema(SchemaIdentity::Anonymous, &["x", "y"]);
let p = record_of(&mut ctx, point, &[1, 2]);
let v = record_of(&mut ctx, vector, &[1, 2]);
let a = record_of(&mut ctx, anon, &[1, 2]);
assert!(!equal(p, v), "two record types are not one type");
assert!(!equal(p, a), "a declared type is not a structural shape");
let point_again = leak_schema(SchemaIdentity::Nominal("Point"), &["x", "y"]);
let p2 = record_of(&mut ctx, point_again, &[1, 2]);
assert!(equal(p, p2));
assert_eq!(hash_of(p), hash_of(p2));
}
#[test]
fn one_nominal_name_over_two_shapes_is_two_types() {
let mut rt = crate::Runtime::new();
let mut ctx = rt.context();
let two_fields = leak_schema(SchemaIdentity::Nominal("P"), &["x", "y"]);
let renamed = leak_schema(SchemaIdentity::Nominal("P"), &["x", "z"]);
let a = record_of(&mut ctx, two_fields, &[1, 2]);
let b = record_of(&mut ctx, renamed, &[1, 2]);
assert!(!equal(a, b));
}
#[test]
fn around_schemas_match_the_catalog() {
use praxis_stdlib::type_pattern::{CollectionCtor, TypePattern};
let catalog = praxis_stdlib::builtin_catalog();
let grid = TypePattern::Collection {
ctor: CollectionCtor::Grid,
args: vec![TypePattern::var("T")],
};
for (method, schema, directions) in [
(
"around4",
super::around4_schema(),
super::AROUND4_DIRECTIONS,
),
(
"around8",
super::around8_schema(),
super::AROUND8_DIRECTIONS,
),
] {
let entry = catalog
.by_receiver_and_name(&grid, method)
.next()
.unwrap_or_else(|| panic!("`Grid[T].{method}` is a catalog row"));
let TypePattern::Record { name, fields } = &entry.result else {
panic!("`Grid[T].{method}` answers a nominal record");
};
assert_eq!(
schema.identity,
SchemaIdentity::Nominal(name),
"`{method}`'s schema must name the type its row does"
);
let from_catalog: Vec<&str> = fields.iter().map(|(n, _)| *n).collect();
let from_schema: Vec<&str> = schema.fields.iter().map(|f| f.name).collect();
assert_eq!(
from_catalog, from_schema,
"`{method}`'s catalog field order is the slot index a field read \
compiles to, and the schema's is where the value's fields land"
);
let from_directions: Vec<&str> = directions.iter().map(|d| d.name).collect();
assert_eq!(from_directions, from_schema);
for (fname, fpat) in fields {
assert!(
matches!(fpat, TypePattern::Option(_)),
"`{method}.{fname}` must be an Option: a direction that \
leaves the grid has no point"
);
}
for field in schema.fields {
assert!(
std::ptr::eq(field.descriptor, &crate::enums::ENUM),
"`{method}.{}` holds an Option, so its slot dispatches \
through ENUM",
field.name
);
}
}
}
#[test]
fn around4_and_around8_are_two_types() {
let four = super::around4_schema();
let eight = super::around8_schema();
assert_eq!(four.identity, SchemaIdentity::Nominal("Around4"));
assert_eq!(eight.identity, SchemaIdentity::Nominal("Around8"));
assert_eq!(four.arity(), 4);
assert_eq!(eight.arity(), 8);
assert!(!four.same_type(eight));
assert!(std::ptr::eq(four, super::around4_schema()));
assert!(std::ptr::eq(eight, super::around8_schema()));
}
#[test]
fn record_equals_identical_int_fields() {
let mut rt = crate::Runtime::new();
let mut ctx = rt.context();
let descriptors: &'static [*const TypeDescriptor] =
Box::leak(vec![&crate::scalars::INT as *const TypeDescriptor; 2].into_boxed_slice());
let schema = Box::leak(Box::new(RecordSchema {
identity: SchemaIdentity::Anonymous,
fields: Box::leak(
vec![
RecordField {
name: "x",
descriptor: descriptors[0],
},
RecordField {
name: "y",
descriptor: descriptors[1],
},
]
.into_boxed_slice(),
),
}));
let a = unsafe { crate::abi::praxis_alloc_record(&mut ctx, schema) };
let b = unsafe { crate::abi::praxis_alloc_record(&mut ctx, schema) };
let one = unsafe { crate::abi::praxis_alloc_int(&mut ctx, 1) };
let two = unsafe { crate::abi::praxis_alloc_int(&mut ctx, 2) };
unsafe {
crate::abi::praxis_record_set_field(&mut ctx, a, 0, one);
crate::abi::praxis_record_set_field(&mut ctx, a, 1, two);
crate::abi::praxis_record_set_field(&mut ctx, b, 0, one);
crate::abi::praxis_record_set_field(&mut ctx, b, 1, two);
}
assert!(unsafe {
record_equals(
a.payload::<u8>() as *const u8,
b.payload::<u8>() as *const u8,
)
});
let three = unsafe { crate::abi::praxis_alloc_int(&mut ctx, 3) };
unsafe { crate::abi::praxis_record_set_field(&mut ctx, b, 1, three) };
assert!(!unsafe {
record_equals(
a.payload::<u8>() as *const u8,
b.payload::<u8>() as *const u8,
)
});
}
}