use std::fmt::Write as _;
use crate::GcRef;
use crate::descriptor::{BuiltinTypeId, DynamicHasher, FormatSink, Tracer, TypeDescriptor};
use crate::records::SchemaIdentity;
#[repr(C)]
pub struct EnumVariantShape {
pub name: &'static str,
pub payload: &'static [*const TypeDescriptor],
}
#[repr(C)]
pub struct EnumSchema {
pub identity: SchemaIdentity,
pub variants: &'static [EnumVariantShape],
}
impl EnumSchema {
#[must_use]
pub fn variant_at(&self, tag: usize) -> Option<&'static EnumVariantShape> {
let variants: &'static [EnumVariantShape] = self.variants;
variants.get(tag)
}
#[must_use]
pub fn arity_of(&self, tag: usize) -> usize {
self.variants.get(tag).map_or(0, |v| v.payload.len())
}
#[must_use]
pub fn descriptor_at(&self, tag: usize, i: usize, value: GcRef) -> &'static TypeDescriptor {
match self
.variants
.get(tag)
.and_then(|v| v.payload.get(i))
.copied()
{
Some(d) if !d.is_null() => {
unsafe { &*d }
}
_ => value.descriptor(),
}
}
#[must_use]
pub fn same_type(&self, other: &EnumSchema) -> bool {
if self.identity != other.identity {
return false;
}
self.variants.len() == other.variants.len()
&& self
.variants
.iter()
.zip(other.variants.iter())
.all(|(a, b)| {
a.name == b.name
&& a.payload.len() == b.payload.len()
&& a.payload
.iter()
.zip(b.payload.iter())
.all(|(x, y)| x.is_null() || y.is_null() || std::ptr::eq(*x, *y))
})
}
}
#[repr(C)]
pub struct EnumPayload {
pub schema: *const EnumSchema,
pub tag: u32,
pub items: Vec<GcRef>,
}
unsafe fn enum_trace(payload: *mut u8, tracer: &mut dyn Tracer) {
let p = unsafe { &*(payload as *const EnumPayload) };
for item in p.items.iter() {
tracer.trace(*item);
}
}
unsafe fn enum_drop(payload: *mut u8) {
unsafe { std::ptr::drop_in_place(payload as *mut EnumPayload) };
}
unsafe fn enum_format(payload: *const u8, out: &mut FormatSink<'_>) {
let p = unsafe { &*(payload as *const EnumPayload) };
if p.schema.is_null() {
let _ = write!(out, "<variant {}>", p.tag);
return;
}
let schema = unsafe { &*p.schema };
let tag = p.tag as usize;
match schema.variant_at(tag) {
Some(variant) => {
let _ = out.write_str(variant.name);
if p.items.is_empty() {
return;
}
let _ = out.write_str("(");
for (i, item) in p.items.iter().enumerate() {
if i > 0 {
let _ = out.write_str(", ");
}
let desc = schema.descriptor_at(tag, i, *item);
unsafe { (desc.format)(item.payload::<u8>() as *const u8, out) };
}
let _ = out.write_str(")");
}
None => {
let _ = write!(out, "<variant {}>", p.tag);
}
}
}
unsafe fn enum_equals(a: *const u8, b: *const u8) -> bool {
let pa = unsafe { &*(a as *const EnumPayload) };
let pb = unsafe { &*(b as *const EnumPayload) };
if pa.schema.is_null() || pb.schema.is_null() {
return false;
}
if !unsafe { (*pa.schema).same_type(&*pb.schema) } {
return false;
}
if pa.tag != pb.tag {
return false;
}
if pa.items.len() != pb.items.len() {
return false;
}
let schema = unsafe { &*pa.schema };
let tag = pa.tag as usize;
for (i, (x, y)) in pa.items.iter().zip(pb.items.iter()).enumerate() {
let desc = schema.descriptor_at(tag, i, *x);
if !std::ptr::eq(desc, schema.descriptor_at(tag, 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 enum_hash(payload: *const u8, hasher: &mut dyn DynamicHasher) {
let p = unsafe { &*(payload as *const EnumPayload) };
if !p.schema.is_null() {
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());
}
}
if let Some(variant) = schema.variant_at(p.tag as usize) {
hasher.write_bytes(variant.name.as_bytes());
}
}
hasher.write_bytes(&(p.tag as u64).to_le_bytes());
hasher.write_bytes(&(p.items.len() as u64).to_le_bytes());
for (i, item) in p.items.iter().enumerate() {
let desc = if p.schema.is_null() {
item.descriptor()
} else {
unsafe { &*p.schema }.descriptor_at(p.tag as usize, i, *item)
};
hasher.write_bytes(&desc.id().to_u32().to_le_bytes());
let Some(hash_item) = desc.hash else {
return;
};
let elem_payload = item.payload::<u8>() as *const u8;
unsafe { hash_item(elem_payload, hasher) };
}
}
unsafe fn enum_compare(a: *const u8, b: *const u8) -> std::cmp::Ordering {
use std::cmp::Ordering;
let pa = unsafe { &*(a as *const EnumPayload) };
let pb = unsafe { &*(b as *const EnumPayload) };
match (pa.schema.is_null(), pb.schema.is_null()) {
(true, true) => return pa.tag.cmp(&pb.tag),
(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.tag.cmp(&pb.tag) {
Ordering::Equal => {}
other => return other,
}
match pa.items.len().cmp(&pb.items.len()) {
Ordering::Equal => {}
other => return other,
}
let tag = pa.tag as usize;
for (i, (x, y)) in pa.items.iter().zip(pb.items.iter()).enumerate() {
let dx = schema_a.descriptor_at(tag, i, *x);
let dy = schema_b.descriptor_at(pb.tag as usize, i, *y);
match unsafe { crate::ordering::slot_cmp(*x, *y, dx, dy) } {
Ordering::Equal => {}
other => return other,
}
}
Ordering::Equal
}
pub static ENUM: TypeDescriptor = TypeDescriptor::builtin::<EnumPayload>(
BuiltinTypeId::Enum,
"Enum",
enum_trace,
enum_drop,
enum_format,
Some(enum_equals),
Some(enum_hash),
Some(enum_compare),
)
.with_owned_bytes(enum_owned_bytes);
unsafe fn enum_owned_bytes(payload: *const u8) -> usize {
let p = unsafe { &*(payload as *const EnumPayload) };
p.items.capacity() * std::mem::size_of::<GcRef>()
}
pub const OPTION_SOME_TAG: i64 = 0;
pub const OPTION_NONE_TAG: i64 = 1;
#[must_use]
pub fn option_schema() -> &'static EnumSchema {
use std::sync::OnceLock;
struct SyncPtr(&'static EnumSchema);
unsafe impl Send for SyncPtr {}
unsafe impl Sync for SyncPtr {}
static OPTION: OnceLock<SyncPtr> = OnceLock::new();
OPTION
.get_or_init(|| {
let some_payload: &'static [*const TypeDescriptor] =
Box::leak(vec![std::ptr::null(); 1].into_boxed_slice());
let variants: &'static [EnumVariantShape] = Box::leak(
vec![
EnumVariantShape {
name: "Some",
payload: some_payload,
},
EnumVariantShape {
name: "None",
payload: &[],
},
]
.into_boxed_slice(),
);
SyncPtr(Box::leak(Box::new(EnumSchema {
identity: SchemaIdentity::Nominal("Option"),
variants,
})))
})
.0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn enum_descriptor_reports_capabilities() {
assert!(ENUM.is_equatable());
assert!(ENUM.is_hashable());
assert_eq!(ENUM.name, "Enum");
assert_eq!(ENUM.as_builtin(), Some(BuiltinTypeId::Enum));
}
}
#[cfg(test)]
mod alloc_tests {
use super::*;
use crate::abi::{praxis_alloc_enum, praxis_enum_set_payload};
fn leak_schema(identity: SchemaIdentity, names: &[&'static str]) -> &'static EnumSchema {
let variants: Vec<EnumVariantShape> = names
.iter()
.map(|name| EnumVariantShape { name, payload: &[] })
.collect();
Box::leak(Box::new(EnumSchema {
identity,
variants: Box::leak(variants.into_boxed_slice()),
}))
}
fn equal(a: GcRef, b: GcRef) -> bool {
unsafe {
enum_equals(
a.payload::<u8>() as *const u8,
b.payload::<u8>() as *const u8,
)
}
}
fn hash_of(e: GcRef) -> u64 {
let mut h = crate::descriptor::StructHasher::new();
unsafe { enum_hash(e.payload::<u8>() as *const u8, &mut h) };
h.finish()
}
fn rendered(e: GcRef) -> String {
let mut s = String::new();
unsafe {
enum_format(
e.payload::<u8>() as *const u8,
&mut crate::FormatSink::display(&mut s),
)
};
s
}
#[test]
fn alloc_enum_round_trips_the_tag_and_the_schema() {
let mut rt = crate::Runtime::new();
let mut ctx = rt.context();
let schema = leak_schema(SchemaIdentity::Nominal("Colour"), &["Red", "Green"]);
for tag in 0..2_i64 {
let eref = unsafe { praxis_alloc_enum(&mut ctx, schema, tag) };
let payload = eref.payload::<u8>() as *const EnumPayload;
assert_eq!(unsafe { (*payload).tag }, tag as u32);
assert_eq!(unsafe { (*payload).schema }, schema as *const EnumSchema);
assert_eq!(unsafe { (*payload).items.len() }, 0);
}
}
#[test]
fn an_out_of_range_tag_answers_the_unit_sentinel() {
let mut rt = crate::Runtime::new();
let mut ctx = rt.context();
let schema = leak_schema(SchemaIdentity::Nominal("Colour"), &["Red", "Green"]);
let past_the_end = unsafe { praxis_alloc_enum(&mut ctx, schema, 2) };
assert_eq!(
past_the_end.descriptor().id(),
crate::scalars::UNIT.id(),
"a tag the schema has no variant for cannot allocate an enum value"
);
let null = unsafe { praxis_alloc_enum(&mut ctx, std::ptr::null(), 0) };
assert_eq!(null.descriptor().id(), crate::scalars::UNIT.id());
}
#[test]
fn two_enum_types_of_one_shape_are_not_one_type() {
let mut rt = crate::Runtime::new();
let mut ctx = rt.context();
let colour = leak_schema(SchemaIdentity::Nominal("Colour"), &["Red", "Green"]);
let light = leak_schema(SchemaIdentity::Nominal("Light"), &["Red", "Green"]);
let anon = leak_schema(SchemaIdentity::Anonymous, &["Red", "Green"]);
let red = unsafe { praxis_alloc_enum(&mut ctx, colour, 0) };
let stop = unsafe { praxis_alloc_enum(&mut ctx, light, 0) };
let bare = unsafe { praxis_alloc_enum(&mut ctx, anon, 0) };
assert!(!equal(red, stop), "two enum types are not one type");
assert!(
!equal(red, bare),
"a declared type is not a structural shape"
);
let green = unsafe { praxis_alloc_enum(&mut ctx, colour, 1) };
assert!(!equal(red, green));
}
#[test]
fn one_enum_type_built_by_two_schema_allocations_is_one_type() {
let mut rt = crate::Runtime::new();
let mut ctx = rt.context();
let first = leak_schema(SchemaIdentity::Nominal("Colour"), &["Red", "Green"]);
let second = leak_schema(SchemaIdentity::Nominal("Colour"), &["Red", "Green"]);
assert!(
!std::ptr::eq(first, second),
"the two schemas must really be distinct allocations"
);
let a = unsafe { praxis_alloc_enum(&mut ctx, first, 0) };
let b = unsafe { praxis_alloc_enum(&mut ctx, second, 0) };
assert!(equal(a, b));
assert_eq!(hash_of(a), hash_of(b), "equal enums must hash equally");
}
#[test]
fn one_enum_name_over_two_variant_lists_is_two_types() {
let mut rt = crate::Runtime::new();
let mut ctx = rt.context();
let two = leak_schema(SchemaIdentity::Nominal("C"), &["Red", "Green"]);
let renamed = leak_schema(SchemaIdentity::Nominal("C"), &["Red", "Blue"]);
let a = unsafe { praxis_alloc_enum(&mut ctx, two, 0) };
let b = unsafe { praxis_alloc_enum(&mut ctx, renamed, 0) };
assert!(!equal(a, b));
}
#[test]
fn a_some_of_two_different_payload_types_is_not_equal() {
let mut rt = crate::Runtime::new();
let mut ctx = rt.context();
let schema = option_schema();
let build = |ctx: &mut crate::RuntimeContext, v: GcRef| {
let e = unsafe { praxis_alloc_enum(ctx, schema, OPTION_SOME_TAG) };
unsafe { praxis_enum_set_payload(ctx, e, 0, v) };
e
};
let one = rt.alloc_int(1);
let text = rt.alloc_text("1");
let boxed_int = build(&mut ctx, one);
let boxed_text = build(&mut ctx, text);
assert!(!equal(boxed_int, boxed_text));
let again = build(&mut ctx, rt.alloc_int(1));
assert!(equal(boxed_int, again));
assert_eq!(hash_of(boxed_int), hash_of(again));
}
#[test]
fn a_known_payload_slot_and_an_unknown_one_describe_one_option_type() {
let mut rt = crate::Runtime::new();
let mut ctx = rt.context();
let typed: &'static EnumSchema = Box::leak(Box::new(EnumSchema {
identity: SchemaIdentity::Nominal("Option"),
variants: Box::leak(
vec![
EnumVariantShape {
name: "Some",
payload: Box::leak(
vec![&crate::scalars::INT as *const TypeDescriptor].into_boxed_slice(),
),
},
EnumVariantShape {
name: "None",
payload: &[],
},
]
.into_boxed_slice(),
),
}));
assert!(typed.same_type(option_schema()));
assert!(option_schema().same_type(typed));
let from_codegen = unsafe { praxis_alloc_enum(&mut ctx, typed, OPTION_SOME_TAG) };
let seven = rt.alloc_int(7);
unsafe { praxis_enum_set_payload(&mut ctx, from_codegen, 0, seven) };
let from_runtime = unsafe { praxis_alloc_enum(&mut ctx, option_schema(), OPTION_SOME_TAG) };
let seven_again = rt.alloc_int(7);
unsafe { praxis_enum_set_payload(&mut ctx, from_runtime, 0, seven_again) };
assert!(equal(from_codegen, from_runtime));
assert_eq!(hash_of(from_codegen), hash_of(from_runtime));
}
#[test]
fn an_enum_renders_its_variant_name_and_payload() {
let mut rt = crate::Runtime::new();
let mut ctx = rt.context();
let some = unsafe { praxis_alloc_enum(&mut ctx, option_schema(), OPTION_SOME_TAG) };
let three = rt.alloc_int(3);
unsafe { praxis_enum_set_payload(&mut ctx, some, 0, three) };
assert_eq!(rendered(some), "Some(3)");
let none = unsafe { praxis_alloc_enum(&mut ctx, option_schema(), OPTION_NONE_TAG) };
assert_eq!(rendered(none), "None");
}
#[test]
fn enum_compare_is_declaration_order_then_payload() {
let mut rt = crate::Runtime::new();
let ten = rt.alloc_int(10);
let two = rt.alloc_int(2);
let zero = rt.alloc_int(0);
let mut ctx = rt.context();
let some = |ctx: &mut crate::RuntimeContext, payload| {
unsafe {
let e = praxis_alloc_enum(ctx, option_schema(), OPTION_SOME_TAG);
praxis_enum_set_payload(ctx, e, 0, payload);
e
}
};
let cmp = |a: GcRef, b: GcRef| unsafe {
enum_compare(
a.payload::<u8>() as *const u8,
b.payload::<u8>() as *const u8,
)
};
let none = unsafe { praxis_alloc_enum(&mut ctx, option_schema(), OPTION_NONE_TAG) };
let some_zero = some(&mut ctx, zero);
let some_two = some(&mut ctx, two);
let some_ten = some(&mut ctx, ten);
assert_eq!(cmp(some_zero, none), std::cmp::Ordering::Less);
assert_eq!(cmp(none, some_zero), std::cmp::Ordering::Greater);
assert_eq!(cmp(some_two, some_ten), std::cmp::Ordering::Less);
assert_eq!(cmp(some_ten, some_ten), std::cmp::Ordering::Equal);
}
}