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