use std::cmp::Ordering;
use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::sync::{Arc, Mutex};
use crate::ast::Sexp;
use crate::error::{LispError, MacroDefHead, Result, TemplateInvariantKind, UnquoteForm};
pub const DEFAULT_MAX_EXPANSION_DEPTH: usize = 256;
pub const DEFAULT_MAX_CACHE_ENTRIES: usize = 8192;
pub const DEFAULT_MAX_EXPANSION_SIZE: usize = 65_536;
pub const DEFAULT_MAX_MACRO_BODY_SIZE: usize = 16_384;
pub const DEFAULT_MAX_REGISTERED_MACROS: usize = 4096;
pub const DEFAULT_MAX_MACRO_ARITY: usize = 128;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ResourceLimits {
pub max_expansion_depth: usize,
pub max_cache_entries: usize,
pub max_expansion_size: usize,
pub max_macro_body_size: usize,
pub max_registered_macros: usize,
pub max_macro_arity: usize,
}
impl Default for ResourceLimits {
fn default() -> Self {
DEFAULT_RESOURCE_LIMITS
}
}
pub const DEFAULT_RESOURCE_LIMITS: ResourceLimits = ResourceLimits {
max_expansion_depth: DEFAULT_MAX_EXPANSION_DEPTH,
max_cache_entries: DEFAULT_MAX_CACHE_ENTRIES,
max_expansion_size: DEFAULT_MAX_EXPANSION_SIZE,
max_macro_body_size: DEFAULT_MAX_MACRO_BODY_SIZE,
max_registered_macros: DEFAULT_MAX_REGISTERED_MACROS,
max_macro_arity: DEFAULT_MAX_MACRO_ARITY,
};
pub const UNBOUNDED_RESOURCE_LIMITS: ResourceLimits = ResourceLimits {
max_expansion_depth: usize::MAX,
max_cache_entries: usize::MAX,
max_expansion_size: usize::MAX,
max_macro_body_size: usize::MAX,
max_registered_macros: usize::MAX,
max_macro_arity: usize::MAX,
};
pub const EMPTY_RESOURCE_LIMITS: ResourceLimits = ResourceLimits {
max_expansion_depth: 0,
max_cache_entries: 0,
max_expansion_size: 0,
max_macro_body_size: 0,
max_registered_macros: 0,
max_macro_arity: 0,
};
const fn min_usize(a: usize, b: usize) -> usize {
if a <= b {
a
} else {
b
}
}
const fn max_usize(a: usize, b: usize) -> usize {
if a >= b {
a
} else {
b
}
}
impl ResourceLimits {
#[must_use]
pub const fn strictest(self, other: Self) -> Self {
Self {
max_expansion_depth: min_usize(self.max_expansion_depth, other.max_expansion_depth),
max_cache_entries: min_usize(self.max_cache_entries, other.max_cache_entries),
max_expansion_size: min_usize(self.max_expansion_size, other.max_expansion_size),
max_macro_body_size: min_usize(self.max_macro_body_size, other.max_macro_body_size),
max_registered_macros: min_usize(
self.max_registered_macros,
other.max_registered_macros,
),
max_macro_arity: min_usize(self.max_macro_arity, other.max_macro_arity),
}
}
#[must_use]
pub const fn most_permissive(self, other: Self) -> Self {
Self {
max_expansion_depth: max_usize(self.max_expansion_depth, other.max_expansion_depth),
max_cache_entries: max_usize(self.max_cache_entries, other.max_cache_entries),
max_expansion_size: max_usize(self.max_expansion_size, other.max_expansion_size),
max_macro_body_size: max_usize(self.max_macro_body_size, other.max_macro_body_size),
max_registered_macros: max_usize(
self.max_registered_macros,
other.max_registered_macros,
),
max_macro_arity: max_usize(self.max_macro_arity, other.max_macro_arity),
}
}
#[must_use]
pub const fn leq(self, other: Self) -> bool {
self.max_expansion_depth <= other.max_expansion_depth
&& self.max_cache_entries <= other.max_cache_entries
&& self.max_expansion_size <= other.max_expansion_size
&& self.max_macro_body_size <= other.max_macro_body_size
&& self.max_registered_macros <= other.max_registered_macros
&& self.max_macro_arity <= other.max_macro_arity
}
#[must_use]
pub const fn strictest_of(postures: &[Self]) -> Self {
let mut acc = UNBOUNDED_RESOURCE_LIMITS;
let mut i = 0;
while i < postures.len() {
acc = acc.strictest(postures[i]);
i += 1;
}
acc
}
#[must_use]
pub const fn most_permissive_of(postures: &[Self]) -> Self {
let mut acc = EMPTY_RESOURCE_LIMITS;
let mut i = 0;
while i < postures.len() {
acc = acc.most_permissive(postures[i]);
i += 1;
}
acc
}
#[must_use]
pub const fn clamp(self, lower: Self, upper: Self) -> Self {
self.most_permissive(lower).strictest(upper)
}
#[must_use]
pub const fn within(self, lower: Self, upper: Self) -> bool {
lower.leq(self) && self.leq(upper)
}
#[must_use]
pub const fn is_lower_bound_of(self, postures: &[Self]) -> bool {
let mut i = 0;
while i < postures.len() {
if !self.leq(postures[i]) {
return false;
}
i += 1;
}
true
}
#[must_use]
pub const fn is_upper_bound_of(self, postures: &[Self]) -> bool {
let mut i = 0;
while i < postures.len() {
if !postures[i].leq(self) {
return false;
}
i += 1;
}
true
}
#[must_use]
pub const fn lt(self, other: Self) -> bool {
self.leq(other) && !other.leq(self)
}
#[must_use]
pub const fn gt(self, other: Self) -> bool {
other.lt(self)
}
#[must_use]
pub const fn geq(self, other: Self) -> bool {
other.leq(self)
}
#[must_use]
pub const fn is_incomparable(self, other: Self) -> bool {
!self.leq(other) && !self.geq(other)
}
#[must_use]
pub const fn is_comparable(self, other: Self) -> bool {
self.leq(other) || self.geq(other)
}
#[must_use]
pub const fn partial_cmp(self, other: Self) -> Option<Ordering> {
let self_leq = self.leq(other);
let other_leq = other.leq(self);
if self_leq && other_leq {
Some(Ordering::Equal)
} else if self_leq {
Some(Ordering::Less)
} else if other_leq {
Some(Ordering::Greater)
} else {
None
}
}
#[must_use]
pub const fn is_chain(postures: &[Self]) -> bool {
let n = postures.len();
let mut i = 0;
while i < n {
let mut j = i + 1;
while j < n {
if !postures[i].is_comparable(postures[j]) {
return false;
}
j += 1;
}
i += 1;
}
true
}
#[must_use]
pub const fn is_antichain(postures: &[Self]) -> bool {
let n = postures.len();
let mut i = 0;
while i < n {
let mut j = i + 1;
while j < n {
if !postures[i].is_incomparable(postures[j]) {
return false;
}
j += 1;
}
i += 1;
}
true
}
#[must_use]
pub const fn is_mixed(postures: &[Self]) -> bool {
!Self::is_chain(postures) && !Self::is_antichain(postures)
}
#[must_use]
pub const fn is_ascending(postures: &[Self]) -> bool {
let n = postures.len();
let mut i = 0;
while i + 1 < n {
if !postures[i].leq(postures[i + 1]) {
return false;
}
i += 1;
}
true
}
#[must_use]
pub const fn is_descending(postures: &[Self]) -> bool {
let n = postures.len();
let mut i = 0;
while i + 1 < n {
if !postures[i].geq(postures[i + 1]) {
return false;
}
i += 1;
}
true
}
}
type CacheKey = (String, u64);
#[derive(Debug, Clone)]
pub struct MacroDef {
pub name: String,
pub params: MacroParams,
pub body: Sexp,
}
impl MacroDef {
#[must_use]
pub fn template_body(&self) -> &Sexp {
match &self.body {
Sexp::Quasiquote(inner) => inner.as_ref(),
other => other,
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct MacroParams {
pub required: Vec<String>,
pub optional: Vec<OptionalParam>,
pub rest: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct OptionalParam {
pub name: String,
pub default: Option<Sexp>,
}
impl OptionalParam {
#[must_use]
pub fn bare(name: impl Into<String>) -> Self {
Self {
name: name.into(),
default: None,
}
}
#[must_use]
pub fn with_default(name: impl Into<String>, default: Sexp) -> Self {
Self {
name: name.into(),
default: Some(default),
}
}
#[must_use]
pub fn resolved_default(&self) -> Sexp {
self.default.clone().unwrap_or(Sexp::Nil)
}
}
pub trait MacroArgCarrier: Clone {
type Site: Copy;
fn lift_default(default: &Sexp, site: Self::Site) -> Self;
fn collect_rest(items: Vec<Self>, site: Self::Site) -> Self;
}
impl MacroArgCarrier for Sexp {
type Site = ();
fn lift_default(default: &Sexp, (): ()) -> Self {
default.clone()
}
fn collect_rest(items: Vec<Self>, (): ()) -> Self {
Sexp::List(items)
}
}
impl MacroParams {
pub const REST_MARKER: &'static str = "&rest";
pub const OPTIONAL_MARKER: &'static str = "&optional";
pub const LAMBDA_LIST_KEYWORD_LEAD: char = '&';
pub const LAMBDA_LIST_KEYWORDS: [&'static str; 2] = [Self::REST_MARKER, Self::OPTIONAL_MARKER];
#[must_use]
pub fn is_lambda_list_keyword(s: &str) -> bool {
Self::LAMBDA_LIST_KEYWORDS.contains(&s)
}
#[must_use]
pub fn names(&self) -> Vec<&str> {
self.required
.iter()
.map(String::as_str)
.chain(self.optional.iter().map(|p| p.name.as_str()))
.chain(self.rest.as_deref())
.collect()
}
#[must_use]
pub fn fixed_arity(&self) -> usize {
self.required.len() + self.optional.len()
}
#[must_use]
pub fn total_arity(&self) -> usize {
self.fixed_arity() + usize::from(self.rest.is_some())
}
fn bind(&self, macro_name: &str, args: &[Sexp]) -> Result<Vec<Sexp>> {
self.bind_carrier(macro_name, args, ())
}
pub fn bind_carrier<V: MacroArgCarrier>(
&self,
macro_name: &str,
args: &[V],
site: V::Site,
) -> Result<Vec<V>> {
let mut out = Vec::with_capacity(self.total_arity());
for (i, name) in self.required.iter().enumerate() {
let arg = args
.get(i)
.cloned()
.ok_or_else(|| missing_macro_arg(macro_name, name))?;
out.push(arg);
}
let opt_start = self.required.len();
for (j, param) in self.optional.iter().enumerate() {
let arg = match args.get(opt_start + j) {
Some(supplied) => supplied.clone(),
None => V::lift_default(¶m.resolved_default(), site),
};
out.push(arg);
}
if let Some(_rest_name) = self.rest.as_ref() {
let rest = args.get(self.fixed_arity()..).unwrap_or(&[]).to_vec();
out.push(V::collect_rest(rest, site));
} else {
let expected = self.fixed_arity();
if args.len() > expected {
return Err(too_many_macro_args(macro_name, expected, args.len()));
}
}
Ok(out)
}
}
#[derive(Clone, Default)]
pub struct Expander {
macros: HashMap<String, MacroDef>,
templates: HashMap<String, CompiledTemplate>,
compile_templates: bool,
cache: Arc<Mutex<HashMap<CacheKey, Sexp>>>,
cache_enabled: bool,
limits: ResourceLimits,
}
impl Expander {
pub fn with_limits(limits: ResourceLimits) -> Self {
Self {
macros: HashMap::new(),
templates: HashMap::new(),
compile_templates: true,
cache: Arc::new(Mutex::new(HashMap::new())),
cache_enabled: true,
limits,
}
}
pub fn new() -> Self {
Self::with_limits(DEFAULT_RESOURCE_LIMITS)
}
pub fn new_substitute_only() -> Self {
let mut e = Self::with_limits(DEFAULT_RESOURCE_LIMITS);
e.compile_templates = false;
e.cache_enabled = false;
e
}
pub fn new_bytecode_no_cache() -> Self {
let mut e = Self::new();
e.cache_enabled = false;
e
}
pub fn set_cache_enabled(&mut self, enabled: bool) {
self.cache_enabled = enabled;
}
pub fn cache_size(&self) -> usize {
self.cache.lock().unwrap().len()
}
pub fn clear_cache(&self) {
self.cache.lock().unwrap().clear();
}
#[must_use]
pub fn max_expansion_depth(&self) -> usize {
self.limits.max_expansion_depth
}
pub fn set_max_expansion_depth(&mut self, depth: usize) {
self.limits.max_expansion_depth = depth;
}
#[must_use]
pub fn max_cache_entries(&self) -> usize {
self.limits.max_cache_entries
}
pub fn set_max_cache_entries(&mut self, cap: usize) {
self.limits.max_cache_entries = cap;
}
#[must_use]
pub fn max_expansion_size(&self) -> usize {
self.limits.max_expansion_size
}
pub fn set_max_expansion_size(&mut self, size: usize) {
self.limits.max_expansion_size = size;
}
#[must_use]
pub fn max_macro_body_size(&self) -> usize {
self.limits.max_macro_body_size
}
pub fn set_max_macro_body_size(&mut self, size: usize) {
self.limits.max_macro_body_size = size;
}
#[must_use]
pub fn max_registered_macros(&self) -> usize {
self.limits.max_registered_macros
}
pub fn set_max_registered_macros(&mut self, cap: usize) {
self.limits.max_registered_macros = cap;
}
#[must_use]
pub fn max_macro_arity(&self) -> usize {
self.limits.max_macro_arity
}
pub fn set_max_macro_arity(&mut self, cap: usize) {
self.limits.max_macro_arity = cap;
}
#[must_use]
pub fn resource_limits(&self) -> ResourceLimits {
self.limits
}
pub fn set_resource_limits(&mut self, limits: ResourceLimits) {
self.limits = limits;
}
pub fn with_macros<I: IntoIterator<Item = MacroDef>>(defs: I) -> Result<Self> {
let mut e = Self::new();
for d in defs {
e.register_macro_def(d)?;
}
Ok(e)
}
pub fn register_macro_def(&mut self, def: MacroDef) -> Result<()> {
let is_overwrite = self.macros.contains_key(&def.name);
if !is_overwrite && self.macros.len() >= self.limits.max_registered_macros {
return Err(LispError::RegisteredMacrosExceeded {
macro_name: def.name.clone(),
count: self.macros.len(),
limit: self.limits.max_registered_macros,
});
}
let arity = def.params.total_arity();
if arity > self.limits.max_macro_arity {
return Err(LispError::MacroArityExceeded {
macro_name: def.name.clone(),
arity,
limit: self.limits.max_macro_arity,
});
}
let body_size = def.body.node_count();
if body_size > self.limits.max_macro_body_size {
return Err(LispError::MacroBodySizeExceeded {
macro_name: def.name.clone(),
size: body_size,
limit: self.limits.max_macro_body_size,
});
}
if self.compile_templates {
self.templates
.insert(def.name.clone(), compile_template(&def)?);
}
self.macros.insert(def.name.clone(), def);
Ok(())
}
pub fn expand_program(&mut self, forms: Vec<Sexp>) -> Result<Vec<Sexp>> {
let mut out = Vec::new();
for form in forms {
if let Some(def) = macro_def_from(&form)? {
self.register_macro_def(def)?;
continue;
}
out.push(self.expand(&form)?);
}
Ok(out)
}
pub fn expand_source_program(&mut self, src: &str) -> Result<Vec<Sexp>> {
let forms = crate::reader::read(src)?;
self.expand_program(forms)
}
pub fn expand_and_collect_calls_to<R, F>(
&mut self,
forms: Vec<Sexp>,
keyword: &str,
mut project: F,
) -> Result<Vec<R>>
where
F: FnMut(&[Sexp]) -> Result<R>,
{
self.expand_and_collect_calls_to_any(
forms,
|h| (h == keyword).then_some(()),
move |(), args| project(args),
)
}
pub fn expand_and_collect_calls_to_any<R, F, D, T>(
&mut self,
forms: Vec<Sexp>,
decode: D,
mut project: F,
) -> Result<Vec<R>>
where
D: FnMut(&str) -> Option<T>,
F: FnMut(T, &[Sexp]) -> Result<R>,
{
let expanded = self.expand_program(forms)?;
crate::ast::iter_calls_to_any(&expanded, decode)
.map(|(decoded, args)| project(decoded, args))
.collect()
}
pub fn expand_source_and_collect_calls_to<R, F>(
&mut self,
src: &str,
keyword: &str,
mut project: F,
) -> Result<Vec<R>>
where
F: FnMut(&[Sexp]) -> Result<R>,
{
self.expand_source_and_collect_calls_to_any(
src,
|h| (h == keyword).then_some(()),
move |(), args| project(args),
)
}
pub fn expand_source_and_collect_calls_to_any<R, F, D, T>(
&mut self,
src: &str,
decode: D,
project: F,
) -> Result<Vec<R>>
where
D: FnMut(&str) -> Option<T>,
F: FnMut(T, &[Sexp]) -> Result<R>,
{
let forms = crate::reader::read(src)?;
self.expand_and_collect_calls_to_any(forms, decode, project)
}
pub fn expand_and_collect_named_calls_to_any<R, F, D, T>(
&mut self,
forms: Vec<Sexp>,
decode: D,
mut project: F,
) -> Result<Vec<R>>
where
D: FnMut(&str) -> Option<(T, &'static str)>,
F: FnMut(T, &str, &[Sexp]) -> Result<R>,
{
let expanded = self.expand_program(forms)?;
crate::ast::iter_named_calls_to_any(&expanded, decode)
.map(|maybe_triple| {
let (decoded, name, spec_args) = maybe_triple?;
project(decoded, name, spec_args)
})
.collect()
}
pub fn expand_source_and_collect_named_calls_to_any<R, F, D, T>(
&mut self,
src: &str,
decode: D,
project: F,
) -> Result<Vec<R>>
where
D: FnMut(&str) -> Option<(T, &'static str)>,
F: FnMut(T, &str, &[Sexp]) -> Result<R>,
{
let forms = crate::reader::read(src)?;
self.expand_and_collect_named_calls_to_any(forms, decode, project)
}
pub fn expand_and_collect_named_calls_to<R, F>(
&mut self,
forms: Vec<Sexp>,
keyword: &'static str,
mut project: F,
) -> Result<Vec<R>>
where
F: FnMut(&str, &[Sexp]) -> Result<R>,
{
self.expand_and_collect_named_calls_to_any(
forms,
|h| (h == keyword).then_some(((), keyword)),
move |(), name, args| project(name, args),
)
}
pub fn expand_source_and_collect_named_calls_to<R, F>(
&mut self,
src: &str,
keyword: &'static str,
mut project: F,
) -> Result<Vec<R>>
where
F: FnMut(&str, &[Sexp]) -> Result<R>,
{
self.expand_source_and_collect_named_calls_to_any(
src,
|h| (h == keyword).then_some(((), keyword)),
move |(), name, args| project(name, args),
)
}
pub fn expand(&self, form: &Sexp) -> Result<Sexp> {
self.expand_with_depth(form, 0)
}
fn expand_with_depth(&self, form: &Sexp, depth: usize) -> Result<Sexp> {
if let Some((def, args)) = form.as_call_to_any(|h| self.macros.get(h)) {
if depth >= self.limits.max_expansion_depth {
return Err(LispError::ExpansionDepthExceeded {
macro_name: def.name.clone(),
limit: self.limits.max_expansion_depth,
});
}
let expanded = self.apply(def, args)?;
let expanded_size = expanded.node_count();
if expanded_size > self.limits.max_expansion_size {
return Err(LispError::ExpansionSizeExceeded {
macro_name: def.name.clone(),
size: expanded_size,
limit: self.limits.max_expansion_size,
});
}
return self.expand_with_depth(&expanded, depth + 1);
}
let Some(list) = form.as_list() else {
return Ok(form.clone());
};
let mut out = Vec::with_capacity(list.len());
for item in list {
out.push(self.expand_with_depth(item, depth)?);
}
Ok(Sexp::List(out))
}
fn apply(&self, def: &MacroDef, args: &[Sexp]) -> Result<Sexp> {
let cache_key = if self.cache_enabled {
args_cache_key(&def.name, args)
} else {
None
};
if let Some(ref key) = cache_key {
if let Some(cached) = self.cache.lock().unwrap().get(key) {
return Ok(cached.clone());
}
}
let result = if let Some(tmpl) = self.templates.get(&def.name) {
apply_compiled(&def.name, &def.params, tmpl, args)?
} else {
let bindings = bind_args(&def.name, &def.params, args)?;
substitute(def.template_body(), &bindings)?
};
if let Some(key) = cache_key {
let mut cache = self.cache.lock().unwrap();
if cache.len() < self.limits.max_cache_entries {
cache.insert(key, result.clone());
}
}
Ok(result)
}
pub fn has(&self, name: &str) -> bool {
self.macros.contains_key(name)
}
pub fn len(&self) -> usize {
self.macros.len()
}
pub fn is_empty(&self) -> bool {
self.macros.is_empty()
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum TemplateOp {
Literal(Sexp),
Subst(usize),
Splice(usize),
BeginList,
EndList,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct CompiledTemplate {
pub ops: Vec<TemplateOp>,
}
pub fn compile_template(def: &MacroDef) -> Result<CompiledTemplate> {
let body = def.template_body();
if let Some((UnquoteForm::Splice, inner)) = body.as_unquote() {
return Err(splice_outside_list(inner));
}
let names = def.params.names();
let mut ops = Vec::new();
compile_node(body, &names, &mut ops)?;
Ok(CompiledTemplate { ops })
}
fn compile_node(node: &Sexp, params: &[&str], ops: &mut Vec<TemplateOp>) -> Result<()> {
if !contains_unquote(node) {
ops.push(TemplateOp::Literal(node.clone()));
return Ok(());
}
if let Some((form, inner)) = node.as_unquote() {
let idx = resolve_unquote_in_params(inner, params, form)?;
ops.push(match form {
UnquoteForm::Unquote => TemplateOp::Subst(idx),
UnquoteForm::Splice => TemplateOp::Splice(idx),
});
return Ok(());
}
match node {
Sexp::List(items) => {
ops.push(TemplateOp::BeginList);
for item in items {
compile_node(item, params, ops)?;
}
ops.push(TemplateOp::EndList);
}
_ => ops.push(TemplateOp::Literal(node.clone())),
}
Ok(())
}
fn contains_unquote(node: &Sexp) -> bool {
if let Some((form, inner)) = node.as_quote_form() {
return form.as_unquote_form().is_some() || contains_unquote(inner);
}
match node {
Sexp::List(items) => items.iter().any(contains_unquote),
_ => false,
}
}
fn splice_value_into(builder: &mut Vec<Sexp>, value: &Sexp) {
match value {
Sexp::List(items) => builder.extend(items.iter().cloned()),
Sexp::Nil => {}
other => builder.push(other.clone()),
}
}
fn template_invariant_violation(macro_name: &str, kind: TemplateInvariantKind) -> LispError {
LispError::TemplateInvariant {
macro_name: macro_name.into(),
kind,
}
}
fn resolve_bound_arg<'a>(
args_by_index: &'a [Sexp],
idx: usize,
macro_name: &str,
kind: impl FnOnce(usize) -> TemplateInvariantKind,
) -> Result<&'a Sexp> {
args_by_index
.get(idx)
.ok_or_else(|| template_invariant_violation(macro_name, kind(idx)))
}
fn current_builder_mut(stack: &mut [Vec<Sexp>]) -> &mut Vec<Sexp> {
stack
.last_mut()
.expect("bytecode-runtime invariant: at least one stack frame during op-loop")
}
fn pop_builder_frame(
stack: &mut Vec<Vec<Sexp>>,
macro_name: &str,
kind: TemplateInvariantKind,
) -> Result<Vec<Sexp>> {
stack
.pop()
.ok_or_else(|| template_invariant_violation(macro_name, kind))
}
fn apply_compiled(
macro_name: &str,
params: &MacroParams,
tmpl: &CompiledTemplate,
args: &[Sexp],
) -> Result<Sexp> {
let args_by_index = params.bind(macro_name, args)?;
let mut stack: Vec<Vec<Sexp>> = vec![Vec::with_capacity(1)];
for op in &tmpl.ops {
match op {
TemplateOp::Literal(s) => current_builder_mut(&mut stack).push(s.clone()),
TemplateOp::Subst(idx) => {
let v = resolve_bound_arg(
&args_by_index,
*idx,
macro_name,
TemplateInvariantKind::SubstBadIndex,
)?
.clone();
current_builder_mut(&mut stack).push(v);
}
TemplateOp::Splice(idx) => {
let v = resolve_bound_arg(
&args_by_index,
*idx,
macro_name,
TemplateInvariantKind::SpliceBadIndex,
)?;
splice_value_into(current_builder_mut(&mut stack), v);
}
TemplateOp::BeginList => stack.push(Vec::new()),
TemplateOp::EndList => {
let items = pop_builder_frame(
&mut stack,
macro_name,
TemplateInvariantKind::EndListEmptyStack,
)?;
current_builder_mut(&mut stack).push(Sexp::List(items));
}
}
}
let mut top = pop_builder_frame(&mut stack, macro_name, TemplateInvariantKind::FinalNoValue)?;
if top.len() == 1 {
Ok(top.remove(0))
} else {
Ok(Sexp::List(top))
}
}
fn args_cache_key(macro_name: &str, args: &[Sexp]) -> Option<CacheKey> {
let mut h = DefaultHasher::new();
args.len().hash(&mut h);
for a in args {
a.hash(&mut h);
}
Some((macro_name.to_string(), h.finish()))
}
pub(crate) fn macro_def_from(form: &Sexp) -> Result<Option<MacroDef>> {
let Some((head, args)) = form.as_call_to_any(MacroDefHead::from_keyword) else {
return Ok(None);
};
if args.len() < 3 {
return Err(defmacro_arity(head, args.len() + 1));
}
let name = args[0]
.as_symbol()
.ok_or_else(|| defmacro_non_symbol_name(head, &args[0]))?
.to_string();
let param_list = args[1]
.as_list()
.ok_or_else(|| defmacro_non_list_params(head, &args[1]))?;
let params = parse_params(param_list)?;
let body = args[2].clone();
Ok(Some(MacroDef { name, params, body }))
}
fn parse_params(list: &[Sexp]) -> Result<MacroParams> {
let mut required = Vec::new();
let mut optional: Vec<OptionalParam> = Vec::new();
let mut optional_marker: Option<usize> = None;
let mut i = 0;
while i < list.len() {
if optional_marker.is_some() {
if let Sexp::List(items) = &list[i] {
optional.push(parse_optional_list_spec(i, &list[i], items)?);
i += 1;
continue;
}
}
let s = list[i]
.as_symbol()
.ok_or_else(|| non_symbol_param(i, &list[i]))?;
if s == MacroParams::REST_MARKER {
let Some(next) = list.get(i + 1) else {
return Err(rest_param_missing_name(i, None));
};
let Some(name) = next.as_symbol() else {
return Err(rest_param_missing_name(i, Some(next)));
};
let trailing = &list[i + 2..];
if !trailing.is_empty() {
return Err(rest_param_trailing_tokens(i, trailing));
}
return Ok(MacroParams {
required,
optional,
rest: Some(name.to_string()),
});
}
if s == MacroParams::OPTIONAL_MARKER {
if let Some(first) = optional_marker {
return Err(optional_marker_repeated(first, i));
}
optional_marker = Some(i);
i += 1;
continue;
}
if optional_marker.is_some() {
optional.push(OptionalParam::bare(s));
} else {
required.push(s.to_string());
}
i += 1;
}
Ok(MacroParams {
required,
optional,
rest: None,
})
}
fn parse_optional_list_spec(
position: usize,
list_form: &Sexp,
items: &[Sexp],
) -> Result<OptionalParam> {
use crate::error::OptionalParamMalformedReason as R;
if let Some(reason) = R::classify_arity(items.len()) {
return Err(optional_param_malformed(position, list_form, reason));
}
let Some(name) = items[0].as_symbol() else {
return Err(optional_param_malformed(
position,
list_form,
R::NonSymbolName,
));
};
Ok(OptionalParam::with_default(name, items[1].clone()))
}
fn bind_args(
macro_name: &str,
params: &MacroParams,
args: &[Sexp],
) -> Result<HashMap<String, Sexp>> {
let vals = params.bind(macro_name, args)?;
Ok(params
.names()
.into_iter()
.map(String::from)
.zip(vals)
.collect())
}
fn substitute(form: &Sexp, bindings: &HashMap<String, Sexp>) -> Result<Sexp> {
if let Some((kind, inner)) = form.as_unquote() {
return match kind {
UnquoteForm::Unquote => resolve_unquote_in_bindings(inner, bindings, kind).cloned(),
UnquoteForm::Splice => Err(splice_outside_list(inner)),
};
}
match form {
Sexp::List(items) => {
let mut out: Vec<Sexp> = Vec::with_capacity(items.len());
for item in items {
if let Some((UnquoteForm::Splice, inner)) = item.as_unquote() {
let val = resolve_unquote_in_bindings(inner, bindings, UnquoteForm::Splice)?;
splice_value_into(&mut out, val);
} else {
out.push(substitute(item, bindings)?);
}
}
Ok(Sexp::List(out))
}
_ => Ok(form.clone()),
}
}
fn unbound_template_var(prefix: UnquoteForm, name: &str, candidates: &[&str]) -> LispError {
LispError::UnboundTemplateVar {
prefix,
name: name.to_string(),
hint: crate::domain::suggest(name, candidates).map(str::to_string),
}
}
fn non_symbol_unquote_target(prefix: UnquoteForm, got: &Sexp) -> LispError {
LispError::NonSymbolUnquoteTarget {
prefix,
got: got.witness(),
}
}
fn unquote_target_symbol(inner: &Sexp, form: UnquoteForm) -> Result<&str> {
inner
.as_symbol()
.ok_or_else(|| non_symbol_unquote_target(form, inner))
}
fn resolve_param_index(name: &str, params: &[&str], form: UnquoteForm) -> Result<usize> {
params
.iter()
.position(|p| *p == name)
.ok_or_else(|| unbound_template_var(form, name, params))
}
fn resolve_binding<'a>(
bindings: &'a HashMap<String, Sexp>,
name: &str,
form: UnquoteForm,
) -> Result<&'a Sexp> {
bindings
.get(name)
.ok_or_else(|| unbound_template_var(form, name, &bound_names(bindings)))
}
fn resolve_unquote_in_params(inner: &Sexp, params: &[&str], form: UnquoteForm) -> Result<usize> {
let name = unquote_target_symbol(inner, form)?;
resolve_param_index(name, params, form)
}
fn resolve_unquote_in_bindings<'a>(
inner: &Sexp,
bindings: &'a HashMap<String, Sexp>,
form: UnquoteForm,
) -> Result<&'a Sexp> {
let name = unquote_target_symbol(inner, form)?;
resolve_binding(bindings, name, form)
}
fn splice_outside_list(inner: &Sexp) -> LispError {
LispError::SpliceOutsideList {
got: inner.witness(),
}
}
fn missing_macro_arg(macro_name: &str, param: &str) -> LispError {
LispError::MissingMacroArg {
macro_name: macro_name.to_string(),
param: param.to_string(),
}
}
fn too_many_macro_args(macro_name: &str, expected: usize, got: usize) -> LispError {
LispError::TooManyMacroArgs {
macro_name: macro_name.to_string(),
expected,
got,
}
}
fn non_symbol_param(position: usize, got: &Sexp) -> LispError {
LispError::NonSymbolParam {
position,
got: got.witness(),
}
}
fn rest_param_missing_name(rest_position: usize, got: Option<&Sexp>) -> LispError {
LispError::RestParamMissingName {
rest_position,
got: got.map(Sexp::witness),
}
}
fn rest_param_trailing_tokens(rest_position: usize, trailing: &[Sexp]) -> LispError {
LispError::RestParamTrailingTokens {
rest_position,
extra: trailing.len(),
first: trailing[0].witness(),
}
}
fn optional_marker_repeated(first_position: usize, second_position: usize) -> LispError {
LispError::OptionalMarkerRepeated {
first_position,
second_position,
}
}
fn optional_param_malformed(
position: usize,
got: &Sexp,
reason: crate::error::OptionalParamMalformedReason,
) -> LispError {
LispError::OptionalParamMalformed {
position,
got: got.witness(),
reason,
}
}
fn defmacro_arity(head: MacroDefHead, arity: usize) -> LispError {
LispError::DefmacroArity { head, arity }
}
fn defmacro_non_symbol_name(head: MacroDefHead, got: &Sexp) -> LispError {
LispError::DefmacroNonSymbolName {
head,
got: got.witness(),
}
}
fn defmacro_non_list_params(head: MacroDefHead, got: &Sexp) -> LispError {
LispError::DefmacroNonListParams {
head,
got: got.witness(),
}
}
fn bound_names(bindings: &HashMap<String, Sexp>) -> Vec<&str> {
bindings.keys().map(String::as_str).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::reader::read;
fn parse(src: &str) -> Sexp {
read(src).unwrap().into_iter().next().unwrap()
}
#[test]
fn identity_macro() {
let mut e = Expander::new();
let forms = read("(defmacro id (x) `,x) (id 42)").unwrap();
let out = e.expand_program(forms).unwrap();
assert_eq!(out.len(), 1);
assert_eq!(out[0], Sexp::int(42));
}
#[test]
fn wrap_macro_duplicates_arg() {
let mut e = Expander::new();
let forms = read("(defmacro wrap (x) `(list ,x ,x)) (wrap hello)").unwrap();
let out = e.expand_program(forms).unwrap();
assert_eq!(out[0], parse("(list hello hello)"));
}
#[test]
fn rest_param_splices_with_at() {
let mut e = Expander::new();
let forms = read("(defmacro call (f &rest args) `(,f ,@args)) (call foo a b c)").unwrap();
let out = e.expand_program(forms).unwrap();
assert_eq!(out[0], parse("(foo a b c)"));
}
#[test]
fn nested_macro_expansion() {
let mut e = Expander::new();
let forms = read(
"(defmacro twice (x) `(list ,x ,x))
(defmacro quad (x) `(twice ,x))
(quad hey)",
)
.unwrap();
let out = e.expand_program(forms).unwrap();
assert_eq!(out[0], parse("(list hey hey)"));
}
#[test]
fn unbound_unquote_errors() {
let mut e = Expander::new();
let forms = read("(defmacro bad (x) `(list ,y)) (bad 1)").unwrap();
assert!(e.expand_program(forms).is_err());
}
#[test]
fn missing_required_arg_errors() {
let mut e = Expander::new();
let forms = read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap();
assert!(e.expand_program(forms).is_err());
}
#[test]
fn defpoint_template_treated_as_defmacro() {
let mut e = Expander::new();
let forms = read(
"(defpoint-template obs (name) `(defpoint ,name :class (Gate Observability)))
(obs grafana)",
)
.unwrap();
let out = e.expand_program(forms).unwrap();
assert_eq!(
out[0],
parse("(defpoint grafana :class (Gate Observability))")
);
}
#[test]
fn defcheck_treated_as_defmacro() {
let mut e = Expander::new();
let forms = read(
"(defcheck pair (a b) `(do (yaml-parses ,a) (yaml-parses ,b)))
(pair \"x.yaml\" \"y.yaml\")",
)
.unwrap();
let out = e.expand_program(forms).unwrap();
assert_eq!(
out[0],
parse("(do (yaml-parses \"x.yaml\") (yaml-parses \"y.yaml\"))")
);
}
#[test]
fn empty_rest_splices_nothing() {
let mut e = Expander::new();
let forms = read("(defmacro f (x &rest r) `(list ,x ,@r)) (f 1)").unwrap();
let out = e.expand_program(forms).unwrap();
assert_eq!(out[0], parse("(list 1)"));
}
#[test]
fn macro_expanded_inside_list() {
let mut e = Expander::new();
let forms = read("(defmacro two () `(list 1 2)) (outer (two))").unwrap();
let out = e.expand_program(forms).unwrap();
assert_eq!(out[0], parse("(outer (list 1 2))"));
}
#[test]
fn compiled_template_matches_substitute_path() {
let src = "
(defmacro wrap (x) `(list ,x ,x))
(defmacro call (f &rest args) `(,f ,@args))
(defmacro twice (x) `(list ,x ,x))
(defmacro quad (x) `(twice ,x))
(wrap hello)
(call foo a b c)
(quad hey)
(outer (wrap deep))
";
let forms = read(src).unwrap();
let mut fast = Expander::new();
let mut slow = Expander::new_substitute_only();
let out_fast = fast.expand_program(forms.clone()).unwrap();
let out_slow = slow.expand_program(forms).unwrap();
assert_eq!(out_fast, out_slow);
}
#[test]
fn literal_subtree_compiles_to_single_literal_op() {
let def = MacroDef {
name: "label".into(),
params: MacroParams {
required: vec!["x".into()],
optional: Vec::new(),
rest: None,
},
body: Sexp::Quasiquote(Box::new(parse(
"(observed (at timestamp) (in region) (value ,x) (tags (one two three)))",
))),
};
let compiled = compile_template(&def).expect("compile");
let ops_count = compiled.ops.len();
assert!(
ops_count < 15,
"expected pruned op stream, got {ops_count} ops: {:?}",
compiled.ops
);
}
#[test]
fn expansion_layers_agree_on_output_and_cache_wins() {
use std::time::Instant;
let macros = "
(defmacro m1 (a b) `(list ,a ,b))
(defmacro m2 (x) `(if ,x true false))
(defmacro m3 (a b c) `(list ,a ,b ,c ,a ,b ,c))
(defmacro m4 (f &rest args) `(,f ,@args))
(defmacro m5 (x) `(and ,x (not (not ,x))))
(defmacro m6 (a b) `(or ,a ,b (and ,a ,b)))
(defmacro m7 (x) `(debug (at timestamp) (in region) (value ,x)))
(defmacro m8 (x y) `(cond ((= ,x ,y) equal) (#t not-equal)))
(defmacro m9 (x) `(loop (times 10) (eval ,x)))
(defmacro m10 (f g &rest args) `(,f (,g ,@args)))
";
let mut call_src = String::with_capacity(80_000);
for i in 0..10_000 {
match i % 10 {
0 => call_src.push_str("(m1 a b)\n"),
1 => call_src.push_str("(m2 true)\n"),
2 => call_src.push_str("(m3 x y z)\n"),
3 => call_src.push_str("(m4 f a b c d e)\n"),
4 => call_src.push_str("(m5 y)\n"),
5 => call_src.push_str("(m6 a b)\n"),
6 => call_src.push_str("(m7 answer)\n"),
7 => call_src.push_str("(m8 p q)\n"),
8 => call_src.push_str("(m9 body)\n"),
_ => call_src.push_str("(m10 f g a b c)\n"),
}
}
let all_src = format!("{macros}\n{call_src}");
let forms = read(&all_src).unwrap();
let mut subst = Expander::new_substitute_only();
let t0 = Instant::now();
let out_subst = subst.expand_program(forms.clone()).unwrap();
let t_subst = t0.elapsed();
let mut byte_no_cache = Expander::new_bytecode_no_cache();
let t0 = Instant::now();
let out_byte = byte_no_cache.expand_program(forms.clone()).unwrap();
let t_byte = t0.elapsed();
let mut byte_cache = Expander::new();
let t0 = Instant::now();
let out_cached = byte_cache.expand_program(forms).unwrap();
let t_cached = t0.elapsed();
assert_eq!(out_subst, out_byte);
assert_eq!(out_subst, out_cached);
let cache_size = byte_cache.cache_size();
assert!(
(10..=50).contains(&cache_size),
"expected ~10 unique cache entries, got {cache_size}"
);
eprintln!(
"\n=== macroexpand: 10k calls × 10 unique (macro, args) pairs ===\n\
substitute only : {t_subst:?}\n\
bytecode no cache : {t_byte:?}\n\
bytecode + cache : {t_cached:?} (cache_size={cache_size})\n\
cache speedup vs subst : {:.2}×\n\
cache speedup vs byte : {:.2}×\n",
t_subst.as_secs_f64() / t_cached.as_secs_f64(),
t_byte.as_secs_f64() / t_cached.as_secs_f64(),
);
assert!(
t_cached < t_subst,
"cache should beat substitute ({t_cached:?} vs {t_subst:?})"
);
assert!(
t_cached < t_byte,
"cache should beat bytecode-no-cache ({t_cached:?} vs {t_byte:?})"
);
}
#[test]
fn cache_respects_arg_changes() {
let src = "
(defmacro wrap (x) `(list ,x ,x))
(wrap a)
(wrap b)
(wrap a) ;; same as first — cached hit
";
let mut e = Expander::new();
let out = e.expand_program(read(src).unwrap()).unwrap();
assert_eq!(out.len(), 3);
assert_eq!(out[0], parse("(list a a)"));
assert_eq!(out[1], parse("(list b b)"));
assert_eq!(out[2], parse("(list a a)"));
assert_eq!(e.cache_size(), 2);
}
#[test]
fn clear_cache_empties_memo() {
let mut e = Expander::new();
let out = e
.expand_program(read("(defmacro id (x) `,x) (id 1) (id 2)").unwrap())
.unwrap();
assert_eq!(out.len(), 2);
assert_eq!(e.cache_size(), 2);
e.clear_cache();
assert_eq!(e.cache_size(), 0);
}
fn unbound_var(err: &LispError) -> (UnquoteForm, &str, Option<&str>) {
match err {
LispError::UnboundTemplateVar { prefix, name, hint } => {
(*prefix, name.as_str(), hint.as_deref())
}
other => panic!("expected UnboundTemplateVar, got: {other:?}"),
}
}
#[test]
fn unbound_unquote_in_compile_template_emits_structural_variant_with_hint() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
.expect_err("unbound template var must error");
let (prefix, name, hint) = unbound_var(&err);
assert_eq!(prefix, UnquoteForm::Unquote);
assert_eq!(name, "xs");
assert_eq!(hint, Some("x"));
}
#[test]
fn unbound_unquote_splice_in_compile_template_emits_structural_variant_with_hint() {
let mut e = Expander::new();
let err = e
.expand_program(
read("(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)").unwrap(),
)
.expect_err("unbound splice must error");
let (prefix, name, hint) = unbound_var(&err);
assert_eq!(prefix, UnquoteForm::Splice);
assert_eq!(name, "argz");
assert_eq!(hint, Some("args"));
}
#[test]
fn unbound_unquote_in_substitute_emits_structural_variant_with_hint() {
let mut e = Expander::new_substitute_only();
let err = e
.expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
.expect_err("substitute unbound must error");
let (prefix, name, hint) = unbound_var(&err);
assert_eq!(prefix, UnquoteForm::Unquote);
assert_eq!(name, "xs");
assert_eq!(hint, Some("x"));
}
#[test]
fn unbound_unquote_splice_in_substitute_emits_structural_variant_with_hint() {
let mut e = Expander::new_substitute_only();
let err = e
.expand_program(
read("(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)").unwrap(),
)
.expect_err("substitute splice unbound must error");
let (prefix, name, hint) = unbound_var(&err);
assert_eq!(prefix, UnquoteForm::Splice);
assert_eq!(name, "argz");
assert_eq!(hint, Some("args"));
}
#[test]
fn unbound_template_var_omits_hint_when_no_close_match() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro w (x) `(list ,wholly-unrelated)) (w 1)").unwrap())
.expect_err("unrelated unbound must error");
let (prefix, name, hint) = unbound_var(&err);
assert_eq!(prefix, UnquoteForm::Unquote);
assert_eq!(name, "wholly-unrelated");
assert_eq!(hint, None);
}
#[test]
fn unbound_template_var_message_includes_hint_suffix_end_to_end() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
.expect_err("unbound must error");
let msg = format!("{err}");
assert!(
msg.contains("did you mean ,x?"),
"expected hint suffix in message, got: {msg}"
);
assert!(
msg.contains("unbound"),
"expected legacy `unbound` substring in message, got: {msg}"
);
assert!(
msg.contains(",xs"),
"expected the offending form in message, got: {msg}"
);
}
#[test]
fn unbound_template_var_position_is_none_today() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro w (x) `(list ,xs)) (w 1)").unwrap())
.expect_err("unbound must error");
assert_eq!(err.position(), None);
}
fn non_symbol_target(err: &LispError) -> (UnquoteForm, &str) {
match err {
LispError::NonSymbolUnquoteTarget { prefix, got } => (*prefix, got.display.as_str()),
other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
}
}
#[test]
fn non_symbol_unquote_in_compile_template_emits_structural_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
.expect_err("non-symbol unquote target must error");
let (prefix, got) = non_symbol_target(&err);
assert_eq!(prefix, UnquoteForm::Unquote);
assert_eq!(got, "(list 1 2)");
}
#[test]
fn non_symbol_unquote_splice_in_compile_template_emits_structural_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro w (x) `(list ,@5)) (w 1)").unwrap())
.expect_err("non-symbol splice target must error");
let (prefix, got) = non_symbol_target(&err);
assert_eq!(prefix, UnquoteForm::Splice);
assert_eq!(got, "5");
}
#[test]
fn non_symbol_unquote_in_substitute_emits_structural_variant() {
let mut e = Expander::new_substitute_only();
let err = e
.expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
.expect_err("substitute non-symbol target must error");
let (prefix, got) = non_symbol_target(&err);
assert_eq!(prefix, UnquoteForm::Unquote);
assert_eq!(got, "(list 1 2)");
}
#[test]
fn non_symbol_unquote_splice_inside_list_in_substitute_emits_structural_variant() {
let mut e = Expander::new_substitute_only();
let err = e
.expand_program(read("(defmacro w (x) `(outer ,@(list 1 2))) (w 1)").unwrap())
.expect_err("substitute non-symbol splice must error");
let (prefix, got) = non_symbol_target(&err);
assert_eq!(prefix, UnquoteForm::Splice);
assert_eq!(got, "(list 1 2)");
}
#[test]
fn non_symbol_unquote_target_position_is_none_today() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
.expect_err("non-symbol target must error");
assert_eq!(err.position(), None);
}
#[test]
fn unquote_target_symbol_returns_symbol_for_symbol_inner_under_unquote() {
let inner = Sexp::symbol("xs");
let name = unquote_target_symbol(&inner, UnquoteForm::Unquote)
.expect("symbol inner must project to Ok");
assert_eq!(name, "xs");
}
#[test]
fn unquote_target_symbol_returns_symbol_for_symbol_inner_under_splice() {
let inner = Sexp::symbol("rest");
let name = unquote_target_symbol(&inner, UnquoteForm::Splice)
.expect("symbol inner must project to Ok under Splice");
assert_eq!(name, "rest");
}
#[test]
fn unquote_target_symbol_rejects_int_inner_under_unquote() {
let inner = Sexp::int(5);
let err = unquote_target_symbol(&inner, UnquoteForm::Unquote)
.expect_err("int inner must error at gate-1");
match err {
LispError::NonSymbolUnquoteTarget { prefix, got } => {
assert_eq!(prefix, UnquoteForm::Unquote);
assert_eq!(got.shape, crate::error::SexpShape::Int);
assert_eq!(got.display, "5");
}
other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
}
}
#[test]
fn unquote_target_symbol_rejects_list_inner_under_splice() {
let inner = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
let err = unquote_target_symbol(&inner, UnquoteForm::Splice)
.expect_err("list inner must error at gate-1");
match err {
LispError::NonSymbolUnquoteTarget { prefix, got } => {
assert_eq!(prefix, UnquoteForm::Splice);
assert_eq!(got.shape, crate::error::SexpShape::List);
assert_eq!(got.display, "(list 1 2)");
}
other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
}
}
#[test]
fn unquote_target_symbol_rejects_keyword_inner_with_typed_witness() {
let inner = Sexp::keyword("foo");
let err = unquote_target_symbol(&inner, UnquoteForm::Unquote)
.expect_err("keyword inner must error at gate-1");
match err {
LispError::NonSymbolUnquoteTarget { prefix, got } => {
assert_eq!(prefix, UnquoteForm::Unquote);
assert_eq!(got.shape, crate::error::SexpShape::Keyword);
assert_eq!(got.display, ":foo");
}
other => panic!("expected NonSymbolUnquoteTarget, got: {other:?}"),
}
}
#[test]
fn unquote_target_symbol_consolidates_four_inline_callsites_into_one_helper() {
let cases: &[(&str, UnquoteForm)] = &[
("(defmacro w (x) `,(list 1 2)) (w 1)", UnquoteForm::Unquote),
("(defmacro w (x) `(list ,@5)) (w 1)", UnquoteForm::Splice),
];
for (src, expected_form) in cases {
let mut e = Expander::new();
let err = e
.expand_program(read(src).unwrap())
.expect_err("non-symbol unquote target must error end-to-end");
match err {
LispError::NonSymbolUnquoteTarget { prefix, .. } => {
assert_eq!(prefix, *expected_form, "for src: {src}");
}
other => panic!("expected NonSymbolUnquoteTarget for {src}, got: {other:?}"),
}
}
let mut e_subst = Expander::new_substitute_only();
let err = e_subst
.expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
.expect_err("substitute Unquote must error end-to-end");
assert!(
matches!(
err,
LispError::NonSymbolUnquoteTarget {
prefix: UnquoteForm::Unquote,
..
}
),
"expected NonSymbolUnquoteTarget at substitute Unquote, got: {err:?}"
);
let mut e_subst2 = Expander::new_substitute_only();
let err = e_subst2
.expand_program(read("(defmacro w (x) `(outer ,@(list 1 2))) (w 1)").unwrap())
.expect_err("substitute UnquoteSplice-in-list must error end-to-end");
assert!(
matches!(
err,
LispError::NonSymbolUnquoteTarget {
prefix: UnquoteForm::Splice,
..
}
),
"expected NonSymbolUnquoteTarget at substitute UnquoteSplice-in-list, got: {err:?}"
);
}
#[test]
fn resolve_param_index_returns_position_for_bound_name_under_unquote() {
let params = ["a", "x", "rest"];
let idx = resolve_param_index("x", ¶ms, UnquoteForm::Unquote)
.expect("bound name must project to Ok at gate-2");
assert_eq!(idx, 1);
}
#[test]
fn resolve_param_index_returns_position_for_bound_name_under_splice() {
let params = ["a", "x", "rest"];
let idx = resolve_param_index("rest", ¶ms, UnquoteForm::Splice)
.expect("bound name must project to Ok at gate-2 under Splice");
assert_eq!(idx, 2);
}
#[test]
fn resolve_param_index_rejects_unbound_name_with_hint_under_unquote() {
let params = ["x"];
let err = resolve_param_index("xs", ¶ms, UnquoteForm::Unquote)
.expect_err("unbound name must error at gate-2");
match err {
LispError::UnboundTemplateVar { prefix, name, hint } => {
assert_eq!(prefix, UnquoteForm::Unquote);
assert_eq!(name, "xs");
assert_eq!(hint.as_deref(), Some("x"));
}
other => panic!("expected UnboundTemplateVar, got: {other:?}"),
}
}
#[test]
fn resolve_param_index_rejects_unbound_name_without_hint_under_splice() {
let params = ["x"];
let err = resolve_param_index("wholly-unrelated", ¶ms, UnquoteForm::Splice)
.expect_err("unrelated unbound must error at gate-2");
match err {
LispError::UnboundTemplateVar { prefix, name, hint } => {
assert_eq!(prefix, UnquoteForm::Splice);
assert_eq!(name, "wholly-unrelated");
assert_eq!(hint, None);
}
other => panic!("expected UnboundTemplateVar, got: {other:?}"),
}
}
#[test]
fn resolve_binding_returns_value_for_bound_name_under_unquote() {
let mut bindings: HashMap<String, Sexp> = HashMap::new();
bindings.insert("x".to_string(), Sexp::int(42));
bindings.insert("y".to_string(), Sexp::string("hi"));
let val = resolve_binding(&bindings, "x", UnquoteForm::Unquote)
.expect("bound name must project to Ok at gate-2 (substitute)");
assert_eq!(val, &Sexp::int(42));
}
#[test]
fn resolve_binding_returns_value_for_bound_name_under_splice() {
let mut bindings: HashMap<String, Sexp> = HashMap::new();
bindings.insert(
"args".to_string(),
Sexp::List(vec![Sexp::int(1), Sexp::int(2)]),
);
let val = resolve_binding(&bindings, "args", UnquoteForm::Splice)
.expect("bound name must project to Ok at gate-2 under Splice");
assert_eq!(val, &Sexp::List(vec![Sexp::int(1), Sexp::int(2)]));
}
#[test]
fn resolve_binding_rejects_unbound_name_with_hint_under_unquote() {
let mut bindings: HashMap<String, Sexp> = HashMap::new();
bindings.insert("x".to_string(), Sexp::int(1));
let err = resolve_binding(&bindings, "xs", UnquoteForm::Unquote)
.expect_err("unbound name must error at gate-2 (substitute)");
match err {
LispError::UnboundTemplateVar { prefix, name, hint } => {
assert_eq!(prefix, UnquoteForm::Unquote);
assert_eq!(name, "xs");
assert_eq!(hint.as_deref(), Some("x"));
}
other => panic!("expected UnboundTemplateVar, got: {other:?}"),
}
}
#[test]
fn resolve_binding_rejects_unbound_name_without_hint_under_splice() {
let mut bindings: HashMap<String, Sexp> = HashMap::new();
bindings.insert("args".to_string(), Sexp::Nil);
let err = resolve_binding(&bindings, "wholly-unrelated", UnquoteForm::Splice)
.expect_err("unrelated unbound must error at gate-2");
match err {
LispError::UnboundTemplateVar { prefix, name, hint } => {
assert_eq!(prefix, UnquoteForm::Splice);
assert_eq!(name, "wholly-unrelated");
assert_eq!(hint, None);
}
other => panic!("expected UnboundTemplateVar, got: {other:?}"),
}
}
#[test]
fn gate_2_consolidates_four_inline_callsites_into_two_helpers() {
struct Case {
src: &'static str,
expander: fn() -> Expander,
expected_form: UnquoteForm,
}
let cases: &[Case] = &[
Case {
src: "(defmacro w (x) `(list ,xs)) (w 1)",
expander: Expander::new,
expected_form: UnquoteForm::Unquote,
},
Case {
src: "(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)",
expander: Expander::new,
expected_form: UnquoteForm::Splice,
},
Case {
src: "(defmacro w (x) `(list ,xs)) (w 1)",
expander: Expander::new_substitute_only,
expected_form: UnquoteForm::Unquote,
},
Case {
src: "(defmacro call (f &rest args) `(,f ,@argz)) (call foo a b)",
expander: Expander::new_substitute_only,
expected_form: UnquoteForm::Splice,
},
];
for case in cases {
let mut e = (case.expander)();
let err = e
.expand_program(read(case.src).unwrap())
.expect_err("unbound template var must error end-to-end");
match err {
LispError::UnboundTemplateVar { prefix, .. } => {
assert_eq!(prefix, case.expected_form, "for src: {}", case.src);
}
other => panic!(
"expected UnboundTemplateVar for {}, got: {other:?}",
case.src
),
}
}
}
#[test]
fn resolve_unquote_in_params_returns_index_for_symbol_inner_under_unquote() {
let inner = Sexp::symbol("x");
let params = ["x", "y"];
let idx = resolve_unquote_in_params(&inner, ¶ms, UnquoteForm::Unquote)
.expect("symbol-inner bound at index 0 must resolve");
assert_eq!(idx, 0);
}
#[test]
fn resolve_unquote_in_params_returns_index_for_symbol_inner_under_splice() {
let inner = Sexp::symbol("args");
let params = ["f", "args"];
let idx = resolve_unquote_in_params(&inner, ¶ms, UnquoteForm::Splice)
.expect("symbol-inner bound at index 1 must resolve");
assert_eq!(idx, 1);
}
#[test]
fn resolve_unquote_in_params_rejects_non_symbol_inner_at_gate_1() {
let inner = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
let params = ["x"];
let err = resolve_unquote_in_params(&inner, ¶ms, UnquoteForm::Unquote)
.expect_err("non-symbol inner must reject at gate-1");
match err {
LispError::NonSymbolUnquoteTarget { prefix, got } => {
assert_eq!(prefix, UnquoteForm::Unquote);
assert_eq!(got.display, "(list 1 2)");
}
other => panic!("expected NonSymbolUnquoteTarget (gate-1), got: {other:?}"),
}
}
#[test]
fn resolve_unquote_in_params_rejects_unbound_symbol_at_gate_2() {
let inner = Sexp::symbol("missing");
let params = ["x", "y"];
let err = resolve_unquote_in_params(&inner, ¶ms, UnquoteForm::Splice)
.expect_err("unbound symbol must reject at gate-2");
match err {
LispError::UnboundTemplateVar { prefix, name, .. } => {
assert_eq!(prefix, UnquoteForm::Splice);
assert_eq!(name, "missing");
}
other => panic!("expected UnboundTemplateVar (gate-2), got: {other:?}"),
}
}
#[test]
fn resolve_unquote_in_bindings_returns_borrow_for_symbol_inner_under_unquote() {
let mut bindings: HashMap<String, Sexp> = HashMap::new();
bindings.insert("v".to_string(), Sexp::int(42));
let inner = Sexp::symbol("v");
let val = resolve_unquote_in_bindings(&inner, &bindings, UnquoteForm::Unquote)
.expect("symbol-inner bound to 42 must resolve");
assert_eq!(val, &Sexp::int(42));
}
#[test]
fn resolve_unquote_in_bindings_rejects_non_symbol_inner_at_gate_1() {
let bindings: HashMap<String, Sexp> = HashMap::new();
let inner = Sexp::int(5);
let err = resolve_unquote_in_bindings(&inner, &bindings, UnquoteForm::Splice)
.expect_err("non-symbol inner must reject at gate-1");
match err {
LispError::NonSymbolUnquoteTarget { prefix, got } => {
assert_eq!(prefix, UnquoteForm::Splice);
assert_eq!(got.display, "5");
}
other => panic!("expected NonSymbolUnquoteTarget (gate-1), got: {other:?}"),
}
}
#[test]
fn resolve_unquote_in_bindings_rejects_unbound_symbol_at_gate_2() {
let mut bindings: HashMap<String, Sexp> = HashMap::new();
bindings.insert("known".to_string(), Sexp::Nil);
let inner = Sexp::symbol("missing");
let err = resolve_unquote_in_bindings(&inner, &bindings, UnquoteForm::Unquote)
.expect_err("unbound symbol must reject at gate-2");
match err {
LispError::UnboundTemplateVar { prefix, name, .. } => {
assert_eq!(prefix, UnquoteForm::Unquote);
assert_eq!(name, "missing");
}
other => panic!("expected UnboundTemplateVar (gate-2), got: {other:?}"),
}
}
#[test]
fn resolve_unquote_helpers_consolidate_four_inline_gate12_sites() {
struct Case {
src: &'static str,
expander: fn() -> Expander,
expected_form: UnquoteForm,
}
let cases: &[Case] = &[
Case {
src: "(defmacro w (x) `,(list 1 2)) (w 1)",
expander: Expander::new,
expected_form: UnquoteForm::Unquote,
},
Case {
src: "(defmacro w (x) `(outer ,@5)) (w 1)",
expander: Expander::new,
expected_form: UnquoteForm::Splice,
},
Case {
src: "(defmacro w (x) `,(list 1 2)) (w 1)",
expander: Expander::new_substitute_only,
expected_form: UnquoteForm::Unquote,
},
Case {
src: "(defmacro w (x) `(outer ,@(list 1 2))) (w 1)",
expander: Expander::new_substitute_only,
expected_form: UnquoteForm::Splice,
},
];
for case in cases {
let mut e = (case.expander)();
let err = e
.expand_program(read(case.src).unwrap())
.expect_err("non-symbol inner must error end-to-end");
match err {
LispError::NonSymbolUnquoteTarget { prefix, .. } => {
assert_eq!(prefix, case.expected_form, "for src: {}", case.src);
}
other => panic!(
"expected NonSymbolUnquoteTarget for {}, got: {other:?}",
case.src
),
}
}
}
fn splice_outside_list_got(err: &LispError) -> &str {
match err {
LispError::SpliceOutsideList { got } => got.display.as_str(),
other => panic!("expected SpliceOutsideList, got: {other:?}"),
}
}
#[test]
fn splice_outside_list_in_substitute_emits_structural_variant() {
let mut e = Expander::new_substitute_only();
let err = e
.expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
.expect_err("splice outside list must error");
assert_eq!(splice_outside_list_got(&err), "xs");
}
#[test]
fn splice_outside_list_with_list_literal_in_substitute_emits_structural_variant() {
let mut e = Expander::new_substitute_only();
let err = e
.expand_program(read("(defmacro f (x) `,@(list 1 2)) (f 1)").unwrap())
.expect_err("splice outside list must error");
assert_eq!(splice_outside_list_got(&err), "(list 1 2)");
}
#[test]
fn splice_outside_list_in_compile_template_emits_structural_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
.expect_err("compile-template splice outside list must error");
assert_eq!(splice_outside_list_got(&err), "xs");
}
#[test]
fn splice_outside_list_with_list_literal_in_compile_template_emits_structural_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f (x) `,@(list 1 2)) (f 1)").unwrap())
.expect_err("compile-template splice outside list must error");
assert_eq!(splice_outside_list_got(&err), "(list 1 2)");
}
#[test]
fn splice_outside_list_substitute_and_bytecode_paths_agree() {
let src = "(defmacro f (xs) `,@xs) (f (list 1 2))";
let mut subst = Expander::new_substitute_only();
let mut bytecode = Expander::new();
let err_subst = subst
.expand_program(read(src).unwrap())
.expect_err("substitute must error");
let err_byte = bytecode
.expand_program(read(src).unwrap())
.expect_err("bytecode must error");
assert_eq!(splice_outside_list_got(&err_subst), "xs");
assert_eq!(splice_outside_list_got(&err_byte), "xs");
}
#[test]
fn splice_outside_list_position_is_none_today() {
let mut e = Expander::new_substitute_only();
let err = e
.expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
.expect_err("splice outside list must error");
assert_eq!(err.position(), None);
}
#[test]
fn splice_outside_list_message_renders_legacy_substring_with_offending_form() {
let mut e = Expander::new_substitute_only();
let err = e
.expand_program(read("(defmacro f (xs) `,@xs) (f (list 1 2))").unwrap())
.expect_err("splice outside list must error");
let msg = format!("{err}");
assert_eq!(
msg,
"compile error in ,@: `,@` may only appear inside a list (got ,@xs)"
);
}
#[test]
fn splice_inside_list_still_succeeds_under_both_paths() {
let src = "(defmacro f (&rest xs) `(outer ,@xs)) (f 1 2)";
let mut subst = Expander::new_substitute_only();
let mut bytecode = Expander::new();
let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
assert_eq!(out_subst, out_byte);
assert_eq!(out_subst[0], parse("(outer 1 2)"));
}
#[test]
fn splice_value_into_list_flattens_elements_into_builder() {
let mut builder = vec![Sexp::symbol("outer")];
splice_value_into(&mut builder, &Sexp::List(vec![Sexp::int(1), Sexp::int(2)]));
assert_eq!(
builder,
vec![Sexp::symbol("outer"), Sexp::int(1), Sexp::int(2)]
);
}
#[test]
fn splice_value_into_nil_is_a_noop() {
let mut builder = vec![Sexp::symbol("outer")];
splice_value_into(&mut builder, &Sexp::Nil);
assert_eq!(builder, vec![Sexp::symbol("outer")]);
}
#[test]
fn splice_value_into_scalar_pushes_single_element() {
let mut builder = vec![Sexp::symbol("outer")];
splice_value_into(&mut builder, &Sexp::int(5));
assert_eq!(builder, vec![Sexp::symbol("outer"), Sexp::int(5)]);
let mut other: Vec<Sexp> = vec![];
splice_value_into(&mut other, &Sexp::keyword("k"));
assert_eq!(other, vec![Sexp::keyword("k")]);
}
#[test]
fn splice_of_non_list_value_coerces_identically_under_both_paths() {
let scalar = "(defmacro f (x) `(outer ,@x)) (f 5)";
let empty = "(defmacro g (x) `(outer ,@x)) (g ())";
for src in [scalar, empty] {
let mut subst = Expander::new_substitute_only();
let mut bytecode = Expander::new();
let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
assert_eq!(out_subst, out_byte, "strategies must agree for {src}");
}
let mut e = Expander::new();
assert_eq!(
e.expand_program(read(scalar).unwrap()).unwrap()[0],
parse("(outer 5)")
);
let mut e2 = Expander::new();
assert_eq!(
e2.expand_program(read(empty).unwrap()).unwrap()[0],
parse("(outer)")
);
}
fn missing_macro_arg_fields(err: &LispError) -> (&str, &str) {
match err {
LispError::MissingMacroArg { macro_name, param } => {
(macro_name.as_str(), param.as_str())
}
other => panic!("expected MissingMacroArg, got: {other:?}"),
}
}
#[test]
fn missing_macro_arg_in_compile_template_emits_structural_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
.expect_err("missing required macro arg must error");
let (macro_name, param) = missing_macro_arg_fields(&err);
assert_eq!(macro_name, "need-two");
assert_eq!(param, "b");
}
#[test]
fn missing_macro_arg_in_substitute_emits_structural_variant() {
let mut e = Expander::new_substitute_only();
let err = e
.expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
.expect_err("missing required macro arg must error");
let (macro_name, param) = missing_macro_arg_fields(&err);
assert_eq!(macro_name, "need-two");
assert_eq!(param, "b");
}
#[test]
fn missing_macro_arg_first_position_is_named() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f (a b) `(,a ,b)) (f)").unwrap())
.expect_err("missing first required arg must error");
let (macro_name, param) = missing_macro_arg_fields(&err);
assert_eq!(macro_name, "f");
assert_eq!(param, "a");
}
#[test]
fn missing_macro_arg_substitute_and_bytecode_paths_agree() {
let src = "(defmacro need-two (a b) `(,a ,b)) (need-two 1)";
let mut subst = Expander::new_substitute_only();
let mut bytecode = Expander::new();
let err_subst = subst
.expand_program(read(src).unwrap())
.expect_err("substitute must error");
let err_byte = bytecode
.expand_program(read(src).unwrap())
.expect_err("bytecode must error");
assert_eq!(missing_macro_arg_fields(&err_subst), ("need-two", "b"));
assert_eq!(missing_macro_arg_fields(&err_byte), ("need-two", "b"));
}
#[test]
fn missing_macro_arg_position_is_none_today() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
.expect_err("missing required macro arg must error");
assert_eq!(err.position(), None);
}
#[test]
fn missing_macro_arg_message_renders_legacy_substring_with_macro_name() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro need-two (a b) `(,a ,b)) (need-two 1)").unwrap())
.expect_err("missing required macro arg must error");
assert_eq!(
format!("{err}"),
"compile error in call to need-two: missing required arg: b"
);
}
#[test]
fn missing_macro_arg_carries_kebab_case_macro_and_param_unchanged() {
let mut e = Expander::new();
let err = e
.expand_program(
read("(defmacro wrap-twice (notify-ref body) `(list ,notify-ref ,body)) (wrap-twice :a)")
.unwrap(),
)
.expect_err("missing required macro arg must error");
let (macro_name, param) = missing_macro_arg_fields(&err);
assert_eq!(macro_name, "wrap-twice");
assert_eq!(param, "body");
}
#[test]
fn rest_param_only_macro_with_no_args_still_succeeds() {
let src = "(defmacro f (&rest xs) `(list ,@xs)) (f)";
let mut subst = Expander::new_substitute_only();
let mut bytecode = Expander::new();
let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
assert_eq!(out_subst, out_byte);
assert_eq!(out_subst[0], parse("(list)"));
}
fn too_many_macro_args_fields(err: &LispError) -> (&str, usize, usize) {
match err {
LispError::TooManyMacroArgs {
macro_name,
expected,
got,
} => (macro_name.as_str(), *expected, *got),
other => panic!("expected TooManyMacroArgs, got: {other:?}"),
}
}
#[test]
fn too_many_macro_args_required_only_rejected_with_expected_and_got() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f (a b) `(list ,a ,b)) (f 1 2 3)").unwrap())
.expect_err("surplus arg on rest-less call must error");
let (macro_name, expected, got) = too_many_macro_args_fields(&err);
assert_eq!(macro_name, "f");
assert_eq!(expected, 2);
assert_eq!(got, 3);
}
#[test]
fn too_many_macro_args_required_plus_optional_capacity_includes_optional() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f (a &optional b) `(list ,a ,b)) (f 1 2 3)").unwrap())
.expect_err("surplus arg beyond required+optional must error");
let (macro_name, expected, got) = too_many_macro_args_fields(&err);
assert_eq!(macro_name, "f");
assert_eq!(expected, 2);
assert_eq!(got, 3);
}
#[test]
fn too_many_macro_args_required_plus_two_optionals_arity_three() {
let mut e = Expander::new();
let err = e
.expand_program(
read("(defmacro f (a &optional b (c 5)) `(list ,a ,b ,c)) (f 1 2 3 4)").unwrap(),
)
.expect_err("surplus arg beyond required+two-optional must error");
let (macro_name, expected, got) = too_many_macro_args_fields(&err);
assert_eq!(macro_name, "f");
assert_eq!(expected, 3);
assert_eq!(got, 4);
}
#[test]
fn too_many_macro_args_does_not_fire_when_rest_is_present() {
let src = "(defmacro f (a &rest xs) `(list ,a ,@xs)) (f 1 2 3 4)";
let mut subst = Expander::new_substitute_only();
let mut bytecode = Expander::new();
let out_subst = subst.expand_program(read(src).unwrap()).unwrap();
let out_byte = bytecode.expand_program(read(src).unwrap()).unwrap();
assert_eq!(out_subst, out_byte);
assert_eq!(out_subst[0], parse("(list 1 2 3 4)"));
}
#[test]
fn too_many_macro_args_does_not_fire_at_exact_max_arity() {
let src = "(defmacro f (a &optional b) `(list ,a ,b)) (f 1 2)";
let mut e = Expander::new();
let out = e.expand_program(read(src).unwrap()).unwrap();
assert_eq!(out[0], parse("(list 1 2)"));
}
#[test]
fn too_many_macro_args_substitute_and_bytecode_paths_agree() {
let src = "(defmacro pair (a b) `(cons ,a ,b)) (pair 1 2 3)";
let mut subst = Expander::new_substitute_only();
let mut bytecode = Expander::new();
let err_subst = subst
.expand_program(read(src).unwrap())
.expect_err("substitute must error");
let err_byte = bytecode
.expand_program(read(src).unwrap())
.expect_err("bytecode must error");
assert_eq!(too_many_macro_args_fields(&err_subst), ("pair", 2, 3));
assert_eq!(too_many_macro_args_fields(&err_byte), ("pair", 2, 3));
}
#[test]
fn too_many_macro_args_fires_after_missing_required_priority_held() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f (a b c) `(list ,a ,b ,c)) (f 1)").unwrap())
.expect_err("missing required must error");
assert!(
matches!(err, LispError::MissingMacroArg { .. }),
"expected MissingMacroArg (priority), got: {err:?}"
);
}
#[test]
fn too_many_macro_args_zero_required_zero_optional_rejects_any_args() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f () `(list)) (f 1)").unwrap())
.expect_err("nullary macro called with arg must error");
let (macro_name, expected, got) = too_many_macro_args_fields(&err);
assert_eq!(macro_name, "f");
assert_eq!(expected, 0);
assert_eq!(got, 1);
}
#[test]
fn too_many_macro_args_display_renders_legacy_compile_substring() {
let err = LispError::TooManyMacroArgs {
macro_name: "pair".into(),
expected: 2,
got: 5,
};
assert_eq!(
err.to_string(),
"compile error in call to pair: too many args: expected at most 2, got 5"
);
}
#[test]
fn too_many_macro_args_position_is_none_today() {
let err = LispError::TooManyMacroArgs {
macro_name: "pair".into(),
expected: 2,
got: 3,
};
assert_eq!(err.position(), None);
}
fn non_symbol_param_fields(err: &LispError) -> (usize, &str) {
match err {
LispError::NonSymbolParam { position, got } => (*position, got.display.as_str()),
other => panic!("expected NonSymbolParam, got: {other:?}"),
}
}
#[test]
fn non_symbol_param_at_first_position_emits_structural_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f (5) `(list ,a))").unwrap())
.expect_err("non-symbol param must error");
let (position, got) = non_symbol_param_fields(&err);
assert_eq!(position, 0);
assert_eq!(got, "5");
}
#[test]
fn non_symbol_param_at_second_position_emits_structural_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f (a 5) `(,a))").unwrap())
.expect_err("non-symbol param must error");
let (position, got) = non_symbol_param_fields(&err);
assert_eq!(position, 1);
assert_eq!(got, "5");
}
#[test]
fn non_symbol_param_carries_keyword_value_unchanged() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f (:k) `(list))").unwrap())
.expect_err("non-symbol param must error");
let (position, got) = non_symbol_param_fields(&err);
assert_eq!(position, 0);
assert_eq!(got, ":k");
}
#[test]
fn non_symbol_param_carries_nested_list_value_unchanged() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f ((nested)) `(list))").unwrap())
.expect_err("non-symbol param must error");
let (position, got) = non_symbol_param_fields(&err);
assert_eq!(position, 0);
assert_eq!(got, "(nested)");
}
#[test]
fn non_symbol_param_in_defpoint_template_emits_same_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defpoint-template obs (5) `(defpoint))").unwrap())
.expect_err("non-symbol param must error");
let (position, got) = non_symbol_param_fields(&err);
assert_eq!(position, 0);
assert_eq!(got, "5");
}
#[test]
fn non_symbol_param_in_defcheck_emits_same_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defcheck pair (a 5) `(do))").unwrap())
.expect_err("non-symbol param must error");
let (position, got) = non_symbol_param_fields(&err);
assert_eq!(position, 1);
assert_eq!(got, "5");
}
#[test]
fn non_symbol_param_position_is_none_today() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f (5) `(list))").unwrap())
.expect_err("non-symbol param must error");
assert_eq!(err.position(), None);
}
#[test]
fn non_symbol_param_message_renders_legacy_substring_with_position() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f (a 5) `(,a))").unwrap())
.expect_err("non-symbol param must error");
assert_eq!(
format!("{err}"),
"compile error in defmacro params: \
expected symbol at position 1, got 5"
);
}
#[test]
fn non_symbol_param_substitute_and_bytecode_paths_agree() {
let src = "(defmacro f (a 5) `(,a))";
let mut subst = Expander::new_substitute_only();
let mut bytecode = Expander::new();
let err_subst = subst
.expand_program(read(src).unwrap())
.expect_err("substitute must error");
let err_byte = bytecode
.expand_program(read(src).unwrap())
.expect_err("bytecode must error");
assert_eq!(non_symbol_param_fields(&err_subst), (1, "5"));
assert_eq!(non_symbol_param_fields(&err_byte), (1, "5"));
}
fn rest_param_missing_name_fields(err: &LispError) -> (usize, Option<&str>) {
match err {
LispError::RestParamMissingName { rest_position, got } => {
(*rest_position, got.as_ref().map(|w| w.display.as_str()))
}
other => panic!("expected RestParamMissingName, got: {other:?}"),
}
}
#[test]
fn rest_param_missing_name_when_only_rest_emits_structural_variant_with_no_got() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f (&rest) `(list))").unwrap())
.expect_err("&rest with no follower must error");
let (rest_position, got) = rest_param_missing_name_fields(&err);
assert_eq!(rest_position, 0);
assert_eq!(got, None);
}
#[test]
fn rest_param_missing_name_at_end_of_param_list_emits_structural_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f (a &rest) `(,a))").unwrap())
.expect_err("&rest with no follower must error");
let (rest_position, got) = rest_param_missing_name_fields(&err);
assert_eq!(rest_position, 1);
assert_eq!(got, None);
}
#[test]
fn rest_param_missing_name_with_int_follower_emits_structural_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f (&rest 5) `(list))").unwrap())
.expect_err("&rest followed by non-symbol must error");
let (rest_position, got) = rest_param_missing_name_fields(&err);
assert_eq!(rest_position, 0);
assert_eq!(got, Some("5"));
}
#[test]
fn rest_param_missing_name_with_keyword_follower_emits_structural_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f (a &rest :foo) `(,a))").unwrap())
.expect_err("&rest followed by keyword must error");
let (rest_position, got) = rest_param_missing_name_fields(&err);
assert_eq!(rest_position, 1);
assert_eq!(got, Some(":foo"));
}
#[test]
fn rest_param_missing_name_with_nested_list_follower_emits_structural_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f (&rest (nested)) `(list))").unwrap())
.expect_err("&rest followed by list must error");
let (rest_position, got) = rest_param_missing_name_fields(&err);
assert_eq!(rest_position, 0);
assert_eq!(got, Some("(nested)"));
}
#[test]
fn rest_param_missing_name_in_defpoint_template_emits_same_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defpoint-template t (a &rest) `(,a))").unwrap())
.expect_err("&rest with no follower must error");
let (rest_position, got) = rest_param_missing_name_fields(&err);
assert_eq!(rest_position, 1);
assert_eq!(got, None);
}
#[test]
fn rest_param_missing_name_in_defcheck_emits_same_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defcheck c (&rest 5) `(list))").unwrap())
.expect_err("&rest followed by non-symbol must error");
let (rest_position, got) = rest_param_missing_name_fields(&err);
assert_eq!(rest_position, 0);
assert_eq!(got, Some("5"));
}
#[test]
fn rest_param_missing_name_substitute_and_bytecode_paths_agree() {
let src = "(defmacro f (a &rest 5) `(,a))";
let mut subst = Expander::new_substitute_only();
let mut bytecode = Expander::new();
let err_subst = subst
.expand_program(read(src).unwrap())
.expect_err("substitute must error");
let err_byte = bytecode
.expand_program(read(src).unwrap())
.expect_err("bytecode must error");
assert_eq!(rest_param_missing_name_fields(&err_subst), (1, Some("5")));
assert_eq!(rest_param_missing_name_fields(&err_byte), (1, Some("5")));
}
#[test]
fn rest_param_missing_name_message_renders_legacy_substring_with_marker() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f (a &rest 5) `(,a))").unwrap())
.expect_err("&rest followed by non-symbol must error");
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_message_renders_none_provided_when_follower_absent() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f (a &rest) `(,a))").unwrap())
.expect_err("&rest with no follower must error");
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_position_is_none_today() {
let err_missing = LispError::RestParamMissingName {
rest_position: 1,
got: None,
};
assert_eq!(err_missing.position(), None);
let err_got = LispError::RestParamMissingName {
rest_position: 0,
got: Some(crate::error::SexpWitness::new(
crate::error::SexpShape::Int,
"5",
)),
};
assert_eq!(err_got.position(), None);
}
fn rest_param_trailing_tokens_fields(err: &LispError) -> (usize, usize, &str) {
match err {
LispError::RestParamTrailingTokens {
rest_position,
extra,
first,
} => (*rest_position, *extra, first.display.as_str()),
other => panic!("expected RestParamTrailingTokens, got: {other:?}"),
}
}
#[test]
fn parse_params_rejects_single_trailing_token_after_rest_name() {
let err = parse_params(&read("a &rest c extra").unwrap())
.expect_err("a trailing token after the rest name must error");
assert_eq!(rest_param_trailing_tokens_fields(&err), (1, 1, "extra"));
}
#[test]
fn rest_param_trailing_tokens_counts_the_whole_trailing_run() {
let err = parse_params(&read("&rest c x y z").unwrap())
.expect_err("multiple trailing tokens must error");
assert_eq!(rest_param_trailing_tokens_fields(&err), (0, 3, "x"));
}
#[test]
fn rest_param_trailing_tokens_first_witness_carries_non_symbol_display() {
let err = parse_params(&read("a &rest c 5").unwrap())
.expect_err("a trailing non-symbol after the rest name must error");
assert_eq!(rest_param_trailing_tokens_fields(&err), (1, 1, "5"));
}
#[test]
fn rest_param_trailing_tokens_no_longer_silently_dropped_end_to_end() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f (a &rest xs extra) `(,a))").unwrap())
.expect_err("trailing token after &rest name must error");
assert_eq!(rest_param_trailing_tokens_fields(&err), (1, 1, "extra"));
}
#[test]
fn rest_param_trailing_tokens_substitute_and_bytecode_paths_agree() {
let src = "(defmacro f (a &rest xs extra) `(,a))";
let mut subst = Expander::new_substitute_only();
let mut bytecode = Expander::new();
let err_subst = subst
.expand_program(read(src).unwrap())
.expect_err("substitute must error");
let err_byte = bytecode
.expand_program(read(src).unwrap())
.expect_err("bytecode must error");
assert_eq!(
rest_param_trailing_tokens_fields(&err_subst),
(1, 1, "extra")
);
assert_eq!(
rest_param_trailing_tokens_fields(&err_byte),
(1, 1, "extra")
);
}
#[test]
fn rest_param_trailing_tokens_message_renders_legacy_style_prefix_and_suffix() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f (a &rest xs extra) `(,a))").unwrap())
.expect_err("trailing token after &rest name must error");
assert_eq!(
format!("{err}"),
"compile error in defmacro params: &rest name must be last \
(rest marker at position 1, 1 trailing after name, first: extra)"
);
}
#[test]
fn rest_param_trailing_tokens_position_is_none_today() {
let err = LispError::RestParamTrailingTokens {
rest_position: 1,
extra: 1,
first: crate::error::SexpWitness::new(crate::error::SexpShape::Symbol, "extra"),
};
assert_eq!(err.position(), None);
}
#[test]
fn macro_def_head_from_keyword_recognizes_defmacro() {
assert_eq!(
MacroDefHead::from_keyword("defmacro"),
Some(MacroDefHead::Defmacro)
);
}
#[test]
fn macro_def_head_from_keyword_recognizes_defpoint_template() {
assert_eq!(
MacroDefHead::from_keyword("defpoint-template"),
Some(MacroDefHead::DefpointTemplate)
);
}
#[test]
fn macro_def_head_from_keyword_recognizes_defcheck() {
assert_eq!(
MacroDefHead::from_keyword("defcheck"),
Some(MacroDefHead::Defcheck)
);
}
#[test]
fn macro_def_head_from_keyword_rejects_unknown() {
assert_eq!(MacroDefHead::from_keyword("if"), None);
assert_eq!(MacroDefHead::from_keyword("defmacroo"), None);
assert_eq!(MacroDefHead::from_keyword("defcheckk"), None);
assert_eq!(MacroDefHead::from_keyword(""), None);
}
#[test]
fn macro_def_head_keyword_round_trips_each_variant() {
let s_defmacro: &'static str = MacroDefHead::Defmacro.keyword();
let s_defpoint: &'static str = MacroDefHead::DefpointTemplate.keyword();
let s_defcheck: &'static str = MacroDefHead::Defcheck.keyword();
assert_eq!(s_defmacro, "defmacro");
assert_eq!(s_defpoint, "defpoint-template");
assert_eq!(s_defcheck, "defcheck");
}
#[test]
fn macro_def_head_keyword_round_trips_through_from_keyword() {
for kw in ["defmacro", "defpoint-template", "defcheck"] {
let head = MacroDefHead::from_keyword(kw).expect("canonical keyword must project");
assert_eq!(head.keyword(), kw);
}
}
#[test]
fn macro_def_head_threads_through_defmacro_arity_helper() {
for head in [
MacroDefHead::Defmacro,
MacroDefHead::DefpointTemplate,
MacroDefHead::Defcheck,
] {
let err = defmacro_arity(head, 2);
match err {
LispError::DefmacroArity { head: h, arity: 2 } => assert_eq!(h, head),
other => panic!("expected DefmacroArity, got: {other:?}"),
}
}
}
#[test]
fn macro_def_head_threads_through_defmacro_non_symbol_name_helper() {
let got = parse("5");
for head in [
MacroDefHead::Defmacro,
MacroDefHead::DefpointTemplate,
MacroDefHead::Defcheck,
] {
let err = defmacro_non_symbol_name(head, &got);
match err {
LispError::DefmacroNonSymbolName { head: h, got: g } => {
assert_eq!(h, head);
assert_eq!(g.shape, crate::error::SexpShape::Int);
assert_eq!(g.display, "5");
}
other => panic!("expected DefmacroNonSymbolName, got: {other:?}"),
}
}
}
#[test]
fn macro_def_head_threads_through_defmacro_non_list_params_helper() {
let got = parse("x");
for head in [
MacroDefHead::Defmacro,
MacroDefHead::DefpointTemplate,
MacroDefHead::Defcheck,
] {
let err = defmacro_non_list_params(head, &got);
match err {
LispError::DefmacroNonListParams { head: h, got: g } => {
assert_eq!(h, head);
assert_eq!(g.shape, crate::error::SexpShape::Symbol);
assert_eq!(g.display, "x");
}
other => panic!("expected DefmacroNonListParams, got: {other:?}"),
}
}
}
fn defmacro_arity_fields(err: &LispError) -> (MacroDefHead, usize) {
match err {
LispError::DefmacroArity { head, arity } => (*head, *arity),
other => panic!("expected DefmacroArity, got: {other:?}"),
}
}
#[test]
fn defmacro_arity_with_head_only_emits_structural_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro)").unwrap())
.expect_err("defmacro arity gate must error");
let (head, arity) = defmacro_arity_fields(&err);
assert_eq!(head, MacroDefHead::Defmacro);
assert_eq!(arity, 1);
}
#[test]
fn defmacro_arity_with_head_and_name_emits_structural_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f)").unwrap())
.expect_err("defmacro arity gate must error");
let (head, arity) = defmacro_arity_fields(&err);
assert_eq!(head, MacroDefHead::Defmacro);
assert_eq!(arity, 2);
}
#[test]
fn defmacro_arity_with_head_name_params_emits_structural_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f ())").unwrap())
.expect_err("defmacro arity gate must error");
let (head, arity) = defmacro_arity_fields(&err);
assert_eq!(head, MacroDefHead::Defmacro);
assert_eq!(arity, 3);
}
#[test]
fn defmacro_arity_in_defpoint_template_emits_same_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defpoint-template t)").unwrap())
.expect_err("defpoint-template arity gate must error");
let (head, arity) = defmacro_arity_fields(&err);
assert_eq!(head, MacroDefHead::DefpointTemplate);
assert_eq!(arity, 2);
}
#[test]
fn defmacro_arity_in_defcheck_emits_same_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defcheck)").unwrap())
.expect_err("defcheck arity gate must error");
let (head, arity) = defmacro_arity_fields(&err);
assert_eq!(head, MacroDefHead::Defcheck);
assert_eq!(arity, 1);
}
#[test]
fn defmacro_arity_substitute_and_bytecode_paths_agree() {
let src = "(defmacro f)";
let mut subst = Expander::new_substitute_only();
let mut bytecode = Expander::new();
let err_subst = subst
.expand_program(read(src).unwrap())
.expect_err("substitute must error");
let err_byte = bytecode
.expand_program(read(src).unwrap())
.expect_err("bytecode must error");
assert_eq!(
defmacro_arity_fields(&err_subst),
(MacroDefHead::Defmacro, 2)
);
assert_eq!(
defmacro_arity_fields(&err_byte),
(MacroDefHead::Defmacro, 2)
);
}
#[test]
fn defmacro_arity_message_renders_legacy_substring_with_arity() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f)").unwrap())
.expect_err("defmacro arity gate must error");
assert_eq!(
format!("{err}"),
"compile error in defmacro: (defmacro name (params) body) required \
(got 2 elements, need 4)"
);
}
#[test]
fn defmacro_arity_position_is_none_today() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro)").unwrap())
.expect_err("defmacro arity gate must error");
assert_eq!(err.position(), None);
}
#[test]
fn defmacro_arity_does_not_fire_for_well_formed_arity_4_defmacro() {
let mut e = Expander::new();
let out = e
.expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
.expect("well-formed defmacro must succeed");
assert_eq!(out[0], Sexp::int(42));
}
fn defmacro_non_symbol_name_fields(err: &LispError) -> (MacroDefHead, &str) {
match err {
LispError::DefmacroNonSymbolName { head, got } => (*head, got.display.as_str()),
other => panic!("expected DefmacroNonSymbolName, got: {other:?}"),
}
}
#[test]
fn defmacro_non_symbol_name_with_int_emits_structural_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro 5 () body)").unwrap())
.expect_err("defmacro non-symbol name gate must error");
let (head, got) = defmacro_non_symbol_name_fields(&err);
assert_eq!(head, MacroDefHead::Defmacro);
assert_eq!(got, "5");
}
#[test]
fn defmacro_non_symbol_name_with_keyword_emits_structural_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro :foo () body)").unwrap())
.expect_err("defmacro non-symbol name gate must error");
let (head, got) = defmacro_non_symbol_name_fields(&err);
assert_eq!(head, MacroDefHead::Defmacro);
assert_eq!(got, ":foo");
}
#[test]
fn defmacro_non_symbol_name_with_string_emits_structural_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro \"name\" () body)").unwrap())
.expect_err("defmacro non-symbol name gate must error");
let (head, got) = defmacro_non_symbol_name_fields(&err);
assert_eq!(head, MacroDefHead::Defmacro);
assert_eq!(got, "\"name\"");
}
#[test]
fn defmacro_non_symbol_name_with_nested_list_emits_structural_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro (nested) () body)").unwrap())
.expect_err("defmacro non-symbol name gate must error");
let (head, got) = defmacro_non_symbol_name_fields(&err);
assert_eq!(head, MacroDefHead::Defmacro);
assert_eq!(got, "(nested)");
}
#[test]
fn defmacro_non_symbol_name_in_defpoint_template_emits_same_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defpoint-template 7 () body)").unwrap())
.expect_err("defpoint-template non-symbol name gate must error");
let (head, got) = defmacro_non_symbol_name_fields(&err);
assert_eq!(head, MacroDefHead::DefpointTemplate);
assert_eq!(got, "7");
}
#[test]
fn defmacro_non_symbol_name_in_defcheck_emits_same_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defcheck :k () body)").unwrap())
.expect_err("defcheck non-symbol name gate must error");
let (head, got) = defmacro_non_symbol_name_fields(&err);
assert_eq!(head, MacroDefHead::Defcheck);
assert_eq!(got, ":k");
}
#[test]
fn defmacro_non_symbol_name_substitute_and_bytecode_paths_agree() {
let src = "(defmacro 5 () body)";
let mut subst = Expander::new_substitute_only();
let mut bytecode = Expander::new();
let err_subst = subst
.expand_program(read(src).unwrap())
.expect_err("substitute must error");
let err_byte = bytecode
.expand_program(read(src).unwrap())
.expect_err("bytecode must error");
assert_eq!(
defmacro_non_symbol_name_fields(&err_subst),
(MacroDefHead::Defmacro, "5")
);
assert_eq!(
defmacro_non_symbol_name_fields(&err_byte),
(MacroDefHead::Defmacro, "5")
);
}
#[test]
fn defmacro_non_symbol_name_message_renders_legacy_substring_with_got() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro 5 () body)").unwrap())
.expect_err("defmacro non-symbol name gate must error");
assert_eq!(
format!("{err}"),
"compile error in defmacro: expected name symbol, got 5"
);
}
#[test]
fn defmacro_non_symbol_name_position_is_none_today() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro 5 () body)").unwrap())
.expect_err("defmacro non-symbol name gate must error");
assert_eq!(err.position(), None);
}
#[test]
fn defmacro_non_symbol_name_does_not_fire_for_well_formed_defmacro() {
let mut e = Expander::new();
let out = e
.expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
.expect("well-formed defmacro must succeed");
assert_eq!(out[0], Sexp::int(42));
}
#[test]
fn defmacro_non_symbol_name_fires_after_arity_gate_passes() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro 5 () body)").unwrap())
.expect_err("name-symbol gate must error");
assert!(
matches!(err, LispError::DefmacroNonSymbolName { .. }),
"expected DefmacroNonSymbolName, got: {err:?}"
);
let err_arity = e
.expand_program(read("(defmacro 5)").unwrap())
.expect_err("arity gate must error");
assert!(
matches!(err_arity, LispError::DefmacroArity { .. }),
"expected DefmacroArity (arity < 4 short-circuits before name check), \
got: {err_arity:?}"
);
}
fn defmacro_non_list_params_fields(err: &LispError) -> (MacroDefHead, &str) {
match err {
LispError::DefmacroNonListParams { head, got } => (*head, got.display.as_str()),
other => panic!("expected DefmacroNonListParams, got: {other:?}"),
}
}
#[test]
fn defmacro_non_list_params_with_symbol_emits_structural_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f x body)").unwrap())
.expect_err("defmacro non-list params gate must error");
let (head, got) = defmacro_non_list_params_fields(&err);
assert_eq!(head, MacroDefHead::Defmacro);
assert_eq!(got, "x");
}
#[test]
fn defmacro_non_list_params_with_int_emits_structural_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f 5 body)").unwrap())
.expect_err("defmacro non-list params gate must error");
let (head, got) = defmacro_non_list_params_fields(&err);
assert_eq!(head, MacroDefHead::Defmacro);
assert_eq!(got, "5");
}
#[test]
fn defmacro_non_list_params_with_keyword_emits_structural_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f :foo body)").unwrap())
.expect_err("defmacro non-list params gate must error");
let (head, got) = defmacro_non_list_params_fields(&err);
assert_eq!(head, MacroDefHead::Defmacro);
assert_eq!(got, ":foo");
}
#[test]
fn defmacro_non_list_params_with_string_emits_structural_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f \"params\" body)").unwrap())
.expect_err("defmacro non-list params gate must error");
let (head, got) = defmacro_non_list_params_fields(&err);
assert_eq!(head, MacroDefHead::Defmacro);
assert_eq!(got, "\"params\"");
}
#[test]
fn defmacro_non_list_params_in_defpoint_template_emits_same_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defpoint-template t x body)").unwrap())
.expect_err("defpoint-template non-list params gate must error");
let (head, got) = defmacro_non_list_params_fields(&err);
assert_eq!(head, MacroDefHead::DefpointTemplate);
assert_eq!(got, "x");
}
#[test]
fn defmacro_non_list_params_in_defcheck_emits_same_variant() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defcheck c 7 body)").unwrap())
.expect_err("defcheck non-list params gate must error");
let (head, got) = defmacro_non_list_params_fields(&err);
assert_eq!(head, MacroDefHead::Defcheck);
assert_eq!(got, "7");
}
#[test]
fn defmacro_non_list_params_substitute_and_bytecode_paths_agree() {
let src = "(defmacro f x body)";
let mut subst = Expander::new_substitute_only();
let mut bytecode = Expander::new();
let err_subst = subst
.expand_program(read(src).unwrap())
.expect_err("substitute must error");
let err_byte = bytecode
.expand_program(read(src).unwrap())
.expect_err("bytecode must error");
assert_eq!(
defmacro_non_list_params_fields(&err_subst),
(MacroDefHead::Defmacro, "x")
);
assert_eq!(
defmacro_non_list_params_fields(&err_byte),
(MacroDefHead::Defmacro, "x")
);
}
#[test]
fn defmacro_non_list_params_message_renders_legacy_substring_with_got() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f x body)").unwrap())
.expect_err("defmacro non-list params gate must error");
assert_eq!(
format!("{err}"),
"compile error in defmacro: expected param list, got x"
);
}
#[test]
fn defmacro_non_list_params_position_is_none_today() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f x body)").unwrap())
.expect_err("defmacro non-list params gate must error");
assert_eq!(err.position(), None);
}
#[test]
fn defmacro_non_list_params_does_not_fire_for_well_formed_defmacro() {
let mut e = Expander::new();
let out = e
.expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
.expect("well-formed defmacro must succeed");
assert_eq!(out[0], Sexp::int(42));
}
#[test]
fn defmacro_non_list_params_fires_after_name_symbol_gate_passes() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f x body)").unwrap())
.expect_err("param-list gate must error");
assert!(
matches!(err, LispError::DefmacroNonListParams { .. }),
"expected DefmacroNonListParams, got: {err:?}"
);
let err_name = e
.expand_program(read("(defmacro 5 x body)").unwrap())
.expect_err("name-symbol gate must error");
assert!(
matches!(err_name, LispError::DefmacroNonSymbolName { .. }),
"expected DefmacroNonSymbolName (name-symbol gate short-circuits before param-list check), \
got: {err_name:?}"
);
}
#[test]
fn defmacro_non_list_params_fires_after_arity_gate_passes() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro f x body)").unwrap())
.expect_err("param-list gate must error");
assert!(
matches!(err, LispError::DefmacroNonListParams { .. }),
"expected DefmacroNonListParams, got: {err:?}"
);
let err_arity = e
.expand_program(read("(defmacro f x)").unwrap())
.expect_err("arity gate must error");
assert!(
matches!(err_arity, LispError::DefmacroArity { .. }),
"expected DefmacroArity (arity < 4 short-circuits before param-list check), \
got: {err_arity:?}"
);
}
#[test]
fn rest_marker_at_param_list_position_is_not_non_symbol_param() {
let mut e = Expander::new();
let out = e
.expand_program(read("(defmacro f (a &rest xs) `(list ,a ,@xs)) (f 1 2 3)").unwrap())
.expect("&rest with name must succeed");
assert_eq!(out[0], parse("(list 1 2 3)"));
}
#[test]
fn non_symbol_unquote_target_message_renders_canonical_type_mismatch_shape() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro w (x) `,(list 1 2)) (w 1)").unwrap())
.expect_err("non-symbol target must error");
assert_eq!(
format!("{err}"),
"compile error in ,: expected symbol, got (list 1 2)"
);
}
#[test]
fn template_invariant_violation_emits_structural_variant_with_macro_name_and_kind() {
let err = template_invariant_violation("test-macro", TemplateInvariantKind::FinalNoValue);
match err {
LispError::TemplateInvariant { macro_name, kind } => {
assert_eq!(macro_name, "test-macro");
assert_eq!(kind, TemplateInvariantKind::FinalNoValue);
}
other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
}
}
#[test]
fn template_invariant_violation_threads_subst_idx_through_typed_variant() {
let err = template_invariant_violation("wrap", TemplateInvariantKind::SubstBadIndex(7));
match err {
LispError::TemplateInvariant { macro_name, kind } => {
assert_eq!(macro_name, "wrap");
assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(7));
}
other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
}
}
#[test]
fn apply_compiled_subst_bad_idx_routes_through_template_invariant_violation() {
let tmpl = CompiledTemplate {
ops: vec![TemplateOp::Subst(99)],
};
let err = apply_compiled("test-macro", &MacroParams::default(), &tmpl, &[])
.expect_err("bad idx must error");
match err {
LispError::TemplateInvariant { macro_name, kind } => {
assert_eq!(macro_name, "test-macro");
assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(99));
}
other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
}
}
#[test]
fn apply_compiled_splice_bad_idx_routes_through_template_invariant_violation() {
let tmpl = CompiledTemplate {
ops: vec![TemplateOp::Splice(42)],
};
let err = apply_compiled("call-macro", &MacroParams::default(), &tmpl, &[])
.expect_err("bad splice idx must error");
match err {
LispError::TemplateInvariant { macro_name, kind } => {
assert_eq!(macro_name, "call-macro");
assert_eq!(kind, TemplateInvariantKind::SpliceBadIndex(42));
}
other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
}
}
#[test]
fn apply_compiled_subst_bad_idx_renders_legacy_compile_shape() {
let tmpl = CompiledTemplate {
ops: vec![TemplateOp::Subst(99)],
};
let err = apply_compiled("test-macro", &MacroParams::default(), &tmpl, &[])
.expect_err("bad idx must error");
assert_eq!(
format!("{err}"),
"compile error in test-macro: compiled template referenced bad param index 99"
);
}
#[test]
fn apply_compiled_splice_bad_idx_renders_legacy_compile_shape() {
let tmpl = CompiledTemplate {
ops: vec![TemplateOp::Splice(42)],
};
let err = apply_compiled("call-macro", &MacroParams::default(), &tmpl, &[])
.expect_err("bad splice idx must error");
assert_eq!(
format!("{err}"),
"compile error in call-macro: compiled template referenced bad splice index 42"
);
}
#[test]
fn apply_compiled_well_formed_template_routes_past_template_invariant_violation() {
let mut e = Expander::new();
let out = e
.expand_program(read("(defmacro id (x) `,x) (id 42)").unwrap())
.expect("well-formed macro expansion must not fire template-invariant-violation");
assert_eq!(out.len(), 1);
assert_eq!(out[0], Sexp::int(42));
}
#[test]
fn apply_compiled_missing_required_arg_does_not_route_through_template_invariant_violation() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro need-one (x) `,x) (need-one)").unwrap())
.expect_err("missing required arg must error");
assert!(
matches!(err, LispError::MissingMacroArg { .. }),
"expected MissingMacroArg, got: {err:?}"
);
}
#[test]
fn resolve_bound_arg_in_range_returns_borrowed_reference_verbatim() {
let args = vec![Sexp::int(1), Sexp::int(2), Sexp::int(3)];
let got = resolve_bound_arg(&args, 1, "m", |_| {
panic!("kind constructor must not fire on the in-range path")
})
.expect("in-range lookup must succeed");
assert!(
std::ptr::eq(got, &args[1]),
"resolve_bound_arg must return the SAME pointer as args_by_index.get(idx)"
);
assert_eq!(*got, Sexp::int(2));
}
#[test]
fn resolve_bound_arg_out_of_range_with_subst_kind_emits_typed_invariant() {
let args: Vec<Sexp> = Vec::new();
let err = resolve_bound_arg(&args, 7, "test-macro", TemplateInvariantKind::SubstBadIndex)
.expect_err("out-of-range lookup must error");
match err {
LispError::TemplateInvariant { macro_name, kind } => {
assert_eq!(macro_name, "test-macro");
assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(7));
}
other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
}
}
#[test]
fn resolve_bound_arg_threads_kind_constructor_per_call_site() {
let args: Vec<Sexp> = Vec::new();
let err = resolve_bound_arg(
&args,
7,
"test-macro",
TemplateInvariantKind::SpliceBadIndex,
)
.expect_err("out-of-range lookup must error");
match err {
LispError::TemplateInvariant { macro_name, kind } => {
assert_eq!(macro_name, "test-macro");
assert_eq!(kind, TemplateInvariantKind::SpliceBadIndex(7));
}
other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
}
}
#[test]
fn resolve_bound_arg_threads_macro_name_verbatim() {
let args: Vec<Sexp> = Vec::new();
for name in ["wrap", "call-macro", "obs"] {
let err = resolve_bound_arg(&args, 0, name, TemplateInvariantKind::SubstBadIndex)
.expect_err("out-of-range lookup must error");
match err {
LispError::TemplateInvariant { macro_name, kind } => {
assert_eq!(macro_name, name, "macro_name slot drifted for {name}");
assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(0));
}
other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
}
}
}
#[test]
fn resolve_bound_arg_yields_first_element_when_idx_is_zero() {
let args = vec![Sexp::int(42)];
let got = resolve_bound_arg(&args, 0, "m", |_| {
panic!("kind constructor must not fire on the in-range path")
})
.expect("idx-0 lookup must succeed");
assert!(std::ptr::eq(got, &args[0]));
assert_eq!(*got, Sexp::int(42));
}
#[test]
fn resolve_bound_arg_yields_last_element_at_exact_upper_bound() {
let args = vec![Sexp::int(1), Sexp::int(2), Sexp::int(3)];
let got = resolve_bound_arg(&args, args.len() - 1, "m", |_| {
panic!("kind constructor must not fire on the in-range path")
})
.expect("last-element lookup must succeed");
assert!(std::ptr::eq(got, args.last().unwrap()));
assert_eq!(*got, Sexp::int(3));
}
#[test]
fn resolve_bound_arg_at_exact_length_routes_to_error_arm() {
let args = vec![Sexp::int(1)];
let err = resolve_bound_arg(&args, 1, "m", TemplateInvariantKind::SubstBadIndex)
.expect_err("idx == len must error");
match err {
LispError::TemplateInvariant { kind, .. } => {
assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(1));
}
other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
}
}
#[test]
fn resolve_bound_arg_empty_slice_with_any_idx_routes_to_error_arm() {
let args: Vec<Sexp> = Vec::new();
let err = resolve_bound_arg(&args, 0, "zero-arity", TemplateInvariantKind::SubstBadIndex)
.expect_err("empty slice rejects every idx");
match err {
LispError::TemplateInvariant { macro_name, kind } => {
assert_eq!(macro_name, "zero-arity");
assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(0));
}
other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
}
}
#[test]
fn apply_compiled_subst_bad_idx_routes_through_resolve_bound_arg_with_subst_kind() {
let tmpl = CompiledTemplate {
ops: vec![TemplateOp::Subst(99)],
};
let err = apply_compiled("test-macro", &MacroParams::default(), &tmpl, &[])
.expect_err("bad idx must error");
match err {
LispError::TemplateInvariant { macro_name, kind } => {
assert_eq!(macro_name, "test-macro");
assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(99));
}
other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
}
}
#[test]
fn apply_compiled_splice_bad_idx_routes_through_resolve_bound_arg_with_splice_kind() {
let tmpl = CompiledTemplate {
ops: vec![TemplateOp::Splice(42)],
};
let err = apply_compiled("call-macro", &MacroParams::default(), &tmpl, &[])
.expect_err("bad splice idx must error");
match err {
LispError::TemplateInvariant { macro_name, kind } => {
assert_eq!(macro_name, "call-macro");
assert_eq!(kind, TemplateInvariantKind::SpliceBadIndex(42));
}
other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
}
}
#[test]
fn apply_compiled_subst_in_range_routes_past_resolve_bound_arg_into_clone_and_push() {
let params = MacroParams {
required: vec!["x".into()],
optional: Vec::new(),
rest: None,
};
let tmpl = CompiledTemplate {
ops: vec![TemplateOp::Subst(0)],
};
let out = apply_compiled("id", ¶ms, &tmpl, &[Sexp::int(42)])
.expect("in-range Subst must succeed");
assert_eq!(out, Sexp::int(42));
}
#[test]
fn current_builder_mut_returns_the_top_frame_reference() {
let mut stack: Vec<Vec<Sexp>> = vec![Vec::new()];
current_builder_mut(&mut stack).push(Sexp::int(42));
assert_eq!(stack.len(), 1);
assert_eq!(stack[0], vec![Sexp::int(42)]);
}
#[test]
fn current_builder_mut_targets_the_topmost_frame_on_a_multi_frame_stack() {
let mut stack: Vec<Vec<Sexp>> = vec![
vec![Sexp::symbol("outer")],
vec![Sexp::symbol("inner-a")],
vec![Sexp::symbol("inner-b")],
];
current_builder_mut(&mut stack).push(Sexp::int(99));
assert_eq!(stack[0], vec![Sexp::symbol("outer")]);
assert_eq!(stack[1], vec![Sexp::symbol("inner-a")]);
assert_eq!(stack[2], vec![Sexp::symbol("inner-b"), Sexp::int(99)]);
}
#[test]
fn current_builder_mut_is_pointer_equal_to_last_mut_unwrap() {
let mut stack: Vec<Vec<Sexp>> = vec![vec![Sexp::int(1), Sexp::int(2)]];
let via_lift_ptr = current_builder_mut(&mut stack).as_ptr();
let via_inline_ptr = stack.last_mut().unwrap().as_ptr();
assert!(
std::ptr::eq(via_lift_ptr, via_inline_ptr),
"current_builder_mut must borrow the SAME frame as stack.last_mut().unwrap()"
);
}
#[test]
#[should_panic(
expected = "bytecode-runtime invariant: at least one stack frame during op-loop"
)]
fn current_builder_mut_panics_with_named_invariant_on_empty_stack() {
let mut empty: Vec<Vec<Sexp>> = Vec::new();
let _ = current_builder_mut(&mut empty);
}
#[test]
fn current_builder_mut_routes_apply_compiled_literal_emit() {
let tmpl = CompiledTemplate {
ops: vec![TemplateOp::Literal(Sexp::symbol("hello"))],
};
let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
.expect("literal-only template must succeed");
assert_eq!(out, Sexp::symbol("hello"));
}
#[test]
fn current_builder_mut_routes_apply_compiled_end_list_parent_fold() {
let tmpl = CompiledTemplate {
ops: vec![
TemplateOp::BeginList,
TemplateOp::Literal(Sexp::symbol("a")),
TemplateOp::Literal(Sexp::symbol("b")),
TemplateOp::EndList,
],
};
let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
.expect("BeginList/EndList template must succeed");
assert_eq!(out, Sexp::List(vec![Sexp::symbol("a"), Sexp::symbol("b")]));
}
#[test]
fn current_builder_mut_routes_apply_compiled_subst_and_splice_emits() {
let params = MacroParams {
required: vec!["f".into()],
optional: Vec::new(),
rest: Some("args".into()),
};
let tmpl = CompiledTemplate {
ops: vec![
TemplateOp::BeginList,
TemplateOp::Subst(0),
TemplateOp::Splice(1),
TemplateOp::EndList,
],
};
let out = apply_compiled(
"call",
¶ms,
&tmpl,
&[
Sexp::symbol("foo"),
Sexp::int(1),
Sexp::int(2),
Sexp::int(3),
],
)
.expect("Subst + Splice template must succeed");
assert_eq!(
out,
Sexp::List(vec![
Sexp::symbol("foo"),
Sexp::int(1),
Sexp::int(2),
Sexp::int(3),
])
);
}
#[test]
fn apply_compiled_splice_in_range_routes_past_resolve_bound_arg_into_splice_value_into() {
let params = MacroParams {
required: vec!["f".into()],
optional: Vec::new(),
rest: Some("args".into()),
};
let tmpl = CompiledTemplate {
ops: vec![
TemplateOp::BeginList,
TemplateOp::Subst(0),
TemplateOp::Splice(1),
TemplateOp::EndList,
],
};
let out = apply_compiled(
"call",
¶ms,
&tmpl,
&[Sexp::symbol("foo"), Sexp::int(1), Sexp::int(2)],
)
.expect("in-range Splice must succeed");
assert_eq!(
out,
Sexp::List(vec![Sexp::symbol("foo"), Sexp::int(1), Sexp::int(2)])
);
}
#[test]
fn pop_builder_frame_pops_top_frame_off_non_empty_stack() {
let mut stack: Vec<Vec<Sexp>> = vec![
vec![Sexp::symbol("outer")],
vec![Sexp::int(1), Sexp::int(2)],
];
let popped =
pop_builder_frame(&mut stack, "wrap", TemplateInvariantKind::EndListEmptyStack)
.expect("non-empty stack must pop cleanly");
assert_eq!(popped, vec![Sexp::int(1), Sexp::int(2)]);
assert_eq!(stack.len(), 1);
assert_eq!(stack[0], vec![Sexp::symbol("outer")]);
}
#[test]
fn pop_builder_frame_emits_template_invariant_with_end_list_empty_stack_kind() {
let mut empty: Vec<Vec<Sexp>> = Vec::new();
let err = pop_builder_frame(&mut empty, "wrap", TemplateInvariantKind::EndListEmptyStack)
.expect_err("empty stack must reject");
match err {
LispError::TemplateInvariant { macro_name, kind } => {
assert_eq!(macro_name, "wrap");
assert_eq!(kind, TemplateInvariantKind::EndListEmptyStack);
}
other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
}
}
#[test]
fn pop_builder_frame_emits_template_invariant_with_final_no_value_kind() {
let mut empty: Vec<Vec<Sexp>> = Vec::new();
let err = pop_builder_frame(&mut empty, "id", TemplateInvariantKind::FinalNoValue)
.expect_err("empty stack must reject");
match err {
LispError::TemplateInvariant { macro_name, kind } => {
assert_eq!(macro_name, "id");
assert_eq!(kind, TemplateInvariantKind::FinalNoValue);
}
other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
}
}
#[test]
fn pop_builder_frame_threads_macro_name_through_variant_for_indexed_kinds() {
let mut empty: Vec<Vec<Sexp>> = Vec::new();
let err = pop_builder_frame(
&mut empty,
"compose",
TemplateInvariantKind::SubstBadIndex(42),
)
.expect_err("empty stack must reject regardless of kind family");
match err {
LispError::TemplateInvariant { macro_name, kind } => {
assert_eq!(macro_name, "compose");
assert_eq!(kind, TemplateInvariantKind::SubstBadIndex(42));
}
other => panic!("expected LispError::TemplateInvariant, got {other:?}"),
}
}
#[test]
fn pop_builder_frame_is_byte_identical_to_inline_pop_then_template_invariant_violation() {
let mut stack_lift: Vec<Vec<Sexp>> = vec![vec![Sexp::symbol("a")], vec![Sexp::int(7)]];
let mut stack_inline: Vec<Vec<Sexp>> = vec![vec![Sexp::symbol("a")], vec![Sexp::int(7)]];
let via_lift = pop_builder_frame(
&mut stack_lift,
"macro",
TemplateInvariantKind::EndListEmptyStack,
)
.expect("non-empty stack pops cleanly through lift");
let via_inline = stack_inline.pop().ok_or_else(|| {
template_invariant_violation("macro", TemplateInvariantKind::EndListEmptyStack)
});
assert_eq!(
via_lift,
via_inline.unwrap(),
"popped frame must be byte-identical across lift vs inline"
);
assert_eq!(
stack_lift.len(),
stack_inline.len(),
"post-pop stack length must be byte-identical across lift vs inline"
);
let mut empty_lift: Vec<Vec<Sexp>> = Vec::new();
let mut empty_inline: Vec<Vec<Sexp>> = Vec::new();
let err_lift = pop_builder_frame(
&mut empty_lift,
"macro",
TemplateInvariantKind::FinalNoValue,
)
.expect_err("empty stack rejects through lift");
let err_inline = empty_inline
.pop()
.ok_or_else(|| {
template_invariant_violation("macro", TemplateInvariantKind::FinalNoValue)
})
.expect_err("empty stack rejects through inline");
match (err_lift, err_inline) {
(
LispError::TemplateInvariant {
macro_name: m_lift,
kind: k_lift,
},
LispError::TemplateInvariant {
macro_name: m_inline,
kind: k_inline,
},
) => {
assert_eq!(m_lift, m_inline);
assert_eq!(k_lift, k_inline);
}
(l, i) => panic!(
"expected LispError::TemplateInvariant on both arms, got lift={l:?}, inline={i:?}"
),
}
}
#[test]
fn pop_builder_frame_routes_apply_compiled_end_list_consume() {
let tmpl = CompiledTemplate {
ops: vec![
TemplateOp::BeginList,
TemplateOp::Literal(Sexp::symbol("only")),
TemplateOp::EndList,
],
};
let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
.expect("BeginList/EndList one-literal template must succeed");
assert_eq!(out, Sexp::List(vec![Sexp::symbol("only")]));
}
#[test]
fn pop_builder_frame_routes_apply_compiled_final_pop_consume() {
let tmpl = CompiledTemplate {
ops: vec![TemplateOp::Literal(Sexp::int(123))],
};
let out = apply_compiled("id", &MacroParams::default(), &tmpl, &[])
.expect("literal-only template must succeed");
assert_eq!(out, Sexp::int(123));
}
#[test]
fn parse_params_maps_required_then_rest_into_typed_shape() {
let params = parse_params(&read("a b &rest c").unwrap()).unwrap();
assert_eq!(
params,
MacroParams {
required: vec!["a".into(), "b".into()],
optional: Vec::new(),
rest: Some("c".into()),
}
);
}
#[test]
fn parse_params_rest_absent_leaves_none() {
let params = parse_params(&read("x y").unwrap()).unwrap();
assert_eq!(
params,
MacroParams {
required: vec!["x".into(), "y".into()],
optional: Vec::new(),
rest: None,
}
);
}
#[test]
fn parse_params_maps_optional_section_between_required_and_rest() {
let params = parse_params(&read("a &optional b c &rest d").unwrap()).unwrap();
assert_eq!(
params,
MacroParams {
required: vec!["a".into()],
optional: vec![OptionalParam::bare("b"), OptionalParam::bare("c")],
rest: Some("d".into()),
}
);
}
#[test]
fn parse_params_optional_with_no_rest_leaves_rest_none() {
let params = parse_params(&read("&optional x").unwrap()).unwrap();
assert_eq!(
params,
MacroParams {
required: Vec::new(),
optional: vec![OptionalParam::bare("x")],
rest: None,
}
);
}
#[test]
fn parse_params_rejects_repeated_optional_marker() {
let err = parse_params(&read("a &optional b &optional c").unwrap())
.expect_err("repeated &optional must error");
assert!(
matches!(
err,
LispError::OptionalMarkerRepeated {
first_position: 1,
second_position: 3,
}
),
"expected OptionalMarkerRepeated {{1, 3}}, got: {err:?}"
);
}
#[test]
fn parse_params_rejects_optional_after_rest_as_trailing_tokens() {
let err = parse_params(&read("&rest xs &optional y").unwrap())
.expect_err("tokens after &rest <name> must error");
assert!(
matches!(err, LispError::RestParamTrailingTokens { .. }),
"expected RestParamTrailingTokens, got: {err:?}"
);
}
#[test]
fn names_are_required_then_optional_then_rest_in_flat_index_order() {
let params = MacroParams {
required: vec!["a".into(), "b".into()],
optional: vec![OptionalParam::bare("c")],
rest: Some("d".into()),
};
assert_eq!(params.names(), vec!["a", "b", "c", "d"]);
assert_eq!(params.names()[params.required.len()], "c");
assert_eq!(params.names()[params.fixed_arity()], "d");
}
#[test]
fn macro_params_rest_marker_projects_canonical_ampersand_rest_str() {
assert_eq!(
MacroParams::REST_MARKER,
"&rest",
"MacroParams::REST_MARKER drifted from the substrate- \
canonical CL lambda-list `&rest` marker — the parser's \
`parse_params` rest-slot dispatch AND every downstream \
authoring / rendering surface binds to this ONE typed \
constant.",
);
}
#[test]
fn macro_params_optional_marker_projects_canonical_ampersand_optional_str() {
assert_eq!(
MacroParams::OPTIONAL_MARKER,
"&optional",
"MacroParams::OPTIONAL_MARKER drifted from the substrate- \
canonical CL lambda-list `&optional` marker — the parser's \
`parse_params` optional-section dispatch AND every \
downstream authoring / rendering surface binds to this \
ONE typed constant.",
);
}
#[test]
fn macro_params_lambda_list_keyword_lead_projects_canonical_ampersand_char() {
assert_eq!(
MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
'&',
"LAMBDA_LIST_KEYWORD_LEAD char drifted from the substrate- \
canonical `&` LEAD byte — the CL lambda-list-keyword \
family (REST_MARKER, OPTIONAL_MARKER) shares this ONE \
typed constant as their common LEAD byte.",
);
}
#[test]
fn macro_params_rest_marker_prefixed_by_lambda_list_keyword_lead() {
assert!(
MacroParams::REST_MARKER.starts_with(MacroParams::LAMBDA_LIST_KEYWORD_LEAD),
"MacroParams::REST_MARKER `{}` does NOT start with \
MacroParams::LAMBDA_LIST_KEYWORD_LEAD `{:?}` — the two \
typed constants have drifted apart on the [`MacroParams`] \
algebra; the CL lambda-list-keyword family disjointness \
contract can no longer bind to ONE shared LEAD byte.",
MacroParams::REST_MARKER,
MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
);
}
#[test]
fn macro_params_optional_marker_prefixed_by_lambda_list_keyword_lead() {
assert!(
MacroParams::OPTIONAL_MARKER.starts_with(MacroParams::LAMBDA_LIST_KEYWORD_LEAD),
"MacroParams::OPTIONAL_MARKER `{}` does NOT start with \
MacroParams::LAMBDA_LIST_KEYWORD_LEAD `{:?}` — the two \
typed constants have drifted apart on the [`MacroParams`] \
algebra; the CL lambda-list-keyword family disjointness \
contract can no longer bind to ONE shared LEAD byte.",
MacroParams::OPTIONAL_MARKER,
MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
);
}
#[test]
fn macro_params_rest_and_optional_markers_pairwise_disjoint() {
assert_ne!(
MacroParams::REST_MARKER,
MacroParams::OPTIONAL_MARKER,
"REST_MARKER and OPTIONAL_MARKER collide — the parser's \
typed dispatch cascade at `parse_params` can no longer \
distinguish the rest-slot boundary from the optional- \
section boundary.",
);
}
#[test]
fn macro_params_lambda_list_keyword_lead_distinct_from_every_other_algebra_marker() {
use crate::ast::{Atom, QuoteForm, Sexp};
assert_ne!(
MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
Atom::STR_DELIMITER,
"LAMBDA_LIST_KEYWORD_LEAD collides with STR_DELIMITER — a \
bare `&rest` at a param-list position would ambiguously \
begin a lambda-list keyword AND open a string.",
);
assert_ne!(
MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
Atom::STR_ESCAPE_LEAD,
"LAMBDA_LIST_KEYWORD_LEAD collides with STR_ESCAPE_LEAD — \
the reader's Str-escape lead byte would alias the CL \
lambda-list-keyword LEAD byte.",
);
assert_ne!(
MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
Atom::KEYWORD_MARKER_LEAD,
"LAMBDA_LIST_KEYWORD_LEAD collides with KEYWORD_MARKER_LEAD \
— a bare `&rest` at a param-list position would \
ambiguously begin a lambda-list keyword AND begin an \
`:foo` keyword.",
);
assert_ne!(
MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
Atom::BOOL_LITERAL_LEAD,
"LAMBDA_LIST_KEYWORD_LEAD collides with BOOL_LITERAL_LEAD — \
a bare `&rest` at a param-list position would ambiguously \
begin a lambda-list keyword AND classify as a Bool prefix.",
);
assert_ne!(
MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
Sexp::LIST_OPEN,
"LAMBDA_LIST_KEYWORD_LEAD collides with LIST_OPEN — a bare \
`&rest` at a param-list position would ambiguously begin \
a lambda-list keyword AND open a list.",
);
assert_ne!(
MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
Sexp::LIST_CLOSE,
"LAMBDA_LIST_KEYWORD_LEAD collides with LIST_CLOSE — a bare \
`&rest` at a param-list position would ambiguously begin \
a lambda-list keyword AND close a list.",
);
assert_ne!(
MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
Sexp::COMMENT_LEAD,
"LAMBDA_LIST_KEYWORD_LEAD collides with COMMENT_LEAD — a \
bare `&rest` at a param-list position would ambiguously \
begin a lambda-list keyword AND begin a comment.",
);
assert_ne!(
MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
Sexp::COMMENT_TERM,
"LAMBDA_LIST_KEYWORD_LEAD collides with COMMENT_TERM — the \
reader's line-comment discard loop would terminate on the \
SAME byte the parser's lambda-list-keyword LEAD dispatch \
binds to.",
);
for qf in QuoteForm::ALL {
assert_ne!(
MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
qf.lead_char(),
"LAMBDA_LIST_KEYWORD_LEAD collides with \
QuoteForm::{qf:?}'s lead_char — a bare `&rest` at a \
param-list position would ambiguously begin a lambda- \
list keyword AND begin a quote-family prefix.",
);
}
assert_ne!(
MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
QuoteForm::SPLICE_DISCRIMINATOR,
"LAMBDA_LIST_KEYWORD_LEAD collides with \
SPLICE_DISCRIMINATOR — the reader's `,@` splice-promotion \
peek byte would alias the CL lambda-list-keyword LEAD \
byte.",
);
}
#[test]
fn parse_params_recognizes_rest_marker_via_typed_constant() {
let src = format!("a {} xs", MacroParams::REST_MARKER);
let params = parse_params(&read(&src).unwrap()).unwrap();
assert_eq!(
params,
MacroParams {
required: vec!["a".into()],
optional: Vec::new(),
rest: Some("xs".into()),
},
"parse_params dispatch drifted away from REST_MARKER — the \
typed constant no longer routes to the rest-slot arm.",
);
}
#[test]
fn parse_params_recognizes_optional_marker_via_typed_constant() {
let src = format!("a {} b c", MacroParams::OPTIONAL_MARKER);
let params = parse_params(&read(&src).unwrap()).unwrap();
assert_eq!(
params,
MacroParams {
required: vec!["a".into()],
optional: vec![OptionalParam::bare("b"), OptionalParam::bare("c")],
rest: None,
},
"parse_params dispatch drifted away from OPTIONAL_MARKER — \
the typed constant no longer routes to the optional- \
section arm.",
);
}
#[test]
fn macro_params_lambda_list_keywords_has_expected_cardinality() {
assert_eq!(
MacroParams::LAMBDA_LIST_KEYWORDS.len(),
2,
"LAMBDA_LIST_KEYWORDS cardinality drifted from 2 — the CL \
lambda-list-keyword family closure now names a different \
number of markers than the two the parser's typed dispatch \
specialises on.",
);
}
#[test]
fn macro_params_lambda_list_keywords_binds_per_role_markers_by_index() {
assert_eq!(
MacroParams::LAMBDA_LIST_KEYWORDS[0],
MacroParams::REST_MARKER,
"LAMBDA_LIST_KEYWORDS[0] drifted from REST_MARKER — the ALL \
array's declaration-order binding to the per-role `pub \
const` broke at the rest-slot marker slot.",
);
assert_eq!(
MacroParams::LAMBDA_LIST_KEYWORDS[1],
MacroParams::OPTIONAL_MARKER,
"LAMBDA_LIST_KEYWORDS[1] drifted from OPTIONAL_MARKER — the \
ALL array's declaration-order binding to the per-role \
`pub const` broke at the optional-section marker slot.",
);
}
#[test]
fn macro_params_every_lambda_list_keyword_prefixed_by_lambda_list_keyword_lead() {
for m in MacroParams::LAMBDA_LIST_KEYWORDS {
assert!(
m.starts_with(MacroParams::LAMBDA_LIST_KEYWORD_LEAD),
"LAMBDA_LIST_KEYWORDS element `{m}` does NOT start with \
LAMBDA_LIST_KEYWORD_LEAD `{lead:?}` — the CL lambda- \
list-keyword family's structural round-trip contract \
no longer binds every marker to its shared LEAD byte.",
lead = MacroParams::LAMBDA_LIST_KEYWORD_LEAD,
);
}
}
#[test]
fn macro_params_lambda_list_keywords_pairwise_distinct() {
for (i, a) in MacroParams::LAMBDA_LIST_KEYWORDS.iter().enumerate() {
for (j, b) in MacroParams::LAMBDA_LIST_KEYWORDS.iter().enumerate() {
if i == j {
continue;
}
assert_ne!(
a, b,
"LAMBDA_LIST_KEYWORDS[{i}] `{a}` collides with \
LAMBDA_LIST_KEYWORDS[{j}] `{b}` — the CL lambda- \
list-keyword family's pairwise disjointness \
contract no longer binds distinct index pairs \
to distinct markers.",
);
}
}
}
#[test]
fn macro_params_is_lambda_list_keyword_accepts_every_marker() {
for m in MacroParams::LAMBDA_LIST_KEYWORDS {
assert!(
MacroParams::is_lambda_list_keyword(m),
"is_lambda_list_keyword rejected LAMBDA_LIST_KEYWORDS \
element `{m}` — the closed-set membership gate's \
acceptance side drifted from the ALL array.",
);
}
}
#[test]
fn macro_params_is_lambda_list_keyword_rejects_bare_lead_byte() {
let bare_lead: String = MacroParams::LAMBDA_LIST_KEYWORD_LEAD.to_string();
assert!(
!MacroParams::is_lambda_list_keyword(&bare_lead),
"is_lambda_list_keyword accepted the bare LEAD byte `{bare_lead}` — \
the closed-set membership gate silently classifies the bare `&` \
LEAD byte as a recognised CL lambda-list keyword despite the ALL \
array containing only the two suffixed markers.",
);
}
#[test]
fn macro_params_is_lambda_list_keyword_rejects_unrecognised_ampersand_prefixed_names() {
for candidate in ["&key", "&aux", "&body"] {
assert!(
!MacroParams::is_lambda_list_keyword(candidate),
"is_lambda_list_keyword accepted the unrecognised \
`&`-prefixed name `{candidate}` — the closed-set \
membership gate silently loosened its acceptance beyond \
the two markers the ALL array names today.",
);
}
}
#[test]
fn macro_params_is_lambda_list_keyword_rejects_bare_identifiers_and_empty_string() {
for candidate in ["a", "xs", "foo", ""] {
assert!(
!MacroParams::is_lambda_list_keyword(candidate),
"is_lambda_list_keyword accepted the bare non-marker \
input `{candidate}` — the closed-set membership gate's \
rejection side no longer excludes bare identifiers \
the parser routes through the fall-through cascade.",
);
}
}
#[test]
fn fixed_arity_is_zero_for_the_empty_param_list() {
let params = MacroParams::default();
assert_eq!(params.fixed_arity(), 0);
}
#[test]
fn fixed_arity_counts_required_only_when_no_optional_or_rest() {
let params = MacroParams {
required: vec!["a".into(), "b".into(), "c".into()],
optional: Vec::new(),
rest: None,
};
assert_eq!(params.fixed_arity(), 3);
}
#[test]
fn fixed_arity_counts_optional_only_when_no_required_or_rest() {
let params = MacroParams {
required: Vec::new(),
optional: vec![OptionalParam::bare("x"), OptionalParam::bare("y")],
rest: None,
};
assert_eq!(params.fixed_arity(), 2);
}
#[test]
fn fixed_arity_sums_required_and_optional_in_canonical_lambda_order() {
let params = MacroParams {
required: vec!["a".into(), "b".into()],
optional: vec![
OptionalParam::bare("c"),
OptionalParam::bare("d"),
OptionalParam::bare("e"),
],
rest: None,
};
assert_eq!(params.fixed_arity(), 5);
}
#[test]
fn fixed_arity_ignores_rest_slot_by_construction() {
let with_rest = MacroParams {
required: vec!["a".into()],
optional: vec![OptionalParam::bare("b")],
rest: Some("r".into()),
};
let without_rest = MacroParams {
required: vec!["a".into()],
optional: vec![OptionalParam::bare("b")],
rest: None,
};
assert_eq!(with_rest.fixed_arity(), without_rest.fixed_arity());
assert_eq!(with_rest.fixed_arity(), 2);
}
#[test]
fn fixed_arity_is_the_rest_start_index_in_names_when_rest_present() {
let params = MacroParams {
required: vec!["a".into(), "b".into()],
optional: vec![OptionalParam::bare("c")],
rest: Some("r".into()),
};
assert_eq!(params.fixed_arity(), 3);
assert_eq!(params.names()[params.fixed_arity()], "r");
}
#[test]
fn fixed_arity_equals_names_length_when_rest_is_absent() {
let params = MacroParams {
required: vec!["a".into(), "b".into()],
optional: vec![OptionalParam::bare("c")],
rest: None,
};
assert_eq!(params.names().len(), params.fixed_arity());
assert_eq!(params.names().len(), 3);
}
#[test]
fn fixed_arity_is_the_rest_less_surplus_rejection_boundary() {
let params = MacroParams {
required: vec!["a".into(), "b".into()],
optional: vec![OptionalParam::bare("c")],
rest: None,
};
assert_eq!(params.fixed_arity(), 3);
let err = params
.bind(
"m",
&[Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)],
)
.expect_err("4 args against fixed_arity 3 must reject");
match err {
LispError::TooManyMacroArgs {
expected,
got,
macro_name,
} => {
assert_eq!(expected, params.fixed_arity());
assert_eq!(got, 4);
assert_eq!(macro_name, "m");
}
other => panic!("expected TooManyMacroArgs, got {other:?}"),
}
}
#[test]
fn fixed_arity_is_the_rest_start_index_consumed_by_bind() {
let params = MacroParams {
required: vec!["a".into()],
optional: vec![OptionalParam::bare("b")],
rest: Some("r".into()),
};
assert_eq!(params.fixed_arity(), 2);
let args = [Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)];
let vals = params.bind("m", &args).unwrap();
let rest_expected: Vec<Sexp> = args[params.fixed_arity()..].to_vec();
assert_eq!(vals.last().unwrap(), &Sexp::List(rest_expected));
}
#[test]
fn bind_rest_present_at_exact_fixed_arity_yields_empty_rest_list() {
let params = MacroParams {
required: vec!["a".into()],
optional: vec![OptionalParam::bare("b")],
rest: Some("r".into()),
};
assert_eq!(params.fixed_arity(), 2);
let vals = params
.bind("m", &[Sexp::int(1), Sexp::int(2)])
.expect("rest-present at exact fixed_arity must bind cleanly");
assert_eq!(vals, vec![Sexp::int(1), Sexp::int(2), Sexp::List(vec![])]);
}
#[test]
fn bind_threads_required_positionally_and_collects_rest_as_list() {
let params = MacroParams {
required: vec!["a".into(), "b".into()],
optional: Vec::new(),
rest: Some("c".into()),
};
let vals = params
.bind(
"m",
&[Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)],
)
.unwrap();
assert_eq!(
vals,
vec![
Sexp::int(1),
Sexp::int(2),
Sexp::List(vec![Sexp::int(3), Sexp::int(4)]),
]
);
}
#[test]
fn bind_supplied_optional_takes_its_positional_arg() {
let params = MacroParams {
required: vec!["a".into()],
optional: vec![OptionalParam::bare("b")],
rest: None,
};
let vals = params.bind("m", &[Sexp::int(1), Sexp::int(2)]).unwrap();
assert_eq!(vals, vec![Sexp::int(1), Sexp::int(2)]);
}
#[test]
fn bind_unsupplied_optional_defaults_to_nil() {
let params = MacroParams {
required: vec!["a".into()],
optional: vec![OptionalParam::bare("b"), OptionalParam::bare("c")],
rest: None,
};
let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
assert_eq!(vals, vec![Sexp::int(1), Sexp::Nil, Sexp::Nil]);
}
#[test]
fn bind_rest_collects_args_beyond_required_and_optional() {
let params = MacroParams {
required: vec!["a".into()],
optional: vec![OptionalParam::bare("b")],
rest: Some("c".into()),
};
let vals = params
.bind(
"m",
&[Sexp::int(1), Sexp::int(2), Sexp::int(3), Sexp::int(4)],
)
.unwrap();
assert_eq!(
vals,
vec![
Sexp::int(1),
Sexp::int(2),
Sexp::List(vec![Sexp::int(3), Sexp::int(4)]),
]
);
}
#[test]
fn bind_unsupplied_optional_then_empty_rest() {
let params = MacroParams {
required: vec!["a".into()],
optional: vec![OptionalParam::bare("b")],
rest: Some("c".into()),
};
let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
assert_eq!(vals, vec![Sexp::int(1), Sexp::Nil, Sexp::List(vec![])]);
}
#[test]
fn bind_rest_with_no_remaining_args_is_the_empty_list() {
let params = MacroParams {
required: vec!["a".into()],
optional: Vec::new(),
rest: Some("c".into()),
};
let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
assert_eq!(vals, vec![Sexp::int(1), Sexp::List(vec![])]);
}
#[test]
fn bind_missing_required_errors_before_any_rest_collection() {
let params = MacroParams {
required: vec!["a".into(), "b".into()],
optional: Vec::new(),
rest: Some("c".into()),
};
let err = params
.bind("m", &[Sexp::int(1)])
.expect_err("missing required `b` must error");
assert!(
matches!(err, LispError::MissingMacroArg { .. }),
"expected MissingMacroArg, got: {err:?}"
);
}
#[test]
fn bind_missing_required_errors_even_with_optional_present() {
let params = MacroParams {
required: vec!["a".into(), "b".into()],
optional: vec![OptionalParam::bare("c")],
rest: None,
};
let err = params
.bind("m", &[Sexp::int(1)])
.expect_err("missing required `b` must error before optional defaulting");
assert!(
matches!(err, LispError::MissingMacroArg { .. }),
"expected MissingMacroArg, got: {err:?}"
);
}
#[test]
fn bind_rest_less_params_reject_surplus_args() {
let params = MacroParams {
required: vec!["a".into()],
optional: Vec::new(),
rest: None,
};
let err = params
.bind("m", &[Sexp::int(1), Sexp::int(2)])
.expect_err("rest-less surplus must error");
match err {
LispError::TooManyMacroArgs {
macro_name,
expected,
got,
} => {
assert_eq!(macro_name, "m");
assert_eq!(expected, 1);
assert_eq!(got, 2);
}
other => panic!("expected TooManyMacroArgs, got: {other:?}"),
}
}
#[test]
fn parse_params_admits_optional_list_spec_with_default() {
let params = parse_params(&read("a &optional (b 5)").unwrap()).unwrap();
assert_eq!(
params,
MacroParams {
required: vec!["a".into()],
optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
rest: None,
}
);
}
#[test]
fn parse_params_mixes_bare_and_list_optional_specs_side_by_side() {
let params =
parse_params(&read("a &optional b (c \"x\") d (e 9) &rest r").unwrap()).unwrap();
assert_eq!(
params,
MacroParams {
required: vec!["a".into()],
optional: vec![
OptionalParam::bare("b"),
OptionalParam::with_default("c", Sexp::string("x")),
OptionalParam::bare("d"),
OptionalParam::with_default("e", Sexp::int(9)),
],
rest: Some("r".into()),
}
);
assert_eq!(params.names(), vec!["a", "b", "c", "d", "e", "r"]);
}
#[test]
fn parse_params_admits_arbitrary_sexp_as_optional_default_form() {
let params = parse_params(&read("&optional (x (list 1 2))").unwrap()).unwrap();
let want_default = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
assert_eq!(
params,
MacroParams {
required: Vec::new(),
optional: vec![OptionalParam::with_default("x", want_default)],
rest: None,
}
);
}
#[test]
fn parse_params_rejects_empty_list_optional_spec() {
let err = parse_params(&read("&optional ()").unwrap())
.expect_err("empty list optional spec must error");
assert!(
matches!(
err,
LispError::OptionalParamMalformed {
position: 1,
reason: crate::error::OptionalParamMalformedReason::EmptyList,
..
}
),
"expected OptionalParamMalformed{{EmptyList, position: 1}}, got: {err:?}"
);
}
#[test]
fn parse_params_rejects_one_element_optional_list_as_missing_default() {
let err = parse_params(&read("&optional (x)").unwrap())
.expect_err("one-element list optional spec must error");
assert!(
matches!(
err,
LispError::OptionalParamMalformed {
position: 1,
reason: crate::error::OptionalParamMalformedReason::MissingDefault,
..
}
),
"expected OptionalParamMalformed{{MissingDefault, position: 1}}, got: {err:?}"
);
}
#[test]
fn parse_params_rejects_three_or_more_element_optional_list_as_extra_elements() {
let err = parse_params(&read("&optional (x 5 6)").unwrap())
.expect_err("three-element list optional spec must error");
assert!(
matches!(
err,
LispError::OptionalParamMalformed {
position: 1,
reason: crate::error::OptionalParamMalformedReason::ExtraElements { length: 3 },
..
}
),
"expected OptionalParamMalformed{{ExtraElements{{3}}, position: 1}}, got: {err:?}"
);
}
#[test]
fn parse_params_rejects_non_symbol_name_in_optional_list_spec() {
let err = parse_params(&read("&optional (5 default)").unwrap())
.expect_err("non-symbol-name optional spec must error");
assert!(
matches!(
err,
LispError::OptionalParamMalformed {
position: 1,
reason: crate::error::OptionalParamMalformedReason::NonSymbolName,
..
}
),
"expected OptionalParamMalformed{{NonSymbolName, position: 1}}, got: {err:?}"
);
}
#[test]
fn parse_params_rejects_list_in_required_section_as_non_symbol_param() {
let err =
parse_params(&read("(a 5)").unwrap()).expect_err("list in required section must error");
assert!(
matches!(err, LispError::NonSymbolParam { position: 0, .. }),
"expected NonSymbolParam{{position: 0}}, got: {err:?}"
);
}
#[test]
fn bind_unsupplied_optional_with_default_takes_the_default() {
let params = MacroParams {
required: vec!["a".into()],
optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
rest: None,
};
let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
assert_eq!(vals, vec![Sexp::int(1), Sexp::int(5)]);
}
#[test]
fn bind_supplied_optional_with_default_takes_the_arg_not_the_default() {
let params = MacroParams {
required: Vec::new(),
optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
rest: None,
};
let vals = params.bind("m", &[Sexp::int(42)]).unwrap();
assert_eq!(vals, vec![Sexp::int(42)]);
}
#[test]
fn bind_mixes_supplied_unsupplied_default_and_nil_floor() {
let params = MacroParams {
required: vec!["a".into()],
optional: vec![
OptionalParam::with_default("b", Sexp::int(5)),
OptionalParam::bare("c"),
OptionalParam::with_default("d", Sexp::string("z")),
],
rest: None,
};
let vals = params.bind("m", &[Sexp::int(1)]).unwrap();
assert_eq!(
vals,
vec![Sexp::int(1), Sexp::int(5), Sexp::Nil, Sexp::string("z")]
);
}
#[test]
fn resolved_default_is_nil_for_bare_optional() {
let p = OptionalParam::bare("x");
assert_eq!(p.resolved_default(), Sexp::Nil);
}
#[test]
fn resolved_default_clones_declared_default_for_with_default_optional() {
let p = OptionalParam::with_default("x", Sexp::int(5));
assert_eq!(p.resolved_default(), Sexp::int(5));
}
#[test]
fn resolved_default_clones_arbitrary_sexp_default_form() {
let arbitrary = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1), Sexp::int(2)]);
let p = OptionalParam::with_default("x", arbitrary.clone());
assert_eq!(p.resolved_default(), arbitrary);
}
#[test]
fn resolved_default_is_clone_stable_across_repeated_calls() {
let p = OptionalParam::with_default("x", Sexp::string("hi"));
let first = p.resolved_default();
let second = p.resolved_default();
assert_eq!(first, second);
assert_eq!(first, Sexp::string("hi"));
}
#[test]
fn resolved_default_is_the_binders_absent_optional_projection() {
let params = MacroParams {
required: Vec::new(),
optional: vec![
OptionalParam::bare("b"),
OptionalParam::with_default("c", Sexp::int(5)),
],
rest: None,
};
let vals = params.bind("m", &[]).unwrap();
assert_eq!(vals.len(), 2);
assert_eq!(vals[0], OptionalParam::bare("b").resolved_default());
assert_eq!(
vals[1],
OptionalParam::with_default("c", Sexp::int(5)).resolved_default()
);
assert_eq!(vals[0], Sexp::Nil);
assert_eq!(vals[1], Sexp::int(5));
}
#[test]
fn resolved_default_is_path_uniform_across_bytecode_and_substitute() {
let src = r#"
(defmacro greet (n &optional (g "hi") h)
`(list ,g ,n ,h))
(greet world)
"#;
let expected = vec![Sexp::List(vec![
Sexp::symbol("list"),
Sexp::string("hi"),
Sexp::symbol("world"),
Sexp::Nil,
])];
let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
let substitute = Expander::new_substitute_only()
.expand_program(read(src).unwrap())
.unwrap();
assert_eq!(
bytecode, expected,
"bytecode resolved_default expansion drifted"
);
assert_eq!(
substitute, expected,
"substitute resolved_default expansion drifted"
);
assert_eq!(
bytecode, substitute,
"the two strategies disagree on resolved_default expansion"
);
}
#[test]
fn resolved_default_supplied_optional_does_not_consult_accessor() {
let params = MacroParams {
required: Vec::new(),
optional: vec![OptionalParam::with_default("b", Sexp::int(5))],
rest: None,
};
let vals = params.bind("m", &[Sexp::int(42)]).unwrap();
assert_eq!(vals, vec![Sexp::int(42)]);
let p = OptionalParam::with_default("b", Sexp::int(5));
assert_ne!(vals[0], p.resolved_default());
}
#[test]
fn optional_default_macro_expands_end_to_end_under_both_strategies() {
let src = r#"
(defmacro greet (n &optional (g "hi"))
`(list ,g ,n))
(greet world)
(greet world there)
"#;
let expected = vec![
Sexp::List(vec![
Sexp::symbol("list"),
Sexp::string("hi"),
Sexp::symbol("world"),
]),
Sexp::List(vec![
Sexp::symbol("list"),
Sexp::symbol("there"),
Sexp::symbol("world"),
]),
];
let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
let substitute = Expander::new_substitute_only()
.expand_program(read(src).unwrap())
.unwrap();
assert_eq!(
bytecode, expected,
"bytecode optional-default expansion drifted"
);
assert_eq!(
substitute, expected,
"substitute optional-default expansion drifted"
);
assert_eq!(
bytecode, substitute,
"the two strategies disagree on optional-default expansion"
);
}
#[test]
fn optional_macro_expands_end_to_end_under_both_strategies() {
let src = "(defmacro pair (a &optional b) `(cons ,a ,b)) (pair 1 2) (pair 3)";
let expected = vec![
Sexp::List(vec![Sexp::symbol("cons"), Sexp::int(1), Sexp::int(2)]),
Sexp::List(vec![Sexp::symbol("cons"), Sexp::int(3), Sexp::Nil]),
];
let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
let substitute = Expander::new_substitute_only()
.expand_program(read(src).unwrap())
.unwrap();
assert_eq!(bytecode, expected, "bytecode optional expansion drifted");
assert_eq!(
substitute, expected,
"substitute optional expansion drifted"
);
assert_eq!(
bytecode, substitute,
"the two strategies disagree on optional expansion"
);
}
#[test]
fn template_body_unwraps_outer_quasiquote_to_inner() {
let inner = Sexp::List(vec![
Sexp::symbol("list"),
Sexp::Unquote(Box::new(Sexp::symbol("a"))),
]);
let def = MacroDef {
name: "f".into(),
params: MacroParams::default(),
body: Sexp::Quasiquote(Box::new(inner.clone())),
};
assert_eq!(def.template_body(), &inner);
}
#[test]
fn template_body_returns_non_quasiquote_body_verbatim() {
let body = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(1)]);
let def = MacroDef {
name: "f".into(),
params: MacroParams::default(),
body: body.clone(),
};
assert_eq!(def.template_body(), &body);
let atom_def = MacroDef {
name: "g".into(),
params: MacroParams::default(),
body: Sexp::symbol("nil-template"),
};
assert_eq!(atom_def.template_body(), &Sexp::symbol("nil-template"));
}
#[test]
fn template_body_peels_single_level_only() {
let inner_payload = Sexp::List(vec![Sexp::symbol("list"), Sexp::int(7)]);
let inner_qq = Sexp::Quasiquote(Box::new(inner_payload.clone()));
let def = MacroDef {
name: "nested".into(),
params: MacroParams::default(),
body: Sexp::Quasiquote(Box::new(inner_qq.clone())),
};
assert_eq!(def.template_body(), &inner_qq);
assert_ne!(def.template_body(), &inner_payload);
}
#[test]
fn template_body_returns_quote_form_verbatim_distinct_from_quasiquote() {
let inner = Sexp::List(vec![Sexp::symbol("opaque"), Sexp::int(42)]);
let body = Sexp::Quote(Box::new(inner.clone()));
let def = MacroDef {
name: "quoted".into(),
params: MacroParams::default(),
body: body.clone(),
};
assert_eq!(def.template_body(), &body);
assert_ne!(def.template_body(), &inner);
}
#[test]
fn template_body_is_the_shared_projection_both_strategies_walk() {
let src = "(defmacro wrap (x) `(list ,x ,x)) (wrap 5)";
let expected = vec![Sexp::List(vec![
Sexp::symbol("list"),
Sexp::int(5),
Sexp::int(5),
])];
let bytecode = Expander::new().expand_program(read(src).unwrap()).unwrap();
let substitute = Expander::new_substitute_only()
.expand_program(read(src).unwrap())
.unwrap();
assert_eq!(bytecode, expected, "bytecode body-projection drifted");
assert_eq!(substitute, expected, "substitute body-projection drifted");
assert_eq!(
bytecode, substitute,
"the two strategies disagree on the body-projection's emission"
);
}
#[test]
fn expand_routes_macro_call_dispatch_observably_through_as_call_to_any() {
let mut e = Expander::new();
e.expand_program(read("(defmacro wrap (x) `(list ,x ,x))").unwrap())
.unwrap();
let call_form = parse("(wrap 42)");
let (def_via_family, args_via_family) = call_form
.as_call_to_any(|h| e.macros.get(h))
.expect("registered macro call must decompose via as_call_to_any");
assert_eq!(def_via_family.name, "wrap");
assert_eq!(args_via_family, &[Sexp::int(42)]);
let expanded = e.expand(&call_form).unwrap();
assert_eq!(
expanded,
Sexp::List(vec![Sexp::symbol("list"), Sexp::int(42), Sexp::int(42)])
);
}
#[test]
fn expand_skips_non_macro_call_into_children_walk_via_family_primitive_none() {
let mut e = Expander::new();
e.expand_program(read("(defmacro wrap (x) `(list ,x ,x))").unwrap())
.unwrap();
let outer = parse("(foo (wrap 5))");
assert!(outer.as_call_to_any(|h| e.macros.get(h)).is_none());
let expanded = e.expand(&outer).unwrap();
assert_eq!(
expanded,
Sexp::List(vec![
Sexp::symbol("foo"),
Sexp::List(vec![Sexp::symbol("list"), Sexp::int(5), Sexp::int(5)]),
])
);
}
#[test]
fn expand_non_call_shapes_route_past_family_primitive_into_fallthrough_clone() {
let e = Expander::new();
let shapes = [
Sexp::symbol("foo"),
Sexp::int(5),
Sexp::keyword("k"),
Sexp::string("s"),
Sexp::boolean(true),
Sexp::float(1.5),
Sexp::Nil,
Sexp::Quote(Box::new(Sexp::symbol("x"))),
Sexp::Quasiquote(Box::new(Sexp::symbol("x"))),
Sexp::Unquote(Box::new(Sexp::symbol("x"))),
Sexp::UnquoteSplice(Box::new(Sexp::symbol("x"))),
];
for s in &shapes {
assert!(
s.as_call_to_any(|_h: &str| Some(0_u8)).is_none(),
"non-call shape must yield None for as_call_to_any: {s}"
);
assert_eq!(
e.expand(s).unwrap(),
s.clone(),
"non-call shape must round-trip unchanged through expand: {s}"
);
}
}
#[test]
fn expand_empty_list_routes_past_family_primitive_into_children_walk() {
let e = Expander::new();
let empty = Sexp::List(vec![]);
assert!(empty.as_call_to_any(|_h: &str| Some(())).is_none());
assert_eq!(e.expand(&empty).unwrap(), Sexp::List(vec![]));
}
#[test]
fn expand_and_collect_calls_to_yields_projection_for_every_matching_form_in_source_order() {
let forms = vec![
Sexp::List(vec![
Sexp::symbol("defmonitor"),
Sexp::keyword("name"),
Sexp::string("first"),
]),
Sexp::List(vec![
Sexp::symbol("defalert"),
Sexp::keyword("name"),
Sexp::string("not-a-match"),
]),
Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::keyword("solo")]),
];
let mut e = Expander::new();
let lengths: Vec<usize> = e
.expand_and_collect_calls_to(forms, "defmonitor", |args| Ok(args.len()))
.expect("matching forms must compose");
assert_eq!(lengths, vec![2, 1]);
}
#[test]
fn expand_and_collect_calls_to_skips_non_matching_forms_without_invoking_project() {
let forms = vec![
Sexp::symbol("bare-atom"),
Sexp::List(vec![Sexp::symbol("defalert"), Sexp::int(1)]),
Sexp::List(vec![Sexp::int(5), Sexp::symbol("not-symbol-head")]),
Sexp::List(vec![]),
Sexp::Nil,
];
let mut count = 0usize;
let mut e = Expander::new();
let out: Vec<()> = e
.expand_and_collect_calls_to(forms, "defmonitor", |_args| {
count += 1;
Ok(())
})
.expect("non-matching-only slice must collect to empty Vec");
assert!(out.is_empty(), "no matching forms — empty Vec, got {out:?}");
assert_eq!(count, 0, "project must never run for non-matching forms");
}
#[test]
fn expand_and_collect_calls_to_short_circuits_on_project_error_at_first_failure() {
let forms = vec![
Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(1)]),
Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(2)]),
Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(3)]),
];
let mut seen = Vec::new();
let mut e = Expander::new();
let err = e
.expand_and_collect_calls_to::<(), _>(forms, "defmonitor", |args| {
let n = args[0].as_int().expect("test args are ints");
seen.push(n);
if n == 2 {
Err(LispError::Compile {
form: "test".to_string(),
message: "stop at two".to_string(),
})
} else {
Ok(())
}
})
.expect_err("project's error must short-circuit collect");
assert_eq!(seen, vec![1, 2]);
assert!(
matches!(err, LispError::Compile { ref message, .. } if message == "stop at two"),
"short-circuit must propagate the project's error verbatim, got {err:?}"
);
}
#[test]
fn expand_and_collect_calls_to_short_circuits_on_expand_program_error_before_project_runs() {
let forms = read("(defmacro 5 (x) `,x) (defmonitor :name \"x\")").unwrap();
let mut count = 0usize;
let mut e = Expander::new();
let err = e
.expand_and_collect_calls_to::<(), _>(forms, "defmonitor", |_args| {
count += 1;
Ok(())
})
.expect_err("expand_program error must short-circuit before project");
assert_eq!(
count, 0,
"project must never run when expand_program errors"
);
let rendered = format!("{err}");
assert!(
rendered.contains("NAME") || rendered.contains("symbol"),
"error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
);
}
#[test]
fn expand_and_collect_calls_to_yields_empty_vec_for_empty_forms_input() {
let mut count = 0usize;
let mut e = Expander::new();
let out: Vec<()> = e
.expand_and_collect_calls_to(Vec::new(), "anything", |_args| {
count += 1;
Ok(())
})
.expect("empty forms is not an error");
assert!(out.is_empty());
assert_eq!(count, 0);
}
#[test]
fn expand_and_collect_calls_to_expands_macros_before_filtering_by_keyword() {
let forms = read(
"(defmacro emit-monitor (n) `(defmonitor :name ,n))
(emit-monitor \"alpha\")
(defmonitor :name \"beta\")",
)
.unwrap();
let mut e = Expander::new();
let names: Vec<String> = e
.expand_and_collect_calls_to(forms, "defmonitor", |args| {
Ok(args[1].as_string().unwrap().to_string())
})
.expect("macroexpanded + directly-authored forms must both flow");
assert_eq!(names, vec!["alpha".to_string(), "beta".to_string()]);
}
#[test]
fn expand_and_collect_calls_to_threads_keyword_argument_verbatim_into_filter() {
let forms = vec![
Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(1)]),
Sexp::List(vec![Sexp::symbol("defalert"), Sexp::int(2)]),
Sexp::List(vec![Sexp::symbol("defmonitor"), Sexp::int(3)]),
Sexp::List(vec![Sexp::symbol("defnotify"), Sexp::int(4)]),
];
let mut e = Expander::new();
let monitors: Vec<i64> = e
.expand_and_collect_calls_to(forms.clone(), "defmonitor", |args| {
Ok(args[0].as_int().unwrap())
})
.unwrap();
assert_eq!(monitors, vec![1, 3]);
let mut e2 = Expander::new();
let alerts: Vec<i64> = e2
.expand_and_collect_calls_to(forms.clone(), "defalert", |args| {
Ok(args[0].as_int().unwrap())
})
.unwrap();
assert_eq!(alerts, vec![2]);
let mut e3 = Expander::new();
let none: Vec<i64> = e3
.expand_and_collect_calls_to(forms, "missing-keyword", |args| {
Ok(args[0].as_int().unwrap())
})
.unwrap();
assert!(none.is_empty());
}
#[test]
fn expand_and_collect_calls_to_matches_inlined_expand_program_plus_iter_calls_to_path() {
let src = "(defmacro emit-foo (n) `(foo :idx ,n))
(foo :idx 1)
(emit-foo 2)
(bar :idx 99)
(foo :idx 3)";
let forms = read(src).unwrap();
let mut exp_inline = Expander::new();
let expanded = exp_inline.expand_program(forms.clone()).unwrap();
let via_inline: Vec<i64> = crate::ast::iter_calls_to(&expanded, "foo")
.map(|args| -> Result<i64> { Ok(args[1].as_int().unwrap()) })
.collect::<Result<Vec<_>>>()
.unwrap();
let mut exp_method = Expander::new();
let via_method: Vec<i64> = exp_method
.expand_and_collect_calls_to(forms, "foo", |args| Ok(args[1].as_int().unwrap()))
.unwrap();
assert_eq!(via_inline, via_method);
assert_eq!(via_inline, vec![1, 2, 3]);
}
#[test]
fn expand_and_collect_calls_to_any_yields_decoded_pair_for_every_matching_form_in_source_order()
{
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Op {
Foo,
Bar,
Baz,
}
let src = "(foo 1)
(bar 2)
(other 99)
(baz 3)
(foo 4)";
let forms = read(src).unwrap();
let mut e = Expander::new();
let pairs: Vec<(Op, i64)> = e
.expand_and_collect_calls_to_any(
forms,
|h| match h {
"foo" => Some(Op::Foo),
"bar" => Some(Op::Bar),
"baz" => Some(Op::Baz),
_ => None,
},
|op, args| Ok((op, args[0].as_int().unwrap())),
)
.unwrap();
assert_eq!(
pairs,
vec![(Op::Foo, 1), (Op::Bar, 2), (Op::Baz, 3), (Op::Foo, 4),],
);
}
#[test]
fn expand_and_collect_calls_to_any_skips_non_matching_forms_without_invoking_project() {
let src = r#":bare-keyword
"bare-string"
42
()
(foo bar)
(defmonitor :name "matches")"#;
let forms = read(src).unwrap();
let mut e = Expander::new();
let lengths: Vec<usize> = e
.expand_and_collect_calls_to_any(
forms,
|h| (h == "defmonitor").then_some(()),
|(), args| {
assert_eq!(args.len(), 2, "projection ran on non-matching form");
Ok(args.len())
},
)
.unwrap();
assert_eq!(lengths, vec![2]);
}
#[test]
fn expand_and_collect_calls_to_any_short_circuits_on_project_error_at_first_failure() {
let src = "(foo 1) (foo 2) (foo 3)";
let forms = read(src).unwrap();
let mut count = 0usize;
let mut e = Expander::new();
let err = e
.expand_and_collect_calls_to_any::<i64, _, _, ()>(
forms,
|h| (h == "foo").then_some(()),
|(), args| {
count += 1;
let v = args[0].as_int().unwrap();
if v == 2 {
return Err(crate::error::LispError::Missing("test-failure"));
}
Ok(v)
},
)
.expect_err("projection must short-circuit on first Err");
assert_eq!(
count, 2,
"projection must have run on first AND failing form, then stopped",
);
assert!(
matches!(err, crate::error::LispError::Missing("test-failure")),
"expected the projection's typed Err verbatim, got {err:?}",
);
}
#[test]
fn expand_and_collect_calls_to_any_expands_macros_before_filtering_by_classifier() {
let src = "(defmacro emit-foo (n) `(foo :idx ,n))
(foo :idx 1)
(emit-foo 2)
(bar :idx 99)
(foo :idx 3)";
let forms = read(src).unwrap();
let mut e = Expander::new();
let idxs: Vec<i64> = e
.expand_and_collect_calls_to_any(
forms,
|h| (h == "foo").then_some(()),
|(), args| Ok(args[1].as_int().unwrap()),
)
.unwrap();
assert_eq!(idxs, vec![1, 2, 3]);
}
#[test]
fn expand_and_collect_calls_to_any_admits_fnmut_classifier_maintaining_state_across_walk() {
let src = "(foo 1) (bar 2) (foo 3) (bar 4) (foo 5)";
let forms = read(src).unwrap();
let mut e = Expander::new();
let mut classifier_calls = 0usize;
let projected: Vec<i64> = e
.expand_and_collect_calls_to_any(
forms,
|h| {
classifier_calls += 1;
(h == "foo").then_some(())
},
|(), args| Ok(args[0].as_int().unwrap()),
)
.unwrap();
assert_eq!(
classifier_calls, 5,
"classifier must run once per call form (5 forms, all calls with symbol heads)",
);
assert_eq!(
projected,
vec![1, 3, 5],
"projection must run only for classifier-accepted forms in source order",
);
}
#[test]
fn expand_and_collect_calls_to_routes_through_expand_and_collect_calls_to_any_via_constant_classifier_composition(
) {
let src = "(defmacro emit-foo (n) `(foo :idx ,n))
(foo :idx 1)
(emit-foo 2)
(bar :idx 99)
(foo :idx 3)";
let forms = read(src).unwrap();
for keyword in ["foo", "bar", "absent"] {
let mut exp_keyword = Expander::new();
let via_keyword: Vec<i64> = exp_keyword
.expand_and_collect_calls_to(forms.clone(), keyword, |args| {
Ok(args[1].as_int().unwrap())
})
.unwrap();
let mut exp_classifier = Expander::new();
let via_classifier: Vec<i64> = exp_classifier
.expand_and_collect_calls_to_any(
forms.clone(),
|h| (h == keyword).then_some(()),
|(), args| Ok(args[1].as_int().unwrap()),
)
.unwrap();
assert_eq!(
via_keyword, via_classifier,
"routing identity drifted for keyword {keyword:?}",
);
}
}
#[test]
fn expand_and_collect_calls_to_any_short_circuits_on_expand_program_error_before_project_runs()
{
let forms = read("(defmacro 5 (x) `,x) (foo :idx 1)").unwrap();
let mut e = Expander::new();
let err = e
.expand_and_collect_calls_to_any::<(), _, _, ()>(
forms,
|_h| -> Option<()> {
panic!("classifier must not run when expand_program errors");
},
|(), _args| {
panic!("project must not run when expand_program errors");
},
)
.expect_err("expand_program error must short-circuit before classifier or project");
let rendered = format!("{err}");
assert!(
rendered.contains("NAME") || rendered.contains("symbol"),
"expected expand_program-stage `defmacro-NAME-not-a-symbol` rejection, got {rendered:?}",
);
}
#[test]
fn expand_source_and_collect_calls_to_routes_through_reader_then_expand_and_collect() {
let src = r#"(defmonitor :name "first")
(defalert :name "not-a-match")
(defmonitor :solo)"#;
let mut e = Expander::new();
let lengths: Vec<usize> = e
.expand_source_and_collect_calls_to(src, "defmonitor", |args| Ok(args.len()))
.expect("matching forms must compose");
assert_eq!(lengths, vec![2, 1]);
}
#[test]
fn expand_source_and_collect_calls_to_short_circuits_on_reader_error_before_expand_program() {
let mut count = 0usize;
let mut e = Expander::new();
let err = e
.expand_source_and_collect_calls_to::<(), _>(
"(defmonitor :name \"unbalanced",
"defmonitor",
|_args| {
count += 1;
Ok(())
},
)
.expect_err("reader error must short-circuit before expand_program");
assert_eq!(
count, 0,
"project must never run when reader errors at parse time"
);
let rendered = format!("{err}");
assert!(
rendered.to_lowercase().contains("string")
|| rendered.to_lowercase().contains("paren")
|| rendered.to_lowercase().contains("eof")
|| rendered.to_lowercase().contains("unexpected")
|| rendered.to_lowercase().contains("unterminated")
|| rendered.to_lowercase().contains("unclosed"),
"error must be the reader-stage rejection, got: {rendered}"
);
}
#[test]
fn expand_source_and_collect_calls_to_short_circuits_on_expand_program_error_before_project_runs(
) {
let mut count = 0usize;
let mut e = Expander::new();
let err = e
.expand_source_and_collect_calls_to::<(), _>(
"(defmacro 5 (x) `,x) (defmonitor :name \"x\")",
"defmonitor",
|_args| {
count += 1;
Ok(())
},
)
.expect_err("expand_program error must short-circuit before project");
assert_eq!(
count, 0,
"project must never run when expand_program errors"
);
let rendered = format!("{err}");
assert!(
rendered.contains("NAME") || rendered.contains("symbol"),
"error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
);
}
#[test]
fn expand_source_and_collect_calls_to_short_circuits_on_project_error_at_first_failure() {
let src = "(defmonitor :idx 1) (defmonitor :idx 2) (defmonitor :idx 3)";
let mut seen = Vec::new();
let mut e = Expander::new();
let err = e
.expand_source_and_collect_calls_to::<(), _>(src, "defmonitor", |args| {
let n = args[1].as_int().expect("test args are ints");
seen.push(n);
if n == 2 {
Err(LispError::Compile {
form: "test".to_string(),
message: "stop at two".to_string(),
})
} else {
Ok(())
}
})
.expect_err("project's error must short-circuit collect");
assert_eq!(seen, vec![1, 2]);
assert!(
matches!(err, LispError::Compile { ref message, .. } if message == "stop at two"),
"short-circuit must propagate the project's error verbatim, got {err:?}"
);
}
#[test]
fn expand_source_and_collect_calls_to_yields_empty_vec_for_empty_source() {
let mut count = 0usize;
let mut e = Expander::new();
let out: Vec<()> = e
.expand_source_and_collect_calls_to("", "anything", |_args| {
count += 1;
Ok(())
})
.expect("empty source is not an error");
assert!(out.is_empty());
assert_eq!(count, 0);
}
#[test]
fn expand_source_and_collect_calls_to_expands_macros_before_filtering_by_keyword() {
let src = "(defmacro emit-monitor (n) `(defmonitor :name ,n))
(emit-monitor \"alpha\")
(defmonitor :name \"beta\")";
let mut e = Expander::new();
let names: Vec<String> = e
.expand_source_and_collect_calls_to(src, "defmonitor", |args| {
Ok(args[1].as_string().unwrap().to_string())
})
.expect("macroexpanded + directly-authored forms must both flow");
assert_eq!(names, vec!["alpha".to_string(), "beta".to_string()]);
}
#[test]
fn expand_source_and_collect_calls_to_matches_inlined_read_plus_expand_and_collect_path() {
let src = "(defmacro emit-foo (n) `(foo :idx ,n))
(foo :idx 1)
(emit-foo 2)
(bar :idx 99)
(foo :idx 3)";
let mut exp_inline = Expander::new();
let inline_forms = read(src).unwrap();
let via_inline: Vec<i64> = exp_inline
.expand_and_collect_calls_to(inline_forms, "foo", |args| Ok(args[1].as_int().unwrap()))
.unwrap();
let mut exp_method = Expander::new();
let via_method: Vec<i64> = exp_method
.expand_source_and_collect_calls_to(src, "foo", |args| Ok(args[1].as_int().unwrap()))
.unwrap();
assert_eq!(via_inline, via_method);
assert_eq!(via_inline, vec![1, 2, 3]);
}
#[test]
fn expand_source_and_collect_calls_to_threads_keyword_argument_verbatim_into_filter() {
let src = "(defmonitor :idx 1) (defalert :idx 2) (defmonitor :idx 3) (defnotify :idx 4)";
let mut e1 = Expander::new();
let monitors: Vec<i64> = e1
.expand_source_and_collect_calls_to(src, "defmonitor", |args| {
Ok(args[1].as_int().unwrap())
})
.unwrap();
assert_eq!(monitors, vec![1, 3]);
let mut e2 = Expander::new();
let alerts: Vec<i64> = e2
.expand_source_and_collect_calls_to(src, "defalert", |args| {
Ok(args[1].as_int().unwrap())
})
.unwrap();
assert_eq!(alerts, vec![2]);
let mut e3 = Expander::new();
let none: Vec<i64> = e3
.expand_source_and_collect_calls_to(src, "missing-keyword", |args| {
Ok(args[1].as_int().unwrap())
})
.unwrap();
assert!(none.is_empty());
}
#[test]
fn expand_source_and_collect_calls_to_skips_non_matching_forms_without_invoking_project() {
let src = "bare-atom (defalert :idx 1) (defnotify :idx 2)";
let mut count = 0usize;
let mut e = Expander::new();
let out: Vec<()> = e
.expand_source_and_collect_calls_to(src, "defmonitor", |_args| {
count += 1;
Ok(())
})
.expect("non-matching-only source must collect to empty Vec");
assert!(out.is_empty(), "no matching forms — empty Vec, got {out:?}");
assert_eq!(count, 0, "project must never run for non-matching forms");
}
#[test]
fn expand_source_and_collect_calls_to_any_yields_decoded_pair_for_every_matching_form_in_source_order(
) {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Op {
Foo,
Bar,
}
fn op_from_keyword(head: &str) -> Option<Op> {
match head {
"foo" => Some(Op::Foo),
"bar" => Some(Op::Bar),
_ => None,
}
}
let src = r#"(foo :idx 1)
(defmonitor :idx 99)
(bar :idx 2)
(foo :idx 3)"#;
let mut e = Expander::new();
let yielded: Vec<(Op, i64)> = e
.expand_source_and_collect_calls_to_any(src, op_from_keyword, |op, args| {
Ok((op, args[1].as_int().expect("test args are ints")))
})
.expect("matching forms must compose through from-source classifier walk");
assert_eq!(
yielded,
vec![(Op::Foo, 1), (Op::Bar, 2), (Op::Foo, 3)],
"yields must be in source order, decoded witness paired with per-form mapper output"
);
}
#[test]
fn expand_source_and_collect_calls_to_any_short_circuits_on_reader_error_before_classifier_runs(
) {
let mut e = Expander::new();
let err = e
.expand_source_and_collect_calls_to_any::<(), _, _, ()>(
"(defmonitor :name \"unbalanced",
|_h: &str| -> Option<()> {
panic!("classifier must not run when reader errors at parse time")
},
|(), _args| -> Result<()> { panic!("project must not run when reader errors") },
)
.expect_err("reader error must short-circuit before classifier");
let rendered = format!("{err}");
assert!(
rendered.to_lowercase().contains("string")
|| rendered.to_lowercase().contains("paren")
|| rendered.to_lowercase().contains("eof")
|| rendered.to_lowercase().contains("unexpected")
|| rendered.to_lowercase().contains("unterminated")
|| rendered.to_lowercase().contains("unclosed"),
"error must be the reader-stage rejection, got: {rendered}"
);
}
#[test]
fn expand_source_and_collect_calls_to_any_short_circuits_on_expand_program_error_before_classifier_runs(
) {
let mut e = Expander::new();
let err = e
.expand_source_and_collect_calls_to_any::<(), _, _, ()>(
"(defmacro 5 (x) `,x) (defmonitor :name \"x\")",
|_h: &str| -> Option<()> {
panic!("classifier must not run when expand_program errors")
},
|(), _args| -> Result<()> {
panic!("project must not run when expand_program errors")
},
)
.expect_err("expand_program error must short-circuit before classifier");
let rendered = format!("{err}");
assert!(
rendered.contains("NAME") || rendered.contains("symbol"),
"error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
);
}
#[test]
fn expand_source_and_collect_calls_to_any_skips_non_matching_forms_without_invoking_project() {
let src = r#"bare-atom
(5 :not-a-symbol-head)
(defmonitor :name "decoder-rejects-me")"#;
let mut e = Expander::new();
let out: Vec<()> = e
.expand_source_and_collect_calls_to_any::<(), _, _, ()>(
src,
|_h: &str| -> Option<()> { None },
|(), _args| -> Result<()> {
panic!("project must not run for classifier-rejected forms")
},
)
.expect("classifier-rejects-all source must collect to empty Vec");
assert!(
out.is_empty(),
"no classifier-accepted forms — empty Vec, got {out:?}"
);
}
#[test]
fn expand_source_and_collect_calls_to_any_short_circuits_on_project_error_at_first_failure() {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Op {
Foo,
}
fn op_from_keyword(head: &str) -> Option<Op> {
(head == "foo").then_some(Op::Foo)
}
let src = "(foo :idx 1) (foo :idx 2) (foo :idx 3)";
let mut seen = Vec::new();
let mut e = Expander::new();
let err = e
.expand_source_and_collect_calls_to_any::<(), _, _, _>(
src,
op_from_keyword,
|_op: Op, args: &[Sexp]| -> Result<()> {
let n = args[1].as_int().expect("test args are ints");
seen.push(n);
if n == 2 {
Err(LispError::Compile {
form: "test".to_string(),
message: "stop at two".to_string(),
})
} else {
Ok(())
}
},
)
.expect_err("project's error must short-circuit collect");
assert_eq!(seen, vec![1, 2], "third match's project must never run");
assert!(
matches!(err, LispError::Compile { ref message, .. } if message == "stop at two"),
"short-circuit must propagate the project's error verbatim, got {err:?}"
);
}
#[test]
fn expand_source_and_collect_calls_to_routes_through_expand_source_and_collect_calls_to_any_via_constant_classifier_composition(
) {
let src = "(defmacro emit-foo (n) `(foo :idx ,n))
(foo :idx 1)
(emit-foo 2)
(bar :idx 99)
(foo :idx 3)";
for k in ["foo", "bar", "missing"] {
let mut e_keyword = Expander::new();
let via_keyword: Vec<i64> = e_keyword
.expand_source_and_collect_calls_to(src, k, |args| Ok(args[1].as_int().unwrap()))
.unwrap();
let mut e_classifier = Expander::new();
let via_classifier: Vec<i64> = e_classifier
.expand_source_and_collect_calls_to_any(
src,
|h: &str| (h == k).then_some(()),
|(), args: &[Sexp]| Ok(args[1].as_int().unwrap()),
)
.unwrap();
assert_eq!(
via_keyword, via_classifier,
"keyword from-source path must equal classifier from-source path for {k:?}"
);
}
}
#[test]
fn expand_source_and_collect_calls_to_any_expands_macros_before_filtering_by_classifier() {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Op {
Foo,
}
fn op_from_keyword(head: &str) -> Option<Op> {
(head == "foo").then_some(Op::Foo)
}
let src = "(defmacro emit-foo (n) `(foo :name ,n))
(emit-foo \"alpha\")
(foo :name \"beta\")";
let mut e = Expander::new();
let names: Vec<(Op, String)> = e
.expand_source_and_collect_calls_to_any(src, op_from_keyword, |op, args| {
Ok((op, args[1].as_string().unwrap().to_string()))
})
.expect("macroexpanded + directly-authored forms must both flow through classifier");
assert_eq!(
names,
vec![
(Op::Foo, "alpha".to_string()),
(Op::Foo, "beta".to_string()),
]
);
}
#[test]
fn expand_source_program_routes_through_reader_then_expand_program() {
let src = r#"(defmacro id (x) `,x)
(defmonitor :name "alpha")
bare-symbol"#;
let mut e = Expander::new();
let out = e
.expand_source_program(src)
.expect("mixed forms must compose");
assert_eq!(out.len(), 2);
assert_eq!(
out[0].as_call_to("defmonitor").map(<[_]>::len),
Some(2),
"first surviving form is the defmonitor with two args, got: {:?}",
out[0]
);
assert_eq!(
out[1].as_symbol(),
Some("bare-symbol"),
"second surviving form is the bare symbol literal, got: {:?}",
out[1]
);
assert!(
e.has("id"),
"defmacro must register `id` in the expander's macro table"
);
}
#[test]
fn expand_source_program_short_circuits_on_reader_error_before_expand_program() {
let mut e = Expander::new();
let err = e
.expand_source_program(r#"(defmacro should-not-register (x) `,x) "unterminated"#)
.expect_err("reader error must short-circuit before expand_program");
let rendered = format!("{err}").to_lowercase();
assert!(
rendered.contains("string")
|| rendered.contains("paren")
|| rendered.contains("eof")
|| rendered.contains("unexpected")
|| rendered.contains("unterminated")
|| rendered.contains("unclosed"),
"error must be the reader-stage rejection, got: {err}"
);
assert!(
!e.has("should-not-register"),
"reader-stage rejection must short-circuit BEFORE expand_program registers any defmacro"
);
}
#[test]
fn expand_source_program_short_circuits_on_expand_program_error() {
let mut e = Expander::new();
let err = e
.expand_source_program("(defmacro 5 (x) `,x)")
.expect_err("expand_program error must propagate");
let rendered = format!("{err}");
assert!(
rendered.contains("NAME") || rendered.contains("symbol"),
"error must be the defmacro-NAME-not-a-symbol rejection, got: {rendered}"
);
}
#[test]
fn expand_source_program_yields_empty_vec_for_empty_source() {
let mut e = Expander::new();
let out = e
.expand_source_program("")
.expect("empty source is not an error");
assert!(
out.is_empty(),
"empty source must yield empty Vec, got: {out:?}"
);
}
#[test]
fn expand_source_program_absorbs_defmacro_and_expands_subsequent_calls() {
let src = r#"(defmacro emit-monitor (n) `(defmonitor :name ,n))
(emit-monitor "alpha")
(emit-monitor "beta")"#;
let mut e = Expander::new();
let out = e
.expand_source_program(src)
.expect("defmacro absorption then expansion must compose");
assert_eq!(out.len(), 2, "expected two expanded forms, got: {out:?}");
for (i, expected_name) in [(0, "alpha"), (1, "beta")] {
let args = out[i]
.as_call_to("defmonitor")
.unwrap_or_else(|| panic!("form {i} must be defmonitor, got: {:?}", out[i]));
assert_eq!(args[0].as_keyword(), Some("name"));
assert_eq!(args[1].as_string(), Some(expected_name));
}
assert!(e.has("emit-monitor"));
}
#[test]
fn expand_source_program_matches_inlined_read_plus_expand_program_path() {
let src = r#"(defmacro id (x) `,x)
(id (foo 1 2))
(bar)"#;
let mut e_inline = Expander::new();
let inline_forms = read(src).unwrap();
let via_inline = e_inline
.expand_program(inline_forms)
.expect("inlined pipeline must succeed");
let mut e_method = Expander::new();
let via_method = e_method
.expand_source_program(src)
.expect("from-source method pipeline must succeed");
assert_eq!(
via_inline, via_method,
"from-source method must emit byte-identical result to inlined read+expand_program"
);
assert_eq!(e_inline.has("id"), e_method.has("id"));
assert!(e_method.has("id"));
}
#[test]
fn expand_source_program_preserves_defmacro_absorption_across_repeated_calls() {
let mut e = Expander::new();
let _ = e
.expand_source_program("(defmacro outer (n) `(inner ,n))")
.unwrap();
assert!(e.has("outer"));
let out = e
.expand_source_program("(defmacro inner (x) `(wrapped ,x)) (outer 42)")
.unwrap();
assert_eq!(out.len(), 1, "call 2 yields one expanded form");
let args = out[0]
.as_call_to("wrapped")
.expect("nested expansion must reach `wrapped`");
assert_eq!(args[0].as_int(), Some(42));
}
fn macro_def_id() -> MacroDef {
MacroDef {
name: "id".into(),
params: MacroParams {
required: vec!["x".into()],
optional: vec![],
rest: None,
},
body: Sexp::Quasiquote(Box::new(Sexp::Unquote(Box::new(Sexp::symbol("x"))))),
}
}
fn macro_def_bad_template() -> MacroDef {
MacroDef {
name: "bad".into(),
params: MacroParams::default(),
body: Sexp::Quasiquote(Box::new(Sexp::Unquote(Box::new(Sexp::symbol("unbound"))))),
}
}
#[test]
fn register_macro_def_bytecode_default_populates_macros_and_templates() {
let mut e = Expander::new();
e.register_macro_def(macro_def_id())
.expect("well-formed MacroDef must register");
assert!(
e.has("id"),
"self.macros must carry the registered name after register_macro_def"
);
assert!(
e.templates.contains_key("id"),
"self.templates must carry the compiled bytecode under the bytecode-default posture"
);
let out = e
.expand_program(read("(id 42)").unwrap())
.expect("registered macro must expand");
assert_eq!(out.len(), 1);
assert_eq!(out[0], Sexp::int(42));
}
#[test]
fn register_macro_def_substitute_only_skips_templates() {
let mut e = Expander::new_substitute_only();
e.register_macro_def(macro_def_id())
.expect("well-formed MacroDef must register under substitute-only");
assert!(
e.has("id"),
"self.macros must carry the registered name even under substitute-only"
);
assert!(
!e.templates.contains_key("id"),
"self.templates MUST be empty under compile_templates: false — the gate fires"
);
let out = e
.expand_program(read("(id 42)").unwrap())
.expect("registered macro must expand via substitute path");
assert_eq!(out.len(), 1);
assert_eq!(out[0], Sexp::int(42));
}
#[test]
fn register_macro_def_template_compile_failure_leaves_both_tables_pristine() {
let mut e = Expander::new();
let err = e
.register_macro_def(macro_def_bad_template())
.expect_err("unbound-template body must reject");
assert!(
matches!(err, LispError::UnboundTemplateVar { .. }),
"expected UnboundTemplateVar, got: {err:?}"
);
assert!(
!e.has("bad"),
"self.macros must be untouched after compile_template failure"
);
assert!(
!e.templates.contains_key("bad"),
"self.templates must be untouched after compile_template failure"
);
}
#[test]
fn with_macros_routes_through_register_macro_def_path_uniformity() {
let mut via_register = Expander::new();
via_register
.register_macro_def(macro_def_id())
.expect("register must succeed");
let mut via_with_macros =
Expander::with_macros([macro_def_id()]).expect("with_macros must succeed");
assert_eq!(via_register.len(), via_with_macros.len());
assert!(via_register.has("id"));
assert!(via_with_macros.has("id"));
assert_eq!(
via_register.templates.contains_key("id"),
via_with_macros.templates.contains_key("id"),
"self.templates key-presence must agree across with_macros and register_macro_def"
);
let out_a = via_register
.expand_program(read("(id 99)").unwrap())
.unwrap();
let out_b = via_with_macros
.expand_program(read("(id 99)").unwrap())
.unwrap();
assert_eq!(out_a, out_b);
assert_eq!(out_a, vec![Sexp::int(99)]);
}
#[test]
fn expand_program_routes_through_register_macro_def_path_uniformity() {
let mut via_register = Expander::new();
via_register
.register_macro_def(macro_def_id())
.expect("register must succeed");
let mut via_expand_program = Expander::new();
let yielded = via_expand_program
.expand_program(read("(defmacro id (x) `,x)").unwrap())
.expect("expand_program of one defmacro must succeed");
assert!(
yielded.is_empty(),
"(defmacro …) is a side-effect-only top-level form; expand_program yields nothing"
);
assert!(via_register.has("id"));
assert!(via_expand_program.has("id"));
assert_eq!(via_register.len(), via_expand_program.len());
assert_eq!(
via_register.templates.contains_key("id"),
via_expand_program.templates.contains_key("id"),
"self.templates key-presence must agree across expand_program and register_macro_def"
);
let out_a = via_register
.expand_program(read("(id 7)").unwrap())
.unwrap();
let out_b = via_expand_program
.expand_program(read("(id 7)").unwrap())
.unwrap();
assert_eq!(out_a, out_b);
assert_eq!(out_a, vec![Sexp::int(7)]);
}
#[test]
fn bytecode_and_substitute_agree_on_unquote_substitution_routed_through_as_unquote() {
let src = "(defmacro id (x) ,x) (id 42)";
let mut bc = Expander::new();
let mut sub = Expander::new_substitute_only();
let out_bc = bc.expand_program(read(src).unwrap()).unwrap();
let out_sub = sub.expand_program(read(src).unwrap()).unwrap();
assert_eq!(out_bc, out_sub, "strategies diverged on `,x` template");
assert_eq!(out_bc, vec![Sexp::int(42)]);
}
#[test]
fn bytecode_and_substitute_agree_on_unquote_splice_routed_through_as_unquote() {
let src = "(defmacro wrap (xs) (list 0 ,@xs 99)) (wrap (1 2 3))";
let mut bc = Expander::new();
let mut sub = Expander::new_substitute_only();
let out_bc = bc.expand_program(read(src).unwrap()).unwrap();
let out_sub = sub.expand_program(read(src).unwrap()).unwrap();
assert_eq!(out_bc, out_sub, "strategies diverged on `,@xs` template");
let expected = Sexp::List(vec![
Sexp::symbol("list"),
Sexp::int(0),
Sexp::int(1),
Sexp::int(2),
Sexp::int(3),
Sexp::int(99),
]);
assert_eq!(out_bc, vec![expected]);
}
#[test]
fn substitute_splice_outside_list_routes_through_as_unquote_typed_marker() {
let src = "(defmacro bad (xs) ,@xs) (bad (1 2 3))";
let mut sub = Expander::new_substitute_only();
let err = sub.expand_program(read(src).unwrap()).unwrap_err();
assert!(
matches!(err, crate::error::LispError::SpliceOutsideList { .. }),
"expected SpliceOutsideList through as_unquote, got: {err:?}"
);
}
#[test]
fn as_unquote_threads_typed_marker_into_unbound_template_var_rejection() {
let src = "(defmacro bad (x) ,unbound)";
let mut bc = Expander::new();
let err = bc.expand_program(read(src).unwrap()).unwrap_err();
match err {
crate::error::LispError::UnboundTemplateVar { prefix, .. } => {
assert_eq!(
prefix,
UnquoteForm::Unquote,
"typed marker drifted from UnquoteForm::Unquote at gate-2"
);
}
other => panic!("expected UnboundTemplateVar, got: {other:?}"),
}
let src_splice = "(defmacro bad (x) (foo ,@unbound))";
let mut bc2 = Expander::new();
let err_splice = bc2.expand_program(read(src_splice).unwrap()).unwrap_err();
match err_splice {
crate::error::LispError::UnboundTemplateVar { prefix, .. } => {
assert_eq!(
prefix,
UnquoteForm::Splice,
"typed marker drifted from UnquoteForm::Splice at gate-2"
);
}
other => panic!("expected UnboundTemplateVar, got: {other:?}"),
}
}
#[test]
fn compile_template_splice_outside_list_routes_through_as_unquote_typed_marker() {
let mut e = Expander::new();
let err = e
.expand_program(read("(defmacro bad (xs) `,@xs)").unwrap())
.expect_err("compile_template must reject top-level ,@X via as_unquote");
assert!(
matches!(err, crate::error::LispError::SpliceOutsideList { .. }),
"expected SpliceOutsideList through as_unquote, got: {err:?}"
);
}
#[test]
fn compile_template_accepts_top_level_unquote_through_as_unquote_typed_marker() {
let src = "(defmacro id (x) ,x) (id 42)";
let mut e = Expander::new();
let expanded = e
.expand_program(read(src).unwrap())
.expect("top-level ,X body must compile through as_unquote-typed gate");
assert_eq!(expanded, vec![Sexp::int(42)]);
}
#[test]
fn contains_unquote_routes_through_as_unquote_for_unquote_family_recognition() {
let bare_unquote = Sexp::Unquote(Box::new(Sexp::symbol("x")));
let bare_splice = Sexp::UnquoteSplice(Box::new(Sexp::symbol("xs")));
let nested = Sexp::Quasiquote(Box::new(Sexp::List(vec![
Sexp::symbol("foo"),
Sexp::UnquoteSplice(Box::new(Sexp::symbol("xs"))),
])));
assert!(super::contains_unquote(&bare_unquote));
assert!(super::contains_unquote(&bare_splice));
assert!(super::contains_unquote(&nested));
assert!(!super::contains_unquote(&Sexp::Nil));
assert!(!super::contains_unquote(&Sexp::symbol("plain")));
assert!(!super::contains_unquote(&Sexp::int(5)));
assert!(!super::contains_unquote(&Sexp::List(vec![
Sexp::symbol("plain"),
Sexp::int(1),
])));
assert!(!super::contains_unquote(&Sexp::Quote(Box::new(
Sexp::symbol("inert")
))));
}
#[test]
fn contains_unquote_routes_quote_family_through_as_quote_form_typed_marker() {
use crate::ast::QuoteForm;
let inner_plain = Sexp::symbol("x");
let inner_unquote = Sexp::Unquote(Box::new(Sexp::symbol("x")));
for qf in QuoteForm::ALL {
let wrapped_plain = match qf {
QuoteForm::Quote => Sexp::Quote(Box::new(inner_plain.clone())),
QuoteForm::Quasiquote => Sexp::Quasiquote(Box::new(inner_plain.clone())),
QuoteForm::Unquote => Sexp::Unquote(Box::new(inner_plain.clone())),
QuoteForm::UnquoteSplice => Sexp::UnquoteSplice(Box::new(inner_plain.clone())),
};
let via_manual =
qf.as_unquote_form().is_some() || super::contains_unquote(&inner_plain);
assert_eq!(
super::contains_unquote(&wrapped_plain),
via_manual,
"contains_unquote drifted from as_quote_form + as_unquote_form composition for {qf:?}"
);
let wrapped_unquote = match qf {
QuoteForm::Quote => Sexp::Quote(Box::new(inner_unquote.clone())),
QuoteForm::Quasiquote => Sexp::Quasiquote(Box::new(inner_unquote.clone())),
QuoteForm::Unquote => Sexp::Unquote(Box::new(inner_unquote.clone())),
QuoteForm::UnquoteSplice => Sexp::UnquoteSplice(Box::new(inner_unquote.clone())),
};
assert!(
super::contains_unquote(&wrapped_unquote),
"contains_unquote missed an inner Unquote under {qf:?} — \
the quote-family recursion through as_quote_form drifted"
);
}
let nested_in_list = Sexp::List(vec![
Sexp::symbol("outer"),
Sexp::Quasiquote(Box::new(Sexp::Unquote(Box::new(Sexp::symbol("x"))))),
]);
assert!(
super::contains_unquote(&nested_in_list),
"contains_unquote failed to descend into a List subtree containing a \
Quasiquote(Unquote(_)) — list recursion arm drifted"
);
}
#[test]
fn expand_and_collect_named_calls_to_any_yields_decoded_triple_for_every_matching_form_in_source_order(
) {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Kind {
Monitor,
Notify,
}
let src = r#"(defmonitor cpu :threshold 80)
(other-form 99)
(defnotify email :to "ops@example.com")
(defmonitor mem :threshold 90)"#;
let forms = read(src).unwrap();
let mut e = Expander::new();
let rows: Vec<(Kind, String, usize)> = e
.expand_and_collect_named_calls_to_any(
forms,
|h| match h {
"defmonitor" => Some((Kind::Monitor, "defmonitor")),
"defnotify" => Some((Kind::Notify, "defnotify")),
_ => None,
},
|kind, name, spec_args| Ok((kind, name.to_string(), spec_args.len())),
)
.unwrap();
assert_eq!(
rows,
vec![
(Kind::Monitor, "cpu".to_string(), 2),
(Kind::Notify, "email".to_string(), 2),
(Kind::Monitor, "mem".to_string(), 2),
],
);
}
#[test]
fn expand_and_collect_named_calls_to_any_skips_non_matching_forms_without_invoking_project() {
let src = r#":bare-keyword
"bare-string"
42
()
(foo bar)
(defmonitor cpu :threshold 80)"#;
let forms = read(src).unwrap();
let mut e = Expander::new();
let names: Vec<String> = e
.expand_and_collect_named_calls_to_any(
forms,
|h| (h == "defmonitor").then_some(((), "defmonitor")),
|(), name, _args| Ok(name.to_string()),
)
.unwrap();
assert_eq!(names, vec!["cpu".to_string()]);
}
#[test]
fn expand_and_collect_named_calls_to_any_emits_named_form_missing_name_through_classifier_keyword(
) {
let forms = read("(defmonitor)").unwrap();
let mut e = Expander::new();
let err = e
.expand_and_collect_named_calls_to_any::<(), _, _, ()>(
forms,
|h| (h == "defmonitor").then_some(((), "defmonitor")),
|(), _name, _args| Ok(()),
)
.unwrap_err();
assert!(
matches!(
err,
crate::error::LispError::NamedFormMissingName {
keyword: "defmonitor"
}
),
"expected NamedFormMissingName with classifier-supplied keyword, got: {err:?}"
);
}
#[test]
fn expand_and_collect_named_calls_to_any_emits_named_form_non_symbol_name_through_classifier_keyword(
) {
let forms = read("(defmonitor 42 :threshold 80)").unwrap();
let mut e = Expander::new();
let err = e
.expand_and_collect_named_calls_to_any::<(), _, _, ()>(
forms,
|h| (h == "defmonitor").then_some(((), "defmonitor")),
|(), _name, _args| Ok(()),
)
.unwrap_err();
assert!(
matches!(
err,
crate::error::LispError::NamedFormNonSymbolName {
keyword: "defmonitor",
got: crate::error::SexpShape::Int,
}
),
"expected NamedFormNonSymbolName with classifier-supplied keyword + SexpShape::Int, got: {err:?}"
);
}
#[test]
fn expand_and_collect_named_calls_to_any_short_circuits_on_project_error_at_first_failure() {
let forms = read("(defmon a :x 1) (defmon b :x 2) (defmon c :x 3)").unwrap();
let mut count = 0usize;
let mut e = Expander::new();
let err = e
.expand_and_collect_named_calls_to_any::<String, _, _, ()>(
forms,
|h| (h == "defmon").then_some(((), "defmon")),
|(), name, _args| {
count += 1;
if name == "b" {
return Err(crate::error::LispError::Missing("test-failure"));
}
Ok(name.to_string())
},
)
.expect_err("projection must short-circuit on first Err");
assert_eq!(
count, 2,
"projection must have run on first matched form AND the failing form, then stopped"
);
assert!(
matches!(err, crate::error::LispError::Missing("test-failure")),
"expected the projection's typed Err verbatim, got: {err:?}"
);
}
#[test]
fn expand_and_collect_named_calls_to_any_expands_macros_before_filtering_by_classifier() {
let src = r#"(defmacro emit-mon (n thr) `(defmonitor ,n :threshold ,thr))
(defmonitor cpu :threshold 80)
(emit-mon mem 90)
(other-form 99)
(emit-mon disk 70)"#;
let forms = read(src).unwrap();
let mut e = Expander::new();
let names: Vec<String> = e
.expand_and_collect_named_calls_to_any(
forms,
|h| (h == "defmonitor").then_some(((), "defmonitor")),
|(), name, _args| Ok(name.to_string()),
)
.unwrap();
assert_eq!(names, vec!["cpu", "mem", "disk"]);
}
#[test]
fn expand_source_and_collect_named_calls_to_any_matches_inlined_read_plus_from_forms_path() {
let src = r#"(defmonitor cpu :threshold 80)
(defmonitor mem :threshold 90)"#;
let via_src: Vec<String> = Expander::new()
.expand_source_and_collect_named_calls_to_any(
src,
|h| (h == "defmonitor").then_some(((), "defmonitor")),
|(), name, _args| Ok(name.to_string()),
)
.unwrap();
let forms = read(src).unwrap();
let via_forms: Vec<String> = Expander::new()
.expand_and_collect_named_calls_to_any(
forms,
|h| (h == "defmonitor").then_some(((), "defmonitor")),
|(), name, _args| Ok(name.to_string()),
)
.unwrap();
assert_eq!(via_src, via_forms);
assert_eq!(via_src, vec!["cpu".to_string(), "mem".to_string()]);
}
#[test]
fn expand_source_and_collect_named_calls_to_any_short_circuits_on_reader_error_before_classifier_runs(
) {
let mut e = Expander::new();
let err = e
.expand_source_and_collect_named_calls_to_any::<(), _, _, ()>(
"(defmonitor cpu :threshold 80",
|_h| panic!("classifier must not run when reader fails"),
|(), _name, _args| Ok(()),
)
.unwrap_err();
assert!(
!matches!(
err,
crate::error::LispError::NamedFormMissingName { .. }
| crate::error::LispError::NamedFormNonSymbolName { .. }
),
"expected reader error, not named-gate variant; got: {err:?}"
);
}
#[test]
fn expand_and_collect_named_calls_to_yields_name_and_args_for_every_matching_form_in_source_order(
) {
let src = r#"(defmonitor cpu :threshold 80)
(other-form 99)
(defmonitor mem :threshold 90 :unit "MiB")
(defmonitor disk :threshold 70)"#;
let forms = read(src).unwrap();
let mut e = Expander::new();
let rows: Vec<(String, usize)> = e
.expand_and_collect_named_calls_to(forms, "defmonitor", |name, spec_args| {
Ok((name.to_string(), spec_args.len()))
})
.unwrap();
assert_eq!(
rows,
vec![
("cpu".to_string(), 2),
("mem".to_string(), 4),
("disk".to_string(), 2),
],
);
}
#[test]
fn expand_and_collect_named_calls_to_skips_non_matching_forms_without_invoking_project() {
let src = r#":bare-keyword
"bare-string"
42
()
(foo bar)
(defmonitor cpu :threshold 80)"#;
let forms = read(src).unwrap();
let mut e = Expander::new();
let names: Vec<String> = e
.expand_and_collect_named_calls_to(forms, "defmonitor", |name, _args| {
Ok(name.to_string())
})
.unwrap();
assert_eq!(names, vec!["cpu".to_string()]);
}
#[test]
fn expand_and_collect_named_calls_to_emits_named_form_missing_name_through_primitive_keyword() {
let src = r#"(defmonitor)"#;
let forms = read(src).unwrap();
let mut e = Expander::new();
let err = e
.expand_and_collect_named_calls_to::<(), _>(forms, "defmonitor", |_name, _args| Ok(()))
.unwrap_err();
assert!(
matches!(
err,
crate::error::LispError::NamedFormMissingName {
keyword: "defmonitor"
}
),
"expected NamedFormMissingName threading the primitive's keyword; got: {err:?}",
);
}
#[test]
fn expand_and_collect_named_calls_to_emits_named_form_non_symbol_name_through_primitive_keyword(
) {
let src = r#"(defmonitor 42 :threshold 80)"#;
let forms = read(src).unwrap();
let mut e = Expander::new();
let err = e
.expand_and_collect_named_calls_to::<(), _>(forms, "defmonitor", |_name, _args| Ok(()))
.unwrap_err();
match err {
crate::error::LispError::NamedFormNonSymbolName { keyword, got } => {
assert_eq!(keyword, "defmonitor");
assert_eq!(got, crate::error::SexpShape::Int);
}
other => panic!(
"expected NamedFormNonSymbolName threading the primitive's keyword + Int shape; got: {other:?}",
),
}
}
#[test]
fn expand_and_collect_named_calls_to_short_circuits_on_project_error_at_first_failure() {
let src = r#"(defmonitor cpu :threshold 80)
(defmonitor mem :threshold 90)
(defmonitor disk :threshold 70)"#;
let forms = read(src).unwrap();
let mut count: usize = 0;
let mut e = Expander::new();
let err = e
.expand_and_collect_named_calls_to::<String, _>(forms, "defmonitor", |name, _args| {
count += 1;
if name == "mem" {
Err(crate::error::LispError::Compile {
form: "test".into(),
message: format!("rejecting at NAME={name}"),
})
} else {
Ok(name.to_string())
}
})
.unwrap_err();
assert_eq!(count, 2, "third matched form must not be projected");
match err {
crate::error::LispError::Compile { message, .. } => {
assert!(message.contains("NAME=mem"));
}
other => panic!("expected projection-driven Compile error; got: {other:?}"),
}
}
#[test]
fn expand_and_collect_named_calls_to_expands_macros_before_filtering_by_keyword() {
let src = r#"(defmacro emit-mon (n thr) `(defmonitor ,n :threshold ,thr))
(defmonitor cpu :threshold 80)
(emit-mon mem 90)
(other-form 99)
(emit-mon disk 70)"#;
let forms = read(src).unwrap();
let mut e = Expander::new();
let names: Vec<String> = e
.expand_and_collect_named_calls_to(forms, "defmonitor", |name, _args| {
Ok(name.to_string())
})
.unwrap();
assert_eq!(names, vec!["cpu", "mem", "disk"]);
}
#[test]
fn expand_source_and_collect_named_calls_to_matches_inlined_read_plus_from_forms_path() {
let src = r#"(defmonitor cpu :threshold 80)
(defmonitor mem :threshold 90)"#;
let via_src: Vec<String> = Expander::new()
.expand_source_and_collect_named_calls_to(src, "defmonitor", |name, _args| {
Ok(name.to_string())
})
.unwrap();
let forms = read(src).unwrap();
let via_forms: Vec<String> = Expander::new()
.expand_and_collect_named_calls_to(forms, "defmonitor", |name, _args| {
Ok(name.to_string())
})
.unwrap();
assert_eq!(via_src, via_forms);
assert_eq!(via_src, vec!["cpu".to_string(), "mem".to_string()]);
}
#[test]
fn expand_source_and_collect_named_calls_to_short_circuits_on_reader_error_before_named_gate_runs(
) {
let mut e = Expander::new();
let err = e
.expand_source_and_collect_named_calls_to::<(), _>(
"(defmonitor cpu :threshold 80",
"defmonitor",
|_name, _args| panic!("projection must not run when reader fails"),
)
.unwrap_err();
assert!(
!matches!(
err,
crate::error::LispError::NamedFormMissingName { .. }
| crate::error::LispError::NamedFormNonSymbolName { .. }
),
"expected reader error, not named-gate variant; got: {err:?}",
);
}
#[test]
fn expand_and_collect_named_calls_to_routes_through_classifier_via_constant_decoder_composition(
) {
let src = r#"(defmonitor cpu :threshold 80)
(other-form 99)
(defmonitor mem :threshold 90)"#;
let forms = read(src).unwrap();
let via_constant_keyword: Vec<(String, usize)> = Expander::new()
.expand_and_collect_named_calls_to(forms.clone(), "defmonitor", |name, args| {
Ok((name.to_string(), args.len()))
})
.unwrap();
let via_classifier: Vec<(String, usize)> = Expander::new()
.expand_and_collect_named_calls_to_any(
forms,
|h| (h == "defmonitor").then_some(((), "defmonitor")),
|(), name, args| Ok((name.to_string(), args.len())),
)
.unwrap();
assert_eq!(via_constant_keyword, via_classifier);
assert_eq!(
via_constant_keyword,
vec![("cpu".to_string(), 2), ("mem".to_string(), 2)],
);
}
#[test]
fn expand_to_named_routes_through_expand_and_collect_named_calls_to_via_constant_keyword_composition(
) {
use crate::compile::NamedDefinition;
use crate::compiler_spec::CompilerSpec;
let src = r#"(defcompiler alpha-compiler :name "x" :dialect "standard")
(defcompiler beta-compiler :name "y" :dialect "standard")"#;
let forms = read(src).unwrap();
let via_expand_to_named = Expander::new()
.expand_to_named::<CompilerSpec>(forms.clone())
.unwrap();
let via_named_constant: Vec<NamedDefinition<CompilerSpec>> = Expander::new()
.expand_and_collect_named_calls_to(forms, "defcompiler", |name, spec_args| {
let spec =
<CompilerSpec as crate::domain::TataraDomain>::compile_from_args(spec_args)?;
Ok(NamedDefinition {
name: name.to_string(),
spec,
})
})
.unwrap();
assert_eq!(via_expand_to_named.len(), 2);
assert_eq!(via_expand_to_named.len(), via_named_constant.len());
for (a, b) in via_expand_to_named.iter().zip(via_named_constant.iter()) {
assert_eq!(a.name, b.name, "NAME slot must agree across cells");
assert_eq!(a.spec.name, b.spec.name, ":name spec must agree");
}
assert_eq!(via_expand_to_named[0].name, "alpha-compiler");
assert_eq!(via_expand_to_named[0].spec.name, "x");
assert_eq!(via_expand_to_named[1].name, "beta-compiler");
assert_eq!(via_expand_to_named[1].spec.name, "y");
}
#[test]
fn expander_default_max_expansion_depth_is_the_module_constant() {
assert_eq!(
Expander::new().max_expansion_depth(),
DEFAULT_MAX_EXPANSION_DEPTH
);
assert_eq!(
Expander::new_substitute_only().max_expansion_depth(),
DEFAULT_MAX_EXPANSION_DEPTH
);
assert_eq!(DEFAULT_MAX_EXPANSION_DEPTH, 256);
}
#[test]
fn set_max_expansion_depth_takes_effect_on_subsequent_expand() {
let mut e = Expander::new();
e.set_max_expansion_depth(7);
assert_eq!(e.max_expansion_depth(), 7);
}
#[test]
fn expand_recursive_macro_rejects_at_depth_limit_bytecode_path() {
let mut e = Expander::new();
e.set_max_expansion_depth(4);
let forms = read("(defmacro loop (x) `(loop ,x)) (loop hello)").unwrap();
let err = e.expand_program(forms).unwrap_err();
match err {
LispError::ExpansionDepthExceeded { macro_name, limit } => {
assert_eq!(macro_name, "loop");
assert_eq!(limit, 4);
}
other => panic!("expected ExpansionDepthExceeded, got: {other:?}"),
}
}
#[test]
fn expand_recursive_macro_rejects_at_depth_limit_substitute_path() {
let mut e = Expander::new_substitute_only();
e.set_max_expansion_depth(4);
let forms = read("(defmacro loop (x) `(loop ,x)) (loop hello)").unwrap();
let err = e.expand_program(forms).unwrap_err();
match err {
LispError::ExpansionDepthExceeded { macro_name, limit } => {
assert_eq!(macro_name, "loop");
assert_eq!(limit, 4);
}
other => panic!("expected ExpansionDepthExceeded, got: {other:?}"),
}
}
#[test]
fn expand_lawful_nested_macros_within_ceiling_succeed() {
let mut e = Expander::new();
e.set_max_expansion_depth(3);
let forms = read(
"(defmacro twice (x) `(list ,x ,x))
(twice (twice hey))",
)
.unwrap();
let out = e.expand_program(forms).unwrap();
assert_eq!(out[0], parse("(list (list hey hey) (list hey hey))"));
}
#[test]
fn expand_depth_ceiling_ignores_lawful_tree_nesting_depth() {
let mut e = Expander::new();
e.set_max_expansion_depth(2);
let forms = read("(a (b (c (d (e f)))))").unwrap();
let out = e.expand_program(forms).unwrap();
assert_eq!(out[0], parse("(a (b (c (d (e f)))))"));
}
#[test]
fn expansion_depth_exceeded_position_is_none() {
let err = LispError::ExpansionDepthExceeded {
macro_name: "loop".to_string(),
limit: 256,
};
assert_eq!(err.position(), None);
}
#[test]
fn expansion_depth_exceeded_display_matches_typed_variant_shape() {
let err = LispError::ExpansionDepthExceeded {
macro_name: "loop".to_string(),
limit: 256,
};
let rendered = err.to_string();
assert!(rendered.contains("loop"), "rendered: {rendered}");
assert!(rendered.contains("256"), "rendered: {rendered}");
assert!(
rendered.contains("macro expansion depth exceeded"),
"rendered: {rendered}"
);
}
#[test]
fn expander_default_max_cache_entries_is_the_module_constant() {
assert_eq!(
Expander::new().max_cache_entries(),
DEFAULT_MAX_CACHE_ENTRIES
);
assert_eq!(
Expander::new_substitute_only().max_cache_entries(),
DEFAULT_MAX_CACHE_ENTRIES
);
assert_eq!(DEFAULT_MAX_CACHE_ENTRIES, 8192);
}
#[test]
fn set_max_cache_entries_takes_effect_on_subsequent_expand() {
let mut e = Expander::new();
e.set_max_cache_entries(7);
assert_eq!(e.max_cache_entries(), 7);
}
#[test]
fn expand_cache_size_is_bounded_by_max_cache_entries() {
let mut e = Expander::new();
e.set_max_cache_entries(2);
let src = "
(defmacro id (x) `,x)
(id one)
(id two)
(id three)
(id four)
(id five)
";
let out = e.expand_program(read(src).unwrap()).unwrap();
assert_eq!(out.len(), 5);
assert_eq!(out[0], parse("one"));
assert_eq!(out[1], parse("two"));
assert_eq!(out[2], parse("three"));
assert_eq!(out[3], parse("four"));
assert_eq!(out[4], parse("five"));
assert_eq!(
e.cache_size(),
2,
"cache grew past the max_cache_entries ceiling",
);
}
#[test]
fn expand_zero_cache_ceiling_disables_caching_effectively() {
let mut e = Expander::new();
e.set_max_cache_entries(0);
let src = "
(defmacro id (x) `,x)
(id one)
(id two)
";
let out = e.expand_program(read(src).unwrap()).unwrap();
assert_eq!(out.len(), 2);
assert_eq!(out[0], parse("one"));
assert_eq!(out[1], parse("two"));
assert_eq!(
e.cache_size(),
0,
"zero cache ceiling grew the cache — the ceiling must be respected on every insert",
);
}
#[test]
fn expand_cached_hits_survive_past_the_ceiling() {
let mut e = Expander::new();
e.set_max_cache_entries(2);
let src = "
(defmacro id (x) `,x)
(id one)
(id two)
(id one)
(id two)
";
let out = e.expand_program(read(src).unwrap()).unwrap();
assert_eq!(out.len(), 4);
assert_eq!(out[0], parse("one"));
assert_eq!(out[1], parse("two"));
assert_eq!(out[2], parse("one"));
assert_eq!(out[3], parse("two"));
assert_eq!(
e.cache_size(),
2,
"cache grew past the max_cache_entries ceiling on repeated (name, args) pairs",
);
}
#[test]
fn clear_cache_reopens_the_insert_path_after_the_ceiling_was_hit() {
let mut e = Expander::new();
e.set_max_cache_entries(1);
let src_fill = "
(defmacro id (x) `,x)
(id one)
(id two)
";
let _ = e.expand_program(read(src_fill).unwrap()).unwrap();
assert_eq!(e.cache_size(), 1, "cache did not stop at the ceiling");
e.clear_cache();
assert_eq!(e.cache_size(), 0, "clear_cache did not empty the memo");
let out = e
.expand_program(read("(defmacro id (x) `,x) (id three)").unwrap())
.unwrap();
assert_eq!(out.len(), 1);
assert_eq!(out[0], parse("three"));
assert_eq!(
e.cache_size(),
1,
"clear_cache did not re-open the insert path — the cache should have accepted the fresh (id, three) pair",
);
}
#[test]
fn expander_default_max_expansion_size_is_the_module_constant() {
assert_eq!(
Expander::new().max_expansion_size(),
DEFAULT_MAX_EXPANSION_SIZE
);
assert_eq!(
Expander::new_substitute_only().max_expansion_size(),
DEFAULT_MAX_EXPANSION_SIZE
);
assert_eq!(DEFAULT_MAX_EXPANSION_SIZE, 65_536);
}
#[test]
fn set_max_expansion_size_takes_effect_on_subsequent_expand() {
let mut e = Expander::new();
e.set_max_expansion_size(64);
assert_eq!(e.max_expansion_size(), 64);
}
#[test]
fn expand_expansion_bomb_rejects_at_size_limit_bytecode_path() {
let mut e = Expander::new();
e.set_max_expansion_size(4);
let forms = read("(defmacro bomb (x) `(list ,x ,x ,x ,x)) (bomb hey)").unwrap();
let err = e.expand_program(forms).unwrap_err();
match err {
LispError::ExpansionSizeExceeded {
macro_name,
size,
limit,
} => {
assert_eq!(macro_name, "bomb");
assert_eq!(size, 6);
assert_eq!(limit, 4);
}
other => panic!("expected ExpansionSizeExceeded, got: {other:?}"),
}
}
#[test]
fn expand_expansion_bomb_rejects_at_size_limit_substitute_path() {
let mut e = Expander::new_substitute_only();
e.set_max_expansion_size(4);
let forms = read("(defmacro bomb (x) `(list ,x ,x ,x ,x)) (bomb hey)").unwrap();
let err = e.expand_program(forms).unwrap_err();
match err {
LispError::ExpansionSizeExceeded {
macro_name,
size,
limit,
} => {
assert_eq!(macro_name, "bomb");
assert_eq!(size, 6);
assert_eq!(limit, 4);
}
other => panic!("expected ExpansionSizeExceeded, got: {other:?}"),
}
}
#[test]
fn expand_lawful_output_within_size_ceiling_succeeds() {
let mut e = Expander::new();
e.set_max_expansion_size(4);
let forms = read("(defmacro twice (x) `(list ,x ,x)) (twice hey)").unwrap();
let out = e.expand_program(forms).unwrap();
assert_eq!(out[0], parse("(list hey hey)"));
}
#[test]
fn expand_size_ceiling_ignores_lawful_tree_nesting_size() {
let mut e = Expander::new();
e.set_max_expansion_size(4);
let forms = read("(a (b (c d)))").unwrap();
let out = e.expand_program(forms).unwrap();
assert_eq!(out[0], parse("(a (b (c d)))"));
}
#[test]
fn expansion_size_exceeded_position_is_none() {
let err = LispError::ExpansionSizeExceeded {
macro_name: "bomb".to_string(),
size: 512,
limit: 256,
};
assert_eq!(err.position(), None);
}
#[test]
fn expansion_size_exceeded_display_matches_typed_variant_shape() {
let err = LispError::ExpansionSizeExceeded {
macro_name: "bomb".to_string(),
size: 512,
limit: 256,
};
let rendered = err.to_string();
assert!(rendered.contains("bomb"), "rendered: {rendered}");
assert!(rendered.contains("512"), "rendered: {rendered}");
assert!(rendered.contains("256"), "rendered: {rendered}");
assert!(
rendered.contains("macro expansion output size exceeded"),
"rendered: {rendered}"
);
}
#[test]
fn expand_size_ceiling_names_the_offending_macro_in_a_nested_expansion() {
let mut e = Expander::new();
e.set_max_expansion_size(4);
let src = "
(defmacro bomb (x) `(list ,x ,x ,x ,x))
(defmacro wrapper (x) `(bomb ,x))
(wrapper hey)
";
let err = e.expand_program(read(src).unwrap()).unwrap_err();
match err {
LispError::ExpansionSizeExceeded {
macro_name,
size,
limit,
} => {
assert_eq!(macro_name, "bomb");
assert_eq!(size, 6);
assert_eq!(limit, 4);
}
other => panic!("expected ExpansionSizeExceeded, got: {other:?}"),
}
}
#[test]
fn expander_default_max_macro_body_size_is_the_module_constant() {
assert_eq!(
Expander::new().max_macro_body_size(),
DEFAULT_MAX_MACRO_BODY_SIZE
);
assert_eq!(
Expander::new_substitute_only().max_macro_body_size(),
DEFAULT_MAX_MACRO_BODY_SIZE
);
assert_eq!(DEFAULT_MAX_MACRO_BODY_SIZE, 16_384);
}
#[test]
fn set_max_macro_body_size_takes_effect_on_subsequent_register() {
let mut e = Expander::new();
e.set_max_macro_body_size(64);
assert_eq!(e.max_macro_body_size(), 64);
}
#[test]
fn register_macro_body_bomb_rejects_at_body_size_limit_bytecode_path() {
let mut e = Expander::new();
e.set_max_macro_body_size(4);
let forms = read("(defmacro huge (x) `(list a b c d e))").unwrap();
let err = e.expand_program(forms).unwrap_err();
match err {
LispError::MacroBodySizeExceeded {
macro_name,
size,
limit,
} => {
assert_eq!(macro_name, "huge");
assert_eq!(size, 8);
assert_eq!(limit, 4);
}
other => panic!("expected MacroBodySizeExceeded, got: {other:?}"),
}
}
#[test]
fn register_macro_body_bomb_rejects_at_body_size_limit_substitute_path() {
let mut e = Expander::new_substitute_only();
e.set_max_macro_body_size(4);
let forms = read("(defmacro huge (x) `(list a b c d e))").unwrap();
let err = e.expand_program(forms).unwrap_err();
match err {
LispError::MacroBodySizeExceeded {
macro_name,
size,
limit,
} => {
assert_eq!(macro_name, "huge");
assert_eq!(size, 8);
assert_eq!(limit, 4);
}
other => panic!("expected MacroBodySizeExceeded, got: {other:?}"),
}
}
#[test]
fn register_macro_body_at_ceiling_admits() {
let mut e = Expander::new();
e.set_max_macro_body_size(5);
let forms = read("(defmacro twice (x) `(list ,x)) (twice hey)").unwrap();
let out = e.expand_program(forms).unwrap();
assert_eq!(out[0], parse("(list hey)"));
}
#[test]
fn register_failure_leaves_both_tables_pristine() {
let mut e = Expander::new();
e.set_max_macro_body_size(4);
let forms = read("(defmacro huge (x) `(list a b c d e))").unwrap();
assert!(e.expand_program(forms).is_err());
assert!(
!e.has("huge"),
"macros table must stay pristine after rejection"
);
assert_eq!(e.len(), 0);
}
#[test]
fn macro_body_size_exceeded_position_is_none() {
let err = LispError::MacroBodySizeExceeded {
macro_name: "huge".to_string(),
size: 512,
limit: 256,
};
assert_eq!(err.position(), None);
}
#[test]
fn macro_body_size_exceeded_display_matches_typed_variant_shape() {
let err = LispError::MacroBodySizeExceeded {
macro_name: "huge".to_string(),
size: 512,
limit: 256,
};
let rendered = err.to_string();
assert!(rendered.contains("huge"), "rendered: {rendered}");
assert!(rendered.contains("512"), "rendered: {rendered}");
assert!(rendered.contains("256"), "rendered: {rendered}");
assert!(
rendered.contains("macro body size exceeded"),
"rendered: {rendered}"
);
}
#[test]
fn lawful_macro_body_within_ceiling_registers_and_expands() {
let mut e = Expander::new();
let forms = read(
"(defmacro when (cond x) `(if ,cond ,x))
(when #t hey)",
)
.unwrap();
let out = e.expand_program(forms).unwrap();
assert_eq!(out[0], parse("(if #t hey)"));
}
#[test]
fn register_macro_def_direct_call_respects_body_size_ceiling() {
let mut e = Expander::new();
e.set_max_macro_body_size(4);
let def = MacroDef {
name: "huge".to_string(),
params: MacroParams {
required: vec!["x".to_string()],
optional: Vec::new(),
rest: None,
},
body: Sexp::List(vec![
Sexp::Atom(crate::ast::Atom::Symbol("a".to_string())),
Sexp::Atom(crate::ast::Atom::Symbol("b".to_string())),
Sexp::Atom(crate::ast::Atom::Symbol("c".to_string())),
Sexp::Atom(crate::ast::Atom::Symbol("d".to_string())),
Sexp::Atom(crate::ast::Atom::Symbol("e".to_string())),
]),
};
let err = e.register_macro_def(def).unwrap_err();
match err {
LispError::MacroBodySizeExceeded {
macro_name,
size,
limit,
} => {
assert_eq!(macro_name, "huge");
assert_eq!(size, 6);
assert_eq!(limit, 4);
}
other => panic!("expected MacroBodySizeExceeded, got: {other:?}"),
}
assert!(!e.has("huge"));
}
#[test]
fn expander_default_max_registered_macros_is_the_module_constant() {
assert_eq!(
Expander::new().max_registered_macros(),
DEFAULT_MAX_REGISTERED_MACROS
);
assert_eq!(
Expander::new_substitute_only().max_registered_macros(),
DEFAULT_MAX_REGISTERED_MACROS
);
assert_eq!(DEFAULT_MAX_REGISTERED_MACROS, 4096);
}
#[test]
fn set_max_registered_macros_takes_effect_on_subsequent_register() {
let mut e = Expander::new();
e.set_max_registered_macros(64);
assert_eq!(e.max_registered_macros(), 64);
}
#[test]
fn register_fresh_macro_past_table_ceiling_rejects_bytecode_path() {
let mut e = Expander::new();
e.set_max_registered_macros(2);
let forms = read(
"(defmacro a (x) `(list ,x))
(defmacro b (x) `(list ,x))
(defmacro c (x) `(list ,x))",
)
.unwrap();
let err = e.expand_program(forms).unwrap_err();
match err {
LispError::RegisteredMacrosExceeded {
macro_name,
count,
limit,
} => {
assert_eq!(macro_name, "c");
assert_eq!(count, 2);
assert_eq!(limit, 2);
}
other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
}
assert!(e.has("a"));
assert!(e.has("b"));
assert!(!e.has("c"));
assert_eq!(e.len(), 2);
}
#[test]
fn register_fresh_macro_past_table_ceiling_rejects_substitute_path() {
let mut e = Expander::new_substitute_only();
e.set_max_registered_macros(2);
let forms = read(
"(defmacro a (x) `(list ,x))
(defmacro b (x) `(list ,x))
(defmacro c (x) `(list ,x))",
)
.unwrap();
let err = e.expand_program(forms).unwrap_err();
match err {
LispError::RegisteredMacrosExceeded {
macro_name,
count,
limit,
} => {
assert_eq!(macro_name, "c");
assert_eq!(count, 2);
assert_eq!(limit, 2);
}
other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
}
assert!(e.has("a"));
assert!(e.has("b"));
assert!(!e.has("c"));
assert_eq!(e.len(), 2);
}
#[test]
fn register_overwrite_of_existing_key_at_table_ceiling_admits() {
let mut e = Expander::new();
e.expand_program(
read(
"(defmacro a (x) `(list ,x))
(defmacro b (x) `(list ,x))",
)
.unwrap(),
)
.unwrap();
assert_eq!(e.len(), 2);
e.set_max_registered_macros(2);
let forms = read("(defmacro a (x y) `(pair ,x ,y))").unwrap();
e.expand_program(forms).unwrap();
assert_eq!(e.len(), 2, "overwrite must not grow the table");
let expanded = e.expand_program(read("(a foo bar)").unwrap()).unwrap();
assert_eq!(expanded[0], parse("(pair foo bar)"));
}
#[test]
fn register_at_table_ceiling_admits_up_to_but_not_past() {
let mut e = Expander::new();
e.set_max_registered_macros(3);
e.expand_program(
read(
"(defmacro a (x) `(list ,x))
(defmacro b (x) `(list ,x))
(defmacro c (x) `(list ,x))",
)
.unwrap(),
)
.unwrap();
assert_eq!(e.len(), 3);
let err = e
.expand_program(read("(defmacro d (x) `(list ,x))").unwrap())
.unwrap_err();
match err {
LispError::RegisteredMacrosExceeded {
macro_name,
count,
limit,
} => {
assert_eq!(macro_name, "d");
assert_eq!(count, 3);
assert_eq!(limit, 3);
}
other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
}
assert_eq!(e.len(), 3);
assert!(!e.has("d"));
}
#[test]
fn register_table_ceiling_leaves_both_tables_pristine_on_rejection() {
let mut e = Expander::new();
e.set_max_registered_macros(1);
e.expand_program(read("(defmacro a (x) `(list ,x))").unwrap())
.unwrap();
assert!(e.has("a"));
let err = e
.expand_program(read("(defmacro b (x) `(list ,x))").unwrap())
.unwrap_err();
assert!(matches!(err, LispError::RegisteredMacrosExceeded { .. }));
assert_eq!(e.len(), 1);
assert!(e.has("a"));
assert!(!e.has("b"));
let expanded = e.expand_program(read("(a hey)").unwrap()).unwrap();
assert_eq!(expanded[0], parse("(list hey)"));
}
#[test]
fn set_max_registered_macros_zero_rejects_every_fresh_registration() {
let mut e = Expander::new();
e.set_max_registered_macros(0);
let err = e
.expand_program(read("(defmacro a (x) `(list ,x))").unwrap())
.unwrap_err();
match err {
LispError::RegisteredMacrosExceeded {
macro_name,
count,
limit,
} => {
assert_eq!(macro_name, "a");
assert_eq!(count, 0);
assert_eq!(limit, 0);
}
other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
}
assert!(e.is_empty());
}
#[test]
fn registered_macros_exceeded_position_is_none() {
let err = LispError::RegisteredMacrosExceeded {
macro_name: "fresh-1000".to_string(),
count: 4096,
limit: 4096,
};
assert_eq!(err.position(), None);
}
#[test]
fn registered_macros_exceeded_display_matches_typed_variant_shape() {
let err = LispError::RegisteredMacrosExceeded {
macro_name: "fresh-1000".to_string(),
count: 4096,
limit: 4096,
};
let rendered = err.to_string();
assert!(rendered.contains("fresh-1000"), "rendered: {rendered}");
assert!(rendered.contains("4096"), "rendered: {rendered}");
assert!(
rendered.contains("registered macros count exceeded"),
"rendered: {rendered}"
);
}
#[test]
fn lawful_typescape_within_ceiling_registers_and_expands() {
let mut e = Expander::new();
let forms = read(
"(defmacro when (cond x) `(if ,cond ,x))
(defmacro twice (x) `(list ,x ,x))
(defmacro pair (x y) `(list ,x ,y))
(when #t (twice hey))",
)
.unwrap();
let out = e.expand_program(forms).unwrap();
assert_eq!(out[0], parse("(if #t (list hey hey))"));
assert_eq!(e.len(), 3);
}
#[test]
fn register_macro_def_direct_call_respects_table_ceiling() {
let mut e = Expander::new();
e.set_max_registered_macros(1);
let def_a = MacroDef {
name: "a".to_string(),
params: MacroParams {
required: vec!["x".to_string()],
optional: Vec::new(),
rest: None,
},
body: Sexp::Atom(crate::ast::Atom::Symbol("y".to_string())),
};
let def_b = MacroDef {
name: "b".to_string(),
params: MacroParams {
required: vec!["x".to_string()],
optional: Vec::new(),
rest: None,
},
body: Sexp::Atom(crate::ast::Atom::Symbol("y".to_string())),
};
e.register_macro_def(def_a).unwrap();
assert!(e.has("a"));
assert_eq!(e.len(), 1);
let err = e.register_macro_def(def_b).unwrap_err();
match err {
LispError::RegisteredMacrosExceeded {
macro_name,
count,
limit,
} => {
assert_eq!(macro_name, "b");
assert_eq!(count, 1);
assert_eq!(limit, 1);
}
other => panic!("expected RegisteredMacrosExceeded, got: {other:?}"),
}
assert!(!e.has("b"));
assert_eq!(e.len(), 1);
}
#[test]
fn register_macro_def_direct_call_admits_overwrite_at_table_ceiling() {
let mut e = Expander::new();
e.set_max_registered_macros(1);
let def_a_v1 = MacroDef {
name: "a".to_string(),
params: MacroParams {
required: vec!["x".to_string()],
optional: Vec::new(),
rest: None,
},
body: Sexp::Atom(crate::ast::Atom::Symbol("y".to_string())),
};
let def_a_v2 = MacroDef {
name: "a".to_string(),
params: MacroParams {
required: vec!["x".to_string(), "y".to_string()],
optional: Vec::new(),
rest: None,
},
body: Sexp::Atom(crate::ast::Atom::Symbol("z".to_string())),
};
e.register_macro_def(def_a_v1).unwrap();
assert_eq!(e.len(), 1);
e.register_macro_def(def_a_v2).unwrap();
assert_eq!(e.len(), 1);
}
#[test]
fn macro_params_total_arity_matches_names_len() {
let params = MacroParams {
required: vec!["a".into(), "b".into()],
optional: vec![OptionalParam::bare("c"), OptionalParam::bare("d")],
rest: Some("e".into()),
};
assert_eq!(params.total_arity(), params.names().len());
assert_eq!(params.total_arity(), 5);
}
#[test]
fn macro_params_total_arity_equals_fixed_arity_plus_rest_bit() {
let rest_none = MacroParams {
required: vec!["a".into()],
optional: vec![OptionalParam::bare("b")],
rest: None,
};
assert_eq!(rest_none.total_arity(), rest_none.fixed_arity());
assert_eq!(rest_none.total_arity(), 2);
let rest_some = MacroParams {
required: vec!["a".into()],
optional: vec![OptionalParam::bare("b")],
rest: Some("r".into()),
};
assert_eq!(rest_some.total_arity(), rest_some.fixed_arity() + 1);
assert_eq!(rest_some.total_arity(), 3);
}
#[test]
fn macro_params_total_arity_is_zero_for_the_empty_param_list() {
let params = MacroParams::default();
assert_eq!(params.total_arity(), 0);
}
#[test]
fn expander_default_max_macro_arity_is_the_module_constant() {
assert_eq!(Expander::new().max_macro_arity(), DEFAULT_MAX_MACRO_ARITY);
assert_eq!(
Expander::new_substitute_only().max_macro_arity(),
DEFAULT_MAX_MACRO_ARITY
);
assert_eq!(DEFAULT_MAX_MACRO_ARITY, 128);
}
#[test]
fn set_max_macro_arity_takes_effect_on_subsequent_register() {
let mut e = Expander::new();
e.set_max_macro_arity(16);
assert_eq!(e.max_macro_arity(), 16);
}
#[test]
fn register_arity_bomb_rejects_at_arity_limit_bytecode_path() {
let mut e = Expander::new();
e.set_max_macro_arity(2);
let forms = read("(defmacro huge (a b c) `,a)").unwrap();
let err = e.expand_program(forms).unwrap_err();
match err {
LispError::MacroArityExceeded {
macro_name,
arity,
limit,
} => {
assert_eq!(macro_name, "huge");
assert_eq!(arity, 3);
assert_eq!(limit, 2);
}
other => panic!("expected MacroArityExceeded, got: {other:?}"),
}
assert!(!e.has("huge"));
}
#[test]
fn register_arity_bomb_rejects_at_arity_limit_substitute_path() {
let mut e = Expander::new_substitute_only();
e.set_max_macro_arity(2);
let forms = read("(defmacro huge (a b c) `,a)").unwrap();
let err = e.expand_program(forms).unwrap_err();
match err {
LispError::MacroArityExceeded {
macro_name,
arity,
limit,
} => {
assert_eq!(macro_name, "huge");
assert_eq!(arity, 3);
assert_eq!(limit, 2);
}
other => panic!("expected MacroArityExceeded, got: {other:?}"),
}
assert!(!e.has("huge"));
}
#[test]
fn register_arity_gate_counts_optional_slots() {
let mut e = Expander::new();
e.set_max_macro_arity(2);
let forms = read("(defmacro foo (a &optional b c) `,a)").unwrap();
let err = e.expand_program(forms).unwrap_err();
match err {
LispError::MacroArityExceeded {
macro_name,
arity,
limit,
} => {
assert_eq!(macro_name, "foo");
assert_eq!(arity, 3);
assert_eq!(limit, 2);
}
other => panic!("expected MacroArityExceeded, got: {other:?}"),
}
}
#[test]
fn register_arity_gate_counts_rest_slot() {
let mut e = Expander::new();
e.set_max_macro_arity(2);
let forms = read("(defmacro spread (a b &rest r) `,a)").unwrap();
let err = e.expand_program(forms).unwrap_err();
match err {
LispError::MacroArityExceeded {
macro_name,
arity,
limit,
} => {
assert_eq!(macro_name, "spread");
assert_eq!(arity, 3);
assert_eq!(limit, 2);
}
other => panic!("expected MacroArityExceeded, got: {other:?}"),
}
}
#[test]
fn register_arity_at_ceiling_admits() {
let mut e = Expander::new();
e.set_max_macro_arity(2);
let forms = read("(defmacro pair (x y) `(list ,x ,y)) (pair a b)").unwrap();
let out = e.expand_program(forms).unwrap();
assert_eq!(out[0], parse("(list a b)"));
assert_eq!(e.len(), 1);
}
#[test]
fn register_arity_ceiling_leaves_both_tables_pristine_on_rejection() {
let mut e = Expander::new();
e.set_max_macro_arity(1);
let forms = read("(defmacro huge (a b c) `,a)").unwrap();
assert!(e.expand_program(forms).is_err());
assert!(
!e.has("huge"),
"macros table must stay pristine after rejection"
);
assert_eq!(e.len(), 0);
let out = e
.expand_program(read("(defmacro id (x) `,x) (id hey)").unwrap())
.unwrap();
assert_eq!(out[0], parse("hey"));
}
#[test]
fn set_max_macro_arity_zero_rejects_every_non_nullary_registration() {
let mut e = Expander::new();
e.set_max_macro_arity(0);
e.expand_program(read("(defmacro nullary () `unit)").unwrap())
.unwrap();
assert!(e.has("nullary"));
let err = e
.expand_program(read("(defmacro id (x) `,x)").unwrap())
.unwrap_err();
match err {
LispError::MacroArityExceeded {
macro_name,
arity,
limit,
} => {
assert_eq!(macro_name, "id");
assert_eq!(arity, 1);
assert_eq!(limit, 0);
}
other => panic!("expected MacroArityExceeded, got: {other:?}"),
}
assert!(!e.has("id"));
}
#[test]
fn macro_arity_exceeded_position_is_none() {
let err = LispError::MacroArityExceeded {
macro_name: "huge".to_string(),
arity: 512,
limit: 128,
};
assert_eq!(err.position(), None);
}
#[test]
fn macro_arity_exceeded_display_matches_typed_variant_shape() {
let err = LispError::MacroArityExceeded {
macro_name: "huge".to_string(),
arity: 512,
limit: 128,
};
let rendered = err.to_string();
assert!(rendered.contains("huge"), "rendered: {rendered}");
assert!(rendered.contains("512"), "rendered: {rendered}");
assert!(rendered.contains("128"), "rendered: {rendered}");
assert!(
rendered.contains("macro arity exceeded"),
"rendered: {rendered}"
);
}
#[test]
fn lawful_macro_arity_within_ceiling_registers_and_expands() {
let mut e = Expander::new();
let forms = read(
"(defmacro when (cond x) `(if ,cond ,x))
(defmacro triple (x y z) `(list ,x ,y ,z))
(when #t (triple a b c))",
)
.unwrap();
let out = e.expand_program(forms).unwrap();
assert_eq!(out[0], parse("(if #t (list a b c))"));
}
#[test]
fn register_macro_def_direct_call_respects_arity_ceiling() {
let mut e = Expander::new();
e.set_max_macro_arity(2);
let def = MacroDef {
name: "huge".to_string(),
params: MacroParams {
required: vec!["a".to_string(), "b".to_string(), "c".to_string()],
optional: Vec::new(),
rest: None,
},
body: Sexp::Atom(crate::ast::Atom::Symbol("a".to_string())),
};
let err = e.register_macro_def(def).unwrap_err();
match err {
LispError::MacroArityExceeded {
macro_name,
arity,
limit,
} => {
assert_eq!(macro_name, "huge");
assert_eq!(arity, 3);
assert_eq!(limit, 2);
}
other => panic!("expected MacroArityExceeded, got: {other:?}"),
}
assert!(!e.has("huge"));
}
#[test]
fn arity_gate_fires_before_body_size_gate() {
let mut e = Expander::new();
e.set_max_macro_arity(1);
e.set_max_macro_body_size(2);
let forms = read("(defmacro dual (a b) `(list ,a ,b))").unwrap();
let err = e.expand_program(forms).unwrap_err();
assert!(
matches!(err, LispError::MacroArityExceeded { .. }),
"arity gate must fire before body-size gate; got: {err:?}"
);
}
#[test]
fn arity_gate_fires_after_table_count_gate() {
let mut e = Expander::new();
e.set_max_registered_macros(1);
e.set_max_macro_arity(1);
e.expand_program(read("(defmacro anchor (x) `,x)").unwrap())
.unwrap();
assert_eq!(e.len(), 1);
let forms = read("(defmacro huge (a b c) `,a)").unwrap();
let err = e.expand_program(forms).unwrap_err();
assert!(
matches!(err, LispError::RegisteredMacrosExceeded { .. }),
"table-count gate must fire before arity gate; got: {err:?}"
);
}
#[test]
fn default_resource_limits_binds_each_field_to_matching_module_constant() {
assert_eq!(
DEFAULT_RESOURCE_LIMITS.max_expansion_depth,
DEFAULT_MAX_EXPANSION_DEPTH
);
assert_eq!(
DEFAULT_RESOURCE_LIMITS.max_cache_entries,
DEFAULT_MAX_CACHE_ENTRIES
);
assert_eq!(
DEFAULT_RESOURCE_LIMITS.max_expansion_size,
DEFAULT_MAX_EXPANSION_SIZE
);
assert_eq!(
DEFAULT_RESOURCE_LIMITS.max_macro_body_size,
DEFAULT_MAX_MACRO_BODY_SIZE
);
assert_eq!(
DEFAULT_RESOURCE_LIMITS.max_registered_macros,
DEFAULT_MAX_REGISTERED_MACROS
);
assert_eq!(
DEFAULT_RESOURCE_LIMITS.max_macro_arity,
DEFAULT_MAX_MACRO_ARITY
);
}
#[test]
fn resource_limits_default_impl_matches_const_form() {
assert_eq!(ResourceLimits::default(), DEFAULT_RESOURCE_LIMITS);
}
#[test]
fn expander_new_resource_limits_matches_shipped_defaults() {
assert_eq!(Expander::new().resource_limits(), DEFAULT_RESOURCE_LIMITS);
}
#[test]
fn expander_new_substitute_only_resource_limits_matches_shipped_defaults() {
assert_eq!(
Expander::new_substitute_only().resource_limits(),
DEFAULT_RESOURCE_LIMITS
);
}
#[test]
fn resource_limits_snapshot_reflects_each_individual_setter() {
let mut e = Expander::new();
e.set_max_expansion_depth(3);
e.set_max_cache_entries(5);
e.set_max_expansion_size(7);
e.set_max_macro_body_size(11);
e.set_max_registered_macros(13);
e.set_max_macro_arity(17);
let snap = e.resource_limits();
assert_eq!(snap.max_expansion_depth, 3);
assert_eq!(snap.max_cache_entries, 5);
assert_eq!(snap.max_expansion_size, 7);
assert_eq!(snap.max_macro_body_size, 11);
assert_eq!(snap.max_registered_macros, 13);
assert_eq!(snap.max_macro_arity, 17);
assert_eq!(e.max_expansion_depth(), 3);
assert_eq!(e.max_cache_entries(), 5);
assert_eq!(e.max_expansion_size(), 7);
assert_eq!(e.max_macro_body_size(), 11);
assert_eq!(e.max_registered_macros(), 13);
assert_eq!(e.max_macro_arity(), 17);
}
#[test]
fn set_resource_limits_bulk_propagates_every_field_to_individual_getters() {
let mut e = Expander::new();
e.set_resource_limits(ResourceLimits {
max_expansion_depth: 2,
max_cache_entries: 4,
max_expansion_size: 8,
max_macro_body_size: 16,
max_registered_macros: 32,
max_macro_arity: 64,
});
assert_eq!(e.max_expansion_depth(), 2);
assert_eq!(e.max_cache_entries(), 4);
assert_eq!(e.max_expansion_size(), 8);
assert_eq!(e.max_macro_body_size(), 16);
assert_eq!(e.max_registered_macros(), 32);
assert_eq!(e.max_macro_arity(), 64);
}
#[test]
fn resource_limits_round_trip_through_bundled_getter_and_setter_is_identity() {
let mut e = Expander::new();
e.set_max_expansion_depth(9);
e.set_max_cache_entries(19);
e.set_max_expansion_size(29);
e.set_max_macro_body_size(39);
e.set_max_registered_macros(49);
e.set_max_macro_arity(59);
let snap = e.resource_limits();
e.set_resource_limits(ResourceLimits::default());
assert_eq!(e.resource_limits(), DEFAULT_RESOURCE_LIMITS);
e.set_resource_limits(snap);
assert_eq!(e.resource_limits(), snap);
}
#[test]
fn resource_limits_struct_update_syntax_overrides_one_ceiling() {
let mut e = Expander::new();
e.set_resource_limits(ResourceLimits {
max_macro_arity: 4,
..DEFAULT_RESOURCE_LIMITS
});
assert_eq!(e.max_macro_arity(), 4);
assert_eq!(e.max_expansion_depth(), DEFAULT_MAX_EXPANSION_DEPTH);
assert_eq!(e.max_cache_entries(), DEFAULT_MAX_CACHE_ENTRIES);
assert_eq!(e.max_expansion_size(), DEFAULT_MAX_EXPANSION_SIZE);
assert_eq!(e.max_macro_body_size(), DEFAULT_MAX_MACRO_BODY_SIZE);
assert_eq!(e.max_registered_macros(), DEFAULT_MAX_REGISTERED_MACROS);
}
#[test]
fn expander_default_derives_resource_limits_from_bundled_field_default() {
assert_eq!(
Expander::default().resource_limits(),
DEFAULT_RESOURCE_LIMITS
);
}
#[test]
fn resource_limits_bulk_setter_keeps_the_arity_gate_in_effect() {
let mut e = Expander::new();
e.set_resource_limits(ResourceLimits {
max_macro_arity: 1,
..DEFAULT_RESOURCE_LIMITS
});
let forms = read("(defmacro two (a b) `,a)").unwrap();
let err = e.expand_program(forms).unwrap_err();
assert!(
matches!(
err,
LispError::MacroArityExceeded {
arity: 2,
limit: 1,
..
}
),
"bulk-set arity ceiling must reach the register-time gate; got: {err:?}"
);
}
#[test]
fn expander_with_limits_seeds_the_provided_resource_posture() {
let want = ResourceLimits {
max_expansion_depth: 3,
max_cache_entries: 5,
max_expansion_size: 7,
max_macro_body_size: 11,
max_registered_macros: 13,
max_macro_arity: 17,
};
assert_eq!(Expander::with_limits(want).resource_limits(), want);
}
#[test]
fn expander_with_default_limits_agrees_with_new_on_resource_posture() {
assert_eq!(
Expander::with_limits(DEFAULT_RESOURCE_LIMITS).resource_limits(),
Expander::new().resource_limits()
);
}
#[test]
fn expander_with_limits_composes_with_struct_update_override() {
let e = Expander::with_limits(ResourceLimits {
max_macro_arity: 4,
..DEFAULT_RESOURCE_LIMITS
});
assert_eq!(e.max_macro_arity(), 4);
assert_eq!(e.max_expansion_depth(), DEFAULT_MAX_EXPANSION_DEPTH);
assert_eq!(e.max_cache_entries(), DEFAULT_MAX_CACHE_ENTRIES);
assert_eq!(e.max_expansion_size(), DEFAULT_MAX_EXPANSION_SIZE);
assert_eq!(e.max_macro_body_size(), DEFAULT_MAX_MACRO_BODY_SIZE);
assert_eq!(e.max_registered_macros(), DEFAULT_MAX_REGISTERED_MACROS);
}
#[test]
fn expander_with_limits_carries_the_new_execution_strategy() {
let mut e = Expander::with_limits(DEFAULT_RESOURCE_LIMITS);
let forms = read(
"(defmacro id (x) `,x)
(id one)
(id one)",
)
.unwrap();
e.expand_program(forms).unwrap();
assert!(
e.cache_size() >= 1,
"with_limits carries the bytecode+cache strategy; cache_size must be >= 1 after repeat calls, got {}",
e.cache_size()
);
}
#[test]
fn expander_with_limits_fires_the_arity_gate_from_construction() {
let mut e = Expander::with_limits(ResourceLimits {
max_macro_arity: 1,
..DEFAULT_RESOURCE_LIMITS
});
let forms = read("(defmacro two (a b) `,a)").unwrap();
let err = e.expand_program(forms).unwrap_err();
assert!(
matches!(
err,
LispError::MacroArityExceeded {
arity: 2,
limit: 1,
..
}
),
"at-construction arity ceiling must reach the register-time gate; got: {err:?}"
);
}
#[test]
fn unbounded_resource_limits_binds_every_ceiling_to_usize_max() {
assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_expansion_depth, usize::MAX);
assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_cache_entries, usize::MAX);
assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_expansion_size, usize::MAX);
assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_macro_body_size, usize::MAX);
assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_registered_macros, usize::MAX);
assert_eq!(UNBOUNDED_RESOURCE_LIMITS.max_macro_arity, usize::MAX);
}
#[test]
fn unbounded_resource_limits_disagrees_with_default_on_every_ceiling() {
assert_ne!(
UNBOUNDED_RESOURCE_LIMITS.max_expansion_depth,
DEFAULT_RESOURCE_LIMITS.max_expansion_depth
);
assert_ne!(
UNBOUNDED_RESOURCE_LIMITS.max_cache_entries,
DEFAULT_RESOURCE_LIMITS.max_cache_entries
);
assert_ne!(
UNBOUNDED_RESOURCE_LIMITS.max_expansion_size,
DEFAULT_RESOURCE_LIMITS.max_expansion_size
);
assert_ne!(
UNBOUNDED_RESOURCE_LIMITS.max_macro_body_size,
DEFAULT_RESOURCE_LIMITS.max_macro_body_size
);
assert_ne!(
UNBOUNDED_RESOURCE_LIMITS.max_registered_macros,
DEFAULT_RESOURCE_LIMITS.max_registered_macros
);
assert_ne!(
UNBOUNDED_RESOURCE_LIMITS.max_macro_arity,
DEFAULT_RESOURCE_LIMITS.max_macro_arity
);
}
#[test]
fn expander_with_unbounded_limits_projects_through_resource_limits_getter() {
assert_eq!(
Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS).resource_limits(),
UNBOUNDED_RESOURCE_LIMITS
);
}
#[test]
fn expander_with_unbounded_limits_admits_a_body_over_the_default_size_ceiling() {
let mut e = Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS);
let mut body = String::from("(defmacro huge (k) `(");
for _ in 0..(DEFAULT_MAX_MACRO_BODY_SIZE + 1) {
body.push_str(",k ");
}
body.push_str("))");
let forms = read(&body).unwrap();
e.expand_program(forms)
.expect("unbounded body-size ceiling must admit a body over the default limit");
assert!(e.has("huge"));
}
#[test]
fn expander_with_unbounded_limits_admits_arity_over_the_default_arity_ceiling() {
use std::fmt::Write as _;
let mut e = Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS);
let mut src = String::from("(defmacro many-arity (");
for i in 0..(DEFAULT_MAX_MACRO_ARITY + 1) {
write!(src, "a-{i} ").unwrap();
}
src.push_str(") `,a-0)");
let forms = read(&src).unwrap();
e.expand_program(forms)
.expect("unbounded arity ceiling must admit a param list over the default limit");
assert!(e.has("many-arity"));
}
#[test]
fn unbounded_resource_limits_composes_via_struct_update_to_isolate_one_ceiling() {
let mut e = Expander::with_limits(ResourceLimits {
max_macro_arity: 1,
..UNBOUNDED_RESOURCE_LIMITS
});
assert_eq!(e.max_macro_arity(), 1);
assert_eq!(e.max_expansion_depth(), usize::MAX);
assert_eq!(e.max_cache_entries(), usize::MAX);
assert_eq!(e.max_expansion_size(), usize::MAX);
assert_eq!(e.max_macro_body_size(), usize::MAX);
assert_eq!(e.max_registered_macros(), usize::MAX);
let forms = read("(defmacro two (a b) `,a)").unwrap();
let err = e.expand_program(forms).unwrap_err();
assert!(
matches!(
err,
LispError::MacroArityExceeded {
arity: 2,
limit: 1,
..
}
),
"struct-update override on the ceiling-lifted preset must \
leave the isolated arity ceiling gating; got: {err:?}"
);
}
#[test]
fn unbounded_resource_limits_carries_through_the_bulk_setter() {
let a = Expander::with_limits(UNBOUNDED_RESOURCE_LIMITS);
let mut b = Expander::new();
b.set_resource_limits(UNBOUNDED_RESOURCE_LIMITS);
assert_eq!(a.resource_limits(), b.resource_limits());
assert_eq!(a.resource_limits(), UNBOUNDED_RESOURCE_LIMITS);
}
const HAND_AUTHORED_MID_POSTURE: ResourceLimits = ResourceLimits {
max_expansion_depth: 7,
max_cache_entries: 11,
max_expansion_size: 13,
max_macro_body_size: 17,
max_registered_macros: 19,
max_macro_arity: 23,
};
const HAND_AUTHORED_OTHER_POSTURE: ResourceLimits = ResourceLimits {
max_expansion_depth: 3, max_cache_entries: 29, max_expansion_size: 5, max_macro_body_size: 31, max_registered_macros: 2, max_macro_arity: 41, };
#[test]
fn resource_limits_strictest_of_default_and_unbounded_projects_the_default() {
assert_eq!(
DEFAULT_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
DEFAULT_RESOURCE_LIMITS,
);
assert_eq!(
UNBOUNDED_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
DEFAULT_RESOURCE_LIMITS,
);
}
#[test]
fn resource_limits_most_permissive_of_default_and_unbounded_projects_the_unbounded() {
assert_eq!(
DEFAULT_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
UNBOUNDED_RESOURCE_LIMITS,
);
assert_eq!(
UNBOUNDED_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
UNBOUNDED_RESOURCE_LIMITS,
);
}
#[test]
fn resource_limits_strictest_takes_pointwise_min_on_every_axis() {
let m = HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE);
assert_eq!(m.max_expansion_depth, 3);
assert_eq!(m.max_cache_entries, 11);
assert_eq!(m.max_expansion_size, 5);
assert_eq!(m.max_macro_body_size, 17);
assert_eq!(m.max_registered_macros, 2);
assert_eq!(m.max_macro_arity, 23);
}
#[test]
fn resource_limits_most_permissive_takes_pointwise_max_on_every_axis() {
let j = HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE);
assert_eq!(j.max_expansion_depth, 7);
assert_eq!(j.max_cache_entries, 29);
assert_eq!(j.max_expansion_size, 13);
assert_eq!(j.max_macro_body_size, 31);
assert_eq!(j.max_registered_macros, 19);
assert_eq!(j.max_macro_arity, 41);
}
#[test]
fn resource_limits_strictest_is_idempotent() {
assert_eq!(
DEFAULT_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
DEFAULT_RESOURCE_LIMITS,
);
assert_eq!(
UNBOUNDED_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
UNBOUNDED_RESOURCE_LIMITS,
);
assert_eq!(
HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_MID_POSTURE),
HAND_AUTHORED_MID_POSTURE,
);
}
#[test]
fn resource_limits_most_permissive_is_idempotent() {
assert_eq!(
DEFAULT_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
DEFAULT_RESOURCE_LIMITS,
);
assert_eq!(
UNBOUNDED_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
UNBOUNDED_RESOURCE_LIMITS,
);
assert_eq!(
HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_MID_POSTURE),
HAND_AUTHORED_MID_POSTURE,
);
}
#[test]
fn resource_limits_strictest_is_commutative() {
assert_eq!(
HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE),
HAND_AUTHORED_OTHER_POSTURE.strictest(HAND_AUTHORED_MID_POSTURE),
);
assert_eq!(
DEFAULT_RESOURCE_LIMITS.strictest(HAND_AUTHORED_MID_POSTURE),
HAND_AUTHORED_MID_POSTURE.strictest(DEFAULT_RESOURCE_LIMITS),
);
}
#[test]
fn resource_limits_most_permissive_is_commutative() {
assert_eq!(
HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE),
HAND_AUTHORED_OTHER_POSTURE.most_permissive(HAND_AUTHORED_MID_POSTURE),
);
assert_eq!(
DEFAULT_RESOURCE_LIMITS.most_permissive(HAND_AUTHORED_MID_POSTURE),
HAND_AUTHORED_MID_POSTURE.most_permissive(DEFAULT_RESOURCE_LIMITS),
);
}
#[test]
fn resource_limits_strictest_is_associative() {
let a = DEFAULT_RESOURCE_LIMITS;
let b = HAND_AUTHORED_MID_POSTURE;
let c = HAND_AUTHORED_OTHER_POSTURE;
assert_eq!(a.strictest(b).strictest(c), a.strictest(b.strictest(c)));
}
#[test]
fn resource_limits_most_permissive_is_associative() {
let a = DEFAULT_RESOURCE_LIMITS;
let b = HAND_AUTHORED_MID_POSTURE;
let c = HAND_AUTHORED_OTHER_POSTURE;
assert_eq!(
a.most_permissive(b).most_permissive(c),
a.most_permissive(b.most_permissive(c)),
);
}
#[test]
fn resource_limits_meet_and_join_satisfy_absorption() {
let a = HAND_AUTHORED_MID_POSTURE;
let b = HAND_AUTHORED_OTHER_POSTURE;
assert_eq!(a.strictest(a.most_permissive(b)), a);
assert_eq!(a.most_permissive(a.strictest(b)), a);
}
#[test]
fn resource_limits_meet_distributes_over_join() {
let a = DEFAULT_RESOURCE_LIMITS;
let b = HAND_AUTHORED_MID_POSTURE;
let c = HAND_AUTHORED_OTHER_POSTURE;
assert_eq!(
a.strictest(b.most_permissive(c)),
a.strictest(b).most_permissive(a.strictest(c)),
);
}
#[test]
fn resource_limits_join_distributes_over_meet() {
let a = DEFAULT_RESOURCE_LIMITS;
let b = HAND_AUTHORED_MID_POSTURE;
let c = HAND_AUTHORED_OTHER_POSTURE;
assert_eq!(
a.most_permissive(b.strictest(c)),
a.most_permissive(b).strictest(a.most_permissive(c)),
);
}
#[test]
fn resource_limits_strictest_is_dominated_by_both_operands_pointwise() {
let a = HAND_AUTHORED_MID_POSTURE;
let b = HAND_AUTHORED_OTHER_POSTURE;
let m = a.strictest(b);
assert!(m.max_expansion_depth <= a.max_expansion_depth);
assert!(m.max_expansion_depth <= b.max_expansion_depth);
assert!(m.max_cache_entries <= a.max_cache_entries);
assert!(m.max_cache_entries <= b.max_cache_entries);
assert!(m.max_expansion_size <= a.max_expansion_size);
assert!(m.max_expansion_size <= b.max_expansion_size);
assert!(m.max_macro_body_size <= a.max_macro_body_size);
assert!(m.max_macro_body_size <= b.max_macro_body_size);
assert!(m.max_registered_macros <= a.max_registered_macros);
assert!(m.max_registered_macros <= b.max_registered_macros);
assert!(m.max_macro_arity <= a.max_macro_arity);
assert!(m.max_macro_arity <= b.max_macro_arity);
}
#[test]
fn resource_limits_most_permissive_dominates_both_operands_pointwise() {
let a = HAND_AUTHORED_MID_POSTURE;
let b = HAND_AUTHORED_OTHER_POSTURE;
let j = a.most_permissive(b);
assert!(j.max_expansion_depth >= a.max_expansion_depth);
assert!(j.max_expansion_depth >= b.max_expansion_depth);
assert!(j.max_cache_entries >= a.max_cache_entries);
assert!(j.max_cache_entries >= b.max_cache_entries);
assert!(j.max_expansion_size >= a.max_expansion_size);
assert!(j.max_expansion_size >= b.max_expansion_size);
assert!(j.max_macro_body_size >= a.max_macro_body_size);
assert!(j.max_macro_body_size >= b.max_macro_body_size);
assert!(j.max_registered_macros >= a.max_registered_macros);
assert!(j.max_registered_macros >= b.max_registered_macros);
assert!(j.max_macro_arity >= a.max_macro_arity);
assert!(j.max_macro_arity >= b.max_macro_arity);
}
#[test]
fn resource_limits_strictest_composes_at_compile_time_via_const_fn() {
const DEFAULT_TIGHTENED_BY_MID: ResourceLimits =
DEFAULT_RESOURCE_LIMITS.strictest(HAND_AUTHORED_MID_POSTURE);
assert_eq!(
DEFAULT_TIGHTENED_BY_MID.max_expansion_depth,
min_usize(
DEFAULT_RESOURCE_LIMITS.max_expansion_depth,
HAND_AUTHORED_MID_POSTURE.max_expansion_depth,
),
);
assert_eq!(DEFAULT_TIGHTENED_BY_MID, HAND_AUTHORED_MID_POSTURE);
}
#[test]
fn resource_limits_most_permissive_composes_at_compile_time_via_const_fn() {
const DEFAULT_LOOSENED_BY_UNBOUNDED: ResourceLimits =
DEFAULT_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS);
assert_eq!(DEFAULT_LOOSENED_BY_UNBOUNDED, UNBOUNDED_RESOURCE_LIMITS);
}
#[test]
fn resource_limits_leq_is_pointwise_field_conjunction() {
const LOOSER: ResourceLimits = ResourceLimits {
max_expansion_depth: HAND_AUTHORED_MID_POSTURE.max_expansion_depth + 1,
max_cache_entries: HAND_AUTHORED_MID_POSTURE.max_cache_entries + 1,
max_expansion_size: HAND_AUTHORED_MID_POSTURE.max_expansion_size + 1,
max_macro_body_size: HAND_AUTHORED_MID_POSTURE.max_macro_body_size + 1,
max_registered_macros: HAND_AUTHORED_MID_POSTURE.max_registered_macros + 1,
max_macro_arity: HAND_AUTHORED_MID_POSTURE.max_macro_arity + 1,
};
assert!(HAND_AUTHORED_MID_POSTURE.leq(LOOSER));
assert!(!LOOSER.leq(HAND_AUTHORED_MID_POSTURE));
let exceed_depth = ResourceLimits {
max_expansion_depth: LOOSER.max_expansion_depth + 1,
..HAND_AUTHORED_MID_POSTURE
};
assert!(!exceed_depth.leq(LOOSER));
let exceed_cache = ResourceLimits {
max_cache_entries: LOOSER.max_cache_entries + 1,
..HAND_AUTHORED_MID_POSTURE
};
assert!(!exceed_cache.leq(LOOSER));
let exceed_expansion = ResourceLimits {
max_expansion_size: LOOSER.max_expansion_size + 1,
..HAND_AUTHORED_MID_POSTURE
};
assert!(!exceed_expansion.leq(LOOSER));
let exceed_body = ResourceLimits {
max_macro_body_size: LOOSER.max_macro_body_size + 1,
..HAND_AUTHORED_MID_POSTURE
};
assert!(!exceed_body.leq(LOOSER));
let exceed_registered = ResourceLimits {
max_registered_macros: LOOSER.max_registered_macros + 1,
..HAND_AUTHORED_MID_POSTURE
};
assert!(!exceed_registered.leq(LOOSER));
let exceed_arity = ResourceLimits {
max_macro_arity: LOOSER.max_macro_arity + 1,
..HAND_AUTHORED_MID_POSTURE
};
assert!(!exceed_arity.leq(LOOSER));
}
#[test]
fn resource_limits_leq_is_reflexive() {
assert!(DEFAULT_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
assert!(UNBOUNDED_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
assert!(HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_MID_POSTURE));
assert!(HAND_AUTHORED_OTHER_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
}
#[test]
fn resource_limits_leq_is_antisymmetric() {
let a = DEFAULT_RESOURCE_LIMITS;
let b = ResourceLimits {
max_expansion_depth: DEFAULT_RESOURCE_LIMITS.max_expansion_depth,
max_cache_entries: DEFAULT_RESOURCE_LIMITS.max_cache_entries,
max_expansion_size: DEFAULT_RESOURCE_LIMITS.max_expansion_size,
max_macro_body_size: DEFAULT_RESOURCE_LIMITS.max_macro_body_size,
max_registered_macros: DEFAULT_RESOURCE_LIMITS.max_registered_macros,
max_macro_arity: DEFAULT_RESOURCE_LIMITS.max_macro_arity,
};
assert!(a.leq(b));
assert!(b.leq(a));
assert_eq!(a, b);
}
#[test]
fn resource_limits_leq_is_transitive() {
const LOOSER: ResourceLimits = ResourceLimits {
max_expansion_depth: HAND_AUTHORED_MID_POSTURE.max_expansion_depth + 1,
max_cache_entries: HAND_AUTHORED_MID_POSTURE.max_cache_entries + 1,
max_expansion_size: HAND_AUTHORED_MID_POSTURE.max_expansion_size + 1,
max_macro_body_size: HAND_AUTHORED_MID_POSTURE.max_macro_body_size + 1,
max_registered_macros: HAND_AUTHORED_MID_POSTURE.max_registered_macros + 1,
max_macro_arity: HAND_AUTHORED_MID_POSTURE.max_macro_arity + 1,
};
const LOOSEST: ResourceLimits = ResourceLimits {
max_expansion_depth: LOOSER.max_expansion_depth + 1,
max_cache_entries: LOOSER.max_cache_entries + 1,
max_expansion_size: LOOSER.max_expansion_size + 1,
max_macro_body_size: LOOSER.max_macro_body_size + 1,
max_registered_macros: LOOSER.max_registered_macros + 1,
max_macro_arity: LOOSER.max_macro_arity + 1,
};
assert!(HAND_AUTHORED_MID_POSTURE.leq(LOOSER));
assert!(LOOSER.leq(LOOSEST));
assert!(HAND_AUTHORED_MID_POSTURE.leq(LOOSEST));
}
#[test]
fn resource_limits_leq_of_default_and_unbounded_is_a_strict_order() {
assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
}
#[test]
fn resource_limits_leq_is_not_total_on_asymmetric_postures() {
assert!(!HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
assert!(!HAND_AUTHORED_OTHER_POSTURE.leq(HAND_AUTHORED_MID_POSTURE));
}
#[test]
fn resource_limits_leq_agrees_with_meet() {
assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
assert_eq!(
DEFAULT_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
DEFAULT_RESOURCE_LIMITS,
);
assert_eq!(
DEFAULT_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
DEFAULT_RESOURCE_LIMITS,
);
assert!(DEFAULT_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
assert!(!HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
assert_ne!(
HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE),
HAND_AUTHORED_MID_POSTURE,
);
}
#[test]
fn resource_limits_leq_agrees_with_join() {
assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
assert_eq!(
DEFAULT_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
UNBOUNDED_RESOURCE_LIMITS,
);
assert_eq!(
DEFAULT_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
DEFAULT_RESOURCE_LIMITS,
);
assert!(DEFAULT_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
assert!(!HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_OTHER_POSTURE));
assert_ne!(
HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE),
HAND_AUTHORED_OTHER_POSTURE,
);
}
#[test]
fn resource_limits_strictest_is_leq_both_operands() {
let a = HAND_AUTHORED_MID_POSTURE;
let b = HAND_AUTHORED_OTHER_POSTURE;
let m = a.strictest(b);
assert!(m.leq(a));
assert!(m.leq(b));
}
#[test]
fn resource_limits_most_permissive_is_geq_both_operands() {
let a = HAND_AUTHORED_MID_POSTURE;
let b = HAND_AUTHORED_OTHER_POSTURE;
let j = a.most_permissive(b);
assert!(a.leq(j));
assert!(b.leq(j));
}
#[test]
fn resource_limits_leq_evaluates_at_compile_time_via_const_fn() {
const _: () = assert!(DEFAULT_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
}
#[test]
fn empty_resource_limits_binds_every_ceiling_to_zero() {
assert_eq!(EMPTY_RESOURCE_LIMITS.max_expansion_depth, 0);
assert_eq!(EMPTY_RESOURCE_LIMITS.max_cache_entries, 0);
assert_eq!(EMPTY_RESOURCE_LIMITS.max_expansion_size, 0);
assert_eq!(EMPTY_RESOURCE_LIMITS.max_macro_body_size, 0);
assert_eq!(EMPTY_RESOURCE_LIMITS.max_registered_macros, 0);
assert_eq!(EMPTY_RESOURCE_LIMITS.max_macro_arity, 0);
}
#[test]
fn empty_resource_limits_disagrees_with_default_on_every_ceiling() {
assert_ne!(
EMPTY_RESOURCE_LIMITS.max_expansion_depth,
DEFAULT_RESOURCE_LIMITS.max_expansion_depth
);
assert_ne!(
EMPTY_RESOURCE_LIMITS.max_cache_entries,
DEFAULT_RESOURCE_LIMITS.max_cache_entries
);
assert_ne!(
EMPTY_RESOURCE_LIMITS.max_expansion_size,
DEFAULT_RESOURCE_LIMITS.max_expansion_size
);
assert_ne!(
EMPTY_RESOURCE_LIMITS.max_macro_body_size,
DEFAULT_RESOURCE_LIMITS.max_macro_body_size
);
assert_ne!(
EMPTY_RESOURCE_LIMITS.max_registered_macros,
DEFAULT_RESOURCE_LIMITS.max_registered_macros
);
assert_ne!(
EMPTY_RESOURCE_LIMITS.max_macro_arity,
DEFAULT_RESOURCE_LIMITS.max_macro_arity
);
}
#[test]
fn empty_resource_limits_disagrees_with_unbounded_on_every_ceiling() {
assert_ne!(
EMPTY_RESOURCE_LIMITS.max_expansion_depth,
UNBOUNDED_RESOURCE_LIMITS.max_expansion_depth
);
assert_ne!(
EMPTY_RESOURCE_LIMITS.max_cache_entries,
UNBOUNDED_RESOURCE_LIMITS.max_cache_entries
);
assert_ne!(
EMPTY_RESOURCE_LIMITS.max_expansion_size,
UNBOUNDED_RESOURCE_LIMITS.max_expansion_size
);
assert_ne!(
EMPTY_RESOURCE_LIMITS.max_macro_body_size,
UNBOUNDED_RESOURCE_LIMITS.max_macro_body_size
);
assert_ne!(
EMPTY_RESOURCE_LIMITS.max_registered_macros,
UNBOUNDED_RESOURCE_LIMITS.max_registered_macros
);
assert_ne!(
EMPTY_RESOURCE_LIMITS.max_macro_arity,
UNBOUNDED_RESOURCE_LIMITS.max_macro_arity
);
}
#[test]
fn empty_resource_limits_is_the_join_identity() {
assert_eq!(
DEFAULT_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
DEFAULT_RESOURCE_LIMITS,
);
assert_eq!(
EMPTY_RESOURCE_LIMITS.most_permissive(DEFAULT_RESOURCE_LIMITS),
DEFAULT_RESOURCE_LIMITS,
);
assert_eq!(
UNBOUNDED_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
UNBOUNDED_RESOURCE_LIMITS,
);
assert_eq!(
EMPTY_RESOURCE_LIMITS.most_permissive(UNBOUNDED_RESOURCE_LIMITS),
UNBOUNDED_RESOURCE_LIMITS,
);
assert_eq!(
HAND_AUTHORED_MID_POSTURE.most_permissive(EMPTY_RESOURCE_LIMITS),
HAND_AUTHORED_MID_POSTURE,
);
assert_eq!(
HAND_AUTHORED_OTHER_POSTURE.most_permissive(EMPTY_RESOURCE_LIMITS),
HAND_AUTHORED_OTHER_POSTURE,
);
assert_eq!(
EMPTY_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
EMPTY_RESOURCE_LIMITS,
);
}
#[test]
fn empty_resource_limits_is_the_meet_annihilator() {
assert_eq!(
DEFAULT_RESOURCE_LIMITS.strictest(EMPTY_RESOURCE_LIMITS),
EMPTY_RESOURCE_LIMITS,
);
assert_eq!(
EMPTY_RESOURCE_LIMITS.strictest(DEFAULT_RESOURCE_LIMITS),
EMPTY_RESOURCE_LIMITS,
);
assert_eq!(
UNBOUNDED_RESOURCE_LIMITS.strictest(EMPTY_RESOURCE_LIMITS),
EMPTY_RESOURCE_LIMITS,
);
assert_eq!(
EMPTY_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
EMPTY_RESOURCE_LIMITS,
);
assert_eq!(
HAND_AUTHORED_MID_POSTURE.strictest(EMPTY_RESOURCE_LIMITS),
EMPTY_RESOURCE_LIMITS,
);
assert_eq!(
HAND_AUTHORED_OTHER_POSTURE.strictest(EMPTY_RESOURCE_LIMITS),
EMPTY_RESOURCE_LIMITS,
);
}
#[test]
fn empty_resource_limits_is_the_lattice_minimum() {
assert!(EMPTY_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
assert!(EMPTY_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
assert!(EMPTY_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
assert!(EMPTY_RESOURCE_LIMITS.leq(HAND_AUTHORED_MID_POSTURE));
assert!(EMPTY_RESOURCE_LIMITS.leq(HAND_AUTHORED_OTHER_POSTURE));
assert!(!DEFAULT_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
assert!(!HAND_AUTHORED_MID_POSTURE.leq(EMPTY_RESOURCE_LIMITS));
assert!(!HAND_AUTHORED_OTHER_POSTURE.leq(EMPTY_RESOURCE_LIMITS));
}
#[test]
fn empty_resource_limits_composes_at_compile_time_via_const_fn() {
const _: () = assert!(EMPTY_RESOURCE_LIMITS.leq(DEFAULT_RESOURCE_LIMITS));
const _: () = assert!(EMPTY_RESOURCE_LIMITS.leq(UNBOUNDED_RESOURCE_LIMITS));
const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(EMPTY_RESOURCE_LIMITS));
}
#[test]
fn empty_resource_limits_seeds_most_permissive_fold_over_slice() {
let postures: [ResourceLimits; 3] = [
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
DEFAULT_RESOURCE_LIMITS,
];
let joined = postures
.iter()
.copied()
.fold(EMPTY_RESOURCE_LIMITS, ResourceLimits::most_permissive);
assert!(HAND_AUTHORED_MID_POSTURE.leq(joined));
assert!(HAND_AUTHORED_OTHER_POSTURE.leq(joined));
assert!(DEFAULT_RESOURCE_LIMITS.leq(joined));
assert_eq!(
joined.max_expansion_depth,
HAND_AUTHORED_MID_POSTURE
.max_expansion_depth
.max(HAND_AUTHORED_OTHER_POSTURE.max_expansion_depth)
.max(DEFAULT_RESOURCE_LIMITS.max_expansion_depth),
);
assert_eq!(
joined.max_cache_entries,
HAND_AUTHORED_MID_POSTURE
.max_cache_entries
.max(HAND_AUTHORED_OTHER_POSTURE.max_cache_entries)
.max(DEFAULT_RESOURCE_LIMITS.max_cache_entries),
);
assert_eq!(
joined.max_expansion_size,
HAND_AUTHORED_MID_POSTURE
.max_expansion_size
.max(HAND_AUTHORED_OTHER_POSTURE.max_expansion_size)
.max(DEFAULT_RESOURCE_LIMITS.max_expansion_size),
);
assert_eq!(
joined.max_macro_body_size,
HAND_AUTHORED_MID_POSTURE
.max_macro_body_size
.max(HAND_AUTHORED_OTHER_POSTURE.max_macro_body_size)
.max(DEFAULT_RESOURCE_LIMITS.max_macro_body_size),
);
assert_eq!(
joined.max_registered_macros,
HAND_AUTHORED_MID_POSTURE
.max_registered_macros
.max(HAND_AUTHORED_OTHER_POSTURE.max_registered_macros)
.max(DEFAULT_RESOURCE_LIMITS.max_registered_macros),
);
assert_eq!(
joined.max_macro_arity,
HAND_AUTHORED_MID_POSTURE
.max_macro_arity
.max(HAND_AUTHORED_OTHER_POSTURE.max_macro_arity)
.max(DEFAULT_RESOURCE_LIMITS.max_macro_arity),
);
let empty_slice: [ResourceLimits; 0] = [];
assert_eq!(
empty_slice
.iter()
.copied()
.fold(EMPTY_RESOURCE_LIMITS, ResourceLimits::most_permissive),
EMPTY_RESOURCE_LIMITS,
);
}
#[test]
fn unbounded_resource_limits_seeds_strictest_fold_over_slice() {
let postures: [ResourceLimits; 3] = [
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
DEFAULT_RESOURCE_LIMITS,
];
let met = postures
.iter()
.copied()
.fold(UNBOUNDED_RESOURCE_LIMITS, ResourceLimits::strictest);
assert!(met.leq(HAND_AUTHORED_MID_POSTURE));
assert!(met.leq(HAND_AUTHORED_OTHER_POSTURE));
assert!(met.leq(DEFAULT_RESOURCE_LIMITS));
assert_eq!(
met.max_expansion_depth,
HAND_AUTHORED_MID_POSTURE
.max_expansion_depth
.min(HAND_AUTHORED_OTHER_POSTURE.max_expansion_depth)
.min(DEFAULT_RESOURCE_LIMITS.max_expansion_depth),
);
assert_eq!(
met.max_cache_entries,
HAND_AUTHORED_MID_POSTURE
.max_cache_entries
.min(HAND_AUTHORED_OTHER_POSTURE.max_cache_entries)
.min(DEFAULT_RESOURCE_LIMITS.max_cache_entries),
);
assert_eq!(
met.max_expansion_size,
HAND_AUTHORED_MID_POSTURE
.max_expansion_size
.min(HAND_AUTHORED_OTHER_POSTURE.max_expansion_size)
.min(DEFAULT_RESOURCE_LIMITS.max_expansion_size),
);
assert_eq!(
met.max_macro_body_size,
HAND_AUTHORED_MID_POSTURE
.max_macro_body_size
.min(HAND_AUTHORED_OTHER_POSTURE.max_macro_body_size)
.min(DEFAULT_RESOURCE_LIMITS.max_macro_body_size),
);
assert_eq!(
met.max_registered_macros,
HAND_AUTHORED_MID_POSTURE
.max_registered_macros
.min(HAND_AUTHORED_OTHER_POSTURE.max_registered_macros)
.min(DEFAULT_RESOURCE_LIMITS.max_registered_macros),
);
assert_eq!(
met.max_macro_arity,
HAND_AUTHORED_MID_POSTURE
.max_macro_arity
.min(HAND_AUTHORED_OTHER_POSTURE.max_macro_arity)
.min(DEFAULT_RESOURCE_LIMITS.max_macro_arity),
);
let empty_slice: [ResourceLimits; 0] = [];
assert_eq!(
empty_slice
.iter()
.copied()
.fold(UNBOUNDED_RESOURCE_LIMITS, ResourceLimits::strictest),
UNBOUNDED_RESOURCE_LIMITS,
);
}
#[test]
fn resource_limits_strictest_of_empty_slice_returns_the_meet_identity() {
assert_eq!(ResourceLimits::strictest_of(&[]), UNBOUNDED_RESOURCE_LIMITS,);
}
#[test]
fn resource_limits_most_permissive_of_empty_slice_returns_the_join_identity() {
assert_eq!(
ResourceLimits::most_permissive_of(&[]),
EMPTY_RESOURCE_LIMITS,
);
}
#[test]
fn resource_limits_strictest_of_single_element_returns_the_element_verbatim() {
assert_eq!(
ResourceLimits::strictest_of(&[HAND_AUTHORED_MID_POSTURE]),
HAND_AUTHORED_MID_POSTURE,
);
assert_eq!(
ResourceLimits::strictest_of(&[HAND_AUTHORED_OTHER_POSTURE]),
HAND_AUTHORED_OTHER_POSTURE,
);
assert_eq!(
ResourceLimits::strictest_of(&[DEFAULT_RESOURCE_LIMITS]),
DEFAULT_RESOURCE_LIMITS,
);
}
#[test]
fn resource_limits_most_permissive_of_single_element_returns_the_element_verbatim() {
assert_eq!(
ResourceLimits::most_permissive_of(&[HAND_AUTHORED_MID_POSTURE]),
HAND_AUTHORED_MID_POSTURE,
);
assert_eq!(
ResourceLimits::most_permissive_of(&[HAND_AUTHORED_OTHER_POSTURE]),
HAND_AUTHORED_OTHER_POSTURE,
);
assert_eq!(
ResourceLimits::most_permissive_of(&[DEFAULT_RESOURCE_LIMITS]),
DEFAULT_RESOURCE_LIMITS,
);
}
#[test]
fn resource_limits_strictest_of_two_elements_reduces_to_pairwise_strictest() {
assert_eq!(
ResourceLimits::strictest_of(
&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]
),
HAND_AUTHORED_MID_POSTURE.strictest(HAND_AUTHORED_OTHER_POSTURE),
);
assert_eq!(
ResourceLimits::strictest_of(&[DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS,]),
DEFAULT_RESOURCE_LIMITS.strictest(UNBOUNDED_RESOURCE_LIMITS),
);
}
#[test]
fn resource_limits_most_permissive_of_two_elements_reduces_to_pairwise_most_permissive() {
assert_eq!(
ResourceLimits::most_permissive_of(&[
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
]),
HAND_AUTHORED_MID_POSTURE.most_permissive(HAND_AUTHORED_OTHER_POSTURE),
);
assert_eq!(
ResourceLimits::most_permissive_of(&[DEFAULT_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS,]),
DEFAULT_RESOURCE_LIMITS.most_permissive(EMPTY_RESOURCE_LIMITS),
);
}
#[test]
fn resource_limits_strictest_of_agrees_with_direct_fold_over_slice() {
let postures: [ResourceLimits; 3] = [
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
DEFAULT_RESOURCE_LIMITS,
];
assert_eq!(
ResourceLimits::strictest_of(&postures),
postures
.iter()
.copied()
.fold(UNBOUNDED_RESOURCE_LIMITS, ResourceLimits::strictest),
);
}
#[test]
fn resource_limits_most_permissive_of_agrees_with_direct_fold_over_slice() {
let postures: [ResourceLimits; 3] = [
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
DEFAULT_RESOURCE_LIMITS,
];
assert_eq!(
ResourceLimits::most_permissive_of(&postures),
postures
.iter()
.copied()
.fold(EMPTY_RESOURCE_LIMITS, ResourceLimits::most_permissive),
);
}
#[test]
fn resource_limits_strictest_of_is_order_independent() {
let forward = ResourceLimits::strictest_of(&[
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
DEFAULT_RESOURCE_LIMITS,
]);
let reversed = ResourceLimits::strictest_of(&[
DEFAULT_RESOURCE_LIMITS,
HAND_AUTHORED_OTHER_POSTURE,
HAND_AUTHORED_MID_POSTURE,
]);
assert_eq!(forward, reversed);
}
#[test]
fn resource_limits_most_permissive_of_is_order_independent() {
let forward = ResourceLimits::most_permissive_of(&[
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
DEFAULT_RESOURCE_LIMITS,
]);
let reversed = ResourceLimits::most_permissive_of(&[
DEFAULT_RESOURCE_LIMITS,
HAND_AUTHORED_OTHER_POSTURE,
HAND_AUTHORED_MID_POSTURE,
]);
assert_eq!(forward, reversed);
}
#[test]
fn resource_limits_strictest_of_composes_at_compile_time_via_const_fn() {
const AGGREGATED: ResourceLimits =
ResourceLimits::strictest_of(&[DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS]);
assert_eq!(AGGREGATED, DEFAULT_RESOURCE_LIMITS);
}
#[test]
fn resource_limits_most_permissive_of_composes_at_compile_time_via_const_fn() {
const AGGREGATED: ResourceLimits =
ResourceLimits::most_permissive_of(&[EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS]);
assert_eq!(AGGREGATED, DEFAULT_RESOURCE_LIMITS);
}
#[test]
fn resource_limits_strictest_of_result_is_leq_every_operand() {
let postures: [ResourceLimits; 3] = [
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
DEFAULT_RESOURCE_LIMITS,
];
let met = ResourceLimits::strictest_of(&postures);
for p in postures {
assert!(met.leq(p));
}
}
#[test]
fn resource_limits_most_permissive_of_result_is_geq_every_operand() {
let postures: [ResourceLimits; 3] = [
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
DEFAULT_RESOURCE_LIMITS,
];
let joined = ResourceLimits::most_permissive_of(&postures);
for p in postures {
assert!(p.leq(joined));
}
}
#[test]
fn expander_built_at_strictest_of_slice_gates_at_tightest_ceiling_across_the_slice() {
let tightened = ResourceLimits::strictest_of(&[
UNBOUNDED_RESOURCE_LIMITS,
ResourceLimits {
max_macro_arity: 1,
..UNBOUNDED_RESOURCE_LIMITS
},
UNBOUNDED_RESOURCE_LIMITS,
]);
assert_eq!(tightened.max_macro_arity, 1);
assert_eq!(tightened.max_expansion_depth, usize::MAX);
assert_eq!(tightened.max_cache_entries, usize::MAX);
assert_eq!(tightened.max_expansion_size, usize::MAX);
assert_eq!(tightened.max_macro_body_size, usize::MAX);
assert_eq!(tightened.max_registered_macros, usize::MAX);
let mut e = Expander::with_limits(tightened);
let forms = read("(defmacro two (a b) `,a)").unwrap();
let err = e.expand_program(forms).unwrap_err();
assert!(
matches!(
err,
LispError::MacroArityExceeded {
arity: 2,
limit: 1,
..
}
),
"expected MacroArityExceeded from N-ary meet's tightened arity gate, got {err:?}",
);
}
#[test]
fn expander_built_at_strictest_of_two_presets_gates_at_the_tighter_ceiling() {
let tightened = UNBOUNDED_RESOURCE_LIMITS.strictest(ResourceLimits {
max_macro_arity: 1,
..UNBOUNDED_RESOURCE_LIMITS
});
assert_eq!(tightened.max_macro_arity, 1);
assert_eq!(tightened.max_expansion_depth, usize::MAX);
assert_eq!(tightened.max_cache_entries, usize::MAX);
assert_eq!(tightened.max_expansion_size, usize::MAX);
assert_eq!(tightened.max_macro_body_size, usize::MAX);
assert_eq!(tightened.max_registered_macros, usize::MAX);
let mut e = Expander::with_limits(tightened);
let forms = read("(defmacro two (a b) `,a)").unwrap();
let err = e.expand_program(forms).unwrap_err();
assert!(
matches!(
err,
LispError::MacroArityExceeded {
arity: 2,
limit: 1,
..
}
),
"the tighter arity ceiling from the meet MUST reach the register-time gate; got: {err:?}"
);
}
const HAND_AUTHORED_CLAMP_FLOOR: ResourceLimits = ResourceLimits {
max_expansion_depth: 2,
max_cache_entries: 3,
max_expansion_size: 5,
max_macro_body_size: 7,
max_registered_macros: 11,
max_macro_arity: 13,
};
const HAND_AUTHORED_CLAMP_CEILING: ResourceLimits = ResourceLimits {
max_expansion_depth: 47,
max_cache_entries: 53,
max_expansion_size: 59,
max_macro_body_size: 61,
max_registered_macros: 67,
max_macro_arity: 71,
};
#[test]
fn resource_limits_clamp_of_posture_already_in_range_returns_the_posture() {
assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_MID_POSTURE));
assert!(HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_CLAMP_CEILING));
let clamped =
HAND_AUTHORED_MID_POSTURE.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
assert_eq!(clamped, HAND_AUTHORED_MID_POSTURE);
}
#[test]
fn resource_limits_clamp_of_posture_below_lower_returns_lower() {
assert!(EMPTY_RESOURCE_LIMITS.leq(HAND_AUTHORED_CLAMP_FLOOR));
assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
let clamped =
EMPTY_RESOURCE_LIMITS.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
assert_eq!(clamped, HAND_AUTHORED_CLAMP_FLOOR);
}
#[test]
fn resource_limits_clamp_of_posture_above_upper_returns_upper() {
assert!(HAND_AUTHORED_CLAMP_CEILING.leq(UNBOUNDED_RESOURCE_LIMITS));
assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
let clamped =
UNBOUNDED_RESOURCE_LIMITS.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
assert_eq!(clamped, HAND_AUTHORED_CLAMP_CEILING);
}
#[test]
fn resource_limits_clamp_result_sits_within_the_bracket() {
assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
let inputs = [
EMPTY_RESOURCE_LIMITS, HAND_AUTHORED_MID_POSTURE, UNBOUNDED_RESOURCE_LIMITS, HAND_AUTHORED_OTHER_POSTURE, ];
for a in inputs {
let clamped = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
assert!(
HAND_AUTHORED_CLAMP_FLOOR.leq(clamped),
"clamp result must sit at or above FLOOR; input={a:?} clamped={clamped:?}",
);
assert!(
clamped.leq(HAND_AUTHORED_CLAMP_CEILING),
"clamp result must sit at or below CEILING; input={a:?} clamped={clamped:?}",
);
}
}
#[test]
fn resource_limits_clamp_with_lattice_extrema_returns_the_input() {
for a in [
DEFAULT_RESOURCE_LIMITS,
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
EMPTY_RESOURCE_LIMITS,
UNBOUNDED_RESOURCE_LIMITS,
] {
assert_eq!(
a.clamp(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
a,
"clamp against [EMPTY, UNBOUNDED] must be the identity on {a:?}",
);
}
}
#[test]
fn resource_limits_clamp_with_equal_bounds_returns_the_bound() {
for x in [
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
DEFAULT_RESOURCE_LIMITS,
] {
for a in [
EMPTY_RESOURCE_LIMITS,
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
UNBOUNDED_RESOURCE_LIMITS,
] {
assert_eq!(
a.clamp(x, x),
x,
"zero-width bracket [x, x] must collapse input {a:?} to x={x:?}",
);
}
}
}
#[test]
fn resource_limits_clamp_is_idempotent() {
assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
for a in [
EMPTY_RESOURCE_LIMITS,
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
UNBOUNDED_RESOURCE_LIMITS,
] {
let once = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
let twice = once.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
assert_eq!(
once, twice,
"clamp must be idempotent; input={a:?} once={once:?} twice={twice:?}",
);
}
}
#[test]
fn resource_limits_clamp_composes_at_compile_time_via_const_fn() {
const CLAMPED_DEFAULT: ResourceLimits =
DEFAULT_RESOURCE_LIMITS.clamp(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS);
assert_eq!(CLAMPED_DEFAULT, DEFAULT_RESOURCE_LIMITS);
const _: () = assert!(EMPTY_RESOURCE_LIMITS.leq(CLAMPED_DEFAULT));
const _: () = assert!(CLAMPED_DEFAULT.leq(UNBOUNDED_RESOURCE_LIMITS));
}
#[test]
fn resource_limits_clamp_agrees_with_direct_two_step_cascade() {
for a in [
EMPTY_RESOURCE_LIMITS,
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
UNBOUNDED_RESOURCE_LIMITS,
] {
let via_method = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
let via_cascade = a
.most_permissive(HAND_AUTHORED_CLAMP_FLOOR)
.strictest(HAND_AUTHORED_CLAMP_CEILING);
assert_eq!(via_method, via_cascade);
}
}
#[test]
fn expander_built_at_clamped_preset_gates_at_the_bracket_ceiling() {
let bracket_ceiling = ResourceLimits {
max_macro_arity: 1,
..UNBOUNDED_RESOURCE_LIMITS
};
let clamped = UNBOUNDED_RESOURCE_LIMITS.clamp(EMPTY_RESOURCE_LIMITS, bracket_ceiling);
assert_eq!(clamped.max_macro_arity, 1);
assert_eq!(clamped.max_expansion_depth, usize::MAX);
assert_eq!(clamped.max_cache_entries, usize::MAX);
assert_eq!(clamped.max_expansion_size, usize::MAX);
assert_eq!(clamped.max_macro_body_size, usize::MAX);
assert_eq!(clamped.max_registered_macros, usize::MAX);
let mut e = Expander::with_limits(clamped);
let forms = read("(defmacro two (a b) `,a)").unwrap();
let err = e.expand_program(forms).unwrap_err();
assert!(
matches!(
err,
LispError::MacroArityExceeded {
arity: 2,
limit: 1,
..
}
),
"the bracket ceiling's arity gate MUST reach the register-time check; got: {err:?}",
);
}
#[test]
fn resource_limits_within_of_posture_in_range_is_true() {
assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_MID_POSTURE));
assert!(HAND_AUTHORED_MID_POSTURE.leq(HAND_AUTHORED_CLAMP_CEILING));
assert!(HAND_AUTHORED_MID_POSTURE
.within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING),);
}
#[test]
fn resource_limits_within_of_posture_below_lower_is_false() {
assert!(!HAND_AUTHORED_CLAMP_FLOOR.leq(EMPTY_RESOURCE_LIMITS));
assert!(
!EMPTY_RESOURCE_LIMITS.within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING),
);
}
#[test]
fn resource_limits_within_of_posture_above_upper_is_false() {
assert!(!UNBOUNDED_RESOURCE_LIMITS.leq(HAND_AUTHORED_CLAMP_CEILING));
assert!(!UNBOUNDED_RESOURCE_LIMITS
.within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING),);
}
#[test]
fn resource_limits_within_with_lattice_extrema_is_true() {
for a in [
DEFAULT_RESOURCE_LIMITS,
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
EMPTY_RESOURCE_LIMITS,
UNBOUNDED_RESOURCE_LIMITS,
] {
assert!(
a.within(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
"every posture must sit within the extrema bracket [EMPTY, UNBOUNDED]; got: {a:?}",
);
}
}
#[test]
fn resource_limits_within_of_equal_bounds_iff_equal_to_bound() {
let bounds = [
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
DEFAULT_RESOURCE_LIMITS,
EMPTY_RESOURCE_LIMITS,
UNBOUNDED_RESOURCE_LIMITS,
];
for x in bounds {
for a in bounds {
assert_eq!(
a.within(x, x),
a == x,
"a.within(x, x) must hold iff a == x; a={a:?} x={x:?}",
);
}
}
}
#[test]
fn resource_limits_within_of_self_is_reflexive() {
for a in [
EMPTY_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
UNBOUNDED_RESOURCE_LIMITS,
] {
assert!(
a.within(a, a),
"a.within(a, a) must hold by reflexivity of leq; a={a:?}",
);
}
}
#[test]
fn resource_limits_within_agrees_with_clamp_fixed_point() {
assert!(HAND_AUTHORED_CLAMP_FLOOR.leq(HAND_AUTHORED_CLAMP_CEILING));
for a in [
EMPTY_RESOURCE_LIMITS, HAND_AUTHORED_MID_POSTURE, UNBOUNDED_RESOURCE_LIMITS, HAND_AUTHORED_OTHER_POSTURE, ] {
let clamped = a.clamp(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
let within_holds = a.within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
let clamp_fixed = clamped == a;
assert_eq!(
within_holds, clamp_fixed,
"within(lower, upper) must agree with clamp fixed-point; \
a={a:?} within={within_holds} clamp_fixed={clamp_fixed} clamped={clamped:?}",
);
}
}
#[test]
fn resource_limits_within_agrees_with_direct_two_primitive_conjunction() {
for a in [
EMPTY_RESOURCE_LIMITS,
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
UNBOUNDED_RESOURCE_LIMITS,
] {
let via_method = a.within(HAND_AUTHORED_CLAMP_FLOOR, HAND_AUTHORED_CLAMP_CEILING);
let via_conjunction =
HAND_AUTHORED_CLAMP_FLOOR.leq(a) && a.leq(HAND_AUTHORED_CLAMP_CEILING);
assert_eq!(via_method, via_conjunction, "a={a:?}");
}
}
#[test]
fn resource_limits_within_composes_at_compile_time_via_const_fn() {
const _: () = assert!(
DEFAULT_RESOURCE_LIMITS.within(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS)
);
const _: () = assert!(
DEFAULT_RESOURCE_LIMITS.within(DEFAULT_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS)
);
const _: () = assert!(
!UNBOUNDED_RESOURCE_LIMITS.within(EMPTY_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS,)
);
}
#[test]
fn expander_built_at_within_gated_preset_reaches_the_runtime_guards() {
let bracket_ceiling = ResourceLimits {
max_macro_arity: 1,
..UNBOUNDED_RESOURCE_LIMITS
};
let candidate = ResourceLimits {
max_macro_arity: 1,
..UNBOUNDED_RESOURCE_LIMITS
};
assert!(candidate.within(EMPTY_RESOURCE_LIMITS, bracket_ceiling));
let clamped = candidate.clamp(EMPTY_RESOURCE_LIMITS, bracket_ceiling);
assert_eq!(clamped, candidate);
let mut e = Expander::with_limits(clamped);
let forms = read("(defmacro two (a b) `,a)").unwrap();
let err = e.expand_program(forms).unwrap_err();
assert!(
matches!(
err,
LispError::MacroArityExceeded {
arity: 2,
limit: 1,
..
}
),
"the within-gated candidate's arity ceiling MUST reach the register-time check; got: {err:?}",
);
}
#[test]
fn resource_limits_is_lower_bound_of_empty_slice_is_vacuously_true() {
assert!(HAND_AUTHORED_MID_POSTURE.is_lower_bound_of(&[]));
assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[]));
assert!(UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[]));
assert!(DEFAULT_RESOURCE_LIMITS.is_lower_bound_of(&[]));
}
#[test]
fn resource_limits_is_upper_bound_of_empty_slice_is_vacuously_true() {
assert!(HAND_AUTHORED_MID_POSTURE.is_upper_bound_of(&[]));
assert!(EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[]));
assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[]));
assert!(DEFAULT_RESOURCE_LIMITS.is_upper_bound_of(&[]));
}
#[test]
fn resource_limits_is_lower_bound_of_single_element_reduces_to_leq() {
let cases: [(ResourceLimits, ResourceLimits); 6] = [
(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
(UNBOUNDED_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS),
(DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
(UNBOUNDED_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS),
(HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE),
(HAND_AUTHORED_OTHER_POSTURE, HAND_AUTHORED_MID_POSTURE),
];
for (a, b) in cases {
assert_eq!(
a.is_lower_bound_of(&[b]),
a.leq(b),
"1-input is_lower_bound_of must agree with pairwise leq for ({a:?}, {b:?})",
);
}
}
#[test]
fn resource_limits_is_upper_bound_of_single_element_reduces_to_leq() {
let cases: [(ResourceLimits, ResourceLimits); 6] = [
(EMPTY_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
(UNBOUNDED_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS),
(DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS),
(UNBOUNDED_RESOURCE_LIMITS, DEFAULT_RESOURCE_LIMITS),
(HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE),
(HAND_AUTHORED_OTHER_POSTURE, HAND_AUTHORED_MID_POSTURE),
];
for (a, b) in cases {
assert_eq!(
a.is_upper_bound_of(&[b]),
b.leq(a),
"1-input is_upper_bound_of must agree with pairwise leq(other, self) for ({a:?}, {b:?})",
);
}
}
#[test]
fn resource_limits_is_lower_bound_of_holds_for_the_meet_of_the_slice() {
let postures: [ResourceLimits; 3] = [
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
DEFAULT_RESOURCE_LIMITS,
];
let met = ResourceLimits::strictest_of(&postures);
assert!(met.is_lower_bound_of(&postures));
}
#[test]
fn resource_limits_is_upper_bound_of_holds_for_the_join_of_the_slice() {
let postures: [ResourceLimits; 3] = [
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
DEFAULT_RESOURCE_LIMITS,
];
let joined = ResourceLimits::most_permissive_of(&postures);
assert!(joined.is_upper_bound_of(&postures));
}
#[test]
fn resource_limits_empty_is_universal_lower_bound_of_every_slice() {
let postures: [ResourceLimits; 4] = [
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
DEFAULT_RESOURCE_LIMITS,
UNBOUNDED_RESOURCE_LIMITS,
];
assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&postures));
assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[UNBOUNDED_RESOURCE_LIMITS]));
assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[HAND_AUTHORED_MID_POSTURE]));
}
#[test]
fn resource_limits_unbounded_is_universal_upper_bound_of_every_slice() {
let postures: [ResourceLimits; 4] = [
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
DEFAULT_RESOURCE_LIMITS,
EMPTY_RESOURCE_LIMITS,
];
assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&postures));
assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[EMPTY_RESOURCE_LIMITS]));
assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[HAND_AUTHORED_MID_POSTURE]));
}
#[test]
fn resource_limits_is_lower_bound_of_rejects_when_any_operand_is_below_self() {
assert!(!HAND_AUTHORED_MID_POSTURE
.is_lower_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
assert!(!HAND_AUTHORED_OTHER_POSTURE
.is_lower_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
assert!(!UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[EMPTY_RESOURCE_LIMITS]));
assert!(!UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
}
#[test]
fn resource_limits_is_upper_bound_of_rejects_when_any_operand_is_above_self() {
assert!(!HAND_AUTHORED_MID_POSTURE
.is_upper_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
assert!(!HAND_AUTHORED_OTHER_POSTURE
.is_upper_bound_of(&[HAND_AUTHORED_MID_POSTURE, HAND_AUTHORED_OTHER_POSTURE,]));
assert!(!EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[DEFAULT_RESOURCE_LIMITS]));
assert!(!EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[UNBOUNDED_RESOURCE_LIMITS]));
}
#[test]
fn resource_limits_is_lower_bound_of_agrees_with_direct_all_leq_conjunction() {
let candidates: [ResourceLimits; 5] = [
EMPTY_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
UNBOUNDED_RESOURCE_LIMITS,
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
];
let slice: [ResourceLimits; 3] = [
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
DEFAULT_RESOURCE_LIMITS,
];
for c in candidates {
let direct = slice.iter().all(|p| c.leq(*p));
assert_eq!(
c.is_lower_bound_of(&slice),
direct,
"is_lower_bound_of must agree with iter().all(|p| self.leq(*p)) on candidate {c:?}",
);
}
}
#[test]
fn resource_limits_is_upper_bound_of_agrees_with_direct_all_leq_conjunction() {
let candidates: [ResourceLimits; 5] = [
EMPTY_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
UNBOUNDED_RESOURCE_LIMITS,
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
];
let slice: [ResourceLimits; 3] = [
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
DEFAULT_RESOURCE_LIMITS,
];
for c in candidates {
let direct = slice.iter().all(|p| p.leq(c));
assert_eq!(
c.is_upper_bound_of(&slice),
direct,
"is_upper_bound_of must agree with iter().all(|p| p.leq(self)) on candidate {c:?}",
);
}
}
#[test]
fn resource_limits_is_lower_bound_of_composes_at_compile_time_via_const_fn() {
const _: () = assert!(EMPTY_RESOURCE_LIMITS
.is_lower_bound_of(&[DEFAULT_RESOURCE_LIMITS, UNBOUNDED_RESOURCE_LIMITS]));
const _: () = assert!(EMPTY_RESOURCE_LIMITS.is_lower_bound_of(&[]));
const _: () =
assert!(!UNBOUNDED_RESOURCE_LIMITS.is_lower_bound_of(&[EMPTY_RESOURCE_LIMITS]));
}
#[test]
fn resource_limits_is_upper_bound_of_composes_at_compile_time_via_const_fn() {
const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS
.is_upper_bound_of(&[DEFAULT_RESOURCE_LIMITS, EMPTY_RESOURCE_LIMITS]));
const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_upper_bound_of(&[]));
const _: () =
assert!(!EMPTY_RESOURCE_LIMITS.is_upper_bound_of(&[UNBOUNDED_RESOURCE_LIMITS]));
}
const STRICT_ORDER_ROSTER: &[ResourceLimits] = &[
EMPTY_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
UNBOUNDED_RESOURCE_LIMITS,
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
];
#[test]
fn resource_limits_lt_is_irreflexive() {
for &a in STRICT_ORDER_ROSTER {
assert!(!a.lt(a), "irreflexivity failed on {a:?}");
}
}
#[test]
fn resource_limits_lt_is_asymmetric() {
for &a in STRICT_ORDER_ROSTER {
for &b in STRICT_ORDER_ROSTER {
if a.lt(b) {
assert!(!b.lt(a), "asymmetry failed on ({a:?}, {b:?})");
}
}
}
}
#[test]
fn resource_limits_lt_is_transitive() {
for &a in STRICT_ORDER_ROSTER {
for &b in STRICT_ORDER_ROSTER {
for &c in STRICT_ORDER_ROSTER {
if a.lt(b) && b.lt(c) {
assert!(a.lt(c), "transitivity failed on ({a:?}, {b:?}, {c:?})");
}
}
}
}
}
#[test]
fn resource_limits_lt_refines_leq() {
for &a in STRICT_ORDER_ROSTER {
for &b in STRICT_ORDER_ROSTER {
if a.lt(b) {
assert!(a.leq(b), "refinement failed on ({a:?}, {b:?})");
}
}
}
}
#[test]
fn resource_limits_lt_agrees_with_leq_minus_equality() {
for &a in STRICT_ORDER_ROSTER {
for &b in STRICT_ORDER_ROSTER {
let shipped = a.lt(b);
let alt = a.leq(b) && a != b;
assert_eq!(
shipped, alt,
"encoding cross-check failed on ({a:?}, {b:?})",
);
}
}
}
#[test]
fn resource_limits_lt_of_bottom_diagonal_pinned() {
assert!(EMPTY_RESOURCE_LIMITS.lt(DEFAULT_RESOURCE_LIMITS));
assert!(DEFAULT_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
assert!(EMPTY_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
}
#[test]
fn resource_limits_lt_rejects_incomparable_postures() {
assert!(!HAND_AUTHORED_MID_POSTURE.lt(HAND_AUTHORED_OTHER_POSTURE));
assert!(!HAND_AUTHORED_OTHER_POSTURE.lt(HAND_AUTHORED_MID_POSTURE));
}
#[test]
fn resource_limits_lt_agrees_with_direct_antisymmetric_encoding() {
for &a in STRICT_ORDER_ROSTER {
for &b in STRICT_ORDER_ROSTER {
let shipped = a.lt(b);
let direct = a.leq(b) && !b.leq(a);
assert_eq!(
shipped, direct,
"direct-encoding cross-check failed on ({a:?}, {b:?})",
);
}
}
}
#[test]
fn resource_limits_gt_is_dual_of_lt() {
for &a in STRICT_ORDER_ROSTER {
for &b in STRICT_ORDER_ROSTER {
assert_eq!(a.gt(b), b.lt(a), "gt/lt duality failed on ({a:?}, {b:?})");
}
}
}
#[test]
fn resource_limits_gt_of_top_diagonal_pinned() {
assert!(UNBOUNDED_RESOURCE_LIMITS.gt(DEFAULT_RESOURCE_LIMITS));
assert!(DEFAULT_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
assert!(UNBOUNDED_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
}
#[test]
fn resource_limits_gt_is_irreflexive() {
for &a in STRICT_ORDER_ROSTER {
assert!(!a.gt(a), "irreflexivity failed on {a:?}");
}
}
#[test]
fn resource_limits_lt_evaluates_at_compile_time_via_const_fn() {
const _: () = assert!(EMPTY_RESOURCE_LIMITS.lt(DEFAULT_RESOURCE_LIMITS));
const _: () = assert!(DEFAULT_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
const _: () = assert!(EMPTY_RESOURCE_LIMITS.lt(UNBOUNDED_RESOURCE_LIMITS));
const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.lt(DEFAULT_RESOURCE_LIMITS));
const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.lt(EMPTY_RESOURCE_LIMITS));
}
#[test]
fn resource_limits_gt_evaluates_at_compile_time_via_const_fn() {
const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.gt(DEFAULT_RESOURCE_LIMITS));
const _: () = assert!(DEFAULT_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.gt(EMPTY_RESOURCE_LIMITS));
const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.gt(DEFAULT_RESOURCE_LIMITS));
const _: () = assert!(!EMPTY_RESOURCE_LIMITS.gt(UNBOUNDED_RESOURCE_LIMITS));
}
#[test]
fn resource_limits_geq_is_dual_of_leq() {
for &a in STRICT_ORDER_ROSTER {
for &b in STRICT_ORDER_ROSTER {
assert_eq!(
a.geq(b),
b.leq(a),
"geq/leq duality failed on ({a:?}, {b:?})",
);
}
}
}
#[test]
fn resource_limits_geq_is_reflexive() {
for &a in STRICT_ORDER_ROSTER {
assert!(a.geq(a), "reflexivity failed on {a:?}");
}
}
#[test]
fn resource_limits_geq_is_antisymmetric() {
for &a in STRICT_ORDER_ROSTER {
for &b in STRICT_ORDER_ROSTER {
if a.geq(b) && b.geq(a) {
assert_eq!(a, b, "antisymmetry failed on ({a:?}, {b:?})");
}
}
}
}
#[test]
fn resource_limits_geq_is_transitive() {
for &a in STRICT_ORDER_ROSTER {
for &b in STRICT_ORDER_ROSTER {
for &c in STRICT_ORDER_ROSTER {
if a.geq(b) && b.geq(c) {
assert!(a.geq(c), "transitivity failed on ({a:?}, {b:?}, {c:?})");
}
}
}
}
}
#[test]
fn resource_limits_geq_refines_gt() {
for &a in STRICT_ORDER_ROSTER {
for &b in STRICT_ORDER_ROSTER {
if a.gt(b) {
assert!(a.geq(b), "refinement failed on ({a:?}, {b:?})");
}
}
}
}
#[test]
fn resource_limits_geq_agrees_with_gt_plus_equality() {
for &a in STRICT_ORDER_ROSTER {
for &b in STRICT_ORDER_ROSTER {
let shipped = a.geq(b);
let alt = a.gt(b) || a == b;
assert_eq!(
shipped, alt,
"encoding cross-check failed on ({a:?}, {b:?})",
);
}
}
}
#[test]
fn resource_limits_geq_of_top_diagonal_pinned() {
assert!(UNBOUNDED_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
assert!(DEFAULT_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
assert!(UNBOUNDED_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
assert!(EMPTY_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
assert!(DEFAULT_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
assert!(UNBOUNDED_RESOURCE_LIMITS.geq(UNBOUNDED_RESOURCE_LIMITS));
}
#[test]
fn resource_limits_geq_rejects_incomparable_postures() {
assert!(!HAND_AUTHORED_MID_POSTURE.geq(HAND_AUTHORED_OTHER_POSTURE));
assert!(!HAND_AUTHORED_OTHER_POSTURE.geq(HAND_AUTHORED_MID_POSTURE));
}
#[test]
fn resource_limits_geq_agrees_with_is_upper_bound_of_singleton() {
for &a in STRICT_ORDER_ROSTER {
for &b in STRICT_ORDER_ROSTER {
assert_eq!(
a.geq(b),
a.is_upper_bound_of(&[b]),
"arity-reduction failed on ({a:?}, {b:?})",
);
}
}
}
#[test]
fn resource_limits_geq_evaluates_at_compile_time_via_const_fn() {
const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
const _: () = assert!(DEFAULT_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.geq(EMPTY_RESOURCE_LIMITS));
const _: () = assert!(DEFAULT_RESOURCE_LIMITS.geq(DEFAULT_RESOURCE_LIMITS));
const _: () = assert!(!EMPTY_RESOURCE_LIMITS.geq(UNBOUNDED_RESOURCE_LIMITS));
}
#[test]
fn resource_limits_is_incomparable_is_irreflexive() {
for &a in STRICT_ORDER_ROSTER {
assert!(!a.is_incomparable(a), "irreflexivity failed on {a:?}");
}
}
#[test]
fn resource_limits_is_incomparable_is_symmetric() {
for &a in STRICT_ORDER_ROSTER {
for &b in STRICT_ORDER_ROSTER {
assert_eq!(
a.is_incomparable(b),
b.is_incomparable(a),
"symmetry failed on ({a:?}, {b:?})",
);
}
}
}
#[test]
fn resource_limits_is_incomparable_is_de_morgan_dual_of_comparable() {
for &a in STRICT_ORDER_ROSTER {
for &b in STRICT_ORDER_ROSTER {
let via_named = a.is_incomparable(b);
let via_composition = !(a.leq(b) || a.geq(b));
assert_eq!(
via_named, via_composition,
"De Morgan dual failed on ({a:?}, {b:?})",
);
}
}
}
#[test]
fn resource_limits_is_incomparable_partitions_ordered_pair_surface_with_comparable() {
for &a in STRICT_ORDER_ROSTER {
for &b in STRICT_ORDER_ROSTER {
let comparable = a.leq(b) || a.geq(b);
let incomparable = a.is_incomparable(b);
assert!(
comparable != incomparable,
"partition failed on ({a:?}, {b:?}): comparable={comparable}, incomparable={incomparable}",
);
}
}
}
#[test]
fn resource_limits_is_incomparable_folds_false_at_the_bottom_pole() {
for &a in STRICT_ORDER_ROSTER {
assert!(
!a.is_incomparable(EMPTY_RESOURCE_LIMITS),
"bottom-pole absorption failed on {a:?}",
);
assert!(
!EMPTY_RESOURCE_LIMITS.is_incomparable(a),
"bottom-pole absorption failed on flipped ({a:?}, EMPTY)",
);
}
}
#[test]
fn resource_limits_is_incomparable_folds_false_at_the_top_pole() {
for &a in STRICT_ORDER_ROSTER {
assert!(
!a.is_incomparable(UNBOUNDED_RESOURCE_LIMITS),
"top-pole absorption failed on {a:?}",
);
assert!(
!UNBOUNDED_RESOURCE_LIMITS.is_incomparable(a),
"top-pole absorption failed on flipped ({a:?}, UNBOUNDED)",
);
}
}
#[test]
fn resource_limits_is_incomparable_holds_on_the_hand_authored_antichain_pair() {
assert!(
HAND_AUTHORED_MID_POSTURE.is_incomparable(HAND_AUTHORED_OTHER_POSTURE),
"antichain arm failed on (MID, OTHER)",
);
assert!(
HAND_AUTHORED_OTHER_POSTURE.is_incomparable(HAND_AUTHORED_MID_POSTURE),
"antichain arm failed on flipped (OTHER, MID)",
);
}
#[test]
fn resource_limits_is_incomparable_folds_false_on_the_shipped_preset_triangle() {
const TRIANGLE: &[ResourceLimits] = &[
EMPTY_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
UNBOUNDED_RESOURCE_LIMITS,
];
for &a in TRIANGLE {
for &b in TRIANGLE {
assert!(
!a.is_incomparable(b),
"shipped preset triangle comparability failed on ({a:?}, {b:?})",
);
}
}
}
#[test]
fn resource_limits_is_incomparable_evaluates_at_compile_time_via_const_fn() {
const _: () = assert!(!EMPTY_RESOURCE_LIMITS.is_incomparable(EMPTY_RESOURCE_LIMITS));
const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.is_incomparable(DEFAULT_RESOURCE_LIMITS));
const _: () =
assert!(!UNBOUNDED_RESOURCE_LIMITS.is_incomparable(UNBOUNDED_RESOURCE_LIMITS));
const _: () = assert!(!EMPTY_RESOURCE_LIMITS.is_incomparable(UNBOUNDED_RESOURCE_LIMITS));
const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.is_incomparable(EMPTY_RESOURCE_LIMITS));
const _: () = assert!(!DEFAULT_RESOURCE_LIMITS.is_incomparable(EMPTY_RESOURCE_LIMITS));
const _: () = assert!(!UNBOUNDED_RESOURCE_LIMITS.is_incomparable(DEFAULT_RESOURCE_LIMITS));
}
#[test]
fn resource_limits_is_comparable_is_reflexive() {
for &a in STRICT_ORDER_ROSTER {
assert!(a.is_comparable(a), "reflexivity failed on {a:?}");
}
}
#[test]
fn resource_limits_is_comparable_is_symmetric() {
for &a in STRICT_ORDER_ROSTER {
for &b in STRICT_ORDER_ROSTER {
assert_eq!(
a.is_comparable(b),
b.is_comparable(a),
"symmetry failed on ({a:?}, {b:?})",
);
}
}
}
#[test]
fn resource_limits_is_comparable_is_de_morgan_dual_of_incomparable() {
for &a in STRICT_ORDER_ROSTER {
for &b in STRICT_ORDER_ROSTER {
let via_named = a.is_comparable(b);
let via_negated_dual = !a.is_incomparable(b);
assert_eq!(
via_named, via_negated_dual,
"De Morgan dual failed on ({a:?}, {b:?})",
);
}
}
}
#[test]
fn resource_limits_is_comparable_agrees_with_leq_or_geq_composition() {
for &a in STRICT_ORDER_ROSTER {
for &b in STRICT_ORDER_ROSTER {
let via_named = a.is_comparable(b);
let via_composition = a.leq(b) || a.geq(b);
assert_eq!(
via_named, via_composition,
"leq||geq composition failed on ({a:?}, {b:?})",
);
}
}
}
#[test]
fn resource_limits_is_comparable_folds_true_at_the_bottom_pole() {
for &a in STRICT_ORDER_ROSTER {
assert!(
a.is_comparable(EMPTY_RESOURCE_LIMITS),
"bottom-pole absorption failed on {a:?}",
);
assert!(
EMPTY_RESOURCE_LIMITS.is_comparable(a),
"bottom-pole absorption failed on flipped ({a:?}, EMPTY)",
);
}
}
#[test]
fn resource_limits_is_comparable_folds_true_at_the_top_pole() {
for &a in STRICT_ORDER_ROSTER {
assert!(
a.is_comparable(UNBOUNDED_RESOURCE_LIMITS),
"top-pole absorption failed on {a:?}",
);
assert!(
UNBOUNDED_RESOURCE_LIMITS.is_comparable(a),
"top-pole absorption failed on flipped ({a:?}, UNBOUNDED)",
);
}
}
#[test]
fn resource_limits_is_comparable_falsifies_on_the_hand_authored_antichain_pair() {
assert!(
!HAND_AUTHORED_MID_POSTURE.is_comparable(HAND_AUTHORED_OTHER_POSTURE),
"antichain arm failed on (MID, OTHER)",
);
assert!(
!HAND_AUTHORED_OTHER_POSTURE.is_comparable(HAND_AUTHORED_MID_POSTURE),
"antichain arm failed on flipped (OTHER, MID)",
);
}
#[test]
fn resource_limits_is_comparable_holds_on_the_shipped_preset_triangle() {
const TRIANGLE: &[ResourceLimits] = &[
EMPTY_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
UNBOUNDED_RESOURCE_LIMITS,
];
for &a in TRIANGLE {
for &b in TRIANGLE {
assert!(
a.is_comparable(b),
"shipped preset triangle comparability failed on ({a:?}, {b:?})",
);
}
}
}
#[test]
fn resource_limits_is_comparable_partitions_ordered_pair_surface_exhaustively() {
for &a in STRICT_ORDER_ROSTER {
for &b in STRICT_ORDER_ROSTER {
let comparable = a.is_comparable(b);
let incomparable = a.is_incomparable(b);
assert!(
comparable != incomparable,
"partition failed on ({a:?}, {b:?}): comparable={comparable}, incomparable={incomparable}",
);
}
}
}
#[test]
fn resource_limits_is_comparable_evaluates_at_compile_time_via_const_fn() {
const _: () = assert!(EMPTY_RESOURCE_LIMITS.is_comparable(EMPTY_RESOURCE_LIMITS));
const _: () = assert!(DEFAULT_RESOURCE_LIMITS.is_comparable(DEFAULT_RESOURCE_LIMITS));
const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_comparable(UNBOUNDED_RESOURCE_LIMITS));
const _: () = assert!(EMPTY_RESOURCE_LIMITS.is_comparable(UNBOUNDED_RESOURCE_LIMITS));
const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_comparable(EMPTY_RESOURCE_LIMITS));
const _: () = assert!(DEFAULT_RESOURCE_LIMITS.is_comparable(EMPTY_RESOURCE_LIMITS));
const _: () = assert!(UNBOUNDED_RESOURCE_LIMITS.is_comparable(DEFAULT_RESOURCE_LIMITS));
}
#[test]
fn resource_limits_partial_cmp_is_equal_on_the_diagonal() {
for &a in STRICT_ORDER_ROSTER {
assert_eq!(
a.partial_cmp(a),
Some(Ordering::Equal),
"diagonal-equality failed on {a:?}",
);
}
}
#[test]
fn resource_limits_partial_cmp_reverses_under_argument_swap() {
for &a in STRICT_ORDER_ROSTER {
for &b in STRICT_ORDER_ROSTER {
let forward = a.partial_cmp(b);
let swapped_reversed = b.partial_cmp(a).map(Ordering::reverse);
assert_eq!(
forward, swapped_reversed,
"swap-reverse failed on ({a:?}, {b:?})",
);
}
}
}
#[test]
fn resource_limits_partial_cmp_none_iff_incomparable() {
for &a in STRICT_ORDER_ROSTER {
for &b in STRICT_ORDER_ROSTER {
let via_partial_cmp = a.partial_cmp(b).is_none();
let via_is_incomparable = a.is_incomparable(b);
assert_eq!(
via_partial_cmp, via_is_incomparable,
"none-iff-incomparable failed on ({a:?}, {b:?})",
);
}
}
}
#[test]
fn resource_limits_partial_cmp_some_iff_comparable() {
for &a in STRICT_ORDER_ROSTER {
for &b in STRICT_ORDER_ROSTER {
let via_partial_cmp = a.partial_cmp(b).is_some();
let via_is_comparable = a.is_comparable(b);
assert_eq!(
via_partial_cmp, via_is_comparable,
"some-iff-comparable failed on ({a:?}, {b:?})",
);
}
}
}
#[test]
fn resource_limits_partial_cmp_less_iff_lt() {
for &a in STRICT_ORDER_ROSTER {
for &b in STRICT_ORDER_ROSTER {
let via_partial_cmp = a.partial_cmp(b) == Some(Ordering::Less);
let via_lt = a.lt(b);
assert_eq!(
via_partial_cmp, via_lt,
"less-iff-lt failed on ({a:?}, {b:?})",
);
}
}
}
#[test]
fn resource_limits_partial_cmp_greater_iff_gt() {
for &a in STRICT_ORDER_ROSTER {
for &b in STRICT_ORDER_ROSTER {
let via_partial_cmp = a.partial_cmp(b) == Some(Ordering::Greater);
let via_gt = a.gt(b);
assert_eq!(
via_partial_cmp, via_gt,
"greater-iff-gt failed on ({a:?}, {b:?})",
);
}
}
}
#[test]
fn resource_limits_partial_cmp_equal_iff_eq() {
for &a in STRICT_ORDER_ROSTER {
for &b in STRICT_ORDER_ROSTER {
let via_partial_cmp = a.partial_cmp(b) == Some(Ordering::Equal);
let via_eq = a == b;
assert_eq!(
via_partial_cmp, via_eq,
"equal-iff-eq failed on ({a:?}, {b:?})",
);
}
}
}
#[test]
fn resource_limits_partial_cmp_folds_less_ascending_shipped_preset_chain() {
assert_eq!(
EMPTY_RESOURCE_LIMITS.partial_cmp(DEFAULT_RESOURCE_LIMITS),
Some(Ordering::Less),
);
assert_eq!(
DEFAULT_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
Some(Ordering::Less),
);
assert_eq!(
EMPTY_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
Some(Ordering::Less),
);
}
#[test]
fn resource_limits_partial_cmp_folds_greater_descending_shipped_preset_chain() {
assert_eq!(
DEFAULT_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
Some(Ordering::Greater),
);
assert_eq!(
UNBOUNDED_RESOURCE_LIMITS.partial_cmp(DEFAULT_RESOURCE_LIMITS),
Some(Ordering::Greater),
);
assert_eq!(
UNBOUNDED_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
Some(Ordering::Greater),
);
}
#[test]
fn resource_limits_partial_cmp_folds_none_on_the_hand_authored_antichain_pair() {
assert_eq!(
HAND_AUTHORED_MID_POSTURE.partial_cmp(HAND_AUTHORED_OTHER_POSTURE),
None,
);
assert_eq!(
HAND_AUTHORED_OTHER_POSTURE.partial_cmp(HAND_AUTHORED_MID_POSTURE),
None,
);
}
#[test]
fn resource_limits_partial_cmp_evaluates_at_compile_time_via_const_fn() {
const _: () = assert!(matches!(
EMPTY_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
Some(Ordering::Equal),
));
const _: () = assert!(matches!(
EMPTY_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
Some(Ordering::Less),
));
const _: () = assert!(matches!(
UNBOUNDED_RESOURCE_LIMITS.partial_cmp(EMPTY_RESOURCE_LIMITS),
Some(Ordering::Greater),
));
const _: () = assert!(matches!(
DEFAULT_RESOURCE_LIMITS.partial_cmp(UNBOUNDED_RESOURCE_LIMITS),
Some(Ordering::Less),
));
const _: () = assert!(matches!(
EMPTY_RESOURCE_LIMITS.partial_cmp(DEFAULT_RESOURCE_LIMITS),
Some(Ordering::Less),
));
}
#[test]
fn resource_limits_is_chain_empty_slice_is_vacuously_true() {
assert!(ResourceLimits::is_chain(&[]));
}
#[test]
fn resource_limits_is_antichain_empty_slice_is_vacuously_true() {
assert!(ResourceLimits::is_antichain(&[]));
}
#[test]
fn resource_limits_is_chain_singleton_is_vacuously_true() {
assert!(ResourceLimits::is_chain(&[EMPTY_RESOURCE_LIMITS]));
assert!(ResourceLimits::is_chain(&[DEFAULT_RESOURCE_LIMITS]));
assert!(ResourceLimits::is_chain(&[UNBOUNDED_RESOURCE_LIMITS]));
assert!(ResourceLimits::is_chain(&[HAND_AUTHORED_MID_POSTURE]));
assert!(ResourceLimits::is_chain(&[HAND_AUTHORED_OTHER_POSTURE]));
}
#[test]
fn resource_limits_is_antichain_singleton_is_vacuously_true() {
assert!(ResourceLimits::is_antichain(&[EMPTY_RESOURCE_LIMITS]));
assert!(ResourceLimits::is_antichain(&[DEFAULT_RESOURCE_LIMITS]));
assert!(ResourceLimits::is_antichain(&[UNBOUNDED_RESOURCE_LIMITS]));
assert!(ResourceLimits::is_antichain(&[HAND_AUTHORED_MID_POSTURE]));
assert!(ResourceLimits::is_antichain(&[HAND_AUTHORED_OTHER_POSTURE]));
}
#[test]
fn resource_limits_is_chain_of_diagonal_duplicate_is_true() {
assert!(ResourceLimits::is_chain(&[
DEFAULT_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
]));
assert!(ResourceLimits::is_chain(&[
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_MID_POSTURE,
]));
}
#[test]
fn resource_limits_is_antichain_of_diagonal_duplicate_is_false() {
assert!(!ResourceLimits::is_antichain(&[
DEFAULT_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
]));
assert!(!ResourceLimits::is_antichain(&[
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_MID_POSTURE,
]));
}
#[test]
fn resource_limits_is_chain_holds_on_the_shipped_preset_triple() {
assert!(ResourceLimits::is_chain(&[
EMPTY_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
UNBOUNDED_RESOURCE_LIMITS,
]));
assert!(ResourceLimits::is_chain(&[
UNBOUNDED_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
EMPTY_RESOURCE_LIMITS,
]));
assert!(ResourceLimits::is_chain(&[
DEFAULT_RESOURCE_LIMITS,
EMPTY_RESOURCE_LIMITS,
UNBOUNDED_RESOURCE_LIMITS,
]));
}
#[test]
fn resource_limits_is_antichain_rejects_the_shipped_preset_pair() {
assert!(!ResourceLimits::is_antichain(&[
EMPTY_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
]));
assert!(!ResourceLimits::is_antichain(&[
DEFAULT_RESOURCE_LIMITS,
UNBOUNDED_RESOURCE_LIMITS,
]));
assert!(!ResourceLimits::is_antichain(&[
EMPTY_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
UNBOUNDED_RESOURCE_LIMITS,
]));
}
#[test]
fn resource_limits_is_chain_rejects_the_hand_authored_antichain_pair() {
assert!(!ResourceLimits::is_chain(&[
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
]));
assert!(!ResourceLimits::is_chain(&[
HAND_AUTHORED_OTHER_POSTURE,
HAND_AUTHORED_MID_POSTURE,
]));
}
#[test]
fn resource_limits_is_antichain_holds_on_the_hand_authored_antichain_pair() {
assert!(ResourceLimits::is_antichain(&[
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
]));
assert!(ResourceLimits::is_antichain(&[
HAND_AUTHORED_OTHER_POSTURE,
HAND_AUTHORED_MID_POSTURE,
]));
}
#[test]
fn resource_limits_is_chain_rejects_mixed_slice_with_one_antichain_pair() {
assert!(!ResourceLimits::is_chain(&[
DEFAULT_RESOURCE_LIMITS,
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
]));
}
#[test]
fn resource_limits_is_antichain_rejects_mixed_slice_with_one_comparable_pair() {
assert!(!ResourceLimits::is_antichain(&[
DEFAULT_RESOURCE_LIMITS,
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
]));
}
#[test]
fn resource_limits_is_antichain_and_is_chain_are_mutually_exclusive_on_distinct_pairs() {
let presets: [ResourceLimits; 5] = [
EMPTY_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
UNBOUNDED_RESOURCE_LIMITS,
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
];
for a in presets {
for b in presets {
if a == b {
continue;
}
let pair = [a, b];
let chain = ResourceLimits::is_chain(&pair);
let antichain = ResourceLimits::is_antichain(&pair);
assert_ne!(
chain, antichain,
"distinct-pair mutual exclusivity failed on pair {a:?} / {b:?}",
);
}
}
}
#[test]
fn resource_limits_is_chain_and_is_antichain_agree_at_empty_and_singleton_slices() {
assert_eq!(
ResourceLimits::is_chain(&[]),
ResourceLimits::is_antichain(&[]),
);
let presets: [ResourceLimits; 5] = [
EMPTY_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
UNBOUNDED_RESOURCE_LIMITS,
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
];
for a in presets {
let single = [a];
assert_eq!(
ResourceLimits::is_chain(&single),
ResourceLimits::is_antichain(&single),
"singleton agreement failed on {a:?}",
);
}
}
#[test]
fn resource_limits_is_chain_evaluates_at_compile_time_via_const_fn() {
const _: () = assert!(ResourceLimits::is_chain(&[]));
const _: () = assert!(ResourceLimits::is_chain(&[EMPTY_RESOURCE_LIMITS]));
const _: () = assert!(ResourceLimits::is_chain(&[
EMPTY_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
UNBOUNDED_RESOURCE_LIMITS,
]));
}
#[test]
fn resource_limits_is_antichain_evaluates_at_compile_time_via_const_fn() {
const _: () = assert!(ResourceLimits::is_antichain(&[]));
const _: () = assert!(ResourceLimits::is_antichain(&[EMPTY_RESOURCE_LIMITS]));
const _: () = assert!(ResourceLimits::is_antichain(&[
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
]));
}
#[test]
fn resource_limits_is_mixed_empty_slice_is_false() {
assert!(!ResourceLimits::is_mixed(&[]));
}
#[test]
fn resource_limits_is_mixed_singleton_is_false() {
assert!(!ResourceLimits::is_mixed(&[EMPTY_RESOURCE_LIMITS]));
assert!(!ResourceLimits::is_mixed(&[DEFAULT_RESOURCE_LIMITS]));
assert!(!ResourceLimits::is_mixed(&[UNBOUNDED_RESOURCE_LIMITS]));
assert!(!ResourceLimits::is_mixed(&[HAND_AUTHORED_MID_POSTURE]));
assert!(!ResourceLimits::is_mixed(&[HAND_AUTHORED_OTHER_POSTURE]));
}
#[test]
fn resource_limits_is_mixed_of_diagonal_duplicate_is_false() {
assert!(!ResourceLimits::is_mixed(&[
DEFAULT_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
]));
assert!(!ResourceLimits::is_mixed(&[
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_MID_POSTURE,
]));
}
#[test]
fn resource_limits_is_mixed_rejects_the_shipped_preset_triple() {
assert!(!ResourceLimits::is_mixed(&[
EMPTY_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
UNBOUNDED_RESOURCE_LIMITS,
]));
assert!(!ResourceLimits::is_mixed(&[
UNBOUNDED_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
EMPTY_RESOURCE_LIMITS,
]));
}
#[test]
fn resource_limits_is_mixed_rejects_the_hand_authored_antichain_pair() {
assert!(!ResourceLimits::is_mixed(&[
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
]));
assert!(!ResourceLimits::is_mixed(&[
HAND_AUTHORED_OTHER_POSTURE,
HAND_AUTHORED_MID_POSTURE,
]));
}
#[test]
fn resource_limits_is_mixed_holds_on_the_mixed_slice_with_one_comparable_and_one_antichain_pair(
) {
assert!(ResourceLimits::is_mixed(&[
DEFAULT_RESOURCE_LIMITS,
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
]));
}
#[test]
fn resource_limits_is_chain_is_antichain_and_is_mixed_partition_the_verdict_surface_on_distinct_slices(
) {
let presets: [ResourceLimits; 5] = [
EMPTY_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
UNBOUNDED_RESOURCE_LIMITS,
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
];
for a in presets {
for b in presets {
if a == b {
continue;
}
let pair = [a, b];
let chain = ResourceLimits::is_chain(&pair);
let antichain = ResourceLimits::is_antichain(&pair);
let mixed = ResourceLimits::is_mixed(&pair);
let true_count = usize::from(chain) + usize::from(antichain) + usize::from(mixed);
assert_eq!(
true_count, 1,
"trichotomy partition failed on distinct pair {a:?} / {b:?} — (chain, antichain, mixed) = ({chain}, {antichain}, {mixed})",
);
assert!(
!mixed,
"is_mixed unexpectedly true on distinct pair {a:?} / {b:?} — the mixed cell requires ≥3 elements to open",
);
}
}
for a in presets {
for b in presets {
for c in presets {
if a == b || b == c || a == c {
continue;
}
let triple = [a, b, c];
let chain = ResourceLimits::is_chain(&triple);
let antichain = ResourceLimits::is_antichain(&triple);
let mixed = ResourceLimits::is_mixed(&triple);
let true_count =
usize::from(chain) + usize::from(antichain) + usize::from(mixed);
assert_eq!(
true_count, 1,
"trichotomy partition failed on distinct triple {a:?} / {b:?} / {c:?} — (chain, antichain, mixed) = ({chain}, {antichain}, {mixed})",
);
}
}
}
}
#[test]
fn resource_limits_is_mixed_evaluates_at_compile_time_via_const_fn() {
const _: () = assert!(!ResourceLimits::is_mixed(&[]));
const _: () = assert!(!ResourceLimits::is_mixed(&[EMPTY_RESOURCE_LIMITS]));
const _: () = assert!(ResourceLimits::is_mixed(&[
DEFAULT_RESOURCE_LIMITS,
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
]));
}
#[test]
fn resource_limits_is_ascending_empty_slice_is_vacuously_true() {
assert!(ResourceLimits::is_ascending(&[]));
}
#[test]
fn resource_limits_is_descending_empty_slice_is_vacuously_true() {
assert!(ResourceLimits::is_descending(&[]));
}
#[test]
fn resource_limits_is_ascending_singleton_is_vacuously_true() {
assert!(ResourceLimits::is_ascending(&[EMPTY_RESOURCE_LIMITS]));
assert!(ResourceLimits::is_ascending(&[DEFAULT_RESOURCE_LIMITS]));
assert!(ResourceLimits::is_ascending(&[UNBOUNDED_RESOURCE_LIMITS]));
assert!(ResourceLimits::is_ascending(&[HAND_AUTHORED_MID_POSTURE]));
assert!(ResourceLimits::is_ascending(&[HAND_AUTHORED_OTHER_POSTURE]));
}
#[test]
fn resource_limits_is_descending_singleton_is_vacuously_true() {
assert!(ResourceLimits::is_descending(&[EMPTY_RESOURCE_LIMITS]));
assert!(ResourceLimits::is_descending(&[DEFAULT_RESOURCE_LIMITS]));
assert!(ResourceLimits::is_descending(&[UNBOUNDED_RESOURCE_LIMITS]));
assert!(ResourceLimits::is_descending(&[HAND_AUTHORED_MID_POSTURE]));
assert!(ResourceLimits::is_descending(&[
HAND_AUTHORED_OTHER_POSTURE
]));
}
#[test]
fn resource_limits_is_ascending_of_diagonal_duplicate_is_true() {
assert!(ResourceLimits::is_ascending(&[
DEFAULT_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
]));
assert!(ResourceLimits::is_ascending(&[
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_MID_POSTURE,
]));
}
#[test]
fn resource_limits_is_descending_of_diagonal_duplicate_is_true() {
assert!(ResourceLimits::is_descending(&[
DEFAULT_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
]));
assert!(ResourceLimits::is_descending(&[
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_MID_POSTURE,
]));
}
#[test]
fn resource_limits_is_ascending_holds_on_the_ascending_shipped_preset_triple() {
assert!(ResourceLimits::is_ascending(&[
EMPTY_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
]));
assert!(ResourceLimits::is_ascending(&[
DEFAULT_RESOURCE_LIMITS,
UNBOUNDED_RESOURCE_LIMITS,
]));
assert!(ResourceLimits::is_ascending(&[
EMPTY_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
UNBOUNDED_RESOURCE_LIMITS,
]));
}
#[test]
fn resource_limits_is_descending_holds_on_the_descending_shipped_preset_triple() {
assert!(ResourceLimits::is_descending(&[
DEFAULT_RESOURCE_LIMITS,
EMPTY_RESOURCE_LIMITS,
]));
assert!(ResourceLimits::is_descending(&[
UNBOUNDED_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
]));
assert!(ResourceLimits::is_descending(&[
UNBOUNDED_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
EMPTY_RESOURCE_LIMITS,
]));
}
#[test]
fn resource_limits_is_ascending_rejects_the_descending_shipped_preset_triple() {
assert!(!ResourceLimits::is_ascending(&[
UNBOUNDED_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
EMPTY_RESOURCE_LIMITS,
]));
}
#[test]
fn resource_limits_is_descending_rejects_the_ascending_shipped_preset_triple() {
assert!(!ResourceLimits::is_descending(&[
EMPTY_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
UNBOUNDED_RESOURCE_LIMITS,
]));
}
#[test]
fn resource_limits_is_ascending_rejects_the_non_monotone_chain_permutation() {
let slice = [
DEFAULT_RESOURCE_LIMITS,
EMPTY_RESOURCE_LIMITS,
UNBOUNDED_RESOURCE_LIMITS,
];
assert!(ResourceLimits::is_chain(&slice));
assert!(!ResourceLimits::is_ascending(&slice));
assert!(!ResourceLimits::is_descending(&slice));
}
#[test]
fn resource_limits_is_ascending_rejects_the_hand_authored_antichain_pair() {
assert!(!ResourceLimits::is_ascending(&[
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
]));
assert!(!ResourceLimits::is_ascending(&[
HAND_AUTHORED_OTHER_POSTURE,
HAND_AUTHORED_MID_POSTURE,
]));
}
#[test]
fn resource_limits_is_descending_rejects_the_hand_authored_antichain_pair() {
assert!(!ResourceLimits::is_descending(&[
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
]));
assert!(!ResourceLimits::is_descending(&[
HAND_AUTHORED_OTHER_POSTURE,
HAND_AUTHORED_MID_POSTURE,
]));
}
#[test]
fn resource_limits_is_ascending_implies_is_chain_on_every_shipped_slice() {
let presets: [ResourceLimits; 5] = [
EMPTY_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
UNBOUNDED_RESOURCE_LIMITS,
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
];
for a in presets {
for b in presets {
let slice = [a, b];
if ResourceLimits::is_ascending(&slice) {
assert!(
ResourceLimits::is_chain(&slice),
"is_ascending ⇒ is_chain failed on pair {a:?} / {b:?}",
);
}
if ResourceLimits::is_descending(&slice) {
assert!(
ResourceLimits::is_chain(&slice),
"is_descending ⇒ is_chain failed on pair {a:?} / {b:?}",
);
}
}
}
let asc = [
EMPTY_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
UNBOUNDED_RESOURCE_LIMITS,
];
assert!(ResourceLimits::is_ascending(&asc));
assert!(ResourceLimits::is_chain(&asc));
let desc = [
UNBOUNDED_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
EMPTY_RESOURCE_LIMITS,
];
assert!(ResourceLimits::is_descending(&desc));
assert!(ResourceLimits::is_chain(&desc));
}
#[test]
fn resource_limits_is_ascending_and_is_descending_agree_at_empty_and_singleton_slices() {
assert_eq!(
ResourceLimits::is_ascending(&[]),
ResourceLimits::is_descending(&[]),
);
let presets: [ResourceLimits; 5] = [
EMPTY_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
UNBOUNDED_RESOURCE_LIMITS,
HAND_AUTHORED_MID_POSTURE,
HAND_AUTHORED_OTHER_POSTURE,
];
for a in presets {
let single = [a];
assert_eq!(
ResourceLimits::is_ascending(&single),
ResourceLimits::is_descending(&single),
"empty-and-singleton agreement failed on singleton {a:?}",
);
let dup = [a, a];
assert_eq!(
ResourceLimits::is_ascending(&dup),
ResourceLimits::is_descending(&dup),
"diagonal-duplicate agreement failed on {a:?}",
);
}
}
#[test]
fn resource_limits_is_ascending_evaluates_at_compile_time_via_const_fn() {
const _: () = assert!(ResourceLimits::is_ascending(&[]));
const _: () = assert!(ResourceLimits::is_ascending(&[EMPTY_RESOURCE_LIMITS]));
const _: () = assert!(ResourceLimits::is_ascending(&[
EMPTY_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
UNBOUNDED_RESOURCE_LIMITS,
]));
const _: () = assert!(!ResourceLimits::is_ascending(&[
UNBOUNDED_RESOURCE_LIMITS,
EMPTY_RESOURCE_LIMITS,
]));
}
#[test]
fn resource_limits_is_descending_evaluates_at_compile_time_via_const_fn() {
const _: () = assert!(ResourceLimits::is_descending(&[]));
const _: () = assert!(ResourceLimits::is_descending(&[EMPTY_RESOURCE_LIMITS]));
const _: () = assert!(ResourceLimits::is_descending(&[
UNBOUNDED_RESOURCE_LIMITS,
DEFAULT_RESOURCE_LIMITS,
EMPTY_RESOURCE_LIMITS,
]));
const _: () = assert!(!ResourceLimits::is_descending(&[
EMPTY_RESOURCE_LIMITS,
UNBOUNDED_RESOURCE_LIMITS,
]));
}
}