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}