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    CitizenLib, CitizenRuntime, field_error, registered_citizens, 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    cx.load_lib(&CitizenLib::all())?;
22    for info in registered_citizens() {
23        (info.conformance)(cx)?;
24    }
25    Ok(())
26}
27
28/// Checks the citizen's default [`CitizenRuntime::example`] fixture round trip.
29///
30/// Convenience wrapper over [`check_fixture`] using the type's canonical
31/// example value.
32pub fn check_default_fixture<T>(cx: &mut Cx) -> Result<()>
33where
34    T: CitizenRuntime,
35{
36    check_fixture(cx, T::example())
37}
38
39/// Checks one runtime fixture's read-construct round trip and failure paths.
40///
41/// Encodes `fixture` to its constructor encoding, derives a deliberately wrong
42/// version argument, and delegates to
43/// [`check_value_fixture_with_wrong_version`] so the gate also rejects bad
44/// versions alongside the round-trip, capability, and arity assertions.
45pub fn check_fixture<T>(cx: &mut Cx, fixture: T) -> Result<()>
46where
47    T: CitizenRuntime,
48{
49    let original = cx.factory().opaque(Arc::new(fixture))?;
50    let ObjectEncoding::Constructor { args, .. } = object_constructor_encoding(cx, &original)?
51    else {
52        unreachable!("object_constructor_encoding only returns constructor encodings");
53    };
54    let mut wrong_version = args.clone();
55    if let Some(first) = wrong_version.first_mut() {
56        *first = Expr::Symbol(Symbol::new("v999999"));
57    }
58    check_value_fixture_with_wrong_version(cx, original, Some(wrong_version))
59}
60
61/// Checks an already-built citizen [`Value`]'s read-construct round trip.
62///
63/// Like [`check_value_fixture_with_wrong_version`] but without a wrong-version
64/// case, for citizens whose encoding has no version argument to corrupt.
65pub fn check_value_fixture(cx: &mut Cx, original: Value) -> Result<()> {
66    check_value_fixture_with_wrong_version(cx, original, None)
67}
68
69/// Asserts the full citizen read-construct contract for one value.
70///
71/// Confirms the constructor encoding renders to `#(<class> ...)` text, that
72/// read-construct is denied without the capability and succeeds with it, that
73/// the decoded value is `CitizenEq` to `original`, that a truncated argument
74/// list is rejected, and, when `wrong_version` is supplied, that a mismatched
75/// version is rejected. Read-construct stays capability-gated by the runtime
76/// path; this helper only exercises the contract the kernel enforces.
77pub fn check_value_fixture_with_wrong_version(
78    cx: &mut Cx,
79    original: Value,
80    wrong_version: Option<Vec<Expr>>,
81) -> Result<()> {
82    let ObjectEncoding::Constructor { class, args } = object_constructor_encoding(cx, &original)?
83    else {
84        unreachable!("object_constructor_encoding only returns constructor encodings");
85    };
86
87    check_constructor_fixture(cx, original, class, args, wrong_version)
88}
89
90fn object_constructor_encoding(cx: &mut Cx, value: &Value) -> Result<ObjectEncoding> {
91    let Some(encoder) = value.object().as_object_encoder() else {
92        return Err(Error::Eval(
93            "citizen conformance expects constructor encoding".to_owned(),
94        ));
95    };
96    let encoding = encoder.object_encoding(cx)?;
97    if !matches!(encoding, ObjectEncoding::Constructor { .. }) {
98        return Err(Error::Eval(
99            "citizen conformance expects constructor encoding".to_owned(),
100        ));
101    }
102    Ok(encoding)
103}
104
105fn check_constructor_fixture(
106    cx: &mut Cx,
107    original: Value,
108    class: Symbol,
109    args: Vec<Expr>,
110    wrong_version: Option<Vec<Expr>>,
111) -> Result<()> {
112    let text = render_constructor(&class, &args);
113    let prefix = format!("#({class}");
114    if !text.starts_with(&prefix) {
115        return Err(Error::Eval(format!(
116            "citizen constructor text {text:?} does not start with {prefix:?}"
117        )));
118    }
119
120    let values = exprs_to_values(cx, &args)?;
121    cx.with_capabilities(CapabilitySet::default(), |cx| {
122        assert_capability_denied(cx.read_construct(&class, values.clone()))
123    })?;
124
125    let mut allowed = cx.capabilities().clone();
126    allowed.insert(read_construct_capability());
127    cx.with_capabilities(allowed, |cx| {
128        let decoded = cx.read_construct(&class, values)?;
129        if !values_citizen_eq(cx, &original, &decoded)? {
130            return Err(Error::Eval(format!(
131                "citizen {class} failed constructor round-trip equality"
132            )));
133        }
134
135        let malformed = exprs_to_values(cx, &args[..args.len().saturating_sub(1)])?;
136        if cx.read_construct(&class, malformed).is_ok() {
137            return Err(Error::Eval(format!(
138                "citizen {class} accepted malformed arity"
139            )));
140        }
141
142        if let Some(wrong_version) = wrong_version {
143            let wrong_version = exprs_to_values(cx, &wrong_version)?;
144            if cx.read_construct(&class, wrong_version).is_ok() {
145                return Err(Error::Eval(format!(
146                    "citizen {class} accepted wrong version"
147                )));
148            }
149        }
150
151        Ok(())
152    })?;
153
154    Ok(())
155}
156
157fn exprs_to_values(cx: &mut Cx, exprs: &[Expr]) -> Result<Vec<Value>> {
158    exprs.iter().map(|expr| value_from_expr(cx, expr)).collect()
159}
160
161fn assert_capability_denied(result: Result<Value>) -> Result<()> {
162    match result {
163        Err(Error::CapabilityDenied { capability })
164            if capability == read_construct_capability() =>
165        {
166            Ok(())
167        }
168        Err(err) => Err(field_error(
169            "read-construct",
170            format!("expected capability denial, found {err}"),
171        )),
172        Ok(_) => Err(field_error(
173            "read-construct",
174            "expected capability denial, found success",
175        )),
176    }
177}
178
179fn render_constructor(class: &Symbol, args: &[Expr]) -> String {
180    let mut out = format!("#({class}");
181    for arg in args {
182        out.push(' ');
183        out.push_str(&render_expr(arg));
184    }
185    out.push(')');
186    out
187}
188
189fn render_expr(expr: &Expr) -> String {
190    match expr {
191        Expr::Nil => "nil".to_owned(),
192        Expr::Bool(value) => value.to_string(),
193        Expr::Number(value) => value.canonical.clone(),
194        Expr::Symbol(value) => value.to_string(),
195        Expr::String(value) => format!("{value:?}"),
196        Expr::List(items) => {
197            let rendered = items.iter().map(render_expr).collect::<Vec<_>>().join(" ");
198            format!("({rendered})")
199        }
200        other => format!("{other:?}"),
201    }
202}