sim_citizen/
conformance.rs1use 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
15pub 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
28pub fn check_default_fixture<T>(cx: &mut Cx) -> Result<()>
33where
34 T: CitizenRuntime,
35{
36 check_fixture(cx, T::example())
37}
38
39pub 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
61pub fn check_value_fixture(cx: &mut Cx, original: Value) -> Result<()> {
66 check_value_fixture_with_wrong_version(cx, original, None)
67}
68
69pub 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}