Skip to main content

sim_kernel/
shape_report.rs

1//! Shape reports: the contract for diagnostics from [`shape`](crate::shape)
2//! checks.
3//!
4//! The kernel defines the report record built from a shape match and the
5//! satisfaction-claim it publishes; libraries produce reports by running the
6//! shape protocol.
7
8use crate::{
9    capability::fact_private_capability,
10    claim::{Claim, ClaimKind, Visibility},
11    datum::Datum,
12    datum_store::DatumStore,
13    env::Cx,
14    error::{Diagnostic, Error, Result, Severity},
15    expr::{Expr, NumberLiteral, Span},
16    hint::HintMetadata,
17    id::Symbol,
18    object::ShapeRef,
19    ref_id::{ContentId, Coordinate, HandleId, Ref},
20    ref_resolver::{RefResolver, TemporaryRefResolver},
21    shape::{MatchScore, ShapeBindings, ShapeMatch},
22    term::Term,
23    value::Value,
24};
25
26/// The canonical record of a shape check: who was checked against what, the
27/// outcome, and the diagnostics.
28///
29/// A report is interned content (the `id`) and is the evidence backing the
30/// `satisfies-shape` satisfaction claim published when a match is accepted.
31/// The kernel defines this record and that claim; libraries produce reports by
32/// running the [`shape`](crate::shape) protocol.
33#[derive(Clone, Debug, PartialEq, Eq)]
34pub struct ShapeReport {
35    /// Content reference identifying this report.
36    pub id: Ref,
37    /// Reference to the shape that was checked.
38    pub shape: Ref,
39    /// Reference to the value or expr that was checked.
40    pub target: Ref,
41    /// Whether the target satisfied the shape.
42    pub accepted: bool,
43    /// The match score.
44    pub score: MatchScore,
45    /// Reference to the interned captures datum.
46    pub captures: Ref,
47    /// Diagnostics gathered during the check.
48    pub diagnostics: Vec<Diagnostic>,
49}
50
51impl ShapeReport {
52    /// Build the canonical [`Datum`] for this report (excluding its own id).
53    pub fn canonical_datum(&self) -> Datum {
54        shape_report_datum(
55            &self.shape,
56            &self.target,
57            self.accepted,
58            self.score,
59            &self.captures,
60            &self.diagnostics,
61        )
62    }
63}
64
65/// Check `value` against `shape_value`, building a [`ShapeReport`] and
66/// publishing the satisfaction claim when accepted.
67///
68/// Returns a [`TypeMismatch`](crate::Error::TypeMismatch) error when
69/// `shape_value` is not a shape.
70pub fn check_value_report(
71    cx: &mut Cx,
72    shape_value: &ShapeRef,
73    value: Value,
74) -> Result<ShapeReport> {
75    let shape_ref = ref_for_shape(cx, shape_value)?;
76    let mut resolver = TemporaryRefResolver::new();
77    let target_ref = resolver.ref_for_value(cx, &value)?;
78    let visibility = satisfaction_visibility(cx, &target_ref, &shape_ref, &value)?;
79
80    let Some(shape) = shape_value.object().as_shape() else {
81        return Err(Error::TypeMismatch {
82            expected: "shape",
83            found: "non-shape",
84        });
85    };
86    let matched = shape.check_value(cx, value)?;
87    let report = shape_report_from_match(cx, shape_ref, target_ref, matched)?;
88    insert_shape_satisfaction_claim(cx, &report, visibility)?;
89    Ok(report)
90}
91
92/// Intern a [`ShapeMatch`] into a [`ShapeReport`] for the given shape and
93/// target references.
94pub fn shape_report_from_match(
95    cx: &mut Cx,
96    shape: Ref,
97    target: Ref,
98    matched: ShapeMatch,
99) -> Result<ShapeReport> {
100    let captures = captures_ref(cx, &matched.captures)?;
101    let datum = shape_report_datum(
102        &shape,
103        &target,
104        matched.accepted,
105        matched.score,
106        &captures,
107        &matched.diagnostics,
108    );
109    let id = cx.datum_store_mut().intern(datum)?;
110    Ok(ShapeReport {
111        id: Ref::Content(id),
112        shape,
113        target,
114        accepted: matched.accepted,
115        score: matched.score,
116        captures,
117        diagnostics: matched.diagnostics,
118    })
119}
120
121/// Publish the `satisfies-shape` claim for an accepted report.
122///
123/// Returns `Ok(None)` when the report was rejected. Private visibility inserts
124/// the claim under the fact-private capability.
125pub fn insert_shape_satisfaction_claim(
126    cx: &mut Cx,
127    report: &ShapeReport,
128    visibility: Visibility,
129) -> Result<Option<Ref>> {
130    if !report.accepted {
131        return Ok(None);
132    }
133
134    let claim = Claim::public(
135        report.target.clone(),
136        satisfies_shape_predicate(),
137        report.shape.clone(),
138    )
139    .with_kind(ClaimKind::Observed)
140    .with_evidence(vec![report.id.clone()])
141    .with_visibility(visibility);
142
143    if visibility == Visibility::Private {
144        let mut capabilities = cx.capabilities().clone();
145        capabilities.insert(fact_private_capability());
146        cx.with_capabilities(capabilities, |cx| cx.insert_fact(claim))
147            .map(Some)
148    } else {
149        cx.insert_fact(claim).map(Some)
150    }
151}
152
153/// The predicate symbol (`core/satisfies-shape`) used by satisfaction claims.
154pub fn satisfies_shape_predicate() -> Symbol {
155    core_symbol("satisfies-shape")
156}
157
158fn ref_for_shape(cx: &mut Cx, shape_value: &ShapeRef) -> Result<Ref> {
159    if let Some(symbol) = shape_value
160        .object()
161        .as_shape()
162        .and_then(|shape| shape.symbol())
163    {
164        return Ok(Ref::Symbol(symbol));
165    }
166    TemporaryRefResolver::new().ref_for_value(cx, shape_value)
167}
168
169fn satisfaction_visibility(
170    cx: &mut Cx,
171    target: &Ref,
172    shape: &Ref,
173    value: &Value,
174) -> Result<Visibility> {
175    if matches!(target, Ref::Handle(_))
176        && !value
177            .object()
178            .publish_shape_satisfaction_claims(cx, shape)?
179    {
180        return Ok(Visibility::Private);
181    }
182    Ok(Visibility::Public)
183}
184
185fn captures_ref(cx: &mut Cx, captures: &ShapeBindings) -> Result<Ref> {
186    let datum = captures_datum(cx, captures)?;
187    cx.datum_store_mut().intern(datum).map(Ref::Content)
188}
189
190fn captures_datum(cx: &mut Cx, captures: &ShapeBindings) -> Result<Datum> {
191    let mut resolver = TemporaryRefResolver::new();
192    let values = captures
193        .values()
194        .iter()
195        .map(|(name, value)| {
196            let value_ref = resolver.ref_for_value(cx, value)?;
197            Ok(binding_datum(name, "value", ref_datum(&value_ref)))
198        })
199        .collect::<Result<Vec<_>>>()?;
200    let exprs = captures
201        .exprs()
202        .iter()
203        .map(|(name, expr)| Ok(binding_datum(name, "expr", expr_datum(expr)?)))
204        .collect::<Result<Vec<_>>>()?;
205
206    Ok(Datum::Node {
207        tag: core_symbol("ShapeCaptures"),
208        fields: vec![
209            (Symbol::new("values"), Datum::Vector(values)),
210            (Symbol::new("exprs"), Datum::Vector(exprs)),
211        ],
212    })
213}
214
215fn binding_datum(name: &Symbol, kind: &str, value: Datum) -> Datum {
216    Datum::Node {
217        tag: core_symbol("shape-binding"),
218        fields: vec![
219            (Symbol::new("name"), Datum::Symbol(name.clone())),
220            (Symbol::new("kind"), Datum::Symbol(core_symbol(kind))),
221            (Symbol::new("value"), value),
222        ],
223    }
224}
225
226fn expr_datum(expr: &Expr) -> Result<Datum> {
227    match Datum::try_from(expr.clone()) {
228        Ok(datum) => Ok(datum),
229        Err(_) => Ok(Term::lower(expr.clone())?.to_datum()),
230    }
231}
232
233fn shape_report_datum(
234    shape: &Ref,
235    target: &Ref,
236    accepted: bool,
237    score: MatchScore,
238    captures: &Ref,
239    diagnostics: &[Diagnostic],
240) -> Datum {
241    Datum::Node {
242        tag: core_symbol("ShapeReport"),
243        fields: vec![
244            (Symbol::new("shape"), ref_datum(shape)),
245            (Symbol::new("target"), ref_datum(target)),
246            (Symbol::new("accepted"), Datum::Bool(accepted)),
247            (Symbol::new("score"), score_datum(score)),
248            (Symbol::new("captures"), ref_datum(captures)),
249            (
250                Symbol::new("diagnostics"),
251                Datum::Vector(diagnostics.iter().map(diagnostic_datum).collect()),
252            ),
253        ],
254    }
255}
256
257fn diagnostic_datum(diagnostic: &Diagnostic) -> Datum {
258    let hints = HintMetadata::collect_from_diagnostic(diagnostic)
259        .into_iter()
260        .map(|hint| hint.as_datum())
261        .collect();
262    let mut fields = vec![
263        (
264            Symbol::new("severity"),
265            Datum::Symbol(severity_symbol(diagnostic.severity)),
266        ),
267        (
268            Symbol::new("message"),
269            Datum::String(diagnostic.message.clone()),
270        ),
271        (
272            Symbol::new("related"),
273            Datum::Vector(
274                diagnostic
275                    .related
276                    .iter()
277                    .filter(|related| !HintMetadata::is_hint_diagnostic(related))
278                    .map(diagnostic_datum)
279                    .collect(),
280            ),
281        ),
282        (Symbol::new("hints"), Datum::Vector(hints)),
283    ];
284    if let Some(source) = &diagnostic.source {
285        fields.push((Symbol::new("source"), Datum::String(source.0.clone())));
286    }
287    if let Some(span) = &diagnostic.span {
288        fields.push((Symbol::new("span"), span_datum(span)));
289    }
290    if let Some(code) = &diagnostic.code {
291        fields.push((Symbol::new("code"), Datum::Symbol(code.clone())));
292    }
293    Datum::Node {
294        tag: core_symbol("diagnostic"),
295        fields,
296    }
297}
298
299fn span_datum(span: &Span) -> Datum {
300    Datum::Node {
301        tag: core_symbol("span"),
302        fields: vec![
303            (
304                Symbol::new("start"),
305                Datum::Number(usize_number(span.start)),
306            ),
307            (Symbol::new("end"), Datum::Number(usize_number(span.end))),
308        ],
309    }
310}
311
312fn severity_symbol(severity: Severity) -> Symbol {
313    match severity {
314        Severity::Error => core_symbol("error"),
315        Severity::Warning => core_symbol("warning"),
316        Severity::Info => core_symbol("info"),
317        Severity::Note => core_symbol("note"),
318    }
319}
320
321fn score_datum(score: MatchScore) -> Datum {
322    Datum::Number(NumberLiteral {
323        domain: Symbol::qualified("numbers", "f64"),
324        canonical: score.value().to_string(),
325    })
326}
327
328fn usize_number(value: usize) -> NumberLiteral {
329    NumberLiteral {
330        domain: Symbol::qualified("numbers", "i64"),
331        canonical: value.to_string(),
332    }
333}
334
335fn ref_datum(reference: &Ref) -> Datum {
336    match reference {
337        Ref::Symbol(symbol) => Datum::Node {
338            tag: core_symbol("ref"),
339            fields: vec![
340                (Symbol::new("kind"), Datum::Symbol(core_symbol("symbol"))),
341                (Symbol::new("symbol"), Datum::Symbol(symbol.clone())),
342            ],
343        },
344        Ref::Content(content) => Datum::Node {
345            tag: core_symbol("ref"),
346            fields: vec![
347                (Symbol::new("kind"), Datum::Symbol(core_symbol("content"))),
348                (Symbol::new("content"), content_id_datum(content)),
349            ],
350        },
351        Ref::Handle(handle) => Datum::Node {
352            tag: core_symbol("ref"),
353            fields: vec![
354                (Symbol::new("kind"), Datum::Symbol(core_symbol("handle"))),
355                (Symbol::new("id"), handle_id_datum(*handle)),
356            ],
357        },
358        Ref::Coord(coordinate) => coordinate_datum(coordinate),
359    }
360}
361
362fn coordinate_datum(coordinate: &Coordinate) -> Datum {
363    Datum::Node {
364        tag: core_symbol("ref"),
365        fields: vec![
366            (Symbol::new("kind"), Datum::Symbol(core_symbol("coord"))),
367            (
368                Symbol::new("space"),
369                Datum::Symbol(coordinate.space.clone()),
370            ),
371            (
372                Symbol::new("ordinal"),
373                content_id_datum(&coordinate.ordinal),
374            ),
375        ],
376    }
377}
378
379fn content_id_datum(content: &ContentId) -> Datum {
380    Datum::Node {
381        tag: core_symbol("content-id"),
382        fields: vec![
383            (
384                Symbol::new("algorithm"),
385                Datum::Symbol(content.algorithm.clone()),
386            ),
387            (Symbol::new("bytes"), Datum::Bytes(content.bytes.to_vec())),
388        ],
389    }
390}
391
392fn handle_id_datum(handle: HandleId) -> Datum {
393    Datum::Bytes(handle.0.to_be_bytes().to_vec())
394}
395
396fn core_symbol(name: &str) -> Symbol {
397    Symbol::qualified("core", name)
398}
399
400#[cfg(test)]
401#[path = "shape_report/tests.rs"]
402mod tests;