use std::sync::Arc;
use tatara_closed_set::ClosedSet;
use tatara_lisp::{NumericAxis, NumericLiteral, NumericWidth, Span};
use crate::error::{EvalError, Result};
use crate::value::{ErrorObj, Value};
pub const TYPE_NAMES: &[&str] = &["the", "type-of", "is?", "cast"];
const NON_NUMERIC_ATOMIC_TYPES: &[&str] = &[
"bool",
"string",
"symbol",
"keyword",
"nil",
"list",
"map",
"error",
"promise",
"procedure",
"foreign",
"any",
"number",
];
const PARAMETRIC: &[&str] = &["list-of", "map-of", "fn", "union"];
const WIDTH_ALIASES: [(&str, NumericWidth); 2] =
[("int", NumericWidth::I64), ("float", NumericWidth::F64)];
#[must_use]
pub fn atomic_type_keywords() -> Vec<&'static str> {
let mut out = NON_NUMERIC_ATOMIC_TYPES.to_vec();
out.extend(WIDTH_ALIASES.iter().map(|&(alias, _)| alias));
out.extend(NumericWidth::ALL.iter().map(|w| ClosedSet::label(*w)));
out
}
pub fn install_type_check<H: 'static>(interp: &mut crate::eval::Interpreter<H>) {
use crate::ffi::Arity;
interp.register_fn(
"type-of",
Arity::Exact(1),
|args: &[Value], _h: &mut H, _sp| {
let kw = type_keyword_of(&args[0]);
Ok(Value::Keyword(Arc::from(kw)))
},
);
interp.register_fn(
"is?",
Arity::Exact(2),
|args: &[Value], _h: &mut H, sp: Span| match check_value(&args[0], &args[1], sp) {
Ok(()) => Ok(Value::Bool(true)),
Err(EvalError::User { .. }) => Ok(Value::Bool(false)),
Err(other) => Err(other),
},
);
interp.register_fn(
"the",
Arity::Exact(2),
|args: &[Value], _h: &mut H, sp: Span| {
check_value(&args[1], &args[0], sp)?;
Ok(args[1].clone())
},
);
interp.register_fn(
"cast",
Arity::Exact(2),
|args: &[Value], _h: &mut H, sp: Span| coerce_value(&args[1], &args[0], sp),
);
}
pub(crate) fn numeric_width_of(name: &str) -> Option<NumericWidth> {
NumericWidth::find_by_label(name).or_else(|| width_alias_of(name))
}
fn width_alias_of(name: &str) -> Option<NumericWidth> {
WIDTH_ALIASES
.iter()
.find(|(alias, _)| *alias == name)
.map(|&(_, width)| width)
}
pub(crate) fn is_width_alias(name: &str) -> bool {
width_alias_of(name).is_some()
}
fn is_numeric_width_keyword(name: &str) -> bool {
NumericWidth::contains_label(name) || is_width_alias(name)
}
fn wide_literal_on(value: &Value, axis: NumericAxis) -> Option<NumericLiteral> {
match (axis, value) {
(NumericAxis::Int, Value::Int(n)) => Some(NumericLiteral::Int(*n)),
(NumericAxis::Float, Value::Float(x)) => Some(NumericLiteral::Float(*x)),
(NumericAxis::Float, Value::Int(n)) =>
{
#[allow(clippy::cast_precision_loss)]
Some(NumericLiteral::Float(*n as f64))
}
_ => None,
}
}
fn value_of_literal(lit: NumericLiteral) -> Value {
match lit {
NumericLiteral::Int(n) => Value::Int(n),
NumericLiteral::Float(x) => Value::Float(x),
}
}
pub fn coerce_value(value: &Value, ty: &Value, span: Span) -> Result<Value> {
let width = match ty {
Value::Keyword(name) => numeric_width_of(name),
_ => None,
};
let Some(width) = width else {
check_value(value, ty, span)?;
return Ok(value.clone());
};
let Some(wide) = wide_literal_on(value, width.axis()) else {
check_value(value, ty, span)?;
return Ok(value.clone());
};
width
.narrow_literal(wide)
.map(value_of_literal)
.ok_or_else(|| out_of_range(width, wide, value, span))
}
fn out_of_range(
target: NumericWidth,
literal: NumericLiteral,
original: &Value,
span: Span,
) -> EvalError {
let msg = format!("{literal} is out of range for {target}");
EvalError::User {
value: Value::Error(Arc::new(ErrorObj {
tag: Arc::from("out-of-range"),
message: Arc::from(msg),
data: vec![
(
Value::Keyword(Arc::from("target")),
Value::Keyword(Arc::from(target.label())),
),
(Value::Keyword(Arc::from("value")), original.clone()),
],
})),
at: span,
}
}
pub fn type_keyword_of(v: &Value) -> &'static str {
match v {
Value::Nil => "nil",
Value::Bool(_) => "bool",
Value::Int(_) => "int",
Value::Float(_) => "float",
Value::Str(_) => "string",
Value::Symbol(_) => "symbol",
Value::Keyword(_) => "keyword",
Value::List(_) => "list",
Value::Map(_) => "map",
Value::Closure(_) | Value::NativeFn(_) => "procedure",
Value::Promise(_) => "promise",
Value::Error(_) => "error",
Value::Sexp(..) => "sexp",
Value::Foreign(_) => "foreign",
}
}
pub fn check_value(value: &Value, ty: &Value, span: Span) -> Result<()> {
if matches_type(value, ty)? {
Ok(())
} else {
let expected = render_type(ty);
let actual = type_keyword_of(value);
let msg = format!("expected {expected}, got :{actual}");
Err(EvalError::User {
value: Value::Error(Arc::new(ErrorObj {
tag: Arc::from("type-mismatch"),
message: Arc::from(msg),
data: vec![
(Value::Keyword(Arc::from("expected")), ty.clone()),
(
Value::Keyword(Arc::from("got")),
Value::Keyword(Arc::from(actual)),
),
],
})),
at: span,
})
}
}
fn matches_type(value: &Value, ty: &Value) -> Result<bool> {
match ty {
Value::Keyword(name) => Ok(match_atomic_keyword(value, name)),
Value::List(items) if !items.is_empty() => {
let head = match &items[0] {
Value::Keyword(k) => k.as_ref(),
_ => {
return Err(EvalError::native_fn(
Arc::<str>::from("type-check"),
"type spec list must start with a keyword",
Span::synthetic(),
));
}
};
match head {
"list-of" => match_list_of(value, items),
"map-of" => match_map_of(value, items),
"fn" => Ok(matches!(value, Value::Closure(_) | Value::NativeFn(_))),
"union" => match_union(value, items),
other => Err(EvalError::native_fn(
Arc::<str>::from("type-check"),
format!("unknown parametric type: {other}"),
Span::synthetic(),
)),
}
}
_ => Err(EvalError::native_fn(
Arc::<str>::from("type-check"),
format!("type spec must be a keyword or list, got {ty}"),
Span::synthetic(),
)),
}
}
fn match_atomic_keyword(value: &Value, name: &str) -> bool {
match name {
"any" => true,
"nil" => matches!(value, Value::Nil),
"bool" => matches!(value, Value::Bool(_)),
"number" => matches!(value, Value::Int(_) | Value::Float(_)),
"string" => matches!(value, Value::Str(_)),
"symbol" => matches!(value, Value::Symbol(_)),
"keyword" => matches!(value, Value::Keyword(_)),
"list" => matches!(value, Value::List(_) | Value::Nil),
"map" => matches!(value, Value::Map(_)),
"error" => matches!(value, Value::Error(_)),
"promise" => matches!(value, Value::Promise(_)),
"procedure" => matches!(value, Value::Closure(_) | Value::NativeFn(_)),
"foreign" => matches!(value, Value::Foreign(_)),
other => numeric_width_of(other).is_some_and(|w| {
wide_literal_on(value, w.axis()).is_some_and(|lit| w.narrow_literal(lit).is_some())
}),
}
}
fn match_list_of(value: &Value, items: &[Value]) -> Result<bool> {
if items.len() != 2 {
return Err(EvalError::native_fn(
Arc::<str>::from("type-check"),
"(:list-of T) takes exactly one type argument",
Span::synthetic(),
));
}
let element_ty = &items[1];
let xs = match value {
Value::Nil => return Ok(true),
Value::List(xs) => xs.as_ref(),
_ => return Ok(false),
};
for x in xs {
if !matches_type(x, element_ty)? {
return Ok(false);
}
}
Ok(true)
}
fn match_map_of(value: &Value, items: &[Value]) -> Result<bool> {
if items.len() != 3 {
return Err(EvalError::native_fn(
Arc::<str>::from("type-check"),
"(:map-of K V) takes exactly two type arguments",
Span::synthetic(),
));
}
let key_ty = &items[1];
let val_ty = &items[2];
let m = match value {
Value::Map(m) => m,
_ => return Ok(false),
};
for (k, v) in m.iter() {
if !matches_type(&k.to_value(), key_ty)? {
return Ok(false);
}
if !matches_type(v, val_ty)? {
return Ok(false);
}
}
Ok(true)
}
fn match_union(value: &Value, items: &[Value]) -> Result<bool> {
for branch in &items[1..] {
if matches_type(value, branch)? {
return Ok(true);
}
}
Ok(false)
}
pub fn render_type(ty: &Value) -> String {
match ty {
Value::Keyword(k) => format!(":{k}"),
Value::List(items) => {
let mut parts = Vec::with_capacity(items.len());
for item in items.iter() {
parts.push(render_type(item));
}
format!("({})", parts.join(" "))
}
other => format!("{other}"),
}
}
pub fn is_type_keyword(name: &str) -> bool {
NON_NUMERIC_ATOMIC_TYPES.contains(&name)
|| PARAMETRIC.contains(&name)
|| is_numeric_width_keyword(name)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::install_full_stdlib_with;
use crate::Interpreter;
use tatara_lisp::read_spanned;
struct NoHost;
fn run(src: &str) -> Value {
let mut i: Interpreter<NoHost> = Interpreter::new();
install_full_stdlib_with(&mut i, &mut NoHost);
install_type_check(&mut i);
let forms = read_spanned(src).unwrap();
i.eval_program(&forms, &mut NoHost).unwrap()
}
fn run_err(src: &str) -> EvalError {
let mut i: Interpreter<NoHost> = Interpreter::new();
install_full_stdlib_with(&mut i, &mut NoHost);
install_type_check(&mut i);
let forms = read_spanned(src).unwrap();
i.eval_program(&forms, &mut NoHost).unwrap_err()
}
fn try_run(src: &str) -> Result<Value> {
let mut i: Interpreter<NoHost> = Interpreter::new();
install_full_stdlib_with(&mut i, &mut NoHost);
install_type_check(&mut i);
let forms = read_spanned(src).unwrap();
i.eval_program(&forms, &mut NoHost)
}
const WITNESSES: &[&str] = &[
"7",
"-1",
"0",
"3.5",
"0.1",
"4294967296",
"2147483648",
"-9223372036854775808",
"1.0e300",
"\"x\"",
"#t",
"(list 1 2)",
"(hash-map :a 1)",
"(quote sym)",
":kw",
"(lambda (x) x)",
];
#[test]
fn type_of_returns_kind_keyword() {
assert_eq!(format!("{}", run("(type-of 42)")), ":int");
assert_eq!(format!("{}", run("(type-of 3.14)")), ":float");
assert_eq!(format!("{}", run("(type-of #t)")), ":bool");
assert_eq!(format!("{}", run("(type-of \"hi\")")), ":string");
assert_eq!(format!("{}", run("(type-of (list 1 2))")), ":list");
assert_eq!(format!("{}", run("(type-of (hash-map :a 1))")), ":map");
}
#[test]
fn the_passes_through_when_matched() {
assert!(matches!(run("(the :int 42)"), Value::Int(42)));
assert!(matches!(run("(the :string \"hi\")"), Value::Str(_)));
assert!(matches!(run("(the :any 99)"), Value::Int(99)));
}
#[test]
fn the_raises_on_mismatch() {
let err = run_err("(the :int \"not an int\")");
match err {
EvalError::User { value, .. } => match value {
Value::Error(e) => {
assert_eq!(&*e.tag, "type-mismatch");
assert!(e.message.contains(":int"));
assert!(e.message.contains(":string"));
}
other => panic!("{other:?}"),
},
other => panic!("{other:?}"),
}
}
#[test]
fn is_predicate_is_total() {
assert!(matches!(run("(is? 42 :int)"), Value::Bool(true)));
assert!(matches!(run("(is? 42 :string)"), Value::Bool(false)));
assert!(matches!(run("(is? 42 :any)"), Value::Bool(true)));
}
#[test]
fn list_of_int_match() {
assert!(matches!(
run("(is? (list 1 2 3) (list :list-of :int))"),
Value::Bool(true)
));
assert!(matches!(
run("(is? (list 1 \"x\" 3) (list :list-of :int))"),
Value::Bool(false)
));
assert!(matches!(
run("(is? (list) (list :list-of :int))"),
Value::Bool(true)
));
}
#[test]
fn map_of_keyword_int_match() {
assert!(matches!(
run("(is? (hash-map :a 1 :b 2) (list :map-of :keyword :int))"),
Value::Bool(true)
));
assert!(matches!(
run("(is? (hash-map :a \"x\") (list :map-of :keyword :int))"),
Value::Bool(false)
));
}
#[test]
fn union_admits_any_branch() {
let v = run("(is? 42 (list :union :string :int))");
assert!(matches!(v, Value::Bool(true)));
let v = run("(is? \"x\" (list :union :string :int))");
assert!(matches!(v, Value::Bool(true)));
let v = run("(is? #t (list :union :string :int))");
assert!(matches!(v, Value::Bool(false)));
}
#[test]
fn number_admits_int_or_float() {
assert!(matches!(run("(is? 42 :number)"), Value::Bool(true)));
assert!(matches!(run("(is? 3.14 :number)"), Value::Bool(true)));
assert!(matches!(run("(is? \"x\" :number)"), Value::Bool(false)));
}
#[test]
fn fn_type_admits_any_procedure() {
assert!(matches!(
run("(is? (lambda (x) x) (list :fn (list :int) (quote ->) :int))"),
Value::Bool(true)
));
assert!(matches!(
run("(is? + (list :fn (list :int) (quote ->) :int))"),
Value::Bool(true)
));
assert!(matches!(
run("(is? 42 (list :fn (list :int) (quote ->) :int))"),
Value::Bool(false)
));
}
#[test]
fn the_inside_an_expression_round_trips() {
let v = run("(+ 1 (the :int 2) 3)");
assert!(matches!(v, Value::Int(6)));
}
#[test]
fn nested_list_of_list_of_int() {
assert!(matches!(
run("(is? (list (list 1 2) (list 3)) (list :list-of (list :list-of :int)))"),
Value::Bool(true)
));
}
fn cast_err(src: &str) -> Arc<ErrorObj> {
match run_err(src) {
EvalError::User {
value: Value::Error(e),
..
} => e,
other => panic!("expected a typed Value::Error from cast, got {other:?}"),
}
}
#[test]
fn cast_returns_the_value_when_it_fits() {
assert!(matches!(run("(cast :int 42)"), Value::Int(42)));
assert!(matches!(run("(cast :u32 8080)"), Value::Int(8080)));
assert!(matches!(run("(cast :i32 -42)"), Value::Int(-42)));
assert!(matches!(
run("(cast :u32 4294967295)"),
Value::Int(4_294_967_295)
));
assert!(matches!(run("(+ 1 (cast :u32 2) 3)"), Value::Int(6)));
}
#[test]
fn cast_on_a_non_numeric_type_behaves_as_the_assertion() {
assert!(matches!(run("(cast :string \"hi\")"), Value::Str(_)));
assert!(matches!(run("(cast :any 99)"), Value::Int(99)));
assert!(matches!(
run("(cast (list :list-of :int) (list 1 2 3))"),
Value::List(_)
));
let e = cast_err("(cast :string 42)");
assert_eq!(&*e.tag, "type-mismatch");
}
#[test]
fn cast_off_the_targets_axis_is_a_shape_mismatch_not_a_range_one() {
for src in [
"(cast :u32 \"8080\")",
"(cast :u32 3.5)",
"(cast :i32 #t)",
"(cast :f32 \"7\")",
"(cast :f32 #t)",
] {
let e = cast_err(src);
assert_eq!(&*e.tag, "type-mismatch", "{src} named the wrong gate");
}
}
#[test]
fn cast_copies_the_readers_int_into_float_widening() {
assert!(matches!(run("(cast :f32 7)"), Value::Float(x) if (x - 7.0).abs() < f64::EPSILON));
assert!(matches!(run("(cast :f64 7)"), Value::Float(x) if (x - 7.0).abs() < f64::EPSILON));
assert!(
matches!(run("(cast :float 7)"), Value::Float(x) if (x - 7.0).abs() < f64::EPSILON)
);
assert_eq!(&*cast_err("(cast :u32 3.5)").tag, "type-mismatch");
assert_eq!(&*cast_err("(cast :int 3.5)").tag, "type-mismatch");
}
#[test]
fn cast_rejects_every_value_the_derive_rejects() {
for (src, width) in [
("(cast :u32 4294967296)", "u32"), ("(cast :u32 -1)", "u32"), ("(cast :i32 2147483648)", "i32"), ("(cast :f32 1.0e300)", "f32"), ] {
let e = cast_err(src);
assert_eq!(&*e.tag, "out-of-range", "{src} named the wrong gate");
assert!(
e.message.contains(width),
"{src} must name the width it failed to fit, got {}",
e.message
);
}
}
#[test]
fn cast_message_is_the_tail_of_the_derive_diagnostic() {
for (src, target, literal) in [
(
"(cast :u32 4294967296)",
NumericWidth::U32,
NumericLiteral::Int(4_294_967_296),
),
("(cast :u32 -1)", NumericWidth::U32, NumericLiteral::Int(-1)),
(
"(cast :i32 2147483648)",
NumericWidth::I32,
NumericLiteral::Int(2_147_483_648),
),
(
"(cast :f32 1.0e300)",
NumericWidth::F32,
NumericLiteral::Float(1.0e300),
),
] {
let derived = tatara_lisp::LispError::KwargOutOfRange {
form: tatara_lisp::KwargPath::named("port"),
target,
value: literal,
}
.to_string();
let cast = cast_err(src).message.to_string();
assert_eq!(
derived,
format!("compile error in :port: {cast}"),
"the caster and the derive disagree about how to say this"
);
}
}
#[test]
fn the_range_rejection_carries_the_width_and_the_value_as_data() {
let e = cast_err("(cast :u32 -1)");
let target = e
.data
.iter()
.find(|(k, _)| matches!(k, Value::Keyword(k) if &**k == "target"))
.map(|(_, v)| format!("{v}"));
let value = e
.data
.iter()
.find(|(k, _)| matches!(k, Value::Keyword(k) if &**k == "value"))
.map(|(_, v)| format!("{v}"));
assert_eq!(target.as_deref(), Some(":u32"));
assert_eq!(value.as_deref(), Some("-1"));
}
#[test]
fn cast_to_the_identity_widths_never_rejects() {
assert!(matches!(
run("(cast :int -9223372036854775808)"),
Value::Int(i64::MIN)
));
assert!(matches!(
run("(cast :i64 -9223372036854775808)"),
Value::Int(i64::MIN)
));
assert!(matches!(run("(cast :f64 1.0e300)"), Value::Float(_)));
assert!(matches!(run("(cast :float 1.0e300)"), Value::Float(_)));
}
#[test]
fn cast_to_f32_returns_the_value_at_f32_precision() {
#[allow(clippy::cast_possible_truncation)]
let expected = f64::from(0.1_f64 as f32);
match run("(cast :f32 0.1)") {
Value::Float(x) => {
assert!((x - expected).abs() < f64::EPSILON, "got {x}");
assert_ne!(x, 0.1_f64, "the coercion must be real, not a pass-through");
}
other => panic!("{other:?}"),
}
}
#[test]
fn a_failing_cast_is_catchable_lisp_data() {
let v = run("(try
(cast :u32 -1)
(catch (e) (error-tag e)))");
assert!(matches!(v, Value::Keyword(s) if &*s == "out-of-range"));
}
#[test]
fn the_width_predicate_agrees_with_the_caster() {
assert!(matches!(run("(is? 8080 :u32)"), Value::Bool(true)));
assert!(matches!(run("(is? -1 :u32)"), Value::Bool(false)));
assert!(matches!(run("(is? 4294967296 :u32)"), Value::Bool(false)));
assert!(matches!(run("(is? 2147483648 :i32)"), Value::Bool(false)));
assert!(matches!(run("(is? 1.0e300 :f32)"), Value::Bool(false)));
assert!(matches!(run("(is? 2.5 :f32)"), Value::Bool(true)));
assert!(matches!(run("(is? 7 :f32)"), Value::Bool(true)));
assert!(matches!(run("(is? 3.5 :u32)"), Value::Bool(false)));
}
#[test]
fn every_closed_set_width_is_reachable_as_a_type_keyword() {
for w in NumericWidth::ALL {
assert_eq!(
numeric_width_of(w.label()),
Some(w),
"{w} is in the closed set but not reachable from lisp"
);
assert!(is_type_keyword(w.label()));
}
assert_eq!(numeric_width_of("i8"), None, "i8 is not in the closed set");
assert_eq!(numeric_width_of("u16"), None);
assert_eq!(numeric_width_of("nonsense"), None);
}
#[test]
fn defn_typed_passes_when_args_match() {
let v = run("(defn-typed greet ((name :string) (count :int)) -> :string
(string-append \"hi \" name))
(greet \"luis\" 5)");
assert_eq!(format!("{v}"), "\"hi luis\"");
}
#[test]
fn defn_typed_raises_on_arg_mismatch() {
let err = run_err(
"(defn-typed double-it ((n :int)) -> :int (* n 2))
(double-it \"oops\")",
);
match err {
EvalError::User { value, .. } => match value {
Value::Error(e) => assert_eq!(&*e.tag, "type-mismatch"),
other => panic!("{other:?}"),
},
other => panic!("{other:?}"),
}
}
#[test]
fn defn_typed_raises_on_return_mismatch() {
let err = run_err(
"(defn-typed wrong ((n :int)) -> :string (* n 2))
(wrong 5)",
);
match err {
EvalError::User { value, .. } => match value {
Value::Error(e) => assert_eq!(&*e.tag, "type-mismatch"),
other => panic!("{other:?}"),
},
other => panic!("{other:?}"),
}
}
#[test]
fn every_type_keyword_agrees_between_the_predicate_and_the_caster() {
let mut disagreements = Vec::new();
for kw in atomic_type_keywords() {
for witness in WITNESSES {
let predicate = matches!(
try_run(&format!("(is? {witness} :{kw})")),
Ok(Value::Bool(true))
);
let caster = try_run(&format!("(cast :{kw} {witness})")).is_ok();
if predicate != caster {
disagreements.push(format!(
" (is? {witness} :{kw}) = {predicate} but (cast :{kw} {witness}) \
accepted = {caster}"
));
}
}
}
assert!(
disagreements.is_empty(),
"the predicate must describe the caster exactly — {} disagreement(s):\n{}",
disagreements.len(),
disagreements.join("\n")
);
}
#[test]
fn every_width_alias_behaves_exactly_like_the_width_it_aliases() {
let mut drift = Vec::new();
for (alias, width) in WIDTH_ALIASES {
assert_eq!(
numeric_width_of(alias),
Some(width),
":{alias} must decode to the width it documents"
);
let canonical = ClosedSet::label(width);
for witness in WITNESSES {
let alias_is = matches!(
try_run(&format!("(is? {witness} :{alias})")),
Ok(Value::Bool(true))
);
let canonical_is = matches!(
try_run(&format!("(is? {witness} :{canonical})")),
Ok(Value::Bool(true))
);
let alias_cast = try_run(&format!("(cast :{alias} {witness})")).is_ok();
let canonical_cast = try_run(&format!("(cast :{canonical} {witness})")).is_ok();
if alias_is != canonical_is {
drift.push(format!(
" (is? {witness} :{alias}) = {alias_is} but \
(is? {witness} :{canonical}) = {canonical_is}"
));
}
if alias_cast != canonical_cast {
drift.push(format!(
" (cast :{alias} {witness}) accepted = {alias_cast} but \
(cast :{canonical} {witness}) accepted = {canonical_cast}"
));
}
}
}
assert!(
drift.is_empty(),
"an alias diverged from the width it aliases — {} case(s):\n{}",
drift.len(),
drift.join("\n")
);
}
#[test]
fn every_runtime_type_keyword_is_a_build_check_type_spec() {
let mut unparsed = Vec::new();
for kw in atomic_type_keywords() {
let src = format!(":{kw}");
let forms = read_spanned(&src).unwrap();
if crate::build_check::StaticType::from_spanned(&forms[0]).is_none() {
unparsed.push(src);
}
}
assert!(
unparsed.is_empty(),
"the runtime accepts {} keyword(s) the build checker calls a bad type spec: {}",
unparsed.len(),
unparsed.join(" ")
);
}
#[test]
fn the_build_checker_never_flags_an_annotation_the_runtime_accepts() {
let mut false_positives = Vec::new();
for kw in atomic_type_keywords() {
for witness in WITNESSES {
let src = format!("(the :{kw} {witness})");
if try_run(&src).is_err() {
continue;
}
let forms = read_spanned(&src).unwrap();
for d in crate::build_check::check_program(&forms) {
false_positives.push(format!(" {src} → {}", d.render(&src)));
}
}
}
assert!(
false_positives.is_empty(),
"the build checker flagged {} annotation(s) the runtime accepts:\n{}",
false_positives.len(),
false_positives.join("\n")
);
}
#[test]
fn the_type_keyword_vocabulary_is_swept_not_hand_listed() {
let vocabulary = atomic_type_keywords();
for width in NumericWidth::ALL {
let label = ClosedSet::label(width);
assert!(
vocabulary.contains(&label),
"{label} is in the closed set but not in the type vocabulary"
);
assert!(is_type_keyword(label), "{label} must be a type keyword");
}
for (alias, _) in WIDTH_ALIASES {
assert!(
vocabulary.contains(&alias),
":{alias} is not in the vocabulary"
);
assert!(is_type_keyword(alias), ":{alias} must be a type keyword");
}
for outsider in ["i8", "u16", "i128", "nonsense", ""] {
assert!(
!is_type_keyword(outsider),
":{outsider} is not in the closed set and must not be a type keyword"
);
}
let mut sorted = vocabulary.clone();
sorted.sort_unstable();
let mut deduped = sorted.clone();
deduped.dedup();
assert_eq!(
sorted, deduped,
"a keyword is spelled in two tables — that duplication IS the defect class"
);
}
}