Skip to main content

soil_rt/
descriptor.rs

1use std::collections::HashMap;
2
3use crate::error::{PanicKind, SoilError};
4use crate::value::Closure;
5
6/// A dense, per-runtime-instance index into the type registry (resolved
7/// decision, impl plan §1). Ids are not stable across runs; anything
8/// persistent identifies types by *name* (filename is identity, design
9/// §4.3).
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub struct TypeId(pub(crate) u32);
12
13impl TypeId {
14    pub fn index(self) -> u32 {
15        self.0
16    }
17
18    /// Rebuild an id from its index — the C ABI's spelling of a
19    /// `TypeId`. A forged index is caught by `Registry::get`, never UB.
20    pub fn from_index(index: u32) -> Self {
21        TypeId(index)
22    }
23}
24
25/// The type of a field, payload, or element, as the JSON layer and the
26/// derived operations need to see it. `Named` covers records, sums, and
27/// opaque types — including recursive references.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum TypeShape {
30    I64,
31    U64,
32    I32,
33    U32,
34    I16,
35    U16,
36    I8,
37    U8,
38    BigInt,
39    F64,
40    Utf8,
41    Bytes,
42    Unit,
43    List(Box<TypeShape>),
44    Map(Box<TypeShape>, Box<TypeShape>),
45    /// A function type. Legal in a shape, but poisons derivation:
46    /// deriving `eq` on a type containing an arrow is a type error
47    /// (design §3.7).
48    Closure,
49    Named(TypeId),
50}
51
52/// Per-type derivation strategy (design §3.7). `ignored` is per-field,
53/// not per-type: see [`FieldDesc::ignored`].
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum Strategy {
56    Structural,
57    Opaque,
58}
59
60/// The default of an `ignored` field — how the field is refilled
61/// wherever a value is materialized without it (design §3.7).
62///
63/// `Const` and `CopyField` are the serializable vocabulary of the
64/// descriptor JSON (fixtures are data, not code — impl plan §4 step 6);
65/// `Native` is an arbitrary thunk supplied by the embedder, which is
66/// what a Soil-level default expression eventually compiles to and is
67/// not serializable.
68#[derive(Debug)]
69pub enum IgnoredDefault {
70    /// A constant of the field's own (scalar) shape.
71    Const(crate::value::Value),
72    /// Copy another (non-ignored, same-shaped) field's value.
73    CopyField {
74        name: String,
75        /// Index into the thunk-argument list — the record's
76        /// non-ignored fields in declaration order (micro-pin §8.6).
77        /// Resolved at registration.
78        index: usize,
79    },
80    Native(Closure),
81}
82
83impl IgnoredDefault {
84    /// `args` are the record's non-ignored field values in declaration
85    /// order (micro-pin §8.6).
86    pub fn call(&self, args: &[crate::value::Value]) -> Result<crate::value::Value, SoilError> {
87        match self {
88            IgnoredDefault::Const(value) => Ok(value.clone()),
89            IgnoredDefault::CopyField { index, .. } => Ok(args[*index].clone()),
90            IgnoredDefault::Native(closure) => closure.call(args),
91        }
92    }
93}
94
95#[derive(Debug)]
96pub struct FieldDesc {
97    pub name: String,
98    pub shape: TypeShape,
99    /// `Some(default)` marks the field `ignored` (design §3.7): skipped
100    /// by `eq`/`compare`, omitted by `show`, refilled by this default on
101    /// decode.
102    pub ignored: Option<IgnoredDefault>,
103}
104
105#[derive(Debug)]
106pub struct VariantDesc {
107    pub name: String,
108    /// At most one payload (design §3.1); its fields are a record if
109    /// more is needed.
110    pub payload: Option<TypeShape>,
111}
112
113#[derive(Debug)]
114pub enum TypeBody {
115    Record(Vec<FieldDesc>),
116    Sum(Vec<VariantDesc>),
117    /// No visible structure at all — FFI handles, abstract types. Only
118    /// legal with [`Strategy::Opaque`].
119    Opaque,
120}
121
122#[derive(Debug)]
123pub struct TypeDesc {
124    /// The cased name (`"SummaryRow"`), the stable identity.
125    pub name: String,
126    pub strategy: Strategy,
127    pub body: TypeBody,
128}
129
130enum Slot {
131    /// Declared but not yet defined — the target of recursive
132    /// references during two-phase registration.
133    Declared {
134        name: String,
135    },
136    Defined {
137        desc: TypeDesc,
138    },
139}
140
141/// The per-runtime-instance table of type descriptors. Descriptors
142/// drive the derived operations and type-directed JSON decode;
143/// registration is used by the interpreter now and compiled code later
144/// (plan 01).
145///
146/// Two-phase registration (`declare`, then `define`) exists because
147/// recursive types across definitions are legal (design §3.8): declare
148/// every member of a cycle first, then define each against the declared
149/// ids. `register` is the one-shot spelling for the common case.
150pub struct Registry {
151    slots: Vec<Slot>,
152    by_name: HashMap<String, TypeId>,
153    bool_id: TypeId,
154}
155
156impl Default for Registry {
157    fn default() -> Self {
158        Self::new()
159    }
160}
161
162impl Registry {
163    pub fn new() -> Self {
164        let mut registry = Registry {
165            slots: Vec::new(),
166            by_name: HashMap::new(),
167            bool_id: TypeId(0),
168        };
169        // `Bool` is the prelude sum `True | False` (plan 01): not a
170        // `Value` variant, but pre-registered so the JSON layer can
171        // special-case its encoding to JSON booleans (tr-grammar §7).
172        registry.bool_id = registry
173            .register(TypeDesc {
174                name: "Bool".to_string(),
175                strategy: Strategy::Structural,
176                body: TypeBody::Sum(vec![
177                    VariantDesc {
178                        name: "True".to_string(),
179                        payload: None,
180                    },
181                    VariantDesc {
182                        name: "False".to_string(),
183                        payload: None,
184                    },
185                ]),
186            })
187            .expect("Bool registration cannot fail on an empty registry");
188        registry
189    }
190
191    pub fn bool_id(&self) -> TypeId {
192        self.bool_id
193    }
194
195    pub fn declare(&mut self, name: &str) -> Result<TypeId, SoilError> {
196        if self.by_name.contains_key(name) {
197            return Err(SoilError::new(
198                PanicKind::CapiMisuse,
199                format!("type name already registered: {name}"),
200            ));
201        }
202        let id = TypeId(
203            u32::try_from(self.slots.len())
204                .map_err(|_| SoilError::new(PanicKind::CapiMisuse, "type registry full"))?,
205        );
206        self.slots.push(Slot::Declared {
207            name: name.to_string(),
208        });
209        self.by_name.insert(name.to_string(), id);
210        Ok(id)
211    }
212
213    pub fn define(&mut self, id: TypeId, desc: TypeDesc) -> Result<(), SoilError> {
214        let slot = self.slots.get(id.0 as usize).ok_or_else(|| {
215            SoilError::new(PanicKind::CapiMisuse, format!("undefined type id {}", id.0))
216        })?;
217        match slot {
218            Slot::Defined { .. } => {
219                return Err(SoilError::new(
220                    PanicKind::CapiMisuse,
221                    format!("type id {} is already defined", id.0),
222                ));
223            }
224            Slot::Declared { name } => {
225                if *name != desc.name {
226                    return Err(SoilError::new(
227                        PanicKind::CapiMisuse,
228                        format!(
229                            "type id {} was declared as {name} but defined as {}",
230                            id.0, desc.name
231                        ),
232                    ));
233                }
234            }
235        }
236        self.validate(&desc)?;
237        self.slots[id.0 as usize] = Slot::Defined { desc };
238        Ok(())
239    }
240
241    /// `declare` + `define` for the non-recursive common case. Unlike a
242    /// bare `declare`, a failed `register` leaves no trace: the declared
243    /// name is rolled back so the caller can retry with a fixed
244    /// descriptor.
245    pub fn register(&mut self, desc: TypeDesc) -> Result<TypeId, SoilError> {
246        let name = desc.name.clone();
247        let id = self.declare(&name)?;
248        match self.define(id, desc) {
249            Ok(()) => Ok(id),
250            Err(err) => {
251                self.slots.pop();
252                self.by_name.remove(&name);
253                Err(err)
254            }
255        }
256    }
257
258    pub fn get(&self, id: TypeId) -> Result<&TypeDesc, SoilError> {
259        match self.slots.get(id.0 as usize) {
260            Some(Slot::Defined { desc }) => Ok(desc),
261            Some(Slot::Declared { name }) => Err(SoilError::new(
262                PanicKind::CapiMisuse,
263                format!("type {name} is declared but not defined"),
264            )),
265            None => Err(SoilError::new(
266                PanicKind::CapiMisuse,
267                format!("undefined type id {}", id.0),
268            )),
269        }
270    }
271
272    pub fn lookup(&self, name: &str) -> Option<TypeId> {
273        self.by_name.get(name).copied()
274    }
275
276    /// Whether the derived operations exist for this type: false iff its
277    /// shape transitively contains a function type ("deriving `eq` on a
278    /// type containing an arrow is a type error", design §3.7).
279    ///
280    /// The impl plan places this check at `define` time; it is computed
281    /// on demand instead because a member of a type cycle can be defined
282    /// while its cycle-mates are still only declared, so the transitive
283    /// walk is not possible until the whole cycle is in. The walk is
284    /// cycle-safe and cheap at registry scale.
285    pub fn derivable(&self, id: TypeId) -> Result<bool, SoilError> {
286        let mut visited = vec![false; self.slots.len()];
287        self.derivable_walk(id, &mut visited)
288    }
289
290    fn derivable_walk(&self, id: TypeId, visited: &mut [bool]) -> Result<bool, SoilError> {
291        if visited[id.0 as usize] {
292            // A cycle back to a type already on the walk adds nothing
293            // new; closures on the cycle are found by the other paths.
294            return Ok(true);
295        }
296        visited[id.0 as usize] = true;
297        let desc = self.get(id)?;
298        match &desc.body {
299            // Opaque types derive by identity regardless of payload.
300            TypeBody::Opaque => Ok(true),
301            TypeBody::Record(fields) => {
302                for field in fields {
303                    if !self.shape_derivable(&field.shape, visited)? {
304                        return Ok(false);
305                    }
306                }
307                Ok(true)
308            }
309            TypeBody::Sum(variants) => {
310                for variant in variants {
311                    if let Some(shape) = &variant.payload {
312                        if !self.shape_derivable(shape, visited)? {
313                            return Ok(false);
314                        }
315                    }
316                }
317                Ok(true)
318            }
319        }
320    }
321
322    fn shape_derivable(&self, shape: &TypeShape, visited: &mut [bool]) -> Result<bool, SoilError> {
323        match shape {
324            TypeShape::Closure => Ok(false),
325            TypeShape::List(elem) => self.shape_derivable(elem, visited),
326            TypeShape::Map(key, value) => {
327                Ok(self.shape_derivable(key, visited)? && self.shape_derivable(value, visited)?)
328            }
329            TypeShape::Named(id) => self.derivable_walk(*id, visited),
330            _ => Ok(true),
331        }
332    }
333
334    fn validate(&self, desc: &TypeDesc) -> Result<(), SoilError> {
335        if matches!(desc.body, TypeBody::Opaque) && desc.strategy != Strategy::Opaque {
336            return Err(SoilError::new(
337                PanicKind::CapiMisuse,
338                format!(
339                    "opaque-bodied type {} must use the opaque strategy",
340                    desc.name
341                ),
342            ));
343        }
344        match &desc.body {
345            TypeBody::Record(fields) => {
346                let mut seen = std::collections::HashSet::new();
347                for field in fields {
348                    if !seen.insert(field.name.as_str()) {
349                        return Err(SoilError::new(
350                            PanicKind::CapiMisuse,
351                            format!("duplicate field {} in type {}", field.name, desc.name),
352                        ));
353                    }
354                    self.validate_shape(&field.shape, &desc.name)?;
355                    if let Some(IgnoredDefault::CopyField { name, index }) = &field.ignored {
356                        let target = fields.iter().find(|f| f.name == *name).ok_or_else(|| {
357                            SoilError::new(
358                                PanicKind::CapiMisuse,
359                                format!(
360                                    "ignored field {} of {} copies unknown field {name}",
361                                    field.name, desc.name
362                                ),
363                            )
364                        })?;
365                        if target.ignored.is_some() {
366                            return Err(SoilError::new(
367                                PanicKind::CapiMisuse,
368                                format!(
369                                    "ignored field {} of {} may not copy another ignored field",
370                                    field.name, desc.name
371                                ),
372                            ));
373                        }
374                        if target.shape != field.shape {
375                            return Err(SoilError::new(
376                                PanicKind::CapiMisuse,
377                                format!(
378                                    "ignored field {} of {} copies a field of a different shape",
379                                    field.name, desc.name
380                                ),
381                            ));
382                        }
383                        let expected = fields
384                            .iter()
385                            .filter(|f| f.ignored.is_none())
386                            .position(|f| f.name == *name)
387                            .expect("target is non-ignored");
388                        if *index != expected {
389                            return Err(SoilError::new(
390                                PanicKind::CapiMisuse,
391                                format!(
392                                    "ignored field {} of {}: CopyField index {index} does not \
393                                     match target position {expected}",
394                                    field.name, desc.name
395                                ),
396                            ));
397                        }
398                    }
399                }
400            }
401            TypeBody::Sum(variants) => {
402                if variants.is_empty() {
403                    return Err(SoilError::new(
404                        PanicKind::CapiMisuse,
405                        format!("sum type {} has no variants", desc.name),
406                    ));
407                }
408                let mut seen = std::collections::HashSet::new();
409                for variant in variants {
410                    if !seen.insert(variant.name.as_str()) {
411                        return Err(SoilError::new(
412                            PanicKind::CapiMisuse,
413                            format!("duplicate variant {} in type {}", variant.name, desc.name),
414                        ));
415                    }
416                    if let Some(shape) = &variant.payload {
417                        self.validate_shape(shape, &desc.name)?;
418                    }
419                }
420            }
421            TypeBody::Opaque => {}
422        }
423        Ok(())
424    }
425
426    fn validate_shape(&self, shape: &TypeShape, context: &str) -> Result<(), SoilError> {
427        match shape {
428            TypeShape::List(elem) => self.validate_shape(elem, context),
429            TypeShape::Map(key, value) => {
430                self.validate_shape(key, context)?;
431                self.validate_shape(value, context)
432            }
433            TypeShape::Named(id) => {
434                if self.slots.get(id.0 as usize).is_none() {
435                    return Err(SoilError::new(
436                        PanicKind::CapiMisuse,
437                        format!("type {context} references undeclared type id {}", id.0),
438                    ));
439                }
440                Ok(())
441            }
442            _ => Ok(()),
443        }
444    }
445}
446
447#[cfg(test)]
448mod tests {
449    use super::*;
450
451    fn record(name: &str, fields: Vec<FieldDesc>) -> TypeDesc {
452        TypeDesc {
453            name: name.to_string(),
454            strategy: Strategy::Structural,
455            body: TypeBody::Record(fields),
456        }
457    }
458
459    fn field(name: &str, shape: TypeShape) -> FieldDesc {
460        FieldDesc {
461            name: name.to_string(),
462            shape,
463            ignored: None,
464        }
465    }
466
467    #[test]
468    fn bool_is_preregistered() {
469        let registry = Registry::new();
470        let id = registry.lookup("Bool").unwrap();
471        assert_eq!(id, registry.bool_id());
472        let desc = registry.get(id).unwrap();
473        match &desc.body {
474            TypeBody::Sum(variants) => {
475                let names: Vec<&str> = variants.iter().map(|v| v.name.as_str()).collect();
476                assert_eq!(names, vec!["True", "False"]);
477            }
478            other => panic!("Bool should be a sum, got {other:?}"),
479        }
480    }
481
482    #[test]
483    fn register_and_lookup_round_trip() {
484        let mut registry = Registry::new();
485        let id = registry
486            .register(record(
487                "SummaryRow",
488                vec![
489                    field("count", TypeShape::U64),
490                    field("mean", TypeShape::F64),
491                ],
492            ))
493            .unwrap();
494        assert_eq!(registry.lookup("SummaryRow"), Some(id));
495        assert_eq!(registry.get(id).unwrap().name, "SummaryRow");
496        assert!(registry.derivable(id).unwrap());
497    }
498
499    #[test]
500    fn duplicate_names_rejected() {
501        let mut registry = Registry::new();
502        registry.register(record("A", vec![])).unwrap();
503        assert!(registry.register(record("A", vec![])).is_err());
504        assert!(registry.declare("Bool").is_err());
505    }
506
507    #[test]
508    fn recursive_type_via_declare_define() {
509        let mut registry = Registry::new();
510        let tree = registry.declare("Tree").unwrap();
511        registry
512            .define(
513                tree,
514                TypeDesc {
515                    name: "Tree".to_string(),
516                    strategy: Strategy::Structural,
517                    body: TypeBody::Sum(vec![
518                        VariantDesc {
519                            name: "Leaf".to_string(),
520                            payload: Some(TypeShape::I64),
521                        },
522                        VariantDesc {
523                            name: "Node".to_string(),
524                            payload: Some(TypeShape::List(Box::new(TypeShape::Named(tree)))),
525                        },
526                    ]),
527                },
528            )
529            .unwrap();
530        assert!(registry.derivable(tree).unwrap());
531        // Reading a declared-but-undefined type is an error, not a panic.
532        let pending = registry.declare("Pending").unwrap();
533        assert!(registry.get(pending).is_err());
534    }
535
536    #[test]
537    fn define_checks_name_and_double_definition() {
538        let mut registry = Registry::new();
539        let id = registry.declare("A").unwrap();
540        assert!(registry.define(id, record("B", vec![])).is_err());
541        registry.define(id, record("A", vec![])).unwrap();
542        assert!(registry.define(id, record("A", vec![])).is_err());
543    }
544
545    #[test]
546    fn closure_poisons_derivability_transitively() {
547        let mut registry = Registry::new();
548        let inner = registry
549            .register(record("Inner", vec![field("callback", TypeShape::Closure)]))
550            .unwrap();
551        let outer = registry
552            .register(record(
553                "Outer",
554                vec![field(
555                    "items",
556                    TypeShape::List(Box::new(TypeShape::Named(inner))),
557                )],
558            ))
559            .unwrap();
560        assert!(!registry.derivable(inner).unwrap());
561        assert!(!registry.derivable(outer).unwrap());
562    }
563
564    #[test]
565    fn opaque_body_requires_opaque_strategy_and_derives() {
566        let mut registry = Registry::new();
567        assert!(registry
568            .register(TypeDesc {
569                name: "Handle".to_string(),
570                strategy: Strategy::Structural,
571                body: TypeBody::Opaque,
572            })
573            .is_err());
574        let id = registry
575            .register(TypeDesc {
576                name: "Handle".to_string(),
577                strategy: Strategy::Opaque,
578                body: TypeBody::Opaque,
579            })
580            .unwrap();
581        assert!(registry.derivable(id).unwrap());
582    }
583
584    #[test]
585    fn sum_must_have_variants_and_unique_names() {
586        let mut registry = Registry::new();
587        assert!(registry
588            .register(TypeDesc {
589                name: "Empty".to_string(),
590                strategy: Strategy::Structural,
591                body: TypeBody::Sum(vec![]),
592            })
593            .is_err());
594        assert!(registry
595            .register(TypeDesc {
596                name: "Dup".to_string(),
597                strategy: Strategy::Structural,
598                body: TypeBody::Sum(vec![
599                    VariantDesc {
600                        name: "X".to_string(),
601                        payload: None
602                    },
603                    VariantDesc {
604                        name: "X".to_string(),
605                        payload: None
606                    },
607                ]),
608            })
609            .is_err());
610    }
611}