Skip to main content

sim_citizen/
conformance.rs

1//! Conformance fixtures that check a citizen's read-construct round trip.
2
3use std::sync::Arc;
4
5use sim_kernel::{
6    CapabilitySet, Cx, Error, Expr, ObjectEncoding, Result, Symbol, Value,
7    read_construct_capability,
8};
9
10use crate::{
11    CitizenInfo, CitizenLib, CitizenRegistry, CitizenRuntime, field_error, value_from_expr,
12    values_citizen_eq,
13};
14
15/// Loads every registered citizen and runs each citizen's conformance fixture.
16///
17/// Installs [`CitizenLib::all`] into `cx`, then invokes the recorded
18/// conformance check for each registered `CitizenInfo`. This is the entry
19/// point a test harness calls to assert the whole registered set conforms.
20pub fn run_registered_conformance(cx: &mut Cx) -> Result<()> {
21    let registry = CitizenRegistry::from_inventory()?;
22    cx.load_lib(&CitizenLib::all())?;
23    run_conformance_rows(cx, registry.citizens())
24}
25
26/// Runs inventory-backed conformance after checking expected symbols.
27///
28/// This is the fail-closed inventory gate for tests that know which citizens a
29/// build must contain. If link-time constructor collection drops a citizen row,
30/// or if a wasm host exposes an empty inventory, the missing expected symbol is
31/// reported before any shorter conformance sweep can pass.
32pub fn run_registered_conformance_expecting(cx: &mut Cx, expected: &[&str]) -> Result<()> {
33    let registry = CitizenRegistry::from_inventory()?;
34    registry.ensure_contains_symbols(expected)?;
35    cx.load_lib(&CitizenLib::all())?;
36    run_conformance_rows(cx, registry.citizens())
37}
38
39/// Loads an explicit registry and runs each row's conformance fixture.
40///
41/// Use this DCE-safe path when a crate can name the citizen types it owns with
42/// [`CitizenRegistry::register`]. The registry itself is loaded as a kernel
43/// library before its fixture hooks run.
44pub fn run_registry_conformance(cx: &mut Cx, registry: &CitizenRegistry) -> Result<()> {
45    cx.load_lib(registry)?;
46    run_conformance_rows(cx, registry.citizens())
47}
48
49/// Runs explicit-registry conformance after checking expected symbols.
50///
51/// This combines the DCE-safe registry path with the same completeness guard
52/// used by [`run_registered_conformance_expecting`].
53pub fn run_registry_conformance_expecting(
54    cx: &mut Cx,
55    registry: &CitizenRegistry,
56    expected: &[&str],
57) -> Result<()> {
58    registry.ensure_contains_symbols(expected)?;
59    run_registry_conformance(cx, registry)
60}
61
62fn run_conformance_rows<'a>(
63    cx: &mut Cx,
64    rows: impl IntoIterator<Item = &'a CitizenInfo>,
65) -> Result<()> {
66    for info in rows {
67        (info.conformance)(cx)?;
68    }
69    Ok(())
70}
71
72/// Checks the citizen's default [`CitizenRuntime::example`] fixture round trip.
73///
74/// Convenience wrapper over [`check_fixture`] using the type's canonical
75/// example value.
76pub fn check_default_fixture<T>(cx: &mut Cx) -> Result<()>
77where
78    T: CitizenRuntime,
79{
80    check_fixture(cx, T::example())
81}
82
83/// Checks one runtime fixture's read-construct round trip and failure paths.
84///
85/// Encodes `fixture` to its constructor encoding, derives a deliberately wrong
86/// version argument, and delegates to
87/// [`check_value_fixture_with_wrong_version`] so the gate also rejects bad
88/// versions alongside the round-trip, capability, and arity assertions.
89pub fn check_fixture<T>(cx: &mut Cx, fixture: T) -> Result<()>
90where
91    T: CitizenRuntime,
92{
93    let original = cx.factory().opaque(Arc::new(fixture))?;
94    let ObjectEncoding::Constructor { args, .. } = object_constructor_encoding(cx, &original)?
95    else {
96        unreachable!("object_constructor_encoding only returns constructor encodings");
97    };
98    let mut wrong_version = args.clone();
99    if let Some(first) = wrong_version.first_mut() {
100        *first = Expr::Symbol(Symbol::new("v999999"));
101    }
102    check_value_fixture_with_wrong_version(cx, original, Some(wrong_version))
103}
104
105/// Checks an already-built citizen [`Value`]'s read-construct round trip.
106///
107/// Like [`check_value_fixture_with_wrong_version`] but without a wrong-version
108/// case, for citizens whose encoding has no version argument to corrupt.
109pub fn check_value_fixture(cx: &mut Cx, original: Value) -> Result<()> {
110    check_value_fixture_with_wrong_version(cx, original, None)
111}
112
113/// Asserts the full citizen read-construct contract for one value.
114///
115/// Confirms the constructor encoding renders to `#(<class> ...)` text, that
116/// read-construct is denied without the capability and succeeds with it, that
117/// the decoded value is `CitizenEq` to `original`, that a truncated argument
118/// list is rejected, and, when `wrong_version` is supplied, that a mismatched
119/// version is rejected. Read-construct stays capability-gated by the runtime
120/// path; this helper only exercises the contract the kernel enforces.
121pub fn check_value_fixture_with_wrong_version(
122    cx: &mut Cx,
123    original: Value,
124    wrong_version: Option<Vec<Expr>>,
125) -> Result<()> {
126    let ObjectEncoding::Constructor { class, args } = object_constructor_encoding(cx, &original)?
127    else {
128        unreachable!("object_constructor_encoding only returns constructor encodings");
129    };
130
131    check_constructor_fixture(cx, original, class, args, wrong_version)
132}
133
134fn object_constructor_encoding(cx: &mut Cx, value: &Value) -> Result<ObjectEncoding> {
135    let Some(encoder) = value.object().as_object_encoder() else {
136        return Err(Error::Eval(
137            "citizen conformance expects constructor encoding".to_owned(),
138        ));
139    };
140    let encoding = encoder.object_encoding(cx)?;
141    if !matches!(encoding, ObjectEncoding::Constructor { .. }) {
142        return Err(Error::Eval(
143            "citizen conformance expects constructor encoding".to_owned(),
144        ));
145    }
146    Ok(encoding)
147}
148
149fn check_constructor_fixture(
150    cx: &mut Cx,
151    original: Value,
152    class: Symbol,
153    args: Vec<Expr>,
154    wrong_version: Option<Vec<Expr>>,
155) -> Result<()> {
156    let text = render_constructor(&class, &args);
157    let prefix = format!("#({class}");
158    if !text.starts_with(&prefix) {
159        return Err(Error::Eval(format!(
160            "citizen constructor text {text:?} does not start with {prefix:?}"
161        )));
162    }
163
164    let values = exprs_to_values(cx, &args)?;
165    cx.with_capabilities(CapabilitySet::default(), |cx| {
166        assert_capability_denied(cx.read_construct(&class, values.clone()))
167    })?;
168
169    let mut allowed = cx.capabilities().clone();
170    allowed.insert(read_construct_capability());
171    cx.with_capabilities(allowed, |cx| {
172        let decoded = cx.read_construct(&class, values)?;
173        if !values_citizen_eq(cx, &original, &decoded)? {
174            return Err(Error::Eval(format!(
175                "citizen {class} failed constructor round-trip equality"
176            )));
177        }
178
179        let malformed = exprs_to_values(cx, &args[..args.len().saturating_sub(1)])?;
180        if cx.read_construct(&class, malformed).is_ok() {
181            return Err(Error::Eval(format!(
182                "citizen {class} accepted malformed arity"
183            )));
184        }
185
186        if let Some(wrong_version) = wrong_version {
187            let wrong_version = exprs_to_values(cx, &wrong_version)?;
188            if cx.read_construct(&class, wrong_version).is_ok() {
189                return Err(Error::Eval(format!(
190                    "citizen {class} accepted wrong version"
191                )));
192            }
193        }
194
195        Ok(())
196    })?;
197
198    Ok(())
199}
200
201fn exprs_to_values(cx: &mut Cx, exprs: &[Expr]) -> Result<Vec<Value>> {
202    exprs.iter().map(|expr| value_from_expr(cx, expr)).collect()
203}
204
205fn assert_capability_denied(result: Result<Value>) -> Result<()> {
206    match result {
207        Err(Error::CapabilityDenied { capability })
208            if capability == read_construct_capability() =>
209        {
210            Ok(())
211        }
212        Err(err) => Err(field_error(
213            "read-construct",
214            format!("expected capability denial, found {err}"),
215        )),
216        Ok(_) => Err(field_error(
217            "read-construct",
218            "expected capability denial, found success",
219        )),
220    }
221}
222
223fn render_constructor(class: &Symbol, args: &[Expr]) -> String {
224    let mut out = format!("#({class}");
225    for arg in args {
226        out.push(' ');
227        out.push_str(&render_expr(arg));
228    }
229    out.push(')');
230    out
231}
232
233fn render_expr(expr: &Expr) -> String {
234    match expr {
235        Expr::Nil => "nil".to_owned(),
236        Expr::Bool(value) => value.to_string(),
237        Expr::Number(value) => value.canonical.clone(),
238        Expr::Symbol(value) => value.to_string(),
239        Expr::String(value) => format!("{value:?}"),
240        Expr::List(items) => {
241            let rendered = items.iter().map(render_expr).collect::<Vec<_>>().join(" ");
242            format!("({rendered})")
243        }
244        other => format!("{other:?}"),
245    }
246}