use thiserror::Error;
pub type Result<T> = std::result::Result<T, LispError>;
const _: () = crate::ast::assert_str_array_pairwise_distinct(&CompilerSpecIoStage::LABELS);
const _: () =
crate::ast::assert_str_array_pairwise_distinct(&TemplateInvariantKind::STATIC_MESSAGES);
const _: () =
crate::ast::assert_str_array_pairwise_distinct(&TemplateInvariantKind::DYNAMIC_DESCRIPTORS);
const _: () = crate::ast::assert_str_array_pairwise_distinct(&MacroDefHead::KEYWORDS);
const _: () =
crate::ast::assert_str_array_pairwise_distinct(&OptionalParamMalformedReason::STATIC_LABELS);
const _: () = crate::ast::assert_str_array_pairwise_distinct(
&OptionalParamMalformedReason::DYNAMIC_DESCRIPTORS,
);
const _: () = crate::ast::assert_str_array_pairwise_distinct(&UnquoteForm::MARKERS);
const _: () = crate::ast::assert_str_array_pairwise_distinct(&UnquoteForm::IAC_FORGE_TAGS);
const _: () = crate::ast::assert_str_array_pairwise_distinct(&UnquoteForm::LABELS);
const _: () = crate::ast::assert_str_array_pairwise_distinct(&KwargPathKind::LABELS);
const _: () = crate::ast::assert_str_array_pairwise_distinct(&ExpectedKwargShape::LABELS);
const _: () = crate::ast::assert_str_array_pairwise_distinct(&SexpShape::LABELS);
const _: () = crate::ast::assert_str_array_pairwise_distinct(&StructuralKind::LABELS);
const _: () = crate::ast::assert_str_array_all_nonempty(&CompilerSpecIoStage::LABELS);
const _: () = crate::ast::assert_str_array_all_nonempty(&TemplateInvariantKind::STATIC_MESSAGES);
const _: () =
crate::ast::assert_str_array_all_nonempty(&TemplateInvariantKind::DYNAMIC_DESCRIPTORS);
const _: () = crate::ast::assert_str_array_all_nonempty(&MacroDefHead::KEYWORDS);
const _: () =
crate::ast::assert_str_array_all_nonempty(&OptionalParamMalformedReason::STATIC_LABELS);
const _: () =
crate::ast::assert_str_array_all_nonempty(&OptionalParamMalformedReason::DYNAMIC_DESCRIPTORS);
const _: () = crate::ast::assert_str_array_all_nonempty(&UnquoteForm::MARKERS);
const _: () = crate::ast::assert_str_array_all_nonempty(&UnquoteForm::IAC_FORGE_TAGS);
const _: () = crate::ast::assert_str_array_all_nonempty(&UnquoteForm::LABELS);
const _: () = crate::ast::assert_str_array_all_nonempty(&KwargPathKind::LABELS);
const _: () = crate::ast::assert_str_array_all_nonempty(&ExpectedKwargShape::LABELS);
const _: () = crate::ast::assert_str_array_all_nonempty(&SexpShape::LABELS);
const _: () = crate::ast::assert_str_array_all_nonempty(&StructuralKind::LABELS);
const _: () = crate::ast::assert_str_array_all_ascii(&CompilerSpecIoStage::LABELS);
const _: () = crate::ast::assert_str_array_all_ascii(&TemplateInvariantKind::STATIC_MESSAGES);
const _: () = crate::ast::assert_str_array_all_ascii(&TemplateInvariantKind::DYNAMIC_DESCRIPTORS);
const _: () = crate::ast::assert_str_array_all_ascii(&MacroDefHead::KEYWORDS);
const _: () = crate::ast::assert_str_array_all_ascii(&OptionalParamMalformedReason::STATIC_LABELS);
const _: () =
crate::ast::assert_str_array_all_ascii(&OptionalParamMalformedReason::DYNAMIC_DESCRIPTORS);
const _: () = crate::ast::assert_str_array_all_ascii(&UnquoteForm::MARKERS);
const _: () = crate::ast::assert_str_array_all_ascii(&UnquoteForm::IAC_FORGE_TAGS);
const _: () = crate::ast::assert_str_array_all_ascii(&UnquoteForm::LABELS);
const _: () = crate::ast::assert_str_array_all_ascii(&KwargPathKind::LABELS);
const _: () = crate::ast::assert_str_array_all_ascii(&ExpectedKwargShape::LABELS);
const _: () = crate::ast::assert_str_array_all_ascii(&SexpShape::LABELS);
const _: () = crate::ast::assert_str_array_all_ascii(&StructuralKind::LABELS);
const _: () = crate::ast::assert_str_array_within_str_finite_set::<6, 12>(
&crate::ast::AtomKind::LABELS,
&SexpShape::LABELS,
);
const _: () = crate::ast::assert_str_array_within_str_finite_set::<4, 12>(
&crate::ast::QuoteForm::LABELS,
&SexpShape::LABELS,
);
const _: () = crate::ast::assert_str_array_within_str_finite_set::<2, 12>(
&StructuralKind::LABELS,
&SexpShape::LABELS,
);
const _: () = crate::ast::assert_str_arrays_disjoint::<6, 2>(
&crate::ast::AtomKind::LABELS,
&StructuralKind::LABELS,
);
const _: () = crate::ast::assert_str_arrays_disjoint::<4, 2>(
&crate::ast::QuoteForm::LABELS,
&StructuralKind::LABELS,
);
const _: () = crate::ast::assert_str_finite_set_covered_by_three_str_arrays::<6, 4, 2, 12>(
&crate::ast::AtomKind::LABELS,
&crate::ast::QuoteForm::LABELS,
&StructuralKind::LABELS,
&SexpShape::LABELS,
);
const _: () = crate::ast::assert_str_array_slice_equals_str_array::<12, 1, 0>(
&SexpShape::LABELS,
&[StructuralKind::NIL_LABEL],
);
const _: () = crate::ast::assert_str_array_slice_equals_str_array::<12, 6, 1>(
&SexpShape::LABELS,
&crate::ast::AtomKind::LABELS,
);
const _: () = crate::ast::assert_str_array_slice_equals_str_array::<12, 1, 7>(
&SexpShape::LABELS,
&[StructuralKind::LIST_LABEL],
);
const _: () = crate::ast::assert_str_array_slice_equals_str_array::<12, 4, 8>(
&SexpShape::LABELS,
&crate::ast::QuoteForm::LABELS,
);
const _: () = crate::ast::assert_str_array_within_str_finite_set::<2, 4>(
&UnquoteForm::LABELS,
&crate::ast::QuoteForm::LABELS,
);
const _: () = crate::ast::assert_str_array_within_str_finite_set::<2, 4>(
&UnquoteForm::MARKERS,
&crate::ast::QuoteForm::PREFIXES,
);
const _: () = crate::ast::assert_str_array_within_str_finite_set::<2, 4>(
&UnquoteForm::IAC_FORGE_TAGS,
&crate::ast::QuoteForm::IAC_FORGE_TAGS,
);
const _: () = crate::ast::assert_str_array_slice_equals_str_array::<4, 2, 2>(
&crate::ast::QuoteForm::LABELS,
&UnquoteForm::LABELS,
);
const _: () = crate::ast::assert_str_array_slice_equals_str_array::<4, 2, 2>(
&crate::ast::QuoteForm::PREFIXES,
&UnquoteForm::MARKERS,
);
const _: () = crate::ast::assert_str_array_slice_equals_str_array::<4, 2, 2>(
&crate::ast::QuoteForm::IAC_FORGE_TAGS,
&UnquoteForm::IAC_FORGE_TAGS,
);
const _: () = crate::ast::assert_str_arrays_disjoint::<2, 2>(
&TemplateInvariantKind::STATIC_MESSAGES,
&TemplateInvariantKind::DYNAMIC_DESCRIPTORS,
);
const _: () = crate::ast::assert_str_arrays_disjoint::<3, 1>(
&OptionalParamMalformedReason::STATIC_LABELS,
&OptionalParamMalformedReason::DYNAMIC_DESCRIPTORS,
);
const _: () = crate::ast::assert_str_array_is_concatenation_of_two_scalar_replicas::<4, 2>(
&CompilerSpecIoStage::OPERATIONS,
CompilerSpecIoStage::REALIZE_TO_DISK_OPERATION,
CompilerSpecIoStage::LOAD_FROM_DISK_OPERATION,
);
const _: () = crate::ast::assert_str_array_slice_equals_str_array::<7, 7, 0>(
&ExpectedKwargShape::LABELS,
&[
"keyword",
"string",
"int",
"number",
"bool",
"list",
"list of strings",
],
);
const _: () = crate::ast::assert_str_array_slice_equals_str_array::<3, 3, 0>(
&KwargPathKind::LABELS,
&["named", "item", "slot"],
);
const _: () = crate::ast::assert_str_array_slice_equals_str_array::<3, 3, 0>(
&MacroDefHead::KEYWORDS,
&["defmacro", "defpoint-template", "defcheck"],
);
const _: () = crate::ast::assert_str_array_slice_equals_str_array::<4, 4, 0>(
&CompilerSpecIoStage::LABELS,
&["serialize", "write", "read", "deserialize"],
);
const _: () = crate::ast::assert_str_array_slice_equals_str_array::<2, 2, 0>(
&StructuralKind::LABELS,
&["nil", "list"],
);
#[derive(Debug, Error)]
pub enum LispError {
#[error("unexpected character {0:?} at position {1}")]
UnexpectedChar(char, usize),
#[error("unterminated string literal at position {0}")]
UnterminatedString(usize),
#[error("unmatched closing paren at position {pos}")]
UnmatchedParen { pos: usize },
#[error("unmatched opening paren at position {pos}")]
UnmatchedOpenParen { pos: usize },
#[error("unexpected end of input at position {pos}")]
Eof { pos: usize },
#[error("invalid number literal {0:?}")]
InvalidNumber(String),
#[error("unknown symbol: {0}")]
UnknownSymbol(String),
#[error("type error: expected {expected}, got {got}")]
Type { expected: &'static str, got: String },
#[error("compile error in {form}: {message}")]
Compile { form: String, message: String },
#[error("compile error in {form}: expected {expected}, got {got}")]
TypeMismatch {
form: KwargPath,
expected: ExpectedKwargShape,
got: SexpShape,
},
#[error("compile error in {keyword}: expected ({keyword} ...), got ({got} ...)")]
HeadMismatch { keyword: &'static str, got: String },
#[error("unknown {category}: {value}")]
Unknown {
category: &'static str,
value: String,
},
#[error("missing required field: {0}")]
Missing(&'static str),
#[error("odd keyword arguments: dangling element `{dangling}`")]
OddKwargs { dangling: String },
#[error(
"compile error in {prefix}{name}: unbound{}",
unbound_hint_suffix(*prefix, hint.as_deref())
)]
UnboundTemplateVar {
prefix: UnquoteForm,
name: String,
hint: Option<String>,
},
#[error("compile error in :{key}: duplicate keyword")]
DuplicateKwarg { key: String },
#[error("compile error in :{key}: required but not provided")]
MissingKwarg { key: String },
#[error(
"compile error in :{key}: unknown keyword{}",
unknown_kwarg_suffix(hint.as_deref(), allowed)
)]
UnknownKwarg {
key: String,
hint: Option<String>,
allowed: Vec<String>,
},
#[error(
"unknown domain keyword: ({keyword} ...){}",
unknown_domain_keyword_suffix(hint.as_deref(), registered)
)]
UnknownDomainKeyword {
keyword: String,
hint: Option<String>,
registered: Vec<String>,
},
#[error("compile error in {prefix}: expected symbol, got {got}")]
NonSymbolUnquoteTarget {
prefix: UnquoteForm,
got: SexpWitness,
},
#[error("compile error in ,@: `,@` may only appear inside a list (got ,@{got})")]
SpliceOutsideList { got: SexpWitness },
#[error("compile error in call to {macro_name}: missing required arg: {param}")]
MissingMacroArg { macro_name: String, param: String },
#[error(
"compile error in call to {macro_name}: too many args: \
expected at most {expected}, got {got}"
)]
TooManyMacroArgs {
macro_name: String,
expected: usize,
got: usize,
},
#[error(
"compile error in defmacro params: expected symbol at position \
{position}, got {got}"
)]
NonSymbolParam { position: usize, got: SexpWitness },
#[error(
"compile error in defmacro params: &rest needs a name{}",
rest_param_missing_name_suffix(*rest_position, got.as_ref().map(|w| w.display.as_str()))
)]
RestParamMissingName {
rest_position: usize,
got: Option<SexpWitness>,
},
#[error(
"compile error in defmacro params: &rest name must be last{}",
rest_param_trailing_tokens_suffix(*rest_position, *extra, &first.display)
)]
RestParamTrailingTokens {
rest_position: usize,
extra: usize,
first: SexpWitness,
},
#[error(
"compile error in defmacro params: &optional may appear at most once{}",
optional_marker_repeated_suffix(*first_position, *second_position)
)]
OptionalMarkerRepeated {
first_position: usize,
second_position: usize,
},
#[error(
"compile error in defmacro params: malformed &optional spec, got {got}{}",
optional_param_malformed_suffix(*position, *reason)
)]
OptionalParamMalformed {
position: usize,
got: SexpWitness,
reason: OptionalParamMalformedReason,
},
#[error(
"compile error in {head}: (defmacro name (params) body) required \
(got {arity} elements, need 4)"
)]
DefmacroArity { head: MacroDefHead, arity: usize },
#[error("compile error in {head}: expected name symbol, got {got}")]
DefmacroNonSymbolName {
head: MacroDefHead,
got: SexpWitness,
},
#[error("compile error in {head}: expected param list, got {got}")]
DefmacroNonListParams {
head: MacroDefHead,
got: SexpWitness,
},
#[error("compile error in {keyword}: expected list form")]
NotAListForm { keyword: &'static str },
#[error(
"compile error in {keyword}: missing head symbol{}",
missing_head_symbol_suffix(got.as_ref().map(|w| w.display.as_str()))
)]
MissingHeadSymbol {
keyword: &'static str,
got: Option<SexpWitness>,
},
#[error("compile error in {keyword}: expected ({keyword} NAME …)")]
NamedFormMissingName { keyword: &'static str },
#[error("compile error in {keyword}: positional NAME must be a symbol or string (got {got})")]
NamedFormNonSymbolName {
keyword: &'static str,
got: SexpShape,
},
#[error("compile error in {keyword}: rewriter must return a list; got {got}")]
RewriterNonList {
keyword: &'static str,
got: SexpWitness,
},
#[error("compile error in {keyword}: serialize: {message}")]
DomainSerialize {
keyword: &'static str,
message: String,
},
#[error("compile error in {path}: deserialize: {message}")]
KwargDeserialize { path: KwargPath, message: String },
#[error("compile error in {}: {}: {message}", stage.operation(), stage.label())]
CompilerSpecIo {
stage: CompilerSpecIoStage,
message: String,
},
#[error("compile error in {macro_name}: {}", kind.message())]
TemplateInvariant {
macro_name: String,
kind: TemplateInvariantKind,
},
#[error(
"compile error in call to {macro_name}: macro expansion depth exceeded (limit: {limit})"
)]
ExpansionDepthExceeded { macro_name: String, limit: usize },
#[error(
"compile error in call to {macro_name}: macro expansion output size exceeded (size: {size}, limit: {limit})"
)]
ExpansionSizeExceeded {
macro_name: String,
size: usize,
limit: usize,
},
#[error(
"compile error in defmacro {macro_name}: macro body size exceeded (size: {size}, limit: {limit})"
)]
MacroBodySizeExceeded {
macro_name: String,
size: usize,
limit: usize,
},
#[error(
"compile error in defmacro {macro_name}: registered macros count exceeded (count: {count}, limit: {limit})"
)]
RegisteredMacrosExceeded {
macro_name: String,
count: usize,
limit: usize,
},
#[error(
"compile error in defmacro {macro_name}: macro arity exceeded (arity: {arity}, limit: {limit})"
)]
MacroArityExceeded {
macro_name: String,
arity: usize,
limit: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, tatara_closed_set::DeriveClosedSet)]
#[closed_set(no_from_str, generate_unknown)]
pub enum CompilerSpecIoStage {
RealizeToDiskSerialize,
RealizeToDiskWrite,
LoadFromDiskRead,
LoadFromDiskDeserialize,
}
impl CompilerSpecIoStage {
pub const REALIZE_TO_DISK_OPERATION: &'static str = "realize_to_disk";
pub const LOAD_FROM_DISK_OPERATION: &'static str = "load_from_disk";
pub const OPERATIONS: [&'static str; 4] = [
Self::REALIZE_TO_DISK_OPERATION,
Self::REALIZE_TO_DISK_OPERATION,
Self::LOAD_FROM_DISK_OPERATION,
Self::LOAD_FROM_DISK_OPERATION,
];
#[must_use]
pub fn operation(self) -> &'static str {
match self {
Self::RealizeToDiskSerialize | Self::RealizeToDiskWrite => {
Self::REALIZE_TO_DISK_OPERATION
}
Self::LoadFromDiskRead | Self::LoadFromDiskDeserialize => {
Self::LOAD_FROM_DISK_OPERATION
}
}
}
pub const REALIZE_TO_DISK_SERIALIZE_LABEL: &'static str = "serialize";
pub const REALIZE_TO_DISK_WRITE_LABEL: &'static str = "write";
pub const LOAD_FROM_DISK_READ_LABEL: &'static str = "read";
pub const LOAD_FROM_DISK_DESERIALIZE_LABEL: &'static str = "deserialize";
pub const LABELS: [&'static str; 4] = [
Self::REALIZE_TO_DISK_SERIALIZE_LABEL,
Self::REALIZE_TO_DISK_WRITE_LABEL,
Self::LOAD_FROM_DISK_READ_LABEL,
Self::LOAD_FROM_DISK_DESERIALIZE_LABEL,
];
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::RealizeToDiskSerialize => Self::REALIZE_TO_DISK_SERIALIZE_LABEL,
Self::RealizeToDiskWrite => Self::REALIZE_TO_DISK_WRITE_LABEL,
Self::LoadFromDiskRead => Self::LOAD_FROM_DISK_READ_LABEL,
Self::LoadFromDiskDeserialize => Self::LOAD_FROM_DISK_DESERIALIZE_LABEL,
}
}
pub const ALL: [Self; 4] = [
Self::RealizeToDiskSerialize,
Self::RealizeToDiskWrite,
Self::LoadFromDiskRead,
Self::LoadFromDiskDeserialize,
];
pub const COMPOUND_KEY_SEPARATOR: &'static str = ": ";
}
impl std::fmt::Display for CompilerSpecIoStage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.operation())?;
f.write_str(Self::COMPOUND_KEY_SEPARATOR)?;
f.write_str(self.label())
}
}
impl std::str::FromStr for CompilerSpecIoStage {
type Err = UnknownCompilerSpecIoStage;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let Some((op, lbl)) = s.split_once(Self::COMPOUND_KEY_SEPARATOR) else {
return Err(UnknownCompilerSpecIoStage(s.to_owned()));
};
for stage in Self::ALL {
if op == stage.operation() && lbl == stage.label() {
return Ok(stage);
}
}
Err(UnknownCompilerSpecIoStage(s.to_owned()))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TemplateInvariantKind {
SubstBadIndex(usize),
SpliceBadIndex(usize),
EndListEmptyStack,
FinalNoValue,
}
impl TemplateInvariantKind {
pub const END_LIST_EMPTY_STACK_MESSAGE: &'static str =
"compiled template: EndList with empty stack";
pub const FINAL_NO_VALUE_MESSAGE: &'static str = "compiled template produced no value";
pub const STATIC_MESSAGES: [&'static str; 2] = [
Self::END_LIST_EMPTY_STACK_MESSAGE,
Self::FINAL_NO_VALUE_MESSAGE,
];
pub const SUBST_BAD_INDEX_DESCRIPTOR: &'static str = "param";
pub const SPLICE_BAD_INDEX_DESCRIPTOR: &'static str = "splice";
#[allow(dead_code)]
pub const DYNAMIC_DESCRIPTORS: [&'static str; 2] = [
Self::SUBST_BAD_INDEX_DESCRIPTOR,
Self::SPLICE_BAD_INDEX_DESCRIPTOR,
];
#[must_use]
pub const fn message_static(self) -> Option<&'static str> {
match self {
Self::EndListEmptyStack => Some(Self::END_LIST_EMPTY_STACK_MESSAGE),
Self::FinalNoValue => Some(Self::FINAL_NO_VALUE_MESSAGE),
Self::SubstBadIndex(_) | Self::SpliceBadIndex(_) => None,
}
}
#[must_use]
pub const fn descriptor_dynamic(self) -> Option<&'static str> {
match self {
Self::SubstBadIndex(_) => Some(Self::SUBST_BAD_INDEX_DESCRIPTOR),
Self::SpliceBadIndex(_) => Some(Self::SPLICE_BAD_INDEX_DESCRIPTOR),
Self::EndListEmptyStack | Self::FinalNoValue => None,
}
}
#[must_use]
pub fn message_dynamic(self) -> Option<String> {
let descriptor = self.descriptor_dynamic()?;
let idx = match self {
Self::SubstBadIndex(idx) | Self::SpliceBadIndex(idx) => idx,
Self::EndListEmptyStack | Self::FinalNoValue => {
unreachable!(
"descriptor_dynamic returned Some only for dynamic-arm variants; \
the STATIC-arm match here is unreachable via the ? short-circuit"
)
}
};
Some(format!(
"compiled template referenced bad {descriptor} index {idx}"
))
}
#[must_use]
pub fn message(self) -> String {
self.message_static()
.map(str::to_string)
.or_else(|| self.message_dynamic())
.expect(
"message_static/message_dynamic partition covers the four-arm closed set — \
exactly one projection returns Some for every legal variant, so the None \
branch here is unreachable",
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, tatara_closed_set::DeriveClosedSet)]
#[closed_set(via = "keyword", display, generate_unknown = "macro definition head")]
pub enum MacroDefHead {
Defmacro,
DefpointTemplate,
Defcheck,
}
impl MacroDefHead {
pub const ALL: [Self; 3] = [Self::Defmacro, Self::DefpointTemplate, Self::Defcheck];
pub const DEFMACRO_KEYWORD: &'static str = "defmacro";
pub const DEFPOINT_TEMPLATE_KEYWORD: &'static str = "defpoint-template";
pub const DEFCHECK_KEYWORD: &'static str = "defcheck";
pub const KEYWORDS: [&'static str; 3] = [
Self::DEFMACRO_KEYWORD,
Self::DEFPOINT_TEMPLATE_KEYWORD,
Self::DEFCHECK_KEYWORD,
];
#[must_use]
pub fn from_keyword(head: &str) -> Option<Self> {
head.parse().ok()
}
#[must_use]
pub fn keyword(self) -> &'static str {
match self {
Self::Defmacro => Self::DEFMACRO_KEYWORD,
Self::DefpointTemplate => Self::DEFPOINT_TEMPLATE_KEYWORD,
Self::Defcheck => Self::DEFCHECK_KEYWORD,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OptionalParamMalformedReason {
EmptyList,
MissingDefault,
ExtraElements { length: usize },
NonSymbolName,
}
impl OptionalParamMalformedReason {
pub const EMPTY_LIST_LABEL: &'static str = "empty list";
pub const MISSING_DEFAULT_LABEL: &'static str = "missing default";
pub const NON_SYMBOL_NAME_LABEL: &'static str = "name not a symbol";
#[allow(dead_code)]
pub const STATIC_LABELS: [&'static str; 3] = [
Self::EMPTY_LIST_LABEL,
Self::MISSING_DEFAULT_LABEL,
Self::NON_SYMBOL_NAME_LABEL,
];
pub const OPTIONAL_PARAM_SPEC_ARITY: usize = 2;
#[must_use]
pub fn classify_arity(length: usize) -> Option<Self> {
const ARITY: usize = OptionalParamMalformedReason::OPTIONAL_PARAM_SPEC_ARITY;
match length {
0 => Some(Self::EmptyList),
1 => Some(Self::MissingDefault),
ARITY => None,
length => Some(Self::ExtraElements { length }),
}
}
#[must_use]
pub const fn label_static(self) -> Option<&'static str> {
match self {
Self::EmptyList => Some(Self::EMPTY_LIST_LABEL),
Self::MissingDefault => Some(Self::MISSING_DEFAULT_LABEL),
Self::NonSymbolName => Some(Self::NON_SYMBOL_NAME_LABEL),
Self::ExtraElements { .. } => None,
}
}
pub const EXTRA_ELEMENTS_DESCRIPTOR: &'static str = "elements";
#[allow(dead_code)]
pub const DYNAMIC_DESCRIPTORS: [&'static str; 1] = [Self::EXTRA_ELEMENTS_DESCRIPTOR];
#[must_use]
pub const fn descriptor_dynamic(self) -> Option<&'static str> {
match self {
Self::ExtraElements { .. } => Some(Self::EXTRA_ELEMENTS_DESCRIPTOR),
Self::EmptyList | Self::MissingDefault | Self::NonSymbolName => None,
}
}
#[must_use]
pub fn label_dynamic(self) -> Option<String> {
let descriptor = self.descriptor_dynamic()?;
let length = match self {
Self::ExtraElements { length } => length,
Self::EmptyList | Self::MissingDefault | Self::NonSymbolName => {
unreachable!(
"descriptor_dynamic returned Some only for dynamic-arm variants; \
the STATIC-arm match here is unreachable via the ? short-circuit"
)
}
};
Some(format!(
"{length} {descriptor} (need {})",
Self::OPTIONAL_PARAM_SPEC_ARITY
))
}
#[must_use]
pub fn label(self) -> String {
self.label_static()
.map(str::to_string)
.or_else(|| self.label_dynamic())
.expect(
"label_static ⊕ label_dynamic partition the four-arm closed set; \
exactly one projection returns Some for every legal variant",
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, tatara_closed_set::DeriveClosedSet)]
#[closed_set(via = "marker", display, generate_unknown = "unquote form")]
pub enum UnquoteForm {
Unquote,
Splice,
}
impl UnquoteForm {
pub const ALL: [Self; 2] = [Self::Unquote, Self::Splice];
pub const UNQUOTE_MARKER: &'static str = crate::ast::QuoteForm::UNQUOTE_PREFIX;
pub const SPLICE_MARKER: &'static str = crate::ast::QuoteForm::UNQUOTE_SPLICE_PREFIX;
pub const MARKERS: [&'static str; 2] = [Self::UNQUOTE_MARKER, Self::SPLICE_MARKER];
pub const UNQUOTE_LEAD: char = crate::ast::QuoteForm::UNQUOTE_LEAD;
pub const LEADS: [char; 1] = [Self::UNQUOTE_LEAD];
pub const UNQUOTE_IAC_FORGE_TAG: &'static str = crate::ast::QuoteForm::UNQUOTE_IAC_FORGE_TAG;
pub const SPLICE_IAC_FORGE_TAG: &'static str =
crate::ast::QuoteForm::UNQUOTE_SPLICE_IAC_FORGE_TAG;
pub const IAC_FORGE_TAGS: [&'static str; 2] =
[Self::UNQUOTE_IAC_FORGE_TAG, Self::SPLICE_IAC_FORGE_TAG];
pub const UNQUOTE_LABEL: &'static str = crate::ast::QuoteForm::UNQUOTE_LABEL;
pub const SPLICE_LABEL: &'static str = crate::ast::QuoteForm::UNQUOTE_SPLICE_LABEL;
pub const LABELS: [&'static str; 2] = [Self::UNQUOTE_LABEL, Self::SPLICE_LABEL];
pub(crate) const UNQUOTE_HASH_DISCRIMINATOR: u8 =
crate::ast::QuoteForm::UNQUOTE_HASH_DISCRIMINATOR;
pub(crate) const SPLICE_HASH_DISCRIMINATOR: u8 =
crate::ast::QuoteForm::UNQUOTE_SPLICE_HASH_DISCRIMINATOR;
#[allow(dead_code)]
pub(crate) const HASH_DISCRIMINATORS: [u8; 2] = [
Self::UNQUOTE_HASH_DISCRIMINATOR,
Self::SPLICE_HASH_DISCRIMINATOR,
];
#[allow(dead_code)]
pub(crate) const PROMOTIONS: [(Self, char, Self); 1] = [(
Self::Unquote,
crate::ast::QuoteForm::SPLICE_DISCRIMINATOR,
Self::Splice,
)];
#[must_use]
pub fn marker(self) -> &'static str {
self.to_quote_form().prefix()
}
#[must_use]
pub fn to_quote_form(self) -> crate::ast::QuoteForm {
match self {
Self::Unquote => crate::ast::QuoteForm::Unquote,
Self::Splice => crate::ast::QuoteForm::UnquoteSplice,
}
}
#[must_use]
pub fn wrap(self, inner: crate::ast::Sexp) -> crate::ast::Sexp {
self.to_quote_form().wrap(inner)
}
pub const UNQUOTE_SHAPE: SexpShape = crate::ast::QuoteForm::UNQUOTE_SHAPE;
pub const SPLICE_SHAPE: SexpShape = crate::ast::QuoteForm::UNQUOTE_SPLICE_SHAPE;
pub const SHAPES: [SexpShape; 2] = [Self::UNQUOTE_SHAPE, Self::SPLICE_SHAPE];
#[must_use]
pub fn sexp_shape(self) -> SexpShape {
self.to_quote_form().sexp_shape()
}
#[must_use]
pub fn iac_forge_tag(self) -> &'static str {
self.to_quote_form().iac_forge_tag()
}
#[must_use]
pub fn label(self) -> &'static str {
self.to_quote_form().label()
}
#[must_use]
#[allow(dead_code)]
pub(crate) fn hash_discriminator(self) -> u8 {
self.to_quote_form().hash_discriminator()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KwargPath {
Named(String),
Item { key: String, idx: usize },
Slot(usize),
}
impl KwargPath {
#[must_use]
pub fn named(key: &str) -> Self {
Self::Named(key.to_string())
}
#[must_use]
pub fn item(key: &str, idx: usize) -> Self {
Self::Item {
key: key.to_string(),
idx,
}
}
#[must_use]
pub const fn kind(&self) -> KwargPathKind {
match self {
Self::Named(_) => KwargPathKind::Named,
Self::Item { .. } => KwargPathKind::Item,
Self::Slot(_) => KwargPathKind::Slot,
}
}
}
impl std::fmt::Display for KwargPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Named(key) => write!(f, ":{key}"),
Self::Item { key, idx } => write!(f, ":{key}[{idx}]"),
Self::Slot(idx) => write!(f, "kwargs[{idx}]"),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, tatara_closed_set::DeriveClosedSet)]
#[closed_set(via = "label", display, generate_unknown = "kwarg path kind")]
pub enum KwargPathKind {
Named,
Item,
Slot,
}
impl KwargPathKind {
pub const ALL: [Self; 3] = [Self::Named, Self::Item, Self::Slot];
pub const NAMED_LABEL: &'static str = "named";
pub const ITEM_LABEL: &'static str = "item";
pub const SLOT_LABEL: &'static str = "slot";
pub const LABELS: [&'static str; 3] = [Self::NAMED_LABEL, Self::ITEM_LABEL, Self::SLOT_LABEL];
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Named => Self::NAMED_LABEL,
Self::Item => Self::ITEM_LABEL,
Self::Slot => Self::SLOT_LABEL,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, tatara_closed_set::DeriveClosedSet)]
#[closed_set(via = "label", display, generate_unknown = "expected kwarg shape")]
pub enum ExpectedKwargShape {
Keyword,
String,
Int,
Number,
Bool,
List,
ListOfStrings,
}
impl ExpectedKwargShape {
pub const ALL: [Self; 7] = [
Self::Keyword,
Self::String,
Self::Int,
Self::Number,
Self::Bool,
Self::List,
Self::ListOfStrings,
];
pub const KEYWORD_LABEL: &'static str = SexpShape::KEYWORD_LABEL;
pub const STRING_LABEL: &'static str = SexpShape::STRING_LABEL;
pub const INT_LABEL: &'static str = SexpShape::INT_LABEL;
pub const NUMBER_LABEL: &'static str = "number";
pub const BOOL_LABEL: &'static str = SexpShape::BOOL_LABEL;
pub const LIST_LABEL: &'static str = SexpShape::LIST_LABEL;
pub const LIST_OF_STRINGS_LABEL: &'static str = "list of strings";
pub const LABELS: [&'static str; 7] = [
Self::KEYWORD_LABEL,
Self::STRING_LABEL,
Self::INT_LABEL,
Self::NUMBER_LABEL,
Self::BOOL_LABEL,
Self::LIST_LABEL,
Self::LIST_OF_STRINGS_LABEL,
];
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::Keyword => Self::KEYWORD_LABEL,
Self::String => Self::STRING_LABEL,
Self::Int => Self::INT_LABEL,
Self::Number => Self::NUMBER_LABEL,
Self::Bool => Self::BOOL_LABEL,
Self::List => Self::LIST_LABEL,
Self::ListOfStrings => Self::LIST_OF_STRINGS_LABEL,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, tatara_closed_set::DeriveClosedSet)]
#[closed_set(via = "label", display, generate_unknown = "sexp shape")]
pub enum SexpShape {
Nil,
Symbol,
Keyword,
String,
Int,
Float,
Bool,
List,
Quote,
Quasiquote,
Unquote,
UnquoteSplice,
}
impl SexpShape {
pub const ALL: [Self; 12] = [
Self::Nil,
Self::Symbol,
Self::Keyword,
Self::String,
Self::Int,
Self::Float,
Self::Bool,
Self::List,
Self::Quote,
Self::Quasiquote,
Self::Unquote,
Self::UnquoteSplice,
];
pub const NIL_LABEL: &'static str = "nil";
pub const SYMBOL_LABEL: &'static str = "symbol";
pub const KEYWORD_LABEL: &'static str = "keyword";
pub const STRING_LABEL: &'static str = "string";
pub const INT_LABEL: &'static str = "int";
pub const FLOAT_LABEL: &'static str = "float";
pub const BOOL_LABEL: &'static str = "bool";
pub const LIST_LABEL: &'static str = "list";
pub const QUOTE_LABEL: &'static str = "quote";
pub const QUASIQUOTE_LABEL: &'static str = "quasiquote";
pub const UNQUOTE_LABEL: &'static str = "unquote";
pub const UNQUOTE_SPLICE_LABEL: &'static str = "unquote-splice";
pub const LABELS: [&'static str; 12] = [
Self::NIL_LABEL,
Self::SYMBOL_LABEL,
Self::KEYWORD_LABEL,
Self::STRING_LABEL,
Self::INT_LABEL,
Self::FLOAT_LABEL,
Self::BOOL_LABEL,
Self::LIST_LABEL,
Self::QUOTE_LABEL,
Self::QUASIQUOTE_LABEL,
Self::UNQUOTE_LABEL,
Self::UNQUOTE_SPLICE_LABEL,
];
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::Nil => Self::NIL_LABEL,
Self::Symbol => Self::SYMBOL_LABEL,
Self::Keyword => Self::KEYWORD_LABEL,
Self::String => Self::STRING_LABEL,
Self::Int => Self::INT_LABEL,
Self::Float => Self::FLOAT_LABEL,
Self::Bool => Self::BOOL_LABEL,
Self::List => Self::LIST_LABEL,
Self::Quote => Self::QUOTE_LABEL,
Self::Quasiquote => Self::QUASIQUOTE_LABEL,
Self::Unquote => Self::UNQUOTE_LABEL,
Self::UnquoteSplice => Self::UNQUOTE_SPLICE_LABEL,
}
}
#[must_use]
pub fn as_atom_kind(self) -> Option<crate::ast::AtomKind> {
use crate::ast::AtomKind;
match self {
Self::Symbol => Some(AtomKind::Symbol),
Self::Keyword => Some(AtomKind::Keyword),
Self::String => Some(AtomKind::Str),
Self::Int => Some(AtomKind::Int),
Self::Float => Some(AtomKind::Float),
Self::Bool => Some(AtomKind::Bool),
Self::Nil
| Self::List
| Self::Quote
| Self::Quasiquote
| Self::Unquote
| Self::UnquoteSplice => None,
}
}
#[must_use]
pub fn as_quote_form(self) -> Option<crate::ast::QuoteForm> {
use crate::ast::QuoteForm;
match self {
Self::Quote => Some(QuoteForm::Quote),
Self::Quasiquote => Some(QuoteForm::Quasiquote),
Self::Unquote => Some(QuoteForm::Unquote),
Self::UnquoteSplice => Some(QuoteForm::UnquoteSplice),
Self::Nil
| Self::List
| Self::Symbol
| Self::Keyword
| Self::String
| Self::Int
| Self::Float
| Self::Bool => None,
}
}
#[must_use]
pub fn as_unquote_form(self) -> Option<UnquoteForm> {
self.as_quote_form()
.and_then(crate::ast::QuoteForm::as_unquote_form)
}
#[must_use]
pub fn as_structural_kind(self) -> Option<StructuralKind> {
match self {
Self::Nil => Some(StructuralKind::Nil),
Self::List => Some(StructuralKind::List),
Self::Symbol
| Self::Keyword
| Self::String
| Self::Int
| Self::Float
| Self::Bool
| Self::Quote
| Self::Quasiquote
| Self::Unquote
| Self::UnquoteSplice => None,
}
}
#[must_use]
pub fn iac_forge_tag(self) -> Option<&'static str> {
self.as_quote_form()
.map(crate::ast::QuoteForm::iac_forge_tag)
}
#[must_use]
pub fn prefix(self) -> Option<&'static str> {
self.as_quote_form().map(crate::ast::QuoteForm::prefix)
}
#[allow(dead_code)]
#[must_use]
pub(crate) fn hash_discriminator(self) -> u8 {
match self {
Self::Nil => StructuralKind::Nil.hash_discriminator(),
Self::List => StructuralKind::List.hash_discriminator(),
Self::Symbol | Self::Keyword | Self::String | Self::Int | Self::Float | Self::Bool => {
crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR
}
Self::Quote => crate::ast::QuoteForm::Quote.hash_discriminator(),
Self::Quasiquote => crate::ast::QuoteForm::Quasiquote.hash_discriminator(),
Self::Unquote => crate::ast::QuoteForm::Unquote.hash_discriminator(),
Self::UnquoteSplice => crate::ast::QuoteForm::UnquoteSplice.hash_discriminator(),
}
}
pub(crate) const NIL_HASH_DISCRIMINATOR: u8 = StructuralKind::NIL_HASH_DISCRIMINATOR;
pub(crate) const SYMBOL_HASH_DISCRIMINATOR: u8 = crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR;
pub(crate) const KEYWORD_HASH_DISCRIMINATOR: u8 =
crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR;
pub(crate) const STRING_HASH_DISCRIMINATOR: u8 = crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR;
pub(crate) const INT_HASH_DISCRIMINATOR: u8 = crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR;
pub(crate) const FLOAT_HASH_DISCRIMINATOR: u8 = crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR;
pub(crate) const BOOL_HASH_DISCRIMINATOR: u8 = crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR;
pub(crate) const LIST_HASH_DISCRIMINATOR: u8 = StructuralKind::LIST_HASH_DISCRIMINATOR;
pub(crate) const QUOTE_HASH_DISCRIMINATOR: u8 = crate::ast::QuoteForm::QUOTE_HASH_DISCRIMINATOR;
pub(crate) const QUASIQUOTE_HASH_DISCRIMINATOR: u8 =
crate::ast::QuoteForm::QUASIQUOTE_HASH_DISCRIMINATOR;
pub(crate) const UNQUOTE_HASH_DISCRIMINATOR: u8 =
crate::ast::QuoteForm::UNQUOTE_HASH_DISCRIMINATOR;
pub(crate) const UNQUOTE_SPLICE_HASH_DISCRIMINATOR: u8 =
crate::ast::QuoteForm::UNQUOTE_SPLICE_HASH_DISCRIMINATOR;
#[allow(dead_code)]
pub(crate) const HASH_DISCRIMINATORS: [u8; 12] = [
Self::NIL_HASH_DISCRIMINATOR,
Self::SYMBOL_HASH_DISCRIMINATOR,
Self::KEYWORD_HASH_DISCRIMINATOR,
Self::STRING_HASH_DISCRIMINATOR,
Self::INT_HASH_DISCRIMINATOR,
Self::FLOAT_HASH_DISCRIMINATOR,
Self::BOOL_HASH_DISCRIMINATOR,
Self::LIST_HASH_DISCRIMINATOR,
Self::QUOTE_HASH_DISCRIMINATOR,
Self::QUASIQUOTE_HASH_DISCRIMINATOR,
Self::UNQUOTE_HASH_DISCRIMINATOR,
Self::UNQUOTE_SPLICE_HASH_DISCRIMINATOR,
];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, tatara_closed_set::DeriveClosedSet)]
#[closed_set(via = "label", display, generate_unknown = "structural kind")]
pub enum StructuralKind {
Nil,
List,
}
impl StructuralKind {
pub const ALL: [Self; 2] = [Self::Nil, Self::List];
pub const NIL_LABEL: &'static str = SexpShape::NIL_LABEL;
pub const LIST_LABEL: &'static str = SexpShape::LIST_LABEL;
pub const LABELS: [&'static str; 2] = [Self::NIL_LABEL, Self::LIST_LABEL];
#[must_use]
pub fn label(self) -> &'static str {
self.sexp_shape().label()
}
#[must_use]
pub fn sexp_shape(self) -> SexpShape {
match self {
Self::Nil => Self::NIL_SHAPE,
Self::List => Self::LIST_SHAPE,
}
}
pub const NIL_SHAPE: SexpShape = SexpShape::Nil;
pub const LIST_SHAPE: SexpShape = SexpShape::List;
pub const SHAPES: [SexpShape; 2] = [Self::NIL_SHAPE, Self::LIST_SHAPE];
pub(crate) const NIL_HASH_DISCRIMINATOR: u8 = 0;
pub(crate) const LIST_HASH_DISCRIMINATOR: u8 = 2;
#[allow(dead_code)]
pub(crate) const HASH_DISCRIMINATORS: [u8; 2] =
[Self::NIL_HASH_DISCRIMINATOR, Self::LIST_HASH_DISCRIMINATOR];
#[allow(dead_code)]
#[must_use]
pub(crate) fn hash_discriminator(self) -> u8 {
match self {
Self::Nil => Self::NIL_HASH_DISCRIMINATOR,
Self::List => Self::LIST_HASH_DISCRIMINATOR,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SexpWitness {
pub shape: SexpShape,
pub display: String,
}
impl SexpWitness {
#[must_use]
pub fn new(shape: SexpShape, display: impl Into<String>) -> Self {
Self {
shape,
display: display.into(),
}
}
}
impl std::fmt::Display for SexpWitness {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.display)
}
}
fn unbound_hint_suffix(prefix: UnquoteForm, hint: Option<&str>) -> String {
match hint {
Some(h) => format!("; did you mean {}{h}?", prefix.marker()),
None => String::new(),
}
}
fn paren_suffix(body: &str) -> String {
format!(" ({body})")
}
fn unknown_among_suffix(hint: Option<&str>, body: &str) -> String {
match hint {
Some(h) => paren_suffix(&format!("did you mean {h}?; {body}")),
None => paren_suffix(body),
}
}
fn unknown_kwarg_suffix(hint: Option<&str>, allowed: &[String]) -> String {
let allowed_list = allowed
.iter()
.map(|s| format!(":{s}"))
.collect::<Vec<_>>()
.join(", ");
unknown_among_suffix(
hint.map(|h| format!(":{h}")).as_deref(),
&format!("allowed: {allowed_list}"),
)
}
fn rest_param_missing_name_suffix(rest_position: usize, got: Option<&str>) -> String {
let body = match got {
Some(g) => format!("rest marker at position {rest_position}, got {g}"),
None => format!("rest marker at position {rest_position}, none provided"),
};
paren_suffix(&body)
}
fn rest_param_trailing_tokens_suffix(rest_position: usize, extra: usize, first: &str) -> String {
paren_suffix(&format!(
"rest marker at position {rest_position}, {extra} trailing after name, first: {first}"
))
}
fn optional_marker_repeated_suffix(first_position: usize, second_position: usize) -> String {
paren_suffix(&format!(
"first &optional at position {first_position}, second at position {second_position}"
))
}
fn optional_param_malformed_suffix(
position: usize,
reason: OptionalParamMalformedReason,
) -> String {
paren_suffix(&format!("position {position}, {}", reason.label()))
}
fn missing_head_symbol_suffix(got: Option<&str>) -> String {
let body = match got {
Some(g) => format!("got {g}"),
None => "empty list".to_string(),
};
paren_suffix(&body)
}
fn unknown_domain_keyword_suffix(hint: Option<&str>, registered: &[String]) -> String {
let body = if registered.is_empty() {
"no domains registered".to_string()
} else {
format!("registered: {}", registered.join(", "))
};
unknown_among_suffix(hint.map(|h| format!("({h} ...)")).as_deref(), &body)
}
impl LispError {
#[must_use]
pub fn position(&self) -> Option<usize> {
match self {
Self::UnexpectedChar(_, pos) | Self::UnterminatedString(pos) => Some(*pos),
Self::UnmatchedParen { pos } | Self::UnmatchedOpenParen { pos } | Self::Eof { pos } => {
Some(*pos)
}
Self::InvalidNumber(_)
| Self::UnknownSymbol(_)
| Self::Type { .. }
| Self::Compile { .. }
| Self::TypeMismatch { .. }
| Self::HeadMismatch { .. }
| Self::Unknown { .. }
| Self::Missing(_)
| Self::OddKwargs { .. }
| Self::UnboundTemplateVar { .. }
| Self::DuplicateKwarg { .. }
| Self::MissingKwarg { .. }
| Self::UnknownKwarg { .. }
| Self::UnknownDomainKeyword { .. }
| Self::NonSymbolUnquoteTarget { .. }
| Self::SpliceOutsideList { .. }
| Self::MissingMacroArg { .. }
| Self::TooManyMacroArgs { .. }
| Self::NonSymbolParam { .. }
| Self::RestParamMissingName { .. }
| Self::RestParamTrailingTokens { .. }
| Self::OptionalMarkerRepeated { .. }
| Self::OptionalParamMalformed { .. }
| Self::DefmacroArity { .. }
| Self::DefmacroNonSymbolName { .. }
| Self::DefmacroNonListParams { .. }
| Self::NotAListForm { .. }
| Self::MissingHeadSymbol { .. }
| Self::NamedFormMissingName { .. }
| Self::NamedFormNonSymbolName { .. }
| Self::RewriterNonList { .. }
| Self::DomainSerialize { .. }
| Self::KwargDeserialize { .. }
| Self::CompilerSpecIo { .. }
| Self::TemplateInvariant { .. }
| Self::ExpansionDepthExceeded { .. }
| Self::ExpansionSizeExceeded { .. }
| Self::MacroBodySizeExceeded { .. }
| Self::RegisteredMacrosExceeded { .. }
| Self::MacroArityExceeded { .. } => None,
}
}
}
#[cfg(test)]
mod tests {
use super::{
missing_head_symbol_suffix, optional_marker_repeated_suffix,
optional_param_malformed_suffix, paren_suffix, rest_param_missing_name_suffix,
rest_param_trailing_tokens_suffix, unknown_among_suffix, unknown_domain_keyword_suffix,
unknown_kwarg_suffix, CompilerSpecIoStage, ExpectedKwargShape, KwargPath, KwargPathKind,
LispError, MacroDefHead, OptionalParamMalformedReason, SexpShape, SexpWitness,
StructuralKind, TemplateInvariantKind, UnknownExpectedKwargShape, UnknownKwargPathKind,
UnknownMacroDefHead, UnknownSexpShape, UnknownUnquoteForm, UnquoteForm,
};
#[test]
fn position_extracts_offset_from_positional_variants() {
assert_eq!(LispError::UnexpectedChar('?', 7).position(), Some(7));
assert_eq!(LispError::UnterminatedString(11).position(), Some(11));
assert_eq!(LispError::UnmatchedParen { pos: 3 }.position(), Some(3));
assert_eq!(LispError::UnmatchedOpenParen { pos: 0 }.position(), Some(0));
assert_eq!(LispError::Eof { pos: 42 }.position(), Some(42));
}
#[test]
fn position_is_none_for_non_positional_variants() {
assert_eq!(
LispError::OddKwargs {
dangling: ":query".into()
}
.position(),
None
);
assert_eq!(LispError::Missing("name").position(), None);
assert_eq!(LispError::InvalidNumber("nan".into()).position(), None);
assert_eq!(LispError::UnknownSymbol("foo".into()).position(), None);
assert_eq!(
LispError::Type {
expected: "int",
got: "string".into()
}
.position(),
None
);
assert_eq!(
LispError::Compile {
form: ":x".into(),
message: "bad".into()
}
.position(),
None
);
assert_eq!(
LispError::TypeMismatch {
form: KwargPath::named("x"),
expected: ExpectedKwargShape::String,
got: SexpShape::Int,
}
.position(),
None
);
assert_eq!(
LispError::HeadMismatch {
keyword: "defmonitor",
got: "not-a-monitor".into(),
}
.position(),
None
);
assert_eq!(
LispError::Unknown {
category: "domain",
value: "defx".into()
}
.position(),
None
);
assert_eq!(
LispError::UnboundTemplateVar {
prefix: UnquoteForm::Unquote,
name: "xx".into(),
hint: Some("x".into()),
}
.position(),
None
);
assert_eq!(
LispError::UnboundTemplateVar {
prefix: UnquoteForm::Splice,
name: "ys".into(),
hint: None,
}
.position(),
None
);
assert_eq!(
LispError::DuplicateKwarg { key: "name".into() }.position(),
None
);
assert_eq!(
LispError::MissingKwarg { key: "name".into() }.position(),
None
);
assert_eq!(
LispError::UnknownKwarg {
key: "tthreshold".into(),
hint: Some("threshold".into()),
allowed: vec!["name".into(), "threshold".into()],
}
.position(),
None
);
assert_eq!(
LispError::UnknownDomainKeyword {
keyword: "defmoniter".into(),
hint: Some("defmonitor".into()),
registered: vec!["defalertpolicy".into(), "defmonitor".into()],
}
.position(),
None
);
assert_eq!(
LispError::NonSymbolUnquoteTarget {
prefix: UnquoteForm::Unquote,
got: SexpWitness::new(SexpShape::List, "(list 1 2)"),
}
.position(),
None
);
assert_eq!(
LispError::NonSymbolUnquoteTarget {
prefix: UnquoteForm::Splice,
got: SexpWitness::new(SexpShape::Int, "5"),
}
.position(),
None
);
assert_eq!(
LispError::SpliceOutsideList {
got: SexpWitness::new(SexpShape::Symbol, "xs"),
}
.position(),
None
);
assert_eq!(
LispError::SpliceOutsideList {
got: SexpWitness::new(SexpShape::List, "(list 1 2)"),
}
.position(),
None
);
assert_eq!(
LispError::MissingMacroArg {
macro_name: "wrap".into(),
param: "b".into(),
}
.position(),
None
);
assert_eq!(
LispError::MissingMacroArg {
macro_name: "call".into(),
param: "f".into(),
}
.position(),
None
);
assert_eq!(
LispError::TooManyMacroArgs {
macro_name: "pair".into(),
expected: 2,
got: 3,
}
.position(),
None
);
assert_eq!(
LispError::TooManyMacroArgs {
macro_name: "wrap".into(),
expected: 1,
got: 5,
}
.position(),
None
);
assert_eq!(
LispError::NonSymbolParam {
position: 0,
got: SexpWitness::new(SexpShape::Int, "5"),
}
.position(),
None
);
assert_eq!(
LispError::NonSymbolParam {
position: 2,
got: SexpWitness::new(SexpShape::List, "(nested)"),
}
.position(),
None
);
assert_eq!(
LispError::RestParamMissingName {
rest_position: 1,
got: None,
}
.position(),
None
);
assert_eq!(
LispError::RestParamMissingName {
rest_position: 0,
got: Some(SexpWitness::new(SexpShape::Int, "5")),
}
.position(),
None
);
assert_eq!(
LispError::OptionalParamMalformed {
position: 1,
got: SexpWitness::new(SexpShape::List, "()"),
reason: OptionalParamMalformedReason::EmptyList,
}
.position(),
None
);
assert_eq!(
LispError::OptionalParamMalformed {
position: 3,
got: SexpWitness::new(SexpShape::List, "(x 1 2)"),
reason: OptionalParamMalformedReason::ExtraElements { length: 3 },
}
.position(),
None
);
assert_eq!(
LispError::DefmacroArity {
head: MacroDefHead::Defmacro,
arity: 1,
}
.position(),
None
);
assert_eq!(
LispError::DefmacroArity {
head: MacroDefHead::Defcheck,
arity: 3,
}
.position(),
None
);
assert_eq!(
LispError::DefmacroNonSymbolName {
head: MacroDefHead::Defmacro,
got: SexpWitness::new(SexpShape::Int, "5"),
}
.position(),
None
);
assert_eq!(
LispError::DefmacroNonListParams {
head: MacroDefHead::Defmacro,
got: SexpWitness::new(SexpShape::Symbol, "x"),
}
.position(),
None
);
assert_eq!(
LispError::DefmacroNonListParams {
head: MacroDefHead::Defcheck,
got: SexpWitness::new(SexpShape::Keyword, ":foo"),
}
.position(),
None
);
assert_eq!(
LispError::DefmacroNonSymbolName {
head: MacroDefHead::DefpointTemplate,
got: SexpWitness::new(SexpShape::Keyword, ":foo"),
}
.position(),
None
);
assert_eq!(
LispError::NotAListForm {
keyword: "defmonitor",
}
.position(),
None
);
assert_eq!(
LispError::NotAListForm {
keyword: "defpoint",
}
.position(),
None
);
assert_eq!(
LispError::MissingHeadSymbol {
keyword: "defmonitor",
got: None,
}
.position(),
None
);
assert_eq!(
LispError::MissingHeadSymbol {
keyword: "defpoint",
got: Some(SexpWitness::new(SexpShape::Int, "5")),
}
.position(),
None
);
assert_eq!(
LispError::NamedFormMissingName {
keyword: "defpoint",
}
.position(),
None
);
assert_eq!(
LispError::NamedFormMissingName {
keyword: "defalertpolicy",
}
.position(),
None
);
assert_eq!(
LispError::NamedFormNonSymbolName {
keyword: "defpoint",
got: SexpShape::Int,
}
.position(),
None
);
assert_eq!(
LispError::NamedFormNonSymbolName {
keyword: "defalertpolicy",
got: SexpShape::List,
}
.position(),
None
);
assert_eq!(
LispError::RewriterNonList {
keyword: "defmonitor",
got: SexpWitness::new(SexpShape::Int, "42"),
}
.position(),
None
);
assert_eq!(
LispError::DomainSerialize {
keyword: "defmonitor",
message: "expected value at line 1 column 1".into(),
}
.position(),
None
);
assert_eq!(
LispError::DomainSerialize {
keyword: "defalertpolicy",
message: "key must be a string".into(),
}
.position(),
None
);
assert_eq!(
LispError::KwargDeserialize {
path: KwargPath::named("level"),
message: "unknown variant `NotASeverity`".into(),
}
.position(),
None
);
assert_eq!(
LispError::KwargDeserialize {
path: KwargPath::item("steps", 1),
message: "invalid type: integer `7`, expected a string".into(),
}
.position(),
None
);
assert_eq!(
LispError::CompilerSpecIo {
stage: super::CompilerSpecIoStage::RealizeToDiskSerialize,
message: "expected struct CompilerSpec".into(),
}
.position(),
None
);
assert_eq!(
LispError::CompilerSpecIo {
stage: super::CompilerSpecIoStage::LoadFromDiskDeserialize,
message: "expected value at line 1 column 1".into(),
}
.position(),
None
);
}
#[test]
fn missing_head_symbol_display_with_empty_list_renders_legacy_prefix_and_empty_marker() {
let err = LispError::MissingHeadSymbol {
keyword: "defmonitor",
got: None,
};
assert_eq!(
format!("{err}"),
"compile error in defmonitor: missing head symbol (empty list)"
);
}
#[test]
fn missing_head_symbol_display_with_int_got_renders_legacy_prefix_and_got() {
let err = LispError::MissingHeadSymbol {
keyword: "defmonitor",
got: Some(SexpWitness::new(SexpShape::Int, "5")),
};
assert_eq!(
format!("{err}"),
"compile error in defmonitor: missing head symbol (got 5)"
);
}
#[test]
fn missing_head_symbol_display_carries_keyword_atom_got_unchanged() {
let err = LispError::MissingHeadSymbol {
keyword: "defalertpolicy",
got: Some(SexpWitness::new(SexpShape::Keyword, ":foo")),
};
assert_eq!(
format!("{err}"),
"compile error in defalertpolicy: missing head symbol (got :foo)"
);
}
#[test]
fn missing_head_symbol_display_carries_string_got_unchanged() {
let err = LispError::MissingHeadSymbol {
keyword: "defmonitor",
got: Some(SexpWitness::new(SexpShape::String, "\"name\"")),
};
assert_eq!(
format!("{err}"),
"compile error in defmonitor: missing head symbol (got \"name\")"
);
}
#[test]
fn missing_head_symbol_display_carries_nested_list_got_unchanged() {
let err = LispError::MissingHeadSymbol {
keyword: "defmonitor",
got: Some(SexpWitness::new(SexpShape::List, "(nested)")),
};
assert_eq!(
format!("{err}"),
"compile error in defmonitor: missing head symbol (got (nested))"
);
}
#[test]
fn missing_head_symbol_display_preserves_legacy_substring_for_message_grep() {
let err = LispError::MissingHeadSymbol {
keyword: "defmonitor",
got: None,
};
let msg = format!("{err}");
assert!(
msg.contains("missing head symbol"),
"expected legacy substring in message, got: {msg}"
);
assert!(
msg.contains("compile error in defmonitor:"),
"expected legacy form-label prefix in message, got: {msg}"
);
}
#[test]
fn missing_head_symbol_got_carries_typed_witness_through_variant_slot() {
let err = LispError::MissingHeadSymbol {
keyword: "defmonitor",
got: Some(SexpWitness::new(SexpShape::Int, "5")),
};
match &err {
LispError::MissingHeadSymbol { keyword, got } => {
assert_eq!(*keyword, "defmonitor");
let witness = got.as_ref().expect("got must be Some");
assert_eq!(witness.shape, SexpShape::Int);
assert_eq!(witness.display, "5");
}
other => panic!("expected MissingHeadSymbol, got {other:?}"),
}
assert_eq!(
format!("{err}"),
"compile error in defmonitor: missing head symbol (got 5)"
);
}
#[test]
fn missing_head_symbol_got_distinguishes_int_from_keyword_at_variant_slot() {
let err_int = LispError::MissingHeadSymbol {
keyword: "defmonitor",
got: Some(SexpWitness::new(SexpShape::Int, "5")),
};
let err_kw = LispError::MissingHeadSymbol {
keyword: "defalertpolicy",
got: Some(SexpWitness::new(SexpShape::Keyword, ":foo")),
};
let (int_shape, kw_shape) = (
match &err_int {
LispError::MissingHeadSymbol { got: Some(w), .. } => w.shape,
_ => unreachable!(),
},
match &err_kw {
LispError::MissingHeadSymbol { got: Some(w), .. } => w.shape,
_ => unreachable!(),
},
);
assert_ne!(
int_shape, kw_shape,
"Int and Keyword witnesses must remain structurally distinct at the variant slot",
);
assert_eq!(int_shape, SexpShape::Int);
assert_eq!(kw_shape, SexpShape::Keyword);
}
#[test]
fn missing_head_symbol_and_rest_param_gate_share_one_witness_primitive() {
let same_witness = SexpWitness::new(SexpShape::List, "(nested)");
let missing_head = LispError::MissingHeadSymbol {
keyword: "defmonitor",
got: Some(same_witness.clone()),
};
let rest_param_missing_name = LispError::RestParamMissingName {
rest_position: 0,
got: Some(same_witness.clone()),
};
let defmacro_non_list_params = LispError::DefmacroNonListParams {
head: MacroDefHead::Defmacro,
got: same_witness.clone(),
};
let defmacro_non_symbol_name = LispError::DefmacroNonSymbolName {
head: MacroDefHead::Defmacro,
got: same_witness.clone(),
};
let non_symbol_param = LispError::NonSymbolParam {
position: 0,
got: same_witness.clone(),
};
let non_symbol_target = LispError::NonSymbolUnquoteTarget {
prefix: UnquoteForm::Unquote,
got: same_witness.clone(),
};
let splice_outside = LispError::SpliceOutsideList {
got: same_witness.clone(),
};
match (
&missing_head,
&rest_param_missing_name,
&defmacro_non_list_params,
&defmacro_non_symbol_name,
&non_symbol_param,
&non_symbol_target,
&splice_outside,
) {
(
LispError::MissingHeadSymbol { got: Some(a), .. },
LispError::RestParamMissingName { got: Some(b), .. },
LispError::DefmacroNonListParams { got: c, .. },
LispError::DefmacroNonSymbolName { got: d, .. },
LispError::NonSymbolParam { got: e, .. },
LispError::NonSymbolUnquoteTarget { got: f, .. },
LispError::SpliceOutsideList { got: g },
) => {
assert_eq!(a.shape, b.shape);
assert_eq!(b.shape, c.shape);
assert_eq!(c.shape, d.shape);
assert_eq!(d.shape, e.shape);
assert_eq!(e.shape, f.shape);
assert_eq!(f.shape, g.shape);
assert_eq!(a.display, b.display);
assert_eq!(b.display, c.display);
assert_eq!(c.display, d.display);
assert_eq!(d.display, e.display);
assert_eq!(e.display, f.display);
assert_eq!(f.display, g.display);
assert_eq!(*a, same_witness);
assert_eq!(*b, same_witness);
assert_eq!(*c, same_witness);
assert_eq!(*d, same_witness);
assert_eq!(*e, same_witness);
assert_eq!(*f, same_witness);
assert_eq!(*g, same_witness);
}
_ => unreachable!(),
}
}
#[test]
fn not_a_list_form_display_renders_legacy_compile_shape() {
let err = LispError::NotAListForm {
keyword: "defmonitor",
};
assert_eq!(
format!("{err}"),
"compile error in defmonitor: expected list form"
);
}
#[test]
fn not_a_list_form_display_carries_keyword_unchanged() {
let err = LispError::NotAListForm {
keyword: "defpoint",
};
assert_eq!(
format!("{err}"),
"compile error in defpoint: expected list form"
);
}
#[test]
fn not_a_list_form_display_preserves_legacy_substring_for_message_grep() {
let err = LispError::NotAListForm {
keyword: "defalertpolicy",
};
let msg = format!("{err}");
assert!(
msg.contains("expected list form"),
"expected legacy substring in message, got: {msg}"
);
assert!(
msg.contains("compile error in defalertpolicy:"),
"expected legacy form-label prefix in message, got: {msg}"
);
}
#[test]
fn unknown_kwarg_display_with_hint_renders_did_you_mean_then_allowed_list() {
let err = LispError::UnknownKwarg {
key: "tthreshold".into(),
hint: Some("threshold".into()),
allowed: vec!["name".into(), "query".into(), "threshold".into()],
};
assert_eq!(
format!("{err}"),
"compile error in :tthreshold: unknown keyword \
(did you mean :threshold?; allowed: :name, :query, :threshold)"
);
}
#[test]
fn unknown_kwarg_display_without_hint_renders_allowed_list_only() {
let err = LispError::UnknownKwarg {
key: "totally-unrelated".into(),
hint: None,
allowed: vec!["name".into(), "query".into(), "threshold".into()],
};
assert_eq!(
format!("{err}"),
"compile error in :totally-unrelated: unknown keyword \
(allowed: :name, :query, :threshold)"
);
}
#[test]
fn unknown_kwarg_display_carries_kebab_case_keys_unchanged() {
let err = LispError::UnknownKwarg {
key: "windou-seconds".into(),
hint: Some("window-seconds".into()),
allowed: vec!["notify-ref".into(), "window-seconds".into()],
};
assert_eq!(
format!("{err}"),
"compile error in :windou-seconds: unknown keyword \
(did you mean :window-seconds?; allowed: :notify-ref, :window-seconds)"
);
}
#[test]
fn duplicate_kwarg_display_matches_legacy_compile_shape() {
let err = LispError::DuplicateKwarg { key: "name".into() };
assert_eq!(
format!("{err}"),
"compile error in :name: duplicate keyword"
);
}
#[test]
fn duplicate_kwarg_display_carries_kebab_case_keys_unchanged() {
let err = LispError::DuplicateKwarg {
key: "notify-ref".into(),
};
assert_eq!(
format!("{err}"),
"compile error in :notify-ref: duplicate keyword"
);
}
#[test]
fn missing_kwarg_display_matches_legacy_compile_shape() {
let err = LispError::MissingKwarg {
key: "threshold".into(),
};
assert_eq!(
format!("{err}"),
"compile error in :threshold: required but not provided"
);
}
#[test]
fn missing_kwarg_display_carries_kebab_case_keys_unchanged() {
let err = LispError::MissingKwarg {
key: "notify-ref".into(),
};
assert_eq!(
format!("{err}"),
"compile error in :notify-ref: required but not provided"
);
}
#[test]
fn unbound_template_var_display_without_hint_matches_legacy_compile_shape() {
let err = LispError::UnboundTemplateVar {
prefix: UnquoteForm::Unquote,
name: "y".into(),
hint: None,
};
assert_eq!(format!("{err}"), "compile error in ,y: unbound");
}
#[test]
fn unbound_template_var_display_appends_hint_suffix_when_present() {
let err = LispError::UnboundTemplateVar {
prefix: UnquoteForm::Unquote,
name: "xs".into(),
hint: Some("x".into()),
};
assert_eq!(
format!("{err}"),
"compile error in ,xs: unbound; did you mean ,x?"
);
}
#[test]
fn unknown_domain_keyword_display_with_hint_renders_did_you_mean_then_registered_list() {
let err = LispError::UnknownDomainKeyword {
keyword: "defmoniter".into(),
hint: Some("defmonitor".into()),
registered: vec![
"defalertpolicy".into(),
"defmonitor".into(),
"defnotify".into(),
],
};
assert_eq!(
format!("{err}"),
"unknown domain keyword: (defmoniter ...) \
(did you mean (defmonitor ...)?; \
registered: defalertpolicy, defmonitor, defnotify)"
);
}
#[test]
fn unknown_domain_keyword_display_without_hint_renders_registered_list_only() {
let err = LispError::UnknownDomainKeyword {
keyword: "totally-unrelated".into(),
hint: None,
registered: vec!["defalertpolicy".into(), "defmonitor".into()],
};
assert_eq!(
format!("{err}"),
"unknown domain keyword: (totally-unrelated ...) \
(registered: defalertpolicy, defmonitor)"
);
}
#[test]
fn unknown_domain_keyword_display_with_empty_registry_renders_no_domains_registered() {
let err = LispError::UnknownDomainKeyword {
keyword: "defmonitor".into(),
hint: None,
registered: vec![],
};
assert_eq!(
format!("{err}"),
"unknown domain keyword: (defmonitor ...) (no domains registered)"
);
}
#[test]
fn unknown_domain_keyword_display_carries_kebab_case_keywords_unchanged() {
let err = LispError::UnknownDomainKeyword {
keyword: "defalert-policiy".into(),
hint: Some("defalert-policy".into()),
registered: vec!["defalert-policy".into(), "defprocess-spec".into()],
};
assert_eq!(
format!("{err}"),
"unknown domain keyword: (defalert-policiy ...) \
(did you mean (defalert-policy ...)?; \
registered: defalert-policy, defprocess-spec)"
);
}
#[test]
fn unknown_among_suffix_owns_the_parenthetical_wrapping_skeleton() {
assert_eq!(
unknown_among_suffix(Some(":threshold"), "allowed: :name, :threshold"),
" (did you mean :threshold?; allowed: :name, :threshold)"
);
assert_eq!(
unknown_among_suffix(None, "allowed: :name, :threshold"),
" (allowed: :name, :threshold)"
);
}
#[test]
fn optional_param_malformed_display_renders_typed_reason_in_suffix() {
let empty = LispError::OptionalParamMalformed {
position: 1,
got: SexpWitness::new(SexpShape::List, "()"),
reason: OptionalParamMalformedReason::EmptyList,
};
assert_eq!(
format!("{empty}"),
"compile error in defmacro params: malformed &optional spec, got () (position 1, empty list)"
);
let missing = LispError::OptionalParamMalformed {
position: 2,
got: SexpWitness::new(SexpShape::List, "(x)"),
reason: OptionalParamMalformedReason::MissingDefault,
};
assert_eq!(
format!("{missing}"),
"compile error in defmacro params: malformed &optional spec, got (x) (position 2, missing default)"
);
let extra = LispError::OptionalParamMalformed {
position: 3,
got: SexpWitness::new(SexpShape::List, "(x 1 2)"),
reason: OptionalParamMalformedReason::ExtraElements { length: 3 },
};
assert_eq!(
format!("{extra}"),
"compile error in defmacro params: malformed &optional spec, got (x 1 2) (position 3, 3 elements (need 2))"
);
let nonsym = LispError::OptionalParamMalformed {
position: 0,
got: SexpWitness::new(SexpShape::List, "(5 default)"),
reason: OptionalParamMalformedReason::NonSymbolName,
};
assert_eq!(
format!("{nonsym}"),
"compile error in defmacro params: malformed &optional spec, got (5 default) (position 0, name not a symbol)"
);
}
#[test]
fn optional_param_malformed_reason_static_labels_pin_legacy_label_bytes() {
assert_eq!(OptionalParamMalformedReason::EMPTY_LIST_LABEL, "empty list");
assert_eq!(
OptionalParamMalformedReason::MISSING_DEFAULT_LABEL,
"missing default"
);
assert_eq!(
OptionalParamMalformedReason::NON_SYMBOL_NAME_LABEL,
"name not a symbol"
);
assert_eq!(
OptionalParamMalformedReason::EmptyList.label(),
OptionalParamMalformedReason::EMPTY_LIST_LABEL
);
assert_eq!(
OptionalParamMalformedReason::MissingDefault.label(),
OptionalParamMalformedReason::MISSING_DEFAULT_LABEL
);
assert_eq!(
OptionalParamMalformedReason::NonSymbolName.label(),
OptionalParamMalformedReason::NON_SYMBOL_NAME_LABEL
);
assert_eq!(
OptionalParamMalformedReason::ExtraElements { length: 4 }.label(),
"4 elements (need 2)"
);
assert_eq!(OptionalParamMalformedReason::STATIC_LABELS.len(), 3);
assert_eq!(
OptionalParamMalformedReason::STATIC_LABELS,
[
OptionalParamMalformedReason::EMPTY_LIST_LABEL,
OptionalParamMalformedReason::MISSING_DEFAULT_LABEL,
OptionalParamMalformedReason::NON_SYMBOL_NAME_LABEL,
]
);
let labels = OptionalParamMalformedReason::STATIC_LABELS;
for i in 0..labels.len() {
for j in (i + 1)..labels.len() {
assert_ne!(
labels[i], labels[j],
"OptionalParamMalformedReason::STATIC_LABELS[{i}] and [{j}] collide on `{}`",
labels[i]
);
}
}
}
#[test]
fn optional_param_spec_arity_pins_canonical_accept_target() {
assert_eq!(OptionalParamMalformedReason::OPTIONAL_PARAM_SPEC_ARITY, 2);
}
#[test]
fn classify_arity_projects_arity_to_typed_rejection_reason() {
use OptionalParamMalformedReason as R;
assert_eq!(R::classify_arity(0), Some(R::EmptyList));
assert_eq!(R::classify_arity(1), Some(R::MissingDefault));
assert_eq!(R::classify_arity(R::OPTIONAL_PARAM_SPEC_ARITY), None);
assert_eq!(R::classify_arity(2), None);
assert_eq!(R::classify_arity(3), Some(R::ExtraElements { length: 3 }));
assert_eq!(R::classify_arity(4), Some(R::ExtraElements { length: 4 }));
assert_eq!(R::classify_arity(17), Some(R::ExtraElements { length: 17 }));
}
#[test]
fn extra_elements_label_derives_target_arity_from_typed_constant() {
assert_eq!(
OptionalParamMalformedReason::ExtraElements { length: 3 }.label(),
"3 elements (need 2)"
);
assert_eq!(
OptionalParamMalformedReason::ExtraElements { length: 7 }.label(),
"7 elements (need 2)"
);
assert!(OptionalParamMalformedReason::ExtraElements { length: 3 }
.label()
.ends_with(&format!(
"(need {})",
OptionalParamMalformedReason::OPTIONAL_PARAM_SPEC_ARITY
)));
}
#[test]
fn label_static_projects_static_subset_to_typed_option() {
use OptionalParamMalformedReason as R;
assert_eq!(R::EmptyList.label_static(), Some(R::EMPTY_LIST_LABEL));
assert_eq!(
R::MissingDefault.label_static(),
Some(R::MISSING_DEFAULT_LABEL)
);
assert_eq!(
R::NonSymbolName.label_static(),
Some(R::NON_SYMBOL_NAME_LABEL)
);
assert_eq!(R::ExtraElements { length: 3 }.label_static(), None);
assert_eq!(R::ExtraElements { length: 0 }.label_static(), None);
assert_eq!(R::ExtraElements { length: 42 }.label_static(), None);
for reason in [R::EmptyList, R::MissingDefault, R::NonSymbolName] {
assert_eq!(reason.label_static().unwrap().to_string(), reason.label());
}
}
#[test]
fn label_static_matches_static_labels_array_in_declaration_order() {
use OptionalParamMalformedReason as R;
let projected = [
R::EmptyList.label_static().unwrap(),
R::MissingDefault.label_static().unwrap(),
R::NonSymbolName.label_static().unwrap(),
];
assert_eq!(projected, R::STATIC_LABELS);
}
#[test]
fn label_static_composes_with_classify_arity_over_the_3_of_4_carving() {
use OptionalParamMalformedReason as R;
assert_eq!(
R::classify_arity(0).and_then(R::label_static),
Some(R::EMPTY_LIST_LABEL)
);
assert_eq!(
R::classify_arity(1).and_then(R::label_static),
Some(R::MISSING_DEFAULT_LABEL)
);
assert_eq!(
R::classify_arity(R::OPTIONAL_PARAM_SPEC_ARITY).and_then(R::label_static),
None
);
assert_eq!(R::classify_arity(3).and_then(R::label_static), None);
assert_eq!(R::classify_arity(17).and_then(R::label_static), None);
}
#[test]
fn label_delegates_static_arms_through_label_static() {
use OptionalParamMalformedReason as R;
for (variant, static_bytes) in [
(R::EmptyList, R::EMPTY_LIST_LABEL),
(R::MissingDefault, R::MISSING_DEFAULT_LABEL),
(R::NonSymbolName, R::NON_SYMBOL_NAME_LABEL),
] {
assert_eq!(variant.label_static(), Some(static_bytes));
assert_eq!(variant.label(), static_bytes.to_string());
}
let extra = R::ExtraElements { length: 5 };
assert_eq!(extra.label_static(), None);
assert_eq!(
extra.label(),
format!("5 elements (need {})", R::OPTIONAL_PARAM_SPEC_ARITY)
);
}
#[test]
fn optional_param_malformed_reason_extra_elements_descriptor_pins_canonical_noun_bytes() {
assert_eq!(
OptionalParamMalformedReason::EXTRA_ELEMENTS_DESCRIPTOR,
"elements"
);
}
#[test]
fn optional_param_malformed_reason_dynamic_descriptors_forced_arity_pins_1_of_4_carving() {
assert_eq!(OptionalParamMalformedReason::DYNAMIC_DESCRIPTORS.len(), 1);
assert_eq!(
OptionalParamMalformedReason::DYNAMIC_DESCRIPTORS,
[OptionalParamMalformedReason::EXTRA_ELEMENTS_DESCRIPTOR]
);
}
#[test]
fn descriptor_dynamic_projects_dynamic_subset_to_typed_option() {
use OptionalParamMalformedReason as R;
assert_eq!(
R::ExtraElements { length: 3 }.descriptor_dynamic(),
Some(R::EXTRA_ELEMENTS_DESCRIPTOR)
);
assert_eq!(
R::ExtraElements { length: 0 }.descriptor_dynamic(),
Some(R::EXTRA_ELEMENTS_DESCRIPTOR)
);
assert_eq!(
R::ExtraElements { length: 42 }.descriptor_dynamic(),
Some(R::EXTRA_ELEMENTS_DESCRIPTOR)
);
assert_eq!(R::EmptyList.descriptor_dynamic(), None);
assert_eq!(R::MissingDefault.descriptor_dynamic(), None);
assert_eq!(R::NonSymbolName.descriptor_dynamic(), None);
}
#[test]
fn descriptor_dynamic_matches_dynamic_descriptors_array_in_declaration_order() {
use OptionalParamMalformedReason as R;
let projected = [R::ExtraElements { length: 5 }.descriptor_dynamic().unwrap()];
assert_eq!(projected, R::DYNAMIC_DESCRIPTORS);
}
#[test]
fn label_static_and_descriptor_dynamic_partition_the_closed_set() {
use OptionalParamMalformedReason as R;
for reason in [
R::EmptyList,
R::MissingDefault,
R::NonSymbolName,
R::ExtraElements { length: 3 },
R::ExtraElements { length: 0 },
R::ExtraElements { length: 99 },
] {
let static_hit = reason.label_static().is_some();
let dynamic_hit = reason.descriptor_dynamic().is_some();
assert!(
static_hit ^ dynamic_hit,
"STATIC ⊕ DYNAMIC partition violated for {reason:?}: \
label_static={static_hit}, descriptor_dynamic={dynamic_hit}",
);
}
}
#[test]
fn label_dynamic_composes_descriptor_arity_and_payload_through_shared_template() {
use OptionalParamMalformedReason as R;
assert_eq!(R::EmptyList.label_dynamic(), None);
assert_eq!(R::MissingDefault.label_dynamic(), None);
assert_eq!(R::NonSymbolName.label_dynamic(), None);
assert_eq!(
R::ExtraElements { length: 3 }.label_dynamic(),
Some("3 elements (need 2)".to_string())
);
assert_eq!(
R::ExtraElements { length: 7 }.label_dynamic(),
Some("7 elements (need 2)".to_string())
);
assert_eq!(
R::ExtraElements { length: 0 }.label_dynamic(),
Some("0 elements (need 2)".to_string())
);
assert_eq!(
R::ExtraElements { length: 4 }.label_dynamic().unwrap(),
format!(
"4 {} (need {})",
R::EXTRA_ELEMENTS_DESCRIPTOR,
R::OPTIONAL_PARAM_SPEC_ARITY
)
);
}
#[test]
fn label_composes_static_and_dynamic_projections_over_the_full_partition() {
use OptionalParamMalformedReason as R;
for (reason, expected) in [
(R::EmptyList, R::EMPTY_LIST_LABEL.to_string()),
(R::MissingDefault, R::MISSING_DEFAULT_LABEL.to_string()),
(R::NonSymbolName, R::NON_SYMBOL_NAME_LABEL.to_string()),
(
R::ExtraElements { length: 3 },
"3 elements (need 2)".to_string(),
),
(
R::ExtraElements { length: 42 },
"42 elements (need 2)".to_string(),
),
] {
assert_eq!(reason.label(), expected);
let composed = reason
.label_static()
.map(str::to_string)
.or_else(|| reason.label_dynamic())
.unwrap();
assert_eq!(reason.label(), composed);
}
}
#[test]
fn paren_suffix_owns_the_bare_parenthetical_skeleton() {
assert_eq!(paren_suffix("got 5"), " (got 5)");
assert_eq!(paren_suffix(""), " ()");
}
#[test]
fn every_suffix_helper_wraps_through_one_paren_primitive() {
assert_eq!(
unknown_among_suffix(Some(":t"), "allowed: :name"),
paren_suffix("did you mean :t?; allowed: :name")
);
assert_eq!(
unknown_among_suffix(None, "allowed: :name"),
paren_suffix("allowed: :name")
);
assert_eq!(
rest_param_missing_name_suffix(1, Some("5")),
paren_suffix("rest marker at position 1, got 5")
);
assert_eq!(
rest_param_missing_name_suffix(1, None),
paren_suffix("rest marker at position 1, none provided")
);
assert_eq!(
rest_param_trailing_tokens_suffix(1, 2, "extra"),
paren_suffix("rest marker at position 1, 2 trailing after name, first: extra")
);
assert_eq!(missing_head_symbol_suffix(Some("5")), paren_suffix("got 5"));
assert_eq!(missing_head_symbol_suffix(None), paren_suffix("empty list"));
assert_eq!(
optional_marker_repeated_suffix(1, 3),
paren_suffix("first &optional at position 1, second at position 3")
);
assert_eq!(
optional_param_malformed_suffix(1, OptionalParamMalformedReason::EmptyList),
paren_suffix("position 1, empty list")
);
assert_eq!(
optional_param_malformed_suffix(
2,
OptionalParamMalformedReason::ExtraElements { length: 3 }
),
paren_suffix("position 2, 3 elements (need 2)")
);
}
#[test]
fn unknown_kwarg_and_domain_suffixes_share_one_wrapping_primitive() {
let allowed = vec!["name".to_string(), "threshold".to_string()];
assert_eq!(
unknown_kwarg_suffix(Some("threshold"), &allowed),
unknown_among_suffix(Some(":threshold"), "allowed: :name, :threshold")
);
assert_eq!(
unknown_kwarg_suffix(None, &allowed),
unknown_among_suffix(None, "allowed: :name, :threshold")
);
let registered = vec!["defmonitor".to_string(), "defnotify".to_string()];
assert_eq!(
unknown_domain_keyword_suffix(Some("defmonitor"), ®istered),
unknown_among_suffix(
Some("(defmonitor ...)"),
"registered: defmonitor, defnotify"
)
);
assert_eq!(
unknown_domain_keyword_suffix(None, &[]),
unknown_among_suffix(None, "no domains registered")
);
}
#[test]
fn non_symbol_unquote_target_display_renders_canonical_type_mismatch_shape() {
let err = LispError::NonSymbolUnquoteTarget {
prefix: UnquoteForm::Unquote,
got: SexpWitness::new(SexpShape::List, "(list 1 2)"),
};
assert_eq!(
format!("{err}"),
"compile error in ,: expected symbol, got (list 1 2)"
);
}
#[test]
fn non_symbol_unquote_target_display_preserves_splice_prefix() {
let err = LispError::NonSymbolUnquoteTarget {
prefix: UnquoteForm::Splice,
got: SexpWitness::new(SexpShape::Int, "5"),
};
assert_eq!(
format!("{err}"),
"compile error in ,@: expected symbol, got 5"
);
}
#[test]
fn non_symbol_unquote_target_display_carries_keyword_atom_unchanged() {
let err = LispError::NonSymbolUnquoteTarget {
prefix: UnquoteForm::Unquote,
got: SexpWitness::new(SexpShape::Keyword, ":foo"),
};
assert_eq!(
format!("{err}"),
"compile error in ,: expected symbol, got :foo"
);
}
#[test]
fn splice_outside_list_display_renders_legacy_substring_with_offending_form() {
let err = LispError::SpliceOutsideList {
got: SexpWitness::new(SexpShape::Symbol, "xs"),
};
assert_eq!(
format!("{err}"),
"compile error in ,@: `,@` may only appear inside a list (got ,@xs)"
);
}
#[test]
fn splice_outside_list_display_carries_list_literal_unchanged() {
let err = LispError::SpliceOutsideList {
got: SexpWitness::new(SexpShape::List, "(list 1 2)"),
};
assert_eq!(
format!("{err}"),
"compile error in ,@: `,@` may only appear inside a list (got ,@(list 1 2))"
);
}
#[test]
fn splice_outside_list_display_carries_kebab_case_symbol_unchanged() {
let err = LispError::SpliceOutsideList {
got: SexpWitness::new(SexpShape::Symbol, "notify-ref"),
};
assert_eq!(
format!("{err}"),
"compile error in ,@: `,@` may only appear inside a list (got ,@notify-ref)"
);
}
#[test]
fn splice_outside_list_display_preserves_legacy_substring_for_message_grep() {
let err = LispError::SpliceOutsideList {
got: SexpWitness::new(SexpShape::Symbol, "xs"),
};
let msg = format!("{err}");
assert!(
msg.contains("`,@` may only appear inside a list"),
"expected legacy substring in message, got: {msg}"
);
assert!(
msg.contains("(got ,@xs)"),
"expected offending-form parenthetical in message, got: {msg}"
);
}
#[test]
fn sexp_witness_display_writes_only_the_display_field() {
let w = SexpWitness::new(SexpShape::Symbol, "xs");
assert_eq!(format!("{w}"), "xs");
let w = SexpWitness::new(SexpShape::List, "(list 1 2)");
assert_eq!(format!("{w}"), "(list 1 2)");
let w = SexpWitness::new(SexpShape::Int, "5");
assert_eq!(format!("{w}"), "5");
let w = SexpWitness::new(SexpShape::Keyword, ":foo");
assert_eq!(format!("{w}"), ":foo");
}
#[test]
fn sexp_witness_carries_both_shape_and_display_jointly() {
let w = SexpWitness::new(SexpShape::Int, "5");
assert_eq!(w.shape, SexpShape::Int);
assert_eq!(w.display, "5");
let w_sym = SexpWitness::new(SexpShape::Symbol, "5");
assert_eq!(w_sym.shape, SexpShape::Symbol);
assert_eq!(w_sym.display, "5");
assert_ne!(
w, w_sym,
"Witnesses with same display but different shape must NOT be equal — typed shape is load-bearing data",
);
}
#[test]
fn splice_outside_list_got_carries_typed_witness_through_variant_slot() {
let err = LispError::SpliceOutsideList {
got: SexpWitness::new(SexpShape::List, "(list 1 2)"),
};
match &err {
LispError::SpliceOutsideList { got } => {
assert_eq!(got.shape, SexpShape::List);
assert_eq!(got.display, "(list 1 2)");
}
other => panic!("expected SpliceOutsideList, got {other:?}"),
}
assert_eq!(
format!("{err}"),
"compile error in ,@: `,@` may only appear inside a list (got ,@(list 1 2))"
);
}
#[test]
fn splice_outside_list_got_distinguishes_symbol_from_list_at_variant_slot() {
let err_sym = LispError::SpliceOutsideList {
got: SexpWitness::new(SexpShape::Symbol, "xs"),
};
let err_list = LispError::SpliceOutsideList {
got: SexpWitness::new(SexpShape::List, "(list 1 2)"),
};
let (sym_shape, list_shape) = (
match &err_sym {
LispError::SpliceOutsideList { got } => got.shape,
_ => unreachable!(),
},
match &err_list {
LispError::SpliceOutsideList { got } => got.shape,
_ => unreachable!(),
},
);
assert_ne!(
sym_shape, list_shape,
"Symbol and List witnesses must remain structurally distinct at the variant slot",
);
assert_eq!(sym_shape, SexpShape::Symbol);
assert_eq!(list_shape, SexpShape::List);
}
#[test]
fn non_symbol_unquote_target_got_carries_typed_witness_through_variant_slot() {
let err = LispError::NonSymbolUnquoteTarget {
prefix: UnquoteForm::Unquote,
got: SexpWitness::new(SexpShape::List, "(list 1 2)"),
};
match &err {
LispError::NonSymbolUnquoteTarget { prefix, got } => {
assert_eq!(*prefix, UnquoteForm::Unquote);
assert_eq!(got.shape, SexpShape::List);
assert_eq!(got.display, "(list 1 2)");
}
other => panic!("expected NonSymbolUnquoteTarget, got {other:?}"),
}
assert_eq!(
format!("{err}"),
"compile error in ,: expected symbol, got (list 1 2)"
);
}
#[test]
fn non_symbol_unquote_target_got_distinguishes_int_from_keyword_at_variant_slot() {
let err_int = LispError::NonSymbolUnquoteTarget {
prefix: UnquoteForm::Unquote,
got: SexpWitness::new(SexpShape::Int, "5"),
};
let err_kw = LispError::NonSymbolUnquoteTarget {
prefix: UnquoteForm::Unquote,
got: SexpWitness::new(SexpShape::Keyword, ":foo"),
};
let (int_shape, kw_shape) = (
match &err_int {
LispError::NonSymbolUnquoteTarget { got, .. } => got.shape,
_ => unreachable!(),
},
match &err_kw {
LispError::NonSymbolUnquoteTarget { got, .. } => got.shape,
_ => unreachable!(),
},
);
assert_ne!(
int_shape, kw_shape,
"Int and Keyword witnesses must remain structurally distinct at the variant slot",
);
assert_eq!(int_shape, SexpShape::Int);
assert_eq!(kw_shape, SexpShape::Keyword);
}
#[test]
fn non_symbol_unquote_target_and_splice_outside_list_share_one_witness_primitive() {
let same_witness = SexpWitness::new(SexpShape::List, "(list 1 2)");
let non_symbol_target = LispError::NonSymbolUnquoteTarget {
prefix: UnquoteForm::Splice,
got: same_witness.clone(),
};
let splice_outside = LispError::SpliceOutsideList {
got: same_witness.clone(),
};
match (&non_symbol_target, &splice_outside) {
(
LispError::NonSymbolUnquoteTarget { got: lhs_got, .. },
LispError::SpliceOutsideList { got: rhs_got },
) => {
assert_eq!(lhs_got.shape, rhs_got.shape);
assert_eq!(lhs_got.display, rhs_got.display);
assert_eq!(*lhs_got, same_witness);
assert_eq!(*rhs_got, same_witness);
}
_ => unreachable!(),
}
}
#[test]
fn non_symbol_param_got_carries_typed_witness_through_variant_slot() {
let err = LispError::NonSymbolParam {
position: 1,
got: SexpWitness::new(SexpShape::Int, "5"),
};
match &err {
LispError::NonSymbolParam { position, got } => {
assert_eq!(*position, 1);
assert_eq!(got.shape, SexpShape::Int);
assert_eq!(got.display, "5");
}
other => panic!("expected NonSymbolParam, got {other:?}"),
}
assert_eq!(
format!("{err}"),
"compile error in defmacro params: expected symbol at \
position 1, got 5"
);
}
#[test]
fn non_symbol_param_got_distinguishes_int_from_keyword_at_variant_slot() {
let err_int = LispError::NonSymbolParam {
position: 0,
got: SexpWitness::new(SexpShape::Int, "5"),
};
let err_kw = LispError::NonSymbolParam {
position: 0,
got: SexpWitness::new(SexpShape::Keyword, ":foo"),
};
let (int_shape, kw_shape) = (
match &err_int {
LispError::NonSymbolParam { got, .. } => got.shape,
_ => unreachable!(),
},
match &err_kw {
LispError::NonSymbolParam { got, .. } => got.shape,
_ => unreachable!(),
},
);
assert_ne!(
int_shape, kw_shape,
"Int and Keyword witnesses must remain structurally distinct at the variant slot",
);
assert_eq!(int_shape, SexpShape::Int);
assert_eq!(kw_shape, SexpShape::Keyword);
}
#[test]
fn non_symbol_param_and_template_gate_share_one_witness_primitive() {
let same_witness = SexpWitness::new(SexpShape::List, "(nested)");
let non_symbol_param = LispError::NonSymbolParam {
position: 0,
got: same_witness.clone(),
};
let non_symbol_target = LispError::NonSymbolUnquoteTarget {
prefix: UnquoteForm::Unquote,
got: same_witness.clone(),
};
let splice_outside = LispError::SpliceOutsideList {
got: same_witness.clone(),
};
match (&non_symbol_param, &non_symbol_target, &splice_outside) {
(
LispError::NonSymbolParam { got: a, .. },
LispError::NonSymbolUnquoteTarget { got: b, .. },
LispError::SpliceOutsideList { got: c },
) => {
assert_eq!(a.shape, b.shape);
assert_eq!(b.shape, c.shape);
assert_eq!(a.display, b.display);
assert_eq!(b.display, c.display);
assert_eq!(*a, same_witness);
assert_eq!(*b, same_witness);
assert_eq!(*c, same_witness);
}
_ => unreachable!(),
}
}
#[test]
fn defmacro_non_symbol_name_got_carries_typed_witness_through_variant_slot() {
let err = LispError::DefmacroNonSymbolName {
head: MacroDefHead::Defmacro,
got: SexpWitness::new(SexpShape::Int, "5"),
};
match &err {
LispError::DefmacroNonSymbolName { head, got } => {
assert_eq!(*head, MacroDefHead::Defmacro);
assert_eq!(got.shape, SexpShape::Int);
assert_eq!(got.display, "5");
}
other => panic!("expected DefmacroNonSymbolName, got {other:?}"),
}
assert_eq!(
format!("{err}"),
"compile error in defmacro: expected name symbol, got 5"
);
}
#[test]
fn defmacro_non_symbol_name_got_distinguishes_int_from_keyword_at_variant_slot() {
let err_int = LispError::DefmacroNonSymbolName {
head: MacroDefHead::Defmacro,
got: SexpWitness::new(SexpShape::Int, "5"),
};
let err_kw = LispError::DefmacroNonSymbolName {
head: MacroDefHead::Defmacro,
got: SexpWitness::new(SexpShape::Keyword, ":foo"),
};
let (int_shape, kw_shape) = (
match &err_int {
LispError::DefmacroNonSymbolName { got, .. } => got.shape,
_ => unreachable!(),
},
match &err_kw {
LispError::DefmacroNonSymbolName { got, .. } => got.shape,
_ => unreachable!(),
},
);
assert_ne!(
int_shape, kw_shape,
"Int and Keyword witnesses must remain structurally distinct at the variant slot",
);
assert_eq!(int_shape, SexpShape::Int);
assert_eq!(kw_shape, SexpShape::Keyword);
}
#[test]
fn defmacro_non_symbol_name_and_param_gate_share_one_witness_primitive() {
let same_witness = SexpWitness::new(SexpShape::List, "(nested)");
let defmacro_non_symbol_name = LispError::DefmacroNonSymbolName {
head: MacroDefHead::Defmacro,
got: same_witness.clone(),
};
let non_symbol_param = LispError::NonSymbolParam {
position: 0,
got: same_witness.clone(),
};
let non_symbol_target = LispError::NonSymbolUnquoteTarget {
prefix: UnquoteForm::Unquote,
got: same_witness.clone(),
};
let splice_outside = LispError::SpliceOutsideList {
got: same_witness.clone(),
};
match (
&defmacro_non_symbol_name,
&non_symbol_param,
&non_symbol_target,
&splice_outside,
) {
(
LispError::DefmacroNonSymbolName { got: a, .. },
LispError::NonSymbolParam { got: b, .. },
LispError::NonSymbolUnquoteTarget { got: c, .. },
LispError::SpliceOutsideList { got: d },
) => {
assert_eq!(a.shape, b.shape);
assert_eq!(b.shape, c.shape);
assert_eq!(c.shape, d.shape);
assert_eq!(a.display, b.display);
assert_eq!(b.display, c.display);
assert_eq!(c.display, d.display);
assert_eq!(*a, same_witness);
assert_eq!(*b, same_witness);
assert_eq!(*c, same_witness);
assert_eq!(*d, same_witness);
}
_ => unreachable!(),
}
}
#[test]
fn defmacro_non_list_params_got_carries_typed_witness_through_variant_slot() {
let err = LispError::DefmacroNonListParams {
head: MacroDefHead::Defmacro,
got: SexpWitness::new(SexpShape::Symbol, "x"),
};
match &err {
LispError::DefmacroNonListParams { head, got } => {
assert_eq!(*head, MacroDefHead::Defmacro);
assert_eq!(got.shape, SexpShape::Symbol);
assert_eq!(got.display, "x");
}
other => panic!("expected DefmacroNonListParams, got {other:?}"),
}
assert_eq!(
format!("{err}"),
"compile error in defmacro: expected param list, got x"
);
}
#[test]
fn defmacro_non_list_params_got_distinguishes_symbol_from_int_at_variant_slot() {
let err_sym = LispError::DefmacroNonListParams {
head: MacroDefHead::Defmacro,
got: SexpWitness::new(SexpShape::Symbol, "x"),
};
let err_int = LispError::DefmacroNonListParams {
head: MacroDefHead::Defmacro,
got: SexpWitness::new(SexpShape::Int, "5"),
};
let (sym_shape, int_shape) = (
match &err_sym {
LispError::DefmacroNonListParams { got, .. } => got.shape,
_ => unreachable!(),
},
match &err_int {
LispError::DefmacroNonListParams { got, .. } => got.shape,
_ => unreachable!(),
},
);
assert_ne!(
sym_shape, int_shape,
"Symbol and Int witnesses must remain structurally distinct at the variant slot",
);
assert_eq!(sym_shape, SexpShape::Symbol);
assert_eq!(int_shape, SexpShape::Int);
}
#[test]
fn defmacro_non_list_params_and_name_gate_share_one_witness_primitive() {
let same_witness = SexpWitness::new(SexpShape::List, "(nested)");
let defmacro_non_list_params = LispError::DefmacroNonListParams {
head: MacroDefHead::Defmacro,
got: same_witness.clone(),
};
let defmacro_non_symbol_name = LispError::DefmacroNonSymbolName {
head: MacroDefHead::Defmacro,
got: same_witness.clone(),
};
let non_symbol_param = LispError::NonSymbolParam {
position: 0,
got: same_witness.clone(),
};
let non_symbol_target = LispError::NonSymbolUnquoteTarget {
prefix: UnquoteForm::Unquote,
got: same_witness.clone(),
};
let splice_outside = LispError::SpliceOutsideList {
got: same_witness.clone(),
};
match (
&defmacro_non_list_params,
&defmacro_non_symbol_name,
&non_symbol_param,
&non_symbol_target,
&splice_outside,
) {
(
LispError::DefmacroNonListParams { got: a, .. },
LispError::DefmacroNonSymbolName { got: b, .. },
LispError::NonSymbolParam { got: c, .. },
LispError::NonSymbolUnquoteTarget { got: d, .. },
LispError::SpliceOutsideList { got: e },
) => {
assert_eq!(a.shape, b.shape);
assert_eq!(b.shape, c.shape);
assert_eq!(c.shape, d.shape);
assert_eq!(d.shape, e.shape);
assert_eq!(a.display, b.display);
assert_eq!(b.display, c.display);
assert_eq!(c.display, d.display);
assert_eq!(d.display, e.display);
assert_eq!(*a, same_witness);
assert_eq!(*b, same_witness);
assert_eq!(*c, same_witness);
assert_eq!(*d, same_witness);
assert_eq!(*e, same_witness);
}
_ => unreachable!(),
}
}
#[test]
fn rest_param_missing_name_got_carries_typed_witness_through_variant_slot() {
let err = LispError::RestParamMissingName {
rest_position: 1,
got: Some(SexpWitness::new(SexpShape::Int, "5")),
};
match &err {
LispError::RestParamMissingName { rest_position, got } => {
assert_eq!(*rest_position, 1);
let witness = got.as_ref().expect("got must be Some");
assert_eq!(witness.shape, SexpShape::Int);
assert_eq!(witness.display, "5");
}
other => panic!("expected RestParamMissingName, got {other:?}"),
}
assert_eq!(
format!("{err}"),
"compile error in defmacro params: &rest needs a name \
(rest marker at position 1, got 5)"
);
}
#[test]
fn rest_param_missing_name_got_distinguishes_int_from_keyword_at_variant_slot() {
let err_int = LispError::RestParamMissingName {
rest_position: 0,
got: Some(SexpWitness::new(SexpShape::Int, "5")),
};
let err_kw = LispError::RestParamMissingName {
rest_position: 0,
got: Some(SexpWitness::new(SexpShape::Keyword, ":foo")),
};
let (int_shape, kw_shape) = (
match &err_int {
LispError::RestParamMissingName { got: Some(w), .. } => w.shape,
_ => unreachable!(),
},
match &err_kw {
LispError::RestParamMissingName { got: Some(w), .. } => w.shape,
_ => unreachable!(),
},
);
assert_ne!(
int_shape, kw_shape,
"Int and Keyword witnesses must remain structurally distinct at the variant slot",
);
assert_eq!(int_shape, SexpShape::Int);
assert_eq!(kw_shape, SexpShape::Keyword);
}
#[test]
fn rest_param_missing_name_and_macro_def_gate_share_one_witness_primitive() {
let same_witness = SexpWitness::new(SexpShape::List, "(nested)");
let rest_param_missing_name = LispError::RestParamMissingName {
rest_position: 0,
got: Some(same_witness.clone()),
};
let defmacro_non_list_params = LispError::DefmacroNonListParams {
head: MacroDefHead::Defmacro,
got: same_witness.clone(),
};
let defmacro_non_symbol_name = LispError::DefmacroNonSymbolName {
head: MacroDefHead::Defmacro,
got: same_witness.clone(),
};
let non_symbol_param = LispError::NonSymbolParam {
position: 0,
got: same_witness.clone(),
};
let non_symbol_target = LispError::NonSymbolUnquoteTarget {
prefix: UnquoteForm::Unquote,
got: same_witness.clone(),
};
let splice_outside = LispError::SpliceOutsideList {
got: same_witness.clone(),
};
match (
&rest_param_missing_name,
&defmacro_non_list_params,
&defmacro_non_symbol_name,
&non_symbol_param,
&non_symbol_target,
&splice_outside,
) {
(
LispError::RestParamMissingName { got: Some(a), .. },
LispError::DefmacroNonListParams { got: b, .. },
LispError::DefmacroNonSymbolName { got: c, .. },
LispError::NonSymbolParam { got: d, .. },
LispError::NonSymbolUnquoteTarget { got: e, .. },
LispError::SpliceOutsideList { got: f },
) => {
assert_eq!(a.shape, b.shape);
assert_eq!(b.shape, c.shape);
assert_eq!(c.shape, d.shape);
assert_eq!(d.shape, e.shape);
assert_eq!(e.shape, f.shape);
assert_eq!(a.display, b.display);
assert_eq!(b.display, c.display);
assert_eq!(c.display, d.display);
assert_eq!(d.display, e.display);
assert_eq!(e.display, f.display);
assert_eq!(*a, same_witness);
assert_eq!(*b, same_witness);
assert_eq!(*c, same_witness);
assert_eq!(*d, same_witness);
assert_eq!(*e, same_witness);
assert_eq!(*f, same_witness);
}
_ => unreachable!(),
}
}
#[test]
fn missing_macro_arg_display_matches_legacy_compile_shape() {
let err = LispError::MissingMacroArg {
macro_name: "wrap".into(),
param: "b".into(),
};
assert_eq!(
format!("{err}"),
"compile error in call to wrap: missing required arg: b"
);
}
#[test]
fn missing_macro_arg_display_carries_kebab_case_names_unchanged() {
let err = LispError::MissingMacroArg {
macro_name: "wrap-twice".into(),
param: "notify-ref".into(),
};
assert_eq!(
format!("{err}"),
"compile error in call to wrap-twice: \
missing required arg: notify-ref"
);
}
#[test]
fn missing_macro_arg_display_preserves_legacy_substring_for_message_grep() {
let err = LispError::MissingMacroArg {
macro_name: "f".into(),
param: "x".into(),
};
let msg = format!("{err}");
assert!(
msg.contains("missing required arg: x"),
"expected legacy substring in message, got: {msg}"
);
assert!(
msg.contains("call to f"),
"expected call-to clause in message, got: {msg}"
);
}
#[test]
fn non_symbol_param_display_carries_position_and_got() {
let err = LispError::NonSymbolParam {
position: 1,
got: SexpWitness::new(SexpShape::Int, "5"),
};
assert_eq!(
format!("{err}"),
"compile error in defmacro params: \
expected symbol at position 1, got 5"
);
}
#[test]
fn non_symbol_param_display_preserves_legacy_substring_for_message_grep() {
let err = LispError::NonSymbolParam {
position: 0,
got: SexpWitness::new(SexpShape::List, "(nested)"),
};
let msg = format!("{err}");
assert!(
msg.contains("defmacro params"),
"expected legacy form label in message, got: {msg}"
);
assert!(
msg.contains("expected symbol"),
"expected legacy substring in message, got: {msg}"
);
}
#[test]
fn non_symbol_param_display_carries_keyword_got_unchanged() {
let err = LispError::NonSymbolParam {
position: 2,
got: SexpWitness::new(SexpShape::Keyword, ":k"),
};
assert_eq!(
format!("{err}"),
"compile error in defmacro params: \
expected symbol at position 2, got :k"
);
}
#[test]
fn rest_param_missing_name_display_with_got_renders_marker_position_and_got() {
let err = LispError::RestParamMissingName {
rest_position: 1,
got: Some(SexpWitness::new(SexpShape::Int, "5")),
};
assert_eq!(
format!("{err}"),
"compile error in defmacro params: &rest needs a name \
(rest marker at position 1, got 5)"
);
}
#[test]
fn rest_param_missing_name_display_without_got_renders_marker_position_only() {
let err = LispError::RestParamMissingName {
rest_position: 1,
got: None,
};
assert_eq!(
format!("{err}"),
"compile error in defmacro params: &rest needs a name \
(rest marker at position 1, none provided)"
);
}
#[test]
fn rest_param_missing_name_display_preserves_legacy_substring_for_message_grep() {
let err = LispError::RestParamMissingName {
rest_position: 0,
got: None,
};
let msg = format!("{err}");
assert!(
msg.contains("defmacro params"),
"expected legacy form label in message, got: {msg}"
);
assert!(
msg.contains("&rest needs a name"),
"expected legacy substring in message, got: {msg}"
);
}
#[test]
fn rest_param_missing_name_display_carries_keyword_got_unchanged() {
let err = LispError::RestParamMissingName {
rest_position: 2,
got: Some(SexpWitness::new(SexpShape::Keyword, ":foo")),
};
assert_eq!(
format!("{err}"),
"compile error in defmacro params: &rest needs a name \
(rest marker at position 2, got :foo)"
);
}
#[test]
fn defmacro_arity_display_with_defmacro_head_renders_arity_and_legacy_template() {
let err = LispError::DefmacroArity {
head: MacroDefHead::Defmacro,
arity: 1,
};
assert_eq!(
format!("{err}"),
"compile error in defmacro: (defmacro name (params) body) required \
(got 1 elements, need 4)"
);
}
#[test]
fn defmacro_arity_display_carries_defpoint_template_head_unchanged() {
let err = LispError::DefmacroArity {
head: MacroDefHead::DefpointTemplate,
arity: 2,
};
assert_eq!(
format!("{err}"),
"compile error in defpoint-template: \
(defmacro name (params) body) required \
(got 2 elements, need 4)"
);
}
#[test]
fn defmacro_arity_display_carries_defcheck_head_unchanged() {
let err = LispError::DefmacroArity {
head: MacroDefHead::Defcheck,
arity: 3,
};
assert_eq!(
format!("{err}"),
"compile error in defcheck: (defmacro name (params) body) required \
(got 3 elements, need 4)"
);
}
#[test]
fn defmacro_arity_display_preserves_legacy_substring_for_message_grep() {
let err = LispError::DefmacroArity {
head: MacroDefHead::Defmacro,
arity: 0,
};
let msg = format!("{err}");
assert!(
msg.contains("(defmacro name (params) body) required"),
"expected legacy template substring in message, got: {msg}"
);
assert!(
msg.contains("compile error in defmacro:"),
"expected legacy form-label prefix in message, got: {msg}"
);
}
#[test]
fn defmacro_non_symbol_name_display_with_int_got_renders_legacy_prefix_and_got() {
let err = LispError::DefmacroNonSymbolName {
head: MacroDefHead::Defmacro,
got: SexpWitness::new(SexpShape::Int, "5"),
};
assert_eq!(
format!("{err}"),
"compile error in defmacro: expected name symbol, got 5"
);
}
#[test]
fn defmacro_non_symbol_name_display_carries_defpoint_template_head_unchanged() {
let err = LispError::DefmacroNonSymbolName {
head: MacroDefHead::DefpointTemplate,
got: SexpWitness::new(SexpShape::Keyword, ":foo"),
};
assert_eq!(
format!("{err}"),
"compile error in defpoint-template: expected name symbol, got :foo"
);
}
#[test]
fn defmacro_non_symbol_name_display_carries_defcheck_head_unchanged() {
let err = LispError::DefmacroNonSymbolName {
head: MacroDefHead::Defcheck,
got: SexpWitness::new(SexpShape::List, "(nested)"),
};
assert_eq!(
format!("{err}"),
"compile error in defcheck: expected name symbol, got (nested)"
);
}
#[test]
fn defmacro_non_symbol_name_display_carries_string_got_unchanged() {
let err = LispError::DefmacroNonSymbolName {
head: MacroDefHead::Defmacro,
got: SexpWitness::new(SexpShape::String, "\"name\""),
};
assert_eq!(
format!("{err}"),
"compile error in defmacro: expected name symbol, got \"name\""
);
}
#[test]
fn defmacro_non_symbol_name_display_preserves_legacy_substring_for_message_grep() {
let err = LispError::DefmacroNonSymbolName {
head: MacroDefHead::Defmacro,
got: SexpWitness::new(SexpShape::Int, "5"),
};
let msg = format!("{err}");
assert!(
msg.contains("expected name symbol"),
"expected legacy substring in message, got: {msg}"
);
assert!(
msg.contains("compile error in defmacro:"),
"expected legacy form-label prefix in message, got: {msg}"
);
}
#[test]
fn defmacro_non_list_params_display_with_symbol_got_renders_legacy_prefix_and_got() {
let err = LispError::DefmacroNonListParams {
head: MacroDefHead::Defmacro,
got: SexpWitness::new(SexpShape::Symbol, "x"),
};
assert_eq!(
format!("{err}"),
"compile error in defmacro: expected param list, got x"
);
}
#[test]
fn defmacro_non_list_params_display_carries_defpoint_template_head_unchanged() {
let err = LispError::DefmacroNonListParams {
head: MacroDefHead::DefpointTemplate,
got: SexpWitness::new(SexpShape::Int, "5"),
};
assert_eq!(
format!("{err}"),
"compile error in defpoint-template: expected param list, got 5"
);
}
#[test]
fn defmacro_non_list_params_display_carries_defcheck_head_unchanged() {
let err = LispError::DefmacroNonListParams {
head: MacroDefHead::Defcheck,
got: SexpWitness::new(SexpShape::Keyword, ":k"),
};
assert_eq!(
format!("{err}"),
"compile error in defcheck: expected param list, got :k"
);
}
#[test]
fn defmacro_non_list_params_display_carries_string_got_unchanged() {
let err = LispError::DefmacroNonListParams {
head: MacroDefHead::Defmacro,
got: SexpWitness::new(SexpShape::String, "\"params\""),
};
assert_eq!(
format!("{err}"),
"compile error in defmacro: expected param list, got \"params\""
);
}
#[test]
fn defmacro_non_list_params_display_preserves_legacy_substring_for_message_grep() {
let err = LispError::DefmacroNonListParams {
head: MacroDefHead::Defmacro,
got: SexpWitness::new(SexpShape::Symbol, "x"),
};
let msg = format!("{err}");
assert!(
msg.contains("expected param list"),
"expected legacy substring in message, got: {msg}"
);
assert!(
msg.contains("compile error in defmacro:"),
"expected legacy form-label prefix in message, got: {msg}"
);
}
#[test]
fn unbound_template_var_display_preserves_splice_prefix_in_hint() {
let err = LispError::UnboundTemplateVar {
prefix: UnquoteForm::Splice,
name: "rsts".into(),
hint: Some("rest".into()),
};
assert_eq!(
format!("{err}"),
"compile error in ,@rsts: unbound; did you mean ,@rest?"
);
}
#[test]
fn named_form_missing_name_display_renders_legacy_compile_shape() {
let err = LispError::NamedFormMissingName {
keyword: "defpoint",
};
assert_eq!(
format!("{err}"),
"compile error in defpoint: expected (defpoint NAME …)"
);
}
#[test]
fn named_form_missing_name_display_carries_defalertpolicy_keyword_unchanged() {
let err = LispError::NamedFormMissingName {
keyword: "defalertpolicy",
};
assert_eq!(
format!("{err}"),
"compile error in defalertpolicy: expected (defalertpolicy NAME …)"
);
}
#[test]
fn named_form_missing_name_display_carries_kebab_case_keyword_unchanged() {
let err = LispError::NamedFormMissingName {
keyword: "defprocess-spec",
};
assert_eq!(
format!("{err}"),
"compile error in defprocess-spec: expected (defprocess-spec NAME …)"
);
}
#[test]
fn named_form_missing_name_display_preserves_unicode_ellipsis_byte_for_byte() {
let err = LispError::NamedFormMissingName {
keyword: "defmonitor",
};
let msg = format!("{err}");
assert!(
msg.contains('\u{2026}'),
"expected Unicode horizontal-ellipsis (U+2026) in message, got: {msg}"
);
assert!(
!msg.contains("..."),
"expected no ASCII three-dot sequence in message, got: {msg}"
);
}
#[test]
fn named_form_missing_name_display_preserves_legacy_substring_for_message_grep() {
let err = LispError::NamedFormMissingName {
keyword: "defmonitor",
};
let msg = format!("{err}");
assert!(
msg.contains("expected (defmonitor NAME"),
"expected legacy form-label prefix in message, got: {msg}"
);
assert!(
msg.contains("compile error in defmonitor:"),
"expected legacy form-label prefix in message, got: {msg}"
);
}
#[test]
fn named_form_non_symbol_name_display_renders_legacy_prefix_with_int_got() {
let err = LispError::NamedFormNonSymbolName {
keyword: "defpoint",
got: SexpShape::Int,
};
assert_eq!(
format!("{err}"),
"compile error in defpoint: positional NAME must be a symbol or string (got int)"
);
}
#[test]
fn named_form_non_symbol_name_display_carries_keyword_got_unchanged() {
let err = LispError::NamedFormNonSymbolName {
keyword: "defpoint",
got: SexpShape::Keyword,
};
assert_eq!(
format!("{err}"),
"compile error in defpoint: positional NAME must be a symbol or string (got keyword)"
);
}
#[test]
fn named_form_non_symbol_name_display_carries_list_got_unchanged() {
let err = LispError::NamedFormNonSymbolName {
keyword: "defalertpolicy",
got: SexpShape::List,
};
assert_eq!(
format!("{err}"),
"compile error in defalertpolicy: positional NAME must be a symbol or string (got list)"
);
}
#[test]
fn named_form_non_symbol_name_display_carries_kebab_case_keyword_unchanged() {
let err = LispError::NamedFormNonSymbolName {
keyword: "defprocess-spec",
got: SexpShape::Int,
};
assert_eq!(
format!("{err}"),
"compile error in defprocess-spec: positional NAME must be a symbol or string (got int)"
);
}
#[test]
fn named_form_non_symbol_name_display_preserves_legacy_substring_for_message_grep() {
let err = LispError::NamedFormNonSymbolName {
keyword: "defmonitor",
got: SexpShape::Int,
};
let msg = format!("{err}");
assert!(
msg.contains("positional NAME must be a symbol or string"),
"expected legacy substring in message, got: {msg}"
);
assert!(
msg.contains("compile error in defmonitor:"),
"expected legacy form-label prefix in message, got: {msg}"
);
}
#[test]
fn rewriter_non_list_display_renders_legacy_shape_with_int_got() {
let err = LispError::RewriterNonList {
keyword: "defmonitor",
got: SexpWitness::new(SexpShape::Int, "42"),
};
assert_eq!(
format!("{err}"),
"compile error in defmonitor: rewriter must return a list; got 42"
);
}
#[test]
fn rewriter_non_list_display_carries_symbol_got_unchanged() {
let err = LispError::RewriterNonList {
keyword: "defmonitor",
got: SexpWitness::new(SexpShape::Symbol, "not-a-list"),
};
assert_eq!(
format!("{err}"),
"compile error in defmonitor: rewriter must return a list; got not-a-list"
);
}
#[test]
fn rewriter_non_list_display_carries_nil_got_as_paren_paren() {
let err = LispError::RewriterNonList {
keyword: "defmonitor",
got: SexpWitness::new(SexpShape::Nil, "()"),
};
assert_eq!(
format!("{err}"),
"compile error in defmonitor: rewriter must return a list; got ()"
);
}
#[test]
fn rewriter_non_list_display_carries_kebab_case_keyword_unchanged() {
let err = LispError::RewriterNonList {
keyword: "defprocess-spec",
got: SexpWitness::new(SexpShape::Int, "7"),
};
assert_eq!(
format!("{err}"),
"compile error in defprocess-spec: rewriter must return a list; got 7"
);
}
#[test]
fn rewriter_non_list_display_preserves_legacy_substring_for_message_grep() {
let err = LispError::RewriterNonList {
keyword: "defmonitor",
got: SexpWitness::new(SexpShape::Int, "42"),
};
let msg = format!("{err}");
assert!(
msg.contains("rewriter must return a list; got "),
"expected legacy substring in message, got: {msg}"
);
assert!(
msg.contains("compile error in defmonitor:"),
"expected legacy form-label prefix in message, got: {msg}"
);
}
#[test]
fn rewriter_non_list_position_is_none_today() {
let err = LispError::RewriterNonList {
keyword: "defmonitor",
got: SexpWitness::new(SexpShape::Int, "42"),
};
assert_eq!(err.position(), None);
}
#[test]
fn rewriter_non_list_got_carries_typed_witness_through_variant_slot() {
let err = LispError::RewriterNonList {
keyword: "defmonitor",
got: SexpWitness::new(SexpShape::Int, "42"),
};
match &err {
LispError::RewriterNonList { keyword, got } => {
assert_eq!(*keyword, "defmonitor");
assert_eq!(got.shape, SexpShape::Int);
assert_eq!(got.display, "42");
}
other => panic!("expected RewriterNonList, got {other:?}"),
}
assert_eq!(
format!("{err}"),
"compile error in defmonitor: rewriter must return a list; got 42"
);
}
#[test]
fn rewriter_non_list_got_distinguishes_int_from_keyword_at_variant_slot() {
let err_int = LispError::RewriterNonList {
keyword: "defmonitor",
got: SexpWitness::new(SexpShape::Int, "42"),
};
let err_kw = LispError::RewriterNonList {
keyword: "defmonitor",
got: SexpWitness::new(SexpShape::Keyword, ":foo"),
};
let (int_shape, kw_shape) = (
match &err_int {
LispError::RewriterNonList { got, .. } => got.shape,
_ => unreachable!(),
},
match &err_kw {
LispError::RewriterNonList { got, .. } => got.shape,
_ => unreachable!(),
},
);
assert_ne!(
int_shape, kw_shape,
"Int and Keyword witnesses must remain structurally distinct at the variant slot",
);
assert_eq!(int_shape, SexpShape::Int);
assert_eq!(kw_shape, SexpShape::Keyword);
}
#[test]
fn rewriter_non_list_and_typed_entry_gates_share_one_witness_primitive() {
let same_witness = SexpWitness::new(SexpShape::Int, "42");
let rewriter_non_list = LispError::RewriterNonList {
keyword: "defmonitor",
got: same_witness.clone(),
};
let missing_head = LispError::MissingHeadSymbol {
keyword: "defmonitor",
got: Some(same_witness.clone()),
};
let rest_param_missing_name = LispError::RestParamMissingName {
rest_position: 0,
got: Some(same_witness.clone()),
};
let defmacro_non_list_params = LispError::DefmacroNonListParams {
head: MacroDefHead::Defmacro,
got: same_witness.clone(),
};
let defmacro_non_symbol_name = LispError::DefmacroNonSymbolName {
head: MacroDefHead::Defmacro,
got: same_witness.clone(),
};
let non_symbol_param = LispError::NonSymbolParam {
position: 0,
got: same_witness.clone(),
};
let non_symbol_target = LispError::NonSymbolUnquoteTarget {
prefix: UnquoteForm::Unquote,
got: same_witness.clone(),
};
let splice_outside = LispError::SpliceOutsideList {
got: same_witness.clone(),
};
match (
&rewriter_non_list,
&missing_head,
&rest_param_missing_name,
&defmacro_non_list_params,
&defmacro_non_symbol_name,
&non_symbol_param,
&non_symbol_target,
&splice_outside,
) {
(
LispError::RewriterNonList { got: a, .. },
LispError::MissingHeadSymbol { got: Some(b), .. },
LispError::RestParamMissingName { got: Some(c), .. },
LispError::DefmacroNonListParams { got: d, .. },
LispError::DefmacroNonSymbolName { got: e, .. },
LispError::NonSymbolParam { got: f, .. },
LispError::NonSymbolUnquoteTarget { got: g, .. },
LispError::SpliceOutsideList { got: h },
) => {
assert_eq!(a.shape, b.shape);
assert_eq!(b.shape, c.shape);
assert_eq!(c.shape, d.shape);
assert_eq!(d.shape, e.shape);
assert_eq!(e.shape, f.shape);
assert_eq!(f.shape, g.shape);
assert_eq!(g.shape, h.shape);
assert_eq!(a.display, b.display);
assert_eq!(b.display, c.display);
assert_eq!(c.display, d.display);
assert_eq!(d.display, e.display);
assert_eq!(e.display, f.display);
assert_eq!(f.display, g.display);
assert_eq!(g.display, h.display);
assert_eq!(*a, same_witness);
assert_eq!(*b, same_witness);
assert_eq!(*c, same_witness);
assert_eq!(*d, same_witness);
assert_eq!(*e, same_witness);
assert_eq!(*f, same_witness);
assert_eq!(*g, same_witness);
assert_eq!(*h, same_witness);
}
_ => unreachable!(),
}
}
#[test]
fn domain_serialize_display_renders_legacy_shape_with_short_message() {
let err = LispError::DomainSerialize {
keyword: "defmonitor",
message: "key must be a string".into(),
};
assert_eq!(
format!("{err}"),
"compile error in defmonitor: serialize: key must be a string"
);
}
#[test]
fn domain_serialize_display_carries_serde_json_diagnostic_unchanged() {
let raw = serde_json::from_str::<i32>("not-a-number")
.expect_err("parse must fail")
.to_string();
let err = LispError::DomainSerialize {
keyword: "defmonitor",
message: raw.clone(),
};
assert_eq!(
format!("{err}"),
format!("compile error in defmonitor: serialize: {raw}")
);
}
#[test]
fn domain_serialize_display_carries_kebab_case_keyword_unchanged() {
let err = LispError::DomainSerialize {
keyword: "defalert-policy",
message: "expected struct".into(),
};
assert_eq!(
format!("{err}"),
"compile error in defalert-policy: serialize: expected struct"
);
}
#[test]
fn domain_serialize_display_preserves_legacy_substring_for_message_grep() {
let err = LispError::DomainSerialize {
keyword: "defmonitor",
message: "boom".into(),
};
let msg = format!("{err}");
assert!(
msg.contains("serialize: "),
"expected legacy substring in message, got: {msg}"
);
assert!(
msg.contains("compile error in defmonitor:"),
"expected legacy form-label prefix in message, got: {msg}"
);
}
#[test]
fn domain_serialize_display_empty_message_renders_bare_prefix() {
let err = LispError::DomainSerialize {
keyword: "defmonitor",
message: String::new(),
};
assert_eq!(format!("{err}"), "compile error in defmonitor: serialize: ");
}
#[test]
fn kwarg_deserialize_display_scalar_path_renders_legacy_shape() {
let err = LispError::KwargDeserialize {
path: KwargPath::named("level"),
message: "unknown variant `NotASeverity`".into(),
};
assert_eq!(
format!("{err}"),
"compile error in :level: deserialize: unknown variant `NotASeverity`"
);
}
#[test]
fn kwarg_deserialize_display_per_item_path_renders_indexed_shape() {
let err = LispError::KwargDeserialize {
path: KwargPath::item("steps", 1),
message: "invalid type: integer `7`, expected a string".into(),
};
assert_eq!(
format!("{err}"),
"compile error in :steps[1]: deserialize: invalid type: integer `7`, expected a string"
);
}
#[test]
fn kwarg_deserialize_display_carries_serde_json_diagnostic_unchanged() {
let raw = serde_json::from_str::<i32>("not-a-number")
.expect_err("parse must fail")
.to_string();
let err = LispError::KwargDeserialize {
path: KwargPath::named("count"),
message: raw.clone(),
};
assert_eq!(
format!("{err}"),
format!("compile error in :count: deserialize: {raw}")
);
}
#[test]
fn kwarg_deserialize_display_carries_kebab_case_key_unchanged() {
let err = LispError::KwargDeserialize {
path: KwargPath::named("notify-ref"),
message: "missing field `notify-ref`".into(),
};
assert_eq!(
format!("{err}"),
"compile error in :notify-ref: deserialize: missing field `notify-ref`"
);
}
#[test]
fn kwarg_deserialize_display_carries_kebab_case_key_with_index_unchanged() {
let err = LispError::KwargDeserialize {
path: KwargPath::item("wait-minutes", 2),
message: "expected u64".into(),
};
assert_eq!(
format!("{err}"),
"compile error in :wait-minutes[2]: deserialize: expected u64"
);
}
#[test]
fn kwarg_deserialize_display_preserves_legacy_substring_for_message_grep() {
let scalar = LispError::KwargDeserialize {
path: KwargPath::named("level"),
message: "boom".into(),
};
let scalar_msg = format!("{scalar}");
assert!(
scalar_msg.contains("deserialize: "),
"expected legacy substring in scalar message, got: {scalar_msg}"
);
assert!(
scalar_msg.contains("compile error in :level:"),
"expected legacy form-label prefix in scalar message, got: {scalar_msg}"
);
let item = LispError::KwargDeserialize {
path: KwargPath::item("steps", 3),
message: "boom".into(),
};
let item_msg = format!("{item}");
assert!(
item_msg.contains("deserialize: "),
"expected legacy substring in item message, got: {item_msg}"
);
assert!(
item_msg.contains("compile error in :steps[3]:"),
"expected indexed form-label prefix in item message, got: {item_msg}"
);
}
#[test]
fn kwarg_deserialize_display_zero_index_is_first_class() {
let err = LispError::KwargDeserialize {
path: KwargPath::item("steps", 0),
message: "bad".into(),
};
assert_eq!(
format!("{err}"),
"compile error in :steps[0]: deserialize: bad"
);
}
#[test]
fn kwarg_deserialize_path_named_threads_typed_kwarg_path_through_variant_slot() {
let err = LispError::KwargDeserialize {
path: KwargPath::named("level"),
message: "boom".into(),
};
let LispError::KwargDeserialize {
ref path,
ref message,
} = err
else {
panic!("expected KwargDeserialize, got {err:?}");
};
assert_eq!(*path, KwargPath::Named("level".into()));
assert_eq!(message, "boom");
assert_eq!(
format!("{err}"),
"compile error in :level: deserialize: boom"
);
}
#[test]
fn kwarg_deserialize_path_item_threads_typed_kwarg_path_through_variant_slot() {
let err = LispError::KwargDeserialize {
path: KwargPath::item("steps", 1),
message: "bad".into(),
};
let LispError::KwargDeserialize {
ref path,
ref message,
} = err
else {
panic!("expected KwargDeserialize, got {err:?}");
};
assert_eq!(
*path,
KwargPath::Item {
key: "steps".into(),
idx: 1
}
);
assert_eq!(message, "bad");
assert_eq!(
format!("{err}"),
"compile error in :steps[1]: deserialize: bad"
);
}
#[test]
fn kwarg_deserialize_display_prefix_matches_kwarg_path_display() {
let scalar = LispError::KwargDeserialize {
path: KwargPath::named("level"),
message: "boom".into(),
};
assert_eq!(
format!("{scalar}"),
format!(
"compile error in {}: deserialize: boom",
KwargPath::named("level")
)
);
let item = LispError::KwargDeserialize {
path: KwargPath::item("steps", 3),
message: "boom".into(),
};
assert_eq!(
format!("{item}"),
format!(
"compile error in {}: deserialize: boom",
KwargPath::item("steps", 3)
)
);
}
#[test]
fn compiler_spec_io_stage_operation_projects_realize_for_serialize_and_write() {
assert_eq!(
super::CompilerSpecIoStage::RealizeToDiskSerialize.operation(),
"realize_to_disk"
);
assert_eq!(
super::CompilerSpecIoStage::RealizeToDiskWrite.operation(),
"realize_to_disk"
);
}
#[test]
fn compiler_spec_io_stage_operation_projects_load_for_read_and_deserialize() {
assert_eq!(
super::CompilerSpecIoStage::LoadFromDiskRead.operation(),
"load_from_disk"
);
assert_eq!(
super::CompilerSpecIoStage::LoadFromDiskDeserialize.operation(),
"load_from_disk"
);
}
#[test]
fn compiler_spec_io_stage_label_projects_canonical_stage_strings() {
assert_eq!(
super::CompilerSpecIoStage::RealizeToDiskSerialize.label(),
"serialize"
);
assert_eq!(
super::CompilerSpecIoStage::RealizeToDiskWrite.label(),
"write"
);
assert_eq!(super::CompilerSpecIoStage::LoadFromDiskRead.label(), "read");
assert_eq!(
super::CompilerSpecIoStage::LoadFromDiskDeserialize.label(),
"deserialize"
);
}
#[test]
fn compiler_spec_io_stage_label_method_routes_through_typed_constants() {
assert_eq!(
super::CompilerSpecIoStage::RealizeToDiskSerialize.label(),
super::CompilerSpecIoStage::REALIZE_TO_DISK_SERIALIZE_LABEL,
"CompilerSpecIoStage::RealizeToDiskSerialize.label() \
drifted from CompilerSpecIoStage::\
REALIZE_TO_DISK_SERIALIZE_LABEL — the match arm reverted \
to an inline literal"
);
assert_eq!(
super::CompilerSpecIoStage::RealizeToDiskWrite.label(),
super::CompilerSpecIoStage::REALIZE_TO_DISK_WRITE_LABEL,
"CompilerSpecIoStage::RealizeToDiskWrite.label() drifted \
from CompilerSpecIoStage::REALIZE_TO_DISK_WRITE_LABEL — \
the match arm reverted to an inline literal"
);
assert_eq!(
super::CompilerSpecIoStage::LoadFromDiskRead.label(),
super::CompilerSpecIoStage::LOAD_FROM_DISK_READ_LABEL,
"CompilerSpecIoStage::LoadFromDiskRead.label() drifted \
from CompilerSpecIoStage::LOAD_FROM_DISK_READ_LABEL — \
the match arm reverted to an inline literal"
);
assert_eq!(
super::CompilerSpecIoStage::LoadFromDiskDeserialize.label(),
super::CompilerSpecIoStage::LOAD_FROM_DISK_DESERIALIZE_LABEL,
"CompilerSpecIoStage::LoadFromDiskDeserialize.label() \
drifted from CompilerSpecIoStage::\
LOAD_FROM_DISK_DESERIALIZE_LABEL — the match arm reverted \
to an inline literal"
);
}
#[test]
fn compiler_spec_io_stage_labels_has_expected_cardinality() {
assert_eq!(
super::CompilerSpecIoStage::LABELS.len(),
4,
"CompilerSpecIoStage::LABELS cardinality drifted from 4 \
— the disk-persistence stage-label closed set gained or \
lost a variant without the ALL / LABELS pair being \
updated in tandem"
);
}
#[test]
fn compiler_spec_io_stage_labels_align_with_all_by_index() {
assert_eq!(
super::CompilerSpecIoStage::LABELS.len(),
super::CompilerSpecIoStage::ALL.len(),
"CompilerSpecIoStage::LABELS and CompilerSpecIoStage::ALL \
diverged in cardinality — the per-role constants and \
enum variants must stay in lockstep"
);
for (i, stage) in super::CompilerSpecIoStage::ALL.iter().enumerate() {
assert_eq!(
super::CompilerSpecIoStage::LABELS[i],
stage.label(),
"CompilerSpecIoStage::LABELS[{i}] `{lb}` drifted from \
CompilerSpecIoStage::ALL[{i}].label() `{via_variant}` \
— the canonical declaration order of the two ALL \
arrays must match element-wise",
lb = super::CompilerSpecIoStage::LABELS[i],
via_variant = stage.label(),
);
}
}
#[test]
fn compiler_spec_io_stage_labels_pairwise_distinct() {
for (i, a) in super::CompilerSpecIoStage::LABELS.iter().enumerate() {
for (j, b) in super::CompilerSpecIoStage::LABELS.iter().enumerate() {
if i == j {
continue;
}
assert_ne!(
a, b,
"CompilerSpecIoStage::LABELS[{i}] `{a}` collides \
with CompilerSpecIoStage::LABELS[{j}] `{b}` — \
the singular label projection is no longer a \
bijection with ALL, breaking every consumer that \
keys on the label alone"
);
}
}
}
#[test]
fn compiler_spec_io_stage_operation_method_routes_through_typed_constants() {
assert_eq!(
super::CompilerSpecIoStage::RealizeToDiskSerialize.operation(),
super::CompilerSpecIoStage::REALIZE_TO_DISK_OPERATION,
"CompilerSpecIoStage::RealizeToDiskSerialize.operation() \
drifted from CompilerSpecIoStage::\
REALIZE_TO_DISK_OPERATION — the match arm reverted to an \
inline literal"
);
assert_eq!(
super::CompilerSpecIoStage::RealizeToDiskWrite.operation(),
super::CompilerSpecIoStage::REALIZE_TO_DISK_OPERATION,
"CompilerSpecIoStage::RealizeToDiskWrite.operation() \
drifted from CompilerSpecIoStage::\
REALIZE_TO_DISK_OPERATION — the match arm reverted to an \
inline literal"
);
assert_eq!(
super::CompilerSpecIoStage::LoadFromDiskRead.operation(),
super::CompilerSpecIoStage::LOAD_FROM_DISK_OPERATION,
"CompilerSpecIoStage::LoadFromDiskRead.operation() drifted \
from CompilerSpecIoStage::LOAD_FROM_DISK_OPERATION — the \
match arm reverted to an inline literal"
);
assert_eq!(
super::CompilerSpecIoStage::LoadFromDiskDeserialize.operation(),
super::CompilerSpecIoStage::LOAD_FROM_DISK_OPERATION,
"CompilerSpecIoStage::LoadFromDiskDeserialize.operation() \
drifted from CompilerSpecIoStage::LOAD_FROM_DISK_OPERATION \
— the match arm reverted to an inline literal"
);
}
#[test]
fn compiler_spec_io_stage_operations_has_expected_cardinality() {
assert_eq!(
super::CompilerSpecIoStage::OPERATIONS.len(),
4,
"CompilerSpecIoStage::OPERATIONS cardinality drifted from \
4 — the disk-persistence operation-label closed set \
gained or lost a variant without the ALL / OPERATIONS \
pair being updated in tandem"
);
}
#[test]
fn compiler_spec_io_stage_operations_align_with_all_by_index() {
assert_eq!(
super::CompilerSpecIoStage::OPERATIONS.len(),
super::CompilerSpecIoStage::ALL.len(),
"CompilerSpecIoStage::OPERATIONS and \
CompilerSpecIoStage::ALL diverged in cardinality — the \
per-role constants and enum variants must stay in \
lockstep"
);
for (i, stage) in super::CompilerSpecIoStage::ALL.iter().enumerate() {
assert_eq!(
super::CompilerSpecIoStage::OPERATIONS[i],
stage.operation(),
"CompilerSpecIoStage::OPERATIONS[{i}] `{op}` drifted \
from CompilerSpecIoStage::ALL[{i}].operation() \
`{via_variant}` — the canonical declaration order \
of the two ALL arrays must match element-wise",
op = super::CompilerSpecIoStage::OPERATIONS[i],
via_variant = stage.operation(),
);
}
}
#[test]
fn compiler_spec_io_stage_operations_partition_all_two_ways() {
let realize_count = super::CompilerSpecIoStage::OPERATIONS
.iter()
.filter(|&&op| op == super::CompilerSpecIoStage::REALIZE_TO_DISK_OPERATION)
.count();
let load_count = super::CompilerSpecIoStage::OPERATIONS
.iter()
.filter(|&&op| op == super::CompilerSpecIoStage::LOAD_FROM_DISK_OPERATION)
.count();
assert_eq!(
realize_count, 2,
"CompilerSpecIoStage::OPERATIONS multiplicity of \
REALIZE_TO_DISK_OPERATION drifted from 2 — the \
realize-side bifurcation over {{serialize, write}} is \
load-bearing for the compound-key surface"
);
assert_eq!(
load_count, 2,
"CompilerSpecIoStage::OPERATIONS multiplicity of \
LOAD_FROM_DISK_OPERATION drifted from 2 — the load-side \
bifurcation over {{read, deserialize}} is load-bearing \
for the compound-key surface"
);
assert_eq!(
realize_count + load_count,
super::CompilerSpecIoStage::OPERATIONS.len(),
"CompilerSpecIoStage::OPERATIONS gained an operation label \
outside the two-way partition — the closed-set operation \
axis is no longer a clean {{realize_to_disk ⊎ load_from_disk}} \
bifurcation"
);
assert_ne!(
super::CompilerSpecIoStage::REALIZE_TO_DISK_OPERATION,
super::CompilerSpecIoStage::LOAD_FROM_DISK_OPERATION,
"CompilerSpecIoStage::REALIZE_TO_DISK_OPERATION collided \
with CompilerSpecIoStage::LOAD_FROM_DISK_OPERATION — the \
two-way partition collapsed to one group, breaking every \
compound-key `{{operation}}: {{label}}` round-trip"
);
}
#[test]
fn compiler_spec_io_display_renders_legacy_shape_for_realize_serialize() {
let err = LispError::CompilerSpecIo {
stage: super::CompilerSpecIoStage::RealizeToDiskSerialize,
message: "expected struct CompilerSpec".into(),
};
assert_eq!(
format!("{err}"),
"compile error in realize_to_disk: serialize: expected struct CompilerSpec"
);
}
#[test]
fn compiler_spec_io_display_renders_legacy_shape_for_realize_write() {
let err = LispError::CompilerSpecIo {
stage: super::CompilerSpecIoStage::RealizeToDiskWrite,
message: "No such file or directory (os error 2)".into(),
};
assert_eq!(
format!("{err}"),
"compile error in realize_to_disk: write: No such file or directory (os error 2)"
);
}
#[test]
fn compiler_spec_io_display_renders_legacy_shape_for_load_read() {
let err = LispError::CompilerSpecIo {
stage: super::CompilerSpecIoStage::LoadFromDiskRead,
message: "No such file or directory (os error 2)".into(),
};
assert_eq!(
format!("{err}"),
"compile error in load_from_disk: read: No such file or directory (os error 2)"
);
}
#[test]
fn compiler_spec_io_display_renders_legacy_shape_for_load_deserialize() {
let err = LispError::CompilerSpecIo {
stage: super::CompilerSpecIoStage::LoadFromDiskDeserialize,
message: "expected value at line 1 column 1".into(),
};
assert_eq!(
format!("{err}"),
"compile error in load_from_disk: deserialize: expected value at line 1 column 1"
);
}
#[test]
fn compiler_spec_io_display_carries_serde_json_diagnostic_unchanged() {
let raw = serde_json::from_str::<i32>("not-a-number")
.expect_err("parse must fail")
.to_string();
let err = LispError::CompilerSpecIo {
stage: super::CompilerSpecIoStage::LoadFromDiskDeserialize,
message: raw.clone(),
};
assert_eq!(
format!("{err}"),
format!("compile error in load_from_disk: deserialize: {raw}")
);
}
#[test]
fn compiler_spec_io_display_preserves_legacy_substring_for_message_grep() {
let realize_serialize = LispError::CompilerSpecIo {
stage: super::CompilerSpecIoStage::RealizeToDiskSerialize,
message: "boom".into(),
};
let msg = format!("{realize_serialize}");
assert!(
msg.contains("realize_to_disk"),
"expected realize-side operation in message, got: {msg}"
);
assert!(
msg.contains("serialize: "),
"expected serialize-stage substring in message, got: {msg}"
);
let realize_write = LispError::CompilerSpecIo {
stage: super::CompilerSpecIoStage::RealizeToDiskWrite,
message: "boom".into(),
};
let msg = format!("{realize_write}");
assert!(
msg.contains("write: "),
"expected write-stage substring in message, got: {msg}"
);
let load_read = LispError::CompilerSpecIo {
stage: super::CompilerSpecIoStage::LoadFromDiskRead,
message: "boom".into(),
};
let msg = format!("{load_read}");
assert!(
msg.contains("load_from_disk"),
"expected load-side operation in message, got: {msg}"
);
assert!(
msg.contains("read: "),
"expected read-stage substring in message, got: {msg}"
);
let load_deserialize = LispError::CompilerSpecIo {
stage: super::CompilerSpecIoStage::LoadFromDiskDeserialize,
message: "boom".into(),
};
let msg = format!("{load_deserialize}");
assert!(
msg.contains("deserialize: "),
"expected deserialize-stage substring in message, got: {msg}"
);
}
#[test]
fn compiler_spec_io_display_empty_message_renders_bare_stage_marker() {
let err = LispError::CompilerSpecIo {
stage: super::CompilerSpecIoStage::RealizeToDiskWrite,
message: String::new(),
};
assert_eq!(
format!("{err}"),
"compile error in realize_to_disk: write: "
);
}
#[test]
fn compiler_spec_io_stage_is_copy_and_partial_eq() {
let stage = super::CompilerSpecIoStage::LoadFromDiskRead;
let copied = stage;
assert_eq!(stage, copied);
assert_eq!(stage, super::CompilerSpecIoStage::LoadFromDiskRead);
assert_ne!(stage, super::CompilerSpecIoStage::RealizeToDiskWrite);
}
#[test]
fn compiler_spec_io_stage_all_is_unique_and_complete() {
assert_eq!(super::CompilerSpecIoStage::ALL.len(), 4);
let mut sorted: Vec<String> = super::CompilerSpecIoStage::ALL
.iter()
.map(std::string::ToString::to_string)
.collect();
sorted.sort_unstable();
let mut deduped = sorted.clone();
deduped.dedup();
assert_eq!(
sorted, deduped,
"CompilerSpecIoStage::ALL must not contain duplicates"
);
assert_eq!(
sorted,
vec![
"load_from_disk: deserialize".to_string(),
"load_from_disk: read".to_string(),
"realize_to_disk: serialize".to_string(),
"realize_to_disk: write".to_string(),
],
"CompilerSpecIoStage::ALL must cover every reachable (operation, label) pair"
);
}
#[test]
fn compiler_spec_io_stage_display_matches_diagnostic_prefix() {
for stage in super::CompilerSpecIoStage::ALL {
let standalone = stage.to_string();
let full = format!(
"{}",
LispError::CompilerSpecIo {
stage,
message: "MSG".into()
}
);
let prefix = full
.strip_prefix("compile error in ")
.expect("legacy rendering prefix must hold");
let extracted = prefix
.strip_suffix(": MSG")
.expect("legacy rendering suffix must hold");
assert_eq!(
extracted, standalone,
"extracted prefix `{extracted}` must equal standalone Display `{standalone}`"
);
}
}
#[test]
fn compiler_spec_io_stage_compound_key_round_trips_through_from_str() {
for stage in super::CompilerSpecIoStage::ALL {
let key = stage.to_string();
let parsed: super::CompilerSpecIoStage = key
.parse()
.expect("every ALL variant's compound key must round-trip through FromStr");
assert_eq!(
parsed, stage,
"FromStr({key}) must round-trip to the same variant"
);
}
}
#[test]
fn compiler_spec_io_stage_from_str_rejects_partial_and_unreachable_keys() {
for partial in [
"serialize",
"write",
"read",
"deserialize",
"realize_to_disk",
"load_from_disk",
"",
] {
partial.parse::<super::CompilerSpecIoStage>().expect_err(
"partial key (one projection slot alone, or empty) must NOT decode to a variant",
);
}
for unreachable in [
"realize_to_disk: read",
"realize_to_disk: deserialize",
"load_from_disk: serialize",
"load_from_disk: write",
] {
unreachable
.parse::<super::CompilerSpecIoStage>()
.expect_err(
"conceivable-but-unreachable (operation, label) pair must NOT decode to a variant",
);
}
assert_eq!(
"realize_to_disk: serialize"
.parse::<super::CompilerSpecIoStage>()
.unwrap(),
super::CompilerSpecIoStage::RealizeToDiskSerialize
);
assert_eq!(
"load_from_disk: deserialize"
.parse::<super::CompilerSpecIoStage>()
.unwrap(),
super::CompilerSpecIoStage::LoadFromDiskDeserialize
);
}
#[test]
fn unknown_compiler_spec_io_stage_carries_offending_input_verbatim() {
let err: super::UnknownCompilerSpecIoStage = "Realize_to_disk: serialize"
.parse::<super::CompilerSpecIoStage>()
.expect_err("capitalized operation must NOT decode — keys are byte-equal");
assert_eq!(err.0, "Realize_to_disk: serialize");
assert_eq!(
format!("{err}"),
"unknown compiler spec io stage: Realize_to_disk: serialize"
);
let err: super::UnknownCompilerSpecIoStage = "load_from_disk: write"
.parse::<super::CompilerSpecIoStage>()
.expect_err("unreachable cross-product pair must NOT decode");
assert_eq!(err.0, "load_from_disk: write");
assert_eq!(
format!("{err}"),
"unknown compiler spec io stage: load_from_disk: write"
);
let err: super::UnknownCompilerSpecIoStage = ""
.parse::<super::CompilerSpecIoStage>()
.expect_err("empty input must NOT decode to a CompilerSpecIoStage");
assert_eq!(err.0, "");
assert_eq!(format!("{err}"), "unknown compiler spec io stage: ");
}
#[test]
fn compiler_spec_io_stage_compound_key_separator_is_colon_space() {
assert_eq!(
super::CompilerSpecIoStage::COMPOUND_KEY_SEPARATOR,
": ",
"compound-key separator must be ASCII colon-space byte-for-byte",
);
assert_eq!(
super::CompilerSpecIoStage::COMPOUND_KEY_SEPARATOR.len(),
2,
"compound-key separator is exactly two bytes — no invisible whitespace drift",
);
assert_eq!(
super::CompilerSpecIoStage::COMPOUND_KEY_SEPARATOR.as_bytes(),
b": ",
"compound-key separator bytes are `:` then ` ` — pinned byte-array shape",
);
}
#[test]
fn compiler_spec_io_stage_display_composes_operation_separator_label_verbatim() {
for stage in super::CompilerSpecIoStage::ALL {
let rendered = stage.to_string();
let expected = {
let mut out = String::with_capacity(
stage.operation().len()
+ super::CompilerSpecIoStage::COMPOUND_KEY_SEPARATOR.len()
+ stage.label().len(),
);
out.push_str(stage.operation());
out.push_str(super::CompilerSpecIoStage::COMPOUND_KEY_SEPARATOR);
out.push_str(stage.label());
out
};
assert_eq!(
rendered, expected,
"Display for {stage:?} must compose `operation` ++ COMPOUND_KEY_SEPARATOR ++ `label` verbatim",
);
}
}
#[test]
fn compiler_spec_io_stage_from_str_uses_typed_separator_bytes() {
for wrong_separator in [":", " ", " : ", " ", "|", " - ", "\t"] {
let key = {
let mut out = String::new();
out.push_str(super::CompilerSpecIoStage::RealizeToDiskSerialize.operation());
out.push_str(wrong_separator);
out.push_str(super::CompilerSpecIoStage::RealizeToDiskSerialize.label());
out
};
key.parse::<super::CompilerSpecIoStage>().expect_err(
"compound key with wrong-separator bytes must NOT decode — the parse boundary keys on COMPOUND_KEY_SEPARATOR verbatim",
);
}
let key = {
let mut out = String::new();
out.push_str(super::CompilerSpecIoStage::LoadFromDiskRead.operation());
out.push_str(super::CompilerSpecIoStage::COMPOUND_KEY_SEPARATOR);
out.push_str(super::CompilerSpecIoStage::LoadFromDiskRead.label());
out
};
assert_eq!(
key.parse::<super::CompilerSpecIoStage>()
.expect("compound key composed through COMPOUND_KEY_SEPARATOR must decode"),
super::CompilerSpecIoStage::LoadFromDiskRead,
);
}
#[test]
fn compiler_spec_io_stage_display_middle_bytes_equal_typed_separator() {
for stage in super::CompilerSpecIoStage::ALL {
let rendered = stage.to_string();
let after_op = rendered
.strip_prefix(stage.operation())
.expect("Display must start with operation bytes");
let middle = after_op
.strip_suffix(stage.label())
.expect("Display must end with label bytes");
assert_eq!(
middle,
super::CompilerSpecIoStage::COMPOUND_KEY_SEPARATOR,
"the substring between operation and label in Display for {stage:?} must be COMPOUND_KEY_SEPARATOR verbatim",
);
}
}
#[test]
fn template_invariant_kind_message_for_subst_bad_idx() {
assert_eq!(
super::TemplateInvariantKind::SubstBadIndex(99).message(),
"compiled template referenced bad param index 99"
);
assert_eq!(
super::TemplateInvariantKind::SubstBadIndex(0).message(),
"compiled template referenced bad param index 0"
);
}
#[test]
fn template_invariant_kind_message_for_splice_bad_idx() {
assert_eq!(
super::TemplateInvariantKind::SpliceBadIndex(42).message(),
"compiled template referenced bad splice index 42"
);
}
#[test]
fn template_invariant_kind_message_for_endlist_empty_stack() {
assert_eq!(
super::TemplateInvariantKind::EndListEmptyStack.message(),
"compiled template: EndList with empty stack"
);
}
#[test]
fn template_invariant_kind_message_for_final_no_value() {
assert_eq!(
super::TemplateInvariantKind::FinalNoValue.message(),
"compiled template produced no value"
);
}
#[test]
fn template_invariant_kind_static_message_constants_pin_canonical_bytes() {
assert_eq!(
super::TemplateInvariantKind::END_LIST_EMPTY_STACK_MESSAGE,
"compiled template: EndList with empty stack",
"END_LIST_EMPTY_STACK_MESSAGE must pin the exact mid-loop stack-gate diagnostic bytes",
);
assert_eq!(
super::TemplateInvariantKind::FINAL_NO_VALUE_MESSAGE,
"compiled template produced no value",
"FINAL_NO_VALUE_MESSAGE must pin the exact post-loop final-pop diagnostic bytes",
);
}
#[test]
fn template_invariant_kind_static_messages_pin_two_arm_forced_arity_array() {
assert_eq!(
super::TemplateInvariantKind::STATIC_MESSAGES.len(),
2,
"STATIC_MESSAGES must have exactly two entries — the 2-of-4 static subset carving",
);
assert_eq!(
super::TemplateInvariantKind::STATIC_MESSAGES,
[
super::TemplateInvariantKind::END_LIST_EMPTY_STACK_MESSAGE,
super::TemplateInvariantKind::FINAL_NO_VALUE_MESSAGE,
],
"STATIC_MESSAGES must compose from the two per-role message constants in declaration order",
);
}
#[test]
fn template_invariant_kind_message_static_projects_static_subset_to_typed_option() {
assert_eq!(
super::TemplateInvariantKind::EndListEmptyStack.message_static(),
Some(super::TemplateInvariantKind::END_LIST_EMPTY_STACK_MESSAGE),
);
assert_eq!(
super::TemplateInvariantKind::FinalNoValue.message_static(),
Some(super::TemplateInvariantKind::FINAL_NO_VALUE_MESSAGE),
);
for idx in [0_usize, 1, 42, 99, usize::MAX] {
assert_eq!(
super::TemplateInvariantKind::SubstBadIndex(idx).message_static(),
None,
"SubstBadIndex({idx}) must project to None — the `usize` payload cannot bind to a &'static str",
);
assert_eq!(
super::TemplateInvariantKind::SpliceBadIndex(idx).message_static(),
None,
"SpliceBadIndex({idx}) must project to None — the `usize` payload cannot bind to a &'static str",
);
}
}
#[test]
fn template_invariant_kind_static_messages_align_with_message_static_by_index() {
assert_eq!(
super::TemplateInvariantKind::STATIC_MESSAGES[0],
super::TemplateInvariantKind::EndListEmptyStack
.message_static()
.unwrap(),
);
assert_eq!(
super::TemplateInvariantKind::STATIC_MESSAGES[1],
super::TemplateInvariantKind::FinalNoValue
.message_static()
.unwrap(),
);
}
#[test]
fn template_invariant_kind_message_delegates_static_arms_through_message_static() {
for (variant, static_bytes) in [
(
super::TemplateInvariantKind::EndListEmptyStack,
super::TemplateInvariantKind::END_LIST_EMPTY_STACK_MESSAGE,
),
(
super::TemplateInvariantKind::FinalNoValue,
super::TemplateInvariantKind::FINAL_NO_VALUE_MESSAGE,
),
] {
assert_eq!(
variant.message(),
static_bytes,
"message() for {variant:?} must equal its message_static() bytes — routed through the typed subset projection",
);
}
assert_eq!(
super::TemplateInvariantKind::SubstBadIndex(7).message(),
"compiled template referenced bad param index 7",
);
assert_eq!(
super::TemplateInvariantKind::SpliceBadIndex(11).message(),
"compiled template referenced bad splice index 11",
);
}
#[test]
fn template_invariant_kind_dynamic_descriptor_constants_pin_canonical_bytes() {
assert_eq!(
super::TemplateInvariantKind::SUBST_BAD_INDEX_DESCRIPTOR,
"param",
);
assert_eq!(
super::TemplateInvariantKind::SPLICE_BAD_INDEX_DESCRIPTOR,
"splice",
);
}
#[test]
fn template_invariant_kind_dynamic_descriptors_pin_two_arm_forced_arity_array() {
assert_eq!(
super::TemplateInvariantKind::DYNAMIC_DESCRIPTORS.len(),
2,
"DYNAMIC_DESCRIPTORS must have exactly two entries — the 2-of-4 dynamic subset carving",
);
assert_eq!(
super::TemplateInvariantKind::DYNAMIC_DESCRIPTORS,
[
super::TemplateInvariantKind::SUBST_BAD_INDEX_DESCRIPTOR,
super::TemplateInvariantKind::SPLICE_BAD_INDEX_DESCRIPTOR,
],
"DYNAMIC_DESCRIPTORS must compose from the two per-role descriptor constants in declaration order",
);
}
#[test]
fn template_invariant_kind_descriptor_dynamic_projects_dynamic_subset_to_typed_option() {
for idx in [0_usize, 1, 42, 99, usize::MAX] {
assert_eq!(
super::TemplateInvariantKind::SubstBadIndex(idx).descriptor_dynamic(),
Some(super::TemplateInvariantKind::SUBST_BAD_INDEX_DESCRIPTOR),
"SubstBadIndex({idx}) must project to Some(SUBST_BAD_INDEX_DESCRIPTOR) — descriptor is per-arm, not per-idx",
);
assert_eq!(
super::TemplateInvariantKind::SpliceBadIndex(idx).descriptor_dynamic(),
Some(super::TemplateInvariantKind::SPLICE_BAD_INDEX_DESCRIPTOR),
"SpliceBadIndex({idx}) must project to Some(SPLICE_BAD_INDEX_DESCRIPTOR) — descriptor is per-arm, not per-idx",
);
}
assert_eq!(
super::TemplateInvariantKind::EndListEmptyStack.descriptor_dynamic(),
None,
);
assert_eq!(
super::TemplateInvariantKind::FinalNoValue.descriptor_dynamic(),
None,
);
}
#[test]
fn template_invariant_kind_dynamic_descriptors_align_with_descriptor_dynamic_by_index() {
assert_eq!(
super::TemplateInvariantKind::DYNAMIC_DESCRIPTORS[0],
super::TemplateInvariantKind::SubstBadIndex(0)
.descriptor_dynamic()
.unwrap(),
);
assert_eq!(
super::TemplateInvariantKind::DYNAMIC_DESCRIPTORS[1],
super::TemplateInvariantKind::SpliceBadIndex(0)
.descriptor_dynamic()
.unwrap(),
);
}
#[test]
fn template_invariant_kind_message_dynamic_projects_dynamic_subset_to_typed_option() {
for idx in [0_usize, 1, 42, 99] {
assert_eq!(
super::TemplateInvariantKind::SubstBadIndex(idx).message_dynamic(),
Some(format!("compiled template referenced bad param index {idx}")),
"SubstBadIndex({idx}).message_dynamic() must render the shared template with the `param` descriptor and idx payload",
);
assert_eq!(
super::TemplateInvariantKind::SpliceBadIndex(idx).message_dynamic(),
Some(format!("compiled template referenced bad splice index {idx}")),
"SpliceBadIndex({idx}).message_dynamic() must render the shared template with the `splice` descriptor and idx payload",
);
}
assert_eq!(
super::TemplateInvariantKind::EndListEmptyStack.message_dynamic(),
None,
);
assert_eq!(
super::TemplateInvariantKind::FinalNoValue.message_dynamic(),
None,
);
}
#[test]
fn template_invariant_kind_static_dynamic_projections_partition_closed_set() {
for variant in [
super::TemplateInvariantKind::EndListEmptyStack,
super::TemplateInvariantKind::FinalNoValue,
super::TemplateInvariantKind::SubstBadIndex(0),
super::TemplateInvariantKind::SubstBadIndex(usize::MAX),
super::TemplateInvariantKind::SpliceBadIndex(0),
super::TemplateInvariantKind::SpliceBadIndex(usize::MAX),
] {
let static_hit = variant.message_static().is_some();
let dynamic_hit = variant.descriptor_dynamic().is_some();
assert!(
static_hit ^ dynamic_hit,
"message_static/descriptor_dynamic must partition the closed set — \
exactly one projection returns Some for {variant:?}, got static_hit={static_hit} dynamic_hit={dynamic_hit}",
);
}
}
#[test]
fn template_invariant_kind_message_delegates_dynamic_arms_through_message_dynamic() {
for variant in [
super::TemplateInvariantKind::SubstBadIndex(0),
super::TemplateInvariantKind::SubstBadIndex(99),
super::TemplateInvariantKind::SubstBadIndex(usize::MAX),
super::TemplateInvariantKind::SpliceBadIndex(0),
super::TemplateInvariantKind::SpliceBadIndex(42),
super::TemplateInvariantKind::SpliceBadIndex(usize::MAX),
] {
assert_eq!(
variant.message(),
variant.message_dynamic().unwrap(),
"message() for {variant:?} must equal its message_dynamic() bytes — routed through the typed subset projection",
);
}
}
#[test]
fn template_invariant_display_renders_legacy_compile_shape_for_subst_bad_idx() {
let err = LispError::TemplateInvariant {
macro_name: "test-macro".into(),
kind: super::TemplateInvariantKind::SubstBadIndex(99),
};
assert_eq!(
format!("{err}"),
"compile error in test-macro: compiled template referenced bad param index 99"
);
}
#[test]
fn template_invariant_display_renders_legacy_compile_shape_for_splice_bad_idx() {
let err = LispError::TemplateInvariant {
macro_name: "call-macro".into(),
kind: super::TemplateInvariantKind::SpliceBadIndex(42),
};
assert_eq!(
format!("{err}"),
"compile error in call-macro: compiled template referenced bad splice index 42"
);
}
#[test]
fn template_invariant_display_renders_legacy_compile_shape_for_endlist() {
let err = LispError::TemplateInvariant {
macro_name: "wrap".into(),
kind: super::TemplateInvariantKind::EndListEmptyStack,
};
assert_eq!(
format!("{err}"),
"compile error in wrap: compiled template: EndList with empty stack"
);
}
#[test]
fn template_invariant_display_renders_legacy_compile_shape_for_final_no_value() {
let err = LispError::TemplateInvariant {
macro_name: "id".into(),
kind: super::TemplateInvariantKind::FinalNoValue,
};
assert_eq!(
format!("{err}"),
"compile error in id: compiled template produced no value"
);
}
#[test]
fn template_invariant_display_preserves_legacy_substring_for_message_grep() {
let subst = LispError::TemplateInvariant {
macro_name: "m".into(),
kind: super::TemplateInvariantKind::SubstBadIndex(0),
};
let msg = format!("{subst}");
assert!(
msg.contains("compiled template"),
"expected `compiled template` prefix, got: {msg}"
);
assert!(
msg.contains("bad param index"),
"expected `bad param index` substring, got: {msg}"
);
let splice = LispError::TemplateInvariant {
macro_name: "m".into(),
kind: super::TemplateInvariantKind::SpliceBadIndex(0),
};
let msg = format!("{splice}");
assert!(
msg.contains("bad splice index"),
"expected `bad splice index` substring, got: {msg}"
);
let endlist = LispError::TemplateInvariant {
macro_name: "m".into(),
kind: super::TemplateInvariantKind::EndListEmptyStack,
};
let msg = format!("{endlist}");
assert!(
msg.contains("EndList with empty stack"),
"expected `EndList with empty stack` substring, got: {msg}"
);
let final_nv = LispError::TemplateInvariant {
macro_name: "m".into(),
kind: super::TemplateInvariantKind::FinalNoValue,
};
let msg = format!("{final_nv}");
assert!(
msg.contains("produced no value"),
"expected `produced no value` substring, got: {msg}"
);
}
#[test]
fn template_invariant_kind_is_copy_and_partial_eq() {
let kind = super::TemplateInvariantKind::SubstBadIndex(7);
let copied = kind;
assert_eq!(kind, copied);
assert_eq!(kind, super::TemplateInvariantKind::SubstBadIndex(7));
assert_ne!(kind, super::TemplateInvariantKind::SubstBadIndex(8));
assert_ne!(kind, super::TemplateInvariantKind::SpliceBadIndex(7));
assert_ne!(kind, super::TemplateInvariantKind::EndListEmptyStack);
}
#[test]
fn template_invariant_kind_index_payload_is_structurally_scoped_to_index_carrying_variants() {
match super::TemplateInvariantKind::SubstBadIndex(5) {
super::TemplateInvariantKind::SubstBadIndex(idx) => assert_eq!(idx, 5),
other => panic!("expected SubstBadIndex, got {other:?}"),
}
match super::TemplateInvariantKind::EndListEmptyStack {
super::TemplateInvariantKind::EndListEmptyStack => {}
other => panic!("expected EndListEmptyStack, got {other:?}"),
}
}
#[test]
fn defmacro_arity_head_slot_is_macro_def_head_not_static_str() {
let err = LispError::DefmacroArity {
head: MacroDefHead::Defmacro,
arity: 1,
};
match err {
LispError::DefmacroArity { head, arity } => {
assert_eq!(head, MacroDefHead::Defmacro);
assert_eq!(arity, 1);
}
other => panic!("expected DefmacroArity, got {other:?}"),
}
}
#[test]
fn defmacro_non_symbol_name_head_slot_is_macro_def_head_not_static_str() {
let err = LispError::DefmacroNonSymbolName {
head: MacroDefHead::DefpointTemplate,
got: SexpWitness::new(SexpShape::Int, "5"),
};
match err {
LispError::DefmacroNonSymbolName { head, got } => {
assert_eq!(head, MacroDefHead::DefpointTemplate);
assert_eq!(got.shape, SexpShape::Int);
assert_eq!(got.display, "5");
}
other => panic!("expected DefmacroNonSymbolName, got {other:?}"),
}
}
#[test]
fn defmacro_non_list_params_head_slot_is_macro_def_head_not_static_str() {
let err = LispError::DefmacroNonListParams {
head: MacroDefHead::Defcheck,
got: SexpWitness::new(SexpShape::Int, "7"),
};
match err {
LispError::DefmacroNonListParams { head, got } => {
assert_eq!(head, MacroDefHead::Defcheck);
assert_eq!(got.shape, SexpShape::Int);
assert_eq!(got.display, "7");
}
other => panic!("expected DefmacroNonListParams, got {other:?}"),
}
}
#[test]
fn macro_def_head_display_renders_canonical_keyword_for_each_variant() {
assert_eq!(format!("{}", MacroDefHead::Defmacro), "defmacro");
assert_eq!(
format!("{}", MacroDefHead::DefpointTemplate),
"defpoint-template"
);
assert_eq!(format!("{}", MacroDefHead::Defcheck), "defcheck");
}
#[test]
fn macro_def_head_defmacro_keyword_projects_canonical_defmacro_bytes() {
assert_eq!(
MacroDefHead::DEFMACRO_KEYWORD,
"defmacro",
"MacroDefHead::DEFMACRO_KEYWORD drifted from the substrate- \
canonical CL-style macro-definition head keyword `\"defmacro\"`"
);
}
#[test]
fn macro_def_head_defpoint_template_keyword_projects_canonical_defpoint_template_bytes() {
assert_eq!(
MacroDefHead::DEFPOINT_TEMPLATE_KEYWORD,
"defpoint-template",
"MacroDefHead::DEFPOINT_TEMPLATE_KEYWORD drifted from the substrate- \
canonical K8s-as-processes authoring-surface head keyword \
`\"defpoint-template\"`"
);
}
#[test]
fn macro_def_head_defcheck_keyword_projects_canonical_defcheck_bytes() {
assert_eq!(
MacroDefHead::DEFCHECK_KEYWORD,
"defcheck",
"MacroDefHead::DEFCHECK_KEYWORD drifted from the substrate- \
canonical workspace-coherence authoring-surface head keyword \
`\"defcheck\"`"
);
}
#[test]
fn macro_def_head_keyword_method_routes_through_typed_constants() {
assert_eq!(
MacroDefHead::Defmacro.keyword(),
MacroDefHead::DEFMACRO_KEYWORD,
"MacroDefHead::Defmacro.keyword() drifted from \
MacroDefHead::DEFMACRO_KEYWORD — the match arm reverted to \
an inline literal"
);
assert_eq!(
MacroDefHead::DefpointTemplate.keyword(),
MacroDefHead::DEFPOINT_TEMPLATE_KEYWORD,
"MacroDefHead::DefpointTemplate.keyword() drifted from \
MacroDefHead::DEFPOINT_TEMPLATE_KEYWORD — the match arm \
reverted to an inline literal"
);
assert_eq!(
MacroDefHead::Defcheck.keyword(),
MacroDefHead::DEFCHECK_KEYWORD,
"MacroDefHead::Defcheck.keyword() drifted from \
MacroDefHead::DEFCHECK_KEYWORD — the match arm reverted to \
an inline literal"
);
}
#[test]
fn macro_def_head_keywords_has_expected_cardinality() {
assert_eq!(
MacroDefHead::KEYWORDS.len(),
3,
"MacroDefHead::KEYWORDS cardinality drifted from 3 — the \
head-keyword closed set gained or lost a variant without \
the ALL / KEYWORDS pair being updated in tandem"
);
}
#[test]
fn macro_def_head_keywords_align_with_all_by_index() {
assert_eq!(
MacroDefHead::KEYWORDS.len(),
MacroDefHead::ALL.len(),
"MacroDefHead::KEYWORDS and MacroDefHead::ALL diverged in \
cardinality — the per-role constants and enum variants must \
stay in lockstep"
);
for (i, head) in MacroDefHead::ALL.iter().enumerate() {
assert_eq!(
MacroDefHead::KEYWORDS[i],
head.keyword(),
"MacroDefHead::KEYWORDS[{i}] `{kw}` drifted from \
MacroDefHead::ALL[{i}].keyword() `{via_variant}` — the \
canonical declaration order of the two ALL arrays must \
match element-wise",
kw = MacroDefHead::KEYWORDS[i],
via_variant = head.keyword(),
);
}
}
#[test]
fn macro_def_head_keywords_pairwise_distinct() {
for (i, a) in MacroDefHead::KEYWORDS.iter().enumerate() {
for (j, b) in MacroDefHead::KEYWORDS.iter().enumerate() {
if i == j {
continue;
}
assert_ne!(
a, b,
"MacroDefHead::KEYWORDS[{i}] `{a}` collides with \
MacroDefHead::KEYWORDS[{j}] `{b}` — the derive- \
generated FromStr sweep would route two variants \
through the same arm"
);
}
}
}
#[test]
fn macro_def_head_keywords_all_round_trip_through_from_str() {
for kw in MacroDefHead::KEYWORDS {
let parsed: MacroDefHead = kw.parse().unwrap_or_else(|_| {
panic!(
"MacroDefHead::KEYWORDS entry `{kw}` failed to parse — \
the entry drifted from Self::keyword"
)
});
assert_eq!(
parsed.keyword(),
kw,
"MacroDefHead::KEYWORDS entry `{kw}` parses to a variant \
whose keyword() `{}` does not match — the constant and \
the match arm drifted apart",
parsed.keyword(),
);
}
}
#[test]
fn defmacro_arity_display_renders_legacy_prefix_via_macro_def_head_display() {
let err = LispError::DefmacroArity {
head: MacroDefHead::Defmacro,
arity: 2,
};
assert_eq!(
format!("{err}"),
"compile error in defmacro: (defmacro name (params) body) required \
(got 2 elements, need 4)"
);
}
#[test]
fn defmacro_non_symbol_name_display_renders_via_macro_def_head_display_for_defpoint_template() {
let err = LispError::DefmacroNonSymbolName {
head: MacroDefHead::DefpointTemplate,
got: SexpWitness::new(SexpShape::Keyword, ":foo"),
};
assert_eq!(
format!("{err}"),
"compile error in defpoint-template: expected name symbol, got :foo"
);
}
#[test]
fn defmacro_non_list_params_display_renders_via_macro_def_head_display_for_defcheck() {
let err = LispError::DefmacroNonListParams {
head: MacroDefHead::Defcheck,
got: SexpWitness::new(SexpShape::Symbol, "x"),
};
assert_eq!(
format!("{err}"),
"compile error in defcheck: expected param list, got x"
);
}
#[test]
fn macro_def_head_is_copy_and_partial_eq_for_pattern_match_ergonomics() {
let h = MacroDefHead::Defmacro;
let h_copy: MacroDefHead = h; assert_eq!(h, h_copy); assert!(matches!(h, MacroDefHead::Defmacro)); let _: String = format!("{h:?}"); }
#[test]
fn unbound_template_var_prefix_slot_is_unquote_form_not_static_str() {
let err = LispError::UnboundTemplateVar {
prefix: UnquoteForm::Unquote,
name: "xs".into(),
hint: None,
};
match err {
LispError::UnboundTemplateVar { prefix, name, hint } => {
assert_eq!(prefix, UnquoteForm::Unquote);
assert_eq!(name, "xs");
assert_eq!(hint, None);
}
other => panic!("expected UnboundTemplateVar, got {other:?}"),
}
}
#[test]
fn non_symbol_unquote_target_prefix_slot_is_unquote_form_not_static_str() {
let err = LispError::NonSymbolUnquoteTarget {
prefix: UnquoteForm::Splice,
got: SexpWitness::new(SexpShape::List, "(list 1 2)"),
};
match err {
LispError::NonSymbolUnquoteTarget { prefix, got } => {
assert_eq!(prefix, UnquoteForm::Splice);
assert_eq!(got.shape, SexpShape::List);
assert_eq!(got.display, "(list 1 2)");
}
other => panic!("expected NonSymbolUnquoteTarget, got {other:?}"),
}
}
#[test]
fn unquote_form_marker_projects_canonical_literal_for_each_variant() {
assert_eq!(UnquoteForm::Unquote.marker(), ",");
assert_eq!(UnquoteForm::Splice.marker(), ",@");
}
#[test]
fn unquote_form_display_renders_canonical_marker_for_each_variant() {
assert_eq!(format!("{}", UnquoteForm::Unquote), ",");
assert_eq!(format!("{}", UnquoteForm::Splice), ",@");
}
#[test]
fn unbound_template_var_display_renders_canonical_marker_for_each_variant() {
let unquote = LispError::UnboundTemplateVar {
prefix: UnquoteForm::Unquote,
name: "xs".into(),
hint: None,
};
assert_eq!(format!("{unquote}"), "compile error in ,xs: unbound");
let splice = LispError::UnboundTemplateVar {
prefix: UnquoteForm::Splice,
name: "argz".into(),
hint: Some("args".into()),
};
assert_eq!(
format!("{splice}"),
"compile error in ,@argz: unbound; did you mean ,@args?"
);
}
#[test]
fn non_symbol_unquote_target_display_renders_canonical_marker_for_each_variant() {
let unquote = LispError::NonSymbolUnquoteTarget {
prefix: UnquoteForm::Unquote,
got: SexpWitness::new(SexpShape::List, "(list 1 2)"),
};
assert_eq!(
format!("{unquote}"),
"compile error in ,: expected symbol, got (list 1 2)"
);
let splice = LispError::NonSymbolUnquoteTarget {
prefix: UnquoteForm::Splice,
got: SexpWitness::new(SexpShape::Int, "5"),
};
assert_eq!(
format!("{splice}"),
"compile error in ,@: expected symbol, got 5"
);
}
#[test]
fn unquote_form_is_copy_and_partial_eq_for_pattern_match_ergonomics() {
let f = UnquoteForm::Splice;
let f_copy: UnquoteForm = f; assert_eq!(f, f_copy); assert!(matches!(f, UnquoteForm::Splice)); assert_ne!(f, UnquoteForm::Unquote); let _: String = format!("{f:?}"); }
#[test]
fn kwarg_path_named_display_renders_legacy_colon_key_literal() {
assert_eq!(format!("{}", KwargPath::named("threshold")), ":threshold");
}
#[test]
fn kwarg_path_item_display_renders_legacy_colon_key_bracket_idx_literal() {
assert_eq!(format!("{}", KwargPath::item("steps", 1)), ":steps[1]");
}
#[test]
fn kwarg_path_slot_display_renders_legacy_kwargs_bracket_idx_literal() {
assert_eq!(format!("{}", KwargPath::Slot(0)), "kwargs[0]");
}
#[test]
fn kwarg_path_named_carries_kebab_case_keys_unchanged() {
assert_eq!(format!("{}", KwargPath::named("notify-ref")), ":notify-ref");
assert_eq!(
format!("{}", KwargPath::item("window-seconds", 3)),
":window-seconds[3]"
);
}
#[test]
fn kwarg_path_is_clone_and_partial_eq_for_pattern_match_ergonomics() {
let p = KwargPath::item("steps", 2);
let p_clone = p.clone(); assert_eq!(p, p_clone); assert!(matches!(p, KwargPath::Item { idx: 2, .. })); assert_ne!(p, KwargPath::Slot(2)); let _: String = format!("{p:?}"); }
#[test]
fn kwarg_path_named_and_slot_have_distinct_display_shapes() {
let named = format!("{}", KwargPath::named("kwargs"));
let slot = format!("{}", KwargPath::Slot(0));
assert_eq!(named, ":kwargs");
assert_eq!(slot, "kwargs[0]");
assert_ne!(named, slot);
}
#[test]
fn type_mismatch_form_carries_typed_kwarg_path_named_through_variant_slot() {
let err = LispError::TypeMismatch {
form: KwargPath::named("threshold"),
expected: ExpectedKwargShape::Number,
got: SexpShape::String,
};
match &err {
LispError::TypeMismatch { form, .. } => {
assert_eq!(*form, KwargPath::Named("threshold".into()));
}
other => panic!("expected TypeMismatch, got {other:?}"),
}
assert_eq!(
format!("{err}"),
"compile error in :threshold: expected number, got string"
);
}
#[test]
fn type_mismatch_form_carries_typed_kwarg_path_item_through_variant_slot() {
let err = LispError::TypeMismatch {
form: KwargPath::item("steps", 3),
expected: ExpectedKwargShape::String,
got: SexpShape::Int,
};
match &err {
LispError::TypeMismatch { form, .. } => {
assert_eq!(
*form,
KwargPath::Item {
key: "steps".into(),
idx: 3
}
);
}
other => panic!("expected TypeMismatch, got {other:?}"),
}
assert_eq!(
format!("{err}"),
"compile error in :steps[3]: expected string, got int"
);
}
#[test]
fn type_mismatch_form_carries_typed_kwarg_path_slot_through_variant_slot() {
let err = LispError::TypeMismatch {
form: KwargPath::Slot(2),
expected: ExpectedKwargShape::Keyword,
got: SexpShape::String,
};
match &err {
LispError::TypeMismatch { form, .. } => {
assert_eq!(*form, KwargPath::Slot(2));
}
other => panic!("expected TypeMismatch, got {other:?}"),
}
assert_eq!(
format!("{err}"),
"compile error in kwargs[2]: expected keyword, got string"
);
}
#[test]
fn kwarg_path_kind_all_is_unique_and_complete() {
assert_eq!(KwargPathKind::ALL.len(), 3);
assert_eq!(
<KwargPathKind as tatara_closed_set::ClosedSet>::sorted_labels(),
vec!["item", "named", "slot"],
"KwargPathKind::ALL must cover every reachable path-shape category"
);
}
#[test]
fn kwarg_path_kind_label_round_trips_through_from_str() {
for kind in KwargPathKind::ALL {
let parsed: KwargPathKind = kind
.label()
.parse()
.expect("every ALL variant's label must round-trip through FromStr");
assert_eq!(
parsed,
kind,
"FromStr({}) must round-trip to the same variant",
kind.label()
);
}
}
#[test]
fn kwarg_path_kind_display_matches_label_for_every_variant() {
for kind in KwargPathKind::ALL {
assert_eq!(format!("{kind}"), kind.label());
}
}
#[test]
fn unknown_kwarg_path_kind_carries_offending_input_verbatim() {
let err: UnknownKwargPathKind = "Named".parse::<KwargPathKind>().expect_err(
"capitalized `Named` must NOT decode — labels are byte-equal case-sensitive",
);
assert_eq!(err.0, "Named");
assert_eq!(format!("{err}"), "unknown kwarg path kind: Named");
let err: UnknownKwargPathKind = ":foo"
.parse::<KwargPathKind>()
.expect_err("`:foo` is a KwargPath rendering, not a KwargPathKind label");
assert_eq!(err.0, ":foo");
assert_eq!(format!("{err}"), "unknown kwarg path kind: :foo");
let err: UnknownKwargPathKind = ""
.parse::<KwargPathKind>()
.expect_err("empty input must NOT decode to a KwargPathKind");
assert_eq!(err.0, "");
assert_eq!(format!("{err}"), "unknown kwarg path kind: ");
}
#[test]
fn kwarg_path_kind_projects_each_variant_to_canonical_kind() {
assert_eq!(KwargPath::named("threshold").kind(), KwargPathKind::Named);
assert_eq!(KwargPath::item("steps", 3).kind(), KwargPathKind::Item);
assert_eq!(KwargPath::Slot(2).kind(), KwargPathKind::Slot);
}
#[test]
fn kwarg_path_kind_is_copy_and_hash_for_metrics_label_ergonomics() {
let k = KwargPathKind::Named;
let k_copy = k; assert_eq!(k, k_copy);
let _: String = format!("{k:?}"); let mut s: std::collections::HashSet<KwargPathKind> = std::collections::HashSet::new();
s.insert(KwargPathKind::Named);
s.insert(KwargPathKind::Item);
s.insert(KwargPathKind::Slot);
s.insert(KwargPathKind::Named); assert_eq!(s.len(), 3);
}
#[test]
fn kwarg_path_kind_label_does_not_overlap_kwarg_path_display_renderings() {
for kind in KwargPathKind::ALL {
let label = kind.label();
assert!(
!label.starts_with(':'),
"kind label {label:?} must not start with `:` (would collide with KwargPath::Named/Item rendering)"
);
assert!(
!label.contains('['),
"kind label {label:?} must not contain `[` (would collide with KwargPath::Item/Slot rendering)"
);
}
}
#[test]
fn kwarg_path_kind_named_label_projects_canonical_named_bytes() {
assert_eq!(
KwargPathKind::NAMED_LABEL,
"named",
"KwargPathKind::NAMED_LABEL drifted from the substrate-\
canonical typed-atom-extractor failure category `\"named\"`"
);
}
#[test]
fn kwarg_path_kind_item_label_projects_canonical_item_bytes() {
assert_eq!(
KwargPathKind::ITEM_LABEL,
"item",
"KwargPathKind::ITEM_LABEL drifted from the substrate-\
canonical list-item failure category `\"item\"`"
);
}
#[test]
fn kwarg_path_kind_slot_label_projects_canonical_slot_bytes() {
assert_eq!(
KwargPathKind::SLOT_LABEL,
"slot",
"KwargPathKind::SLOT_LABEL drifted from the substrate-\
canonical kwargs-slice-slot failure category `\"slot\"`"
);
}
#[test]
fn kwarg_path_kind_label_method_routes_through_typed_constants() {
assert_eq!(
KwargPathKind::Named.label(),
KwargPathKind::NAMED_LABEL,
"KwargPathKind::Named.label() drifted from \
KwargPathKind::NAMED_LABEL — the match arm reverted to \
an inline literal"
);
assert_eq!(
KwargPathKind::Item.label(),
KwargPathKind::ITEM_LABEL,
"KwargPathKind::Item.label() drifted from \
KwargPathKind::ITEM_LABEL — the match arm reverted to \
an inline literal"
);
assert_eq!(
KwargPathKind::Slot.label(),
KwargPathKind::SLOT_LABEL,
"KwargPathKind::Slot.label() drifted from \
KwargPathKind::SLOT_LABEL — the match arm reverted to \
an inline literal"
);
}
#[test]
fn kwarg_path_kind_labels_has_expected_cardinality() {
assert_eq!(
KwargPathKind::LABELS.len(),
3,
"KwargPathKind::LABELS cardinality drifted from 3 — the \
category-label closed set gained or lost a variant \
without the ALL / LABELS pair being updated in tandem"
);
}
#[test]
fn kwarg_path_kind_labels_align_with_all_by_index() {
assert_eq!(
KwargPathKind::LABELS.len(),
KwargPathKind::ALL.len(),
"KwargPathKind::LABELS and KwargPathKind::ALL diverged in \
cardinality — the per-role constants and enum variants \
must stay in lockstep"
);
for (i, kind) in KwargPathKind::ALL.iter().enumerate() {
assert_eq!(
KwargPathKind::LABELS[i],
kind.label(),
"KwargPathKind::LABELS[{i}] `{lb}` drifted from \
KwargPathKind::ALL[{i}].label() `{via_variant}` — \
the canonical declaration order of the two ALL \
arrays must match element-wise",
lb = KwargPathKind::LABELS[i],
via_variant = kind.label(),
);
}
}
#[test]
fn kwarg_path_kind_labels_pairwise_distinct() {
for (i, a) in KwargPathKind::LABELS.iter().enumerate() {
for (j, b) in KwargPathKind::LABELS.iter().enumerate() {
if i == j {
continue;
}
assert_ne!(
a, b,
"KwargPathKind::LABELS[{i}] `{a}` collides with \
KwargPathKind::LABELS[{j}] `{b}` — the derive-\
generated FromStr sweep would route two variants \
through the same arm"
);
}
}
}
#[test]
fn kwarg_path_kind_labels_all_round_trip_through_from_str() {
for lb in KwargPathKind::LABELS {
let parsed: KwargPathKind = lb.parse().unwrap_or_else(|_| {
panic!(
"KwargPathKind::LABELS entry `{lb}` failed to parse \
— the entry drifted from Self::label"
)
});
assert_eq!(
parsed.label(),
lb,
"KwargPathKind::LABELS entry `{lb}` parses to a variant \
whose label() `{}` does not match — the constant and \
the match arm drifted apart",
parsed.label(),
);
}
}
#[test]
fn label_renders_canonical_string_for_every_variant() {
assert_eq!(ExpectedKwargShape::Keyword.label(), "keyword");
assert_eq!(ExpectedKwargShape::String.label(), "string");
assert_eq!(ExpectedKwargShape::Int.label(), "int");
assert_eq!(ExpectedKwargShape::Number.label(), "number");
assert_eq!(ExpectedKwargShape::Bool.label(), "bool");
assert_eq!(ExpectedKwargShape::List.label(), "list");
assert_eq!(ExpectedKwargShape::ListOfStrings.label(), "list of strings");
}
#[test]
fn display_matches_label_for_every_variant() {
assert_eq!(format!("{}", ExpectedKwargShape::Keyword), "keyword");
assert_eq!(format!("{}", ExpectedKwargShape::String), "string");
assert_eq!(format!("{}", ExpectedKwargShape::Int), "int");
assert_eq!(format!("{}", ExpectedKwargShape::Number), "number");
assert_eq!(format!("{}", ExpectedKwargShape::Bool), "bool");
assert_eq!(format!("{}", ExpectedKwargShape::List), "list");
assert_eq!(
format!("{}", ExpectedKwargShape::ListOfStrings),
"list of strings"
);
}
#[test]
fn type_mismatch_expected_carries_typed_shape_through_variant_slot() {
let err = LispError::TypeMismatch {
form: KwargPath::named("threshold"),
expected: ExpectedKwargShape::Number,
got: SexpShape::String,
};
match &err {
LispError::TypeMismatch { expected, .. } => {
assert_eq!(*expected, ExpectedKwargShape::Number);
}
other => panic!("expected TypeMismatch, got {other:?}"),
}
assert_eq!(
format!("{err}"),
"compile error in :threshold: expected number, got string"
);
}
#[test]
fn type_mismatch_expected_list_of_strings_bifurcates_from_list() {
let list_of_strings = LispError::TypeMismatch {
form: KwargPath::named("tags"),
expected: ExpectedKwargShape::ListOfStrings,
got: SexpShape::String,
};
let list = LispError::TypeMismatch {
form: KwargPath::named("steps"),
expected: ExpectedKwargShape::List,
got: SexpShape::String,
};
assert_eq!(
format!("{list_of_strings}"),
"compile error in :tags: expected list of strings, got string"
);
assert_eq!(
format!("{list}"),
"compile error in :steps: expected list, got string"
);
match (&list_of_strings, &list) {
(
LispError::TypeMismatch { expected: a, .. },
LispError::TypeMismatch { expected: b, .. },
) => {
assert_ne!(a, b);
assert_eq!(*a, ExpectedKwargShape::ListOfStrings);
assert_eq!(*b, ExpectedKwargShape::List);
}
_ => panic!("both must be TypeMismatch"),
}
}
#[test]
fn expected_kwarg_shape_all_is_unique_and_complete() {
assert_eq!(ExpectedKwargShape::ALL.len(), 7);
assert_eq!(
<ExpectedKwargShape as tatara_closed_set::ClosedSet>::sorted_labels(),
vec![
"bool",
"int",
"keyword",
"list",
"list of strings",
"number",
"string",
],
"ExpectedKwargShape::ALL must cover every reachable expected-shape label"
);
}
#[test]
fn expected_kwarg_shape_label_round_trips_through_from_str() {
for shape in ExpectedKwargShape::ALL {
let parsed: ExpectedKwargShape = shape
.label()
.parse()
.expect("every ALL variant's label must round-trip through FromStr");
assert_eq!(
parsed,
shape,
"FromStr({}) must round-trip to the same variant",
shape.label()
);
}
}
#[test]
fn unknown_expected_kwarg_shape_carries_offending_input_verbatim() {
let err: UnknownExpectedKwargShape = "Number".parse::<ExpectedKwargShape>().expect_err(
"capitalized `Number` must NOT decode — labels are byte-equal case-sensitive",
);
assert_eq!(err.0, "Number");
assert_eq!(format!("{err}"), "unknown expected kwarg shape: Number");
let err: UnknownExpectedKwargShape = "float"
.parse::<ExpectedKwargShape>()
.expect_err("`float` is SexpShape's vocabulary, not ExpectedKwargShape's");
assert_eq!(err.0, "float");
assert_eq!(format!("{err}"), "unknown expected kwarg shape: float");
let err: UnknownExpectedKwargShape = ""
.parse::<ExpectedKwargShape>()
.expect_err("empty input must NOT decode to an ExpectedKwargShape");
assert_eq!(err.0, "");
assert_eq!(format!("{err}"), "unknown expected kwarg shape: ");
}
#[test]
fn expected_kwarg_shape_from_str_accepts_only_canonical_labels() {
assert_eq!(
"keyword".parse::<ExpectedKwargShape>().unwrap(),
ExpectedKwargShape::Keyword
);
assert_eq!(
"string".parse::<ExpectedKwargShape>().unwrap(),
ExpectedKwargShape::String
);
assert_eq!(
"int".parse::<ExpectedKwargShape>().unwrap(),
ExpectedKwargShape::Int
);
assert_eq!(
"bool".parse::<ExpectedKwargShape>().unwrap(),
ExpectedKwargShape::Bool
);
assert_eq!(
"list".parse::<ExpectedKwargShape>().unwrap(),
ExpectedKwargShape::List
);
for sexp_only in ["nil", "symbol", "float", "quote", "quasiquote", "unquote"] {
sexp_only.parse::<ExpectedKwargShape>().unwrap_err();
}
assert_eq!(
"number".parse::<ExpectedKwargShape>().unwrap(),
ExpectedKwargShape::Number
);
assert_eq!(
"list of strings".parse::<ExpectedKwargShape>().unwrap(),
ExpectedKwargShape::ListOfStrings
);
"number".parse::<SexpShape>().unwrap_err();
"list of strings".parse::<SexpShape>().unwrap_err();
}
#[test]
fn expected_kwarg_shape_keyword_label_projects_canonical_keyword_bytes() {
assert_eq!(
ExpectedKwargShape::KEYWORD_LABEL,
"keyword",
"ExpectedKwargShape::KEYWORD_LABEL drifted from the \
substrate-canonical slot-must-be-a-keyword gate label \
`\"keyword\"`"
);
}
#[test]
fn expected_kwarg_shape_string_label_projects_canonical_string_bytes() {
assert_eq!(
ExpectedKwargShape::STRING_LABEL,
"string",
"ExpectedKwargShape::STRING_LABEL drifted from the \
substrate-canonical extract-string gate label \
`\"string\"`"
);
}
#[test]
fn expected_kwarg_shape_int_label_projects_canonical_int_bytes() {
assert_eq!(
ExpectedKwargShape::INT_LABEL,
"int",
"ExpectedKwargShape::INT_LABEL drifted from the substrate-\
canonical extract-int gate label `\"int\"`"
);
}
#[test]
fn expected_kwarg_shape_number_label_projects_canonical_number_bytes() {
assert_eq!(
ExpectedKwargShape::NUMBER_LABEL,
"number",
"ExpectedKwargShape::NUMBER_LABEL drifted from the \
substrate-canonical extract-float gate label \
`\"number\"`"
);
}
#[test]
fn expected_kwarg_shape_bool_label_projects_canonical_bool_bytes() {
assert_eq!(
ExpectedKwargShape::BOOL_LABEL,
"bool",
"ExpectedKwargShape::BOOL_LABEL drifted from the substrate-\
canonical extract-bool gate label `\"bool\"`"
);
}
#[test]
fn expected_kwarg_shape_list_label_projects_canonical_list_bytes() {
assert_eq!(
ExpectedKwargShape::LIST_LABEL,
"list",
"ExpectedKwargShape::LIST_LABEL drifted from the substrate-\
canonical extract-vec-via-serde gate label `\"list\"`"
);
}
#[test]
fn expected_kwarg_shape_list_of_strings_label_projects_canonical_bytes() {
assert_eq!(
ExpectedKwargShape::LIST_OF_STRINGS_LABEL,
"list of strings",
"ExpectedKwargShape::LIST_OF_STRINGS_LABEL drifted from \
the substrate-canonical extract-string-list outer-shape \
gate label `\"list of strings\"`"
);
}
#[test]
fn expected_kwarg_shape_label_method_routes_through_typed_constants() {
assert_eq!(
ExpectedKwargShape::Keyword.label(),
ExpectedKwargShape::KEYWORD_LABEL,
"ExpectedKwargShape::Keyword.label() drifted from \
ExpectedKwargShape::KEYWORD_LABEL — the match arm \
reverted to an inline literal"
);
assert_eq!(
ExpectedKwargShape::String.label(),
ExpectedKwargShape::STRING_LABEL,
"ExpectedKwargShape::String.label() drifted from \
ExpectedKwargShape::STRING_LABEL — the match arm \
reverted to an inline literal"
);
assert_eq!(
ExpectedKwargShape::Int.label(),
ExpectedKwargShape::INT_LABEL,
"ExpectedKwargShape::Int.label() drifted from \
ExpectedKwargShape::INT_LABEL — the match arm reverted \
to an inline literal"
);
assert_eq!(
ExpectedKwargShape::Number.label(),
ExpectedKwargShape::NUMBER_LABEL,
"ExpectedKwargShape::Number.label() drifted from \
ExpectedKwargShape::NUMBER_LABEL — the match arm \
reverted to an inline literal"
);
assert_eq!(
ExpectedKwargShape::Bool.label(),
ExpectedKwargShape::BOOL_LABEL,
"ExpectedKwargShape::Bool.label() drifted from \
ExpectedKwargShape::BOOL_LABEL — the match arm reverted \
to an inline literal"
);
assert_eq!(
ExpectedKwargShape::List.label(),
ExpectedKwargShape::LIST_LABEL,
"ExpectedKwargShape::List.label() drifted from \
ExpectedKwargShape::LIST_LABEL — the match arm reverted \
to an inline literal"
);
assert_eq!(
ExpectedKwargShape::ListOfStrings.label(),
ExpectedKwargShape::LIST_OF_STRINGS_LABEL,
"ExpectedKwargShape::ListOfStrings.label() drifted from \
ExpectedKwargShape::LIST_OF_STRINGS_LABEL — the match \
arm reverted to an inline literal"
);
}
#[test]
fn expected_kwarg_shape_labels_has_expected_cardinality() {
assert_eq!(
ExpectedKwargShape::LABELS.len(),
7,
"ExpectedKwargShape::LABELS cardinality drifted from 7 \
— the expected-shape closed set gained or lost a variant \
without the ALL / LABELS pair being updated in tandem"
);
}
#[test]
fn expected_kwarg_shape_labels_align_with_all_by_index() {
assert_eq!(
ExpectedKwargShape::LABELS.len(),
ExpectedKwargShape::ALL.len(),
"ExpectedKwargShape::LABELS and ExpectedKwargShape::ALL \
diverged in cardinality — the per-role constants and \
enum variants must stay in lockstep"
);
for (i, shape) in ExpectedKwargShape::ALL.iter().enumerate() {
assert_eq!(
ExpectedKwargShape::LABELS[i],
shape.label(),
"ExpectedKwargShape::LABELS[{i}] `{lb}` drifted from \
ExpectedKwargShape::ALL[{i}].label() `{via_variant}` \
— the canonical declaration order of the two ALL \
arrays must match element-wise",
lb = ExpectedKwargShape::LABELS[i],
via_variant = shape.label(),
);
}
}
#[test]
fn expected_kwarg_shape_labels_pairwise_distinct() {
for (i, a) in ExpectedKwargShape::LABELS.iter().enumerate() {
for (j, b) in ExpectedKwargShape::LABELS.iter().enumerate() {
if i == j {
continue;
}
assert_ne!(
a, b,
"ExpectedKwargShape::LABELS[{i}] `{a}` collides \
with ExpectedKwargShape::LABELS[{j}] `{b}` — the \
derive-generated FromStr sweep would route two \
variants through the same arm"
);
}
}
}
#[test]
fn expected_kwarg_shape_labels_all_round_trip_through_from_str() {
for lb in ExpectedKwargShape::LABELS {
let parsed: ExpectedKwargShape = lb.parse().unwrap_or_else(|_| {
panic!(
"ExpectedKwargShape::LABELS entry `{lb}` failed to \
parse — the entry drifted from Self::label"
)
});
assert_eq!(
parsed.label(),
lb,
"ExpectedKwargShape::LABELS entry `{lb}` parses to a \
variant whose label() `{}` does not match — the \
constant and the match arm drifted apart",
parsed.label(),
);
}
}
#[test]
fn expected_kwarg_shape_labels_cover_sorted_closed_set() {
let mut sorted_labels: Vec<&'static str> = ExpectedKwargShape::LABELS.to_vec();
sorted_labels.sort_unstable();
assert_eq!(
sorted_labels,
<ExpectedKwargShape as tatara_closed_set::ClosedSet>::sorted_labels(),
"ExpectedKwargShape::LABELS diverged from the derive-\
generated `sorted_labels()` — a variant was added or \
removed from one array without the other tracking"
);
}
#[test]
fn sexp_shape_label_renders_canonical_string_for_every_variant() {
assert_eq!(SexpShape::Nil.label(), "nil");
assert_eq!(SexpShape::Symbol.label(), "symbol");
assert_eq!(SexpShape::Keyword.label(), "keyword");
assert_eq!(SexpShape::String.label(), "string");
assert_eq!(SexpShape::Int.label(), "int");
assert_eq!(SexpShape::Float.label(), "float");
assert_eq!(SexpShape::Bool.label(), "bool");
assert_eq!(SexpShape::List.label(), "list");
assert_eq!(SexpShape::Quote.label(), "quote");
assert_eq!(SexpShape::Quasiquote.label(), "quasiquote");
assert_eq!(SexpShape::Unquote.label(), "unquote");
assert_eq!(SexpShape::UnquoteSplice.label(), "unquote-splice");
}
#[test]
fn sexp_shape_display_matches_label_for_every_variant() {
assert_eq!(format!("{}", SexpShape::Nil), "nil");
assert_eq!(format!("{}", SexpShape::Symbol), "symbol");
assert_eq!(format!("{}", SexpShape::Keyword), "keyword");
assert_eq!(format!("{}", SexpShape::String), "string");
assert_eq!(format!("{}", SexpShape::Int), "int");
assert_eq!(format!("{}", SexpShape::Float), "float");
assert_eq!(format!("{}", SexpShape::Bool), "bool");
assert_eq!(format!("{}", SexpShape::List), "list");
assert_eq!(format!("{}", SexpShape::Quote), "quote");
assert_eq!(format!("{}", SexpShape::Quasiquote), "quasiquote");
assert_eq!(format!("{}", SexpShape::Unquote), "unquote");
assert_eq!(format!("{}", SexpShape::UnquoteSplice), "unquote-splice");
}
#[test]
fn sexp_shape_label_routes_through_typed_per_variant_constants() {
assert_eq!(SexpShape::Nil.label(), SexpShape::NIL_LABEL);
assert_eq!(SexpShape::Symbol.label(), SexpShape::SYMBOL_LABEL);
assert_eq!(SexpShape::Keyword.label(), SexpShape::KEYWORD_LABEL);
assert_eq!(SexpShape::String.label(), SexpShape::STRING_LABEL);
assert_eq!(SexpShape::Int.label(), SexpShape::INT_LABEL);
assert_eq!(SexpShape::Float.label(), SexpShape::FLOAT_LABEL);
assert_eq!(SexpShape::Bool.label(), SexpShape::BOOL_LABEL);
assert_eq!(SexpShape::List.label(), SexpShape::LIST_LABEL);
assert_eq!(SexpShape::Quote.label(), SexpShape::QUOTE_LABEL);
assert_eq!(SexpShape::Quasiquote.label(), SexpShape::QUASIQUOTE_LABEL);
assert_eq!(SexpShape::Unquote.label(), SexpShape::UNQUOTE_LABEL);
assert_eq!(
SexpShape::UnquoteSplice.label(),
SexpShape::UNQUOTE_SPLICE_LABEL
);
}
#[test]
fn sexp_shape_labels_has_expected_cardinality() {
assert_eq!(
SexpShape::LABELS.len(),
12,
"SexpShape::LABELS cardinality drifted from 12 — the twelve \
reachable Sexp outermost shapes were extended without \
extending the LABELS array in lockstep"
);
assert_eq!(SexpShape::LABELS.len(), SexpShape::ALL.len(),);
}
#[test]
fn sexp_shape_labels_align_with_all_by_index() {
for (i, v) in SexpShape::ALL.iter().enumerate() {
assert_eq!(
SexpShape::LABELS[i],
v.label(),
"SexpShape::LABELS[{i}] `{kw}` drifted from \
SexpShape::ALL[{i}].label() `{via_variant}` — the \
canonical ALL ordering and the LABELS ordering must \
match element-wise",
kw = SexpShape::LABELS[i],
via_variant = v.label(),
);
}
}
#[test]
fn sexp_shape_labels_pairwise_distinct() {
for (i, a) in SexpShape::LABELS.iter().enumerate() {
for (j, b) in SexpShape::LABELS.iter().enumerate() {
if i == j {
continue;
}
assert_ne!(
a, b,
"SexpShape::LABELS[{i}] `{a}` collides with \
SexpShape::LABELS[{j}] `{b}` — the diagnostic \
label surface must be pairwise byte-distinct across \
the twelve reachable Sexp outermost shapes"
);
}
}
}
#[test]
fn sexp_shape_atom_carving_labels_align_with_expected_kwarg_shape_labels() {
assert_eq!(SexpShape::KEYWORD_LABEL, ExpectedKwargShape::KEYWORD_LABEL);
assert_eq!(SexpShape::STRING_LABEL, ExpectedKwargShape::STRING_LABEL);
assert_eq!(SexpShape::INT_LABEL, ExpectedKwargShape::INT_LABEL);
assert_eq!(SexpShape::BOOL_LABEL, ExpectedKwargShape::BOOL_LABEL);
assert_eq!(SexpShape::LIST_LABEL, ExpectedKwargShape::LIST_LABEL);
assert_ne!(SexpShape::FLOAT_LABEL, ExpectedKwargShape::NUMBER_LABEL);
}
#[test]
fn expected_kwarg_shape_per_role_labels_alias_sexp_shape_per_role_labels_via_static_ptr() {
assert!(
std::ptr::eq(
ExpectedKwargShape::KEYWORD_LABEL.as_ptr(),
SexpShape::KEYWORD_LABEL.as_ptr(),
),
"ExpectedKwargShape::KEYWORD_LABEL and SexpShape::KEYWORD_LABEL disagree on `&'static str` rodata address — the alias chain has been broken by an inline-literal re-derivation on the ExpectedKwargShape side",
);
assert!(
std::ptr::eq(
ExpectedKwargShape::STRING_LABEL.as_ptr(),
SexpShape::STRING_LABEL.as_ptr(),
),
"ExpectedKwargShape::STRING_LABEL and SexpShape::STRING_LABEL disagree on `&'static str` rodata address — the alias chain has been broken by an inline-literal re-derivation on the ExpectedKwargShape side",
);
assert!(
std::ptr::eq(
ExpectedKwargShape::INT_LABEL.as_ptr(),
SexpShape::INT_LABEL.as_ptr(),
),
"ExpectedKwargShape::INT_LABEL and SexpShape::INT_LABEL disagree on `&'static str` rodata address — the alias chain has been broken by an inline-literal re-derivation on the ExpectedKwargShape side",
);
assert!(
std::ptr::eq(
ExpectedKwargShape::BOOL_LABEL.as_ptr(),
SexpShape::BOOL_LABEL.as_ptr(),
),
"ExpectedKwargShape::BOOL_LABEL and SexpShape::BOOL_LABEL disagree on `&'static str` rodata address — the alias chain has been broken by an inline-literal re-derivation on the ExpectedKwargShape side",
);
assert!(
std::ptr::eq(
ExpectedKwargShape::LIST_LABEL.as_ptr(),
SexpShape::LIST_LABEL.as_ptr(),
),
"ExpectedKwargShape::LIST_LABEL and SexpShape::LIST_LABEL disagree on `&'static str` rodata address — the alias chain has been broken by an inline-literal re-derivation on the ExpectedKwargShape side",
);
}
#[test]
fn expected_kwarg_shape_residual_labels_lie_outside_sexp_shape_label_vocabulary() {
for &shape in SexpShape::ALL.iter() {
let via_shape = SexpShape::label(shape);
assert_ne!(
ExpectedKwargShape::NUMBER_LABEL,
via_shape,
"ExpectedKwargShape::NUMBER_LABEL `\"number\"` collides with SexpShape::{shape:?}.label() `{via_shape}` — the 5-of-7 aliased subset carving's residual arm {{NUMBER}} MUST lie outside every SexpShape label; a collision means the carving needs to be re-derived to a 6-of-7 subset aliased through the newly-overlapping SexpShape arm",
);
assert_ne!(
ExpectedKwargShape::LIST_OF_STRINGS_LABEL,
via_shape,
"ExpectedKwargShape::LIST_OF_STRINGS_LABEL `\"list of strings\"` collides with SexpShape::{shape:?}.label() `{via_shape}` — the 5-of-7 aliased subset carving's residual arm {{LIST_OF_STRINGS}} MUST lie outside every SexpShape label",
);
}
}
#[test]
fn sexp_shape_quote_carving_labels_align_with_quote_form_iac_forge_tags() {
assert_eq!(
SexpShape::QUOTE_LABEL,
crate::ast::QuoteForm::QUOTE_IAC_FORGE_TAG
);
assert_eq!(
SexpShape::QUASIQUOTE_LABEL,
crate::ast::QuoteForm::QUASIQUOTE_IAC_FORGE_TAG
);
assert_eq!(
SexpShape::UNQUOTE_LABEL,
crate::ast::QuoteForm::UNQUOTE_IAC_FORGE_TAG
);
}
#[test]
fn sexp_shape_unquote_splice_label_differs_from_quote_form_iac_forge_tag() {
assert_ne!(
SexpShape::UNQUOTE_SPLICE_LABEL,
crate::ast::QuoteForm::UNQUOTE_SPLICE_IAC_FORGE_TAG,
"SexpShape::UNQUOTE_SPLICE_LABEL and \
QuoteForm::UNQUOTE_SPLICE_IAC_FORGE_TAG collapsed to ONE \
spelling — the two sibling label surfaces must render \
distinct bytes so the UnquoteSplice diagnostic renders \
the short `\"unquote-splice\"` bytes while iac-forge \
canonical-form emits the long `\"unquote-splicing\"` bytes"
);
assert_eq!(SexpShape::UNQUOTE_SPLICE_LABEL, "unquote-splice");
assert_eq!(
crate::ast::QuoteForm::UNQUOTE_SPLICE_IAC_FORGE_TAG,
"unquote-splicing"
);
}
#[test]
fn type_mismatch_got_carries_typed_shape_through_variant_slot() {
let err = LispError::TypeMismatch {
form: KwargPath::named("threshold"),
expected: ExpectedKwargShape::Number,
got: SexpShape::String,
};
match &err {
LispError::TypeMismatch { got, .. } => {
assert_eq!(*got, SexpShape::String);
}
other => panic!("expected TypeMismatch, got {other:?}"),
}
assert_eq!(
format!("{err}"),
"compile error in :threshold: expected number, got string"
);
}
#[test]
fn named_form_non_symbol_name_got_carries_typed_shape_through_variant_slot() {
let err = LispError::NamedFormNonSymbolName {
keyword: "defpoint",
got: SexpShape::List,
};
match &err {
LispError::NamedFormNonSymbolName { got, .. } => {
assert_eq!(*got, SexpShape::List);
}
other => panic!("expected NamedFormNonSymbolName, got {other:?}"),
}
assert_eq!(
format!("{err}"),
"compile error in defpoint: positional NAME must be a symbol or string (got list)"
);
}
#[test]
fn sexp_shape_all_is_unique_and_complete() {
assert_eq!(SexpShape::ALL.len(), 12);
assert_eq!(
<SexpShape as tatara_closed_set::ClosedSet>::sorted_labels(),
vec![
"bool",
"float",
"int",
"keyword",
"list",
"nil",
"quasiquote",
"quote",
"string",
"symbol",
"unquote",
"unquote-splice",
],
"SexpShape::ALL must cover every reachable Sexp outermost shape"
);
}
#[test]
fn sexp_shape_label_round_trips_through_from_str() {
for shape in SexpShape::ALL {
let parsed: SexpShape = shape
.label()
.parse()
.expect("every ALL variant's label must round-trip through FromStr");
assert_eq!(
parsed,
shape,
"FromStr({}) must round-trip to the same variant",
shape.label()
);
}
}
#[test]
fn unknown_sexp_shape_carries_offending_input_verbatim() {
let err: UnknownSexpShape = "Symbol".parse::<SexpShape>().expect_err(
"capitalized `Symbol` must NOT decode — labels are byte-equal case-sensitive",
);
assert_eq!(err.0, "Symbol");
assert_eq!(format!("{err}"), "unknown sexp shape: Symbol");
let err: UnknownSexpShape = "number"
.parse::<SexpShape>()
.expect_err("`number` is ExpectedKwargShape's vocabulary, not SexpShape's");
assert_eq!(err.0, "number");
assert_eq!(format!("{err}"), "unknown sexp shape: number");
let err: UnknownSexpShape = ""
.parse::<SexpShape>()
.expect_err("empty input must NOT decode to a SexpShape");
assert_eq!(err.0, "");
assert_eq!(format!("{err}"), "unknown sexp shape: ");
}
#[test]
fn sexp_shape_from_str_accepts_only_canonical_labels() {
assert_eq!("keyword".parse::<SexpShape>().unwrap(), SexpShape::Keyword);
assert_eq!("string".parse::<SexpShape>().unwrap(), SexpShape::String);
assert_eq!("int".parse::<SexpShape>().unwrap(), SexpShape::Int);
assert_eq!("bool".parse::<SexpShape>().unwrap(), SexpShape::Bool);
assert_eq!("list".parse::<SexpShape>().unwrap(), SexpShape::List);
assert!("number".parse::<SexpShape>().is_err());
assert!("list of strings".parse::<SexpShape>().is_err());
assert!(",".parse::<SexpShape>().is_err());
assert!(",@".parse::<SexpShape>().is_err());
}
#[test]
fn sexp_shape_int_bifurcates_from_float_through_variant_slot() {
let int_err = LispError::TypeMismatch {
form: KwargPath::named("count"),
expected: ExpectedKwargShape::String,
got: SexpShape::Int,
};
let float_err = LispError::TypeMismatch {
form: KwargPath::named("ratio"),
expected: ExpectedKwargShape::String,
got: SexpShape::Float,
};
assert_eq!(
format!("{int_err}"),
"compile error in :count: expected string, got int"
);
assert_eq!(
format!("{float_err}"),
"compile error in :ratio: expected string, got float"
);
match (&int_err, &float_err) {
(LispError::TypeMismatch { got: a, .. }, LispError::TypeMismatch { got: b, .. }) => {
assert_ne!(a, b);
assert_eq!(*a, SexpShape::Int);
assert_eq!(*b, SexpShape::Float);
}
_ => panic!("both must be TypeMismatch"),
}
}
#[test]
fn as_atom_kind_projects_each_atom_shape_to_canonical_atom_kind_and_rejects_non_atom_shapes() {
use crate::ast::AtomKind;
for shape in SexpShape::ALL {
let projected = shape.as_atom_kind();
let expected = match shape {
SexpShape::Symbol => Some(AtomKind::Symbol),
SexpShape::Keyword => Some(AtomKind::Keyword),
SexpShape::String => Some(AtomKind::Str),
SexpShape::Int => Some(AtomKind::Int),
SexpShape::Float => Some(AtomKind::Float),
SexpShape::Bool => Some(AtomKind::Bool),
SexpShape::Nil
| SexpShape::List
| SexpShape::Quote
| SexpShape::Quasiquote
| SexpShape::Unquote
| SexpShape::UnquoteSplice => None,
};
assert_eq!(
projected, expected,
"SexpShape::{shape:?}.as_atom_kind() drifted from canonical mapping"
);
}
}
#[test]
fn atom_kind_sexp_shape_round_trips_through_as_atom_kind() {
use crate::ast::AtomKind;
for kind in AtomKind::ALL {
let shape = kind.sexp_shape();
let recovered = shape.as_atom_kind();
assert_eq!(
recovered,
Some(kind),
"AtomKind::{kind:?} did NOT round-trip — sexp_shape().as_atom_kind() must recover the typed marker"
);
}
}
#[test]
fn as_quote_form_projects_each_quote_shape_to_canonical_quote_form_and_rejects_non_quote_shapes(
) {
use crate::ast::QuoteForm;
for shape in SexpShape::ALL {
let projected = shape.as_quote_form();
let expected = match shape {
SexpShape::Quote => Some(QuoteForm::Quote),
SexpShape::Quasiquote => Some(QuoteForm::Quasiquote),
SexpShape::Unquote => Some(QuoteForm::Unquote),
SexpShape::UnquoteSplice => Some(QuoteForm::UnquoteSplice),
SexpShape::Nil
| SexpShape::List
| SexpShape::Symbol
| SexpShape::Keyword
| SexpShape::String
| SexpShape::Int
| SexpShape::Float
| SexpShape::Bool => None,
};
assert_eq!(
projected, expected,
"SexpShape::{shape:?}.as_quote_form() drifted from canonical mapping"
);
}
}
#[test]
fn quote_form_sexp_shape_round_trips_through_as_quote_form() {
use crate::ast::QuoteForm;
for qf in QuoteForm::ALL {
let shape = qf.sexp_shape();
let recovered = shape.as_quote_form();
assert_eq!(
recovered,
Some(qf),
"QuoteForm::{qf:?} did NOT round-trip — sexp_shape().as_quote_form() must recover the typed marker"
);
}
}
#[test]
fn as_atom_kind_and_as_quote_form_partition_carvable_sexp_shape_variants() {
for shape in SexpShape::ALL {
let atom = shape.as_atom_kind().is_some();
let quote = shape.as_quote_form().is_some();
assert!(
!(atom && quote),
"SexpShape::{shape:?} projects as BOTH an atom kind AND a quote form — typed-shape carvings must be disjoint"
);
let carved = atom || quote;
let expected_carved = shape.as_structural_kind().is_none();
assert_eq!(
carved, expected_carved,
"SexpShape::{shape:?} must be carved iff it does not project through as_structural_kind"
);
}
}
#[test]
fn sexp_shape_partition_is_total_across_atom_quote_structural_carvings() {
let mut atom_count = 0;
let mut quote_count = 0;
let mut structural_count = 0;
for shape in SexpShape::ALL {
let atom = shape.as_atom_kind().is_some();
let quote = shape.as_quote_form().is_some();
let structural = shape.as_structural_kind().is_some();
let membership = usize::from(atom) + usize::from(quote) + usize::from(structural);
assert_eq!(
membership, 1,
"SexpShape::{shape:?} lands in {membership} carvings — partition must land it in EXACTLY ONE of (as_atom_kind, as_quote_form, as_structural_kind)",
);
atom_count += usize::from(atom);
quote_count += usize::from(quote);
structural_count += usize::from(structural);
}
assert_eq!(
atom_count, 6,
"atomic-payload carving must cover EXACTLY 6 of SexpShape::ALL",
);
assert_eq!(
quote_count, 4,
"quote-family carving must cover EXACTLY 4 of SexpShape::ALL",
);
assert_eq!(
structural_count, 2,
"structural-residual carving must cover EXACTLY 2 of SexpShape::ALL",
);
assert_eq!(
atom_count + quote_count + structural_count,
SexpShape::ALL.len(),
"the three carvings must partition SexpShape::ALL totally (6 + 4 + 2 = 12)",
);
}
#[test]
fn as_atom_kind_composes_with_sexp_shape_via_atom_kind_label_round_trip() {
use crate::ast::AtomKind;
for kind in AtomKind::ALL {
let via_round_trip = kind
.sexp_shape()
.as_atom_kind()
.expect("every AtomKind round-trips through the embed/project pair")
.label();
assert_eq!(
via_round_trip,
kind.label(),
"AtomKind::{kind:?}.label() drifted from sexp_shape().as_atom_kind().label() — embed/project must preserve label coherence"
);
}
}
#[test]
fn as_quote_form_composes_with_sexp_shape_via_quote_form_prefix_round_trip() {
use crate::ast::QuoteForm;
for qf in QuoteForm::ALL {
let via_round_trip = qf
.sexp_shape()
.as_quote_form()
.expect("every QuoteForm round-trips through the embed/project pair")
.prefix();
assert_eq!(
via_round_trip,
qf.prefix(),
"QuoteForm::{qf:?}.prefix() drifted from sexp_shape().as_quote_form().prefix() — embed/project must preserve prefix coherence"
);
}
}
#[test]
fn unquote_form_all_is_unique_and_complete() {
assert_eq!(UnquoteForm::ALL.len(), 2);
assert_eq!(
<UnquoteForm as tatara_closed_set::ClosedSet>::sorted_labels(),
vec![",", ",@"],
"UnquoteForm::ALL must cover both template-marker syntactic forms"
);
}
#[test]
fn unquote_form_marker_round_trips_through_from_str() {
for form in UnquoteForm::ALL {
let parsed: UnquoteForm = form
.marker()
.parse()
.expect("every ALL variant's marker must round-trip through FromStr");
assert_eq!(
parsed,
form,
"FromStr({}) must round-trip to the same variant",
form.marker()
);
}
}
#[test]
fn unknown_unquote_form_carries_offending_input_verbatim() {
let err: UnknownUnquoteForm = ",,"
.parse::<UnquoteForm>()
.expect_err("doubled comma `,,` is not a canonical template marker");
assert_eq!(err.0, ",,");
assert_eq!(format!("{err}"), "unknown unquote form: ,,");
let err: UnknownUnquoteForm = ",@@"
.parse::<UnquoteForm>()
.expect_err("doubled-at `,@@` is not a canonical template marker");
assert_eq!(err.0, ",@@");
assert_eq!(format!("{err}"), "unknown unquote form: ,@@");
let err: UnknownUnquoteForm = ""
.parse::<UnquoteForm>()
.expect_err("empty input must NOT decode to an UnquoteForm");
assert_eq!(err.0, "");
assert_eq!(format!("{err}"), "unknown unquote form: ");
}
#[test]
fn unquote_form_from_str_rejects_sexp_shape_labels_on_template_marker_axis() {
assert_eq!(",".parse::<UnquoteForm>().unwrap(), UnquoteForm::Unquote);
assert_eq!(",@".parse::<UnquoteForm>().unwrap(), UnquoteForm::Splice);
assert!("unquote".parse::<UnquoteForm>().is_err());
assert!("unquote-splice".parse::<UnquoteForm>().is_err());
assert!("'".parse::<UnquoteForm>().is_err());
assert!("`".parse::<UnquoteForm>().is_err());
assert!(" ,".parse::<UnquoteForm>().is_err());
assert!(", ".parse::<UnquoteForm>().is_err());
assert!(", @".parse::<UnquoteForm>().is_err());
}
#[test]
fn unquote_form_to_quote_form_round_trips_through_as_unquote_form() {
use crate::ast::QuoteForm;
for uf in UnquoteForm::ALL {
assert_eq!(
uf.to_quote_form().as_unquote_form(),
Some(uf),
"UnquoteForm::{uf:?} → QuoteForm via to_quote_form does not invert through QuoteForm::as_unquote_form — the 2-of-4 subset projection is no longer a section",
);
}
assert_eq!(UnquoteForm::Unquote.to_quote_form(), QuoteForm::Unquote);
assert_eq!(
UnquoteForm::Splice.to_quote_form(),
QuoteForm::UnquoteSplice
);
}
#[test]
fn unquote_form_marker_routes_through_to_quote_form_prefix_via_composition() {
for uf in UnquoteForm::ALL {
let from_marker = uf.marker();
let from_composition = uf.to_quote_form().prefix();
assert_eq!(
from_marker, from_composition,
"UnquoteForm::{uf:?}.marker() bytes drifted from .to_quote_form().prefix() bytes — the subset's diagnostic vocabulary is no longer derived from the superset's canonical site",
);
assert!(
std::ptr::eq(from_marker.as_ptr(), from_composition.as_ptr()),
"UnquoteForm::{uf:?}.marker() and .to_quote_form().prefix() disagree on `&'static str` address — pointer drift means the lift composes through a parallel literal table rather than routing into the canonical QuoteForm::prefix site",
);
}
}
#[test]
fn unquote_form_per_role_markers_alias_quote_form_per_role_prefixes_byte_for_byte() {
use crate::ast::QuoteForm;
assert_eq!(UnquoteForm::UNQUOTE_MARKER, QuoteForm::UNQUOTE_PREFIX);
assert_eq!(UnquoteForm::SPLICE_MARKER, QuoteForm::UNQUOTE_SPLICE_PREFIX);
}
#[test]
fn unquote_form_marker_arms_route_through_per_role_markers_for_every_variant() {
assert_eq!(UnquoteForm::Unquote.marker(), UnquoteForm::UNQUOTE_MARKER);
assert_eq!(UnquoteForm::Splice.marker(), UnquoteForm::SPLICE_MARKER);
}
#[test]
fn unquote_form_markers_has_expected_cardinality() {
assert_eq!(UnquoteForm::MARKERS.len(), 2);
assert_eq!(UnquoteForm::MARKERS.len(), UnquoteForm::ALL.len());
}
#[test]
fn unquote_form_markers_align_with_all_by_index() {
assert_eq!(UnquoteForm::MARKERS.len(), UnquoteForm::ALL.len());
for (i, form) in UnquoteForm::ALL.iter().enumerate() {
assert_eq!(
UnquoteForm::MARKERS[i],
form.marker(),
"UnquoteForm::MARKERS[{i}] `{mkr}` drifted from \
UnquoteForm::ALL[{i}].marker() `{via_variant}` — the \
canonical ALL ordering and the MARKERS ordering must \
match element-wise",
mkr = UnquoteForm::MARKERS[i],
via_variant = form.marker(),
);
}
}
#[test]
fn unquote_form_markers_pairwise_distinct() {
for (i, a) in UnquoteForm::MARKERS.iter().enumerate() {
for (j, b) in UnquoteForm::MARKERS.iter().enumerate() {
if i == j {
continue;
}
assert_ne!(
a, b,
"UnquoteForm::MARKERS[{i}] ({a:?}) collides with \
UnquoteForm::MARKERS[{j}] ({b:?}) — two distinct \
template markers cannot share diagnostic bytes",
);
}
}
}
#[test]
fn unquote_form_unquote_lead_aliases_quote_form_unquote_lead_byte_for_byte() {
assert_eq!(
UnquoteForm::UNQUOTE_LEAD,
crate::ast::QuoteForm::UNQUOTE_LEAD,
"UnquoteForm::UNQUOTE_LEAD drifted from QuoteForm::UNQUOTE_LEAD — the alias chain broke",
);
}
#[test]
fn unquote_form_unquote_lead_pins_canonical_reader_lead_char() {
assert_eq!(
UnquoteForm::UNQUOTE_LEAD,
',',
"UnquoteForm::UNQUOTE_LEAD drifted from legacy reader-lead char ','",
);
}
#[test]
fn unquote_form_unquote_lead_routes_through_to_quote_form_lead_char_via_composition() {
for uf in UnquoteForm::ALL {
let via_composition = uf.to_quote_form().lead_char();
assert_eq!(
UnquoteForm::UNQUOTE_LEAD, via_composition,
"UnquoteForm::{uf:?} projects through .to_quote_form().lead_char() = `{via_composition}` — but UnquoteForm::UNQUOTE_LEAD = `{}` — the SHAPE-ASYMMETRIC-COLLAPSE identity (both subset arms → one lead byte) broke",
UnquoteForm::UNQUOTE_LEAD,
);
}
}
#[test]
fn unquote_form_leads_has_expected_shape_asymmetric_cardinality() {
assert_eq!(
UnquoteForm::LEADS.len(),
1,
"UnquoteForm::LEADS cardinality drifted from 1 — the shared-lead-char collapse invariant on the 2-of-4 substitution-subset carving is broken",
);
assert!(
UnquoteForm::LEADS.len() < UnquoteForm::ALL.len(),
"UnquoteForm::LEADS.len() ({leads}) MUST be strictly less than UnquoteForm::ALL.len() ({all}) — the SHAPE-ASYMMETRIC-COLLAPSE identity requires a strict cardinality gap",
leads = UnquoteForm::LEADS.len(),
all = UnquoteForm::ALL.len(),
);
}
#[test]
fn unquote_form_leads_covers_all_variants_via_to_quote_form_lead_char() {
for uf in UnquoteForm::ALL {
let lead = uf.to_quote_form().lead_char();
assert!(
UnquoteForm::LEADS.contains(&lead),
"UnquoteForm::{uf:?} projects through .to_quote_form().lead_char() = `{lead}` — but UnquoteForm::LEADS ({leads:?}) does NOT contain `{lead}`; the distinct-lead-byte ALL array must cover every variant's lead byte",
leads = UnquoteForm::LEADS,
);
}
}
#[test]
fn unquote_form_leads_is_subset_of_quote_form_leads() {
use crate::ast::QuoteForm;
for lead in UnquoteForm::LEADS {
assert!(
QuoteForm::LEADS.contains(&lead),
"UnquoteForm::LEADS entry `{lead}` MUST appear in QuoteForm::LEADS ({parent:?}) — the UnquoteForm ⊂ QuoteForm subset-inclusion breaks otherwise",
parent = QuoteForm::LEADS,
);
}
}
#[test]
fn unquote_form_leads_matches_to_quote_form_lead_char_image_exactly() {
use std::collections::HashSet;
let leads_set: HashSet<char> = UnquoteForm::LEADS.iter().copied().collect();
let image_set: HashSet<char> = UnquoteForm::ALL
.iter()
.map(|uf| uf.to_quote_form().lead_char())
.collect();
assert_eq!(
leads_set, image_set,
"UnquoteForm::LEADS ({leads_set:?}) as a set MUST equal the image of UnquoteForm::ALL under .to_quote_form().lead_char() ({image_set:?}) — the distinct-lead-byte closed-set ALL array drifted from its composition source",
);
}
#[test]
fn unquote_form_per_role_iac_forge_tags_alias_quote_form_per_role_iac_forge_tags_byte_for_byte()
{
use crate::ast::QuoteForm;
assert_eq!(
UnquoteForm::UNQUOTE_IAC_FORGE_TAG,
QuoteForm::UNQUOTE_IAC_FORGE_TAG,
);
assert_eq!(
UnquoteForm::SPLICE_IAC_FORGE_TAG,
QuoteForm::UNQUOTE_SPLICE_IAC_FORGE_TAG,
);
}
#[test]
fn unquote_form_iac_forge_tag_arms_route_through_per_role_iac_forge_tags_for_every_variant() {
assert_eq!(
UnquoteForm::Unquote.iac_forge_tag(),
UnquoteForm::UNQUOTE_IAC_FORGE_TAG,
);
assert_eq!(
UnquoteForm::Splice.iac_forge_tag(),
UnquoteForm::SPLICE_IAC_FORGE_TAG,
);
}
#[test]
fn unquote_form_iac_forge_tags_has_expected_cardinality() {
assert_eq!(UnquoteForm::IAC_FORGE_TAGS.len(), 2);
assert_eq!(UnquoteForm::IAC_FORGE_TAGS.len(), UnquoteForm::ALL.len(),);
}
#[test]
fn unquote_form_iac_forge_tags_align_with_all_by_index() {
assert_eq!(UnquoteForm::IAC_FORGE_TAGS.len(), UnquoteForm::ALL.len(),);
for (i, form) in UnquoteForm::ALL.iter().enumerate() {
assert_eq!(
UnquoteForm::IAC_FORGE_TAGS[i],
form.iac_forge_tag(),
"UnquoteForm::IAC_FORGE_TAGS[{i}] `{tag}` drifted \
from UnquoteForm::ALL[{i}].iac_forge_tag() \
`{via_variant}` — the canonical ALL ordering and \
the IAC_FORGE_TAGS ordering must match element-wise",
tag = UnquoteForm::IAC_FORGE_TAGS[i],
via_variant = form.iac_forge_tag(),
);
}
}
#[test]
fn unquote_form_iac_forge_tags_pairwise_distinct() {
for (i, a) in UnquoteForm::IAC_FORGE_TAGS.iter().enumerate() {
for (j, b) in UnquoteForm::IAC_FORGE_TAGS.iter().enumerate() {
if i == j {
continue;
}
assert_ne!(
a, b,
"UnquoteForm::IAC_FORGE_TAGS[{i}] ({a:?}) \
collides with UnquoteForm::IAC_FORGE_TAGS[{j}] \
({b:?}) — two distinct template markers cannot \
share cross-crate canonical-form bytes",
);
}
}
}
#[test]
fn unquote_form_label_routes_through_to_quote_form_label_via_composition() {
for uf in UnquoteForm::ALL {
let from_label = uf.label();
let from_composition = uf.to_quote_form().label();
assert_eq!(
from_label, from_composition,
"UnquoteForm::{uf:?}.label() bytes drifted from .to_quote_form().label() bytes — the subset's diagnostic vocabulary is no longer derived from the superset's canonical site",
);
assert!(
std::ptr::eq(from_label.as_ptr(), from_composition.as_ptr()),
"UnquoteForm::{uf:?}.label() and .to_quote_form().label() disagree on `&'static str` address — pointer drift means the lift composes through a parallel literal table rather than routing into the canonical QuoteForm::label / SexpShape::*_LABEL site",
);
}
}
#[test]
fn unquote_form_per_role_labels_alias_quote_form_per_role_labels_byte_for_byte() {
use crate::ast::QuoteForm;
assert_eq!(UnquoteForm::UNQUOTE_LABEL, QuoteForm::UNQUOTE_LABEL);
assert_eq!(UnquoteForm::SPLICE_LABEL, QuoteForm::UNQUOTE_SPLICE_LABEL);
}
#[test]
fn unquote_form_label_arms_route_through_per_role_labels_for_every_variant() {
assert_eq!(UnquoteForm::Unquote.label(), UnquoteForm::UNQUOTE_LABEL);
assert_eq!(UnquoteForm::Splice.label(), UnquoteForm::SPLICE_LABEL);
}
#[test]
fn unquote_form_labels_has_expected_cardinality() {
assert_eq!(UnquoteForm::LABELS.len(), 2);
assert_eq!(UnquoteForm::LABELS.len(), UnquoteForm::ALL.len());
}
#[test]
fn unquote_form_labels_align_with_all_by_index() {
assert_eq!(UnquoteForm::LABELS.len(), UnquoteForm::ALL.len());
for (i, form) in UnquoteForm::ALL.iter().enumerate() {
assert_eq!(
UnquoteForm::LABELS[i],
form.label(),
"UnquoteForm::LABELS[{i}] `{lbl}` drifted from \
UnquoteForm::ALL[{i}].label() `{via_variant}` — the \
canonical ALL ordering and the LABELS ordering must \
match element-wise",
lbl = UnquoteForm::LABELS[i],
via_variant = form.label(),
);
}
}
#[test]
fn unquote_form_labels_pairwise_distinct() {
for (i, a) in UnquoteForm::LABELS.iter().enumerate() {
for (j, b) in UnquoteForm::LABELS.iter().enumerate() {
if i == j {
continue;
}
assert_ne!(
a, b,
"UnquoteForm::LABELS[{i}] ({a:?}) collides with \
UnquoteForm::LABELS[{j}] ({b:?}) — two distinct \
template markers cannot share diagnostic bytes",
);
}
}
}
#[test]
fn unquote_form_label_emits_canonical_sexp_shape_label_for_every_marker() {
assert_eq!(
UnquoteForm::Unquote.label(),
"unquote",
"UnquoteForm::Unquote.label() drifted from canonical substrate 'unquote' diagnostic",
);
assert_eq!(
UnquoteForm::Splice.label(),
"unquote-splice",
"UnquoteForm::Splice.label() drifted from canonical substrate 'unquote-splice' \
diagnostic — the short form is load-bearing (distinct from iac-forge 'unquote-splicing')",
);
}
#[test]
fn unquote_form_label_diverges_from_iac_forge_tag_at_splice_arm() {
assert_eq!(
UnquoteForm::Unquote.label(),
UnquoteForm::Unquote.iac_forge_tag(),
"UnquoteForm::Unquote.label() and .iac_forge_tag() must agree on 'unquote' — the two axes only diverge at the Splice arm",
);
assert_ne!(
UnquoteForm::Splice.label(),
UnquoteForm::Splice.iac_forge_tag(),
"UnquoteForm::Splice.label() and .iac_forge_tag() must diverge — substrate renders 'unquote-splice', iac-forge canonical form is 'unquote-splicing'",
);
}
#[test]
fn unquote_form_wrap_routes_through_to_quote_form_wrap_via_composition() {
use crate::ast::Sexp;
let inners = [
Sexp::Nil,
Sexp::symbol("x"),
Sexp::keyword("k"),
Sexp::string("s"),
Sexp::int(42),
Sexp::float(1.5),
Sexp::boolean(true),
Sexp::List(vec![Sexp::symbol("f"), Sexp::int(1), Sexp::int(2)]),
];
for uf in UnquoteForm::ALL {
for inner in &inners {
let via_wrap = uf.wrap(inner.clone());
let via_composition = uf.to_quote_form().wrap(inner.clone());
assert_eq!(
via_wrap, via_composition,
"UnquoteForm::{uf:?}.wrap(inner) drifted from .to_quote_form().wrap(inner) — the subset's construct vocabulary is no longer derived from the superset's canonical site",
);
}
}
}
#[test]
fn unquote_form_wrap_emits_canonical_tuple_variant_for_every_marker() {
use crate::ast::Sexp;
let inners = [
Sexp::Nil,
Sexp::symbol("x"),
Sexp::keyword("k"),
Sexp::string("s"),
Sexp::int(-7),
Sexp::float(2.0),
Sexp::boolean(false),
Sexp::List(vec![Sexp::symbol("f"), Sexp::int(1)]),
];
for inner in &inners {
assert_eq!(
UnquoteForm::Unquote.wrap(inner.clone()),
Sexp::Unquote(Box::new(inner.clone())),
"UnquoteForm::Unquote.wrap({inner:?}) drifted from Sexp::Unquote(Box::new({inner:?})) canonical tuple-variant shape",
);
assert_eq!(
UnquoteForm::Splice.wrap(inner.clone()),
Sexp::UnquoteSplice(Box::new(inner.clone())),
"UnquoteForm::Splice.wrap({inner:?}) drifted from Sexp::UnquoteSplice(Box::new({inner:?})) canonical tuple-variant shape",
);
}
}
#[test]
fn unquote_form_wrap_round_trips_through_sexp_as_unquote() {
use crate::ast::Sexp;
let inners = [
Sexp::Nil,
Sexp::symbol("x"),
Sexp::keyword("k"),
Sexp::string("s"),
Sexp::int(1),
Sexp::List(vec![Sexp::symbol("f")]),
];
for uf in UnquoteForm::ALL {
for inner in &inners {
let wrapped = uf.wrap(inner.clone());
assert_eq!(
wrapped.as_unquote(),
Some((uf, inner)),
"UnquoteForm::{uf:?}.wrap({inner:?}).as_unquote() failed to round-trip — the (construct, project) pair on the subset algebra is not a section-for-retraction of Sexp::as_unquote",
);
}
}
}
#[test]
fn unquote_form_wrap_composes_with_shape_via_to_quote_form_sexp_shape() {
use crate::ast::Sexp;
let inners = [
Sexp::Nil,
Sexp::symbol("x"),
Sexp::int(42),
Sexp::List(vec![Sexp::symbol("f")]),
];
for uf in UnquoteForm::ALL {
for inner in &inners {
assert_eq!(
uf.wrap(inner.clone()).shape(),
uf.to_quote_form().sexp_shape(),
"UnquoteForm::{uf:?}.wrap({inner:?}).shape() drifted from .to_quote_form().sexp_shape() — the (subset marker, outer SexpShape) pairing is no longer derived from the superset's canonical composition",
);
}
}
}
#[test]
fn unquote_form_sexp_shape_routes_through_to_quote_form_sexp_shape_via_composition() {
for uf in UnquoteForm::ALL {
let from_shape = uf.sexp_shape();
let from_composition = uf.to_quote_form().sexp_shape();
assert_eq!(
from_shape, from_composition,
"UnquoteForm::{uf:?}.sexp_shape() drifted from .to_quote_form().sexp_shape() — the subset's outer-shape vocabulary is no longer derived from the superset's canonical site",
);
}
}
#[test]
fn unquote_form_sexp_shape_emits_canonical_shape_for_every_marker() {
assert_eq!(
UnquoteForm::Unquote.sexp_shape(),
SexpShape::Unquote,
"UnquoteForm::Unquote.sexp_shape() drifted from SexpShape::Unquote",
);
assert_eq!(
UnquoteForm::Splice.sexp_shape(),
SexpShape::UnquoteSplice,
"UnquoteForm::Splice.sexp_shape() drifted from SexpShape::UnquoteSplice",
);
}
#[test]
fn unquote_form_iac_forge_tag_routes_through_to_quote_form_iac_forge_tag_via_composition() {
for uf in UnquoteForm::ALL {
let from_tag = uf.iac_forge_tag();
let from_composition = uf.to_quote_form().iac_forge_tag();
assert_eq!(
from_tag, from_composition,
"UnquoteForm::{uf:?}.iac_forge_tag() drifted from .to_quote_form().iac_forge_tag() — the subset's canonical interop vocabulary is no longer derived from the superset's canonical site",
);
}
}
#[test]
fn unquote_form_iac_forge_tag_emits_canonical_cl_tag_for_every_marker() {
assert_eq!(
UnquoteForm::Unquote.iac_forge_tag(),
"unquote",
"UnquoteForm::Unquote.iac_forge_tag() drifted from canonical CL 'unquote' tag",
);
assert_eq!(
UnquoteForm::Splice.iac_forge_tag(),
"unquote-splicing",
"UnquoteForm::Splice.iac_forge_tag() drifted from canonical CL 'unquote-splicing' tag \
— the '-splicing' suffix is load-bearing for iac-forge canonical-form round-trip",
);
}
#[test]
fn unquote_form_iac_forge_tag_diverges_from_sexp_shape_label_at_splice_arm() {
assert_eq!(
UnquoteForm::Unquote.iac_forge_tag(),
SexpShape::Unquote.label(),
"the Unquote arm's iac-forge tag AND diagnostic label MUST agree — \
both spell 'unquote' by CL + substrate convention alike",
);
assert_eq!(
UnquoteForm::Splice.iac_forge_tag(),
"unquote-splicing",
"UnquoteForm::Splice.iac_forge_tag() drifted from canonical CL 'unquote-splicing' tag",
);
assert_eq!(
SexpShape::UnquoteSplice.label(),
"unquote-splice",
"SexpShape::UnquoteSplice.label() drifted from substrate diagnostic 'unquote-splice' label",
);
assert_ne!(
UnquoteForm::Splice.iac_forge_tag(),
SexpShape::UnquoteSplice.label(),
"the two projections MUST disagree at Splice — the CL canonical form \
requires '-splicing' while the substrate's diagnostic label uses the \
shorter '-splice'; consolidating them would break either side",
);
}
#[test]
fn unquote_form_iac_forge_tag_agrees_with_outer_sexp_iac_forge_arm_when_iac_forge_feature_gate_is_on(
) {
for uf in UnquoteForm::ALL {
let via_subset = uf.iac_forge_tag();
let via_superset = uf.to_quote_form().iac_forge_tag();
assert_eq!(
via_subset, via_superset,
"UnquoteForm::{uf:?}.iac_forge_tag() drifted from .to_quote_form().iac_forge_tag() — \
the outer interop arm's byte-identity on the substitution-subset carving is broken",
);
}
}
#[test]
fn unquote_form_iac_forge_tag_specializes_to_matching_arm_of_quote_form_iac_forge_tag() {
use crate::ast::QuoteForm;
assert_eq!(
UnquoteForm::Unquote.iac_forge_tag(),
QuoteForm::Unquote.iac_forge_tag(),
"UnquoteForm::Unquote.iac_forge_tag() drifted from QuoteForm::Unquote.iac_forge_tag()",
);
assert_eq!(
UnquoteForm::Splice.iac_forge_tag(),
QuoteForm::UnquoteSplice.iac_forge_tag(),
"UnquoteForm::Splice.iac_forge_tag() drifted from QuoteForm::UnquoteSplice.iac_forge_tag()",
);
}
#[test]
fn unquote_form_hash_discriminator_routes_through_to_quote_form_hash_discriminator_via_composition(
) {
for uf in UnquoteForm::ALL {
let from_disc = uf.hash_discriminator();
let from_composition = uf.to_quote_form().hash_discriminator();
assert_eq!(
from_disc, from_composition,
"UnquoteForm::{uf:?}.hash_discriminator() drifted from .to_quote_form().hash_discriminator() — the subset's cache-key vocabulary is no longer derived from the superset's canonical site",
);
}
}
#[test]
fn unquote_form_hash_discriminator_pins_legacy_cache_key_bytes() {
assert_eq!(
UnquoteForm::Unquote.hash_discriminator(),
5,
"UnquoteForm::Unquote.hash_discriminator() drifted from legacy cache-key byte 5",
);
assert_eq!(
UnquoteForm::Splice.hash_discriminator(),
6,
"UnquoteForm::Splice.hash_discriminator() drifted from legacy cache-key byte 6",
);
}
#[test]
fn unquote_form_hash_discriminator_specializes_to_matching_arm_of_quote_form_hash_discriminator(
) {
use crate::ast::QuoteForm;
assert_eq!(
UnquoteForm::Unquote.hash_discriminator(),
QuoteForm::Unquote.hash_discriminator(),
"UnquoteForm::Unquote.hash_discriminator() drifted from QuoteForm::Unquote.hash_discriminator()",
);
assert_eq!(
UnquoteForm::Splice.hash_discriminator(),
QuoteForm::UnquoteSplice.hash_discriminator(),
"UnquoteForm::Splice.hash_discriminator() drifted from QuoteForm::UnquoteSplice.hash_discriminator()",
);
}
#[test]
fn unquote_form_hash_discriminator_partitions_disjointly_from_non_substitution_carvings() {
use crate::ast::QuoteForm;
use std::collections::HashSet;
let subset_image: HashSet<u8> = UnquoteForm::ALL
.iter()
.map(|uf| uf.hash_discriminator())
.collect();
assert_eq!(
subset_image,
HashSet::from([5, 6]),
"UnquoteForm::ALL swept through hash_discriminator MUST cover exactly {{5, 6}} — the substitution-subset cache-key partition",
);
let structural_image: HashSet<u8> = StructuralKind::ALL
.iter()
.map(|sk| sk.hash_discriminator())
.collect();
assert!(
subset_image.is_disjoint(&structural_image),
"UnquoteForm hash_discriminator image {subset_image:?} MUST be disjoint from StructuralKind image {structural_image:?} — a cache-key collision would leak macro-expansion hits across the substitution-subset and structural-residual carvings",
);
let atom_outer_byte: HashSet<u8> = HashSet::from([1]);
assert!(
subset_image.is_disjoint(&atom_outer_byte),
"UnquoteForm hash_discriminator image {subset_image:?} MUST be disjoint from the outer Atom carve marker {{1}} — a cache-key collision would leak macro-expansion hits across the substitution-subset and atomic-payload carvings",
);
let non_substitution_quote_image: HashSet<u8> = [QuoteForm::Quote, QuoteForm::Quasiquote]
.iter()
.map(|qf| qf.hash_discriminator())
.collect();
assert!(
subset_image.is_disjoint(&non_substitution_quote_image),
"UnquoteForm hash_discriminator image {subset_image:?} MUST be disjoint from the non-substitution QuoteForm arms' image {non_substitution_quote_image:?} — a cache-key collision would leak macro-expansion hits across the substitution-subset and non-substitution quote-family carvings",
);
}
#[test]
fn unquote_form_per_role_hash_discriminators_alias_quote_form_per_role_hash_discriminators_byte_for_byte(
) {
use crate::ast::QuoteForm;
assert_eq!(
UnquoteForm::UNQUOTE_HASH_DISCRIMINATOR,
QuoteForm::UNQUOTE_HASH_DISCRIMINATOR
);
assert_eq!(
UnquoteForm::SPLICE_HASH_DISCRIMINATOR,
QuoteForm::UNQUOTE_SPLICE_HASH_DISCRIMINATOR
);
}
#[test]
fn unquote_form_hash_discriminator_arms_route_through_per_role_hash_discriminators_for_every_variant(
) {
assert_eq!(
UnquoteForm::Unquote.hash_discriminator(),
UnquoteForm::UNQUOTE_HASH_DISCRIMINATOR
);
assert_eq!(
UnquoteForm::Splice.hash_discriminator(),
UnquoteForm::SPLICE_HASH_DISCRIMINATOR
);
}
#[test]
fn unquote_form_hash_discriminators_has_expected_cardinality() {
assert_eq!(UnquoteForm::HASH_DISCRIMINATORS.len(), 2);
assert_eq!(
UnquoteForm::HASH_DISCRIMINATORS.len(),
UnquoteForm::ALL.len()
);
}
#[test]
fn unquote_form_hash_discriminators_align_with_all_by_index() {
assert_eq!(
UnquoteForm::HASH_DISCRIMINATORS.len(),
UnquoteForm::ALL.len()
);
for (i, form) in UnquoteForm::ALL.iter().enumerate() {
assert_eq!(
UnquoteForm::HASH_DISCRIMINATORS[i],
form.hash_discriminator(),
"UnquoteForm::HASH_DISCRIMINATORS[{i}] `{byte}` drifted \
from UnquoteForm::ALL[{i}].hash_discriminator() \
`{via_variant}` — the canonical ALL ordering and the \
HASH_DISCRIMINATORS ordering must match element-wise",
byte = UnquoteForm::HASH_DISCRIMINATORS[i],
via_variant = form.hash_discriminator(),
);
}
}
#[test]
fn unquote_form_hash_discriminators_pairwise_distinct() {
for (i, a) in UnquoteForm::HASH_DISCRIMINATORS.iter().enumerate() {
for (j, b) in UnquoteForm::HASH_DISCRIMINATORS.iter().enumerate() {
if i == j {
continue;
}
assert_ne!(
a, b,
"UnquoteForm::HASH_DISCRIMINATORS[{i}] ({a}) collides \
with UnquoteForm::HASH_DISCRIMINATORS[{j}] ({b}) — \
two distinct template markers cannot share cache-key \
bytes",
);
}
}
}
#[test]
fn unquote_form_hash_discriminators_pin_legacy_cache_key_bytes() {
assert_eq!(
UnquoteForm::UNQUOTE_HASH_DISCRIMINATOR,
5,
"UnquoteForm::UNQUOTE_HASH_DISCRIMINATOR drifted from legacy cache-key byte 5",
);
assert_eq!(
UnquoteForm::SPLICE_HASH_DISCRIMINATOR,
6,
"UnquoteForm::SPLICE_HASH_DISCRIMINATOR drifted from legacy cache-key byte 6",
);
}
#[test]
fn unquote_form_hash_discriminators_align_with_superset_by_projection() {
use crate::ast::QuoteForm;
for (i, uf) in UnquoteForm::ALL.iter().enumerate() {
let subset_byte = UnquoteForm::HASH_DISCRIMINATORS[i];
let superset_byte = uf.to_quote_form().hash_discriminator();
assert_eq!(
subset_byte, superset_byte,
"UnquoteForm::HASH_DISCRIMINATORS[{i}] ({subset_byte}) \
drifted from ALL[{i}].to_quote_form().hash_discriminator() \
({superset_byte}) — the subset-through-superset composition \
no longer agrees with the aliased `pub(crate) const` array",
);
}
assert_eq!(
UnquoteForm::HASH_DISCRIMINATORS[0],
QuoteForm::HASH_DISCRIMINATORS[2]
);
assert_eq!(
UnquoteForm::HASH_DISCRIMINATORS[1],
QuoteForm::HASH_DISCRIMINATORS[3]
);
}
#[test]
fn unquote_form_promotions_has_expected_cardinality() {
assert_eq!(UnquoteForm::PROMOTIONS.len(), 1);
}
#[test]
fn unquote_form_promotions_pin_legacy_substitution_subset_splice_promotion_triple() {
let (head, disc, promoted) = UnquoteForm::PROMOTIONS[0];
assert_eq!(head, UnquoteForm::Unquote);
assert_eq!(disc, crate::ast::QuoteForm::SPLICE_DISCRIMINATOR);
assert_eq!(disc, '@');
assert_eq!(promoted, UnquoteForm::Splice);
}
#[test]
fn unquote_form_promotions_align_with_quote_form_promotions_by_projection() {
use crate::ast::QuoteForm;
for i in 0..UnquoteForm::PROMOTIONS.len() {
let (subset_head, subset_disc, subset_promoted) = UnquoteForm::PROMOTIONS[i];
let projected_head = subset_head.to_quote_form();
let projected_promoted = subset_promoted.to_quote_form();
let superset_triple = QuoteForm::PROMOTIONS[i];
assert_eq!(
(projected_head, subset_disc, projected_promoted),
superset_triple,
"UnquoteForm::PROMOTIONS[{i}] projection through \
`to_quote_form()` drifted from \
QuoteForm::PROMOTIONS[{i}]",
);
}
}
#[test]
fn unquote_form_promotions_head_column_is_unquote_and_promoted_column_is_splice() {
for (head, _, promoted) in UnquoteForm::PROMOTIONS {
assert_eq!(
head,
UnquoteForm::Unquote,
"the substitution-subset promotion head must be the \
single-char-marker variant `UnquoteForm::Unquote`",
);
assert_eq!(
promoted,
UnquoteForm::Splice,
"the substitution-subset promotion target must be \
the two-char-marker variant `UnquoteForm::Splice`",
);
assert_eq!(
head.marker(),
crate::ast::QuoteForm::UNQUOTE_PREFIX,
"the substitution-subset promotion head's marker \
must alias `QuoteForm::UNQUOTE_PREFIX`",
);
assert_eq!(
promoted.marker(),
crate::ast::QuoteForm::UNQUOTE_SPLICE_PREFIX,
"the substitution-subset promotion target's marker \
must alias `QuoteForm::UNQUOTE_SPLICE_PREFIX`",
);
}
}
#[test]
fn unquote_form_promotions_compose_marker_from_head_marker_and_discriminator() {
for (head, disc, promoted) in UnquoteForm::PROMOTIONS {
let composed = format!("{}{}", head.marker(), disc);
assert_eq!(
composed,
promoted.marker(),
"UnquoteForm::PROMOTIONS `({head:?}, {disc:?}, {promoted:?})` \
marker composition drifted — expected \
`head.marker() + disc` to equal `promoted.marker()`",
);
}
}
#[test]
fn unquote_form_sexp_shape_round_trips_through_as_unquote_form() {
for uf in UnquoteForm::ALL {
let shape = uf.sexp_shape();
let recovered = shape.as_unquote_form();
assert_eq!(
recovered,
Some(uf),
"UnquoteForm::{uf:?} did NOT round-trip — sexp_shape().as_unquote_form() must recover the typed marker",
);
}
}
#[test]
fn unquote_form_per_role_shapes_alias_quote_form_per_role_shapes_byte_for_byte() {
assert_eq!(
UnquoteForm::UNQUOTE_SHAPE,
crate::ast::QuoteForm::UNQUOTE_SHAPE,
);
assert_eq!(
UnquoteForm::SPLICE_SHAPE,
crate::ast::QuoteForm::UNQUOTE_SPLICE_SHAPE,
);
}
#[test]
fn unquote_form_per_role_shapes_pin_canonical_sexp_shape_variants() {
assert_eq!(UnquoteForm::UNQUOTE_SHAPE, SexpShape::Unquote);
assert_eq!(UnquoteForm::SPLICE_SHAPE, SexpShape::UnquoteSplice);
}
#[test]
fn unquote_form_shapes_has_expected_cardinality() {
assert_eq!(
UnquoteForm::SHAPES.len(),
2,
"UnquoteForm::SHAPES cardinality drifted from the two-arm substitution-subset algebra",
);
assert_eq!(
UnquoteForm::SHAPES.len(),
UnquoteForm::ALL.len(),
"UnquoteForm::SHAPES cardinality drifted from UnquoteForm::ALL — every family-wide array on this algebra must stay ALL-length in lockstep",
);
}
#[test]
fn unquote_form_shapes_align_with_all_by_index() {
for (i, uf) in UnquoteForm::ALL.iter().enumerate() {
let via_variant = uf.sexp_shape();
let via_array = UnquoteForm::SHAPES[i];
assert_eq!(
via_variant, via_array,
"UnquoteForm::SHAPES[{i}] `{via_array:?}` diverged from UnquoteForm::ALL[{i}].sexp_shape() `{via_variant:?}`",
);
}
}
#[test]
fn unquote_form_shapes_align_with_all_by_index_through_as_unquote_form() {
for (i, uf) in UnquoteForm::ALL.iter().enumerate() {
let shape = UnquoteForm::SHAPES[i];
assert_eq!(
shape.as_unquote_form(),
Some(*uf),
"UnquoteForm::SHAPES[{i}] `{shape:?}` did NOT round-trip through SexpShape::as_unquote_form — the (embed, project) family-wide law broke",
);
}
}
#[test]
fn unquote_form_shapes_pairwise_distinct() {
assert_ne!(
UnquoteForm::SHAPES[0],
UnquoteForm::SHAPES[1],
"UnquoteForm::SHAPES[0] `{a:?}` collides with UnquoteForm::SHAPES[1] `{b:?}` — the two-arm carving must be injective",
a = UnquoteForm::SHAPES[0],
b = UnquoteForm::SHAPES[1],
);
}
#[test]
fn unquote_form_shapes_align_with_superset_by_projection() {
for (i, uf) in UnquoteForm::ALL.iter().enumerate() {
let via_subset = UnquoteForm::SHAPES[i];
let via_superset = uf.to_quote_form().sexp_shape();
assert_eq!(
via_subset, via_superset,
"UnquoteForm::SHAPES[{i}] `{via_subset:?}` diverged from UnquoteForm::ALL[{i}].to_quote_form().sexp_shape() `{via_superset:?}` — the composition law across the subset carving broke",
);
}
}
#[test]
fn as_unquote_form_projects_each_unquote_shape_to_canonical_unquote_form_and_rejects_non_unquote_shapes(
) {
for shape in SexpShape::ALL {
let projected = shape.as_unquote_form();
let expected = match shape {
SexpShape::Unquote => Some(UnquoteForm::Unquote),
SexpShape::UnquoteSplice => Some(UnquoteForm::Splice),
SexpShape::Nil
| SexpShape::List
| SexpShape::Quote
| SexpShape::Quasiquote
| SexpShape::Symbol
| SexpShape::Keyword
| SexpShape::String
| SexpShape::Int
| SexpShape::Float
| SexpShape::Bool => None,
};
assert_eq!(
projected, expected,
"SexpShape::{shape:?}.as_unquote_form() drifted from canonical mapping",
);
}
}
#[test]
fn as_unquote_form_routes_through_as_quote_form_and_quote_form_as_unquote_form_via_composition()
{
for shape in SexpShape::ALL {
let from_projection = shape.as_unquote_form();
let from_composition = shape
.as_quote_form()
.and_then(crate::ast::QuoteForm::as_unquote_form);
assert_eq!(
from_projection, from_composition,
"SexpShape::{shape:?}.as_unquote_form() drifted from .as_quote_form().and_then(QuoteForm::as_unquote_form) — the retraction is no longer derived from the composition through the quote-family carving",
);
}
}
#[test]
fn as_unquote_form_composes_with_marker_via_unquote_form_marker_round_trip() {
for uf in UnquoteForm::ALL {
let via_round_trip = uf
.sexp_shape()
.as_unquote_form()
.expect("every UnquoteForm round-trips through the embed/project pair")
.marker();
assert_eq!(
via_round_trip,
uf.marker(),
"UnquoteForm::{uf:?}.marker() drifted from sexp_shape().as_unquote_form().marker() — embed/project must preserve marker coherence",
);
}
}
#[test]
fn macro_def_head_all_is_unique_and_complete() {
assert_eq!(MacroDefHead::ALL.len(), 3);
assert_eq!(
<MacroDefHead as tatara_closed_set::ClosedSet>::sorted_labels(),
vec!["defcheck", "defmacro", "defpoint-template"],
"MacroDefHead::ALL must cover all three macro-definition heads"
);
}
#[test]
fn macro_def_head_keyword_round_trips_through_from_str() {
for head in MacroDefHead::ALL {
let parsed: MacroDefHead = head
.keyword()
.parse()
.expect("every ALL variant's keyword must round-trip through FromStr");
assert_eq!(
parsed,
head,
"FromStr({}) must round-trip to the same variant",
head.keyword()
);
}
}
#[test]
fn macro_def_head_from_keyword_matches_from_str_for_every_input() {
let inputs: &[&str] = &[
"defmacro",
"defpoint-template",
"defcheck",
"Defmacro",
"DEFCHECK",
"DefpointTemplate",
"defmacroo",
"defcheckk",
"defpoint_template",
"defpoint",
"defpoint-templates",
"defmonitor",
"defnotify",
"defpoint",
"defalertpolicy",
"symbol",
"list",
"nil",
",",
",@",
"'",
"`",
"",
" ",
" defmacro",
"defmacro ",
];
for s in inputs {
let from_kw = MacroDefHead::from_keyword(s);
let from_str = s.parse::<MacroDefHead>().ok();
assert_eq!(
from_kw,
from_str,
"`from_keyword` and `FromStr` must agree on {s:?}: from_keyword={from_kw:?}, FromStr={from_str:?}",
);
}
}
#[test]
fn unknown_macro_def_head_carries_offending_input_verbatim() {
let err: UnknownMacroDefHead = "Defmacro"
.parse::<MacroDefHead>()
.expect_err("capitalized `Defmacro` is not a canonical macro-definition head");
assert_eq!(err.0, "Defmacro");
assert_eq!(format!("{err}"), "unknown macro definition head: Defmacro");
let err: UnknownMacroDefHead = "defmacroo"
.parse::<MacroDefHead>()
.expect_err("`defmacroo` is not a canonical macro-definition head");
assert_eq!(err.0, "defmacroo");
assert_eq!(format!("{err}"), "unknown macro definition head: defmacroo");
let err: UnknownMacroDefHead = ""
.parse::<MacroDefHead>()
.expect_err("empty input must NOT decode to a MacroDefHead");
assert_eq!(err.0, "");
assert_eq!(format!("{err}"), "unknown macro definition head: ");
let err: UnknownMacroDefHead = " defmacro"
.parse::<MacroDefHead>()
.expect_err("leading-space `defmacro` must reject — typed identity is byte-exact");
assert_eq!(err.0, " defmacro");
assert_eq!(
format!("{err}"),
"unknown macro definition head: defmacro",
"leading whitespace is preserved verbatim in the rendered diagnostic",
);
}
#[test]
fn macro_def_head_from_str_rejects_cross_axis_vocabularies() {
assert_eq!(
"defmacro".parse::<MacroDefHead>().unwrap(),
MacroDefHead::Defmacro
);
assert_eq!(
"defpoint-template".parse::<MacroDefHead>().unwrap(),
MacroDefHead::DefpointTemplate
);
assert_eq!(
"defcheck".parse::<MacroDefHead>().unwrap(),
MacroDefHead::Defcheck
);
for shape in SexpShape::ALL {
assert!(
shape.label().parse::<MacroDefHead>().is_err(),
"SexpShape::{shape:?} label `{}` must NOT decode as a MacroDefHead",
shape.label(),
);
}
for form in UnquoteForm::ALL {
assert!(
form.marker().parse::<MacroDefHead>().is_err(),
"UnquoteForm marker `{}` must NOT decode as a MacroDefHead",
form.marker(),
);
}
assert!("defpoint".parse::<MacroDefHead>().is_err());
assert!("defmonitor".parse::<MacroDefHead>().is_err());
assert!("defnotify".parse::<MacroDefHead>().is_err());
assert!("defalertpolicy".parse::<MacroDefHead>().is_err());
assert!("def-macro".parse::<MacroDefHead>().is_err());
assert!("defun".parse::<MacroDefHead>().is_err());
assert!("define-syntax".parse::<MacroDefHead>().is_err());
}
#[test]
fn macro_def_head_is_well_formed_closed_set() {
tatara_closed_set::assert_closed_set_well_formed::<MacroDefHead>();
}
#[test]
fn unquote_form_is_well_formed_closed_set() {
tatara_closed_set::assert_closed_set_well_formed::<UnquoteForm>();
}
#[test]
fn kwarg_path_kind_is_well_formed_closed_set() {
tatara_closed_set::assert_closed_set_well_formed::<KwargPathKind>();
}
#[test]
fn expected_kwarg_shape_is_well_formed_closed_set() {
tatara_closed_set::assert_closed_set_well_formed::<ExpectedKwargShape>();
}
#[test]
fn sexp_shape_is_well_formed_closed_set() {
tatara_closed_set::assert_closed_set_well_formed::<SexpShape>();
}
#[test]
fn compiler_spec_io_stage_is_well_formed_closed_set() {
tatara_closed_set::assert_closed_set_well_formed::<CompilerSpecIoStage>();
}
#[test]
fn structural_kind_all_is_unique_and_complete() {
assert_eq!(
StructuralKind::ALL.len(),
2,
"StructuralKind::ALL must have exactly 2 entries (Nil + List)",
);
let mut seen = Vec::new();
for sk in StructuralKind::ALL {
assert!(
!seen.contains(&sk),
"StructuralKind::{sk:?} repeated in ALL — variants must be pairwise distinct",
);
seen.push(sk);
}
}
#[test]
fn structural_kind_sexp_shape_projects_canonical_variant_for_every_marker() {
assert_eq!(
StructuralKind::Nil.sexp_shape(),
SexpShape::Nil,
"StructuralKind::Nil.sexp_shape() drifted from SexpShape::Nil",
);
assert_eq!(
StructuralKind::List.sexp_shape(),
SexpShape::List,
"StructuralKind::List.sexp_shape() drifted from SexpShape::List",
);
}
#[test]
fn structural_kind_sexp_shape_round_trips_through_as_structural_kind() {
for sk in StructuralKind::ALL {
let shape = sk.sexp_shape();
let recovered = shape.as_structural_kind();
assert_eq!(
recovered,
Some(sk),
"StructuralKind::{sk:?} did NOT round-trip — sexp_shape().as_structural_kind() must recover the typed marker",
);
}
}
#[test]
fn as_structural_kind_projects_each_structural_shape_to_canonical_structural_kind_and_rejects_non_structural_shapes(
) {
for shape in SexpShape::ALL {
let projected = shape.as_structural_kind();
let expected = match shape {
SexpShape::Nil => Some(StructuralKind::Nil),
SexpShape::List => Some(StructuralKind::List),
SexpShape::Symbol
| SexpShape::Keyword
| SexpShape::String
| SexpShape::Int
| SexpShape::Float
| SexpShape::Bool
| SexpShape::Quote
| SexpShape::Quasiquote
| SexpShape::Unquote
| SexpShape::UnquoteSplice => None,
};
assert_eq!(
projected, expected,
"SexpShape::{shape:?}.as_structural_kind() drifted from canonical mapping",
);
}
}
#[test]
fn structural_kind_hash_discriminator_pins_legacy_cache_key_bytes() {
assert_eq!(StructuralKind::Nil.hash_discriminator(), 0);
assert_eq!(StructuralKind::List.hash_discriminator(), 2);
}
#[test]
fn structural_kind_hash_discriminator_bytes_are_pairwise_disjoint() {
let bytes: Vec<u8> = StructuralKind::ALL
.iter()
.map(|sk| sk.hash_discriminator())
.collect();
let mut sorted = bytes.clone();
sorted.sort_unstable();
let mut deduped = sorted.clone();
deduped.dedup();
assert_eq!(
sorted, deduped,
"StructuralKind hash discriminator bytes must be pairwise disjoint",
);
assert_eq!(sorted, vec![0, 2]);
}
#[test]
fn structural_kind_hash_discriminator_disjoint_from_atom_outer_carve_byte_and_quote_form_hash_discriminator_partition(
) {
let structural: Vec<u8> = StructuralKind::ALL
.iter()
.map(|sk| sk.hash_discriminator())
.collect();
let quote_family: Vec<u8> = crate::ast::QuoteForm::ALL
.iter()
.map(|qf| qf.hash_discriminator())
.collect();
for byte in &structural {
assert_ne!(
*byte,
crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR,
"StructuralKind::hash_discriminator collided with the atomic-carve outer marker byte {marker}",
marker = crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR,
);
assert!(
!quote_family.contains(byte),
"StructuralKind::hash_discriminator byte {byte} collided with QuoteForm::hash_discriminator partition {quote_family:?}",
);
}
let mut union: Vec<u8> = structural
.iter()
.copied()
.chain(std::iter::once(
crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR,
))
.chain(quote_family.iter().copied())
.collect();
union.sort_unstable();
let mut deduped = union.clone();
deduped.dedup();
assert_eq!(
union, deduped,
"the three-carving-plus-atom-marker joint discriminator space {union:?} must partition without overlap",
);
assert_eq!(union, vec![0, 1, 2, 3, 4, 5, 6]);
}
#[test]
fn structural_kind_hash_discriminators_pin_legacy_cache_key_bytes() {
assert_eq!(StructuralKind::NIL_HASH_DISCRIMINATOR, 0);
assert_eq!(StructuralKind::LIST_HASH_DISCRIMINATOR, 2);
}
#[test]
fn structural_kind_hash_discriminator_routes_through_typed_per_role_constants() {
for (kind, expected) in [
(StructuralKind::Nil, StructuralKind::NIL_HASH_DISCRIMINATOR),
(
StructuralKind::List,
StructuralKind::LIST_HASH_DISCRIMINATOR,
),
] {
let actual = kind.hash_discriminator();
assert_eq!(
actual, expected,
"StructuralKind::{kind:?}.hash_discriminator() `{actual}` \
drifted from per-role constant `{expected}` — the arm \
must route through the typed `pub(crate) const` rather \
than an inline `u8` literal",
);
}
}
#[test]
fn structural_kind_hash_discriminators_has_expected_cardinality() {
assert_eq!(
StructuralKind::HASH_DISCRIMINATORS.len(),
2,
"StructuralKind::HASH_DISCRIMINATORS cardinality drifted \
from 2 — the closed structural-residual domain admits \
exactly two kinds by construction; a third extension \
surfaces here"
);
}
#[test]
fn structural_kind_hash_discriminators_align_with_all_by_index() {
for (i, kind) in StructuralKind::ALL.iter().enumerate() {
assert_eq!(
StructuralKind::HASH_DISCRIMINATORS[i],
kind.hash_discriminator(),
"StructuralKind::HASH_DISCRIMINATORS[{i}] `{disc}` \
drifted from StructuralKind::ALL[{i}] ({kind:?}).hash_discriminator() \
`{via_variant}` — the canonical declaration order of \
the ALL array and the hash_discriminator projection \
must match element-wise",
disc = StructuralKind::HASH_DISCRIMINATORS[i],
via_variant = kind.hash_discriminator(),
);
}
}
#[test]
fn structural_kind_hash_discriminators_pairwise_distinct() {
for (i, a) in StructuralKind::HASH_DISCRIMINATORS.iter().enumerate() {
for (j, b) in StructuralKind::HASH_DISCRIMINATORS.iter().enumerate() {
if i == j {
continue;
}
assert_ne!(
a, b,
"StructuralKind::HASH_DISCRIMINATORS[{i}] `{a}` \
collides with StructuralKind::HASH_DISCRIMINATORS[{j}] \
`{b}` — the outer-Sexp Hash body's cache-key \
partition would route two structural-residual \
variants through the same slot"
);
}
}
}
#[test]
fn structural_kind_label_matches_sexp_shape_label_via_composition() {
for sk in StructuralKind::ALL {
let from_direct = sk.label();
let from_composition = sk.sexp_shape().label();
assert_eq!(
from_direct, from_composition,
"StructuralKind::{sk:?}.label() drifted from .sexp_shape().label() — the residual carving's label vocabulary is no longer derived from the superset's canonical site",
);
assert!(
std::ptr::eq(from_direct.as_ptr(), from_composition.as_ptr()),
"StructuralKind::{sk:?}.label() and .sexp_shape().label() must point at the SAME `&'static str` — a parallel inline table has a different pointer",
);
}
}
#[test]
fn structural_kind_label_round_trips_through_from_str() {
for sk in StructuralKind::ALL {
let parsed = sk.label().parse::<StructuralKind>();
assert_eq!(
parsed,
Ok(sk),
"StructuralKind::{sk:?}.label() must round-trip through FromStr",
);
}
}
#[test]
fn structural_kind_from_str_rejects_non_structural_sexp_shape_labels() {
let residual_labels: Vec<&'static str> =
StructuralKind::ALL.iter().map(|sk| sk.label()).collect();
for shape in SexpShape::ALL {
let label = shape.label();
let parsed = label.parse::<StructuralKind>();
if residual_labels.contains(&label) {
assert!(
parsed.is_ok(),
"SexpShape::{shape:?}.label() = {label:?} is a residual label — StructuralKind::from_str must accept",
);
} else {
assert!(
parsed.is_err(),
"SexpShape::{shape:?}.label() = {label:?} lies outside the residual carving — StructuralKind::from_str must reject",
);
}
}
}
#[test]
fn structural_kind_is_well_formed_closed_set() {
tatara_closed_set::assert_closed_set_well_formed::<StructuralKind>();
}
#[test]
fn structural_kind_per_role_labels_alias_sexp_shape_per_role_labels_byte_for_byte() {
assert_eq!(StructuralKind::NIL_LABEL, SexpShape::NIL_LABEL);
assert_eq!(StructuralKind::LIST_LABEL, SexpShape::LIST_LABEL);
}
#[test]
fn structural_kind_label_arms_route_through_per_role_labels_for_every_variant() {
assert_eq!(StructuralKind::Nil.label(), StructuralKind::NIL_LABEL);
assert_eq!(StructuralKind::List.label(), StructuralKind::LIST_LABEL);
}
#[test]
fn structural_kind_labels_has_expected_cardinality() {
assert_eq!(StructuralKind::LABELS.len(), 2);
assert_eq!(StructuralKind::LABELS.len(), StructuralKind::ALL.len());
}
#[test]
fn structural_kind_labels_align_with_all_by_index() {
assert_eq!(StructuralKind::LABELS.len(), StructuralKind::ALL.len());
for (i, kind) in StructuralKind::ALL.iter().enumerate() {
assert_eq!(
StructuralKind::LABELS[i],
kind.label(),
"StructuralKind::LABELS[{i}] `{lbl}` drifted from \
StructuralKind::ALL[{i}].label() `{via_variant}` — \
the canonical ALL ordering and the LABELS ordering \
must match element-wise",
lbl = StructuralKind::LABELS[i],
via_variant = kind.label(),
);
}
}
#[test]
fn structural_kind_labels_pairwise_distinct() {
for (i, a) in StructuralKind::LABELS.iter().enumerate() {
for (j, b) in StructuralKind::LABELS.iter().enumerate() {
if i == j {
continue;
}
assert_ne!(
a, b,
"StructuralKind::LABELS[{i}] ({a:?}) collides with \
StructuralKind::LABELS[{j}] ({b:?}) — two distinct \
structural-residual markers cannot share \
diagnostic bytes",
);
}
}
}
#[test]
fn structural_kind_labels_match_sexp_shape_labels_element_wise_via_alias_chain() {
for (i, kind) in StructuralKind::ALL.iter().enumerate() {
assert_eq!(
StructuralKind::LABELS[i],
kind.sexp_shape().label(),
"StructuralKind::LABELS[{i}] drifted from \
StructuralKind::ALL[{i}].sexp_shape().label() — the \
LABELS array must project through the sexp_shape + \
SexpShape::label composition byte-for-byte",
);
}
}
#[test]
fn structural_kind_per_role_shapes_pin_canonical_sexp_shape_variants() {
assert_eq!(StructuralKind::NIL_SHAPE, SexpShape::Nil);
assert_eq!(StructuralKind::LIST_SHAPE, SexpShape::List);
}
#[test]
fn structural_kind_shapes_has_expected_cardinality() {
assert_eq!(StructuralKind::SHAPES.len(), 2);
assert_eq!(StructuralKind::SHAPES.len(), StructuralKind::ALL.len());
}
#[test]
fn structural_kind_shapes_align_with_all_by_index() {
for (i, kind) in StructuralKind::ALL.iter().enumerate() {
assert_eq!(
StructuralKind::SHAPES[i],
kind.sexp_shape(),
"StructuralKind::SHAPES[{i}] `{shape:?}` drifted from \
StructuralKind::ALL[{i}] ({kind:?}).sexp_shape() \
`{via_variant:?}` — the canonical declaration order \
of the ALL array and the sexp_shape projection must \
match element-wise",
shape = StructuralKind::SHAPES[i],
via_variant = kind.sexp_shape(),
);
}
}
#[test]
fn structural_kind_shapes_align_with_all_by_index_through_as_structural_kind() {
for (i, kind) in StructuralKind::ALL.iter().enumerate() {
assert_eq!(
StructuralKind::SHAPES[i].as_structural_kind(),
Some(*kind),
"StructuralKind::SHAPES[{i}] `{shape:?}`.as_structural_kind() = \
{actual:?} drifted from Some(StructuralKind::ALL[{i}]) = \
Some({expected:?}) — the (SHAPES entry, ALL variant) \
round-trip through the peer inverse \
SexpShape::as_structural_kind must hold element-wise",
shape = StructuralKind::SHAPES[i],
actual = StructuralKind::SHAPES[i].as_structural_kind(),
expected = kind,
);
}
}
#[test]
fn structural_kind_sexp_shape_routes_through_typed_per_role_constants() {
assert_eq!(StructuralKind::Nil.sexp_shape(), StructuralKind::NIL_SHAPE);
assert_eq!(
StructuralKind::List.sexp_shape(),
StructuralKind::LIST_SHAPE
);
}
#[test]
fn structural_kind_shapes_pairwise_distinct() {
for (i, a) in StructuralKind::SHAPES.iter().enumerate() {
for (j, b) in StructuralKind::SHAPES.iter().enumerate() {
if i == j {
continue;
}
assert_ne!(
a, b,
"StructuralKind::SHAPES[{i}] `{a:?}` collides with \
StructuralKind::SHAPES[{j}] `{b:?}` — the \
StructuralKind ⊂ SexpShape 2-of-12 carving would \
route two structural-residual variants through the \
same outer-shape identity"
);
}
}
}
#[test]
fn sexp_shape_hash_discriminator_pins_legacy_outer_cache_key_bytes() {
assert_eq!(SexpShape::Nil.hash_discriminator(), 0);
assert_eq!(SexpShape::Symbol.hash_discriminator(), 1);
assert_eq!(SexpShape::Keyword.hash_discriminator(), 1);
assert_eq!(SexpShape::String.hash_discriminator(), 1);
assert_eq!(SexpShape::Int.hash_discriminator(), 1);
assert_eq!(SexpShape::Float.hash_discriminator(), 1);
assert_eq!(SexpShape::Bool.hash_discriminator(), 1);
assert_eq!(SexpShape::List.hash_discriminator(), 2);
assert_eq!(SexpShape::Quote.hash_discriminator(), 3);
assert_eq!(SexpShape::Quasiquote.hash_discriminator(), 4);
assert_eq!(SexpShape::Unquote.hash_discriminator(), 5);
assert_eq!(SexpShape::UnquoteSplice.hash_discriminator(), 6);
}
#[test]
fn sexp_shape_hash_discriminator_covers_outer_partition_zero_through_six() {
let outer_bytes: std::collections::BTreeSet<u8> = SexpShape::ALL
.iter()
.map(|shape| shape.hash_discriminator())
.collect();
let expected: std::collections::BTreeSet<u8> = (0u8..=6u8).collect();
assert_eq!(
outer_bytes, expected,
"SexpShape::hash_discriminator must cover exactly the outer discriminator space {{0..=6}}",
);
}
#[test]
fn sexp_shape_hash_discriminator_atomic_arms_collapse_to_outer_atom_marker() {
for shape in SexpShape::ALL {
if shape.as_atom_kind().is_some() {
assert_eq!(
shape.hash_discriminator(),
crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR,
"SexpShape::{shape:?} is an atomic-payload shape but did not collapse to outer byte {marker}",
marker = crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR,
);
}
}
}
#[test]
fn sexp_shape_hash_discriminator_atomic_arms_route_through_atom_kind_outer_hash_discriminator()
{
for shape in SexpShape::ALL {
if shape.as_atom_kind().is_some() {
assert_eq!(
shape.hash_discriminator(),
crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR,
"SexpShape::{shape:?} atomic arm must route \
through the typed `AtomKind::OUTER_HASH_DISCRIMINATOR` \
rather than an inline `1u8` literal — the arm's \
projection `{proj}` diverged from the typed \
primitive `{expected}`",
proj = shape.hash_discriminator(),
expected = crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR,
);
}
}
}
#[test]
fn atom_kind_outer_hash_discriminator_closes_outer_sexp_partition_with_structural_and_quote_form(
) {
let atomic: std::collections::BTreeSet<u8> =
std::iter::once(crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR).collect();
let structural: std::collections::BTreeSet<u8> = StructuralKind::HASH_DISCRIMINATORS
.iter()
.copied()
.collect();
let quote_family: std::collections::BTreeSet<u8> =
crate::ast::QuoteForm::HASH_DISCRIMINATORS
.iter()
.copied()
.collect();
assert!(
atomic.is_disjoint(&structural),
"atomic-carve outer marker `{atomic:?}` and structural-carve \
partition `{structural:?}` MUST be disjoint on the outer-Sexp \
cache-key space `{{0..=6}}`",
);
assert!(
atomic.is_disjoint("e_family),
"atomic-carve outer marker `{atomic:?}` and quote-family carve \
partition `{quote_family:?}` MUST be disjoint on the outer-Sexp \
cache-key space `{{0..=6}}`",
);
assert!(
structural.is_disjoint("e_family),
"structural-carve partition `{structural:?}` and quote-family \
carve partition `{quote_family:?}` MUST be disjoint on the \
outer-Sexp cache-key space `{{0..=6}}`",
);
let union: std::collections::BTreeSet<u8> = atomic
.iter()
.chain(structural.iter())
.chain(quote_family.iter())
.copied()
.collect();
let expected: std::collections::BTreeSet<u8> = (0u8..=6u8).collect();
assert_eq!(
union, expected,
"the four-way typed closure `atomic ∪ structural ∪ quote_family` \
= `{union:?}` MUST equal exactly the outer-Sexp cache-key \
partition `{{0..=6}}` — a regression that drifts ANY of the four \
primitives into an outer byte outside `{{0..=6}}` OR leaves \
ANY outer byte unclaimed fails-loudly here",
);
}
#[test]
fn sexp_shape_hash_discriminator_structural_arms_route_through_structural_kind() {
for shape in SexpShape::ALL {
if let Some(sk) = shape.as_structural_kind() {
assert_eq!(
shape.hash_discriminator(),
sk.hash_discriminator(),
"SexpShape::{shape:?} structural arm must delegate to StructuralKind::{sk:?}.hash_discriminator()",
);
}
}
}
#[test]
fn sexp_shape_hash_discriminator_quote_family_arms_route_through_quote_form() {
for shape in SexpShape::ALL {
if let Some(qf) = shape.as_quote_form() {
assert_eq!(
shape.hash_discriminator(),
qf.hash_discriminator(),
"SexpShape::{shape:?} quote-family arm must delegate to QuoteForm::{qf:?}.hash_discriminator()",
);
}
}
}
#[test]
fn sexp_shape_hash_discriminator_partitions_by_three_way_carving_disjointly() {
let atomic_image: std::collections::BTreeSet<u8> = SexpShape::ALL
.iter()
.filter(|s| s.as_atom_kind().is_some())
.map(|s| s.hash_discriminator())
.collect();
let quote_image: std::collections::BTreeSet<u8> = SexpShape::ALL
.iter()
.filter(|s| s.as_quote_form().is_some())
.map(|s| s.hash_discriminator())
.collect();
let structural_image: std::collections::BTreeSet<u8> = SexpShape::ALL
.iter()
.filter(|s| s.as_structural_kind().is_some())
.map(|s| s.hash_discriminator())
.collect();
let expected_atomic: std::collections::BTreeSet<u8> =
std::iter::once(crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR).collect();
let expected_quote: std::collections::BTreeSet<u8> = (3u8..=6u8).collect();
let expected_structural: std::collections::BTreeSet<u8> = [0u8, 2u8].into_iter().collect();
assert_eq!(
atomic_image, expected_atomic,
"the atomic-shape carving's `hash_discriminator` image must be {{1}} (the outer Atom marker byte)",
);
assert_eq!(
quote_image, expected_quote,
"the quote-family carving's `hash_discriminator` image must be {{3, 4, 5, 6}}",
);
assert_eq!(
structural_image, expected_structural,
"the structural-residual carving's `hash_discriminator` image must be {{0, 2}}",
);
assert!(
atomic_image.is_disjoint("e_image),
"atomic and quote-family carvings' `hash_discriminator` images must be disjoint",
);
assert!(
atomic_image.is_disjoint(&structural_image),
"atomic and structural-residual carvings' `hash_discriminator` images must be disjoint",
);
assert!(
quote_image.is_disjoint(&structural_image),
"quote-family and structural-residual carvings' `hash_discriminator` images must be disjoint",
);
let mut union: std::collections::BTreeSet<u8> = atomic_image;
union.extend("e_image);
union.extend(&structural_image);
let full_outer: std::collections::BTreeSet<u8> = (0u8..=6u8).collect();
assert_eq!(
union, full_outer,
"the three-carving `hash_discriminator` image union must exactly cover {{0..=6}}",
);
}
#[test]
fn sexp_shape_hash_discriminators_pin_legacy_outer_cache_key_bytes() {
assert_eq!(SexpShape::NIL_HASH_DISCRIMINATOR, 0);
assert_eq!(SexpShape::SYMBOL_HASH_DISCRIMINATOR, 1);
assert_eq!(SexpShape::KEYWORD_HASH_DISCRIMINATOR, 1);
assert_eq!(SexpShape::STRING_HASH_DISCRIMINATOR, 1);
assert_eq!(SexpShape::INT_HASH_DISCRIMINATOR, 1);
assert_eq!(SexpShape::FLOAT_HASH_DISCRIMINATOR, 1);
assert_eq!(SexpShape::BOOL_HASH_DISCRIMINATOR, 1);
assert_eq!(SexpShape::LIST_HASH_DISCRIMINATOR, 2);
assert_eq!(SexpShape::QUOTE_HASH_DISCRIMINATOR, 3);
assert_eq!(SexpShape::QUASIQUOTE_HASH_DISCRIMINATOR, 4);
assert_eq!(SexpShape::UNQUOTE_HASH_DISCRIMINATOR, 5);
assert_eq!(SexpShape::UNQUOTE_SPLICE_HASH_DISCRIMINATOR, 6);
}
#[test]
fn sexp_shape_hash_discriminator_routes_through_typed_per_role_constants() {
let pairs = [
(SexpShape::Nil, SexpShape::NIL_HASH_DISCRIMINATOR),
(SexpShape::Symbol, SexpShape::SYMBOL_HASH_DISCRIMINATOR),
(SexpShape::Keyword, SexpShape::KEYWORD_HASH_DISCRIMINATOR),
(SexpShape::String, SexpShape::STRING_HASH_DISCRIMINATOR),
(SexpShape::Int, SexpShape::INT_HASH_DISCRIMINATOR),
(SexpShape::Float, SexpShape::FLOAT_HASH_DISCRIMINATOR),
(SexpShape::Bool, SexpShape::BOOL_HASH_DISCRIMINATOR),
(SexpShape::List, SexpShape::LIST_HASH_DISCRIMINATOR),
(SexpShape::Quote, SexpShape::QUOTE_HASH_DISCRIMINATOR),
(
SexpShape::Quasiquote,
SexpShape::QUASIQUOTE_HASH_DISCRIMINATOR,
),
(SexpShape::Unquote, SexpShape::UNQUOTE_HASH_DISCRIMINATOR),
(
SexpShape::UnquoteSplice,
SexpShape::UNQUOTE_SPLICE_HASH_DISCRIMINATOR,
),
];
for (shape, expected) in pairs {
assert_eq!(
shape.hash_discriminator(),
expected,
"SexpShape::hash_discriminator arm for {shape:?} must route through the typed \
`pub(crate) const` per-role byte",
);
}
}
#[test]
fn sexp_shape_hash_discriminators_has_expected_cardinality() {
assert_eq!(SexpShape::HASH_DISCRIMINATORS.len(), 12);
assert_eq!(
SexpShape::HASH_DISCRIMINATORS.len(),
SexpShape::ALL.len(),
"SexpShape::HASH_DISCRIMINATORS cardinality drifted from SexpShape::ALL",
);
}
#[test]
fn sexp_shape_hash_discriminators_align_with_all_by_index() {
for (i, shape) in SexpShape::ALL.iter().copied().enumerate() {
let disc = shape.hash_discriminator();
assert_eq!(
SexpShape::HASH_DISCRIMINATORS[i],
disc,
"SexpShape::HASH_DISCRIMINATORS[{i}] `{disc}` != \
SexpShape::ALL[{i}].hash_discriminator() `{disc}` — array-vs-projection drift",
);
}
}
#[test]
fn sexp_shape_hash_discriminators_cover_outer_partition_zero_through_six() {
let bytes: std::collections::BTreeSet<u8> =
SexpShape::HASH_DISCRIMINATORS.iter().copied().collect();
let expected: std::collections::BTreeSet<u8> = (0u8..=6u8).collect();
assert_eq!(
bytes, expected,
"SexpShape::HASH_DISCRIMINATORS must cover exactly the outer discriminator space {{0..=6}}",
);
}
#[test]
fn sexp_shape_hash_discriminators_align_with_typed_per_role_constants_by_index() {
let per_role = [
SexpShape::NIL_HASH_DISCRIMINATOR,
SexpShape::SYMBOL_HASH_DISCRIMINATOR,
SexpShape::KEYWORD_HASH_DISCRIMINATOR,
SexpShape::STRING_HASH_DISCRIMINATOR,
SexpShape::INT_HASH_DISCRIMINATOR,
SexpShape::FLOAT_HASH_DISCRIMINATOR,
SexpShape::BOOL_HASH_DISCRIMINATOR,
SexpShape::LIST_HASH_DISCRIMINATOR,
SexpShape::QUOTE_HASH_DISCRIMINATOR,
SexpShape::QUASIQUOTE_HASH_DISCRIMINATOR,
SexpShape::UNQUOTE_HASH_DISCRIMINATOR,
SexpShape::UNQUOTE_SPLICE_HASH_DISCRIMINATOR,
];
assert_eq!(
per_role.len(),
SexpShape::HASH_DISCRIMINATORS.len(),
"the twelve per-role SexpShape::*_HASH_DISCRIMINATOR constants \
must cover the same cardinality as SexpShape::HASH_DISCRIMINATORS",
);
for (i, expected) in per_role.iter().copied().enumerate() {
assert_eq!(
SexpShape::HASH_DISCRIMINATORS[i],
expected,
"SexpShape::HASH_DISCRIMINATORS[{i}] `{actual}` drifted from \
the per-role SexpShape::*_HASH_DISCRIMINATOR constant `{expected}` \
for {shape:?} — the family-wide array MUST alias through the \
per-role source of truth rather than inline a byte literal",
actual = SexpShape::HASH_DISCRIMINATORS[i],
shape = SexpShape::ALL[i],
);
}
}
#[test]
fn sexp_shape_hash_discriminators_align_with_sub_carvings_by_projection() {
for (i, shape) in SexpShape::ALL.iter().copied().enumerate() {
let expected = if shape.as_atom_kind().is_some() {
crate::ast::AtomKind::OUTER_HASH_DISCRIMINATOR
} else if let Some(sk) = shape.as_structural_kind() {
sk.hash_discriminator()
} else if let Some(qf) = shape.as_quote_form() {
qf.hash_discriminator()
} else {
unreachable!(
"SexpShape::{shape:?} lies outside all three sub-carvings — \
the three-carving partition (Atom + Structural + Quote) is not exhaustive",
);
};
assert_eq!(
SexpShape::HASH_DISCRIMINATORS[i], expected,
"SexpShape::HASH_DISCRIMINATORS[{i}] must equal the sub-carving projection for {shape:?}",
);
}
}
#[test]
fn sexp_shape_iac_forge_tag_pins_canonical_cl_tags_for_every_quote_family_arm() {
assert_eq!(SexpShape::Quote.iac_forge_tag(), Some("quote"));
assert_eq!(SexpShape::Quasiquote.iac_forge_tag(), Some("quasiquote"));
assert_eq!(SexpShape::Unquote.iac_forge_tag(), Some("unquote"));
assert_eq!(
SexpShape::UnquoteSplice.iac_forge_tag(),
Some("unquote-splicing"),
);
}
#[test]
fn sexp_shape_iac_forge_tag_returns_none_on_every_non_quote_family_shape() {
assert_eq!(SexpShape::Nil.iac_forge_tag(), None);
assert_eq!(SexpShape::Symbol.iac_forge_tag(), None);
assert_eq!(SexpShape::Keyword.iac_forge_tag(), None);
assert_eq!(SexpShape::String.iac_forge_tag(), None);
assert_eq!(SexpShape::Int.iac_forge_tag(), None);
assert_eq!(SexpShape::Float.iac_forge_tag(), None);
assert_eq!(SexpShape::Bool.iac_forge_tag(), None);
assert_eq!(SexpShape::List.iac_forge_tag(), None);
}
#[test]
fn sexp_shape_iac_forge_tag_composes_through_as_quote_form_for_every_variant() {
for shape in SexpShape::ALL {
let via_method = shape.iac_forge_tag();
let via_composition = shape
.as_quote_form()
.map(crate::ast::QuoteForm::iac_forge_tag);
assert_eq!(
via_method, via_composition,
"SexpShape::{shape:?}.iac_forge_tag() drifted from \
.as_quote_form().map(QuoteForm::iac_forge_tag) — the shape-level tag projection is no longer derived from the sub-carving's canonical site",
);
}
}
#[test]
fn sexp_shape_iac_forge_tag_partitions_quote_family_and_kernel_disjointly() {
let tag_image: std::collections::BTreeSet<&'static str> = SexpShape::ALL
.iter()
.filter_map(|shape| shape.iac_forge_tag())
.collect();
let expected_tag_image: std::collections::BTreeSet<&'static str> =
["quote", "quasiquote", "unquote", "unquote-splicing"]
.into_iter()
.collect();
assert_eq!(
tag_image, expected_tag_image,
"SexpShape::iac_forge_tag image must exactly cover the four canonical CL quote-family tags",
);
let some_count = SexpShape::ALL
.iter()
.filter(|shape| shape.iac_forge_tag().is_some())
.count();
assert_eq!(
some_count, 4,
"SexpShape::iac_forge_tag must return `Some(_)` on exactly the four-arm quote-family carving",
);
let none_count = SexpShape::ALL
.iter()
.filter(|shape| shape.iac_forge_tag().is_none())
.count();
assert_eq!(
none_count, 8,
"SexpShape::iac_forge_tag must return `None` on exactly the eight-shape non-quote-family kernel",
);
assert_eq!(
some_count + none_count,
SexpShape::ALL.len(),
"SexpShape::iac_forge_tag's image + kernel must partition SexpShape::ALL exactly",
);
}
#[test]
fn sexp_shape_prefix_pins_canonical_reader_prefixes_for_every_quote_family_arm() {
assert_eq!(SexpShape::Quote.prefix(), Some("'"));
assert_eq!(SexpShape::Quasiquote.prefix(), Some("`"));
assert_eq!(SexpShape::Unquote.prefix(), Some(","));
assert_eq!(SexpShape::UnquoteSplice.prefix(), Some(",@"));
}
#[test]
fn sexp_shape_prefix_returns_none_on_every_non_quote_family_shape() {
assert_eq!(SexpShape::Nil.prefix(), None);
assert_eq!(SexpShape::Symbol.prefix(), None);
assert_eq!(SexpShape::Keyword.prefix(), None);
assert_eq!(SexpShape::String.prefix(), None);
assert_eq!(SexpShape::Int.prefix(), None);
assert_eq!(SexpShape::Float.prefix(), None);
assert_eq!(SexpShape::Bool.prefix(), None);
assert_eq!(SexpShape::List.prefix(), None);
}
#[test]
fn sexp_shape_prefix_composes_through_as_quote_form_for_every_variant() {
for shape in SexpShape::ALL {
let via_method = shape.prefix();
let via_composition = shape.as_quote_form().map(crate::ast::QuoteForm::prefix);
assert_eq!(
via_method, via_composition,
"SexpShape::{shape:?}.prefix() drifted from \
.as_quote_form().map(QuoteForm::prefix) — the shape-level prefix projection is no longer derived from the sub-carving's canonical site",
);
}
}
#[test]
fn sexp_shape_prefix_partitions_quote_family_and_kernel_disjointly() {
let prefix_image: std::collections::BTreeSet<&'static str> = SexpShape::ALL
.iter()
.filter_map(|shape| shape.prefix())
.collect();
let expected_prefix_image: std::collections::BTreeSet<&'static str> =
["'", "`", ",", ",@"].into_iter().collect();
assert_eq!(
prefix_image, expected_prefix_image,
"SexpShape::prefix image must exactly cover the four canonical reader-punctuation quote-family prefixes",
);
let some_count = SexpShape::ALL
.iter()
.filter(|shape| shape.prefix().is_some())
.count();
assert_eq!(
some_count, 4,
"SexpShape::prefix must return `Some(_)` on exactly the four-arm quote-family carving",
);
let none_count = SexpShape::ALL
.iter()
.filter(|shape| shape.prefix().is_none())
.count();
assert_eq!(
none_count, 8,
"SexpShape::prefix must return `None` on exactly the eight-shape non-quote-family kernel",
);
assert_eq!(
some_count + none_count,
SexpShape::ALL.len(),
"SexpShape::prefix's image + kernel must partition SexpShape::ALL exactly",
);
}
#[test]
fn assert_str_array_pairwise_distinct_accepts_every_family_wide_error_module_array() {
use crate::ast::assert_str_array_pairwise_distinct;
assert_str_array_pairwise_distinct(&CompilerSpecIoStage::LABELS);
assert_str_array_pairwise_distinct(&TemplateInvariantKind::STATIC_MESSAGES);
assert_str_array_pairwise_distinct(&TemplateInvariantKind::DYNAMIC_DESCRIPTORS);
assert_str_array_pairwise_distinct(&MacroDefHead::KEYWORDS);
assert_str_array_pairwise_distinct(&OptionalParamMalformedReason::STATIC_LABELS);
assert_str_array_pairwise_distinct(&OptionalParamMalformedReason::DYNAMIC_DESCRIPTORS);
assert_str_array_pairwise_distinct(&UnquoteForm::MARKERS);
assert_str_array_pairwise_distinct(&UnquoteForm::IAC_FORGE_TAGS);
assert_str_array_pairwise_distinct(&UnquoteForm::LABELS);
assert_str_array_pairwise_distinct(&KwargPathKind::LABELS);
assert_str_array_pairwise_distinct(&ExpectedKwargShape::LABELS);
assert_str_array_pairwise_distinct(&SexpShape::LABELS);
assert_str_array_pairwise_distinct(&StructuralKind::LABELS);
}
#[test]
fn assert_str_array_all_ascii_accepts_every_family_wide_error_module_array() {
use crate::ast::assert_str_array_all_ascii;
assert_str_array_all_ascii(&CompilerSpecIoStage::LABELS);
assert_str_array_all_ascii(&CompilerSpecIoStage::OPERATIONS);
assert_str_array_all_ascii(&TemplateInvariantKind::STATIC_MESSAGES);
assert_str_array_all_ascii(&TemplateInvariantKind::DYNAMIC_DESCRIPTORS);
assert_str_array_all_ascii(&MacroDefHead::KEYWORDS);
assert_str_array_all_ascii(&OptionalParamMalformedReason::STATIC_LABELS);
assert_str_array_all_ascii(&OptionalParamMalformedReason::DYNAMIC_DESCRIPTORS);
assert_str_array_all_ascii(&UnquoteForm::MARKERS);
assert_str_array_all_ascii(&UnquoteForm::IAC_FORGE_TAGS);
assert_str_array_all_ascii(&UnquoteForm::LABELS);
assert_str_array_all_ascii(&KwargPathKind::LABELS);
assert_str_array_all_ascii(&ExpectedKwargShape::LABELS);
assert_str_array_all_ascii(&SexpShape::LABELS);
assert_str_array_all_ascii(&StructuralKind::LABELS);
}
#[test]
#[should_panic(expected = "assert_str_array_pairwise_distinct")]
fn assert_str_array_pairwise_distinct_panics_on_compiler_spec_io_stage_operations() {
use crate::ast::assert_str_array_pairwise_distinct;
assert_str_array_pairwise_distinct(&CompilerSpecIoStage::OPERATIONS);
}
#[test]
fn assert_str_arrays_disjoint_accepts_each_structural_kind_partition_pair() {
use crate::ast::assert_str_arrays_disjoint;
assert_str_arrays_disjoint::<6, 2>(&crate::ast::AtomKind::LABELS, &StructuralKind::LABELS);
assert_str_arrays_disjoint::<4, 2>(&crate::ast::QuoteForm::LABELS, &StructuralKind::LABELS);
}
#[test]
fn assert_str_arrays_disjoint_accepts_each_static_dynamic_carving_pair() {
use crate::ast::assert_str_arrays_disjoint;
assert_str_arrays_disjoint::<2, 2>(
&TemplateInvariantKind::STATIC_MESSAGES,
&TemplateInvariantKind::DYNAMIC_DESCRIPTORS,
);
assert_str_arrays_disjoint::<3, 1>(
&OptionalParamMalformedReason::STATIC_LABELS,
&OptionalParamMalformedReason::DYNAMIC_DESCRIPTORS,
);
}
#[test]
fn assert_str_array_is_concatenation_of_two_scalar_replicas_accepts_compiler_spec_io_stage_operations(
) {
use crate::ast::assert_str_array_is_concatenation_of_two_scalar_replicas;
assert_str_array_is_concatenation_of_two_scalar_replicas::<4, 2>(
&CompilerSpecIoStage::OPERATIONS,
CompilerSpecIoStage::REALIZE_TO_DISK_OPERATION,
CompilerSpecIoStage::LOAD_FROM_DISK_OPERATION,
);
}
#[test]
fn assert_str_array_slice_equals_str_array_accepts_every_family_wide_error_module_array_full_array_literal_listing(
) {
use crate::ast::assert_str_array_slice_equals_str_array;
assert_str_array_slice_equals_str_array::<7, 7, 0>(
&ExpectedKwargShape::LABELS,
&[
"keyword",
"string",
"int",
"number",
"bool",
"list",
"list of strings",
],
);
assert_str_array_slice_equals_str_array::<3, 3, 0>(
&KwargPathKind::LABELS,
&["named", "item", "slot"],
);
assert_str_array_slice_equals_str_array::<3, 3, 0>(
&MacroDefHead::KEYWORDS,
&["defmacro", "defpoint-template", "defcheck"],
);
assert_str_array_slice_equals_str_array::<4, 4, 0>(
&CompilerSpecIoStage::LABELS,
&["serialize", "write", "read", "deserialize"],
);
assert_str_array_slice_equals_str_array::<2, 2, 0>(
&StructuralKind::LABELS,
&["nil", "list"],
);
}
#[test]
fn sexp_shape_labels_is_disjoint_union_of_three_sub_vocabularies() {
for outer in SexpShape::LABELS.iter() {
let hits_atom = crate::ast::AtomKind::LABELS
.iter()
.filter(|s| s == &outer)
.count();
let hits_quote = crate::ast::QuoteForm::LABELS
.iter()
.filter(|s| s == &outer)
.count();
let hits_struct = StructuralKind::LABELS
.iter()
.filter(|s| s == &outer)
.count();
let total = hits_atom + hits_quote + hits_struct;
assert_eq!(
total, 1,
"outer SexpShape label {outer:?} must appear in EXACTLY ONE \
sub-vocabulary (atom={hits_atom}, quote={hits_quote}, \
struct={hits_struct}); the twelve-arm partition triple \
(AtomKind::LABELS, QuoteForm::LABELS, StructuralKind::LABELS) \
must be a DISJOINT UNION of SexpShape::LABELS"
);
}
for inner in crate::ast::AtomKind::LABELS
.iter()
.chain(crate::ast::QuoteForm::LABELS.iter())
.chain(StructuralKind::LABELS.iter())
{
assert!(
SexpShape::LABELS.iter().any(|s| s == inner),
"sub-vocabulary label {inner:?} must be embedded in the \
outer SexpShape::LABELS vocabulary — the (⊇) direction \
of the disjoint-union theorem"
);
}
}
#[test]
fn assert_str_array_slice_equals_str_array_accepts_unquote_form_sub_carve_of_quote_form() {
use crate::ast::assert_str_array_slice_equals_str_array;
assert_str_array_slice_equals_str_array::<4, 2, 2>(
&crate::ast::QuoteForm::LABELS,
&UnquoteForm::LABELS,
);
assert_str_array_slice_equals_str_array::<4, 2, 2>(
&crate::ast::QuoteForm::PREFIXES,
&UnquoteForm::MARKERS,
);
assert_str_array_slice_equals_str_array::<4, 2, 2>(
&crate::ast::QuoteForm::IAC_FORGE_TAGS,
&UnquoteForm::IAC_FORGE_TAGS,
);
}
}