Skip to main content

praxis_runtime/
enums.rs

1//! The `Enum` value descriptor (§4.6).
2//!
3//! An enum value carries a discriminant (`tag`) selecting its variant, the
4//! variant's payload values (one `GcRef` per payload type, in declaration
5//! order), and a pointer to the [`EnumSchema`] that says **which enum type it
6//! is**.
7//!
8//! The schema is what records and tuples already carry, for the same reason: a
9//! single `ENUM` descriptor serves every enum value, so without a per-type
10//! schema in the payload two unrelated enums of one shape would be one type —
11//! `Colour::Red` would equal `Light::Red`, they would hash into the same
12//! bucket, and a value could only render as `<variant 0: …>` because nothing
13//! in the runtime would know the variant was called `Red`.
14//!
15//! Each distinct enum *type* gets one schema, built by whichever producer
16//! allocates the value: the codegen's per-generation cache, the input parser's
17//! registry, or the runtime's own [`option_schema`]. Schemas are therefore
18//! compared by **type identity and shape**, never by address.
19
20use std::fmt::Write as _;
21
22use crate::GcRef;
23use crate::descriptor::{BuiltinTypeId, DynamicHasher, FormatSink, Tracer, TypeDescriptor};
24use crate::records::SchemaIdentity;
25
26/// One variant of an enum shape: its source name plus the descriptors for its
27/// payload slots, in declaration order. A payload-less variant has an empty
28/// slice.
29///
30/// A slot may be **null**, meaning the producer had no static type for it —
31/// exactly the encoding [`TupleSchema`](crate::tuples::TupleSchema) uses. The
32/// runtime's own `Option` schema is the motivating case: `praxis_map_get`
33/// learns `V` from the value it found, never from a static type, so `Some`'s
34/// slot is unknown there and known (`Int`, `Text`, …) in the schema the codegen
35/// builds for the same `Option`. The two must still be one type, and the
36/// *value's* descriptor — read off its header, so never wrong — answers for an
37/// unknown slot.
38#[repr(C)]
39pub struct EnumVariantShape {
40    pub name: &'static str,
41    pub payload: &'static [*const TypeDescriptor],
42}
43
44/// The static shape of an enum type: what type it is, plus its variants in
45/// declaration order (the tag indexes this list).
46///
47/// There is no separate `name` field: [`SchemaIdentity`] already carries the
48/// declared name for a nominal type and says there is none for a structural
49/// one, so a second copy could only disagree with it.
50#[repr(C)]
51pub struct EnumSchema {
52    pub identity: SchemaIdentity,
53    pub variants: &'static [EnumVariantShape],
54}
55
56impl EnumSchema {
57    /// The variant `tag` selects, or `None` if the tag is past the end.
58    #[must_use]
59    pub fn variant_at(&self, tag: usize) -> Option<&'static EnumVariantShape> {
60        // `variants` is already `&'static`, so copying the slice reference out
61        // of `self` before indexing keeps the element's lifetime `'static`.
62        let variants: &'static [EnumVariantShape] = self.variants;
63        variants.get(tag)
64    }
65
66    /// How many payload slots variant `tag` carries. Zero for an unknown tag —
67    /// which `praxis_alloc_enum` refuses before it allocates.
68    #[must_use]
69    pub fn arity_of(&self, tag: usize) -> usize {
70        self.variants.get(tag).map_or(0, |v| v.payload.len())
71    }
72
73    /// The descriptor to dispatch payload slot `i` of variant `tag` through for
74    /// `value`: the static one when the producer had it, and the value's own
75    /// otherwise.
76    ///
77    /// The fallback is what makes an unknown slot *safe* rather than merely
78    /// tolerated, and it is the same rule `TupleSchema::descriptor_at` states:
79    /// an object always knows what it is.
80    #[must_use]
81    pub fn descriptor_at(&self, tag: usize, i: usize, value: GcRef) -> &'static TypeDescriptor {
82        match self
83            .variants
84            .get(tag)
85            .and_then(|v| v.payload.get(i))
86            .copied()
87        {
88            Some(d) if !d.is_null() => {
89                // SAFETY: a non-null slot is a `'static` descriptor pointer.
90                unsafe { &*d }
91            }
92            _ => value.descriptor(),
93        }
94    }
95
96    /// Whether two schemas describe the *same enum type* — the same identity
97    /// and the same variant shape.
98    ///
99    /// Type identity, not allocation identity, exactly as
100    /// [`RecordSchema::same_type`](crate::records::RecordSchema::same_type):
101    /// there are three producers of an `Option` schema (every JIT generation,
102    /// the input parser's registry, and [`option_schema`]), so comparing
103    /// addresses would make a `Some(1)` from the runtime unequal to a `Some(1)`
104    /// the program wrote.
105    ///
106    /// A **null** payload slot is unknown, not a fourth type: it agrees with
107    /// whatever the other side says, and the values decide (see `enum_equals`).
108    /// That is what lets [`option_schema`]'s unknown `Some` slot be the same
109    /// type as the codegen's `Option[Int]`.
110    #[must_use]
111    pub fn same_type(&self, other: &EnumSchema) -> bool {
112        if self.identity != other.identity {
113            return false;
114        }
115        self.variants.len() == other.variants.len()
116            && self
117                .variants
118                .iter()
119                .zip(other.variants.iter())
120                .all(|(a, b)| {
121                    a.name == b.name
122                        && a.payload.len() == b.payload.len()
123                        && a.payload
124                            .iter()
125                            .zip(b.payload.iter())
126                            .all(|(x, y)| x.is_null() || y.is_null() || std::ptr::eq(*x, *y))
127                })
128    }
129}
130
131/// The runtime payload of an enum value: which enum type it is, the variant
132/// discriminant, and the variant's payload values (one `GcRef` per payload
133/// field, in declaration order).
134///
135/// `schema` is **first**, mirroring `RecordPayload` and `TuplePayload`, so the
136/// tag does not sit at offset 0: the codegen reads it through `offset_of!`
137/// rather than a literal.
138#[repr(C)]
139pub struct EnumPayload {
140    /// The static enum type. `items.len()` must equal
141    /// `schema.arity_of(tag)`.
142    pub schema: *const EnumSchema,
143    /// Which variant this value is (index into `schema.variants`).
144    pub tag: u32,
145    /// The variant's payload values (empty for a payload-less variant).
146    pub items: Vec<GcRef>,
147}
148
149unsafe fn enum_trace(payload: *mut u8, tracer: &mut dyn Tracer) {
150    // SAFETY: caller guarantees `payload` points at an initialized EnumPayload.
151    let p = unsafe { &*(payload as *const EnumPayload) };
152    for item in p.items.iter() {
153        tracer.trace(*item);
154    }
155}
156
157unsafe fn enum_drop(payload: *mut u8) {
158    // SAFETY: caller guarantees `payload` points at an initialized EnumPayload.
159    // `drop_in_place` frees the items Vec; the schema is static and not owned.
160    unsafe { std::ptr::drop_in_place(payload as *mut EnumPayload) };
161}
162
163unsafe fn enum_format(payload: *const u8, out: &mut FormatSink<'_>) {
164    // SAFETY: caller guarantees `payload` points at an initialized EnumPayload.
165    let p = unsafe { &*(payload as *const EnumPayload) };
166    // The variant name comes from the schema, so `Some(3)` renders as `Some(3)`
167    // rather than the `<variant 0: 3>` an enum without nominal identity could
168    // only manage. A null schema is a producer bug rather than a user-reachable
169    // state; render the tag rather than dereferencing null.
170    if p.schema.is_null() {
171        let _ = write!(out, "<variant {}>", p.tag);
172        return;
173    }
174    // SAFETY: checked non-null above; every producer supplies a `'static` schema.
175    let schema = unsafe { &*p.schema };
176    let tag = p.tag as usize;
177    match schema.variant_at(tag) {
178        Some(variant) => {
179            let _ = out.write_str(variant.name);
180            if p.items.is_empty() {
181                return;
182            }
183            let _ = out.write_str("(");
184            for (i, item) in p.items.iter().enumerate() {
185                if i > 0 {
186                    let _ = out.write_str(", ");
187                }
188                let desc = schema.descriptor_at(tag, i, *item);
189                // SAFETY: the descriptor came from the schema for this slot, so the slot's
190                // payload is the type its `format` expects.
191                unsafe { (desc.format)(item.payload::<u8>() as *const u8, out) };
192            }
193            let _ = out.write_str(")");
194        }
195        None => {
196            let _ = write!(out, "<variant {}>", p.tag);
197        }
198    }
199}
200
201unsafe fn enum_equals(a: *const u8, b: *const u8) -> bool {
202    // SAFETY: caller guarantees both pointers point at initialized EnumPayloads.
203    let pa = unsafe { &*(a as *const EnumPayload) };
204    let pb = unsafe { &*(b as *const EnumPayload) };
205    // Equality is same-type + same-variant + payload-wise equality (§5.5).
206    // "Same type" is the schema's identity and variant shape, not its *address*:
207    // the tag alone is not an enum value's identity, or a `Colour::Red` and a
208    // `Light::Red` would be equal.
209    if pa.schema.is_null() || pb.schema.is_null() {
210        return false;
211    }
212    if !unsafe { (*pa.schema).same_type(&*pb.schema) } {
213        return false;
214    }
215    if pa.tag != pb.tag {
216        return false;
217    }
218    if pa.items.len() != pb.items.len() {
219        return false;
220    }
221    let schema = unsafe { &*pa.schema };
222    let tag = pa.tag as usize;
223    for (i, (x, y)) in pa.items.iter().zip(pb.items.iter()).enumerate() {
224        let desc = schema.descriptor_at(tag, i, *x);
225        // For a slot the producer had no type for, `desc` is `x`'s own — so `y`
226        // must carry the same one before its payload is read through it. Two
227        // values of different types are unequal; reading one as the other is a
228        // wrong-payload read, which is what `Some(1) == Some("1")` would be
229        // under a single `Option` schema.
230        if !std::ptr::eq(desc, schema.descriptor_at(tag, i, *y)) {
231            return false;
232        }
233        // If a payload type is not equatable, the enum is not equatable (§5.5).
234        let Some(eq) = desc.equals else {
235            return false;
236        };
237        let xe = x.payload::<u8>() as *const u8;
238        let ye = y.payload::<u8>() as *const u8;
239        // SAFETY: both slots were just checked to carry the same descriptor, and it
240        // is the one whose `equals` this is.
241        if !unsafe { eq(xe, ye) } {
242            return false;
243        }
244    }
245    true
246}
247
248unsafe fn enum_hash(payload: *const u8, hasher: &mut dyn DynamicHasher) {
249    // SAFETY: caller guarantees `payload` points at an initialized EnumPayload.
250    let p = unsafe { &*(payload as *const EnumPayload) };
251    // Everything `same_type` compares is hashed, so `Eq` and `Hash` agree:
252    // equality is type identity rather than the tag alone, and two enums that
253    // differ only in which type they are must be free to land in different
254    // buckets.
255    if !p.schema.is_null() {
256        // SAFETY: checked non-null; every producer supplies a `'static` schema.
257        let schema = unsafe { &*p.schema };
258        match schema.identity {
259            SchemaIdentity::Anonymous => hasher.write_bytes(b"anon"),
260            SchemaIdentity::Nominal(name) => {
261                hasher.write_bytes(b"nom");
262                hasher.write_bytes(name.as_bytes());
263            }
264        }
265        if let Some(variant) = schema.variant_at(p.tag as usize) {
266            hasher.write_bytes(variant.name.as_bytes());
267        }
268    }
269    // The tag — two values of different variants must hash distinctly even
270    // before the payload is considered.
271    hasher.write_bytes(&(p.tag as u64).to_le_bytes());
272    // Arity, to distinguish payload prefixes.
273    hasher.write_bytes(&(p.items.len() as u64).to_le_bytes());
274    for (i, item) in p.items.iter().enumerate() {
275        // The slot's type is part of the shape `eq` compares, so it is part of
276        // the hash too. Reading it off the *value* for an unknown slot is what
277        // keeps hash and eq agreeing there: both ask the object.
278        let desc = if p.schema.is_null() {
279            item.descriptor()
280        } else {
281            // SAFETY: checked non-null above.
282            unsafe { &*p.schema }.descriptor_at(p.tag as usize, i, *item)
283        };
284        hasher.write_bytes(&desc.id().to_u32().to_le_bytes());
285        // If a payload type is not hashable, the enum is not hashable (§5.5).
286        let Some(hash_item) = desc.hash else {
287            return;
288        };
289        let elem_payload = item.payload::<u8>() as *const u8;
290        // SAFETY: the descriptor came from the schema for this slot, so the slot's
291        // payload is the type its `hash` expects.
292        unsafe { hash_item(elem_payload, hasher) };
293    }
294}
295
296unsafe fn enum_compare(a: *const u8, b: *const u8) -> std::cmp::Ordering {
297    use std::cmp::Ordering;
298    // SAFETY: caller guarantees both pointers point at initialized EnumPayloads.
299    let pa = unsafe { &*(a as *const EnumPayload) };
300    let pb = unsafe { &*(b as *const EnumPayload) };
301    // A null schema is a producer bug rather than a user-reachable state, but it
302    // still needs a deterministic answer rather than a hash-order one (ADR-138).
303    match (pa.schema.is_null(), pb.schema.is_null()) {
304        (true, true) => return pa.tag.cmp(&pb.tag),
305        (true, false) => return Ordering::Less,
306        (false, true) => return Ordering::Greater,
307        (false, false) => {}
308    }
309    // SAFETY: both checked non-null above.
310    let (schema_a, schema_b) = unsafe { (&*pa.schema, &*pb.schema) };
311    match schema_a
312        .identity
313        .order_key()
314        .cmp(&schema_b.identity.order_key())
315    {
316        Ordering::Equal => {}
317        other => return other,
318    }
319    // The tag is the variant's **declaration** order — the order the type was
320    // written in, which is the order `enum_format` names the variants in and
321    // the order a `match`'s arms are read in. Sorting the variant *names*
322    // instead would impose an alphabet the declaration never mentioned, so a
323    // reader of the enum could not predict the order without sorting it
324    // themselves.
325    match pa.tag.cmp(&pb.tag) {
326        Ordering::Equal => {}
327        other => return other,
328    }
329    match pa.items.len().cmp(&pb.items.len()) {
330        Ordering::Equal => {}
331        other => return other,
332    }
333    let tag = pa.tag as usize;
334    // Payload slot-wise, through each side's own schema, falling back to the
335    // value's own descriptor for an unknown slot — the null-slot rule
336    // `descriptor_at` states, and the one `option_schema`'s unknown `Some` slot
337    // depends on.
338    for (i, (x, y)) in pa.items.iter().zip(pb.items.iter()).enumerate() {
339        let dx = schema_a.descriptor_at(tag, i, *x);
340        let dy = schema_b.descriptor_at(pb.tag as usize, i, *y);
341        // SAFETY: each payload matches the descriptor its schema slot names, or
342        // its own header's when that slot is null.
343        match unsafe { crate::ordering::slot_cmp(*x, *y, dx, dy) } {
344            Ordering::Equal => {}
345            other => return other,
346        }
347    }
348    Ordering::Equal
349}
350
351/// Descriptor for the `Enum` value type (§4.6). Structural equality and
352/// hashing (§5.5): two enum values are equal iff they are the same enum *type*,
353/// carry the same variant tag and have equal payloads; hashing mixes the type
354/// identity, the variant, then each payload. An enum is equatable/hashable iff
355/// every payload type is; functions never are. This lets enums serve as
356/// map/set keys — and the container ordering (ADR-138) walks the same three
357/// levels, in declaration order at the tag.
358pub static ENUM: TypeDescriptor = TypeDescriptor::builtin::<EnumPayload>(
359    BuiltinTypeId::Enum,
360    "Enum",
361    enum_trace,
362    enum_drop,
363    enum_format,
364    Some(enum_equals),
365    Some(enum_hash),
366    // An enum can be a key, so a container orders one (ADR-138). `a < b` on two
367    // enum values is still Y006: that is `capability::supports_ord`'s question.
368    Some(enum_compare),
369)
370.with_owned_bytes(enum_owned_bytes);
371
372/// The heap bytes an enum value owns beyond its payload, for GC pacing.
373/// `capacity`, not `len`: the buffer's real footprint is what the collector is
374/// paced against.
375///
376/// # Safety
377/// `payload` must point at an initialized `EnumPayload`.
378unsafe fn enum_owned_bytes(payload: *const u8) -> usize {
379    // SAFETY: caller guarantees `payload` points at an initialized EnumPayload.
380    let p = unsafe { &*(payload as *const EnumPayload) };
381    p.items.capacity() * std::mem::size_of::<GcRef>()
382}
383
384/// `Option`'s variant discriminants, in the order `TypeDb::new` declares them —
385/// `Some` first, `None` second. The codegen uses the same order for a `Some(x)`
386/// the program writes, so a runtime-built `Option` matches against the same
387/// arms.
388pub const OPTION_SOME_TAG: i64 = 0;
389/// See [`OPTION_SOME_TAG`].
390pub const OPTION_NONE_TAG: i64 = 1;
391
392/// The runtime's own `'static` schema for the prelude `Option` (F12).
393///
394/// `Map.get`, `Grid.find` and the graph walks answer `Option[V]` without ever
395/// learning `V` statically — they only have the value they found — so `Some`'s
396/// payload slot is **unknown** here and the value's own descriptor answers for
397/// it. The codegen's schema for the same `Option[Int]` names `INT` in that
398/// slot; [`EnumSchema::same_type`]'s null tolerance is what makes the two one
399/// type, which is what lets `match m.get(k) { Some(v) => …, None => … }` bind a
400/// runtime-built value against user-written arms.
401///
402/// A plain `static` will not do: `*const TypeDescriptor` is neither `Send` nor
403/// `Sync`. This is the `OnceLock<SyncPtr>` + `Box::leak` idiom
404/// `tuples::point_schema` already uses, for the same reason.
405#[must_use]
406pub fn option_schema() -> &'static EnumSchema {
407    use std::sync::OnceLock;
408    struct SyncPtr(&'static EnumSchema);
409    // SAFETY: the leaked schema and everything it points at are immutable and
410    // outlive every thread (mirroring `tuples::point_schema`).
411    unsafe impl Send for SyncPtr {}
412    unsafe impl Sync for SyncPtr {}
413    static OPTION: OnceLock<SyncPtr> = OnceLock::new();
414    OPTION
415        .get_or_init(|| {
416            let some_payload: &'static [*const TypeDescriptor] =
417                Box::leak(vec![std::ptr::null(); 1].into_boxed_slice());
418            let variants: &'static [EnumVariantShape] = Box::leak(
419                vec![
420                    EnumVariantShape {
421                        name: "Some",
422                        payload: some_payload,
423                    },
424                    EnumVariantShape {
425                        name: "None",
426                        payload: &[],
427                    },
428                ]
429                .into_boxed_slice(),
430            );
431            SyncPtr(Box::leak(Box::new(EnumSchema {
432                identity: SchemaIdentity::Nominal("Option"),
433                variants,
434            })))
435        })
436        .0
437}
438
439#[cfg(test)]
440mod tests {
441    use super::*;
442
443    #[test]
444    fn enum_descriptor_reports_capabilities() {
445        assert!(ENUM.is_equatable());
446        assert!(ENUM.is_hashable());
447        assert_eq!(ENUM.name, "Enum");
448        assert_eq!(ENUM.as_builtin(), Some(BuiltinTypeId::Enum));
449    }
450}
451
452#[cfg(test)]
453mod alloc_tests {
454    use super::*;
455    use crate::abi::{praxis_alloc_enum, praxis_enum_set_payload};
456
457    /// A leaked schema of payload-less variants, standing in for one a JIT
458    /// generation or the parser registry would build. Each call leaks its own,
459    /// which is the point wherever two are compared: same shape, different
460    /// address.
461    fn leak_schema(identity: SchemaIdentity, names: &[&'static str]) -> &'static EnumSchema {
462        let variants: Vec<EnumVariantShape> = names
463            .iter()
464            .map(|name| EnumVariantShape { name, payload: &[] })
465            .collect();
466        Box::leak(Box::new(EnumSchema {
467            identity,
468            variants: Box::leak(variants.into_boxed_slice()),
469        }))
470    }
471
472    fn equal(a: GcRef, b: GcRef) -> bool {
473        unsafe {
474            enum_equals(
475                a.payload::<u8>() as *const u8,
476                b.payload::<u8>() as *const u8,
477            )
478        }
479    }
480
481    fn hash_of(e: GcRef) -> u64 {
482        let mut h = crate::descriptor::StructHasher::new();
483        unsafe { enum_hash(e.payload::<u8>() as *const u8, &mut h) };
484        h.finish()
485    }
486
487    fn rendered(e: GcRef) -> String {
488        let mut s = String::new();
489        unsafe {
490            enum_format(
491                e.payload::<u8>() as *const u8,
492                &mut crate::FormatSink::display(&mut s),
493            )
494        };
495        s
496    }
497
498    #[test]
499    fn alloc_enum_round_trips_the_tag_and_the_schema() {
500        let mut rt = crate::Runtime::new();
501        let mut ctx = rt.context();
502        let schema = leak_schema(SchemaIdentity::Nominal("Colour"), &["Red", "Green"]);
503        for tag in 0..2_i64 {
504            let eref = unsafe { praxis_alloc_enum(&mut ctx, schema, tag) };
505            let payload = eref.payload::<u8>() as *const EnumPayload;
506            assert_eq!(unsafe { (*payload).tag }, tag as u32);
507            assert_eq!(unsafe { (*payload).schema }, schema as *const EnumSchema);
508            assert_eq!(unsafe { (*payload).items.len() }, 0);
509        }
510    }
511
512    /// The arity is read from the schema rather than passed alongside it, so a
513    /// tag with no variant has no arity to guess at. `praxis_alloc_tuple`
514    /// answers the Unit sentinel for a null schema for the same reason.
515    #[test]
516    fn an_out_of_range_tag_answers_the_unit_sentinel() {
517        let mut rt = crate::Runtime::new();
518        let mut ctx = rt.context();
519        let schema = leak_schema(SchemaIdentity::Nominal("Colour"), &["Red", "Green"]);
520        let past_the_end = unsafe { praxis_alloc_enum(&mut ctx, schema, 2) };
521        assert_eq!(
522            past_the_end.descriptor().id(),
523            crate::scalars::UNIT.id(),
524            "a tag the schema has no variant for cannot allocate an enum value"
525        );
526        let null = unsafe { praxis_alloc_enum(&mut ctx, std::ptr::null(), 0) };
527        assert_eq!(null.descriptor().id(), crate::scalars::UNIT.id());
528    }
529
530    /// Two enum types of one shape are two types: an enum value's identity is
531    /// its schema's, not its tag's, so `Colour::Red` and `Light::Red` are
532    /// neither equal nor alike in hash.
533    #[test]
534    fn two_enum_types_of_one_shape_are_not_one_type() {
535        let mut rt = crate::Runtime::new();
536        let mut ctx = rt.context();
537        let colour = leak_schema(SchemaIdentity::Nominal("Colour"), &["Red", "Green"]);
538        let light = leak_schema(SchemaIdentity::Nominal("Light"), &["Red", "Green"]);
539        let anon = leak_schema(SchemaIdentity::Anonymous, &["Red", "Green"]);
540
541        let red = unsafe { praxis_alloc_enum(&mut ctx, colour, 0) };
542        let stop = unsafe { praxis_alloc_enum(&mut ctx, light, 0) };
543        let bare = unsafe { praxis_alloc_enum(&mut ctx, anon, 0) };
544        assert!(!equal(red, stop), "two enum types are not one type");
545        assert!(
546            !equal(red, bare),
547            "a declared type is not a structural shape"
548        );
549
550        // And a different variant of the same type is still not equal.
551        let green = unsafe { praxis_alloc_enum(&mut ctx, colour, 1) };
552        assert!(!equal(red, green));
553    }
554
555    /// The other half: one enum type built through two separately allocated
556    /// schemas — what two JIT generations, or a generation and the parser
557    /// registry, produce — is one type.
558    #[test]
559    fn one_enum_type_built_by_two_schema_allocations_is_one_type() {
560        let mut rt = crate::Runtime::new();
561        let mut ctx = rt.context();
562        let first = leak_schema(SchemaIdentity::Nominal("Colour"), &["Red", "Green"]);
563        let second = leak_schema(SchemaIdentity::Nominal("Colour"), &["Red", "Green"]);
564        assert!(
565            !std::ptr::eq(first, second),
566            "the two schemas must really be distinct allocations"
567        );
568
569        let a = unsafe { praxis_alloc_enum(&mut ctx, first, 0) };
570        let b = unsafe { praxis_alloc_enum(&mut ctx, second, 0) };
571        assert!(equal(a, b));
572        assert_eq!(hash_of(a), hash_of(b), "equal enums must hash equally");
573    }
574
575    /// One nominal name over two variant lists is two types — the debugger
576    /// session that reloaded a *changed* definition, which must not compare a
577    /// stale value's payload through the new shape.
578    #[test]
579    fn one_enum_name_over_two_variant_lists_is_two_types() {
580        let mut rt = crate::Runtime::new();
581        let mut ctx = rt.context();
582        let two = leak_schema(SchemaIdentity::Nominal("C"), &["Red", "Green"]);
583        let renamed = leak_schema(SchemaIdentity::Nominal("C"), &["Red", "Blue"]);
584        // The whole variant list is the shape, so even the variant the two
585        // agree on (`Red`, tag 0) does not make them one type: a value built
586        // before the change must not be compared payload-wise through the
587        // descriptors of a definition it never had.
588        let a = unsafe { praxis_alloc_enum(&mut ctx, two, 0) };
589        let b = unsafe { praxis_alloc_enum(&mut ctx, renamed, 0) };
590        assert!(!equal(a, b));
591    }
592
593    /// The null-payload-slot rule, applied to enums. Under one `Option` schema
594    /// whose `Some` slot is unknown, `Some(1)` and `Some("1")` are unequal
595    /// rather than one being read through the other's descriptor.
596    #[test]
597    fn a_some_of_two_different_payload_types_is_not_equal() {
598        let mut rt = crate::Runtime::new();
599        let mut ctx = rt.context();
600        let schema = option_schema();
601        let build = |ctx: &mut crate::RuntimeContext, v: GcRef| {
602            let e = unsafe { praxis_alloc_enum(ctx, schema, OPTION_SOME_TAG) };
603            unsafe { praxis_enum_set_payload(ctx, e, 0, v) };
604            e
605        };
606        let one = rt.alloc_int(1);
607        let text = rt.alloc_text("1");
608        let boxed_int = build(&mut ctx, one);
609        let boxed_text = build(&mut ctx, text);
610        assert!(!equal(boxed_int, boxed_text));
611
612        // Equal payloads of one type are still equal.
613        let again = build(&mut ctx, rt.alloc_int(1));
614        assert!(equal(boxed_int, again));
615        assert_eq!(hash_of(boxed_int), hash_of(again));
616    }
617
618    /// A known payload slot and an unknown one agree rather than contradict, so
619    /// a codegen-built `Option[Int]` and a runtime-built `Some` are one type —
620    /// which is what makes `match m.get(k) { Some(v) => … }` work at all.
621    #[test]
622    fn a_known_payload_slot_and_an_unknown_one_describe_one_option_type() {
623        let mut rt = crate::Runtime::new();
624        let mut ctx = rt.context();
625        let typed: &'static EnumSchema = Box::leak(Box::new(EnumSchema {
626            identity: SchemaIdentity::Nominal("Option"),
627            variants: Box::leak(
628                vec![
629                    EnumVariantShape {
630                        name: "Some",
631                        payload: Box::leak(
632                            vec![&crate::scalars::INT as *const TypeDescriptor].into_boxed_slice(),
633                        ),
634                    },
635                    EnumVariantShape {
636                        name: "None",
637                        payload: &[],
638                    },
639                ]
640                .into_boxed_slice(),
641            ),
642        }));
643        assert!(typed.same_type(option_schema()));
644        assert!(option_schema().same_type(typed));
645
646        let from_codegen = unsafe { praxis_alloc_enum(&mut ctx, typed, OPTION_SOME_TAG) };
647        let seven = rt.alloc_int(7);
648        unsafe { praxis_enum_set_payload(&mut ctx, from_codegen, 0, seven) };
649        let from_runtime = unsafe { praxis_alloc_enum(&mut ctx, option_schema(), OPTION_SOME_TAG) };
650        let seven_again = rt.alloc_int(7);
651        unsafe { praxis_enum_set_payload(&mut ctx, from_runtime, 0, seven_again) };
652        assert!(equal(from_codegen, from_runtime));
653        assert_eq!(hash_of(from_codegen), hash_of(from_runtime));
654    }
655
656    /// An enum renders its variant name, which it reads from its schema.
657    #[test]
658    fn an_enum_renders_its_variant_name_and_payload() {
659        let mut rt = crate::Runtime::new();
660        let mut ctx = rt.context();
661        let some = unsafe { praxis_alloc_enum(&mut ctx, option_schema(), OPTION_SOME_TAG) };
662        let three = rt.alloc_int(3);
663        unsafe { praxis_enum_set_payload(&mut ctx, some, 0, three) };
664        assert_eq!(rendered(some), "Some(3)");
665
666        let none = unsafe { praxis_alloc_enum(&mut ctx, option_schema(), OPTION_NONE_TAG) };
667        assert_eq!(rendered(none), "None");
668    }
669
670    /// The container order is the **variant's declaration order**, then the
671    /// payload (ADR-138). `Option` declares `Some` first (see
672    /// [`OPTION_SOME_TAG`]), so `Some(…)` precedes `None` — an alphabetical
673    /// order over the variant names would answer the opposite, and would be an
674    /// order the declaration never mentioned. Inside one variant the payload
675    /// decides, through its own type's order, so `Some(2)` precedes `Some(10)`
676    /// rather than trailing it as the rendered form would have said.
677    #[test]
678    fn enum_compare_is_declaration_order_then_payload() {
679        let mut rt = crate::Runtime::new();
680        let ten = rt.alloc_int(10);
681        let two = rt.alloc_int(2);
682        let zero = rt.alloc_int(0);
683        let mut ctx = rt.context();
684        let some = |ctx: &mut crate::RuntimeContext, payload| {
685            // SAFETY: a live context, and `Some`'s one slot takes an `Int`.
686            unsafe {
687                let e = praxis_alloc_enum(ctx, option_schema(), OPTION_SOME_TAG);
688                praxis_enum_set_payload(ctx, e, 0, payload);
689                e
690            }
691        };
692        let cmp = |a: GcRef, b: GcRef| unsafe {
693            enum_compare(
694                a.payload::<u8>() as *const u8,
695                b.payload::<u8>() as *const u8,
696            )
697        };
698        let none = unsafe { praxis_alloc_enum(&mut ctx, option_schema(), OPTION_NONE_TAG) };
699        let some_zero = some(&mut ctx, zero);
700        let some_two = some(&mut ctx, two);
701        let some_ten = some(&mut ctx, ten);
702
703        assert_eq!(cmp(some_zero, none), std::cmp::Ordering::Less);
704        assert_eq!(cmp(none, some_zero), std::cmp::Ordering::Greater);
705        assert_eq!(cmp(some_two, some_ten), std::cmp::Ordering::Less);
706        assert_eq!(cmp(some_ten, some_ten), std::cmp::Ordering::Equal);
707    }
708}