use std::fmt::Write as _;
use crate::GcRef;
use crate::descriptor::{BuiltinTypeId, DynamicHasher, FormatSink, Tracer, TypeDescriptor};
#[repr(C)]
pub struct TupleSchema {
pub descriptors: &'static [*const TypeDescriptor],
}
impl TupleSchema {
pub fn arity(&self) -> usize {
self.descriptors.len()
}
fn descriptor_at(&self, i: usize, value: GcRef) -> &'static TypeDescriptor {
match self.descriptors.get(i).copied() {
Some(d) if !d.is_null() => {
unsafe { &*d }
}
_ => value.descriptor(),
}
}
#[must_use]
pub fn same_shape(&self, other: &TupleSchema) -> bool {
self.descriptors.len() == other.descriptors.len()
&& self
.descriptors
.iter()
.zip(other.descriptors.iter())
.all(|(a, b)| a.is_null() || b.is_null() || std::ptr::eq(*a, *b))
}
}
#[repr(C)]
pub struct TuplePayload {
pub schema: *const TupleSchema,
pub items: Vec<GcRef>,
}
unsafe fn tuple_trace(payload: *mut u8, tracer: &mut dyn Tracer) {
let p = unsafe { &*(payload as *const TuplePayload) };
for item in p.items.iter() {
tracer.trace(*item);
}
}
unsafe fn tuple_drop(payload: *mut u8) {
unsafe { std::ptr::drop_in_place(payload as *mut TuplePayload) };
}
unsafe fn tuple_format(payload: *const u8, out: &mut FormatSink<'_>) {
let p = unsafe { &*(payload as *const TuplePayload) };
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 elem_desc = schema.descriptor_at(i, *item);
unsafe { (elem_desc.format)(item.payload::<u8>() as *const u8, out) };
}
let _ = out.write_str(")");
}
unsafe fn tuple_equals(a: *const u8, b: *const u8) -> bool {
let pa = unsafe { &*(a as *const TuplePayload) };
let pb = unsafe { &*(b as *const TuplePayload) };
if pa.schema.is_null() || pb.schema.is_null() {
return false;
}
if !unsafe { (*pa.schema).same_shape(&*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 desc = schema.descriptor_at(i, *x);
if !std::ptr::eq(desc, schema.descriptor_at(i, *y)) {
return false;
}
let Some(eq) = desc.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 tuple_hash(payload: *const u8, hasher: &mut dyn DynamicHasher) {
let p = unsafe { &*(payload as *const TuplePayload) };
let schema = unsafe { &*p.schema };
hasher.write_bytes(&(p.items.len() as u64).to_le_bytes());
for (i, item) in p.items.iter().enumerate() {
let elem_desc = schema.descriptor_at(i, *item);
hasher.write_bytes(&elem_desc.id().to_u32().to_le_bytes());
let Some(hash_elem) = elem_desc.hash else {
return;
};
let elem_payload = item.payload::<u8>() as *const u8;
unsafe { hash_elem(elem_payload, hasher) };
}
}
unsafe fn tuple_compare(a: *const u8, b: *const u8) -> std::cmp::Ordering {
use std::cmp::Ordering;
let pa = unsafe { &*(a as *const TuplePayload) };
let pb = unsafe { &*(b as *const TuplePayload) };
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) => {}
}
match pa.items.len().cmp(&pb.items.len()) {
Ordering::Equal => {}
other => return other,
}
let (schema_a, schema_b) = unsafe { (&*pa.schema, &*pb.schema) };
for (i, (x, y)) in pa.items.iter().zip(pb.items.iter()).enumerate() {
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 TUPLE: TypeDescriptor = TypeDescriptor::builtin::<TuplePayload>(
BuiltinTypeId::Tuple,
"Tuple",
tuple_trace,
tuple_drop,
tuple_format,
Some(tuple_equals),
Some(tuple_hash),
Some(tuple_compare),
)
.with_owned_bytes(tuple_owned_bytes);
unsafe fn tuple_owned_bytes(payload: *const u8) -> usize {
let p = unsafe { &*(payload as *const TuplePayload) };
p.items.capacity() * std::mem::size_of::<GcRef>()
}
pub fn point_schema() -> &'static TupleSchema {
use std::sync::OnceLock;
struct SyncPtr(&'static TupleSchema);
unsafe impl Send for SyncPtr {}
unsafe impl Sync for SyncPtr {}
static POINT: OnceLock<SyncPtr> = OnceLock::new();
POINT
.get_or_init(|| {
let descriptors: &'static [*const TypeDescriptor] = Box::leak(
vec![
&crate::scalars::INT as *const _,
&crate::scalars::INT as *const _,
]
.into_boxed_slice(),
);
SyncPtr(Box::leak(Box::new(TupleSchema { descriptors })))
})
.0
}
#[cfg(test)]
mod tests {
use super::*;
use crate::abi::{praxis_alloc_tuple, praxis_tuple_set};
#[test]
fn tuple_descriptor_reports_capabilities() {
assert!(TUPLE.is_equatable());
assert!(TUPLE.is_hashable());
assert_eq!(TUPLE.name, "Tuple");
assert_eq!(TUPLE.as_builtin(), Some(BuiltinTypeId::Tuple));
}
#[test]
fn alloc_tuple_round_trips_arity() {
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(TupleSchema { descriptors }));
let tref = unsafe { praxis_alloc_tuple(&mut ctx, schema) };
let payload = tref.payload::<u8>() as *const TuplePayload;
let embedded = unsafe { (*payload).schema };
assert_eq!(embedded, schema as *const TupleSchema);
assert_eq!(unsafe { (*payload).items.len() }, 2);
}
#[test]
fn an_unknown_schema_slot_reads_the_values_own_descriptor() {
let mut rt = crate::Runtime::new();
let mut ctx = rt.context();
let unknown: &'static TupleSchema = Box::leak(Box::new(TupleSchema {
descriptors: Box::leak(vec![std::ptr::null(); 2].into_boxed_slice()),
}));
let build = |ctx: &mut crate::RuntimeContext, a: GcRef, b: GcRef| {
let t = unsafe { praxis_alloc_tuple(ctx, unknown) };
unsafe {
praxis_tuple_set(ctx, t, 0, a);
praxis_tuple_set(ctx, t, 1, b);
}
t
};
let (one, txt) = (rt.alloc_int(1), rt.alloc_text("hi"));
let mixed = build(&mut ctx, one, txt);
assert_eq!(
unsafe { (*(mixed.payload::<TuplePayload>())).items.len() },
2
);
let mut rendered = String::new();
unsafe {
tuple_format(
mixed.payload::<u8>() as *const u8,
&mut crate::FormatSink::display(&mut rendered),
);
}
assert_eq!(rendered, "(1, hi)");
let same = build(&mut ctx, rt.alloc_int(1), rt.alloc_text("hi"));
let other = build(&mut ctx, rt.alloc_int(1), rt.alloc_text("no"));
assert!(mixed.equals(&same));
assert!(!mixed.equals(&other));
let swapped = build(&mut ctx, rt.alloc_text("hi"), rt.alloc_int(1));
assert!(!mixed.equals(&swapped));
let known = unsafe { praxis_alloc_tuple(&mut ctx, point_schema()) };
for (index, value) in [3_i64, 4].into_iter().enumerate() {
let v = rt.alloc_int(value);
unsafe { praxis_tuple_set(&mut ctx, known, index as i64, v) };
}
let unknown_pair = build(&mut ctx, rt.alloc_int(3), rt.alloc_int(4));
assert!(known.equals(&unknown_pair));
assert!(unknown_pair.equals(&known));
}
#[test]
fn tuple_equality_uses_shape_not_schema_allocation_identity() {
let mut rt = crate::Runtime::new();
let mut ctx = rt.context();
let runtime_schema = point_schema();
let independently_interned_schema = Box::leak(Box::new(TupleSchema {
descriptors: Box::leak(
vec![
&crate::scalars::INT as *const TypeDescriptor,
&crate::scalars::INT as *const TypeDescriptor,
]
.into_boxed_slice(),
),
}));
let left = unsafe { praxis_alloc_tuple(&mut ctx, runtime_schema) };
let right = unsafe { praxis_alloc_tuple(&mut ctx, independently_interned_schema) };
for (index, value) in [3_i64, 4].into_iter().enumerate() {
let left_value = rt.alloc_int(value);
let right_value = rt.alloc_int(value);
unsafe {
praxis_tuple_set(&mut ctx, left, index as i64, left_value);
praxis_tuple_set(&mut ctx, right, index as i64, right_value);
}
}
assert!(
left.equals(&right),
"equivalent (Int, Int) schemas from runtime and codegen must describe the same tuple type"
);
}
#[test]
fn tuple_compare_is_arity_first_then_element_wise() {
let mut rt = crate::Runtime::new();
let ints = |n: usize| -> &'static TupleSchema {
Box::leak(Box::new(TupleSchema {
descriptors: Box::leak(
vec![&crate::scalars::INT as *const TypeDescriptor; n].into_boxed_slice(),
),
}))
};
let one_slot = ints(1);
let two_slots = ints(2);
let unknown: &'static TupleSchema = Box::leak(Box::new(TupleSchema {
descriptors: Box::leak(vec![std::ptr::null(); 1].into_boxed_slice()),
}));
let values: Vec<GcRef> = [1_i64, 0, 1, 2, 10]
.iter()
.map(|&n| rt.alloc_int(n))
.collect();
let text = rt.alloc_text("hi");
let mut ctx = rt.context();
let build =
|ctx: &mut crate::RuntimeContext, schema: &'static TupleSchema, items: &[GcRef]| {
unsafe {
let t = praxis_alloc_tuple(ctx, schema);
for (i, v) in items.iter().enumerate() {
praxis_tuple_set(ctx, t, i as i64, *v);
}
t
}
};
let cmp = |a: GcRef, b: GcRef| unsafe {
tuple_compare(
a.payload::<u8>() as *const u8,
b.payload::<u8>() as *const u8,
)
};
let single = build(&mut ctx, one_slot, &[values[0]]);
let pair = build(&mut ctx, two_slots, &[values[0], values[1]]);
assert_eq!(cmp(single, pair), std::cmp::Ordering::Less);
assert_eq!(cmp(pair, single), std::cmp::Ordering::Greater);
let low = build(&mut ctx, two_slots, &[values[2], values[3]]);
let high = build(&mut ctx, two_slots, &[values[2], values[4]]);
assert_eq!(cmp(low, high), std::cmp::Ordering::Less);
assert_eq!(cmp(low, low), std::cmp::Ordering::Equal);
let an_int = build(&mut ctx, unknown, &[values[0]]);
let a_text = build(&mut ctx, unknown, &[text]);
assert_eq!(cmp(an_int, a_text), cmp(an_int, a_text));
assert_eq!(cmp(an_int, a_text), cmp(a_text, an_int).reverse());
assert_ne!(cmp(an_int, a_text), std::cmp::Ordering::Equal);
}
}