use std::sync::Arc;
use sim_kernel::{
CapabilitySet, Cx, Error, Expr, ObjectEncoding, Result, Symbol, Value,
read_construct_capability,
};
use crate::{
CitizenLib, CitizenRuntime, field_error, registered_citizens, value_from_expr,
values_citizen_eq,
};
pub fn run_registered_conformance(cx: &mut Cx) -> Result<()> {
cx.load_lib(&CitizenLib::all())?;
for info in registered_citizens() {
(info.conformance)(cx)?;
}
Ok(())
}
pub fn check_default_fixture<T>(cx: &mut Cx) -> Result<()>
where
T: CitizenRuntime,
{
check_fixture(cx, T::example())
}
pub fn check_fixture<T>(cx: &mut Cx, fixture: T) -> Result<()>
where
T: CitizenRuntime,
{
let original = cx.factory().opaque(Arc::new(fixture))?;
let ObjectEncoding::Constructor { args, .. } = object_constructor_encoding(cx, &original)?
else {
unreachable!("object_constructor_encoding only returns constructor encodings");
};
let mut wrong_version = args.clone();
if let Some(first) = wrong_version.first_mut() {
*first = Expr::Symbol(Symbol::new("v999999"));
}
check_value_fixture_with_wrong_version(cx, original, Some(wrong_version))
}
pub fn check_value_fixture(cx: &mut Cx, original: Value) -> Result<()> {
check_value_fixture_with_wrong_version(cx, original, None)
}
pub fn check_value_fixture_with_wrong_version(
cx: &mut Cx,
original: Value,
wrong_version: Option<Vec<Expr>>,
) -> Result<()> {
let ObjectEncoding::Constructor { class, args } = object_constructor_encoding(cx, &original)?
else {
unreachable!("object_constructor_encoding only returns constructor encodings");
};
check_constructor_fixture(cx, original, class, args, wrong_version)
}
fn object_constructor_encoding(cx: &mut Cx, value: &Value) -> Result<ObjectEncoding> {
let Some(encoder) = value.object().as_object_encoder() else {
return Err(Error::Eval(
"citizen conformance expects constructor encoding".to_owned(),
));
};
let encoding = encoder.object_encoding(cx)?;
if !matches!(encoding, ObjectEncoding::Constructor { .. }) {
return Err(Error::Eval(
"citizen conformance expects constructor encoding".to_owned(),
));
}
Ok(encoding)
}
fn check_constructor_fixture(
cx: &mut Cx,
original: Value,
class: Symbol,
args: Vec<Expr>,
wrong_version: Option<Vec<Expr>>,
) -> Result<()> {
let text = render_constructor(&class, &args);
let prefix = format!("#({class}");
if !text.starts_with(&prefix) {
return Err(Error::Eval(format!(
"citizen constructor text {text:?} does not start with {prefix:?}"
)));
}
let values = exprs_to_values(cx, &args)?;
cx.with_capabilities(CapabilitySet::default(), |cx| {
assert_capability_denied(cx.read_construct(&class, values.clone()))
})?;
let mut allowed = cx.capabilities().clone();
allowed.insert(read_construct_capability());
cx.with_capabilities(allowed, |cx| {
let decoded = cx.read_construct(&class, values)?;
if !values_citizen_eq(cx, &original, &decoded)? {
return Err(Error::Eval(format!(
"citizen {class} failed constructor round-trip equality"
)));
}
let malformed = exprs_to_values(cx, &args[..args.len().saturating_sub(1)])?;
if cx.read_construct(&class, malformed).is_ok() {
return Err(Error::Eval(format!(
"citizen {class} accepted malformed arity"
)));
}
if let Some(wrong_version) = wrong_version {
let wrong_version = exprs_to_values(cx, &wrong_version)?;
if cx.read_construct(&class, wrong_version).is_ok() {
return Err(Error::Eval(format!(
"citizen {class} accepted wrong version"
)));
}
}
Ok(())
})?;
Ok(())
}
fn exprs_to_values(cx: &mut Cx, exprs: &[Expr]) -> Result<Vec<Value>> {
exprs.iter().map(|expr| value_from_expr(cx, expr)).collect()
}
fn assert_capability_denied(result: Result<Value>) -> Result<()> {
match result {
Err(Error::CapabilityDenied { capability })
if capability == read_construct_capability() =>
{
Ok(())
}
Err(err) => Err(field_error(
"read-construct",
format!("expected capability denial, found {err}"),
)),
Ok(_) => Err(field_error(
"read-construct",
"expected capability denial, found success",
)),
}
}
fn render_constructor(class: &Symbol, args: &[Expr]) -> String {
let mut out = format!("#({class}");
for arg in args {
out.push(' ');
out.push_str(&render_expr(arg));
}
out.push(')');
out
}
fn render_expr(expr: &Expr) -> String {
match expr {
Expr::Nil => "nil".to_owned(),
Expr::Bool(value) => value.to_string(),
Expr::Number(value) => value.canonical.clone(),
Expr::Symbol(value) => value.to_string(),
Expr::String(value) => format!("{value:?}"),
Expr::List(items) => {
let rendered = items.iter().map(render_expr).collect::<Vec<_>>().join(" ");
format!("({rendered})")
}
other => format!("{other:?}"),
}
}