Skip to main content

sim_kernel/
card.rs

1//! Card records: the contract for structured metadata about runtime subjects.
2//!
3//! The kernel defines the [`Card`] record and its well-known predicate keys
4//! (subject, kind, help, args, result, tests, ops); libraries publish and
5//! consume cards as claims.
6
7use std::{collections::BTreeMap, sync::Arc};
8
9use crate::{
10    claim::{Claim, ClaimPattern},
11    datum_store::DatumStore,
12    env::Cx,
13    error::Result,
14    expr::Expr,
15    force_list_to_vec,
16    handle_store::HandleStore,
17    id::{CORE_CARD_CLASS_ID, Symbol},
18    object::{ClassRef, Object},
19    ref_id::{ContentId, Coordinate, HandleId, Ref},
20    ref_resolver::{RefResolver, TemporaryRefResolver, value_from_datum},
21    value::Value,
22};
23
24// sim-non-citizen(reason = "browse/help Card projection", kind = "marker", descriptor = "")
25/// Structured, machine-readable record describing a runtime subject.
26///
27/// A `Card` is an ordinary runtime [`Object`]: a `subject` [`Ref`] plus ordered
28/// `(Symbol, Value)` entries projected from claims and fallback table data. The
29/// kernel fixes the Card schema and well-known predicate keys (see the README
30/// section "Contract: Card records and the browse/help/test schema"); libraries
31/// implement browse over these records. Browse output is ordinary runtime data;
32/// consumers must not parse display strings.
33#[derive(Clone)]
34pub struct Card {
35    subject: Ref,
36    entries: Vec<(Symbol, Value)>,
37}
38
39impl Card {
40    /// Builds a card for `subject` from already-projected `entries`.
41    pub fn new(subject: Ref, entries: Vec<(Symbol, Value)>) -> Self {
42        Self { subject, entries }
43    }
44
45    /// Returns the subject this card describes.
46    pub fn subject(&self) -> &Ref {
47        &self.subject
48    }
49
50    /// Returns the card's ordered `(predicate, value)` entries.
51    pub fn entries(&self) -> &[(Symbol, Value)] {
52        &self.entries
53    }
54}
55
56impl Object for Card {
57    fn display(&self, _cx: &mut Cx) -> Result<String> {
58        Ok(format!("#<card {:?}>", self.subject))
59    }
60
61    fn as_any(&self) -> &dyn std::any::Any {
62        self
63    }
64}
65
66impl crate::ObjectCompat for Card {
67    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
68        let symbol = Symbol::qualified("core", "Card");
69        if let Some(value) = cx.registry().class_by_symbol(&symbol) {
70            return Ok(value.clone());
71        }
72        cx.factory().class_stub(CORE_CARD_CLASS_ID, symbol)
73    }
74    fn as_table(&self, cx: &mut Cx) -> Result<Value> {
75        cx.factory().table(self.entries.clone())
76    }
77    fn as_expr(&self, cx: &mut Cx) -> Result<Expr> {
78        self.as_table(cx)?.object().as_expr(cx)
79    }
80}
81
82/// Returns the `subject` Card predicate symbol.
83pub fn card_subject_predicate() -> Symbol {
84    core_symbol("subject")
85}
86
87/// Returns the `kind` Card predicate symbol.
88pub fn card_kind_predicate() -> Symbol {
89    core_symbol("kind")
90}
91
92/// Returns the `help` Card predicate symbol.
93pub fn card_help_predicate() -> Symbol {
94    core_symbol("help")
95}
96
97/// Returns the `args` Card predicate symbol.
98pub fn card_args_predicate() -> Symbol {
99    core_symbol("args")
100}
101
102/// Returns the `result` Card predicate symbol.
103pub fn card_result_predicate() -> Symbol {
104    core_symbol("result")
105}
106
107/// Returns the `tests` Card predicate symbol.
108pub fn card_tests_predicate() -> Symbol {
109    core_symbol("tests")
110}
111
112/// Returns the `ops` Card predicate symbol.
113pub fn card_ops_predicate() -> Symbol {
114    core_symbol("ops")
115}
116
117/// Returns the `requires` Card predicate symbol.
118pub fn card_requires_predicate() -> Symbol {
119    core_symbol("requires")
120}
121
122/// Returns the `see-also` Card predicate symbol.
123pub fn card_see_also_predicate() -> Symbol {
124    core_symbol("see-also")
125}
126
127/// Returns the `shape-known` Card predicate symbol.
128pub fn card_shape_known_predicate() -> Symbol {
129    core_symbol("shape-known")
130}
131
132/// Returns the fixed Card predicate symbols in their stable schema order.
133pub fn card_fixed_predicates() -> Vec<Symbol> {
134    vec![
135        card_subject_predicate(),
136        card_kind_predicate(),
137        card_help_predicate(),
138        card_args_predicate(),
139        card_result_predicate(),
140        card_tests_predicate(),
141        card_ops_predicate(),
142        card_requires_predicate(),
143        card_see_also_predicate(),
144        card_shape_known_predicate(),
145    ]
146}
147
148/// Builds a card carrying only the fixed schema fields with default values.
149pub fn minimal_card(cx: &mut Cx, subject: Ref) -> Result<Value> {
150    let entries = minimal_entries(cx, &subject)?;
151    cx.factory().opaque(Arc::new(Card::new(subject, entries)))
152}
153
154/// Builds a card for `subject` by projecting its claims, with no fallback data.
155pub fn card_for_ref(cx: &mut Cx, subject: Ref) -> Result<Value> {
156    card_from_parts(cx, subject, None, None)
157}
158
159/// Builds a card for a runtime `value`, using the value itself as fallback data.
160pub fn card_for_value(cx: &mut Cx, value: Value) -> Result<Value> {
161    let mut resolver = TemporaryRefResolver::new();
162    let subject = resolver.ref_for_value(cx, &value)?;
163    card_from_parts(cx, subject, Some(value), None)
164}
165
166/// Builds a card for `subject` with optional `fallback` table data and a
167/// `default_kind` used when no `kind` claim or fallback is present.
168pub fn card_for_ref_with_fallback(
169    cx: &mut Cx,
170    subject: Ref,
171    fallback: Option<Value>,
172    default_kind: Option<Symbol>,
173) -> Result<Value> {
174    card_from_parts(cx, subject, fallback, default_kind)
175}
176
177fn card_from_parts(
178    cx: &mut Cx,
179    subject: Ref,
180    fallback: Option<Value>,
181    default_kind: Option<Symbol>,
182) -> Result<Value> {
183    let fallback = fallback_entries(cx, fallback.as_ref())?;
184    insert_compatibility_claims(cx, &subject, &fallback, default_kind.as_ref())?;
185    let entries = card_entries(cx, &subject, fallback, default_kind)?;
186    cx.factory().opaque(Arc::new(Card::new(subject, entries)))
187}
188
189fn card_entries(
190    cx: &mut Cx,
191    subject: &Ref,
192    fallback: BTreeMap<Symbol, Value>,
193    default_kind: Option<Symbol>,
194) -> Result<Vec<(Symbol, Value)>> {
195    let args = claim_scalar(cx, subject, card_args_predicate())?
196        .or_else(|| fallback.get(&field_symbol("args")).cloned());
197    let result = claim_scalar(cx, subject, card_result_predicate())?
198        .or_else(|| fallback.get(&field_symbol("result")).cloned());
199    let shape_known = claim_scalar(cx, subject, card_shape_known_predicate())?
200        .or_else(|| fallback.get(&field_symbol("shape-known")).cloned());
201
202    let kind = match claim_scalar(cx, subject, card_kind_predicate())?
203        .or_else(|| fallback.get(&field_symbol("kind")).cloned())
204    {
205        Some(value) => value,
206        None => match default_kind {
207            Some(kind) => cx.factory().symbol(kind)?,
208            None => core_symbol_value(cx, "unknown")?,
209        },
210    };
211    let help = claim_scalar(cx, subject, card_help_predicate())?
212        .or_else(|| fallback.get(&field_symbol("help")).cloned())
213        .or_else(|| fallback.get(&field_symbol("purpose")).cloned())
214        .unwrap_or(cx.factory().string(String::new())?);
215    let tests = claim_list(cx, subject, card_tests_predicate())?
216        .or_else(|| fallback.get(&field_symbol("tests")).cloned())
217        .unwrap_or(empty_list(cx)?);
218    let ops = claim_list(cx, subject, card_ops_predicate())?
219        .or_else(|| fallback.get(&field_symbol("ops")).cloned())
220        .unwrap_or(empty_list(cx)?);
221    let requires = claim_list(cx, subject, card_requires_predicate())?
222        .or_else(|| fallback.get(&field_symbol("requires")).cloned())
223        .unwrap_or(empty_list(cx)?);
224    let see_also = claim_list(cx, subject, card_see_also_predicate())?
225        .or_else(|| fallback.get(&field_symbol("see-also")).cloned())
226        .unwrap_or(empty_list(cx)?);
227
228    let mut entries = vec![
229        (field_symbol("subject"), ref_value(cx, subject)?),
230        (field_symbol("kind"), kind),
231        (field_symbol("help"), help),
232        (
233            field_symbol("args"),
234            args.clone().unwrap_or(core_symbol_value(cx, "Any")?),
235        ),
236        (
237            field_symbol("result"),
238            result.clone().unwrap_or(core_symbol_value(cx, "Any")?),
239        ),
240        (field_symbol("tests"), tests),
241        (field_symbol("ops"), ops),
242        (field_symbol("requires"), requires),
243        (field_symbol("see-also"), see_also),
244        (
245            field_symbol("shape-known"),
246            shape_known.unwrap_or(cx.factory().bool(args.is_some() || result.is_some())?),
247        ),
248    ];
249
250    if let Ref::Symbol(symbol) = subject
251        && !fallback.contains_key(&field_symbol("symbol"))
252    {
253        entries.push((
254            field_symbol("symbol"),
255            cx.factory().string(symbol.to_string())?,
256        ));
257    }
258
259    for (key, value) in fallback {
260        if !entries.iter().any(|(existing, _)| existing == &key) {
261            entries.push((key, value));
262        }
263    }
264    Ok(entries)
265}
266
267fn minimal_entries(cx: &mut Cx, subject: &Ref) -> Result<Vec<(Symbol, Value)>> {
268    Ok(vec![
269        (field_symbol("subject"), ref_value(cx, subject)?),
270        (field_symbol("kind"), core_symbol_value(cx, "unknown")?),
271        (field_symbol("help"), cx.factory().string(String::new())?),
272        (field_symbol("args"), core_symbol_value(cx, "Any")?),
273        (field_symbol("result"), core_symbol_value(cx, "Any")?),
274        (field_symbol("tests"), empty_list(cx)?),
275        (field_symbol("ops"), empty_list(cx)?),
276        (field_symbol("requires"), empty_list(cx)?),
277        (field_symbol("see-also"), empty_list(cx)?),
278        (field_symbol("shape-known"), cx.factory().bool(false)?),
279    ])
280}
281
282fn insert_compatibility_claims(
283    cx: &mut Cx,
284    subject: &Ref,
285    fallback: &BTreeMap<Symbol, Value>,
286    default_kind: Option<&Symbol>,
287) -> Result<()> {
288    let kind = fallback.get(&field_symbol("kind"));
289    if let Some(value) = kind {
290        insert_value_claim_if_missing(cx, subject, card_kind_predicate(), value)?;
291    } else if let Some(kind) = default_kind {
292        insert_ref_claim_if_missing(
293            cx,
294            subject,
295            card_kind_predicate(),
296            Ref::Symbol(kind.clone()),
297        )?;
298    }
299
300    if let Some(value) = fallback
301        .get(&field_symbol("help"))
302        .or_else(|| fallback.get(&field_symbol("purpose")))
303    {
304        insert_value_claim_if_missing(cx, subject, card_help_predicate(), value)?;
305    }
306    for (field, predicate) in [
307        ("args", card_args_predicate()),
308        ("result", card_result_predicate()),
309        ("shape-known", card_shape_known_predicate()),
310    ] {
311        if let Some(value) = fallback.get(&field_symbol(field)) {
312            insert_value_claim_if_missing(cx, subject, predicate, value)?;
313        }
314    }
315    for (field, predicate) in [
316        ("tests", card_tests_predicate()),
317        ("ops", card_ops_predicate()),
318        ("requires", card_requires_predicate()),
319        ("see-also", card_see_also_predicate()),
320    ] {
321        if let Some(value) = fallback.get(&field_symbol(field)) {
322            insert_list_claims_if_missing(cx, subject, predicate, value)?;
323        }
324    }
325    Ok(())
326}
327
328fn insert_value_claim_if_missing(
329    cx: &mut Cx,
330    subject: &Ref,
331    predicate: Symbol,
332    value: &Value,
333) -> Result<()> {
334    let Some(object) = stable_ref_for_value(cx, value)? else {
335        return Ok(());
336    };
337    insert_ref_claim_if_missing(cx, subject, predicate, object)
338}
339
340fn insert_list_claims_if_missing(
341    cx: &mut Cx,
342    subject: &Ref,
343    predicate: Symbol,
344    value: &Value,
345) -> Result<()> {
346    if !claims_for(cx, subject, predicate.clone())?.is_empty() {
347        return Ok(());
348    }
349    let Some(list) = value.object().as_list() else {
350        return insert_value_claim_if_missing(cx, subject, predicate, value);
351    };
352    for item in force_list_to_vec(cx, list, "card compatibility claims")? {
353        let Some(object) = stable_ref_for_value(cx, &item)? else {
354            continue;
355        };
356        cx.insert_fact(Claim::public(subject.clone(), predicate.clone(), object))?;
357    }
358    Ok(())
359}
360
361fn insert_ref_claim_if_missing(
362    cx: &mut Cx,
363    subject: &Ref,
364    predicate: Symbol,
365    object: Ref,
366) -> Result<()> {
367    if claims_for(cx, subject, predicate.clone())?.is_empty() {
368        cx.insert_fact(Claim::public(subject.clone(), predicate, object))?;
369    }
370    Ok(())
371}
372
373fn stable_ref_for_value(cx: &mut Cx, value: &Value) -> Result<Option<Ref>> {
374    if let Expr::Symbol(symbol) = value.object().as_expr(cx)? {
375        return Ok(Some(Ref::Symbol(symbol)));
376    }
377    let mut resolver = TemporaryRefResolver::new();
378    match resolver.ref_for_value(cx, value)? {
379        Ref::Handle(_) => Ok(None),
380        reference => Ok(Some(reference)),
381    }
382}
383
384fn claim_scalar(cx: &mut Cx, subject: &Ref, predicate: Symbol) -> Result<Option<Value>> {
385    let claims = claims_for(cx, subject, predicate)?;
386    claims
387        .first()
388        .map(|reference| claim_object_value(cx, reference))
389        .transpose()
390}
391
392fn claim_list(cx: &mut Cx, subject: &Ref, predicate: Symbol) -> Result<Option<Value>> {
393    let claims = claims_for(cx, subject, predicate)?;
394    if claims.is_empty() {
395        return Ok(None);
396    }
397    let values = claims
398        .iter()
399        .map(|reference| claim_object_value(cx, reference))
400        .collect::<Result<Vec<_>>>()?;
401    cx.factory().list(values).map(Some)
402}
403
404fn claims_for(cx: &mut Cx, subject: &Ref, predicate: Symbol) -> Result<Vec<Ref>> {
405    let claims = cx.query_facts(ClaimPattern {
406        subject: Some(subject.clone()),
407        predicate: Some(predicate),
408        object: None,
409        include_revoked: false,
410    })?;
411    Ok(claims.into_iter().map(|claim| claim.object).collect())
412}
413
414fn claim_object_value(cx: &mut Cx, reference: &Ref) -> Result<Value> {
415    match reference {
416        Ref::Symbol(symbol) => cx.factory().symbol(symbol.clone()),
417        Ref::Content(id) => {
418            let datum = cx.datum_store().get(id)?.cloned();
419            match datum {
420                Some(datum) => value_from_datum(cx, datum),
421                None => ref_value(cx, reference),
422            }
423        }
424        Ref::Handle(handle) => match cx.handles().get(handle).cloned() {
425            Some(value) => Ok(value),
426            None => ref_value(cx, reference),
427        },
428        Ref::Coord(_) => ref_value(cx, reference),
429    }
430}
431
432fn fallback_entries(cx: &mut Cx, value: Option<&Value>) -> Result<BTreeMap<Symbol, Value>> {
433    let Some(value) = value else {
434        return Ok(BTreeMap::new());
435    };
436
437    let entries = if let Some(table) = value.object().as_table_impl() {
438        table.entries(cx)?
439    } else {
440        let table = value.object().as_table(cx)?;
441        match table.object().as_table_impl() {
442            Some(table) => table.entries(cx)?,
443            None => Vec::new(),
444        }
445    };
446
447    Ok(entries.into_iter().collect())
448}
449
450/// Projects a [`Ref`] into its runtime [`Value`] form: symbols become symbol
451/// values; content, handle, and coordinate refs become `core/ref` extension
452/// expressions.
453pub fn ref_value(cx: &mut Cx, reference: &Ref) -> Result<Value> {
454    match reference {
455        Ref::Symbol(symbol) => cx.factory().symbol(symbol.clone()),
456        Ref::Content(id) => cx.factory().expr(content_ref_expr(id)),
457        Ref::Handle(handle) => cx.factory().expr(handle_ref_expr(*handle)),
458        Ref::Coord(coordinate) => cx.factory().expr(coord_ref_expr(coordinate)),
459    }
460}
461
462fn content_ref_expr(id: &ContentId) -> Expr {
463    ref_expr(vec![
464        (
465            Expr::Symbol(field_symbol("kind")),
466            Expr::Symbol(core_symbol("content")),
467        ),
468        (
469            Expr::Symbol(field_symbol("algorithm")),
470            Expr::Symbol(id.algorithm.clone()),
471        ),
472        (
473            Expr::Symbol(field_symbol("bytes")),
474            Expr::Bytes(id.bytes.to_vec()),
475        ),
476    ])
477}
478
479fn handle_ref_expr(handle: HandleId) -> Expr {
480    ref_expr(vec![
481        (
482            Expr::Symbol(field_symbol("kind")),
483            Expr::Symbol(core_symbol("handle")),
484        ),
485        (
486            Expr::Symbol(field_symbol("id")),
487            Expr::Bytes(handle.0.to_be_bytes().to_vec()),
488        ),
489    ])
490}
491
492fn coord_ref_expr(coordinate: &Coordinate) -> Expr {
493    ref_expr(vec![
494        (
495            Expr::Symbol(field_symbol("kind")),
496            Expr::Symbol(core_symbol("coord")),
497        ),
498        (
499            Expr::Symbol(field_symbol("space")),
500            Expr::Symbol(coordinate.space.clone()),
501        ),
502        (
503            Expr::Symbol(field_symbol("ordinal")),
504            content_ref_expr(&coordinate.ordinal),
505        ),
506    ])
507}
508
509fn ref_expr(entries: Vec<(Expr, Expr)>) -> Expr {
510    Expr::Extension {
511        tag: core_symbol("ref"),
512        payload: Box::new(Expr::Map(entries)),
513    }
514}
515
516fn empty_list(cx: &mut Cx) -> Result<Value> {
517    cx.factory().list(Vec::new())
518}
519
520fn core_symbol_value(cx: &mut Cx, name: &'static str) -> Result<Value> {
521    cx.factory().symbol(core_symbol(name))
522}
523
524fn field_symbol(name: &'static str) -> Symbol {
525    Symbol::new(name)
526}
527
528fn core_symbol(name: &'static str) -> Symbol {
529    Symbol::qualified("core", name)
530}
531
532#[cfg(test)]
533#[path = "card/tests.rs"]
534mod tests;