mod type_checking;
mod type_inference;
mod type_rewriting;
use crate::{
builtins::{self, is_js_syntax_procs, is_proc_name, is_registered_proc},
calcit::{
self, Calcit, CalcitArgLabel, CalcitErr, CalcitErrKind, CalcitFn, CalcitFnArgs, CalcitFnTypeAnnotation, CalcitImpl, CalcitImport,
CalcitList, CalcitLocal, CalcitProc, CalcitScope, CalcitStructDef, CalcitSymbolInfo, CalcitSyntax, CalcitTrait,
CalcitTraitMemberKind, CalcitTypeAnnotation, GENERATED_DEF, ImportInfo, LocatedWarning, NodeLocation, RawCodeType, SchemaKind,
brief_type_of_value, pop_type_slot_override, push_type_slot_override, register_type_slot,
},
call_stack::{CallStackList, StackKind},
codegen, program, runner,
};
use type_checking::{
CallTypeCheckInfo, check_core_fn_arg_types, check_function_return_type, check_local_fn_call_arg_types, check_proc_arg_types,
check_user_fn_arg_types, detect_return_type_hint_from_processed_body,
};
pub use type_inference::infer_static_type_from_expr;
use type_inference::{
infer_struct_field_type, infer_type_from_expr, resolve_enum_value, resolve_program_value_for_preprocess, resolve_type_value,
};
use type_rewriting::{
build_enum_ref_node, build_struct_ref_node, try_rewrite_enum_args_to_named_enums, try_rewrite_local_fn_enum_args_to_named_enums,
try_rewrite_loose_struct_args_to_structs, try_rewrite_map_args_to_structs,
};
use std::cell::Cell;
use std::collections::{BTreeSet, HashMap, HashSet};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::{cell::RefCell, vec};
use cirru_edn::EdnTag;
use im_ternary_tree::TernaryTreeList;
use strum::ParseError;
pub(crate) type ScopeTypes = HashMap<Arc<str>, Arc<CalcitTypeAnnotation>>;
static WARN_DYN_METHOD: AtomicBool = AtomicBool::new(false);
static VERBOSE_PREPROCESS: AtomicBool = AtomicBool::new(false);
thread_local! {
static PREPROCESS_COMPILE_GUARD: RefCell<HashSet<(Arc<str>, Arc<str>)>> = RefCell::new(HashSet::new());
static EXPECTED_FN_TYPE: RefCell<Option<Arc<CalcitFnTypeAnnotation>>> = const { RefCell::new(None) };
static EXPECTED_STRUCT_TYPE: RefCell<Option<CalcitStructDef>> = const { RefCell::new(None) };
static CURRENT_FN_FEATURES: RefCell<Option<Arc<HashSet<EdnTag>>>> = const { RefCell::new(None) };
static PREPROCESS_DEPTH: Cell<usize> = const { Cell::new(0) };
}
pub fn set_verbose_preprocess(enabled: bool) {
VERBOSE_PREPROCESS.store(enabled, Ordering::SeqCst);
}
struct PreprocessTrace {
ns: Arc<str>,
def: Arc<str>,
}
impl PreprocessTrace {
fn enter(ns: &str, def: &str) -> Option<Self> {
if !VERBOSE_PREPROCESS.load(Ordering::Relaxed) {
return None;
}
PREPROCESS_DEPTH.with(|depth| {
eprintln!("[verbose] preprocess enter depth={} {ns}/{def}", depth.get());
depth.set(depth.get() + 1);
});
Some(Self {
ns: Arc::from(ns),
def: Arc::from(def),
})
}
}
impl Drop for PreprocessTrace {
fn drop(&mut self) {
PREPROCESS_DEPTH.with(|depth| depth.set(depth.get().saturating_sub(1)));
eprintln!("[verbose] preprocess leave {}/{}", self.ns, self.def);
}
}
fn with_preprocess_compile_guard<T>(ns: &str, def: &str, f: impl FnOnce() -> Result<T, CalcitErr>) -> Result<Option<T>, CalcitErr> {
let key = (Arc::from(ns), Arc::from(def));
let inserted = PREPROCESS_COMPILE_GUARD.with(|guard| guard.borrow_mut().insert(key.clone()));
if !inserted {
return Ok(None);
}
let result = f();
PREPROCESS_COMPILE_GUARD.with(|guard| {
guard.borrow_mut().remove(&key);
});
result.map(Some)
}
pub fn set_warn_dyn_method(enabled: bool) {
WARN_DYN_METHOD.store(enabled, Ordering::SeqCst);
}
fn warn_dyn_method_enabled() -> bool {
WARN_DYN_METHOD.load(Ordering::Relaxed)
}
pub fn is_warn_dyn_method_enabled() -> bool {
warn_dyn_method_enabled()
}
pub(crate) fn tag_annotation(name: &str) -> Arc<CalcitTypeAnnotation> {
Arc::new(CalcitTypeAnnotation::from_tag_name(name))
}
fn removed_data_api_replacement(name: &str) -> Option<String> {
match name {
"record?" => Some("struct? (values) or struct-def? (definitions)".to_owned()),
"tuple?" => Some("enum? (values) or enum-def? (definitions)".to_owned()),
"record-struct" => Some("struct-definition".to_owned()),
"tuple-enum" => Some("enum-definition".to_owned()),
"record-with" => Some("struct-with".to_owned()),
"record-match" => Some("struct-match".to_owned()),
"&record:struct" => Some("&struct:definition".to_owned()),
"&tuple:enum" => Some("&enum:definition".to_owned()),
"&tuple:enum-has-variant?" => Some("&enum-def:has-variant?".to_owned()),
"&tuple:enum-variant-arity" => Some("&enum-def:variant-arity".to_owned()),
"&tuple:validate-enum" => Some("&enum:validate".to_owned()),
_ if name.starts_with("&record:") => Some(name.replacen("&record:", "&struct:", 1)),
_ if name.starts_with("&tuple:") => Some(name.replacen("&tuple:", "&enum:", 1)),
_ => None,
}
}
fn warn_on_removed_data_api_call(
head: &Calcit,
call_location: Option<NodeLocation>,
file_ns: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
) {
let Calcit::Import(CalcitImport { ns, def, .. }) = head else {
return;
};
if ns.as_ref() != calcit::CORE_NS {
return;
}
let Some(replacement) = removed_data_api_replacement(def) else {
return;
};
let message = format!("[Warn] `{def}` was removed by the struct/enum data-model migration; use `{replacement}`");
if let Some(location) = call_location {
gen_check_warning_with_location_code(message, "W_REMOVED_DATA_API", location, check_warnings);
} else {
gen_check_warning_code(message, "W_REMOVED_DATA_API", file_ns, check_warnings);
}
}
fn is_anonymous_struct_type(type_info: &CalcitTypeAnnotation) -> bool {
matches!(
type_info,
CalcitTypeAnnotation::Custom(value)
if matches!(value.as_ref(), Calcit::Tag(tag) if matches!(tag.ref_str().trim_start_matches(':'), "record" | "struct"))
)
}
pub struct PreprocessContext<'a> {
scope_defs: &'a HashSet<Arc<str>>,
scope_types: &'a mut ScopeTypes,
file_ns: &'a str,
check_warnings: &'a RefCell<Vec<LocatedWarning>>,
call_stack: &'a CallStackList,
}
impl<'a> PreprocessContext<'a> {
fn new(
scope_defs: &'a HashSet<Arc<str>>,
scope_types: &'a mut ScopeTypes,
file_ns: &'a str,
check_warnings: &'a RefCell<Vec<LocatedWarning>>,
call_stack: &'a CallStackList,
) -> Self {
Self {
scope_defs,
scope_types,
file_ns,
check_warnings,
call_stack,
}
}
}
fn store_preprocessed_compiled_output(ns: &str, def: &str, source_code: &Calcit, resolved_code: &Calcit) {
let preprocessed_code = resolved_code.to_owned();
let codegen_form = resolved_code.to_owned();
let deps = program::collect_compiled_deps(&codegen_form);
let type_summary = calcit::CalcitTypeAnnotation::summarize_code(source_code).map(Arc::from);
program::store_compiled_output(
ns,
def,
program::CompiledDefPayload {
version_id: 0,
preprocessed_code,
codegen_form,
deps,
type_summary,
source_code: Some(source_code.to_owned()),
schema: program::lookup_def_schema(ns, def),
doc: program::lookup_def_doc(ns, def).map(Arc::from).unwrap_or_else(|| Arc::from("")),
examples: program::lookup_def_examples(ns, def).unwrap_or_default(),
},
);
}
fn ensure_ns_def_preprocessed(
raw_ns: &str,
raw_def: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
call_stack: &CallStackList,
) -> Result<(), CalcitErr> {
let ns = raw_ns;
let def = raw_def;
if program::lookup_compiled_def(ns, def).is_some() {
return Ok(());
}
let _trace = PreprocessTrace::enter(ns, def);
let saved_fn_type = EXPECTED_FN_TYPE.with(|cell| cell.borrow_mut().take());
let saved_struct_type = EXPECTED_STRUCT_TYPE.with(|cell| cell.borrow_mut().take());
let Some(()) = with_preprocess_compile_guard(ns, def, || match program::lookup_def_code(ns, def) {
Some(code) => {
let next_stack = call_stack.extend(ns, def, StackKind::Fn, &code, &[]);
let mut scope_types = ScopeTypes::new();
let context_label = format!("{ns}/{def}");
let resolved_code = builtins::meta::with_compiling_def(ns, def, || {
calcit::with_type_annotation_warning_context(context_label, || {
preprocess_expr(&code, &HashSet::new(), &mut scope_types, ns, check_warnings, &next_stack)
})
})?;
store_preprocessed_compiled_output(ns, def, &code, &resolved_code);
Ok(())
}
None if ns.starts_with('|') || ns.starts_with('"') => Ok(()),
None => {
let loc = NodeLocation::new(Arc::from(ns), Arc::from(def), Arc::from(vec![]));
Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Var,
format!("unknown ns/def in program: {ns}/{def}"),
call_stack,
Some(loc),
))
}
})?
else {
EXPECTED_FN_TYPE.with(|cell| *cell.borrow_mut() = saved_fn_type);
EXPECTED_STRUCT_TYPE.with(|cell| *cell.borrow_mut() = saved_struct_type);
return Ok(());
};
EXPECTED_FN_TYPE.with(|cell| *cell.borrow_mut() = saved_fn_type);
EXPECTED_STRUCT_TYPE.with(|cell| *cell.borrow_mut() = saved_struct_type);
Ok(())
}
pub fn ensure_ns_def_compiled(
raw_ns: &str,
raw_def: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
call_stack: &CallStackList,
) -> Result<(), CalcitErr> {
ensure_ns_def_preprocessed(raw_ns, raw_def, check_warnings, call_stack)
}
fn preprocess_with_type_slot_block(
head_form: &Calcit,
args: &CalcitList,
scope_defs: &HashSet<Arc<str>>,
scope_types: &mut ScopeTypes,
file_ns: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
call_stack: &CallStackList,
) -> Result<Calcit, CalcitErr> {
let head_location = head_form.get_location();
let Some(binding_expr) = args.first() else {
return Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Arity,
"with-type-slot expected a binding pair as first argument, e.g. `(:dispatch-op Op)`",
call_stack,
head_location,
));
};
let Calcit::List(binding_list) = binding_expr else {
return Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Type,
format!("with-type-slot binding must be a list, got: {binding_expr}"),
call_stack,
binding_expr.get_location(),
));
};
if binding_list.len() != 2 {
return Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Arity,
format!(
"with-type-slot binding must be a 2-element list (:slot-name TypeExpr), got {} elements",
binding_list.len()
),
call_stack,
binding_expr.get_location(),
));
}
let slot_name: Arc<str> = match &binding_list[0] {
Calcit::Tag(tag) => Arc::from(tag.ref_str()),
Calcit::Str(s) => Arc::from(s.as_ref()),
other => {
return Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Type,
format!("with-type-slot name must be a tag or string, got: {other}"),
call_stack,
other.get_location(),
));
}
};
let raw_type_expr = &binding_list[1];
let processed_type_expr = preprocess_expr(raw_type_expr, scope_defs, scope_types, file_ns, check_warnings, call_stack)?;
let resolved = match &processed_type_expr {
Calcit::Import(crate::calcit::CalcitImport { ns, def, .. }) => resolve_program_value_for_preprocess(ns, def, None),
Calcit::Symbol { sym, info, .. } => resolve_program_value_for_preprocess(&info.at_ns, sym, None),
other => Some(other.to_owned()),
};
let Some(resolved) = resolved else {
return Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Unexpected,
format!("with-type-slot could not resolve type value for slot `{slot_name}`"),
call_stack,
raw_type_expr.get_location(),
));
};
let import_path: Option<(Arc<str>, Arc<str>)> = match &processed_type_expr {
Calcit::Import(crate::calcit::CalcitImport { ns, def, .. }) => Some((ns.clone(), def.clone())),
_ => None,
};
let type_annotation: Arc<CalcitTypeAnnotation> = if let Some((ns, def)) = &import_path {
match &resolved {
Calcit::EnumDef(_) | Calcit::StructDef(_) => {
Arc::new(CalcitTypeAnnotation::TypeRef(Arc::from(format!("{ns}/{def}")), Arc::new(vec![])))
}
Calcit::Struct(struct_value) => Arc::new(CalcitTypeAnnotation::StructValue(struct_value.struct_ref.clone())),
other => {
return Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Unexpected,
format!(
"with-type-slot expected an enum or struct import, got: {}",
brief_type_of_value(other)
),
call_stack,
raw_type_expr.get_location(),
));
}
}
} else {
match &resolved {
Calcit::EnumDef(enum_def) => Arc::new(CalcitTypeAnnotation::Enum(Arc::new(enum_def.to_owned()), Arc::new(vec![]))),
Calcit::StructDef(struct_def) => Arc::new(CalcitTypeAnnotation::Struct(Arc::new(struct_def.to_owned()), Arc::new(vec![]))),
Calcit::Struct(struct_value) => Arc::new(CalcitTypeAnnotation::StructValue(struct_value.struct_ref.clone())),
other => match infer_type_from_expr(other, scope_types) {
Some(inferred)
if matches!(
inferred.as_ref(),
CalcitTypeAnnotation::Enum(_, _) | CalcitTypeAnnotation::Struct(_, _) | CalcitTypeAnnotation::StructValue(_)
) =>
{
inferred
}
_ => {
return Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Unexpected,
format!(
"with-type-slot expected an enum, struct, or record as type value, got: {}",
brief_type_of_value(other)
),
call_stack,
raw_type_expr.get_location(),
));
}
},
}
};
let body_args = args.drop_left();
if body_args.is_empty() {
return Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Arity,
"with-type-slot expected at least one body expression",
call_stack,
head_location,
));
}
push_type_slot_override(slot_name.clone(), type_annotation);
let mut preprocessed_body: Vec<Calcit> = Vec::with_capacity(body_args.len());
let mut preprocess_err: Option<CalcitErr> = None;
for expr in body_args.iter() {
match preprocess_expr(expr, scope_defs, scope_types, file_ns, check_warnings, call_stack) {
Ok(form) => preprocessed_body.push(form),
Err(e) => {
preprocess_err = Some(e);
break;
}
}
}
pop_type_slot_override(&slot_name);
if let Some(e) = preprocess_err {
return Err(e);
}
if preprocessed_body.len() == 1 {
return Ok(preprocessed_body.remove(0));
}
let mut result_items = vec![
Calcit::Syntax(CalcitSyntax::CoreLet, Arc::from(file_ns)),
Calcit::from(CalcitList::default()),
];
result_items.extend(preprocessed_body);
Ok(Calcit::from(CalcitList::from(result_items.as_slice())))
}
fn lookup_callable_ns_def_for_preprocess(
raw_ns: &str,
raw_def: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
call_stack: &CallStackList,
) -> Result<Option<Calcit>, CalcitErr> {
ensure_ns_def_compiled(raw_ns, raw_def, check_warnings, call_stack)?;
Ok(
match program::resolve_compiled_executable_def(raw_ns, raw_def, call_stack).ok().flatten() {
value @ Some(Calcit::Macro { .. } | Calcit::Fn { .. }) => value,
_ => None,
},
)
}
fn resolve_trait_def_from_source_code(code: &Calcit) -> Option<CalcitTrait> {
if let Calcit::Thunk(thunk) = code {
return resolve_trait_def_from_source_code(thunk.get_code());
}
let Calcit::List(items) = code else {
return None;
};
if let Some(head) = items.first()
&& (matches!(head, Calcit::Syntax(CalcitSyntax::Quote, _))
|| matches!(head, Calcit::Symbol { sym, .. } if sym.as_ref() == "quote")
|| matches!(head, Calcit::Import(CalcitImport { ns, def, .. }) if &**ns == calcit::CORE_NS && &**def == "quote"))
&& let Some(inner) = items.get(1)
{
return resolve_trait_def_from_source_code(inner);
}
let head = items.first()?;
if matches!(head, Calcit::Proc(CalcitProc::NativeTraitNew))
|| matches!(head, Calcit::Symbol { sym, .. } if sym.as_ref() == "&trait::new")
|| matches!(head, Calcit::Import(CalcitImport { ns, def, .. }) if &**ns == calcit::CORE_NS && &**def == "&trait::new")
{
return parse_trait_new_source(items.as_ref());
}
if matches!(head, Calcit::Symbol { sym, .. } if sym.as_ref() == "deftrait")
|| matches!(head, Calcit::Import(CalcitImport { ns, def, .. }) if &**ns == calcit::CORE_NS && &**def == "deftrait")
{
return parse_deftrait_source(items.as_ref());
}
None
}
fn parse_trait_name_from_source(form: &Calcit) -> Option<EdnTag> {
match form {
Calcit::Symbol { sym, .. } | Calcit::Str(sym) => Some(EdnTag::from(sym.as_ref())),
Calcit::Tag(tag) => Some(tag.to_owned()),
_ => None,
}
}
fn parse_trait_method_name_from_source(form: &Calcit) -> Option<EdnTag> {
match form {
Calcit::Method(name, _) | Calcit::Symbol { sym: name, .. } | Calcit::Str(name) => Some(EdnTag::from(name.as_ref())),
Calcit::Tag(tag) => Some(tag.to_owned()),
_ => None,
}
}
fn parse_trait_member_kind_from_source(form: &Calcit) -> CalcitTraitMemberKind {
match form {
Calcit::Tag(_) => CalcitTraitMemberKind::Field,
_ => CalcitTraitMemberKind::Method,
}
}
type TraitMemberSpecs = (Vec<EdnTag>, Vec<Arc<CalcitTypeAnnotation>>, Vec<CalcitTraitMemberKind>);
fn parse_trait_method_specs_from_source<'a>(items: impl Iterator<Item = &'a Calcit>) -> Option<TraitMemberSpecs> {
let mut methods: Vec<EdnTag> = vec![];
let mut method_types: Vec<Arc<CalcitTypeAnnotation>> = vec![];
let mut member_kinds: Vec<CalcitTraitMemberKind> = vec![];
for item in items {
let Calcit::List(entry) = item else {
return None;
};
if entry.len() != 2 {
return None;
}
let method_name = parse_trait_method_name_from_source(entry.first()?)?;
let type_form = entry.get(1)?;
let method_type = calcit::with_type_annotation_warning_context(format!("trait:{}", method_name.ref_str()), || {
CalcitTypeAnnotation::parse_type_annotation_form(type_form)
});
methods.push(method_name);
method_types.push(method_type);
member_kinds.push(parse_trait_member_kind_from_source(entry.first()?));
}
Some((methods, method_types, member_kinds))
}
fn parse_trait_new_source(items: &CalcitList) -> Option<CalcitTrait> {
let name = parse_trait_name_from_source(items.get(1)?)?;
let method_specs = match items.get(2)? {
Calcit::List(list) => list,
_ => return None,
};
let (methods, method_types, member_kinds) = parse_trait_method_specs_from_source(method_specs.iter())?;
Some(CalcitTrait::new_with_member_kinds(name, methods, method_types, Some(member_kinds)))
}
fn parse_deftrait_source(items: &CalcitList) -> Option<CalcitTrait> {
let name = parse_trait_name_from_source(items.get(1)?)?;
let (methods, method_types, member_kinds) = parse_trait_method_specs_from_source(items.iter().skip(2))?;
Some(CalcitTrait::new_with_member_kinds(name, methods, method_types, Some(member_kinds)))
}
fn resolve_where_bound_type_for_body(bound: &crate::calcit::CalcitGenericBound, file_ns: &str) -> Option<Arc<CalcitTypeAnnotation>> {
let mut traits = Vec::with_capacity(bound.traits.len());
for trait_ref in bound.traits.iter() {
if !trait_ref.methods.is_empty() {
traits.push(trait_ref.clone());
continue;
}
let raw_name = trait_ref.name.ref_str();
let (trait_ns, trait_name) = if let Some((ns, name)) = raw_name.rsplit_once('/') {
(Arc::from(ns), Arc::from(name))
} else if program::has_def_code(file_ns, raw_name) {
(Arc::from(file_ns), Arc::from(raw_name))
} else if let Some(target_ns) = program::lookup_def_target_in_import(file_ns, raw_name) {
(target_ns, Arc::from(raw_name))
} else if program::has_def_code(calcit::CORE_NS, raw_name) {
(Arc::from(calcit::CORE_NS), Arc::from(raw_name))
} else {
return None;
};
let resolved = program::lookup_def_code(&trait_ns, &trait_name)
.and_then(|code| resolve_trait_def_from_source_code(&code))?
.with_definition_ref(&trait_ns, &trait_name);
traits.push(Arc::new(resolved));
}
match traits.len() {
0 => None,
1 => Some(Arc::new(CalcitTypeAnnotation::Trait(traits.remove(0)))),
_ => Some(Arc::new(CalcitTypeAnnotation::TraitSet(Arc::new(traits)))),
}
}
fn resolve_local_type_refs_for_body(annotation: Arc<CalcitTypeAnnotation>, scope_types: &ScopeTypes) -> Arc<CalcitTypeAnnotation> {
match annotation.as_ref() {
CalcitTypeAnnotation::TypeRef(name, args) => {
let resolved_args = Arc::new(
args
.iter()
.map(|arg| resolve_local_type_refs_for_body(arg.clone(), scope_types))
.collect::<Vec<_>>(),
);
let short_name = name.rsplit('/').next().unwrap_or(name.as_ref());
let local_type = scope_types.get(name).or_else(|| scope_types.get(short_name));
match local_type.map(AsRef::as_ref) {
Some(CalcitTypeAnnotation::Struct(struct_def, _)) | Some(CalcitTypeAnnotation::StructValue(struct_def)) => {
Arc::new(CalcitTypeAnnotation::Struct(struct_def.clone(), resolved_args))
}
Some(CalcitTypeAnnotation::Enum(enum_def, _)) | Some(CalcitTypeAnnotation::EnumValue(enum_def)) => {
Arc::new(CalcitTypeAnnotation::Enum(enum_def.clone(), resolved_args))
}
Some(CalcitTypeAnnotation::Trait(trait_def)) if resolved_args.is_empty() => {
Arc::new(CalcitTypeAnnotation::Trait(trait_def.clone()))
}
_ => Arc::new(CalcitTypeAnnotation::TypeRef(name.clone(), resolved_args)),
}
}
CalcitTypeAnnotation::List(inner) => Arc::new(CalcitTypeAnnotation::List(resolve_local_type_refs_for_body(
inner.clone(),
scope_types,
))),
CalcitTypeAnnotation::Map(key, value) => Arc::new(CalcitTypeAnnotation::Map(
resolve_local_type_refs_for_body(key.clone(), scope_types),
resolve_local_type_refs_for_body(value.clone(), scope_types),
)),
CalcitTypeAnnotation::Set(inner) => Arc::new(CalcitTypeAnnotation::Set(resolve_local_type_refs_for_body(
inner.clone(),
scope_types,
))),
CalcitTypeAnnotation::Ref(inner) => Arc::new(CalcitTypeAnnotation::Ref(resolve_local_type_refs_for_body(
inner.clone(),
scope_types,
))),
CalcitTypeAnnotation::Optional(inner) => Arc::new(CalcitTypeAnnotation::Optional(resolve_local_type_refs_for_body(
inner.clone(),
scope_types,
))),
CalcitTypeAnnotation::JsNullish(inner) => Arc::new(CalcitTypeAnnotation::JsNullish(resolve_local_type_refs_for_body(
inner.clone(),
scope_types,
))),
CalcitTypeAnnotation::Variadic(inner) => Arc::new(CalcitTypeAnnotation::Variadic(resolve_local_type_refs_for_body(
inner.clone(),
scope_types,
))),
CalcitTypeAnnotation::Fn(signature) => Arc::new(CalcitTypeAnnotation::Fn(Arc::new(CalcitFnTypeAnnotation {
generics: signature.generics.clone(),
where_bounds: signature.where_bounds.clone(),
arg_types: signature
.arg_types
.iter()
.map(|arg| resolve_local_type_refs_for_body(arg.clone(), scope_types))
.collect(),
return_type: resolve_local_type_refs_for_body(signature.return_type.clone(), scope_types),
fn_kind: signature.fn_kind,
rest_type: signature
.rest_type
.as_ref()
.map(|rest| resolve_local_type_refs_for_body(rest.clone(), scope_types)),
features: signature.features.clone(),
}))),
CalcitTypeAnnotation::Struct(struct_def, args) => Arc::new(CalcitTypeAnnotation::Struct(
struct_def.clone(),
Arc::new(
args
.iter()
.map(|arg| resolve_local_type_refs_for_body(arg.clone(), scope_types))
.collect(),
),
)),
CalcitTypeAnnotation::Enum(enum_def, args) => Arc::new(CalcitTypeAnnotation::Enum(
enum_def.clone(),
Arc::new(
args
.iter()
.map(|arg| resolve_local_type_refs_for_body(arg.clone(), scope_types))
.collect(),
),
)),
_ => annotation,
}
}
fn unwrap_named_body_parameter_type(annotation: Arc<CalcitTypeAnnotation>, parameter: Option<&Arc<str>>) -> Arc<CalcitTypeAnnotation> {
let Some(parameter) = parameter else {
return annotation;
};
match annotation.as_ref() {
CalcitTypeAnnotation::TypeRef(label, args)
if args.len() == 1 && label.rsplit('/').next().is_some_and(|name| name == parameter.as_ref()) =>
{
args.first().expect("checked one named parameter type").clone()
}
_ => annotation,
}
}
fn lookup_trait_ns_def_for_preprocess(
raw_ns: &str,
raw_def: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
call_stack: &CallStackList,
) -> Result<Option<Arc<CalcitTrait>>, CalcitErr> {
ensure_ns_def_compiled(raw_ns, raw_def, check_warnings, call_stack)?;
Ok(
program::lookup_compiled_def(raw_ns, raw_def)
.and_then(|compiled| compiled.source_code)
.and_then(|code| resolve_trait_def_from_source_code(&code))
.map(|trait_def| trait_def.with_definition_ref(raw_ns, raw_def))
.map(Arc::new),
)
}
pub fn compile_source_def_for_snapshot(
ns: &str,
def: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
call_stack: &CallStackList,
) -> Result<(), CalcitErr> {
if program::lookup_compiled_def(ns, def).is_some() {
return Ok(());
}
let Some(code) = program::lookup_def_code(ns, def) else {
let loc = NodeLocation::new(Arc::from(ns), Arc::from(def), Arc::from(vec![]));
return Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Var,
format!("unknown ns/def in program: {ns}/{def}"),
call_stack,
Some(loc),
));
};
let mut scope_types = ScopeTypes::new();
let context_label = format!("{ns}/{def}");
let resolved_code = builtins::meta::with_compiling_def(ns, def, || {
calcit::with_type_annotation_warning_context(context_label, || {
preprocess_expr(&code, &HashSet::new(), &mut scope_types, ns, check_warnings, call_stack)
})
})?;
store_preprocessed_compiled_output(ns, def, &code, &resolved_code);
Ok(())
}
pub fn preprocess_expr(
expr: &Calcit,
scope_defs: &HashSet<Arc<str>>,
scope_types: &mut ScopeTypes,
file_ns: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
call_stack: &CallStackList,
) -> Result<Calcit, CalcitErr> {
match expr {
Calcit::Symbol {
sym: def, info, location, ..
} => {
match runner::parse_ns_def(def) {
Some((ns_alias, def_part)) => {
if &*ns_alias == "js" {
Ok(Calcit::RawCode(RawCodeType::Js, def_part))
} else if is_registered_proc(def) {
Ok(Calcit::Registered(def.to_owned()))
} else if let Some(target_ns) = program::lookup_ns_target_in_import(&info.at_ns, &ns_alias) {
ensure_ns_def_compiled(&target_ns, &def_part, check_warnings, call_stack)?;
let form = Calcit::Import(CalcitImport {
ns: target_ns.to_owned(),
def: def_part.to_owned(),
info: Arc::new(ImportInfo::NsAs {
alias: ns_alias.to_owned(),
at_def: info.at_def.to_owned(),
at_ns: ns_alias,
}),
def_id: Some(program::ensure_def_id(&target_ns, &def_part).0),
});
Ok(form)
} else if program::has_def_code(&ns_alias, &def_part) {
ensure_ns_def_compiled(&ns_alias, &def_part, check_warnings, call_stack)?;
let form = Calcit::Import(CalcitImport {
ns: ns_alias.to_owned(),
def: def_part.to_owned(),
info: Arc::new(ImportInfo::NsReferDef {
at_ns: info.at_ns.to_owned(),
at_def: info.at_def.to_owned(),
}),
def_id: Some(program::ensure_def_id(&ns_alias, &def_part).0),
});
Ok(form)
} else {
Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Var,
format!("unknown ns target: {def}"),
call_stack,
expr.get_location(),
))
}
}
None => {
let def_ns = &info.at_ns;
let at_def = &info.at_def;
if scope_defs.contains(def) {
let type_info = scope_types.get(def).cloned().unwrap_or_else(|| calcit::DYNAMIC_TYPE.clone());
Ok(Calcit::Local(CalcitLocal {
idx: CalcitLocal::track_sym(def),
sym: def.to_owned(),
info: Arc::new(CalcitSymbolInfo {
at_ns: def_ns.to_owned(),
at_def: at_def.to_owned(),
}),
location: location.to_owned(),
type_info,
}))
} else if CalcitSyntax::is_valid(def) {
Ok(Calcit::Syntax(
def.parse().map_err(|e: ParseError| {
CalcitErr::use_msg_stack_location(
CalcitErrKind::Syntax,
def.to_string() + " " + &e.to_string(),
call_stack,
expr.get_location(),
)
})?,
def_ns.to_owned(),
))
} else if *def == info.at_def {
ensure_ns_def_compiled(def_ns, def, check_warnings, call_stack)?;
let form = Calcit::Import(CalcitImport {
ns: def_ns.to_owned(),
def: def.to_owned(),
info: Arc::new(ImportInfo::SameFile {
at_def: info.at_def.to_owned(),
}),
def_id: Some(program::ensure_def_id(def_ns, def).0),
});
Ok(form)
} else if let Ok(p) = def.parse::<CalcitProc>() {
Ok(Calcit::Proc(p))
} else if program::has_def_code(calcit::CORE_NS, def) {
ensure_ns_def_compiled(calcit::CORE_NS, def, check_warnings, call_stack)?;
let form = Calcit::Import(CalcitImport {
ns: calcit::CORE_NS.into(),
def: def.to_owned(),
info: Arc::new(ImportInfo::Core { at_ns: file_ns.into() }),
def_id: Some(program::ensure_def_id(calcit::CORE_NS, def).0),
});
Ok(form)
} else if program::has_def_code(def_ns, def) {
ensure_ns_def_compiled(def_ns, def, check_warnings, call_stack)?;
let form = Calcit::Import(CalcitImport {
ns: def_ns.to_owned(),
def: def.to_owned(),
info: Arc::new(if &**def_ns == file_ns {
ImportInfo::SameFile {
at_def: info.at_def.to_owned(),
}
} else {
ImportInfo::NsReferDef {
at_ns: file_ns.into(),
at_def: at_def.to_owned(),
}
}),
def_id: Some(program::ensure_def_id(def_ns, def).0),
});
Ok(form)
} else if is_registered_proc(def) {
Ok(Calcit::Registered(def.to_owned()))
} else {
match program::lookup_def_target_in_import(def_ns, def) {
Some(target_ns) => {
ensure_ns_def_compiled(&target_ns, def, check_warnings, call_stack)?;
let form = Calcit::Import(CalcitImport {
ns: target_ns.to_owned(),
def: def.to_owned(),
info: Arc::new(ImportInfo::NsReferDef {
at_ns: def_ns.to_owned(),
at_def: at_def.to_owned(),
}),
def_id: Some(program::ensure_def_id(&target_ns, def).0),
});
Ok(form)
}
None if codegen::codegen_mode() && is_js_syntax_procs(def) => {
let has_ffi = CURRENT_FN_FEATURES.with(|cell| {
cell
.borrow()
.as_ref()
.is_some_and(|features| features.contains(&EdnTag::new("js-ffi")))
}) || program::lookup_def_schema(&info.at_ns, &info.at_def)
.as_ref()
.as_fn()
.is_some_and(|fn_annot| fn_annot.features.contains(&EdnTag::new("js-ffi")));
if !has_ffi {
gen_check_warning_with_location(
format!(
"[Warn] JS FFI function `{def}` used in `{def_ns}/{at_def}` without `:js-ffi` feature in schema — consider isolating FFI operations into dedicated binding functions with `:features $ #{{}} :js-ffi`",
),
NodeLocation::new(def_ns.to_owned(), at_def.to_owned(), location.to_owned().unwrap_or_default()),
check_warnings,
);
}
Ok(expr.to_owned())
}
None => {
let from_default = program::lookup_default_target_in_import(def_ns, def);
if let Some(target_ns) = from_default {
Ok(Calcit::Import(CalcitImport {
ns: target_ns.to_owned(),
def: Arc::from("default"),
info: Arc::new(ImportInfo::JsDefault {
alias: def.to_owned(),
at_ns: file_ns.into(),
at_def: at_def.to_owned(),
}),
def_id: None,
}))
} else {
let mut names: Vec<Arc<str>> = Vec::with_capacity(scope_defs.len());
for def in scope_defs {
names.push(def.to_owned());
}
let node_location = NodeLocation::new(def_ns.to_owned(), at_def.to_owned(), location.to_owned().unwrap_or_default());
if let Some(replacement) = removed_data_api_replacement(def) {
gen_check_warning_with_location_code(
format!("[Warn] `{def}` was removed by the struct/enum data-model migration; use `{replacement}`"),
"W_REMOVED_DATA_API",
node_location,
check_warnings,
);
} else {
gen_check_warning_with_location(
format!("[Warn] unknown `{def}` in {def_ns}/{at_def}, locals {{{}}}", names.join(" ")),
node_location,
check_warnings,
);
}
Ok(expr.to_owned())
}
}
}
}
}
}
}
Calcit::List(xs) => {
if xs.is_empty() {
Ok(expr.to_owned())
} else {
preprocess_list_call(xs, scope_defs, scope_types, file_ns, check_warnings, call_stack)
}
}
Calcit::Number(..)
| Calcit::Str(..)
| Calcit::Nil
| Calcit::Bool(..)
| Calcit::Tag(..)
| Calcit::CirruQuote(..)
| Calcit::StructDef(..)
| Calcit::EnumDef(..)
| Calcit::Struct(..)
| Calcit::Local(..) => Ok(expr.to_owned()),
Calcit::Method(..) => Ok(expr.to_owned()),
Calcit::Proc(..) => Ok(expr.to_owned()),
Calcit::Syntax(..) => Ok(expr.to_owned()),
Calcit::Import { .. } => Ok(expr.to_owned()),
_ => {
eprintln!("unknown expr: {expr}");
gen_check_warning(
format!("[Warn] unexpected data during preprocess: {expr:?}"),
file_ns,
check_warnings,
);
Ok(expr.to_owned())
}
}
}
fn preprocess_list_call(
xs: &CalcitList,
scope_defs: &HashSet<Arc<str>>,
scope_types: &mut ScopeTypes,
file_ns: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
call_stack: &CallStackList,
) -> Result<Calcit, CalcitErr> {
let head = &xs[0];
let call_location = derive_call_expr_location(head);
let head_form = preprocess_expr(head, scope_defs, scope_types, file_ns, check_warnings, call_stack)?;
let args = xs.drop_left();
let mut def_name = grab_def_name(head);
if def_name.as_ref() == "??"
&& let Some(receiver) = args.first()
{
def_name = grab_def_name(receiver);
}
let call_info = CallTypeCheckInfo {
file_ns,
def_name: &def_name,
call_location: call_location.clone(),
};
warn_on_removed_data_api_call(&head_form, call_location.clone(), file_ns, check_warnings);
let has_anonymous_definition_marker = matches!(args.first(), Some(Calcit::Symbol { sym, .. }) if sym.as_ref() == "_");
let is_constructor_named = |name: &str| {
matches!(&head_form, Calcit::Import(CalcitImport { ns, def, .. }) if ns.as_ref() == calcit::CORE_NS && def.as_ref() == name)
|| matches!(&head_form, Calcit::Symbol { sym, .. } if sym.as_ref() == name)
};
if has_anonymous_definition_marker && is_constructor_named("%{}") {
let mut items = vec![Calcit::Proc(CalcitProc::NativeLooseStruct)];
for entry in args.iter().skip(1) {
let Calcit::List(pair) = entry else {
return CalcitErr::err_str(
CalcitErrKind::Type,
format!("%{{}} _ expects (:field value) entries, but received: {entry}"),
);
};
if pair.len() != 2 {
return CalcitErr::err_str(
CalcitErrKind::Arity,
format!("%{{}} _ expects (:field value) entries, but received: {entry}"),
);
}
items.extend(pair.iter().cloned());
}
return preprocess_expr(
&Calcit::from(CalcitList::from(items.as_slice())),
scope_defs,
scope_types,
file_ns,
check_warnings,
call_stack,
);
}
if has_anonymous_definition_marker
&& (matches!(&head_form, Calcit::Proc(CalcitProc::NativeNamedEnumNew)) || is_constructor_named("%::"))
{
let mut items = vec![Calcit::Proc(CalcitProc::NativeEnum)];
items.extend(args.iter().skip(1).cloned());
return preprocess_expr(
&Calcit::from(CalcitList::from(items.as_slice())),
scope_defs,
scope_types,
file_ns,
check_warnings,
call_stack,
);
}
if let Calcit::Method(field_name, calcit::MethodKind::TagAccess) = &head_form
&& args.len() == 1
&& let Some(receiver_type) = resolve_type_value(&args[0], scope_types)
&& let Some(traits) = trait_list_from_type(receiver_type.as_ref())
&& traits.iter().any(|trait_def| trait_is_external_object(trait_def.as_ref()))
&& find_trait_field_type(&traits, field_name.as_ref()).is_some()
{
let typed_access = Calcit::Method(field_name.clone(), calcit::MethodKind::ExternalAccess(receiver_type));
let processed_receiver = preprocess_expr(&args[0], scope_defs, scope_types, file_ns, check_warnings, call_stack)?;
return Ok(Calcit::from(CalcitList::from(&[typed_access, processed_receiver])));
}
if let Some(rewritten) =
try_rewrite_struct_enum_constructor_head_call(&head_form, &args, scope_types, file_ns, &def_name, check_warnings, call_stack)?
{
return preprocess_expr(&rewritten, scope_defs, scope_types, file_ns, check_warnings, call_stack);
}
if !args.is_empty() {
let first_arg = &args[0];
match first_arg {
Calcit::Tag(field_tag) if args.len() == 1 => {
if let Some(type_info) = resolve_type_value(&head_form, scope_types) {
if let Some(traits) = trait_list_from_type(type_info.as_ref())
&& traits.iter().any(|trait_def| trait_is_external_object(trait_def.as_ref()))
&& find_trait_field_type(&traits, field_tag.ref_str()).is_some()
{
let typed_access = Calcit::Method(
Arc::from(field_tag.ref_str()),
calcit::MethodKind::ExternalAccess(type_info.clone()),
);
return Ok(Calcit::from(CalcitList::from(&[typed_access, head_form])));
}
if let Some(struct_def) = type_info.as_ref().resolve_to_struct()
&& let Some(idx) = struct_def.index_of(field_tag.ref_str())
{
let items: Vec<Calcit> = vec![
Calcit::Proc(CalcitProc::NativeStructNth),
head_form,
Calcit::Number(idx as f64),
Calcit::Tag(field_tag.to_owned()),
];
return Ok(Calcit::from(CalcitList::from(items.as_slice())));
}
if type_info.as_ref().resolve_to_struct().is_some() || is_anonymous_struct_type(type_info.as_ref()) {
return Ok(Calcit::from(CalcitList::from(&[
Calcit::Proc(CalcitProc::NativeStructGet),
head_form,
first_arg.to_owned(),
])));
}
}
}
Calcit::Method(method_name, method_kind) => {
if matches!(method_kind, calcit::MethodKind::Invoke(_))
&& let Some(type_info) = resolve_type_value(&head_form, scope_types)
{
let is_nominal_or_trait = type_info.as_ref().resolve_to_struct().is_some()
|| type_info.as_ref().resolve_to_enum().is_some()
|| matches!(type_info.as_ref(), CalcitTypeAnnotation::TypeRef(..))
|| trait_list_from_type(type_info.as_ref()).is_some();
let has_known_method = static_method_descriptors(type_info.as_ref()).is_some_and(|methods| {
let expected = format!(".{method_name}");
methods.iter().any(|method| method.name == expected)
});
if is_nominal_or_trait || has_known_method {
let is_external = trait_list_from_type(type_info.as_ref())
.is_some_and(|traits| traits.iter().any(|trait_def| trait_is_external_object(trait_def.as_ref())));
let method_kind = if is_external {
calcit::MethodKind::ExternalInvoke(type_info.clone())
} else {
calcit::MethodKind::Invoke(type_info.clone())
};
let typed_method = Calcit::Method(method_name.clone(), method_kind);
let mut processed_args = CalcitList::new_inner_from(&[head_form]);
for arg in args.iter().skip(1) {
processed_args = processed_args.push(preprocess_expr(arg, scope_defs, scope_types, file_ns, check_warnings, call_stack)?);
}
let processed_args = CalcitList::from(processed_args);
check_struct_method_args(&typed_method, &processed_args, scope_types, file_ns, &def_name, check_warnings);
let mut ys = CalcitList::new_inner_from(&[typed_method]);
for arg in processed_args.iter() {
ys = ys.push(arg.to_owned());
}
return Ok(Calcit::from(CalcitList::from(ys)));
}
}
if warn_dyn_method_enabled()
&& file_ns != calcit::CORE_NS
&& resolve_type_value(&head_form, scope_types).is_none_or(|type_info| is_dynamic_annotation(type_info.as_ref()))
{
let message = format!(
"[Warn] postfix method `.{method_name}` has a dynamic receiver in {file_ns}/{def_name}; use prefix syntax `(.{method_name} receiver ...)`, assert-traits, or unsafe-coerce at an FFI boundary"
);
if let Some(loc) = head_form.get_location().or_else(|| first_arg.get_location()) {
gen_check_warning_with_location_code(message, "P_DYNAMIC_POSTFIX_METHOD", loc, check_warnings);
} else {
gen_check_warning_code(message, "P_DYNAMIC_POSTFIX_METHOD", file_ns, check_warnings);
}
}
}
_ => {}
}
}
let head_value = match &head_form {
Calcit::Import(CalcitImport { ns, def, .. }) => lookup_callable_ns_def_for_preprocess(ns, def, check_warnings, call_stack)?,
_ => None,
};
match head_value {
Some(Calcit::Macro { info, .. }) => {
let mut current_values: Vec<Calcit> = args.to_vec();
warn_on_trait_impl_method_tag_syntax(info.as_ref(), &args, file_ns, def_name.as_ref(), check_warnings);
let code = Calcit::List(Arc::new(xs.to_owned()));
let next_stack = call_stack.extend_owned(&info.def_ns, &info.name, StackKind::Macro, code, args.to_vec());
let mut body_scope = CalcitScope::default();
loop {
runner::bind_marked_args(&mut body_scope, &info.args, ¤t_values, &next_stack)?;
let code = runner::evaluate_lines(&info.body.to_vec(), &body_scope, file_ns, &next_stack)?;
match code {
Calcit::Recur(ys) => {
current_values = ys;
}
_ => {
return preprocess_expr(&code, scope_defs, scope_types, file_ns, check_warnings, &next_stack);
}
}
}
}
Some(Calcit::Fn { info, .. }) => {
match &*info.args {
CalcitFnArgs::MarkedArgs(xs) => {
check_fn_marked_args(xs, &args, file_ns, &info.name, &def_name, check_warnings);
}
CalcitFnArgs::Args(xs) => {
check_fn_args(xs, &args, file_ns, &info.name, &def_name, check_warnings);
}
}
let mut ys = CalcitList::new_inner_from(std::slice::from_ref(&head_form));
let mut has_spread = false;
for (arg_idx, a) in args.iter().enumerate() {
if let Calcit::Syntax(CalcitSyntax::ArgSpread, _) = a {
has_spread = true;
ys = ys.push(a.to_owned());
continue;
}
let expected_fn = if arg_idx < info.arg_types.len() {
if let CalcitTypeAnnotation::Fn(fn_annot) = info.arg_types[arg_idx].as_ref() {
Some(fn_annot.clone())
} else {
None
}
} else {
None
};
let expected_struct = if arg_idx < info.arg_types.len() {
info.arg_types[arg_idx].resolve_to_struct_with_ref().map(|(s, _)| s)
} else {
None
};
if let Some(fn_annot) = expected_fn {
EXPECTED_FN_TYPE.with(|cell| cell.borrow_mut().replace(fn_annot));
}
if let Some(struct_def) = expected_struct {
EXPECTED_STRUCT_TYPE.with(|cell| cell.borrow_mut().replace(struct_def));
}
let result = preprocess_expr(a, scope_defs, scope_types, file_ns, check_warnings, call_stack);
EXPECTED_FN_TYPE.with(|cell| *cell.borrow_mut() = None);
EXPECTED_STRUCT_TYPE.with(|cell| *cell.borrow_mut() = None);
let form = result?;
ys = ys.push(form);
}
if !has_spread {
let mut current_args = CalcitList::from(ys.drop_left());
check_struct_field_access(&head_form, ¤t_args, scope_types, file_ns, check_warnings);
warn_on_nominal_enum_legacy_absence_use(head, ¤t_args, scope_types, file_ns, def_name.as_ref(), check_warnings);
warn_on_legacy_js_nullish_predicate(head, ¤t_args, scope_types, file_ns, def_name.as_ref(), check_warnings);
let mut any_rewritten = false;
if let Some(rewritten) = try_rewrite_map_args_to_structs(info.as_ref(), ¤t_args, file_ns, &def_name, check_warnings) {
current_args = rewritten;
any_rewritten = true;
}
if let Some(rewritten) =
try_rewrite_loose_struct_args_to_structs(info.as_ref(), ¤t_args, file_ns, &def_name, check_warnings)
{
current_args = rewritten;
any_rewritten = true;
}
if let Some(rewritten) = try_rewrite_enum_args_to_named_enums(info.as_ref(), ¤t_args, file_ns, &def_name, check_warnings)
{
current_args = rewritten;
any_rewritten = true;
}
if any_rewritten {
let mut new_ys = CalcitList::new_inner_from(std::slice::from_ref(&head_form));
for item in current_args.iter() {
new_ys = new_ys.push(item.to_owned());
}
ys = new_ys;
}
check_core_fn_arg_types(
info.as_ref(),
¤t_args,
scope_types,
file_ns,
&def_name,
call_location.clone(),
check_warnings,
);
check_user_fn_arg_types(info.as_ref(), &head_form, ¤t_args, scope_types, &call_info, check_warnings);
}
if has_spread {
ys = ys.prepend(Calcit::Syntax(CalcitSyntax::CallSpread, info.def_ns.to_owned()));
Ok(Calcit::from(CalcitList::from(ys)))
} else {
if let Calcit::Import(CalcitImport { ns, def, .. }) = &head_form {
let current_args = CalcitList::from(ys.drop_left());
if let Some(specialized) = try_specialize_polymorphic_call(ns, def, ¤t_args, scope_types, file_ns) {
return Ok(specialized);
}
}
Ok(Calcit::from(CalcitList::from(ys)))
}
}
_ => match &head_form {
Calcit::Tag(tag) => {
if args.len() == 1 {
let processed_arg = preprocess_expr(&args[0], scope_defs, scope_types, file_ns, check_warnings, call_stack)?;
if let Some(type_info) = resolve_type_value(&processed_arg, scope_types)
&& let Some(struct_def) = type_info.as_ref().resolve_to_struct()
&& let Some(idx) = struct_def.index_of(tag.ref_str())
{
let items: Vec<Calcit> = vec![
Calcit::Proc(CalcitProc::NativeStructNth),
processed_arg,
Calcit::Number(idx as f64),
Calcit::Tag(tag.to_owned()),
];
let nth_call = Calcit::from(CalcitList::from(items.as_slice()));
return Ok(nth_call);
}
if let Some(type_info) = resolve_type_value(&processed_arg, scope_types)
&& (type_info.as_ref().resolve_to_struct().is_some() || is_anonymous_struct_type(type_info.as_ref()))
{
return Ok(Calcit::from(CalcitList::from(&[
Calcit::Proc(CalcitProc::NativeStructGet),
processed_arg,
head.to_owned(),
])));
}
let get_method = Calcit::Import(CalcitImport {
ns: calcit::CORE_NS.into(),
def: "get".into(),
info: Arc::new(ImportInfo::Core { at_ns: Arc::from(file_ns) }),
def_id: Some(program::ensure_def_id(calcit::CORE_NS, "get").0),
});
let code = Calcit::from(CalcitList::from(&[get_method, args[0].to_owned(), head.to_owned()]));
preprocess_expr(&code, scope_defs, scope_types, file_ns, check_warnings, call_stack)
} else {
Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Arity,
format!("{head} expected 1 hashmap to call"),
call_stack,
head.get_location(),
))
}
}
Calcit::Syntax(name, name_ns) => match name {
CalcitSyntax::Quasiquote => {
let mut ctx = PreprocessContext::new(scope_defs, scope_types, file_ns, check_warnings, call_stack);
Ok(preprocess_quasiquote(name, name_ns, &args, &mut ctx)?)
}
CalcitSyntax::Defn | CalcitSyntax::Defmacro | CalcitSyntax::DefWasmExport | CalcitSyntax::DefWasmImport => {
let mut ctx = PreprocessContext::new(scope_defs, scope_types, file_ns, check_warnings, call_stack);
Ok(preprocess_defn(name, name_ns, &args, &mut ctx)?)
}
CalcitSyntax::CoreLet => {
let mut ctx = PreprocessContext::new(scope_defs, scope_types, file_ns, check_warnings, call_stack);
Ok(preprocess_core_let(name, name_ns, &args, &mut ctx)?)
}
CalcitSyntax::If => {
let mut ctx = PreprocessContext::new(scope_defs, scope_types, file_ns, check_warnings, call_stack);
Ok(preprocess_if(name, name_ns, &args, &mut ctx)?)
}
CalcitSyntax::Try
| CalcitSyntax::Macroexpand
| CalcitSyntax::MacroexpandAll
| CalcitSyntax::Macroexpand1
| CalcitSyntax::Gensym
| CalcitSyntax::Reset => {
let mut ctx = PreprocessContext::new(scope_defs, scope_types, file_ns, check_warnings, call_stack);
Ok(preprocess_each_items(name, name_ns, &args, &mut ctx)?)
}
CalcitSyntax::Quote | CalcitSyntax::Eval => Ok(preprocess_quote(name, name_ns, &args, scope_defs, file_ns)?),
CalcitSyntax::HintFn => {
let mut ctx = PreprocessContext::new(scope_defs, scope_types, file_ns, check_warnings, call_stack);
preprocess_hint_fn(name, name_ns, &args, &mut ctx)
}
CalcitSyntax::Defatom => {
let mut ctx = PreprocessContext::new(scope_defs, scope_types, file_ns, check_warnings, call_stack);
Ok(preprocess_defatom(name, name_ns, &args, &mut ctx)?)
}
CalcitSyntax::CallSpread => {
let mut ys = vec![head_form];
args.traverse_result::<CalcitErr>(&mut |a| {
let form = preprocess_expr(a, scope_defs, scope_types, file_ns, check_warnings, call_stack)?;
ys.push(form);
Ok(())
})?;
Ok(Calcit::from(ys))
}
CalcitSyntax::AssertType => {
let mut ctx = PreprocessContext::new(scope_defs, scope_types, file_ns, check_warnings, call_stack);
preprocess_assert_type(name, name_ns, &args, &mut ctx)
}
CalcitSyntax::UnsafeCoerce => {
let mut ctx = PreprocessContext::new(scope_defs, scope_types, file_ns, check_warnings, call_stack);
preprocess_unsafe_coerce(name, name_ns, &args, &mut ctx)
}
CalcitSyntax::ParseCirruEdnAs => {
let mut ctx = PreprocessContext::new(scope_defs, scope_types, file_ns, check_warnings, call_stack);
preprocess_parse_cirru_edn_as(name, name_ns, &args, &mut ctx)
}
CalcitSyntax::AssertTraits => {
let mut ctx = PreprocessContext::new(scope_defs, scope_types, file_ns, check_warnings, call_stack);
preprocess_assert_traits(name, name_ns, &args, &mut ctx)
}
CalcitSyntax::Match => {
let mut ctx = PreprocessContext::new(scope_defs, scope_types, file_ns, check_warnings, call_stack);
preprocess_match(name, name_ns, &args, &mut ctx)
}
CalcitSyntax::ArgSpread => CalcitErr::err_nodes(CalcitErrKind::Syntax, "`&` cannot be preprocessed as operator", &xs.to_vec()),
CalcitSyntax::ArgOptional => {
CalcitErr::err_nodes(CalcitErrKind::Syntax, "`?` cannot be preprocessed as operator", &xs.to_vec())
}
CalcitSyntax::MacroInterpolate => {
CalcitErr::err_nodes(CalcitErrKind::Syntax, "`~` cannot be preprocessed as operator", &xs.to_vec())
}
CalcitSyntax::MacroInterpolateSpread => {
CalcitErr::err_nodes(CalcitErrKind::Syntax, "`~@` cannot be preprocessed as operator", &xs.to_vec())
}
},
Calcit::Thunk(..) => Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Unexpected,
format!("does not know how to preprocess a thunk: {head}"),
call_stack,
head.get_location(),
)),
Calcit::Proc(CalcitProc::WithTypeSlot) => {
preprocess_with_type_slot_block(&head_form, &args, scope_defs, scope_types, file_ns, check_warnings, call_stack)
}
Calcit::Method(_, _)
| Calcit::Proc(..)
| Calcit::Local { .. }
| Calcit::Import { .. }
| Calcit::Registered { .. }
| Calcit::List(..)
| Calcit::RawCode(..)
| Calcit::Symbol { .. }
| Calcit::StructDef(..)
| Calcit::EnumDef(..) => {
if !matches!(args.first(), Some(Calcit::Method(..))) {
check_callable_type(&head_form, scope_types, file_ns, &def_name, check_warnings);
}
let mut ys = CalcitList::new_inner_from(std::slice::from_ref(&head_form));
let mut has_spread = false;
let struct_hint = if matches!(&head_form, Calcit::Proc(CalcitProc::NativeMap)) {
EXPECTED_STRUCT_TYPE.with(|cell| cell.borrow().clone())
} else {
None
};
if let Some(ref struct_def) = struct_hint {
let items: Vec<&Calcit> = args.iter().collect();
for (i, item) in items.iter().enumerate() {
if let Calcit::Syntax(CalcitSyntax::ArgSpread, _) = item {
has_spread = true;
ys = ys.push((*item).to_owned());
continue;
}
if i % 2 == 1
&& let Some(Calcit::Tag(key_tag)) = items.get(i - 1)
&& let Some(field_idx) = struct_def.fields.iter().position(|f| f == key_tag)
&& let Some(field_type) = struct_def.field_types.get(field_idx)
&& let Some(fn_annot) = field_type.resolve_to_fn()
{
EXPECTED_FN_TYPE.with(|cell| cell.borrow_mut().replace(fn_annot));
}
let result = preprocess_expr(item, scope_defs, scope_types, file_ns, check_warnings, call_stack);
if i % 2 == 1 {
EXPECTED_FN_TYPE.with(|cell| *cell.borrow_mut() = None);
}
let form = result?;
ys = ys.push(form);
}
} else {
args.traverse_result::<CalcitErr>(&mut |a| {
if let Calcit::Syntax(CalcitSyntax::ArgSpread, _) = a {
has_spread = true;
ys = ys.push(a.to_owned());
return Ok(());
}
let form = preprocess_expr(a, scope_defs, scope_types, file_ns, check_warnings, call_stack)?;
ys = ys.push(form);
Ok(())
})?;
}
let processed_args = CalcitList::from(ys.drop_left()); validate_method_call(&head_form, &processed_args, scope_types, call_stack)?;
check_struct_field_access(&head_form, &processed_args, scope_types, file_ns, check_warnings);
check_struct_update_fields(&head_form, &processed_args, scope_types, file_ns, &def_name, check_warnings);
check_struct_method_args(&head_form, &processed_args, scope_types, file_ns, &def_name, check_warnings);
check_typed_js_field_operation(&head_form, &processed_args, scope_types, file_ns, &def_name, check_warnings);
if let Some(rewritten) = rewrite_typed_js_field_operation(&head_form, &processed_args, scope_types) {
return Ok(rewritten);
}
if matches!(&head_form, Calcit::Proc(CalcitProc::NativeStructGet))
&& processed_args.len() == 2
&& let (Some(struct_arg), Some(Calcit::Tag(field_tag))) = (processed_args.first(), processed_args.get(1))
&& let Some(type_info) = resolve_type_value(struct_arg, scope_types)
&& let Some(struct_def) = type_info.as_ref().resolve_to_struct()
&& let Some(idx) = struct_def.index_of(field_tag.ref_str())
{
ys = CalcitList::new_inner_from(&[
Calcit::Proc(CalcitProc::NativeStructNth),
struct_arg.to_owned(),
Calcit::Number(idx as f64),
Calcit::Tag(field_tag.to_owned()),
]);
}
if matches!(&head_form, Calcit::Proc(CalcitProc::NativeStructAssoc))
&& processed_args.len() == 3
&& let (Some(struct_arg), Some(Calcit::Tag(field_tag)), Some(value_arg)) =
(processed_args.first(), processed_args.get(1), processed_args.get(2))
&& let Some(type_info) = resolve_type_value(struct_arg, scope_types)
&& let Some(struct_def) = type_info.as_ref().resolve_to_struct()
&& let Some(idx) = struct_def.index_of(field_tag.ref_str())
{
ys = CalcitList::new_inner_from(&[
Calcit::Proc(CalcitProc::NativeStructAssocAt),
struct_arg.to_owned(),
Calcit::Number(idx as f64),
Calcit::Tag(field_tag.to_owned()),
value_arg.to_owned(),
]);
}
if matches!(&head_form, Calcit::Proc(CalcitProc::NativeStructWith))
&& processed_args.len() >= 3
&& (processed_args.len() - 1) % 2 == 0
&& let Some(struct_arg) = processed_args.first()
&& let Some(type_info) = resolve_type_value(struct_arg, scope_types)
&& let Some(struct_def) = type_info.as_ref().resolve_to_struct()
{
let pair_count = (processed_args.len() - 1) / 2;
let mut all_resolved = true;
let mut new_args: Vec<Calcit> = Vec::with_capacity(1 + pair_count * 3);
new_args.push(struct_arg.to_owned());
for i in 0..pair_count {
let k_idx = 1 + i * 2;
let v_idx = k_idx + 1;
if let Some(Calcit::Tag(field_tag)) = processed_args.get(k_idx) {
if let Some(idx) = struct_def.index_of(field_tag.ref_str()) {
new_args.push(Calcit::Number(idx as f64));
new_args.push(Calcit::Tag(field_tag.to_owned()));
if let Some(val) = processed_args.get(v_idx) {
new_args.push(val.to_owned());
} else {
all_resolved = false;
break;
}
} else {
all_resolved = false;
break;
}
} else {
all_resolved = false;
break;
}
}
if all_resolved {
let mut items: Vec<Calcit> = Vec::with_capacity(1 + new_args.len());
items.push(Calcit::Proc(CalcitProc::NativeStructWithAt));
items.extend(new_args);
ys = CalcitList::new_inner_from(&items);
}
}
if let Calcit::Method(method_name, calcit::MethodKind::Invoke(_)) = &head_form
&& let Some(receiver) = processed_args.first()
&& let Some(type_value) = resolve_type_value(receiver, scope_types)
{
let is_external = trait_list_from_type(type_value.as_ref())
.is_some_and(|traits| traits.iter().any(|trait_def| trait_is_external_object(trait_def.as_ref())));
let method_kind = if is_external {
calcit::MethodKind::ExternalInvoke(type_value)
} else {
calcit::MethodKind::Invoke(type_value)
};
let typed_method = Calcit::Method(method_name.clone(), method_kind);
ys = CalcitList::new_inner_from(&[typed_method]);
for item in processed_args.iter() {
ys = ys.push(item.to_owned());
}
}
if let Some(call_head) = ys.first() {
let processed_args = CalcitList::from(ys.drop_left());
warn_on_nullable_js_ffi_dereference(call_head, &processed_args, scope_types, file_ns, def_name.as_ref(), check_warnings);
warn_on_untyped_js_ffi_field_access(call_head, &processed_args, scope_types, file_ns, def_name.as_ref(), check_warnings);
warn_on_nominal_enum_legacy_absence_use(head, &processed_args, scope_types, file_ns, def_name.as_ref(), check_warnings);
warn_on_legacy_js_nullish_predicate(head, &processed_args, scope_types, file_ns, def_name.as_ref(), check_warnings);
warn_on_dynamic_trait_call(call_head, &processed_args, scope_types, file_ns, def_name.as_ref(), check_warnings);
warn_on_method_name_conflict(call_head, &processed_args, scope_types, file_ns, def_name.as_ref(), check_warnings);
}
if let Some(Calcit::Proc(proc)) = ys.first() {
let processed_args = CalcitList::from(ys.drop_left());
check_proc_arg_types(
proc,
&processed_args,
scope_types,
file_ns,
&def_name,
call_location.clone(),
check_warnings,
);
if let Some(specialized) =
try_specialize_polymorphic_call(calcit::CORE_NS, proc.as_ref(), &processed_args, scope_types, file_ns)
{
return Ok(specialized);
}
}
if let Some(Calcit::Local(local)) = ys.first() {
let local_sym = local.sym.clone();
let local_type_info = local.type_info.clone();
let local_type = if matches!(*local_type_info, CalcitTypeAnnotation::Dynamic) {
scope_types.get(&local_sym).cloned()
} else {
Some(local_type_info)
};
if let Some(ref ty) = local_type
&& let CalcitTypeAnnotation::Fn(fn_annot) = ty.as_ref()
&& let Some(rewritten) =
try_rewrite_local_fn_enum_args_to_named_enums(fn_annot, &local_sym, &processed_args, file_ns, &def_name, check_warnings)
{
ys = CalcitList::new_inner_from(&[ys.first().unwrap().to_owned()]);
for item in rewritten.iter() {
ys = ys.push(item.to_owned());
}
}
if let Some(Calcit::Local(local)) = ys.first() {
let updated_args = CalcitList::from(ys.drop_left());
check_local_fn_call_arg_types(&head_form, local, &updated_args, scope_types, &call_info, check_warnings);
}
}
if let Some(Calcit::Proc(CalcitProc::DeftypeSlot)) = ys.first()
&& let Some(slot_name) = processed_args.first().and_then(|arg| match arg {
Calcit::Tag(tag) => Some(Arc::from(tag.ref_str())),
Calcit::Str(text) => Some(Arc::from(text.as_ref())),
_ => None,
})
{
register_type_slot(slot_name)
.map_err(|msg| CalcitErr::use_msg_stack_location(CalcitErrKind::Unexpected, msg, call_stack, head.get_location()))?;
}
if let Some(Calcit::Proc(CalcitProc::NativeInspectType)) = ys.first() {
if let Some(first_arg) = processed_args.first() {
let sym_name = match first_arg {
Calcit::Symbol { sym, .. } => Some(sym.as_ref()),
Calcit::Local(local) => Some(local.sym.as_ref()),
_ => None,
};
let type_info = if let Some(name) = sym_name {
scope_types.get(name).cloned().unwrap_or_else(|| calcit::DYNAMIC_TYPE.clone())
} else {
infer_type_from_expr(first_arg, scope_types).unwrap_or_else(|| calcit::DYNAMIC_TYPE.clone())
};
let loc = head.get_location().or_else(|| first_arg.get_location());
if let Some(l) = loc {
let coord_repr = format!("@{}", l.coord.iter().map(|c| c.to_string()).collect::<Vec<_>>().join("."));
eprintln!(
"[&inspect-type] in {}/{} [{}]\n {} => {}",
l.ns,
l.def,
coord_repr,
first_arg,
type_info.describe()
);
} else {
eprintln!(
"[&inspect-type] in {}/{}\n {} => {}",
file_ns,
def_name,
first_arg,
type_info.describe()
);
}
if let Some(Calcit::Tag(tag)) = processed_args.get(1)
&& tag.ref_str().trim_start_matches(':') == "fail-on-dynamic"
&& matches!(*type_info, CalcitTypeAnnotation::Dynamic)
{
let msg = format!("&inspect-type failed to infer type for {first_arg}");
if let Some(loc) = head.get_location() {
gen_check_warning_with_location(msg, loc, check_warnings);
} else {
gen_check_warning(msg, file_ns, check_warnings);
}
}
}
return Ok(Calcit::Nil);
}
if !has_spread
&& let Some(call_head) = ys.first()
&& let Some(optimized_call) = try_inline_method_call(call_head, &processed_args, scope_types, file_ns)
{
return Ok(optimized_call);
}
if has_spread {
ys = ys.prepend(Calcit::Syntax(CalcitSyntax::CallSpread, file_ns.into()));
Ok(Calcit::from(CalcitList::List(ys)))
} else {
Ok(Calcit::from(CalcitList::List(ys)))
}
}
h => {
let loc = h.get_location();
Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Unexpected,
format!("unknown head `{h}` in {xs}"),
call_stack,
loc,
))
}
},
}
}
fn try_rewrite_struct_enum_constructor_head_call(
head_form: &Calcit,
args: &CalcitList,
scope_types: &ScopeTypes,
file_ns: &str,
def_name: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
_call_stack: &CallStackList,
) -> Result<Option<Calcit>, CalcitErr> {
let constructor_kind = match head_form {
Calcit::StructDef(_) => Some("defstruct"),
Calcit::EnumDef(_) => Some("defenum"),
Calcit::Import(CalcitImport { ns, def, .. }) => data_definition_kind(ns, def),
Calcit::Symbol { sym, info, .. } => data_definition_kind(&info.at_ns, sym),
_ => None,
};
let Some(type_info) = resolve_type_value(head_form, scope_types) else {
return Ok(None);
};
let resolved_struct_definition = match type_info.as_ref() {
CalcitTypeAnnotation::StructDef(struct_def) => Some((struct_def.as_ref().clone(), None)),
other => other.resolve_to_struct_with_ref(),
};
if constructor_kind == Some("defstruct")
&& let Some((struct_def, ns_def_path)) = resolved_struct_definition
{
if matches!(args.first(), Some(Calcit::Method(..))) {
return Ok(None);
}
if !args.len().is_multiple_of(2) {
gen_check_warning(
format!(
"[Warn] struct constructor rewrite skipped: `{}` has {} positional argument(s), expected key/value pairs, at {}/{}",
brief_type_of_value(head_form),
args.len(),
file_ns,
def_name,
),
file_ns,
check_warnings,
);
return Ok(None);
}
let mut provided_fields: std::collections::HashMap<EdnTag, &Calcit> = std::collections::HashMap::new();
let args_items = args.to_vec();
for chunk in args_items.chunks(2) {
if let [Calcit::Tag(key), value] = chunk {
if !struct_def.fields.iter().any(|f| f == key) {
gen_check_warning(
format!(
"[Warn] struct constructor rewrite skipped for `{}`: key `:{}` is not a field of struct `{}` at {}/{}",
brief_type_of_value(head_form),
key,
struct_def.name,
file_ns,
def_name,
),
file_ns,
check_warnings,
);
return Ok(None);
}
if provided_fields.insert(key.to_owned(), value).is_some() {
gen_check_warning(
format!(
"[Warn] struct constructor rewrite skipped for `{}`: duplicate field `:{}` at {}/{}",
struct_def.name, key, file_ns, def_name,
),
file_ns,
check_warnings,
);
return Ok(None);
}
} else {
gen_check_warning(
format!(
"[Warn] struct constructor rewrite skipped for `{}`: all arguments must be tag/value pairs at {}/{}",
brief_type_of_value(head_form),
file_ns,
def_name,
),
file_ns,
check_warnings,
);
return Ok(None);
}
}
for (idx, field) in struct_def.fields.iter().enumerate() {
if !provided_fields.contains_key(field)
&& !matches!(
struct_def.field_types.get(idx).map(AsRef::as_ref),
Some(CalcitTypeAnnotation::Optional(_))
)
{
gen_check_warning(
format!(
"[Warn] struct constructor rewrite skipped for `{}`: required field `:{}` is missing at {}/{}",
struct_def.name, field, file_ns, def_name,
),
file_ns,
check_warnings,
);
return Ok(None);
}
}
for (field, value) in &provided_fields {
let Some(field_idx) = struct_def.fields.iter().position(|candidate| candidate == field) else {
continue;
};
let Some(expected_type) = struct_def.field_types.get(field_idx) else {
continue;
};
if matches!(expected_type.as_ref(), CalcitTypeAnnotation::Dynamic) {
continue;
}
if let Some(actual_type) = resolve_type_value(value, scope_types)
&& !actual_type.as_ref().matches_annotation(expected_type.as_ref())
{
gen_check_warning(
format!(
"[Warn] struct `{}` field `:{}` expects type `{}`, but got `{}` at {}/{}",
struct_def.name,
field,
expected_type.to_brief_string(),
actual_type.to_brief_string(),
file_ns,
def_name,
),
file_ns,
check_warnings,
);
}
}
let struct_ref_node = build_struct_ref_node(&struct_def, ns_def_path, file_ns, def_name);
let mut struct_items: Vec<Calcit> = Vec::with_capacity(struct_def.fields.len() * 2 + 2);
struct_items.push(Calcit::Proc(CalcitProc::NativeStruct));
struct_items.push(struct_ref_node);
for field in struct_def.fields.iter() {
struct_items.push(Calcit::Tag(field.to_owned()));
if let Some(value) = provided_fields.get(field) {
struct_items.push((*value).to_owned());
} else {
struct_items.push(Calcit::Nil);
}
}
return Ok(Some(Calcit::from(struct_items)));
}
let resolved_enum_definition = match type_info.as_ref() {
CalcitTypeAnnotation::EnumDef(enum_def) => Some((enum_def.as_ref().clone(), None)),
other => other.resolve_to_enum_with_ref(),
};
if constructor_kind == Some("defenum")
&& let Some((enum_def, ns_def_path)) = resolved_enum_definition
{
let Some(first_arg) = args.first() else {
gen_check_warning(
format!(
"[Warn] enum constructor rewrite skipped: `{}` is missing variant tag at {}/{}",
brief_type_of_value(head_form),
file_ns,
def_name
),
file_ns,
check_warnings,
);
return Ok(None);
};
let Calcit::Tag(tag) = first_arg else {
gen_check_warning(
format!(
"[Warn] enum constructor rewrite skipped for `{}`: first argument should be a variant tag, at {}/{}",
brief_type_of_value(head_form),
file_ns,
def_name,
),
file_ns,
check_warnings,
);
return Ok(None);
};
if enum_def.find_variant(tag).is_none() {
let variants: Vec<&str> = enum_def.variants().iter().map(|v| v.tag.ref_str()).collect();
gen_check_warning(
format!(
"[Warn] enum `{}` does not have variant `:{}`. Available: [{}], at {}/{}",
enum_def.name(),
tag.ref_str(),
variants.join(", "),
file_ns,
def_name,
),
file_ns,
check_warnings,
);
return Ok(None);
}
let enum_ref_node = build_enum_ref_node(enum_def, ns_def_path, file_ns, def_name);
let mut items: Vec<Calcit> = Vec::with_capacity(args.len() + 1);
items.push(Calcit::Proc(CalcitProc::NativeNamedEnumNew));
items.push(enum_ref_node);
items.extend(args.to_vec());
return Ok(Some(Calcit::from(items)));
}
Ok(None)
}
fn data_definition_kind(ns: &str, def: &str) -> Option<&'static str> {
let Calcit::List(code) = program::lookup_def_code(ns, def)? else {
return None;
};
match code.first() {
Some(Calcit::Symbol { sym, .. }) if sym.as_ref() == "defstruct" => Some("defstruct"),
Some(Calcit::Symbol { sym, .. }) if sym.as_ref() == "defenum" => Some("defenum"),
_ => None,
}
}
fn check_fn_marked_args(
defined_args: &[CalcitArgLabel],
params: &CalcitList,
file_ns: &str,
f_name: &str,
def_name: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
) {
let mut i = 0;
let mut j = 0;
let mut optional = false;
loop {
let d = defined_args.get(i);
let r = params.get(j);
match (d, r) {
(None, None) => return,
(_, Some(Calcit::Symbol { sym, .. })) if &**sym == "&" => {
return;
}
(Some(CalcitArgLabel::RestMark), _) => {
return;
}
(Some(CalcitArgLabel::OptionalMark), _) => {
optional = true;
i += 1;
continue;
}
(Some(_), None) => {
if optional {
i += 1;
j += 1;
continue;
} else {
gen_check_warning(
format!("[Warn] lack of args in {f_name} `{defined_args:?}` with `{params}`, at {file_ns}/{def_name}"),
file_ns,
check_warnings,
);
return;
}
}
(None, Some(_)) => {
gen_check_warning(
format!("[Warn] too many args for {f_name} `{defined_args:?}` with `{params}`, at {file_ns}/{def_name}"),
file_ns,
check_warnings,
);
return;
}
(Some(_), Some(_)) => {
i += 1;
j += 1;
continue;
}
}
}
}
fn check_fn_args(
defined_args: &[u16],
params: &CalcitList,
file_ns: &str,
f_name: &str,
def_name: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
) {
let expected_size = defined_args.len();
let actual_size = params.len();
for (idx, item) in params.iter().enumerate() {
if let Calcit::Syntax(CalcitSyntax::ArgSpread, _) = item {
if expected_size < (idx + 1) {
let args = CalcitLocal::display_args(defined_args);
gen_check_warning(
format!("[Warn] expected {expected_size} args in {f_name} `{args}`, got spreading form `{params}`, at {file_ns}/{def_name}"),
file_ns,
check_warnings,
);
}
return; }
}
if expected_size != actual_size {
gen_check_warning(
format!("[Warn] expected {expected_size} args in {f_name} `{defined_args:?}` with `{params}`, at {file_ns}/{def_name}"),
file_ns,
check_warnings,
);
}
}
fn grab_def_name(x: &Calcit) -> Arc<str> {
match x {
Calcit::Symbol { info, .. } | Calcit::Local(CalcitLocal { info, .. }) => info.at_def.to_owned(),
_ => String::from("??").into(),
}
}
pub(crate) fn gen_check_warning(message: String, file_ns: &str, check_warnings: &RefCell<Vec<LocatedWarning>>) {
let loc = NodeLocation::new(Arc::from(file_ns), Arc::from(GENERATED_DEF), Arc::from(vec![]));
gen_check_warning_with_location(message, loc, check_warnings);
}
pub(crate) fn gen_check_warning_code(
message: String,
code: &'static str,
file_ns: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
) {
let loc = NodeLocation::new(Arc::from(file_ns), Arc::from(GENERATED_DEF), Arc::from(vec![]));
gen_check_warning_with_location_code(message, code, loc, check_warnings);
}
pub(crate) fn gen_check_warning_code_at(
message: String,
code: &'static str,
file_ns: &str,
location: Option<NodeLocation>,
check_warnings: &RefCell<Vec<LocatedWarning>>,
) {
if let Some(location) = location {
gen_check_warning_with_location_code(message, code, location, check_warnings);
} else {
gen_check_warning_code(message, code, file_ns, check_warnings);
}
}
fn gen_check_warning_with_location(message: String, location: NodeLocation, check_warnings: &RefCell<Vec<LocatedWarning>>) {
let mut warnings = check_warnings.borrow_mut();
warnings.push(LocatedWarning::new(message, location));
}
fn gen_check_warning_with_location_code(
message: String,
code: &'static str,
location: NodeLocation,
check_warnings: &RefCell<Vec<LocatedWarning>>,
) {
let mut warnings = check_warnings.borrow_mut();
warnings.push(LocatedWarning::new_with_detail(message, location, Some(code.to_string()), None));
}
fn derive_call_expr_location(head: &Calcit) -> Option<NodeLocation> {
let location = head.get_location()?;
let mut parent_coord = (*location.coord).clone();
parent_coord.pop();
Some(NodeLocation::new(
location.ns.clone(),
location.def.clone(),
Arc::from(parent_coord),
))
}
fn check_recur_arity_in_expr(
expr: &Calcit,
expected_arity: usize,
file_ns: &str,
def_name: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
) {
match expr {
Calcit::Recur(args) => {
let actual_arity = args.len();
if actual_arity != expected_arity {
let location = expr
.get_location()
.unwrap_or_else(|| NodeLocation::new(Arc::from(file_ns), Arc::from(def_name), Arc::new(vec![])));
gen_check_warning_with_location(
format!("[Warn] recur expects {expected_arity} args but got {actual_arity} in {file_ns}/{def_name}"),
location,
check_warnings,
);
}
for arg in args {
check_recur_arity_in_expr(arg, expected_arity, file_ns, def_name, check_warnings);
}
}
Calcit::List(xs) => {
if xs.is_empty() {
return;
}
if let Some(Calcit::Syntax(s, _)) = xs.first()
&& (s == &CalcitSyntax::Quote || s == &CalcitSyntax::Quasiquote)
{
return;
}
if let Some(Calcit::Proc(CalcitProc::Recur)) = xs.first() {
let actual_arity = xs.len() - 1; if actual_arity != expected_arity {
let location = expr
.get_location()
.unwrap_or_else(|| NodeLocation::new(Arc::from(file_ns), Arc::from(def_name), Arc::new(vec![])));
gen_check_warning_with_location(
format!("[Warn] recur expects {expected_arity} args but got {actual_arity} in {file_ns}/{def_name}"),
location,
check_warnings,
);
}
} else if let Some(Calcit::Syntax(s, _)) = xs.first()
&& (s == &CalcitSyntax::Defn
|| s == &CalcitSyntax::Defmacro
|| s == &CalcitSyntax::DefWasmExport
|| s == &CalcitSyntax::DefWasmImport)
{
return;
}
for item in xs.iter() {
check_recur_arity_in_expr(item, expected_arity, file_ns, def_name, check_warnings);
}
}
Calcit::Fn { info, .. } => {
let nested_arity = match &*info.args {
CalcitFnArgs::Args(args) => args.len(),
CalcitFnArgs::MarkedArgs(args) => {
args
.iter()
.filter(|a| !matches!(a, CalcitArgLabel::RestMark | CalcitArgLabel::OptionalMark))
.count()
}
};
for body_expr in &info.body {
check_recur_arity_in_expr(body_expr, nested_arity, file_ns, def_name, check_warnings);
}
}
_ => {
}
}
}
fn check_impl_traits_top_level_in_expr(expr: &Calcit, file_ns: &str, def_name: &str, check_warnings: &RefCell<Vec<LocatedWarning>>) {
if !warn_dyn_method_enabled() {
return;
}
match expr {
Calcit::List(xs) => {
if xs.is_empty() {
return;
}
if let Some(Calcit::Syntax(s, _)) = xs.first()
&& (s == &CalcitSyntax::Quote || s == &CalcitSyntax::Quasiquote)
{
return;
}
let is_impl_traits = matches!(
xs.first(),
Some(Calcit::Import(CalcitImport { ns, def, .. })) if ns.as_ref() == calcit::CORE_NS && def.as_ref() == "impl-traits"
) || matches!(xs.first(), Some(Calcit::Symbol { sym, .. }) if sym.as_ref() == "impl-traits");
if is_impl_traits {
let msg = format!(
"[Warn] `impl-traits` inside {file_ns}/{def_name} may block preprocess specialization; prefer top-level `def` bindings"
);
if let Some(loc) = expr.get_location() {
gen_check_warning_with_location(msg, loc.clone(), check_warnings);
} else {
gen_check_warning(msg, file_ns, check_warnings);
}
}
for item in xs.iter() {
check_impl_traits_top_level_in_expr(item, file_ns, def_name, check_warnings);
}
}
Calcit::Fn { info, .. } => {
for body_expr in &info.body {
check_impl_traits_top_level_in_expr(body_expr, file_ns, def_name, check_warnings);
}
}
_ => {}
}
}
fn check_struct_field_access(
head: &Calcit,
args: &CalcitList,
scope_types: &ScopeTypes,
file_ns: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
) {
if let Calcit::Proc(CalcitProc::NativeStructGet) = head {
if args.len() >= 2
&& let (Some(struct_arg), Some(field_arg)) = (args.first(), args.get(1))
{
check_field_in_struct(struct_arg, field_arg, scope_types, file_ns, check_warnings);
}
}
else if let Calcit::Import(CalcitImport { ns, def, .. }) = head {
if &**ns == calcit::CORE_NS
&& (&**def == "get" || &**def == "record-get" || &**def == "&struct:get")
&& args.len() >= 2
&& let (Some(struct_arg), Some(field_arg)) = (args.first(), args.get(1))
{
check_field_in_struct(struct_arg, field_arg, scope_types, file_ns, check_warnings);
}
}
else if let Calcit::Method(field_name, calcit::MethodKind::Access) = head {
if let Some(struct_arg) = args.first() {
let field_tag = Calcit::Tag(cirru_edn::EdnTag::from(&**field_name));
check_field_in_struct(struct_arg, &field_tag, scope_types, file_ns, check_warnings);
}
}
}
fn check_struct_update_fields(
head: &Calcit,
args: &CalcitList,
scope_types: &ScopeTypes,
file_ns: &str,
def_name: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
) {
let pairs: Vec<(&Calcit, &Calcit)> = match head {
Calcit::Proc(CalcitProc::NativeStructAssoc) if args.len() == 3 => match (args.get(1), args.get(2)) {
(Some(field), Some(value)) => vec![(field, value)],
_ => return,
},
Calcit::Proc(CalcitProc::NativeStructAssocAt) if args.len() == 4 => match (args.get(2), args.get(3)) {
(Some(field), Some(value)) => vec![(field, value)],
_ => return,
},
Calcit::Proc(CalcitProc::NativeStructWith) if args.len() >= 3 && (args.len() - 1).is_multiple_of(2) => {
let items = args.iter().skip(1).collect::<Vec<_>>();
items.chunks_exact(2).map(|pair| (pair[0], pair[1])).collect()
}
Calcit::Proc(CalcitProc::NativeStructWithAt) if args.len() >= 4 && (args.len() - 1).is_multiple_of(3) => {
let items = args.iter().skip(1).collect::<Vec<_>>();
items.chunks_exact(3).map(|triple| (triple[1], triple[2])).collect()
}
_ => return,
};
let Some(struct_arg) = args.first() else { return };
for (field_arg, value_arg) in pairs {
check_field_in_struct(struct_arg, field_arg, scope_types, file_ns, check_warnings);
let field_name = match field_arg {
Calcit::Tag(tag) => tag.ref_str(),
Calcit::Str(name) => name.as_ref(),
Calcit::Symbol { sym, .. } => sym.as_ref(),
_ => continue,
};
let Some(expected_type) = infer_struct_field_type(struct_arg, field_name, scope_types) else {
continue;
};
if matches!(expected_type.as_ref(), CalcitTypeAnnotation::Dynamic) {
continue;
}
if let Some(actual_type) = resolve_type_value(value_arg, scope_types)
&& !actual_type.as_ref().matches_annotation(expected_type.as_ref())
{
gen_check_warning(
format!(
"[Warn] struct update field `:{field_name}` expects type `{}`, but got `{}` at {file_ns}/{def_name}",
expected_type.to_brief_string(),
actual_type.to_brief_string(),
),
file_ns,
check_warnings,
);
}
}
}
fn check_field_in_struct(
struct_arg: &Calcit,
field_arg: &Calcit,
scope_types: &ScopeTypes,
file_ns: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
) {
let Some(type_info) = resolve_type_value(struct_arg, scope_types) else {
return; };
let Some(struct_def) = type_info.as_ref().resolve_to_struct() else {
return; };
let field_name = match field_arg {
Calcit::Tag(tag) => tag.ref_str(),
Calcit::Str(s) => s.as_ref(),
Calcit::Symbol { sym, .. } => sym.as_ref(),
_ => return, };
if struct_def.index_of(field_name).is_some() {
return; }
let available_fields: Vec<&str> = struct_def.fields.iter().map(|f| f.ref_str()).collect();
gen_check_warning(
format!(
"[Warn] Field `:{field_name}` does not exist in struct `{}`. Available fields: [{}]. Struct field access is required and never returns nil/Option for a missing field; use a declared field instead",
struct_def.name,
available_fields
.iter()
.map(|field| format!(":{field}"))
.collect::<Vec<_>>()
.join(", ")
),
file_ns,
check_warnings,
);
}
pub(crate) fn check_enum_nth_bounds(
_args: &CalcitList,
_scope_types: &ScopeTypes,
_file_ns: &str,
_def_name: &str,
_check_warnings: &RefCell<Vec<LocatedWarning>>,
) {
}
pub(crate) fn check_enum_construction(
args: &CalcitList,
scope_types: &ScopeTypes,
file_ns: &str,
def_name: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
) {
if args.len() < 2 {
return; }
let enum_arg = match args.first() {
Some(arg) => arg,
None => return,
};
let tag_arg = match args.get(1) {
Some(arg) => arg,
None => return,
};
let Some(enum_proto) = resolve_enum_value(enum_arg, scope_types) else {
return; };
let tag_name = match tag_arg {
Calcit::Tag(tag) => tag.ref_str(),
Calcit::Symbol { sym, .. } => sym.as_ref(),
_ => return, };
let Some(variant) = enum_proto.find_variant_by_name(tag_name) else {
let available_variants: Vec<&str> = enum_proto.variants().iter().map(|v| v.tag.ref_str()).collect();
gen_check_warning(
format!(
"[Warn] Enum `{}` does not have variant `:{tag_name}`. Available variants: [{}], at {file_ns}/{def_name}",
enum_proto.name(),
available_variants.join(", ")
),
file_ns,
check_warnings,
);
return;
};
let expected_arity = variant.arity();
let actual_arity = args.len().saturating_sub(2);
if expected_arity != actual_arity {
gen_check_warning(
format!(
"[Warn] Enum `{}::{}` expects {} payload(s), but got {}, at {file_ns}/{def_name}",
enum_proto.name(),
tag_name,
expected_arity,
actual_arity
),
file_ns,
check_warnings,
);
return;
}
for (idx, (payload_arg, expected_type)) in args.iter().skip(2).zip(variant.payload_types().iter()).enumerate() {
if matches!(expected_type.as_ref(), CalcitTypeAnnotation::Dynamic) {
continue; }
if let Some(actual_type) = resolve_type_value(payload_arg, scope_types)
&& !actual_type.as_ref().matches_annotation(expected_type.as_ref())
{
let expected_str = expected_type.as_ref().to_brief_string();
let actual_str = actual_type.as_ref().to_brief_string();
gen_check_warning(
format!(
"[Warn] Enum `{}::{}` payload {} expects type `{expected_str}`, but got `{actual_str}`, at {file_ns}/{def_name}",
enum_proto.name(),
tag_name,
idx + 1
),
file_ns,
check_warnings,
);
}
}
}
fn check_struct_method_args(
head: &Calcit,
args: &CalcitList,
scope_types: &ScopeTypes,
file_ns: &str,
def_name: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
) {
let Calcit::Method(method_name, calcit::MethodKind::Invoke(_) | calcit::MethodKind::ExternalInvoke(_)) = head else {
return;
};
let Some(receiver) = args.first() else {
return;
};
let Some(type_value) = resolve_type_value(receiver, scope_types) else {
return; };
if let Some(traits) = trait_list_from_type(type_value.as_ref())
&& let Some((trait_def, method_type)) = find_trait_method_type(&traits, method_name.as_ref())
{
let Some(signature) = method_type.as_function() else {
return;
};
let Ok(method_args) = args.skip(1) else {
return;
};
let expected_count = signature.arg_types.len();
let actual_with_receiver = method_args.len() + 1;
if expected_count != 0 && expected_count != actual_with_receiver {
gen_check_warning(
format!(
"[Warn] Method `.{method_name}` expects {expected_count} args (including receiver), got {actual_with_receiver} in call at {file_ns}/{def_name}"
),
file_ns,
check_warnings,
);
return;
}
let mut bindings: HashMap<Arc<str>, Arc<CalcitTypeAnnotation>> = HashMap::new();
if let Some(expected_receiver) = signature.arg_types.first()
&& let Some(receiver_type) = resolve_type_value(receiver, scope_types)
{
receiver_type
.as_ref()
.matches_with_bindings(expected_receiver.as_ref(), &mut bindings);
}
let arg_types_without_receiver = signature.arg_types.iter().skip(1);
for (idx, (arg, expected_type)) in method_args.iter().zip(arg_types_without_receiver).enumerate() {
if matches!(**expected_type, CalcitTypeAnnotation::Dynamic) {
continue;
}
if let Some(actual_type) = resolve_type_value(arg, scope_types)
&& !actual_type.as_ref().matches_with_bindings(expected_type.as_ref(), &mut bindings)
{
let expected_str = expected_type.substitute_type_vars(&bindings).to_brief_string();
let actual_str = actual_type.as_ref().to_brief_string();
gen_check_warning(
format!(
"[Warn] Method `.{method_name}` arg {} expects type `{expected_str}`, but got `{actual_str}` in call at {file_ns}/{def_name} (trait {})",
idx + 2,
trait_def.name
),
file_ns,
check_warnings,
);
}
}
return;
}
let Some(impl_values) = get_impls_from_type(&type_value) else {
return; };
let method_str = method_name.as_ref();
let Some(method_entry) = find_method_entry_for_type(type_value.as_ref(), &impl_values, method_str) else {
return; };
let declared_schema = match method_entry {
Calcit::Import(import) => Some((
program::lookup_def_schema(&import.ns, &import.def),
format!("{} / {}", import.ns, import.def),
)),
Calcit::Fn { info, .. } if info.def_ref.is_some() => Some((
program::lookup_def_schema(&info.def_ns, &info.name),
format!("{} / {}", info.def_ns, info.name),
)),
_ => None,
};
if let Some((schema, implementation_name)) = declared_schema
&& schema.contains_type_var()
&& type_value.as_ref().resolve_to_enum().is_some()
{
let Some(signature) = schema.as_function() else {
return;
};
let Ok(method_args) = args.skip(1) else {
return;
};
let expected_count = signature.arg_types.len();
let actual_with_receiver = method_args.len() + 1;
if expected_count != 0 && expected_count != actual_with_receiver {
gen_check_warning(
format!(
"[Warn] Method `.{method_name}` expects {expected_count} args (including receiver), got {actual_with_receiver} in call at {file_ns}/{def_name}"
),
file_ns,
check_warnings,
);
return;
}
let mut bindings: HashMap<Arc<str>, Arc<CalcitTypeAnnotation>> = HashMap::new();
if let Some(expected_receiver) = signature.arg_types.first() {
type_value.as_ref().matches_with_bindings(expected_receiver.as_ref(), &mut bindings);
}
for (idx, (arg, expected_type)) in method_args.iter().zip(signature.arg_types.iter().skip(1)).enumerate() {
if matches!(**expected_type, CalcitTypeAnnotation::Dynamic) {
continue;
}
if let Some(actual_type) = resolve_type_value(arg, scope_types)
&& !actual_type.as_ref().matches_with_bindings(expected_type.as_ref(), &mut bindings)
{
let expected_str = expected_type.substitute_type_vars(&bindings).to_brief_string();
let actual_str = actual_type.as_ref().to_brief_string();
gen_check_warning_code(
format!(
"[Warn] Method `.{method_name}` arg {} expects type `{expected_str}`, but got `{actual_str}` in call at {file_ns}/{def_name} (implementation {implementation_name})",
idx + 2,
),
"W_METHOD_ARG_TYPE_MISMATCH",
file_ns,
check_warnings,
);
}
}
return;
}
let fn_info: Option<&CalcitFn> = match method_entry {
Calcit::Fn { info, .. } => Some(info.as_ref()),
Calcit::Proc(_proc) => {
return;
}
_ => None,
};
let Some(fn_info) = fn_info else {
return; };
let Ok(method_args) = args.skip(1) else {
return;
};
let expected_count = fn_info.args.as_ref().param_len();
let actual_count = method_args.len();
let actual_with_receiver = actual_count + 1;
let has_variadic = match fn_info.args.as_ref() {
CalcitFnArgs::MarkedArgs(xs) => xs.iter().any(|label| matches!(label, CalcitArgLabel::RestMark)),
CalcitFnArgs::Args(_) => false,
};
if !has_variadic && expected_count != actual_with_receiver {
gen_check_warning(
format!(
"[Warn] Method `.{method_name}` expects {expected_count} args (including receiver), got {actual_with_receiver} in call at {file_ns}/{def_name}"
),
file_ns,
check_warnings,
);
return;
}
let mut bindings: HashMap<Arc<str>, Arc<CalcitTypeAnnotation>> = HashMap::new();
let arg_types_without_receiver: Vec<Arc<CalcitTypeAnnotation>> = fn_info.arg_types.iter().skip(1).cloned().collect();
for (idx, (arg, expected_type)) in method_args.iter().zip(arg_types_without_receiver.iter()).enumerate() {
if matches!(**expected_type, CalcitTypeAnnotation::Dynamic) {
continue; }
if let CalcitTypeAnnotation::Variadic(inner_type) = expected_type.as_ref() {
for (rest_idx, rest_arg) in method_args.iter().skip(idx).enumerate() {
if let Some(actual_type) = resolve_type_value(rest_arg, scope_types)
&& !actual_type.as_ref().matches_with_bindings(inner_type.as_ref(), &mut bindings)
{
let expected_str = inner_type.as_ref().to_brief_string();
let actual_str = actual_type.as_ref().to_brief_string();
gen_check_warning(
format!(
"[Warn] Method `.{method_name}` variadic arg {} expects type `{expected_str}`, but got `{actual_str}` in call at {file_ns}/{def_name}",
idx + rest_idx + 2
),
file_ns,
check_warnings,
);
}
}
return;
}
if let Some(actual_type) = resolve_type_value(arg, scope_types) {
if !actual_type.as_ref().matches_with_bindings(expected_type.as_ref(), &mut bindings) {
let expected_str = expected_type.as_ref().to_brief_string();
let actual_str = actual_type.as_ref().to_brief_string();
gen_check_warning(
format!(
"[Warn] Method `.{method_name}` arg {} expects type `{expected_str}`, but got `{actual_str}` in call at {file_ns}/{def_name}",
idx + 2 ),
file_ns,
check_warnings,
);
}
}
}
}
fn warn_on_dynamic_trait_call(
head: &Calcit,
args: &CalcitList,
scope_types: &ScopeTypes,
file_ns: &str,
def_name: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
) {
if file_ns == calcit::CORE_NS {
return;
}
if !warn_dyn_method_enabled() {
return;
}
let Calcit::Method(method_name, calcit::MethodKind::Invoke(_) | calcit::MethodKind::ExternalInvoke(_)) = head else {
return;
};
let Some(receiver) = args.first() else {
return;
};
let receiver_type = resolve_type_value(receiver, scope_types);
let warn = match receiver_type.as_ref().map(|value| value.as_ref()) {
None => true,
Some(ann) if is_trait_annotation(ann) => false,
Some(ann) => is_dynamic_annotation(ann),
};
if !warn {
return;
}
let message = format!(
"[Warn] dynamic trait call `.{method_name}` cannot be monomorphized in {file_ns}/{def_name}; add assert-traits, or use unsafe-coerce only at a trusted FFI boundary"
);
if let Some(loc) = head.get_location().or_else(|| receiver.get_location()) {
gen_check_warning_with_location(message, loc, check_warnings);
} else {
gen_check_warning(message, file_ns, check_warnings);
}
}
fn warn_on_nullable_js_ffi_dereference(
head: &Calcit,
args: &CalcitList,
scope_types: &ScopeTypes,
file_ns: &str,
def_name: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
) {
if file_ns == calcit::CORE_NS {
return;
}
let is_raw_dereference = matches!(
head,
Calcit::Method(_, calcit::MethodKind::Access | calcit::MethodKind::InvokeNative)
) || matches!(head, Calcit::Symbol { sym, .. } if matches!(sym.as_ref(), "aget" | "js-get"));
if !is_raw_dereference {
return;
}
let Some(receiver) = args.first() else {
return;
};
let Some(receiver_type) = resolve_type_value(receiver, scope_types) else {
return;
};
let CalcitTypeAnnotation::JsNullish(inner) = receiver_type.as_ref() else {
return;
};
if !matches!(inner.as_ref(), CalcitTypeAnnotation::JsObject) {
return;
}
let operation = match head {
Calcit::Method(name, calcit::MethodKind::Access) => format!(".-{name}"),
Calcit::Method(name, calcit::MethodKind::InvokeNative) => format!(".!{name}"),
Calcit::Method(name, _) => format!(".{name}"),
Calcit::Symbol { sym, .. } => sym.to_string(),
_ => "JS FFI access".to_owned(),
};
let message = format!(
"[Warn] JsNullish FFI value is dereferenced by `{operation}` in {file_ns}/{def_name}; use optional access, narrow with `js-present?`/`js-nullish?`, then validate or explicitly `unsafe-coerce` the opaque JsObject value"
);
if let Some(location) = head.get_location().or_else(|| receiver.get_location()) {
gen_check_warning_with_location_code(message, "W_JS_FFI_NULLABLE_DEREF", location, check_warnings);
} else {
gen_check_warning_code(message, "W_JS_FFI_NULLABLE_DEREF", file_ns, check_warnings);
}
}
fn static_js_field_name(form: &Calcit) -> Option<&str> {
match form {
Calcit::Tag(tag) => Some(tag.ref_str()),
Calcit::Str(name) => Some(name.as_ref()),
_ => None,
}
}
fn rewrite_typed_js_field_operation(head: &Calcit, args: &CalcitList, scope_types: &ScopeTypes) -> Option<Calcit> {
let operation = match head {
Calcit::Symbol { sym, .. } if sym.as_ref() == "js-get" => "get",
Calcit::Symbol { sym, .. } if sym.as_ref() == "js-set" => "set",
_ => return None,
};
let receiver = args.first()?;
let field_name = static_js_field_name(args.get(1)?)?;
let receiver_type = resolve_type_value(receiver, scope_types)?;
let traits = trait_list_from_type(receiver_type.as_ref())?;
let (trait_def, _) = find_trait_field_type(&traits, field_name)?;
if !trait_is_external_object(trait_def) {
return None;
}
let kind = if operation == "get" {
calcit::MethodKind::ExternalGet(receiver_type)
} else {
calcit::MethodKind::ExternalSet(receiver_type)
};
let mut rewritten = vec![Calcit::Method(Arc::from(field_name), kind), receiver.clone()];
if operation == "set" {
rewritten.push(args.get(2)?.clone());
}
Some(Calcit::from(rewritten))
}
fn check_typed_js_field_operation(
head: &Calcit,
args: &CalcitList,
scope_types: &ScopeTypes,
file_ns: &str,
def_name: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
) {
let operation = match head {
Calcit::Symbol { sym, .. } if sym.as_ref() == "js-get" => "js-get",
Calcit::Symbol { sym, .. } if sym.as_ref() == "js-set" => "js-set",
_ => return,
};
let (Some(receiver), Some(key)) = (args.first(), args.get(1)) else {
return;
};
let Some(field_name) = static_js_field_name(key) else {
return;
};
let Some(receiver_type) = resolve_type_value(receiver, scope_types) else {
return;
};
let Some(traits) = trait_list_from_type(receiver_type.as_ref()) else {
return;
};
let external_traits = traits
.iter()
.filter(|trait_def| trait_is_external_object(trait_def.as_ref()))
.cloned()
.collect::<Vec<_>>();
if external_traits.is_empty() {
return;
}
let Some((_, field_type)) = find_trait_field_type(&external_traits, field_name) else {
let message = format!(
"[Warn] `{operation}` cannot access undeclared external-object field `:{field_name}` in {file_ns}/{def_name}; declare the field on the external trait, use a dynamic key, or use raw `aget`/`aset`"
);
gen_check_warning_code_at(
message,
"W_JS_FFI_UNKNOWN_FIELD",
file_ns,
key.get_location().or_else(|| head.get_location()),
check_warnings,
);
return;
};
if operation == "js-set"
&& let Some(value) = args.get(2)
&& let Some(value_type) = resolve_type_value(value, scope_types)
&& !value_type.as_ref().matches_annotation(field_type.as_ref())
{
let message = format!(
"[Warn] `js-set` field `:{field_name}` expects {}, got {} in {file_ns}/{def_name}",
field_type.to_brief_string(),
value_type.to_brief_string()
);
gen_check_warning_code_at(
message,
"W_JS_FFI_FIELD_TYPE_MISMATCH",
file_ns,
value.get_location().or_else(|| head.get_location()),
check_warnings,
);
}
}
fn warn_on_untyped_js_ffi_field_access(
head: &Calcit,
args: &CalcitList,
scope_types: &ScopeTypes,
file_ns: &str,
def_name: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
) {
if file_ns == calcit::CORE_NS {
return;
}
if !warn_dyn_method_enabled() {
return;
}
let (operation, field_name) = match head {
Calcit::Method(name, calcit::MethodKind::Access) => (format!(".-{name}"), name.to_string()),
Calcit::Method(name, calcit::MethodKind::InvokeNative) => (format!(".!{name}"), name.to_string()),
Calcit::Symbol { sym, .. } if matches!(sym.as_ref(), "aget" | "js-get" | "aset" | "js-set") => {
let Some(field_name) = args.get(1).and_then(static_js_field_name) else {
return;
};
(sym.to_string(), field_name.to_owned())
}
_ => return,
};
let Some(receiver) = args.first() else {
return;
};
let Some(receiver_type) = resolve_type_value(receiver, scope_types) else {
return;
};
if !matches!(receiver_type.as_ref(), CalcitTypeAnnotation::JsObject) {
return;
}
let message = format!(
"[Warn] `{operation}` accesses untyped JS field `:{field_name}` in {file_ns}/{def_name}; still permitted, but declaring an external-object trait (`deftrait ... :ffi {{:kind :external-object ...}}`) for the receiver unlocks static field/method checking and `:names` mapping"
);
gen_check_warning_code_at(
message,
"W_JS_FFI_UNTYPED_ACCESS",
file_ns,
head.get_location().or_else(|| receiver.get_location()),
check_warnings,
);
}
fn warn_on_legacy_js_nullish_predicate(
head: &Calcit,
args: &CalcitList,
scope_types: &ScopeTypes,
file_ns: &str,
def_name: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
) {
if file_ns == calcit::CORE_NS {
return;
}
let Some(operation) = canonical_absence_operation_name(head) else {
return;
};
if !matches!(operation, "nil?" | "some?") {
return;
}
let Some(value) = args.first() else {
return;
};
let Some(value_type) = resolve_type_value(value, scope_types) else {
return;
};
if !matches!(value_type.as_ref(), CalcitTypeAnnotation::JsNullish(_)) {
return;
}
let message = format!(
"[Warn] `{operation}` consumes a JsNullish FFI value in {file_ns}/{def_name}; use `js-nullish?` or `js-present?` so host nullability stays explicit"
);
if let Some(location) = head.get_location().or_else(|| value.get_location()) {
gen_check_warning_with_location_code(message, "W_JS_FFI_NULLABLE_PREDICATE", location, check_warnings);
} else {
gen_check_warning_code(message, "W_JS_FFI_NULLABLE_PREDICATE", file_ns, check_warnings);
}
}
fn canonical_absence_operation_name(head: &Calcit) -> Option<&str> {
match head {
Calcit::Import(CalcitImport { ns, def, .. }) if ns.as_ref() == calcit::CORE_NS => Some(def.as_ref()),
Calcit::Proc(proc) => Some(proc.as_ref()),
Calcit::Method(name, _) => Some(name.as_ref()),
_ => None,
}
}
fn nominal_enum_type_name(annotation: &CalcitTypeAnnotation) -> Option<String> {
if let CalcitTypeAnnotation::TypeRef(name, _) = annotation {
match name.as_ref() {
"calcit.core/Option" => return Some("Option".to_owned()),
"calcit.core/Result" => return Some("Result".to_owned()),
_ => {}
}
}
None
}
fn warn_on_nominal_enum_legacy_absence_use(
head: &Calcit,
args: &CalcitList,
scope_types: &ScopeTypes,
file_ns: &str,
def_name: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
) {
if file_ns == calcit::CORE_NS {
return;
}
let Some(operation) = canonical_absence_operation_name(head) else {
return;
};
if !matches!(
operation,
"nil?"
| "some?"
| "list?"
| "map?"
| "set?"
| "tag?"
| "number?"
| "string?"
| "keyword?"
| "symbol?"
| "fn?"
| "bool?"
| "buffer?"
| "cirru-quote?"
| "ref?"
| "macro?"
| "syntax?"
| "enum?"
| "struct?"
| "get"
| "nth"
| "first"
| "last"
| "count"
| "empty?"
| "contains?"
| "includes?"
| "="
| "&="
| "&compare"
) {
return;
}
let nominal_args = args
.iter()
.filter_map(|value| {
let value_type = resolve_type_value(value, scope_types)?;
let enum_name = nominal_enum_type_name(value_type.as_ref())?;
Some((value, enum_name))
})
.collect::<Vec<_>>();
let Some((value, enum_name)) = nominal_args.first() else {
return;
};
if matches!(operation, "=" | "&=") && nominal_args.len() == args.len() && nominal_args.iter().all(|(_, current)| current == enum_name)
{
return;
}
let guidance = match operation {
"nil?" | "some?" if enum_name == "Option" => {
"use `option:none?`/`option:some?` (or the corresponding methods) instead of nullable-value predicates"
}
"nil?" | "some?" => "use `tag-match` to inspect the nominal enum variant",
"=" | "&=" if enum_name == "Option" => {
"compare Option values only with other Options, or unwrap/pattern-match before comparing a payload"
}
"=" | "&=" => "compare values of the same nominal enum, or pattern-match before comparing a payload",
"&compare" if enum_name == "Option" => "unwrap or pattern-match the Option before comparing its payload",
"list?" | "map?" | "set?" | "struct?" | "enum?" | "struct-def?" | "enum-def?" | "tag?" | "number?" | "string?" | "keyword?"
| "symbol?" | "fn?" | "bool?" | "buffer?" | "cirru-quote?" | "ref?" | "macro?" | "syntax?" => {
"pattern-match the nominal enum before applying a payload type predicate"
}
_ if enum_name == "Option" => "use `tag-match`, `option:unwrap-or`, or an Option method to access the payload",
_ => "use `tag-match` instead of positional access on the nominal enum",
};
let message = format!(
"[Warn] `{operation}` consumes nominal enum `{enum_name}` value `{value}` in {file_ns}/{def_name}; this often indicates a nullable-returning API migrated to a nominal type; {guidance}"
);
if let Some(location) = head.get_location().or_else(|| value.get_location()) {
gen_check_warning_with_location_code(message, "W_NOMINAL_ENUM_LEGACY_USE", location, check_warnings);
} else {
gen_check_warning_code(message, "W_NOMINAL_ENUM_LEGACY_USE", file_ns, check_warnings);
}
}
fn warn_on_nominal_enum_truthiness(
value: &Calcit,
scope_types: &ScopeTypes,
file_ns: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
) {
if file_ns == calcit::CORE_NS {
return;
}
let Some(value_type) = resolve_type_value(value, scope_types) else {
return;
};
let Some(enum_name) = nominal_enum_type_name(value_type.as_ref()) else {
return;
};
let guidance = if enum_name == "Option" {
"use `option:some?`, `option:none?`, or `tag-match`; `%none` is a truthy nominal value"
} else {
"use `tag-match` to select a nominal enum variant explicitly"
};
let message = format!(
"[Warn] nominal enum `{enum_name}` is used directly as an `if` condition in {file_ns}; this can silently select the truthy branch; {guidance}"
);
if let Some(location) = value.get_location() {
gen_check_warning_with_location_code(message, "W_NOMINAL_ENUM_LEGACY_USE", location, check_warnings);
} else {
gen_check_warning_code(message, "W_NOMINAL_ENUM_LEGACY_USE", file_ns, check_warnings);
}
}
fn warn_on_trait_impl_method_tag_syntax(
macro_info: &crate::calcit::CalcitMacro,
args: &CalcitList,
file_ns: &str,
def_name: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
) {
if file_ns == calcit::CORE_NS {
return;
}
if macro_info.def_ns.as_ref() != calcit::CORE_NS {
return;
}
let trait_name = if macro_info.name.as_ref() == "deftrait" {
args.first().and_then(parse_trait_name_from_source).map(|name| name.to_string())
} else {
None
};
let source_name = trait_name.as_deref().unwrap_or(def_name);
if macro_name_is_external_object(macro_info, file_ns, source_name) {
return;
}
let (macro_name, pair_start_idx) = match macro_info.name.as_ref() {
"deftrait" => ("deftrait", 1),
"defimpl" => ("defimpl", 2),
_ => return,
};
for entry in args.iter().skip(pair_start_idx) {
let Calcit::List(pair) = entry else {
continue;
};
let Some(Calcit::Tag(method_name)) = pair.first() else {
continue;
};
let message = format!(
"[Warn] `{macro_name}` method key `:{method_name}` in {file_ns}/{source_name} uses legacy tag style; prefer dot method key `.{method_name}` for migration (`:{method_name}` remains compatible)"
);
if let Some(loc) = entry.get_location() {
gen_check_warning_with_location(message, loc, check_warnings);
} else {
gen_check_warning(message, file_ns, check_warnings);
}
}
}
fn macro_name_is_external_object(macro_info: &crate::calcit::CalcitMacro, file_ns: &str, def_name: &str) -> bool {
if macro_info.name.as_ref() != "deftrait" {
return false;
}
let metadata_external = program::lookup_def_ffi(file_ns, def_name).is_some_and(|ffi| match ffi {
cirru_edn::Edn::Struct(value) => value
.pairs
.iter()
.find(|(key, _)| key.ref_str() == "kind")
.is_some_and(|(_, value)| matches!(value, cirru_edn::Edn::Tag(tag) if tag.ref_str() == "external-object")),
cirru_edn::Edn::Map(value) => value
.get(&cirru_edn::Edn::Tag(EdnTag::new("kind")))
.is_some_and(|value| matches!(value, cirru_edn::Edn::Tag(tag) if tag.ref_str() == "external-object")),
_ => false,
});
metadata_external
|| program::lookup_def_code(file_ns, def_name)
.and_then(|code| resolve_trait_def_from_source_code(&code))
.is_some_and(|trait_def| trait_def.member_kinds.contains(&CalcitTraitMemberKind::Field))
}
fn extract_hint_fn_legacy_clause_name(form: &Calcit) -> Option<&str> {
match form {
Calcit::Symbol { sym, .. } => match sym.as_ref() {
"return-type" => Some("return-type"),
"generics" => Some("generics"),
"type-vars" => Some("type-vars"),
_ => None,
},
Calcit::Import(CalcitImport { def, .. }) => match def.as_ref() {
"return-type" => Some("return-type"),
"generics" => Some("generics"),
"type-vars" => Some("type-vars"),
_ => None,
},
_ => None,
}
}
fn warn_on_method_name_conflict(
head: &Calcit,
args: &CalcitList,
scope_types: &ScopeTypes,
file_ns: &str,
def_name: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
) {
if file_ns == calcit::CORE_NS {
return;
}
if !warn_dyn_method_enabled() {
return;
}
let Calcit::Method(method_name, calcit::MethodKind::Invoke(_) | calcit::MethodKind::ExternalInvoke(_)) = head else {
return;
};
let Some(receiver) = args.first() else {
return;
};
let Some(type_value) = resolve_type_value(receiver, scope_types) else {
return;
};
let Some(impl_values) = get_impls_from_type(type_value.as_ref()) else {
return;
};
if impl_values.len() < 2 {
return;
}
let last_wins = core_impl_list_symbol_from_type_annotation(type_value.as_ref()).is_none();
let matched_impls: Vec<&Arc<CalcitImpl>> = if last_wins {
impl_values
.iter()
.rev()
.filter(|imp| imp.get(method_name.as_ref()).is_some() && imp.origin().is_some())
.collect()
} else {
impl_values
.iter()
.filter(|imp| imp.get(method_name.as_ref()).is_some() && imp.origin().is_some())
.collect()
};
if matched_impls.len() < 2 {
return;
}
let mut trait_names: Vec<String> = vec![];
let mut seen = HashSet::new();
for imp in &matched_impls {
if let Some(origin) = imp.origin() {
let trait_name = origin.name.to_string();
if seen.insert(trait_name.clone()) {
trait_names.push(trait_name);
}
}
}
if trait_names.len() < 2 {
return;
}
let selected_trait = matched_impls
.first()
.and_then(|imp| imp.origin())
.map(|origin| origin.name.to_string())
.unwrap_or_else(|| "<unknown>".to_string());
let message = format!(
"[Warn] method `.{}` has multiple trait candidates ({}) in {}/{}; current dispatch picks `{}` by precedence, use `&trait-call` to disambiguate",
method_name,
trait_names.join(", "),
file_ns,
def_name,
selected_trait,
);
if let Some(loc) = head.get_location().or_else(|| receiver.get_location()) {
gen_check_warning_with_location(message, loc, check_warnings);
} else {
gen_check_warning(message, file_ns, check_warnings);
}
}
fn try_specialize_polymorphic_call(
fn_ns: &str,
fn_def: &str,
processed_args: &CalcitList,
scope_types: &ScopeTypes,
file_ns: &str,
) -> Option<Calcit> {
use CalcitProc::*;
use CalcitTypeAnnotation as T;
if fn_ns != calcit::CORE_NS {
return None;
}
if matches!(
fn_def,
"&list:map" | "&map:map" | "&set:map" | "&list:filter" | "&map:filter" | "&set:filter"
) {
return None;
}
let receiver = processed_args.first()?;
let receiver_type = resolve_type_value(receiver, scope_types)?;
let predicate_true = matches!(
(fn_def, receiver_type.as_ref()),
("list?", T::List(_))
| ("map?", T::Map(_, _))
| ("set?", T::Set(_))
| ("string?", T::String)
| ("number?", T::Number)
| ("bool?", T::Bool)
| ("tag?", T::Tag)
| ("fn?", T::Fn(_) | T::DynFn)
| ("enum?", T::EnumValue(_) | T::AnonymousEnum | T::Enum(_, _))
| ("struct?", T::StructValue(_) | T::Struct(_, _))
| ("struct-def?", T::StructDef(_))
| ("enum-def?", T::EnumDef(_))
);
if predicate_true {
return Some(Calcit::Bool(true));
}
let core_def_name: Option<&'static str> = match (fn_def, receiver_type.as_ref()) {
("map", T::List(_)) => Some("&list:map"),
("map", T::Map(_, _)) => Some("&map:map"),
("filter", T::List(_)) => Some("&list:filter"),
("filter", T::Map(_, _)) => Some("&map:filter"),
("filter", T::Set(_)) => Some("&set:filter"),
_ => None,
};
if let Some(def_name) = core_def_name {
let head = Calcit::Import(CalcitImport {
ns: calcit::CORE_NS.into(),
def: def_name.into(),
info: Arc::new(ImportInfo::Core { at_ns: Arc::from(file_ns) }),
def_id: Some(program::ensure_def_id(calcit::CORE_NS, def_name).0),
});
let mut items: Vec<Calcit> = Vec::with_capacity(processed_args.len() + 1);
items.push(head);
for arg in processed_args.iter() {
items.push(arg.to_owned());
}
return Some(Calcit::from(items));
}
let proc = match (fn_def, receiver_type.as_ref()) {
("count", T::List(_)) => NativeListCount,
("count", T::Map(_, _)) => NativeMapCount,
("count", T::Set(_)) => NativeSetCount,
("count", T::String) => NativeStrCount,
("count", T::EnumValue(_) | T::AnonymousEnum) => NativeEnumCount,
("count", T::StructValue(_)) => NativeStructCount,
("empty?", T::List(_)) => NativeListEmpty,
("empty?", T::Map(_, _)) => NativeMapEmpty,
("empty?", T::Set(_)) => NativeSetEmpty,
("empty?", T::String) => NativeStrEmpty,
("contains?", T::List(_)) => NativeListContains,
("contains?", T::Map(_, _)) => NativeMapContains,
("contains?", T::Set(_)) => NativeSetIncludes,
("contains?", T::String) => NativeStrContains,
("contains?", T::StructValue(_)) => NativeStructContains,
("rest", T::List(_)) => NativeListRest,
("assoc", T::List(_)) => NativeListAssoc,
("assoc", T::Map(_, _)) => NativeMapAssoc,
("assoc", T::EnumValue(_) | T::AnonymousEnum) => NativeEnumAssoc,
("assoc", T::StructValue(_)) => NativeStructAssoc,
("includes?", T::List(_)) => NativeListIncludes,
("includes?", T::Map(_, _)) => NativeMapIncludes,
("includes?", T::Set(_)) => NativeSetIncludes,
("includes?", T::String) => NativeStrIncludes,
("reverse", T::List(_)) => NativeListReverse,
_ => return None,
};
let mut items: Vec<Calcit> = Vec::with_capacity(processed_args.len() + 1);
items.push(Calcit::Proc(proc));
for arg in processed_args.iter() {
items.push(arg.to_owned());
}
Some(Calcit::from(items))
}
fn try_inline_method_call(head: &Calcit, args: &CalcitList, scope_types: &ScopeTypes, file_ns: &str) -> Option<Calcit> {
match head {
Calcit::Method(method_name, calcit::MethodKind::Invoke(type_value)) => {
let mut resolved_type = type_value.clone();
if matches!(**type_value, CalcitTypeAnnotation::Dynamic)
&& let Some(receiver) = args.first()
&& let Some(inferred) = resolve_type_value(receiver, scope_types)
&& !matches!(inferred.as_ref(), CalcitTypeAnnotation::Dynamic)
{
resolved_type = inferred;
}
if matches!(resolved_type.as_ref(), CalcitTypeAnnotation::Dynamic) {
return None;
}
let type_ref = resolved_type.as_ref();
let impl_values = get_impls_from_type(type_ref)?;
let (_impl_index, _impl_value, method_entry) = find_method_entry_with_impl(type_ref, &impl_values, method_name.as_ref())?;
if let Some(callable_head) = pick_callable_from_method_entry(method_entry, file_ns) {
return Some(build_inlined_call(callable_head, args));
}
None
}
_ => None,
}
}
fn pick_callable_from_method_entry(entry: &Calcit, _file_ns: &str) -> Option<Calcit> {
match entry {
Calcit::Import(..) | Calcit::Proc(..) | Calcit::Registered(..) | Calcit::Symbol { .. } => Some(entry.to_owned()),
Calcit::Fn { info, .. }
if info
.def_ref
.as_ref()
.is_some_and(|def_ref| !def_ref.is_macro_gen && program::has_def_code(def_ref.def_ns.as_ref(), def_ref.def_name.as_ref())) =>
{
Some(entry.to_owned())
}
_ => None,
}
}
fn build_inlined_call(callable_head: Calcit, args: &CalcitList) -> Calcit {
let mut call_nodes: Vec<Calcit> = Vec::with_capacity(args.len() + 1);
call_nodes.push(callable_head);
for item in args.iter() {
call_nodes.push(item.to_owned());
}
Calcit::from(call_nodes)
}
fn find_method_entry_with_impl<'a>(
type_ref: &CalcitTypeAnnotation,
impls: &'a [Arc<CalcitImpl>],
name: &str,
) -> Option<(usize, &'a Arc<CalcitImpl>, &'a Calcit)> {
let last_wins = core_impl_list_symbol_from_type_annotation(type_ref).is_none();
if last_wins {
for (idx, imp) in impls.iter().enumerate().rev() {
if let Some(entry) = imp.get(name) {
return Some((idx, imp, entry));
}
}
} else {
for (idx, imp) in impls.iter().enumerate() {
if let Some(entry) = imp.get(name) {
return Some((idx, imp, entry));
}
}
}
None
}
fn validate_method_call(
head: &Calcit,
args: &CalcitList,
scope_types: &ScopeTypes,
call_stack: &CallStackList,
) -> Result<(), CalcitErr> {
let Calcit::Method(method_name, calcit::MethodKind::Invoke(_)) = head else {
return Ok(());
};
let Some(receiver) = args.first() else {
return Ok(());
};
let Some(type_value) = resolve_type_value(receiver, scope_types) else {
return Ok(()); };
if let Some(traits) = trait_list_from_type(type_value.as_ref()) {
let method_str = method_name.as_ref();
if traits.iter().rev().any(|trait_def| {
trait_def
.methods
.iter()
.zip(trait_def.member_kinds.iter())
.any(|(method, kind)| *kind == CalcitTraitMemberKind::Method && method.ref_str() == method_str)
}) {
return Ok(());
}
let methods_list = collect_trait_method_names(&traits).join(" ");
let type_desc = describe_type(type_value.as_ref());
return Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Type,
format!("unknown method `.{method_name}` for {type_desc}. Available methods: {methods_list}"),
call_stack,
head.get_location(),
));
}
let Some(impl_values) = get_impls_from_type(&type_value) else {
return Ok(()); };
let method_str = method_name.as_ref();
if impl_values
.iter()
.any(|struct_def| struct_def.fields().iter().any(|field| field.ref_str() == method_str))
{
return Ok(()); }
let mut methods = vec![];
for struct_def in impl_values.iter() {
for field in struct_def.fields().iter() {
methods.push(field.to_string());
}
}
let methods_list = methods.join(" ");
let type_desc = describe_type(type_value.as_ref());
Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Type,
format!("unknown method `.{method_name}` for {type_desc}. Available methods: {methods_list}"),
call_stack,
head.get_location(),
))
}
fn is_callable_type(type_ann: &CalcitTypeAnnotation) -> bool {
match type_ann {
CalcitTypeAnnotation::Fn(_) => true,
CalcitTypeAnnotation::DynFn => true,
CalcitTypeAnnotation::Optional(inner) => is_callable_type(inner.as_ref()),
CalcitTypeAnnotation::Dynamic => true,
_ => false,
}
}
fn check_callable_type(
expr: &Calcit,
scope_types: &ScopeTypes,
file_ns: &str,
def_name: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
) {
match expr {
Calcit::Fn { .. }
| Calcit::Proc(..)
| Calcit::Import { .. }
| Calcit::Registered { .. }
| Calcit::Method(_, _)
| Calcit::RawCode(..)
| Calcit::Symbol { .. } => (),
Calcit::List(_) => {
if let Some(type_ann) = infer_type_from_expr(expr, scope_types)
&& !is_callable_type(&type_ann)
{
let type_desc = describe_type(&type_ann);
gen_check_warning(
format!("[Warn] trying to call a non-function value of type {type_desc}. Expression: `{expr}`, at {file_ns}/{def_name}"),
file_ns,
check_warnings,
);
}
}
Calcit::Local(local) => {
let type_ann = if matches!(*local.type_info, CalcitTypeAnnotation::Dynamic) {
scope_types.get(&local.sym).map(|t| t.as_ref()).unwrap_or(&*local.type_info)
} else {
&*local.type_info
};
if !is_callable_type(type_ann) {
let type_desc = describe_type(type_ann);
gen_check_warning(
format!(
"[Warn] trying to call variable `{}` of non-function type {type_desc}, at {file_ns}/{def_name}",
local.sym
),
file_ns,
check_warnings,
);
}
}
_ => {
if let Some(type_ann) = infer_type_from_expr(expr, scope_types)
&& !is_callable_type(&type_ann)
{
let type_desc = describe_type(&type_ann);
gen_check_warning(
format!("[Warn] trying to call a non-function value of type {type_desc}. Expression: `{expr}`, at {file_ns}/{def_name}"),
file_ns,
check_warnings,
);
}
}
}
}
fn collect_impls_from_value(value: &Calcit) -> Option<Vec<Arc<CalcitImpl>>> {
let resolve_impl = |value: &Calcit| -> Option<CalcitImpl> {
match value {
Calcit::Impl(imp) => Some(imp.to_owned()),
Calcit::Import(import) => match resolve_program_value_for_preprocess(&import.ns, &import.def, import.def_id) {
Some(Calcit::Impl(imp)) => Some(imp),
_ => None,
},
Calcit::Symbol { sym, info, .. } => match resolve_program_value_for_preprocess(&info.at_ns, sym, None) {
Some(Calcit::Impl(imp)) => Some(imp),
_ => None,
},
_ => None,
}
};
match value {
Calcit::Impl(_) | Calcit::Import(_) | Calcit::Symbol { .. } => resolve_impl(value).map(|imp| vec![Arc::new(imp)]),
Calcit::List(list) => {
let mut impls: Vec<Arc<CalcitImpl>> = Vec::with_capacity(list.len());
for item in list.iter() {
let imp = resolve_impl(item)?;
impls.push(Arc::new(imp));
}
Some(impls)
}
_ => None,
}
}
fn get_impls_from_type(type_value: &CalcitTypeAnnotation) -> Option<Vec<Arc<CalcitImpl>>> {
if let Some(struct_def) = type_value.resolve_to_struct() {
let mut impls = resolve_core_impls("&core-struct-impls").unwrap_or_default();
impls.extend(struct_def.impls.iter().cloned());
return Some(impls);
}
if let CalcitTypeAnnotation::Struct(struct_def, _) = type_value {
return Some(struct_def.impls.to_owned());
}
if let Some(enum_def) = type_value.resolve_to_enum() {
let mut impls = resolve_core_impls("&core-enum-impls").unwrap_or_default();
impls.extend(enum_def.impls.iter().cloned());
return Some(impls);
}
if let CalcitTypeAnnotation::AnonymousEnum = type_value {
if let Some(core_impls) = resolve_core_impls("&core-enum-impls") {
return Some(core_impls);
}
}
if let Some(class_symbol) = core_impl_list_symbol_from_type_annotation(type_value) {
return match resolve_program_value_for_preprocess(calcit::CORE_NS, class_symbol, None) {
Some(value) => collect_impls_from_value(&value),
None => None,
};
}
if let CalcitTypeAnnotation::Custom(value) = type_value {
match value.as_ref() {
Calcit::Import(import) => {
return match resolve_program_value_for_preprocess(&import.ns, &import.def, import.def_id) {
Some(value) => collect_impls_from_value(&value),
None => None,
};
}
Calcit::Symbol { sym, info, .. } => {
let (target_ns, target_def) = match runner::parse_ns_def(sym) {
Some((ns_part, def_part)) => (ns_part, def_part),
None => (info.at_ns.to_owned(), sym.to_owned()),
};
return match resolve_program_value_for_preprocess(&target_ns, &target_def, None) {
Some(value) => collect_impls_from_value(&value),
None => None,
};
}
_ => {}
}
}
None
}
fn resolve_core_impls(symbol: &str) -> Option<Vec<Arc<CalcitImpl>>> {
resolve_program_value_for_preprocess(calcit::CORE_NS, symbol, None).and_then(|v| collect_impls_from_value(&v))
}
fn trait_list_from_type(type_value: &CalcitTypeAnnotation) -> Option<Vec<Arc<CalcitTrait>>> {
match type_value {
CalcitTypeAnnotation::Trait(trait_def) => Some(vec![trait_def.to_owned()]),
CalcitTypeAnnotation::TraitSet(traits) => Some(traits.as_ref().to_owned()),
CalcitTypeAnnotation::Optional(inner) => trait_list_from_type(inner.as_ref()),
_ => None,
}
}
pub(crate) fn trait_is_external_object(trait_def: &CalcitTrait) -> bool {
let Some(def_ref) = trait_def.definition_ref.as_deref() else {
return false;
};
let Some((ns, def)) = def_ref.rsplit_once('/') else { return false };
let Some(ffi) = program::lookup_def_ffi(ns, def) else {
return false;
};
match ffi {
cirru_edn::Edn::Struct(value) => value
.pairs
.iter()
.find(|(key, _)| key.ref_str() == "kind")
.is_some_and(|(_, value)| matches!(value, cirru_edn::Edn::Tag(tag) if tag.ref_str() == "external-object")),
cirru_edn::Edn::Map(value) => value
.get(&cirru_edn::Edn::Tag(EdnTag::new("kind")))
.is_some_and(|value| matches!(value, cirru_edn::Edn::Tag(tag) if tag.ref_str() == "external-object")),
_ => false,
}
}
fn is_trait_annotation(type_value: &CalcitTypeAnnotation) -> bool {
matches!(type_value, CalcitTypeAnnotation::Trait(_) | CalcitTypeAnnotation::TraitSet(_))
|| matches!(type_value, CalcitTypeAnnotation::Optional(inner) if is_trait_annotation(inner.as_ref()))
}
fn is_dynamic_annotation(type_value: &CalcitTypeAnnotation) -> bool {
matches!(type_value, CalcitTypeAnnotation::Dynamic | CalcitTypeAnnotation::DynFn)
|| matches!(type_value, CalcitTypeAnnotation::Optional(inner) if is_dynamic_annotation(inner.as_ref()))
}
fn annotation_dynamic_weight(type_value: &CalcitTypeAnnotation) -> usize {
match type_value {
CalcitTypeAnnotation::Dynamic => 200,
CalcitTypeAnnotation::DynFn => 100,
CalcitTypeAnnotation::List(inner)
| CalcitTypeAnnotation::Set(inner)
| CalcitTypeAnnotation::Ref(inner)
| CalcitTypeAnnotation::Optional(inner)
| CalcitTypeAnnotation::Variadic(inner) => annotation_dynamic_weight(inner),
CalcitTypeAnnotation::Map(key, value) => annotation_dynamic_weight(key) + annotation_dynamic_weight(value),
CalcitTypeAnnotation::Fn(signature) => {
signature.arg_types.iter().map(|arg| annotation_dynamic_weight(arg)).sum::<usize>()
+ signature.rest_type.as_ref().map_or(0, |rest| annotation_dynamic_weight(rest))
+ annotation_dynamic_weight(signature.return_type.as_ref())
}
CalcitTypeAnnotation::Struct(_, args) | CalcitTypeAnnotation::Enum(_, args) | CalcitTypeAnnotation::TypeRef(_, args) => {
args.iter().map(|arg| annotation_dynamic_weight(arg)).sum()
}
_ => 0,
}
}
fn find_trait_method_type<'a>(
traits: &'a [Arc<CalcitTrait>],
method_name: &str,
) -> Option<(&'a CalcitTrait, &'a Arc<CalcitTypeAnnotation>)> {
for trait_def in traits.iter().rev() {
if let Some(method_idx) = trait_def.method_index(method_name)
&& let Some(method_type) = trait_def.method_types.get(method_idx)
{
return Some((trait_def.as_ref(), method_type));
}
}
None
}
pub(crate) fn find_trait_field_type<'a>(
traits: &'a [Arc<CalcitTrait>],
field_name: &str,
) -> Option<(&'a CalcitTrait, &'a Arc<CalcitTypeAnnotation>)> {
for trait_def in traits.iter().rev() {
if let Some(field_idx) = trait_def.field_index(field_name)
&& let Some(field_type) = trait_def.method_types.get(field_idx)
{
return Some((trait_def.as_ref(), field_type));
}
}
None
}
fn collect_trait_method_names(traits: &[Arc<CalcitTrait>]) -> Vec<String> {
let mut seen = std::collections::HashSet::new();
let mut names = vec![];
for trait_def in traits.iter().rev() {
for (method, kind) in trait_def.methods.iter().zip(trait_def.member_kinds.iter()) {
if *kind != CalcitTraitMemberKind::Method {
continue;
}
let name = method.to_string();
if seen.insert(name.clone()) {
names.push(name);
}
}
}
names
}
fn core_impl_list_symbol_from_type_annotation(type_value: &CalcitTypeAnnotation) -> Option<&'static str> {
match type_value {
CalcitTypeAnnotation::List(_) => Some("&core-list-impls"),
CalcitTypeAnnotation::String => Some("&core-string-impls"),
CalcitTypeAnnotation::Map(_, _) => Some("&core-map-impls"),
CalcitTypeAnnotation::Set(_) => Some("&core-set-impls"),
CalcitTypeAnnotation::Number => Some("&core-number-impls"),
CalcitTypeAnnotation::DynFn | CalcitTypeAnnotation::Fn(_) => Some("&core-fn-impls"),
CalcitTypeAnnotation::Optional(inner) => core_impl_list_symbol_from_type_annotation(inner.as_ref()),
_ => None,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StaticMethodDescriptor {
pub name: String,
pub origin: String,
}
pub fn static_method_descriptors(type_value: &CalcitTypeAnnotation) -> Option<Vec<StaticMethodDescriptor>> {
if let Some(traits) = trait_list_from_type(type_value) {
let mut seen = HashSet::new();
let mut methods = vec![];
for trait_def in traits.iter().rev() {
for (method, kind) in trait_def.methods.iter().zip(trait_def.member_kinds.iter()) {
if *kind != CalcitTraitMemberKind::Method {
continue;
}
let name = format!(".{}", method.ref_str());
if seen.insert(name.clone()) {
methods.push(StaticMethodDescriptor {
name,
origin: trait_def.name.ref_str().to_owned(),
});
}
}
}
return Some(methods);
}
if let Some(impls) = get_impls_from_type(type_value) {
let last_wins = core_impl_list_symbol_from_type_annotation(type_value).is_none();
let ordered_impls: Box<dyn Iterator<Item = &Arc<CalcitImpl>>> = if last_wins {
Box::new(impls.iter().rev())
} else {
Box::new(impls.iter())
};
let mut seen = HashSet::new();
let mut methods = vec![];
for imp in ordered_impls {
let origin = imp.trait_name().unwrap_or_else(|| imp.name()).ref_str().to_owned();
for field in imp.fields().iter() {
let name = format!(".{}", field.ref_str());
if seen.insert(name.clone()) {
methods.push(StaticMethodDescriptor {
name,
origin: origin.clone(),
});
}
}
}
return Some(methods);
}
match type_value {
CalcitTypeAnnotation::Dynamic
| CalcitTypeAnnotation::JsObject
| CalcitTypeAnnotation::Custom(_)
| CalcitTypeAnnotation::TypeRef(_, _)
| CalcitTypeAnnotation::TypeSlot(_) => None,
CalcitTypeAnnotation::Optional(inner) => static_method_descriptors(inner.as_ref()),
_ => Some(vec![]),
}
}
fn find_method_entry<'a>(impls: &'a [Arc<CalcitImpl>], name: &str, last_wins: bool) -> Option<&'a Calcit> {
if last_wins {
for imp in impls.iter().rev() {
if let Some(entry) = imp.get(name) {
return Some(entry);
}
}
} else {
for imp in impls.iter() {
if let Some(entry) = imp.get(name) {
return Some(entry);
}
}
}
None
}
fn find_method_entry_for_type<'a>(type_ref: &CalcitTypeAnnotation, impls: &'a [Arc<CalcitImpl>], name: &str) -> Option<&'a Calcit> {
let last_wins = core_impl_list_symbol_from_type_annotation(type_ref).is_none();
find_method_entry(impls, name, last_wins)
}
fn describe_type(type_value: &CalcitTypeAnnotation) -> String {
type_value.describe()
}
pub fn preprocess_each_items(
head: &CalcitSyntax,
head_ns: &str,
args: &CalcitList,
ctx: &mut PreprocessContext,
) -> Result<Calcit, CalcitErr> {
let mut xs: TernaryTreeList<Calcit> = TernaryTreeList::from(&[Calcit::Syntax(head.to_owned(), Arc::from(head_ns))]);
args.traverse_result::<CalcitErr>(&mut |a| {
let form = preprocess_expr(a, ctx.scope_defs, ctx.scope_types, ctx.file_ns, ctx.check_warnings, ctx.call_stack)?;
xs = xs.push_right(form);
Ok(())
})?;
Ok(Calcit::List(Arc::new(xs.into())))
}
fn preprocess_if(head: &CalcitSyntax, head_ns: &str, args: &CalcitList, ctx: &mut PreprocessContext) -> Result<Calcit, CalcitErr> {
if args.len() < 2 {
return preprocess_each_items(head, head_ns, args, ctx);
}
if args.len() > 3 {
return Err(CalcitErr::use_msg_stack(
CalcitErrKind::Syntax,
format!("if expects 2 or 3 arguments, got {}", args.len()),
ctx.call_stack,
));
}
let cond_form = preprocess_expr(
args.first().unwrap(),
ctx.scope_defs,
ctx.scope_types,
ctx.file_ns,
ctx.check_warnings,
ctx.call_stack,
)?;
warn_on_nominal_enum_truthiness(&cond_form, ctx.scope_types, ctx.file_ns, ctx.check_warnings);
let narrowing = extract_predicate_bindings(&cond_form, ctx.scope_types);
let mut true_scope_types = ctx.scope_types.clone();
if let Some((sym, inferred)) = &narrowing.true_binding {
true_scope_types.insert(sym.clone(), inferred.clone());
}
let true_form = preprocess_expr(
args.get(1).unwrap(),
ctx.scope_defs,
&mut true_scope_types,
ctx.file_ns,
ctx.check_warnings,
ctx.call_stack,
)?;
let false_form = if let Some(false_branch) = args.get(2) {
let mut false_scope_types = ctx.scope_types.clone();
if let Some((sym, inferred)) = &narrowing.false_binding {
false_scope_types.insert(sym.clone(), inferred.clone());
}
Some(preprocess_expr(
false_branch,
ctx.scope_defs,
&mut false_scope_types,
ctx.file_ns,
ctx.check_warnings,
ctx.call_stack,
)?)
} else {
None
};
match &cond_form {
Calcit::Bool(true) => return Ok(true_form),
Calcit::Bool(false) | Calcit::Nil => return Ok(false_form.unwrap_or(Calcit::Nil)),
_ => {}
}
let mut xs: TernaryTreeList<Calcit> = TernaryTreeList::from(&[Calcit::Syntax(head.to_owned(), Arc::from(head_ns))]);
xs = xs.push_right(cond_form);
xs = xs.push_right(true_form);
if let Some(f) = false_form {
xs = xs.push_right(f);
}
Ok(Calcit::List(Arc::new(xs.into())))
}
struct PredicateNarrowing {
true_binding: Option<(Arc<str>, Arc<CalcitTypeAnnotation>)>,
false_binding: Option<(Arc<str>, Arc<CalcitTypeAnnotation>)>,
}
fn extract_predicate_bindings(cond_form: &Calcit, scope_types: &ScopeTypes) -> PredicateNarrowing {
let empty = PredicateNarrowing {
true_binding: None,
false_binding: None,
};
let Calcit::List(items) = cond_form else {
return empty;
};
if items.len() != 2 {
return empty;
}
let Some(pred_name) = (match items.first() {
Some(Calcit::Symbol { sym, .. }) => Some(sym.as_ref()),
Some(Calcit::Import(CalcitImport { def, .. })) => Some(def.as_ref()),
Some(Calcit::Proc(proc)) => Some(proc.as_ref()),
_ => None,
}) else {
return empty;
};
let target = match items.get(1) {
Some(t) => t,
None => return empty,
};
let sym = match target {
Calcit::Local(local) => local.sym.to_owned(),
Calcit::Symbol { sym, .. } => sym.to_owned(),
_ => return empty,
};
if let Some(ann) = match pred_name {
"list?" => Some(tag_annotation("list")),
"map?" => Some(tag_annotation("map")),
"set?" => Some(tag_annotation("set")),
"string?" => Some(tag_annotation("string")),
"number?" => Some(tag_annotation("number")),
"enum?" => Some(tag_annotation("enum")),
"struct?" => Some(tag_annotation("struct")),
"enum-def?" => Some(tag_annotation("enum-def")),
"struct-def?" => Some(tag_annotation("struct-def")),
"tag?" => Some(tag_annotation("tag")),
"bool?" => Some(tag_annotation("bool")),
"symbol?" => Some(tag_annotation("symbol")),
"fn?" => Some(tag_annotation("fn")),
_ => None,
} {
return PredicateNarrowing {
true_binding: Some((sym, ann)),
false_binding: None,
};
}
match pred_name {
"nil?" => {
let false_binding = scope_types.get(&sym).and_then(|current| {
if let CalcitTypeAnnotation::Optional(inner) = current.as_ref() {
Some((sym.clone(), inner.clone()))
} else {
None
}
});
PredicateNarrowing {
true_binding: Some((sym, Arc::new(CalcitTypeAnnotation::Unit))),
false_binding,
}
}
"some?" => {
let true_binding = scope_types.get(&sym).and_then(|current| {
if let CalcitTypeAnnotation::Optional(inner) = current.as_ref() {
Some((sym.clone(), inner.clone()))
} else {
None
}
});
PredicateNarrowing {
true_binding,
false_binding: Some((sym, Arc::new(CalcitTypeAnnotation::Unit))),
}
}
"js-nullish?" => {
let false_binding = scope_types.get(&sym).and_then(|current| {
if let CalcitTypeAnnotation::JsNullish(inner) = current.as_ref() {
Some((sym.clone(), inner.clone()))
} else {
None
}
});
PredicateNarrowing {
true_binding: Some((sym, Arc::new(CalcitTypeAnnotation::Unit))),
false_binding,
}
}
"js-present?" => {
let true_binding = scope_types.get(&sym).and_then(|current| {
if let CalcitTypeAnnotation::JsNullish(inner) = current.as_ref() {
Some((sym.clone(), inner.clone()))
} else {
None
}
});
PredicateNarrowing {
true_binding,
false_binding: Some((sym, Arc::new(CalcitTypeAnnotation::Unit))),
}
}
_ => empty,
}
}
fn resolve_enum_type_for_match(
type_ref: &CalcitTypeAnnotation,
file_ns: &str,
scope_types: &ScopeTypes,
) -> Option<calcit::CalcitEnumDef> {
if let Some(enum_def) = type_ref.resolve_to_enum() {
return Some(enum_def);
}
let CalcitTypeAnnotation::TypeRef(name, _) = type_ref else {
return None;
};
let stripped = name.trim_start_matches('\'').trim_start_matches(':');
let short_name = stripped.rsplit('/').next().unwrap_or(stripped);
if let Some(local_type) = scope_types.get(stripped).or_else(|| scope_types.get(short_name))
&& let Some(enum_def) = local_type.resolve_to_enum()
{
return Some(enum_def);
}
let (target_ns, target_def) = if let Some((ns, def)) = stripped.rsplit_once('/') {
(Arc::from(ns), Arc::from(def))
} else if program::has_def_code(file_ns, stripped) {
(Arc::from(file_ns), Arc::from(stripped))
} else if let Some(target_ns) = program::lookup_def_target_in_import(file_ns, stripped) {
(target_ns, Arc::from(stripped))
} else {
(Arc::from(calcit::CORE_NS), Arc::from(stripped))
};
match resolve_program_value_for_preprocess(&target_ns, &target_def, None) {
Some(Calcit::EnumDef(enum_def)) => Some(enum_def),
Some(Calcit::Struct(struct_value)) => calcit::CalcitEnumDef::from_struct(struct_value).ok(),
_ => None,
}
}
fn preprocess_match(head: &CalcitSyntax, head_ns: &str, args: &CalcitList, ctx: &mut PreprocessContext) -> Result<Calcit, CalcitErr> {
if args.is_empty() {
return Err(CalcitErr::use_msg_stack(
CalcitErrKind::Syntax,
"match expected a value expression and branches".to_owned(),
ctx.call_stack,
));
}
let branch_count = args.len() - 1;
if branch_count == 0 {
return Err(CalcitErr::use_msg_stack(
CalcitErrKind::Syntax,
"match expected value followed by (pattern body) pairs, got 0 branches".to_owned(),
ctx.call_stack,
));
}
let mut xs: Vec<Calcit> = vec![Calcit::Syntax(head.to_owned(), Arc::from(head_ns))];
let value_form = preprocess_expr(
args.first().unwrap(),
ctx.scope_defs,
ctx.scope_types,
ctx.file_ns,
ctx.check_warnings,
ctx.call_stack,
)?;
let inferred_match_type = infer_type_from_expr(&value_form, ctx.scope_types);
let enum_match = inferred_match_type.and_then(|t| match t.as_ref() {
CalcitTypeAnnotation::EnumValue(enum_ref) => Some((enum_ref.as_ref().to_owned(), Arc::new(vec![]))),
CalcitTypeAnnotation::Enum(enum_ref, args) => Some((enum_ref.as_ref().to_owned(), args.clone())),
CalcitTypeAnnotation::TypeRef(_, args) => {
resolve_enum_type_for_match(t.as_ref(), ctx.file_ns, ctx.scope_types).map(|enum_ref| (enum_ref, args.clone()))
}
CalcitTypeAnnotation::TypeSlot(name) => calcit::resolve_type_slot(name).and_then(|resolved| match resolved.as_ref() {
CalcitTypeAnnotation::Enum(e, args) => Some((e.as_ref().to_owned(), args.clone())),
CalcitTypeAnnotation::EnumValue(e) => Some((e.as_ref().to_owned(), Arc::new(vec![]))),
_ => None,
}),
_ => None,
});
let enum_def = enum_match.as_ref().map(|(enum_def, _)| enum_def);
let mut enum_bindings: HashMap<Arc<str>, Arc<CalcitTypeAnnotation>> = HashMap::new();
if let Some((enum_def, applied_args)) = enum_match.as_ref() {
for (name, applied) in enum_def.generics().iter().zip(applied_args.iter()) {
if !matches!(applied.as_ref(), CalcitTypeAnnotation::Dynamic) {
enum_bindings.insert(name.clone(), applied.clone());
}
}
for bound in enum_def.where_bounds() {
if !enum_bindings.contains_key(&bound.name)
&& let Some(trait_type) = resolve_where_bound_type_for_body(bound, ctx.file_ns)
{
enum_bindings.insert(bound.name.clone(), trait_type);
}
}
}
xs.push(value_form);
let mut matched_tags: Vec<Arc<str>> = vec![];
let mut has_wildcard = false;
for branch_idx in 1..args.len() {
let branch = &args[branch_idx];
let pair = match branch {
Calcit::List(pair_xs) if pair_xs.len() == 2 => pair_xs,
other => {
return Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Syntax,
format!("match branch expected a 2-element list (pattern body), got: {other}"),
ctx.call_stack,
other.get_location(),
));
}
};
let pattern = &pair[0];
let body = &pair[1];
match pattern {
Calcit::Symbol { sym, .. } if sym.as_ref() == "_" => {
has_wildcard = true;
let processed_body = preprocess_expr(
body,
ctx.scope_defs,
ctx.scope_types,
ctx.file_ns,
ctx.check_warnings,
ctx.call_stack,
)?;
xs.push(Calcit::from(CalcitList::from(&[pattern.to_owned(), processed_body])));
}
Calcit::List(pat_xs) if !pat_xs.is_empty() => {
let pat_tag = match &pat_xs[0] {
Calcit::Tag(t) => t.ref_str(),
other => {
return Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Syntax,
format!("match pattern expected a tag as first element, got: {other}"),
ctx.call_stack,
other.get_location(),
));
}
};
if let Some(enum_def) = enum_def {
if let Some(variant) = enum_def.find_variant_by_name(pat_tag) {
let expected_arity = variant.arity();
let actual_arity = pat_xs.len() - 1;
if expected_arity != actual_arity {
gen_check_warning(
format!(
"[Warn] match: variant `{}::{}` expects {} payload(s), but pattern binds {}, at {}/{}",
enum_def.name(),
pat_tag,
expected_arity,
actual_arity,
ctx.file_ns,
ctx.call_stack.0.first().map(|f| f.def.as_ref()).unwrap_or("?")
),
ctx.file_ns,
ctx.check_warnings,
);
}
} else {
let available: Vec<&str> = enum_def.variants().iter().map(|v| v.tag.ref_str()).collect();
gen_check_warning(
format!(
"[Warn] match: enum `{}` has no variant `:{pat_tag}`. Available: [{}], at {}/{}",
enum_def.name(),
available.join(", "),
ctx.file_ns,
ctx.call_stack.0.first().map(|f| f.def.as_ref()).unwrap_or("?")
),
ctx.file_ns,
ctx.check_warnings,
);
}
}
matched_tags.push(Arc::from(pat_tag));
let mut body_defs = ctx.scope_defs.to_owned();
let mut body_types = ctx.scope_types.clone();
let mut processed_pattern: Vec<Calcit> = vec![pat_xs[0].to_owned()];
for (bind_idx, binding) in pat_xs.iter().skip(1).enumerate() {
match binding {
Calcit::Symbol { sym, info, location } => {
body_defs.insert(sym.to_owned());
let payload_type = enum_def
.and_then(|e| e.find_variant_by_name(pat_tag))
.and_then(|v| v.payload_types().get(bind_idx).cloned())
.map(|payload| payload.substitute_type_vars(&enum_bindings))
.unwrap_or_else(|| crate::calcit::DYNAMIC_TYPE.clone());
let local = Calcit::Local(CalcitLocal {
idx: CalcitLocal::track_sym(sym),
sym: sym.to_owned(),
info: Arc::new(CalcitSymbolInfo {
at_ns: info.at_ns.to_owned(),
at_def: info.at_def.to_owned(),
}),
location: location.to_owned(),
type_info: payload_type.clone(),
});
body_types.insert(sym.to_owned(), payload_type);
processed_pattern.push(local);
}
other => {
return Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Syntax,
format!("match pattern binding expected a symbol, got: {other}"),
ctx.call_stack,
other.get_location(),
));
}
}
}
let processed_body = preprocess_expr(body, &body_defs, &mut body_types, ctx.file_ns, ctx.check_warnings, ctx.call_stack)?;
xs.push(Calcit::from(CalcitList::from(&[
Calcit::from(CalcitList::from(processed_pattern.as_slice())),
processed_body,
])));
}
other => {
return Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Syntax,
format!("match pattern expected (:tag ...) or _, got: {other}"),
ctx.call_stack,
other.get_location(),
));
}
}
}
if let Some(enum_def) = enum_def
&& !has_wildcard
{
let all_variants: BTreeSet<&str> = enum_def.variants().iter().map(|v| v.tag.ref_str()).collect();
let covered: BTreeSet<&str> = matched_tags.iter().map(|t| t.as_ref()).collect();
let missing: Vec<&str> = all_variants.difference(&covered).copied().collect();
if !missing.is_empty() {
gen_check_warning(
format!(
"[Warn] match on `{}` is not exhaustive. Missing variant(s): [{}], at {}/{}",
enum_def.name(),
missing.iter().map(|t| format!(":{t}")).collect::<Vec<_>>().join(", "),
ctx.file_ns,
ctx.call_stack.0.first().map(|f| f.def.as_ref()).unwrap_or("?")
),
ctx.file_ns,
ctx.check_warnings,
);
}
}
Ok(Calcit::List(Arc::from(CalcitList::Vector(xs))))
}
pub fn preprocess_defn(
head: &CalcitSyntax,
head_ns: &str,
args: &CalcitList,
ctx: &mut PreprocessContext,
) -> Result<Calcit, CalcitErr> {
let mut xs: TernaryTreeList<Calcit> = TernaryTreeList::from(&[Calcit::Syntax(head.to_owned(), Arc::from(head_ns))]);
match (args.first(), args.get(1)) {
(
Some(Calcit::Symbol {
sym: def_name,
info,
location,
..
}),
Some(Calcit::List(ys)),
) => {
let mut body_defs: HashSet<Arc<str>> = ctx.scope_defs.to_owned();
let mut body_types: ScopeTypes = ctx.scope_types.clone();
let mut param_symbols: Vec<Arc<str>> = vec![];
let mut has_marked_args = false;
xs = xs.push_right(Calcit::Symbol {
sym: def_name.to_owned(),
info: Arc::new(CalcitSymbolInfo {
at_ns: info.at_ns.to_owned(),
at_def: info.at_def.to_owned(),
}),
location: location.to_owned(),
});
let mut zs = vec![];
ys.traverse_result(&mut |y| {
match y {
Calcit::Syntax(CalcitSyntax::ArgSpread, _)
| Calcit::Syntax(CalcitSyntax::ArgOptional, _)
| Calcit::Syntax(CalcitSyntax::MacroInterpolate, _)
| Calcit::Syntax(CalcitSyntax::MacroInterpolateSpread, _) => {
has_marked_args = true; zs.push(y.to_owned());
Ok(())
}
Calcit::Symbol {
sym,
info,
location: arg_location,
..
} => {
param_symbols.push(sym.to_owned());
let loc = NodeLocation::new(
info.at_ns.to_owned(),
info.at_def.to_owned(),
arg_location.to_owned().unwrap_or_default(),
);
check_symbol(sym, args, loc, ctx.check_warnings);
body_types.remove(sym);
let s = Calcit::Local(CalcitLocal {
idx: CalcitLocal::track_sym(sym),
sym: sym.to_owned(),
info: Arc::new(CalcitSymbolInfo {
at_ns: info.at_ns.to_owned(),
at_def: info.at_def.to_owned(),
}),
location: arg_location.to_owned(),
type_info: crate::calcit::DYNAMIC_TYPE.clone(),
});
zs.push(s);
body_defs.insert(sym.to_owned());
Ok(())
}
_ => Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Type,
format!("expected defn args to be symbols, got: {y}"),
ctx.call_stack,
y.get_location(),
)),
}
})?;
let def_schema = program::lookup_def_schema(ctx.file_ns, def_name.as_ref());
if matches!(head, CalcitSyntax::DefWasmImport) && !has_valid_wasm_import_body(args) {
return Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Syntax,
"defwasm-import expects exactly two literal strings for the WASM module and field name",
ctx.call_stack,
Some(NodeLocation::new(
info.at_ns.to_owned(),
info.at_def.to_owned(),
location.to_owned().unwrap_or_default(),
)),
));
}
warn_on_legacy_optional_public_schema(ctx.file_ns, def_name.as_ref(), &def_schema, ctx.check_warnings);
let schema_issues = validate_def_schema_during_preprocess(head, ctx.file_ns, def_name.as_ref(), ys, &def_schema);
if !schema_issues.is_empty() {
let details = schema_issues.join("\n - ");
return Err(CalcitErr::use_msg_stack_location_with_code(
CalcitErrKind::Type,
format!("schema mismatch while preprocessing definition:\n - {details}"),
"E_SCHEMA_DEF_MISMATCH",
ctx.call_stack,
Some(NodeLocation::new(
info.at_ns.to_owned(),
info.at_def.to_owned(),
location.to_owned().unwrap_or_default(),
)),
));
}
let body_fn_hint = args
.iter()
.skip(2)
.find_map(CalcitTypeAnnotation::extract_fn_annotation_from_hint_form)
.and_then(|annotation| match annotation.as_ref() {
CalcitTypeAnnotation::Fn(fn_annotation) => Some(fn_annotation.clone()),
_ => None,
});
let effective_fn_schema: Option<Arc<CalcitFnTypeAnnotation>> = body_fn_hint.or_else(|| match def_schema.as_ref() {
CalcitTypeAnnotation::Fn(fn_annot) => Some(fn_annot.clone()),
CalcitTypeAnnotation::Dynamic => EXPECTED_FN_TYPE.with(|cell| cell.borrow().clone()),
_ => None,
});
if let Some(fn_annot) = &effective_fn_schema {
let body_type_bindings: HashMap<Arc<str>, Arc<CalcitTypeAnnotation>> = fn_annot
.where_bounds
.iter()
.filter_map(|bound| resolve_where_bound_type_for_body(bound, ctx.file_ns).map(|trait_type| (bound.name.clone(), trait_type)))
.collect();
let parameter_types = fn_annot
.arg_types
.iter()
.enumerate()
.map(|(idx, arg_type)| {
let substituted = arg_type.substitute_type_vars(&body_type_bindings);
let positional = unwrap_named_body_parameter_type(substituted, param_symbols.get(idx));
resolve_local_type_refs_for_body(positional, &body_types)
})
.chain(fn_annot.rest_type.iter().map(|rest_type| {
Arc::new(CalcitTypeAnnotation::Variadic(resolve_local_type_refs_for_body(
rest_type.substitute_type_vars(&body_type_bindings),
&body_types,
)))
}))
.collect::<Vec<_>>();
for (param_sym, arg_type) in param_symbols.iter().zip(parameter_types) {
if !matches!(arg_type.as_ref(), CalcitTypeAnnotation::Dynamic) {
body_types.insert(param_sym.to_owned(), arg_type);
}
}
}
for param in &mut zs {
if let Calcit::Local(local) = param
&& let Some(type_info) = body_types.get(&local.sym)
{
local.type_info = type_info.clone();
}
}
xs = xs.push_right(Calcit::from(zs.clone()));
let mut to_skip = 2;
let mut processed_body: Vec<Calcit> = vec![];
if let CalcitTypeAnnotation::Fn(fn_annot) = def_schema.as_ref() {
let schema_calcit = fn_annot.to_schema_calcit();
let schema_hint = Calcit::from(vec![Calcit::Syntax(CalcitSyntax::HintFn, Arc::from(ctx.file_ns)), schema_calcit]);
processed_body.push(schema_hint.to_owned());
xs = xs.push_right(schema_hint);
}
let prev_features = CURRENT_FN_FEATURES.with(|cell| {
let mut guard = cell.borrow_mut();
let old = guard.take();
*guard = match def_schema.as_ref() {
CalcitTypeAnnotation::Fn(fn_annot) => Some(fn_annot.features.clone()),
_ => None,
};
old
});
args.traverse_result::<CalcitErr>(&mut |a| {
if to_skip > 0 {
to_skip -= 1;
return Ok(());
}
let form = preprocess_expr(a, &body_defs, &mut body_types, ctx.file_ns, ctx.check_warnings, ctx.call_stack)?;
processed_body.push(form.clone());
xs = xs.push_right(form);
Ok(())
})?;
let return_type_hint = detect_return_type_hint_from_processed_body(&processed_body);
check_function_return_type(
&processed_body,
&return_type_hint,
&body_types,
ctx.file_ns,
def_name.as_ref(),
ctx.check_warnings,
);
let is_core_ns = ctx.file_ns == calcit::CORE_NS;
if !has_marked_args && !is_core_ns {
let expected_arity = param_symbols.len();
for body_expr in &processed_body {
check_recur_arity_in_expr(body_expr, expected_arity, ctx.file_ns, def_name.as_ref(), ctx.check_warnings);
}
}
for body_expr in &processed_body {
check_impl_traits_top_level_in_expr(body_expr, ctx.file_ns, def_name.as_ref(), ctx.check_warnings);
}
CURRENT_FN_FEATURES.with(|cell| *cell.borrow_mut() = prev_features);
Ok(Calcit::List(Arc::new(xs.into())))
}
(Some(a), Some(b)) => Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Syntax,
format!("defn/defmacro expected name and args: {a} {b}"),
ctx.call_stack,
a.get_location().or_else(|| b.get_location()),
)),
(a, b) => {
let loc = a
.and_then(|node| node.get_location())
.or_else(|| b.and_then(|node| node.get_location()));
Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Syntax,
format!("defn or defmacro expected name and args, got: {a:?} {b:?}",),
ctx.call_stack,
loc,
))
}
}
}
fn has_valid_wasm_import_body(args: &CalcitList) -> bool {
matches!(
(args.get(2), args.get(3), args.get(4)),
(Some(Calcit::Str(_)), Some(Calcit::Str(_)), None)
)
}
fn check_symbol(sym: &str, args: &CalcitList, location: NodeLocation, check_warnings: &RefCell<Vec<LocatedWarning>>) {
if is_proc_name(sym) || CalcitSyntax::is_valid(sym) || program::has_def_code(calcit::CORE_NS, sym) {
gen_check_warning_with_location(
format!("[Warn] local binding `{sym}` shadowed `calcit.core/{sym}`, with {args}"),
location,
check_warnings,
);
}
}
pub fn preprocess_core_let(
head: &CalcitSyntax,
head_ns: &str,
args: &CalcitList,
ctx: &mut PreprocessContext,
) -> Result<Calcit, CalcitErr> {
let mut xs: Vec<Calcit> = vec![Calcit::Syntax(head.to_owned(), Arc::from(head_ns))];
let mut body_defs: HashSet<Arc<str>> = ctx.scope_defs.to_owned();
let mut body_types: ScopeTypes = ctx.scope_types.clone();
let binding = match args.first() {
Some(Calcit::List(ys)) if ys.is_empty() => Calcit::from(CalcitList::default()),
Some(Calcit::List(ys)) if ys.len() == 2 => match (&ys[0], &ys[1]) {
(Calcit::Symbol { sym, info, location }, a) => {
let loc = NodeLocation::new(
info.at_ns.to_owned(),
info.at_def.to_owned(),
location.to_owned().unwrap_or_default(),
);
check_symbol(sym, ys, loc, ctx.check_warnings);
body_defs.insert(sym.to_owned());
let form = preprocess_expr(a, &body_defs, &mut body_types, ctx.file_ns, ctx.check_warnings, ctx.call_stack)?;
let inferred_type = infer_type_from_expr(&form, &body_types).unwrap_or_else(|| crate::calcit::DYNAMIC_TYPE.clone());
let name = Calcit::Local(CalcitLocal {
idx: CalcitLocal::track_sym(sym),
sym: sym.to_owned(),
info: Arc::new(CalcitSymbolInfo {
at_ns: info.at_ns.to_owned(),
at_def: info.at_def.to_owned(),
}),
location: location.to_owned(),
type_info: inferred_type.clone(),
});
body_types.insert(sym.to_owned(), inferred_type);
Calcit::from(CalcitList::from(&[name, form]))
}
(a, b) => {
return Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Syntax,
format!("invalid pair for &let binding: {a} {b}"),
ctx.call_stack,
a.get_location().or_else(|| b.get_location()),
));
}
},
Some(a @ Calcit::List(_)) => {
return Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Syntax,
format!("expected binding of a pair, got: {a}"),
ctx.call_stack,
a.get_location(),
));
}
Some(a) => {
return Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Syntax,
format!("expected binding of a pair, got: {a}"),
ctx.call_stack,
a.get_location(),
));
}
None => {
return Err(CalcitErr::use_msg_stack(
CalcitErrKind::Syntax,
"expected binding of a pair, got nothing".to_owned(),
ctx.call_stack,
));
}
};
xs.push(binding);
let mut skipped_head = false;
args.traverse_result::<CalcitErr>(&mut |a| {
if !skipped_head {
skipped_head = true;
return Ok(());
}
let form = preprocess_expr(a, &body_defs, &mut body_types, ctx.file_ns, ctx.check_warnings, ctx.call_stack)?;
xs.push(form);
Ok(())
})?;
Ok(Calcit::List(Arc::from(CalcitList::Vector(xs))))
}
pub fn preprocess_quote(
head: &CalcitSyntax,
head_ns: &str,
args: &CalcitList,
_scope_defs: &HashSet<Arc<str>>,
_file_ns: &str,
) -> Result<Calcit, CalcitErr> {
let mut xs: TernaryTreeList<Calcit> = TernaryTreeList::from(&[Calcit::Syntax(head.to_owned(), Arc::from(head_ns))]);
args.traverse_result::<CalcitErr>(&mut |a| {
xs = xs.push_right(a.to_owned());
Ok(())
})?;
Ok(Calcit::List(Arc::new(xs.into())))
}
pub fn preprocess_defatom(
head: &CalcitSyntax,
head_ns: &str,
args: &CalcitList,
ctx: &mut PreprocessContext,
) -> Result<Calcit, CalcitErr> {
let mut xs: TernaryTreeList<Calcit> = TernaryTreeList::from(&[Calcit::Syntax(head.to_owned(), Arc::from(head_ns))]);
args.traverse_result::<CalcitErr>(&mut |a| {
let form = preprocess_expr(a, ctx.scope_defs, ctx.scope_types, ctx.file_ns, ctx.check_warnings, ctx.call_stack)?;
xs = xs.push_right(form.to_owned());
Ok(())
})?;
Ok(Calcit::List(Arc::new(CalcitList::List(xs))))
}
pub fn preprocess_quasiquote(
head: &CalcitSyntax,
head_ns: &str,
args: &CalcitList,
ctx: &mut PreprocessContext,
) -> Result<Calcit, CalcitErr> {
let mut xs: TernaryTreeList<Calcit> = TernaryTreeList::from(&[Calcit::Syntax(head.to_owned(), Arc::from(head_ns))]);
args.traverse_result::<CalcitErr>(&mut |a| {
let form = preprocess_quasiquote_internal(a, ctx.scope_defs, ctx.scope_types, ctx.file_ns, ctx.check_warnings, ctx.call_stack)?;
xs = xs.push_right(form);
Ok(())
})?;
Ok(Calcit::List(Arc::new(xs.into())))
}
pub fn preprocess_quasiquote_internal(
x: &Calcit,
scope_defs: &HashSet<Arc<str>>,
scope_types: &mut ScopeTypes,
file_ns: &str,
check_warnings: &RefCell<Vec<LocatedWarning>>,
call_stack: &CallStackList,
) -> Result<Calcit, CalcitErr> {
match x {
Calcit::List(ys) if ys.is_empty() => Ok(x.to_owned()),
Calcit::List(ys) => match &ys[0] {
Calcit::Syntax(CalcitSyntax::MacroInterpolate, _) | &Calcit::Syntax(CalcitSyntax::MacroInterpolateSpread, _) => {
let mut xs = vec![];
for y in &**ys {
let form = preprocess_expr(y, scope_defs, scope_types, file_ns, check_warnings, call_stack)?;
xs.push(form.to_owned());
}
Ok(Calcit::from(xs))
}
_ => {
let mut xs = vec![];
for y in &**ys {
xs.push(preprocess_quasiquote_internal(y, scope_defs, scope_types, file_ns, check_warnings, call_stack)?.to_owned());
}
Ok(Calcit::from(xs))
}
},
_ => Ok(x.to_owned()),
}
}
pub fn preprocess_hint_fn(
head: &CalcitSyntax,
head_ns: &str,
args: &CalcitList,
ctx: &mut PreprocessContext,
) -> Result<Calcit, CalcitErr> {
let mut legacy_clauses: BTreeSet<&str> = BTreeSet::new();
let mut error_location: Option<NodeLocation> = None;
for item in args {
let Calcit::List(inner) = item else {
continue;
};
let Some(head) = inner.first() else {
continue;
};
if let Some(name) = extract_hint_fn_legacy_clause_name(head) {
legacy_clauses.insert(name);
if error_location.is_none() {
error_location = item.get_location();
}
}
}
if !legacy_clauses.is_empty() {
let clauses = legacy_clauses.into_iter().collect::<Vec<_>>().join(", ");
return Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Syntax,
format!(
"legacy hint-fn clauses are no longer supported ({clauses}); use schema map form like `(hint-fn $ {{}} (:args ...) (:return ...))`"
),
ctx.call_stack,
error_location,
));
}
let mut ys = vec![Calcit::Syntax(head.to_owned(), Arc::from(head_ns))];
for a in args {
ys.push(a.to_owned());
}
if args.len() >= 2
&& let Some(type_entry) = CalcitTypeAnnotation::extract_fn_annotation_from_hint_form(&Calcit::from(ys.clone()))
&& let Some(target_raw) = args.first()
{
let target_form = preprocess_expr(
target_raw,
ctx.scope_defs,
ctx.scope_types,
ctx.file_ns,
ctx.check_warnings,
ctx.call_stack,
)?;
if let Calcit::Local(local) = target_form {
ctx.scope_types.insert(local.sym.to_owned(), type_entry.clone());
let mut typed_local = local;
typed_local.type_info = type_entry;
ys[1] = Calcit::Local(typed_local);
}
}
Ok(Calcit::from(ys))
}
pub fn preprocess_assert_type(
head: &CalcitSyntax,
head_ns: &str,
args: &CalcitList,
ctx: &mut PreprocessContext,
) -> Result<Calcit, CalcitErr> {
if args.len() != 2 {
return Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Arity,
format!("{head} expected an expression and a type expression, got {}", args.len()),
ctx.call_stack,
args.first().and_then(|node| node.get_location()),
));
}
let target_raw = args.get(0).unwrap();
let type_form = args.get(1).unwrap();
let target_form = preprocess_expr(
target_raw,
ctx.scope_defs,
ctx.scope_types,
ctx.file_ns,
ctx.check_warnings,
ctx.call_stack,
)?;
let asserted_target = target_form;
if let Calcit::Local(local) = &asserted_target {
let asserted_type = CalcitTypeAnnotation::parse_type_annotation_form(type_form);
let current_type = resolve_type_value(&asserted_target, ctx.scope_types).unwrap_or_else(|| local.type_info.clone());
let type_entry = if current_type.as_ref().matches_annotation(asserted_type.as_ref())
&& annotation_dynamic_weight(current_type.as_ref()) < annotation_dynamic_weight(asserted_type.as_ref())
{
current_type
} else {
asserted_type
};
ctx.scope_types.insert(local.sym.to_owned(), type_entry.clone());
let mut typed_local = local.to_owned();
typed_local.type_info = type_entry;
return Ok(Calcit::Local(typed_local));
}
Ok(Calcit::from(vec![
Calcit::Syntax(head.to_owned(), Arc::from(head_ns)),
asserted_target,
type_form.to_owned(),
]))
}
pub fn preprocess_unsafe_coerce(
head: &CalcitSyntax,
head_ns: &str,
args: &CalcitList,
ctx: &mut PreprocessContext,
) -> Result<Calcit, CalcitErr> {
if args.len() != 2 {
return Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Arity,
format!("{head} expected a value and a type expression, got {}", args.len()),
ctx.call_stack,
args.first().and_then(|node| node.get_location()),
));
}
let target_form = preprocess_expr(
args.first().expect("validated unsafe-coerce target"),
ctx.scope_defs,
ctx.scope_types,
ctx.file_ns,
ctx.check_warnings,
ctx.call_stack,
)?;
Ok(Calcit::from(vec![
Calcit::Syntax(head.to_owned(), Arc::from(head_ns)),
target_form,
args.get(1).expect("declared unsafe-coerce type").to_owned(),
]))
}
pub fn preprocess_parse_cirru_edn_as(
head: &CalcitSyntax,
head_ns: &str,
args: &CalcitList,
ctx: &mut PreprocessContext,
) -> Result<Calcit, CalcitErr> {
if args.len() != 2 {
return Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Arity,
format!("{head} expected a string and a type expression, got {}", args.len()),
ctx.call_stack,
args.first().and_then(Calcit::get_location),
));
}
let text_form = preprocess_expr(
args.first().expect("validated parse-cirru-edn-as text"),
ctx.scope_defs,
ctx.scope_types,
ctx.file_ns,
ctx.check_warnings,
ctx.call_stack,
)?;
let type_form = args.get(1).expect("validated parse-cirru-edn-as type");
let target = CalcitTypeAnnotation::parse_type_annotation_form_with_generics(type_form, &[]);
let decoder = crate::calcit::data_shape::DataShapeGraph::build(target.as_ref(), ctx.file_ns).map_err(|error| {
CalcitErr::use_msg_stack_location(
CalcitErrKind::Type,
format!("parse-cirru-edn-as cannot derive a decoder: {error}"),
ctx.call_stack,
type_form.get_location(),
)
})?;
Ok(Calcit::from(vec![
Calcit::Syntax(head.to_owned(), Arc::from(head_ns)),
text_form,
type_form.to_owned(),
decoder.into_calcit_handle(),
]))
}
pub fn preprocess_assert_traits(
head: &CalcitSyntax,
_head_ns: &str,
args: &CalcitList,
ctx: &mut PreprocessContext,
) -> Result<Calcit, CalcitErr> {
if args.len() < 2 {
return Err(CalcitErr::use_msg_stack_location(
CalcitErrKind::Arity,
format!(
"assert-traits expected an expression and one or more trait definitions, got {head} with {} argument(s).",
args.len()
),
ctx.call_stack,
args.first().and_then(|node| node.get_location()),
));
}
let target_raw = args.get(0).unwrap();
let trait_forms = args.iter().skip(1).collect::<Vec<_>>();
let target_form = preprocess_expr(
target_raw,
ctx.scope_defs,
ctx.scope_types,
ctx.file_ns,
ctx.check_warnings,
ctx.call_stack,
)?;
let local_opt = match &target_form {
Calcit::Local(local) => Some(local.to_owned()),
_ => None,
};
let mut trait_defs: Vec<Arc<CalcitTrait>> = vec![];
let mut fallback_entry: Option<Arc<CalcitTypeAnnotation>> = None;
for trait_form in trait_forms.iter() {
let parsed_entry = CalcitTypeAnnotation::parse_type_annotation_form(trait_form);
if let CalcitTypeAnnotation::Trait(trait_def) = parsed_entry.as_ref() {
trait_defs.push(trait_def.to_owned());
continue;
}
let resolved = match trait_form {
Calcit::Symbol { sym, info, .. } => match runner::parse_ns_def(sym) {
Some((ns_part, def_part)) => lookup_trait_ns_def_for_preprocess(&ns_part, &def_part, ctx.check_warnings, ctx.call_stack)
.ok()
.flatten(),
None => lookup_trait_ns_def_for_preprocess(&info.at_ns, sym, ctx.check_warnings, ctx.call_stack)
.ok()
.flatten(),
},
Calcit::Import(import) => lookup_trait_ns_def_for_preprocess(&import.ns, &import.def, ctx.check_warnings, ctx.call_stack)
.ok()
.flatten(),
_ => None,
};
if let Some(trait_def) = resolved {
trait_defs.push(trait_def);
} else if fallback_entry.is_none() {
fallback_entry = Some(Arc::new(CalcitTypeAnnotation::Custom(Arc::new((*trait_form).to_owned()))));
}
}
let mut assert_target = target_form;
if let Some(local) = local_opt {
let existing_entry = ctx.scope_types.get(&local.sym).cloned().or_else(|| {
if matches!(*local.type_info, CalcitTypeAnnotation::Dynamic) {
None
} else {
Some(local.type_info.clone())
}
});
let resolved_entry = if let Some(existing) = existing_entry.as_ref() {
let ann = existing.as_ref();
if !is_dynamic_annotation(ann) && !is_trait_annotation(ann) {
existing.clone()
} else if let Some(fallback) = fallback_entry.as_ref() {
let fallback_ann = fallback.as_ref();
if !is_dynamic_annotation(fallback_ann) && !is_trait_annotation(fallback_ann) {
fallback.clone()
} else if !trait_defs.is_empty() {
if trait_defs.len() == 1 {
Arc::new(CalcitTypeAnnotation::Trait(trait_defs.remove(0)))
} else {
Arc::new(CalcitTypeAnnotation::TraitSet(Arc::new(trait_defs)))
}
} else {
fallback.clone()
}
} else if !trait_defs.is_empty() {
if trait_defs.len() == 1 {
Arc::new(CalcitTypeAnnotation::Trait(trait_defs.remove(0)))
} else {
Arc::new(CalcitTypeAnnotation::TraitSet(Arc::new(trait_defs)))
}
} else {
crate::calcit::DYNAMIC_TYPE.clone()
}
} else if let Some(fallback) = fallback_entry.as_ref() {
let fallback_ann = fallback.as_ref();
if !is_dynamic_annotation(fallback_ann) && !is_trait_annotation(fallback_ann) {
fallback.clone()
} else if !trait_defs.is_empty() {
if trait_defs.len() == 1 {
Arc::new(CalcitTypeAnnotation::Trait(trait_defs.remove(0)))
} else {
Arc::new(CalcitTypeAnnotation::TraitSet(Arc::new(trait_defs)))
}
} else {
fallback.clone()
}
} else if !trait_defs.is_empty() {
if trait_defs.len() == 1 {
Arc::new(CalcitTypeAnnotation::Trait(trait_defs.remove(0)))
} else {
Arc::new(CalcitTypeAnnotation::TraitSet(Arc::new(trait_defs)))
}
} else {
crate::calcit::DYNAMIC_TYPE.clone()
};
ctx.scope_types.insert(local.sym.to_owned(), resolved_entry.clone());
let mut typed_local = local.to_owned();
typed_local.type_info = resolved_entry;
assert_target = Calcit::Local(typed_local);
}
let mut assert_expr: Calcit = assert_target;
for trait_form in trait_forms.iter() {
let trait_value = preprocess_expr(
trait_form,
ctx.scope_defs,
ctx.scope_types,
ctx.file_ns,
ctx.check_warnings,
ctx.call_stack,
)?;
assert_expr = Calcit::from(vec![Calcit::Proc(CalcitProc::NativeAssertTraits), assert_expr, trait_value]);
}
Ok(assert_expr)
}
fn analyze_def_schema_param_arity(args: &CalcitList) -> (usize, bool) {
let mut required_count = 0;
let mut has_rest = false;
let mut skip_rest_binding = false;
for item in args {
if matches!(item, Calcit::Syntax(CalcitSyntax::ArgSpread, _)) {
has_rest = true;
skip_rest_binding = true;
continue;
}
if matches!(item, Calcit::Syntax(CalcitSyntax::ArgOptional, _)) {
continue;
}
if skip_rest_binding {
skip_rest_binding = false;
continue;
}
required_count += 1;
}
(required_count, has_rest)
}
fn contains_legacy_optional(annotation: &CalcitTypeAnnotation) -> bool {
match annotation {
CalcitTypeAnnotation::Optional(_) => true,
CalcitTypeAnnotation::List(inner)
| CalcitTypeAnnotation::Set(inner)
| CalcitTypeAnnotation::Ref(inner)
| CalcitTypeAnnotation::Variadic(inner)
| CalcitTypeAnnotation::JsNullish(inner) => contains_legacy_optional(inner),
CalcitTypeAnnotation::Map(key, value) => contains_legacy_optional(key) || contains_legacy_optional(value),
CalcitTypeAnnotation::Fn(info) => {
info.arg_types.iter().any(|item| contains_legacy_optional(item))
|| info.rest_type.as_ref().is_some_and(|item| contains_legacy_optional(item))
|| contains_legacy_optional(info.return_type.as_ref())
}
CalcitTypeAnnotation::Struct(_, args) | CalcitTypeAnnotation::Enum(_, args) | CalcitTypeAnnotation::TypeRef(_, args) => {
args.iter().any(|item| contains_legacy_optional(item))
}
_ => false,
}
}
fn warn_on_legacy_optional_public_schema(
ns: &str,
def_name: &str,
schema: &CalcitTypeAnnotation,
check_warnings: &RefCell<Vec<LocatedWarning>>,
) {
if !contains_legacy_optional(schema) {
return;
}
if ns == calcit::CORE_NS && (def_name.starts_with('&') || def_name == "optionally") {
return;
}
gen_check_warning_code(
format!(
"[Warn] {ns}/{def_name} exposes legacy Optional<T> in its function schema; use Option<T> for Calcit absence, JsNullish<T> only for JavaScript FFI, Result<T,E> for failures, or Unit for effects"
),
"W_LEGACY_OPTIONAL_SCHEMA",
ns,
check_warnings,
);
}
fn validate_def_schema_during_preprocess(
head: &CalcitSyntax,
ns: &str,
def_name: &str,
args: &CalcitList,
schema: &CalcitTypeAnnotation,
) -> Vec<String> {
let CalcitTypeAnnotation::Fn(fn_annot) = schema else {
return vec![];
};
let code_kind = match head {
CalcitSyntax::Defn => "defn",
CalcitSyntax::DefWasmExport => "defwasm-export",
CalcitSyntax::DefWasmImport => "defwasm-import",
CalcitSyntax::Defmacro => "defmacro",
_ => return vec![],
};
let mut issues: Vec<String> = vec![];
match (fn_annot.fn_kind, code_kind) {
(SchemaKind::Fn, "defmacro") => {
issues.push(format!("{ns}/{def_name}: schema :kind is :fn but code uses defmacro"));
}
(SchemaKind::Macro, "defn" | "defwasm-export" | "defwasm-import") => {
issues.push(format!("{ns}/{def_name}: schema :kind is :macro but code uses {code_kind}"));
}
_ => {}
}
if code_kind == "defmacro" {
return issues;
}
let (required_count, has_rest) = analyze_def_schema_param_arity(args);
let schema_required = fn_annot.arg_types.len();
let schema_has_rest = fn_annot.rest_type.is_some();
if required_count != schema_required {
issues.push(format!(
"{ns}/{def_name}: schema has {schema_required} required arg(s) but code has {required_count}"
));
}
if has_rest != schema_has_rest {
if has_rest {
issues.push(format!("{ns}/{def_name}: code has & rest param but schema has no :rest"));
} else {
issues.push(format!("{ns}/{def_name}: schema has :rest but code has no & param"));
}
}
issues
}
#[cfg(test)]
mod tests {
use super::*;
use crate::calcit::{
CalcitFn, CalcitFnArgs, CalcitFnUsageMeta, CalcitImport, CalcitMacro, CalcitScope, CalcitStructDef, CalcitStructValue, ImportInfo,
};
use crate::data::cirru::code_to_calcit;
use cirru_parser::Cirru;
use std::sync::{LazyLock, Mutex};
static PREPROCESS_TEST_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
fn lock_preprocess_test_state() -> std::sync::MutexGuard<'static, ()> {
PREPROCESS_TEST_LOCK.lock().unwrap_or_else(|err| err.into_inner())
}
#[test]
fn removed_data_apis_point_to_their_struct_enum_replacements() {
let cases = [
("record?", "struct? (values) or struct-def? (definitions)"),
("tuple?", "enum? (values) or enum-def? (definitions)"),
("record-struct", "struct-definition"),
("tuple-enum", "enum-definition"),
("record-with", "struct-with"),
("record-match", "struct-match"),
("&record:get", "&struct:get"),
("&record:struct", "&struct:definition"),
("&tuple:nth", "&enum:nth"),
("&tuple:enum", "&enum:definition"),
("&tuple:enum-has-variant?", "&enum-def:has-variant?"),
("&tuple:enum-variant-arity", "&enum-def:variant-arity"),
("&tuple:validate-enum", "&enum:validate"),
];
for (legacy, replacement) in cases {
assert_eq!(removed_data_api_replacement(legacy).as_deref(), Some(replacement));
}
assert_eq!(removed_data_api_replacement("&map:get"), None);
}
#[test]
fn removed_core_data_api_calls_emit_migration_diagnostics() {
let head = Calcit::Import(CalcitImport {
ns: Arc::from(calcit::CORE_NS),
def: Arc::from("record?"),
info: Arc::new(ImportInfo::Core {
at_ns: Arc::from("tests.migration"),
}),
def_id: None,
});
let warnings = RefCell::new(vec![]);
warn_on_removed_data_api_call(&head, None, "tests.migration", &warnings);
let warnings = warnings.borrow();
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].code(), Some("W_REMOVED_DATA_API"));
assert!(warnings[0].message().contains("struct? (values) or struct-def? (definitions)"));
}
#[test]
fn js_nullish_ffi_values_require_safe_dereference_and_dedicated_predicates() {
let sym: Arc<str> = Arc::from("host");
let receiver = Calcit::Local(CalcitLocal {
idx: CalcitLocal::track_sym(&sym),
sym,
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("tests.js-ffi"),
at_def: Arc::from("demo"),
}),
location: None,
type_info: Arc::new(CalcitTypeAnnotation::JsNullish(Arc::new(CalcitTypeAnnotation::JsObject))),
});
let args = CalcitList::from(std::slice::from_ref(&receiver));
let warnings = RefCell::new(vec![]);
warn_on_nullable_js_ffi_dereference(
&Calcit::Method(Arc::from("read"), calcit::MethodKind::InvokeNative),
&args,
&ScopeTypes::new(),
"tests.js-ffi",
"demo",
&warnings,
);
assert_eq!(warnings.borrow().len(), 1);
assert_eq!(warnings.borrow()[0].code(), Some("W_JS_FFI_NULLABLE_DEREF"));
let optional_warnings = RefCell::new(vec![]);
warn_on_nullable_js_ffi_dereference(
&Calcit::Method(Arc::from("read"), calcit::MethodKind::InvokeNativeOptional),
&args,
&ScopeTypes::new(),
"tests.js-ffi",
"demo",
&optional_warnings,
);
assert!(optional_warnings.borrow().is_empty());
let predicate_warnings = RefCell::new(vec![]);
warn_on_legacy_js_nullish_predicate(
&Calcit::Proc(CalcitProc::NilQuestion),
&args,
&ScopeTypes::new(),
"tests.js-ffi",
"demo",
&predicate_warnings,
);
assert_eq!(predicate_warnings.borrow().len(), 1);
assert_eq!(predicate_warnings.borrow()[0].code(), Some("W_JS_FFI_NULLABLE_PREDICATE"));
let mut scope_types = ScopeTypes::new();
scope_types.insert(
Arc::from("host"),
Arc::new(CalcitTypeAnnotation::JsNullish(Arc::new(CalcitTypeAnnotation::JsObject))),
);
let predicate = Calcit::from(vec![
Calcit::Symbol {
sym: Arc::from("js-present?"),
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("tests.js-ffi"),
at_def: Arc::from("demo"),
}),
location: None,
},
receiver,
]);
let narrowing = extract_predicate_bindings(&predicate, &scope_types);
assert!(matches!(
narrowing.true_binding,
Some((name, inferred)) if name.as_ref() == "host" && matches!(inferred.as_ref(), CalcitTypeAnnotation::JsObject)
));
}
#[test]
fn nominal_options_warn_on_legacy_nil_and_enum_operations() {
let core_head = |operation: &str| {
Calcit::Import(CalcitImport {
ns: Arc::from(calcit::CORE_NS),
def: Arc::from(operation),
info: Arc::new(ImportInfo::Core {
at_ns: Arc::from("tests.option-migration"),
}),
def_id: None,
})
};
let sym: Arc<str> = Arc::from("found");
let option_value = Calcit::Local(CalcitLocal {
idx: CalcitLocal::track_sym(&sym),
sym,
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("tests.option-migration"),
at_def: Arc::from("demo"),
}),
location: None,
type_info: Arc::new(CalcitTypeAnnotation::TypeRef(
Arc::from("calcit.core/Option"),
Arc::new(vec![Arc::new(CalcitTypeAnnotation::Number)]),
)),
});
let args = CalcitList::from(std::slice::from_ref(&option_value));
for operation in ["some?", "get", "&compare", "struct?"] {
let head = core_head(operation);
let warnings = RefCell::new(vec![]);
warn_on_nominal_enum_legacy_absence_use(&head, &args, &ScopeTypes::new(), "tests.option-migration", "demo", &warnings);
assert_eq!(warnings.borrow().len(), 1, "{operation} should warn");
assert_eq!(warnings.borrow()[0].code(), Some("W_NOMINAL_ENUM_LEGACY_USE"));
}
let method_warnings = RefCell::new(vec![]);
warn_on_nominal_enum_legacy_absence_use(
&Calcit::Method(Arc::from("count"), calcit::MethodKind::Invoke(calcit::DYNAMIC_TYPE.clone())),
&args,
&ScopeTypes::new(),
"tests.option-migration",
"demo",
&method_warnings,
);
assert_eq!(method_warnings.borrow().len(), 1, "structural Option methods should warn");
let equality_head = core_head("=");
let mixed_equality_args = CalcitList::from(&[option_value.to_owned(), Calcit::Number(1.0)] as &[Calcit]);
let mixed_equality_warnings = RefCell::new(vec![]);
warn_on_nominal_enum_legacy_absence_use(
&equality_head,
&mixed_equality_args,
&ScopeTypes::new(),
"tests.option-migration",
"demo",
&mixed_equality_warnings,
);
assert_eq!(mixed_equality_warnings.borrow().len(), 1, "Option-to-payload equality should warn");
let nominal_equality_args = CalcitList::from(&[option_value.to_owned(), option_value.to_owned()] as &[Calcit]);
let nominal_equality_warnings = RefCell::new(vec![]);
warn_on_nominal_enum_legacy_absence_use(
&equality_head,
&nominal_equality_args,
&ScopeTypes::new(),
"tests.option-migration",
"demo",
&nominal_equality_warnings,
);
assert!(
nominal_equality_warnings.borrow().is_empty(),
"Option-to-Option equality should stay valid"
);
let application_get = Calcit::Symbol {
sym: Arc::from("get"),
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("tests.option-migration"),
at_def: Arc::from("demo"),
}),
location: None,
};
let application_get_warnings = RefCell::new(vec![]);
warn_on_nominal_enum_legacy_absence_use(
&application_get,
&args,
&ScopeTypes::new(),
"tests.option-migration",
"demo",
&application_get_warnings,
);
assert!(
application_get_warnings.borrow().is_empty(),
"application-defined get must not warn"
);
let application_option_sym: Arc<str> = Arc::from("application-option");
let application_option = Calcit::Local(CalcitLocal {
idx: CalcitLocal::track_sym(&application_option_sym),
sym: application_option_sym,
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("tests.option-migration"),
at_def: Arc::from("demo"),
}),
location: None,
type_info: Arc::new(CalcitTypeAnnotation::TypeRef(
Arc::from("app.model/Option"),
Arc::new(vec![Arc::new(CalcitTypeAnnotation::Number)]),
)),
});
let application_option_args = CalcitList::from(std::slice::from_ref(&application_option));
let application_option_warnings = RefCell::new(vec![]);
warn_on_nominal_enum_legacy_absence_use(
&core_head("some?"),
&application_option_args,
&ScopeTypes::new(),
"tests.option-migration",
"demo",
&application_option_warnings,
);
assert!(
application_option_warnings.borrow().is_empty(),
"application-defined Option must not warn"
);
let truthiness_warnings = RefCell::new(vec![]);
warn_on_nominal_enum_truthiness(&option_value, &ScopeTypes::new(), "tests.option-migration", &truthiness_warnings);
assert_eq!(truthiness_warnings.borrow().len(), 1, "Option truthiness should warn");
assert_eq!(truthiness_warnings.borrow()[0].code(), Some("W_NOMINAL_ENUM_LEGACY_USE"));
}
struct WarnDynMethodGuard {
prev: bool,
}
impl WarnDynMethodGuard {
fn new(enabled: bool) -> Self {
let prev = warn_dyn_method_enabled();
set_warn_dyn_method(enabled);
Self { prev }
}
}
impl Drop for WarnDynMethodGuard {
fn drop(&mut self) {
set_warn_dyn_method(self.prev);
}
}
#[test]
fn static_method_descriptors_follow_user_impl_precedence() {
let low_impl = Arc::new(CalcitImpl {
name: EdnTag::new("LowImpl"),
origin: None,
fields: Arc::new(vec![EdnTag::new("low"), EdnTag::new("shared")]),
values: Arc::new(vec![Calcit::Nil, Calcit::Nil]),
});
let high_impl = Arc::new(CalcitImpl {
name: EdnTag::new("HighImpl"),
origin: None,
fields: Arc::new(vec![EdnTag::new("high"), EdnTag::new("shared")]),
values: Arc::new(vec![Calcit::Nil, Calcit::Nil]),
});
let type_value = CalcitTypeAnnotation::Struct(
Arc::new(CalcitStructDef {
name: EdnTag::new("Demo"),
fields: Arc::new(vec![]),
field_types: Arc::new(vec![]),
generics: Arc::new(vec![]),
where_bounds: Arc::new(vec![]),
impls: vec![low_impl, high_impl],
}),
Arc::new(vec![]),
);
let methods = static_method_descriptors(&type_value).expect("struct method metadata should resolve");
assert_eq!(
methods,
vec![
StaticMethodDescriptor {
name: ".high".to_owned(),
origin: "HighImpl".to_owned(),
},
StaticMethodDescriptor {
name: ".shared".to_owned(),
origin: "HighImpl".to_owned(),
},
StaticMethodDescriptor {
name: ".low".to_owned(),
origin: "LowImpl".to_owned(),
},
]
);
}
#[test]
fn passes_assert_type_through_preprocess() {
let expr = Cirru::List(vec![Cirru::leaf("assert-type"), Cirru::leaf("x"), Cirru::leaf(":fn")]);
let code = code_to_calcit(&expr, "tests.assert", "main", vec![]).expect("parse cirru");
let mut scope_defs: HashSet<Arc<str>> = HashSet::new();
scope_defs.insert(Arc::from("x"));
let mut scope_types: ScopeTypes = ScopeTypes::new();
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let resolved =
preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.assert", &warnings, &stack).expect("preprocess assert-type");
assert!(matches!(resolved, Calcit::Local(_)), "local assert-type should return typed local");
assert!(scope_types.contains_key("x"), "type should be registered in scope");
if let Some(type_val) = scope_types.get("x") {
assert!(matches!(type_val.as_ref(), CalcitTypeAnnotation::DynFn), "type should be fn");
}
}
#[test]
fn broad_assert_type_does_not_erase_inferred_element_type() {
let expr = Cirru::List(vec![Cirru::leaf("assert-type"), Cirru::leaf("xs"), Cirru::leaf(":list")]);
let code = code_to_calcit(&expr, "tests.assert", "main", vec![]).expect("parse cirru");
let mut scope_defs: HashSet<Arc<str>> = HashSet::new();
scope_defs.insert(Arc::from("xs"));
let mut scope_types: ScopeTypes = ScopeTypes::new();
scope_types.insert(
Arc::from("xs"),
Arc::new(CalcitTypeAnnotation::List(Arc::new(CalcitTypeAnnotation::Number))),
);
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let resolved =
preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.assert", &warnings, &stack).expect("preprocess assert-type");
assert!(matches!(
resolve_type_value(&resolved, &scope_types).as_deref(),
Some(CalcitTypeAnnotation::List(inner)) if matches!(inner.as_ref(), CalcitTypeAnnotation::Number)
));
}
#[test]
fn unsafe_coerce_preserves_boundary_node_and_declared_expression_type() {
let expr = Cirru::List(vec![Cirru::leaf("unsafe-coerce"), Cirru::leaf("x"), Cirru::leaf(":number")]);
let code = code_to_calcit(&expr, "tests.unsafe-coerce", "main", vec![]).expect("parse cirru");
let mut scope_defs: HashSet<Arc<str>> = HashSet::new();
scope_defs.insert(Arc::from("x"));
let mut scope_types: ScopeTypes = ScopeTypes::new();
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let resolved =
preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.unsafe-coerce", &warnings, &stack).expect("preprocess coercion");
let Calcit::List(nodes) = &resolved else {
panic!("unsafe-coerce should remain visible in the preprocessed tree");
};
assert!(matches!(nodes.first(), Some(Calcit::Syntax(CalcitSyntax::UnsafeCoerce, _))));
assert!(matches!(
infer_type_from_expr(&resolved, &scope_types).map(|t| t.as_ref().clone()),
Some(CalcitTypeAnnotation::Number)
));
assert!(
!scope_types.contains_key("x"),
"coercing one expression must not retype every later use of the local"
);
assert!(warnings.borrow().is_empty());
}
#[test]
fn strict_edn_decode_rejects_dynamic_during_preprocess() {
let expr = Cirru::List(vec![
Cirru::leaf("parse-cirru-edn-as"),
Cirru::leaf("|do 1"),
Cirru::leaf(":dynamic"),
]);
let code = code_to_calcit(&expr, "tests.edn", "main", vec![]).expect("parse strict decoder");
let scope_defs: HashSet<Arc<str>> = HashSet::new();
let mut scope_types: ScopeTypes = ScopeTypes::new();
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let error = preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.edn", &warnings, &stack)
.expect_err("Dynamic decoder must be rejected before runtime");
assert!(error.msg.contains("Dynamic is forbidden"), "unexpected error: {error:?}");
}
#[test]
fn strict_edn_decode_retains_compiled_data_shape() {
let expr = Cirru::List(vec![Cirru::leaf("parse-cirru-edn-as"), Cirru::leaf("|1"), Cirru::leaf(":number")]);
let code = code_to_calcit(&expr, "tests.edn", "main", vec![]).expect("parse strict decoder");
let scope_defs: HashSet<Arc<str>> = HashSet::new();
let mut scope_types: ScopeTypes = ScopeTypes::new();
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let resolved =
preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.edn", &warnings, &stack).expect("preprocess strict decoder");
let Calcit::List(nodes) = resolved else {
panic!("strict decoder should remain a syntax node");
};
assert_eq!(nodes.len(), 4);
assert!(
nodes
.get(3)
.and_then(crate::calcit::data_shape::DataShapeGraph::from_calcit_handle)
.is_some(),
"preprocessing should retain the compiled graph"
);
}
#[test]
fn warns_on_dynamic_postfix_method_when_enabled() {
let _lock = lock_preprocess_test_state();
let _warn_guard = WarnDynMethodGuard::new(true);
let expr = Cirru::List(vec![Cirru::leaf("receiver"), Cirru::leaf(".show")]);
let code = code_to_calcit(&expr, "tests.dynamic-postfix", "main", vec![]).expect("parse cirru");
let mut scope_defs: HashSet<Arc<str>> = HashSet::new();
scope_defs.insert(Arc::from("receiver"));
let mut scope_types: ScopeTypes = ScopeTypes::new();
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.dynamic-postfix", &warnings, &stack)
.expect("dynamic postfix call should continue preprocessing");
let warnings = warnings.borrow();
let matched = warnings
.iter()
.filter(|warning| warning.code() == Some("P_DYNAMIC_POSTFIX_METHOD"))
.collect::<Vec<_>>();
assert_eq!(matched.len(), 1, "expected one dynamic postfix warning, got: {warnings:?}");
assert!(matched[0].message().contains("unsafe-coerce"));
}
#[test]
fn parses_optional_type_annotation() {
let expr = Cirru::List(vec![
Cirru::leaf("assert-type"),
Cirru::leaf("x"),
Cirru::List(vec![Cirru::leaf("::"), Cirru::leaf(":optional"), Cirru::leaf(":string")]),
]);
let code = code_to_calcit(&expr, "tests.assert", "main", vec![]).expect("parse cirru");
let mut scope_defs: HashSet<Arc<str>> = HashSet::new();
scope_defs.insert(Arc::from("x"));
let mut scope_types: ScopeTypes = ScopeTypes::new();
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let resolved =
preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.assert", &warnings, &stack).expect("preprocess assert-type");
assert!(matches!(resolved, Calcit::Local(_)), "local assert-type should return typed local");
if let Some(type_val) = scope_types.get("x") {
match type_val.as_ref() {
CalcitTypeAnnotation::Optional(inner) => {
assert!(
matches!(inner.as_ref(), CalcitTypeAnnotation::String),
"optional inner type should be :string"
);
}
other => panic!("expected optional type annotation, got {other:?}"),
}
}
}
#[test]
fn parses_js_nullish_type_annotation_without_treating_it_as_optional() {
let expr = Cirru::List(vec![
Cirru::leaf("assert-type"),
Cirru::leaf("x"),
Cirru::List(vec![Cirru::leaf("::"), Cirru::leaf(":js-nullish"), Cirru::leaf(":js-object")]),
]);
let code = code_to_calcit(&expr, "tests.assert", "main", vec![]).expect("parse cirru");
let mut scope_defs: HashSet<Arc<str>> = HashSet::new();
scope_defs.insert(Arc::from("x"));
let mut scope_types: ScopeTypes = ScopeTypes::new();
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.assert", &warnings, &stack).expect("preprocess assert-type");
assert!(matches!(
scope_types.get("x").map(AsRef::as_ref),
Some(CalcitTypeAnnotation::JsNullish(inner)) if matches!(inner.as_ref(), CalcitTypeAnnotation::JsObject)
));
}
#[test]
fn warns_on_invalid_optional_arity() {
let expr = Cirru::List(vec![
Cirru::leaf("assert-type"),
Cirru::leaf("x"),
Cirru::List(vec![
Cirru::leaf("::"),
Cirru::leaf(":optional"),
Cirru::leaf(":string"),
Cirru::leaf(":extra"),
]),
]);
let code = code_to_calcit(&expr, "tests.assert", "main", vec![]).expect("parse cirru");
let mut scope_defs: HashSet<Arc<str>> = HashSet::new();
scope_defs.insert(Arc::from("x"));
let mut scope_types: ScopeTypes = ScopeTypes::new();
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let _resolved =
preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.assert", &warnings, &stack).expect("preprocess assert-type");
if let Some(type_val) = scope_types.get("x") {
match type_val.as_ref() {
CalcitTypeAnnotation::Optional(inner) => {
assert!(
matches!(inner.as_ref(), CalcitTypeAnnotation::String),
"should still parse the first argument as inner type even if arity is wrong"
);
}
other => panic!("expected optional type annotation, got {other:?}"),
}
}
}
#[test]
fn warns_on_optional_type_mismatch() {
let expr = Cirru::List(vec![
Cirru::leaf("&let"),
Cirru::List(vec![Cirru::leaf("x"), Cirru::leaf("nil")]),
Cirru::List(vec![
Cirru::leaf("assert-type"),
Cirru::leaf("x"),
Cirru::List(vec![Cirru::leaf("::"), Cirru::leaf(":optional"), Cirru::leaf(":number")]),
]),
Cirru::List(vec![Cirru::leaf("&+"), Cirru::leaf("x"), Cirru::leaf("1")]),
]);
let code = code_to_calcit(&expr, "tests.optional", "demo", vec![]).expect("parse cirru");
let mut scope_defs: HashSet<Arc<str>> = HashSet::new();
scope_defs.insert(Arc::from("x"));
let mut scope_types: ScopeTypes = ScopeTypes::new();
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let _resolved =
preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.optional", &warnings, &stack).expect("preprocess optional");
let warnings_vec = warnings.borrow();
assert!(!warnings_vec.is_empty(), "should warn on optional mismatch");
let warning_msg = warnings_vec[0].to_string();
assert!(
warning_msg.contains("Proc `&+` arg 1 expects type `:number`"),
"warning should mention proc arg mismatch: {warning_msg}"
);
assert!(
warning_msg.contains(":number?"),
"warning should mention optional actual type: {warning_msg}"
);
}
#[test]
fn propagates_type_info_across_scope() {
let expr = Cirru::List(vec![
Cirru::leaf("&let"),
Cirru::List(vec![Cirru::leaf("x"), Cirru::leaf("1")]),
Cirru::List(vec![Cirru::leaf("assert-type"), Cirru::leaf("x"), Cirru::leaf(":fn")]),
Cirru::leaf("x"),
]);
let code = code_to_calcit(&expr, "tests.assert", "demo", vec![]).expect("parse cirru");
let scope_defs: HashSet<Arc<str>> = HashSet::new();
let mut scope_types: ScopeTypes = ScopeTypes::new();
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let resolved =
preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.assert", &warnings, &stack).expect("preprocess assert-type");
let nodes = match resolved {
Calcit::List(xs) => xs.to_vec(),
other => panic!("expected list, got {other}"),
};
let assert_typed_result = nodes.get(2);
assert!(
matches!(assert_typed_result, Some(Calcit::Local(_))),
"local assert-type should be preprocessed into typed local"
);
if let Some(Calcit::Local(local)) = nodes.get(3) {
assert!(
!matches!(*local.type_info, CalcitTypeAnnotation::Dynamic),
"type info should persist for later usages"
);
assert!(matches!(local.type_info.as_ref(), CalcitTypeAnnotation::DynFn), "type should be fn");
} else {
panic!("expected trailing local expression");
}
}
#[test]
fn passes_assert_type_expression_without_local_binding() {
let expr = Cirru::List(vec![
Cirru::leaf("assert-type"),
Cirru::List(vec![Cirru::leaf("&+"), Cirru::leaf("1"), Cirru::leaf("2")]),
Cirru::leaf(":number"),
]);
let code = code_to_calcit(&expr, "tests.assert", "expr", vec![]).expect("parse cirru");
let scope_defs: HashSet<Arc<str>> = HashSet::new();
let mut scope_types: ScopeTypes = ScopeTypes::new();
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let resolved =
preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.assert", &warnings, &stack).expect("preprocess assert-type");
let nodes = match resolved {
Calcit::List(xs) => xs.to_vec(),
other => panic!("assert-type should remain syntax form, got {other}"),
};
assert!(
matches!(nodes.first(), Some(Calcit::Syntax(CalcitSyntax::AssertType, _))),
"assert-type head should remain syntax"
);
assert!(scope_types.is_empty(), "expression assert-type should not mutate local scope types");
}
#[test]
fn passes_assert_traits_expression_without_local_binding() {
let expr = Cirru::List(vec![
Cirru::leaf("assert-traits"),
Cirru::List(vec![Cirru::leaf("&+"), Cirru::leaf("1"), Cirru::leaf("2")]),
Cirru::leaf("Show"),
]);
let code = code_to_calcit(&expr, "tests.assert", "expr", vec![]).expect("parse cirru");
let scope_defs: HashSet<Arc<str>> = HashSet::new();
let mut scope_types: ScopeTypes = ScopeTypes::new();
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let resolved =
preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.assert", &warnings, &stack).expect("preprocess assert-traits");
match resolved {
Calcit::List(xs) => {
assert!(
matches!(xs.first(), Some(Calcit::Proc(CalcitProc::NativeAssertTraits))),
"assert-traits expression should compile to runtime assert proc"
);
}
other => panic!("assert-traits expression should be preserved for runtime check, got {other}"),
}
assert!(
scope_types.is_empty(),
"expression assert-traits should not mutate local scope types"
);
}
#[test]
fn lookup_trait_for_preprocess_reads_source_backed_trait_without_runtime_value() {
let _guard = lock_preprocess_test_state();
let trait_code = code_to_calcit(
&Cirru::List(vec![
Cirru::leaf("deftrait"),
Cirru::leaf("MySourceTrait"),
Cirru::List(vec![Cirru::leaf(".show"), Cirru::leaf(":fn")]),
]),
"tests.source-trait",
"MySourceTrait",
vec![],
)
.expect("parse trait def");
let mut program_code = program::PROGRAM_CODE_DATA.write().expect("open program code");
program_code.insert(
Arc::from("tests.source-trait"),
program::ProgramFileData {
import_map: HashMap::new(),
defs: HashMap::from([(
Arc::from("MySourceTrait"),
program::ProgramDefEntry {
code: trait_code,
schema: calcit::DYNAMIC_TYPE.clone(),
doc: Arc::from(""),
examples: vec![],
ffi: None,
},
)]),
},
);
drop(program_code);
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let trait_def = lookup_trait_ns_def_for_preprocess("tests.source-trait", "MySourceTrait", &warnings, &stack)
.expect("lookup trait")
.expect("trait should resolve from source-backed compiled data");
assert_eq!(trait_def.name.ref_str(), "MySourceTrait");
assert_eq!(trait_def.definition_ref.as_deref(), Some("tests.source-trait/MySourceTrait"));
assert!(trait_def.has_method("show"));
}
fn seed_external_field_trait() -> Arc<CalcitTrait> {
let ns = "tests.external-field";
let def = "HostElement";
let trait_code = code_to_calcit(
&Cirru::List(vec![
Cirru::leaf("deftrait"),
Cirru::leaf(def),
Cirru::List(vec![Cirru::leaf(":value"), Cirru::leaf("'String")]),
]),
ns,
def,
vec![],
)
.expect("parse external trait");
let trait_def = resolve_trait_def_from_source_code(&trait_code)
.expect("resolve external trait")
.with_definition_ref(ns, def);
program::PROGRAM_CODE_DATA.write().expect("open program code").insert(
Arc::from(ns),
program::ProgramFileData {
import_map: HashMap::new(),
defs: HashMap::from([(
Arc::from(def),
program::ProgramDefEntry {
code: trait_code,
schema: calcit::DYNAMIC_TYPE.clone(),
doc: Arc::from(""),
examples: vec![],
ffi: Some(cirru_edn::Edn::map_from_iter([
(cirru_edn::Edn::tag("backend"), cirru_edn::Edn::tag("js")),
(cirru_edn::Edn::tag("kind"), cirru_edn::Edn::tag("external-object")),
])),
},
)]),
},
);
Arc::new(trait_def)
}
fn external_field_test_symbol(name: &str) -> Calcit {
Calcit::Symbol {
sym: Arc::from(name),
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("tests.external-field"),
at_def: Arc::from("demo"),
}),
location: None,
}
}
fn external_field_test_receiver(trait_def: Arc<CalcitTrait>) -> Calcit {
let sym: Arc<str> = Arc::from("element");
Calcit::Local(CalcitLocal {
idx: CalcitLocal::track_sym(&sym),
sym,
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("tests.external-field"),
at_def: Arc::from("demo"),
}),
location: None,
type_info: Arc::new(CalcitTypeAnnotation::Trait(trait_def)),
})
}
#[test]
fn js_get_infers_external_trait_field_payload() {
let _guard = lock_preprocess_test_state();
let receiver = external_field_test_receiver(seed_external_field_trait());
let expression = Calcit::from(vec![
external_field_test_symbol("js-get"),
receiver,
Calcit::Tag(EdnTag::new("value")),
]);
assert!(matches!(
infer_static_type_from_expr(&expression).as_deref(),
Some(CalcitTypeAnnotation::JsNullish(inner)) if matches!(inner.as_ref(), CalcitTypeAnnotation::String)
));
let receiver = external_field_test_receiver(seed_external_field_trait());
let rewritten = rewrite_typed_js_field_operation(
&external_field_test_symbol("js-get"),
&CalcitList::from(&[receiver, Calcit::Tag(EdnTag::new("value"))]),
&ScopeTypes::new(),
)
.expect("typed js-get should rewrite");
assert!(matches!(
rewritten,
Calcit::List(items)
if matches!(items.first(), Some(Calcit::Method(name, calcit::MethodKind::ExternalGet(_))) if name.as_ref() == "value")
));
}
#[test]
fn js_set_checks_external_trait_static_fields() {
let _guard = lock_preprocess_test_state();
let receiver = external_field_test_receiver(seed_external_field_trait());
let head = external_field_test_symbol("js-set");
let valid_warnings = RefCell::new(vec![]);
check_typed_js_field_operation(
&head,
&CalcitList::from(&[receiver.clone(), Calcit::Tag(EdnTag::new("value")), Calcit::Str(Arc::from("ok"))]),
&ScopeTypes::new(),
"tests.external-field",
"valid",
&valid_warnings,
);
assert!(valid_warnings.borrow().is_empty());
let rewritten = rewrite_typed_js_field_operation(
&head,
&CalcitList::from(&[receiver.clone(), Calcit::Tag(EdnTag::new("value")), Calcit::Str(Arc::from("ok"))]),
&ScopeTypes::new(),
)
.expect("typed js-set should rewrite");
assert!(matches!(
rewritten,
Calcit::List(items)
if matches!(items.first(), Some(Calcit::Method(name, calcit::MethodKind::ExternalSet(_))) if name.as_ref() == "value")
));
let unknown_warnings = RefCell::new(vec![]);
check_typed_js_field_operation(
&head,
&CalcitList::from(&[receiver.clone(), Calcit::Tag(EdnTag::new("missing")), Calcit::Str(Arc::from("x"))]),
&ScopeTypes::new(),
"tests.external-field",
"unknown",
&unknown_warnings,
);
assert_eq!(unknown_warnings.borrow()[0].code(), Some("W_JS_FFI_UNKNOWN_FIELD"));
let mismatch_warnings = RefCell::new(vec![]);
check_typed_js_field_operation(
&head,
&CalcitList::from(&[receiver, Calcit::Tag(EdnTag::new("value")), Calcit::Number(1.0)]),
&ScopeTypes::new(),
"tests.external-field",
"mismatch",
&mismatch_warnings,
);
assert_eq!(mismatch_warnings.borrow()[0].code(), Some("W_JS_FFI_FIELD_TYPE_MISMATCH"));
}
fn untyped_js_ffi_test_receiver() -> Calcit {
let sym: Arc<str> = Arc::from("host");
Calcit::Local(CalcitLocal {
idx: CalcitLocal::track_sym(&sym),
sym,
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("tests.untyped-ffi"),
at_def: Arc::from("demo"),
}),
location: None,
type_info: Arc::new(CalcitTypeAnnotation::JsObject),
})
}
#[test]
fn warns_on_untyped_js_ffi_field_access_when_enabled() {
let _lock = lock_preprocess_test_state();
let _warn_guard = WarnDynMethodGuard::new(true);
let receiver = untyped_js_ffi_test_receiver();
let head = Calcit::Method(Arc::from("value"), calcit::MethodKind::Access);
let warnings = RefCell::new(vec![]);
warn_on_untyped_js_ffi_field_access(
&head,
&CalcitList::from(std::slice::from_ref(&receiver)),
&ScopeTypes::new(),
"tests.untyped-ffi",
"demo",
&warnings,
);
assert_eq!(warnings.borrow()[0].code(), Some("W_JS_FFI_UNTYPED_ACCESS"));
}
#[test]
fn untyped_js_ffi_field_access_warning_is_opt_in_and_scoped() {
let _lock = lock_preprocess_test_state();
let receiver = untyped_js_ffi_test_receiver();
let head = Calcit::Method(Arc::from("value"), calcit::MethodKind::Access);
let disabled_warnings = RefCell::new(vec![]);
warn_on_untyped_js_ffi_field_access(
&head,
&CalcitList::from(std::slice::from_ref(&receiver)),
&ScopeTypes::new(),
"tests.untyped-ffi",
"demo",
&disabled_warnings,
);
assert!(disabled_warnings.borrow().is_empty());
let _warn_guard = WarnDynMethodGuard::new(true);
let dynamic_key_warnings = RefCell::new(vec![]);
warn_on_untyped_js_ffi_field_access(
&external_field_test_symbol("aget"),
&CalcitList::from(&[receiver.clone(), external_field_test_symbol("k")]),
&ScopeTypes::new(),
"tests.untyped-ffi",
"demo",
&dynamic_key_warnings,
);
assert!(dynamic_key_warnings.borrow().is_empty());
let typed_receiver = external_field_test_receiver(seed_external_field_trait());
let typed_warnings = RefCell::new(vec![]);
warn_on_untyped_js_ffi_field_access(
&external_field_test_symbol("aget"),
&CalcitList::from(&[typed_receiver, Calcit::Tag(EdnTag::new("value"))]),
&ScopeTypes::new(),
"tests.untyped-ffi",
"demo",
&typed_warnings,
);
assert!(typed_warnings.borrow().is_empty());
}
#[test]
fn named_function_schema_types_are_visible_inside_the_body() {
let _guard = lock_preprocess_test_state();
let ns = "tests.named-schema-body";
let def = "add-one";
let source_code = code_to_calcit(
&Cirru::List(vec![
Cirru::leaf("defn"),
Cirru::leaf(def),
Cirru::List(vec![Cirru::leaf("x")]),
Cirru::List(vec![Cirru::leaf("&+"), Cirru::leaf("x"), Cirru::leaf("1")]),
]),
ns,
def,
vec![],
)
.expect("parse typed function");
let number_type = Arc::new(CalcitTypeAnnotation::Number);
let schema = Arc::new(CalcitTypeAnnotation::from_function_parts(vec![number_type.clone()], number_type));
let mut program_code = program::PROGRAM_CODE_DATA.write().expect("open program code");
program_code.insert(
Arc::from(ns),
program::ProgramFileData {
import_map: HashMap::new(),
defs: HashMap::from([(
Arc::from(def),
program::ProgramDefEntry {
code: source_code,
schema,
doc: Arc::from(""),
examples: vec![],
ffi: None,
},
)]),
},
);
drop(program_code);
let warnings = RefCell::new(vec![]);
compile_source_def_for_snapshot(ns, def, &warnings, &CallStackList::default()).expect("compile typed source function");
let compiled = program::lookup_compiled_def(ns, def).expect("compiled output");
let Calcit::List(defn_nodes) = compiled.preprocessed_code else {
panic!("expected preprocessed defn");
};
let Calcit::List(params) = defn_nodes.get(2).expect("parameter list") else {
panic!("expected parameter list");
};
let Some(Calcit::Local(param)) = params.first() else {
panic!("expected local parameter");
};
assert!(matches!(param.type_info.as_ref(), CalcitTypeAnnotation::Number));
let Calcit::List(body) = defn_nodes.get(defn_nodes.len() - 1).expect("function body") else {
panic!("expected call body");
};
let Some(Calcit::Local(reference)) = body.get(1) else {
panic!("expected local reference in body");
};
assert!(matches!(reference.type_info.as_ref(), CalcitTypeAnnotation::Number));
assert!(warnings.borrow().is_empty(), "typed body should not emit warnings");
}
#[test]
fn infers_imported_generic_return_type_from_compiled_function_without_runtime_ready() {
let _guard = lock_preprocess_test_state();
let ns = "tests.generic-infer";
let def = "identity";
let def_id = program::lookup_def_id(ns, def).unwrap_or_else(|| {
program::mark_runtime_def_cold(ns, def);
program::lookup_def_id(ns, def).expect("register def id")
});
let generic_name: Arc<str> = Arc::from("T");
program::write_compiled_def(
ns,
def,
program::CompiledDef {
def_id,
version_id: 0,
kind: program::CompiledDefKind::Fn,
preprocessed_code: Calcit::Fn {
id: Arc::from("tests.generic-infer/identity"),
info: Arc::new(CalcitFn {
name: Arc::from(def),
def_ns: Arc::from(ns),
def_ref: None,
usage: CalcitFnUsageMeta::default(),
scope: Arc::new(CalcitScope::default()),
args: Arc::new(CalcitFnArgs::Args(vec![CalcitLocal::track_sym(&Arc::from("x"))])),
body: vec![],
generics: Arc::new(vec![generic_name.clone()]),
where_bounds: Arc::new(vec![]),
return_type: Arc::new(CalcitTypeAnnotation::TypeVar(generic_name.clone())),
arg_types: vec![Arc::new(CalcitTypeAnnotation::TypeVar(generic_name))],
rest_type: None,
}),
},
codegen_form: Calcit::Nil,
deps: vec![],
type_summary: None,
source_code: None,
schema: calcit::DYNAMIC_TYPE.clone(),
doc: Arc::from(""),
examples: vec![],
},
);
let call = Calcit::List(Arc::new(CalcitList::from(
&[
Calcit::Import(CalcitImport {
ns: Arc::from(ns),
def: Arc::from(def),
info: Arc::new(ImportInfo::NsReferDef {
at_ns: Arc::from("tests.caller"),
at_def: Arc::from("demo"),
}),
def_id: Some(def_id.0),
}),
Calcit::Number(1.0),
][..],
)));
let inferred = infer_type_from_expr(&call, &ScopeTypes::new()).expect("infer import call type");
assert!(matches!(inferred.as_ref(), CalcitTypeAnnotation::Number));
let call = Calcit::List(Arc::new(CalcitList::from(
&[
Calcit::Symbol {
sym: Arc::from(def),
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from(ns),
at_def: Arc::from("demo"),
}),
location: None,
},
Calcit::Number(1.0),
][..],
)));
let inferred = infer_type_from_expr(&call, &ScopeTypes::new()).expect("infer symbol call type");
assert!(matches!(inferred.as_ref(), CalcitTypeAnnotation::Number));
}
#[test]
fn ensure_ns_def_compiled_refreshes_source_backed_output_even_when_runtime_is_ready() {
let _guard = lock_preprocess_test_state();
let ns = "tests.runtime-shortcut";
let def = "value";
let source_code = Calcit::Number(1.0);
let mut program_code = program::PROGRAM_CODE_DATA.write().expect("open program code");
program_code.insert(
Arc::from(ns),
program::ProgramFileData {
import_map: HashMap::new(),
defs: HashMap::from([(
Arc::from(def),
program::ProgramDefEntry {
code: source_code,
schema: calcit::DYNAMIC_TYPE.clone(),
doc: Arc::from(""),
examples: vec![],
ffi: None,
},
)]),
},
);
drop(program_code);
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
program::write_runtime_ready(ns, def, Calcit::Number(99.0)).expect("seed stale runtime value");
ensure_ns_def_compiled(ns, def, &warnings, &stack).expect("compile source-backed def with ready runtime cell");
let compiled = program::lookup_compiled_def(ns, def).expect("compiled output should exist");
assert_eq!(compiled.preprocessed_code, Calcit::Number(1.0));
}
#[test]
fn ensure_ns_def_compiled_handles_recursive_source_with_compile_guard() {
let _guard = lock_preprocess_test_state();
let ns = "tests.recursive-compile";
let def = "loop";
let recursive_code = code_to_calcit(
&Cirru::List(vec![
Cirru::leaf("defn"),
Cirru::leaf(def),
Cirru::List(vec![]),
Cirru::List(vec![Cirru::leaf(def)]),
]),
ns,
def,
vec![],
)
.expect("parse recursive fn");
let mut program_code = program::PROGRAM_CODE_DATA.write().expect("open program code");
program_code.insert(
Arc::from(ns),
program::ProgramFileData {
import_map: HashMap::new(),
defs: HashMap::from([(
Arc::from(def),
program::ProgramDefEntry {
code: recursive_code,
schema: calcit::DYNAMIC_TYPE.clone(),
doc: Arc::from(""),
examples: vec![],
ffi: None,
},
)]),
},
);
drop(program_code);
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
ensure_ns_def_compiled(ns, def, &warnings, &stack).expect("compile recursive source def");
assert!(
program::lookup_compiled_def(ns, def).is_some(),
"recursive source def should compile once"
);
}
#[test]
fn validates_struct_field_access() {
use cirru_edn::EdnTag;
let test_struct = Arc::new(CalcitTypeAnnotation::StructValue(Arc::new(CalcitStructDef::from_fields(
EdnTag::from("Person"),
vec![EdnTag::from("age"), EdnTag::from("name")],
))));
let expr = Cirru::List(vec![
Cirru::leaf("&let"),
Cirru::List(vec![Cirru::leaf("user"), Cirru::leaf("nil")]),
Cirru::List(vec![
Cirru::leaf("assert-type"),
Cirru::leaf("user"),
Cirru::leaf("struct-type"), ]),
Cirru::List(vec![Cirru::leaf("&struct:get"), Cirru::leaf("user"), Cirru::leaf(":name")]),
]);
let code = code_to_calcit(&expr, "tests.struct", "demo", vec![]).expect("parse cirru");
let scope_defs: HashSet<Arc<str>> = HashSet::new();
let mut scope_types: ScopeTypes = ScopeTypes::new();
scope_types.insert(Arc::from("user"), test_struct.clone());
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let _resolved =
preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.struct", &warnings, &stack).expect("preprocess should succeed");
}
#[test]
fn common_get_reports_missing_known_struct_field_during_preprocess() {
let struct_type = Arc::new(CalcitTypeAnnotation::StructValue(Arc::new(CalcitStructDef::from_fields(
EdnTag::from("Person"),
vec![EdnTag::from("name")],
))));
let mut scope_types = ScopeTypes::new();
scope_types.insert(Arc::from("person"), struct_type);
let head = Calcit::Import(CalcitImport {
ns: Arc::from(calcit::CORE_NS),
def: Arc::from("get"),
info: Arc::new(ImportInfo::Core {
at_ns: Arc::from("tests.struct"),
}),
def_id: None,
});
let args = CalcitList::from(
&[
Calcit::Symbol {
sym: Arc::from("person"),
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("tests.struct"),
at_def: Arc::from("demo"),
}),
location: None,
},
Calcit::Tag(EdnTag::from("missing")),
][..],
);
let warnings = RefCell::new(vec![]);
check_struct_field_access(&head, &args, &scope_types, "tests.struct", &warnings);
let warnings = warnings.borrow();
assert_eq!(warnings.len(), 1);
let message = warnings[0].message();
assert!(message.contains("Field `:missing` does not exist in struct `Person`"));
assert!(message.contains("never returns nil/Option"));
}
#[test]
fn postfix_nominal_struct_access_uses_direct_field_lookup() {
let expr = Cirru::List(vec![Cirru::leaf("person"), Cirru::leaf(":name")]);
let code = code_to_calcit(&expr, "tests.struct", "demo", vec![]).expect("parse postfix field access");
let mut scope_defs = HashSet::new();
scope_defs.insert(Arc::from("person"));
let mut scope_types = ScopeTypes::new();
scope_types.insert(
Arc::from("person"),
Arc::new(CalcitTypeAnnotation::StructValue(Arc::new(CalcitStructDef::from_fields(
EdnTag::from("Person"),
vec![EdnTag::from("name")],
)))),
);
let warnings = RefCell::new(vec![]);
let resolved = preprocess_expr(
&code,
&scope_defs,
&mut scope_types,
"tests.struct",
&warnings,
&CallStackList::default(),
)
.expect("preprocess nominal postfix field access");
let Calcit::List(items) = resolved else {
panic!("expected specialized field access call");
};
assert!(
matches!(items.first(), Some(Calcit::Proc(CalcitProc::NativeStructNth))),
"nominal field access should bypass Option-producing get: {items}"
);
}
#[test]
fn postfix_loose_struct_access_uses_required_struct_get() {
let expr = Cirru::List(vec![Cirru::leaf("record"), Cirru::leaf(":name")]);
let code = code_to_calcit(&expr, "tests.struct", "demo", vec![]).expect("parse postfix field access");
let mut scope_defs = HashSet::new();
scope_defs.insert(Arc::from("record"));
let mut scope_types = ScopeTypes::new();
scope_types.insert(Arc::from("record"), tag_annotation("record"));
let warnings = RefCell::new(vec![]);
let resolved = preprocess_expr(
&code,
&scope_defs,
&mut scope_types,
"tests.struct",
&warnings,
&CallStackList::default(),
)
.expect("preprocess loose record postfix field access");
let Calcit::List(items) = resolved else {
panic!("expected required record get call");
};
assert!(
matches!(items.first(), Some(Calcit::Proc(CalcitProc::NativeStructGet))),
"loose record access should never use Option-producing get: {items}"
);
}
#[test]
fn rewrites_struct_head_call_to_struct_ctor() {
use crate::data::cirru::code_to_calcit;
use cirru_edn::EdnTag;
use cirru_parser::Cirru;
let person_struct = CalcitStructDef::from_fields(EdnTag::from("Person"), vec![EdnTag::from("name"), EdnTag::from("age")]);
let expr = Cirru::List(vec![
Cirru::leaf("Person"),
Cirru::leaf(":name"),
Cirru::leaf("|Alice"),
Cirru::leaf(":age"),
Cirru::leaf("20"),
]);
let parsed = code_to_calcit(&expr, "tests.struct", "demo", vec![]).expect("parse struct ctor");
let Calcit::List(parsed_items) = parsed else {
panic!("expected parsed call");
};
let mut code_items = parsed_items.to_vec();
code_items[0] = Calcit::StructDef(person_struct.clone());
let code = Calcit::from(code_items);
let mut scope_defs: HashSet<Arc<str>> = HashSet::new();
scope_defs.insert(Arc::from("Person"));
let mut scope_types: ScopeTypes = ScopeTypes::new();
scope_types.insert(
Arc::from("Person"),
Arc::new(CalcitTypeAnnotation::Struct(Arc::new(person_struct.clone()), Arc::new(vec![]))),
);
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let result =
preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.struct", &warnings, &stack).expect("preprocess struct head call");
let items = match result {
Calcit::List(xs) => xs.to_vec(),
other => panic!("expected list form, got {other}"),
};
assert!(matches!(items.first(), Some(Calcit::Proc(CalcitProc::NativeStruct))));
match items.get(1) {
Some(Calcit::StructDef(struct_def)) => assert_eq!(struct_def.name, person_struct.name),
other => panic!("expected struct prototype at position 1, got {other:?}"),
}
assert_eq!(*items.get(2).expect("name field key"), Calcit::Tag(EdnTag::from("name")));
assert_eq!(*items.get(4).expect("age field key"), Calcit::Tag(EdnTag::from("age")));
assert_eq!(*items.get(3).expect("name value"), Calcit::Str(Arc::from("Alice")));
assert_eq!(*items.get(5).expect("age value"), Calcit::Number(20.0));
}
#[test]
fn rewrites_enum_head_call_to_enum_ctor() {
use crate::calcit::CalcitEnumDef;
use crate::data::cirru::code_to_calcit;
use cirru_edn::EdnTag;
use cirru_parser::Cirru;
let enum_struct = CalcitStructValue {
struct_ref: Arc::new(CalcitStructDef::from_fields(
EdnTag::from("Result"),
vec![EdnTag::from("ok"), EdnTag::from("err")],
)),
values: Arc::new(vec![
Calcit::List(Arc::new(CalcitList::default())),
Calcit::List(Arc::new(CalcitList::default())),
]),
};
let result_enum = CalcitEnumDef::from_struct(enum_struct.clone()).expect("valid result enum");
let expr = Cirru::List(vec![Cirru::leaf("Result"), Cirru::leaf(":ok")]);
let parsed = code_to_calcit(&expr, "tests.struct", "demo", vec![]).expect("parse enum ctor");
let Calcit::List(parsed_items) = parsed else {
panic!("expected parsed call");
};
let mut code_items = parsed_items.to_vec();
code_items[0] = Calcit::EnumDef(result_enum.clone());
let code = Calcit::from(code_items);
let mut scope_defs: HashSet<Arc<str>> = HashSet::new();
scope_defs.insert(Arc::from("Result"));
let mut scope_types: ScopeTypes = ScopeTypes::new();
scope_types.insert(
Arc::from("Result"),
Arc::new(CalcitTypeAnnotation::Enum(Arc::new(result_enum.clone()), Arc::new(vec![]))),
);
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let result =
preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.struct", &warnings, &stack).expect("preprocess enum head call");
let items = match result {
Calcit::List(xs) => xs.to_vec(),
other => panic!("expected list form, got {other}"),
};
assert!(matches!(items.first(), Some(Calcit::Proc(CalcitProc::NativeNamedEnumNew))));
match items.get(1) {
Some(Calcit::Struct(enum_struct)) => assert_eq!(enum_struct.struct_ref.name, *result_enum.name()),
other => panic!("expected enum prototype at position 1, got {other:?}"),
}
assert_eq!(*items.get(2).expect("tag key"), Calcit::Tag(EdnTag::from("ok")));
assert_eq!(items.len(), 3);
}
#[test]
fn rejects_struct_head_call_with_odd_args() {
use crate::data::cirru::code_to_calcit;
use cirru_edn::EdnTag;
use cirru_parser::Cirru;
let person_struct = CalcitStructDef::from_fields(EdnTag::from("Person"), vec![EdnTag::from("name"), EdnTag::from("age")]);
let expr = Cirru::List(vec![Cirru::leaf("Person"), Cirru::leaf(":name")]);
let parsed = code_to_calcit(&expr, "tests.struct", "demo", vec![]).expect("parse struct ctor");
let Calcit::List(parsed_items) = parsed else {
panic!("expected parsed call");
};
let mut code_items = parsed_items.to_vec();
code_items[0] = Calcit::StructDef(person_struct.clone());
let code = Calcit::from(code_items);
let mut scope_defs: HashSet<Arc<str>> = HashSet::new();
scope_defs.insert(Arc::from("Person"));
let mut scope_types: ScopeTypes = ScopeTypes::new();
scope_types.insert(
Arc::from("Person"),
Arc::new(CalcitTypeAnnotation::Struct(Arc::new(person_struct.clone()), Arc::new(vec![]))),
);
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let result =
preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.struct", &warnings, &stack).expect("preprocess struct head call");
let items = match result {
Calcit::List(xs) => xs.to_vec(),
other => panic!("expected list form, got {other}"),
};
assert!(
!matches!(items.first(), Some(Calcit::Proc(CalcitProc::NativeStruct))),
"should not rewrite when struct constructor args are odd"
);
let warnings_vec = warnings.borrow();
assert!(!warnings_vec.is_empty(), "should warn on odd key/value arguments");
assert!(
warnings_vec.iter().any(|w| w.to_string().contains("expected key/value pairs")),
"warning should mention expected key/value pairs"
);
}
#[test]
fn rejects_struct_head_call_with_unknown_field() {
use crate::data::cirru::code_to_calcit;
use cirru_edn::EdnTag;
use cirru_parser::Cirru;
let person_struct = CalcitStructDef::from_fields(EdnTag::from("Person"), vec![EdnTag::from("name"), EdnTag::from("age")]);
let expr = Cirru::List(vec![
Cirru::leaf("Person"),
Cirru::leaf(":email"),
Cirru::leaf("|alice@example.com"),
Cirru::leaf(":age"),
Cirru::leaf("20"),
]);
let parsed = code_to_calcit(&expr, "tests.struct", "demo", vec![]).expect("parse struct ctor");
let Calcit::List(parsed_items) = parsed else {
panic!("expected parsed call");
};
let mut code_items = parsed_items.to_vec();
code_items[0] = Calcit::StructDef(person_struct.clone());
let code = Calcit::from(code_items);
let mut scope_defs: HashSet<Arc<str>> = HashSet::new();
scope_defs.insert(Arc::from("Person"));
let mut scope_types: ScopeTypes = ScopeTypes::new();
scope_types.insert(
Arc::from("Person"),
Arc::new(CalcitTypeAnnotation::Struct(Arc::new(person_struct.clone()), Arc::new(vec![]))),
);
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let result =
preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.struct", &warnings, &stack).expect("preprocess struct head call");
let items = match result {
Calcit::List(xs) => xs.to_vec(),
other => panic!("expected list form, got {other}"),
};
assert!(
!matches!(items.first(), Some(Calcit::Proc(CalcitProc::NativeStruct))),
"should not rewrite when key not in struct fields"
);
let warnings_vec = warnings.borrow();
assert!(!warnings_vec.is_empty(), "should warn on unknown struct field");
assert!(
warnings_vec.iter().any(|w| w.to_string().contains(":email")),
"warning should mention unknown field"
);
}
#[test]
fn rejects_struct_head_call_with_duplicate_or_missing_required_field() {
use cirru_edn::EdnTag;
let person_struct = CalcitStructDef::from_fields(EdnTag::from("Person"), vec![EdnTag::from("name"), EdnTag::from("age")]);
let warnings = RefCell::new(vec![]);
let scope_types = ScopeTypes::new();
let stack = CallStackList::default();
let duplicate_args = CalcitList::from(
vec![
Calcit::Tag(EdnTag::from("name")),
Calcit::Str(Arc::from("Alice")),
Calcit::Tag(EdnTag::from("name")),
Calcit::Str(Arc::from("Bob")),
]
.as_slice(),
);
let duplicate_result = try_rewrite_struct_enum_constructor_head_call(
&Calcit::StructDef(person_struct.clone()),
&duplicate_args,
&scope_types,
"tests.struct",
"demo",
&warnings,
&stack,
)
.expect("check duplicate fields");
assert!(duplicate_result.is_none());
assert!(
warnings
.borrow()
.iter()
.any(|warning| warning.to_string().contains("duplicate field"))
);
warnings.borrow_mut().clear();
let missing_args = CalcitList::from(vec![Calcit::Tag(EdnTag::from("name")), Calcit::Str(Arc::from("Alice"))].as_slice());
let missing_result = try_rewrite_struct_enum_constructor_head_call(
&Calcit::StructDef(person_struct),
&missing_args,
&scope_types,
"tests.struct",
"demo",
&warnings,
&stack,
)
.expect("check missing fields");
assert!(missing_result.is_none());
assert!(
warnings
.borrow()
.iter()
.any(|warning| warning.to_string().contains("required field `:age` is missing"))
);
}
#[test]
fn struct_and_enum_instances_are_not_constructor_heads() {
use crate::calcit::{CalcitEnumDef, CalcitEnumValue};
use cirru_edn::EdnTag;
let person_struct = CalcitStructDef::from_fields(EdnTag::from("Person"), vec![EdnTag::from("name")]);
let person = Calcit::Struct(CalcitStructValue {
struct_ref: Arc::new(person_struct),
values: Arc::new(vec![Calcit::Str(Arc::from("Alice"))]),
});
let enum_struct = CalcitStructValue {
struct_ref: Arc::new(CalcitStructDef::from_fields(EdnTag::from("Result"), vec![EdnTag::from("ok")])),
values: Arc::new(vec![Calcit::List(Arc::new(CalcitList::default()))]),
};
let result_enum = CalcitEnumDef::from_struct(enum_struct).expect("valid enum");
let enum_value = Calcit::Enum(CalcitEnumValue {
tag: Arc::new(Calcit::Tag(EdnTag::from("ok"))),
extra: vec![],
sum_type: Some(Arc::new(result_enum)),
});
let args = CalcitList::from(vec![Calcit::Tag(EdnTag::from("name")), Calcit::Str(Arc::from("Bob"))].as_slice());
let scope_types = ScopeTypes::new();
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
assert!(
try_rewrite_struct_enum_constructor_head_call(&person, &args, &scope_types, "tests.struct", "demo", &warnings, &stack,)
.expect("record instance boundary")
.is_none()
);
assert!(
try_rewrite_struct_enum_constructor_head_call(&enum_value, &args, &scope_types, "tests.struct", "demo", &warnings, &stack,)
.expect("enum instance boundary")
.is_none()
);
assert!(warnings.borrow().is_empty());
}
#[test]
fn warns_on_struct_constructor_field_type_mismatch() {
use cirru_edn::EdnTag;
let mut point_struct = CalcitStructDef::from_fields(EdnTag::from("Point"), vec![EdnTag::from("x")]);
point_struct.field_types = Arc::new(vec![Arc::new(CalcitTypeAnnotation::Number)]);
let args = CalcitList::from(vec![Calcit::Tag(EdnTag::from("x")), Calcit::Str(Arc::from("wrong"))].as_slice());
let warnings = RefCell::new(vec![]);
let result = try_rewrite_struct_enum_constructor_head_call(
&Calcit::StructDef(point_struct),
&args,
&ScopeTypes::new(),
"tests.struct",
"demo",
&warnings,
&CallStackList::default(),
)
.expect("rewrite typed struct constructor");
assert!(result.is_some());
assert!(warnings.borrow().iter().any(|warning| {
let message = warning.to_string();
message.contains("field `:x`") && message.contains("expects type")
}));
}
#[test]
fn validates_typed_struct_update_fields_after_generic_substitution() {
use cirru_edn::EdnTag;
let generic: Arc<str> = Arc::from("T");
let mut box_struct = CalcitStructDef::from_fields(EdnTag::from("Box"), vec![EdnTag::from("value")]);
box_struct.generics = Arc::new(vec![generic.clone()]);
box_struct.field_types = Arc::new(vec![Arc::new(CalcitTypeAnnotation::TypeVar(generic))]);
let receiver = Calcit::Local(CalcitLocal {
idx: CalcitLocal::track_sym(&Arc::from("box")),
sym: Arc::from("box"),
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("tests.struct"),
at_def: Arc::from("demo"),
}),
location: None,
type_info: Arc::new(CalcitTypeAnnotation::Struct(
Arc::new(box_struct),
Arc::new(vec![Arc::new(CalcitTypeAnnotation::Number)]),
)),
});
let args = CalcitList::from(vec![receiver, Calcit::Tag(EdnTag::from("value")), Calcit::Str(Arc::from("wrong"))].as_slice());
let warnings = RefCell::new(vec![]);
check_struct_update_fields(
&Calcit::Proc(CalcitProc::NativeStructAssoc),
&args,
&ScopeTypes::new(),
"tests.struct",
"demo",
&warnings,
);
assert!(warnings.borrow().iter().any(|warning| {
let message = warning.to_string();
message.contains("struct update field `:value`") && message.contains("expects type `:number`")
}));
}
#[test]
fn preserves_struct_postfix_method_calls() {
use crate::calcit::MethodKind;
use crate::data::cirru::code_to_calcit;
use cirru_edn::EdnTag;
use cirru_parser::Cirru;
let cat_struct = CalcitStructDef::from_fields(EdnTag::from("Cat"), vec![EdnTag::from("name"), EdnTag::from("color")]);
let expr = Cirru::List(vec![Cirru::leaf("kitty"), Cirru::leaf(".rename"), Cirru::leaf("|LagopusB")]);
let code = code_to_calcit(&expr, "tests.struct", "demo", vec![]).expect("parse struct method call");
let mut scope_defs: HashSet<Arc<str>> = HashSet::new();
scope_defs.insert(Arc::from("kitty"));
let mut scope_types: ScopeTypes = ScopeTypes::new();
scope_types.insert(
Arc::from("kitty"),
Arc::new(CalcitTypeAnnotation::StructValue(Arc::new(cat_struct))),
);
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let result =
preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.struct", &warnings, &stack).expect("preprocess struct method call");
let nodes = match result {
Calcit::List(xs) => xs.to_vec(),
other => panic!("expected list form, got {other}"),
};
assert!(
!matches!(nodes.first(), Some(Calcit::Proc(CalcitProc::NativeStruct))),
"method-call path should not be rewritten as a struct constructor"
);
assert!(
matches!(nodes.first(), Some(Calcit::Method(_, MethodKind::Invoke(_)))),
"method-call should become typed method form"
);
assert!(warnings.borrow().is_empty(), "should not warn for valid method-call syntax");
}
#[test]
fn rejects_enum_head_call_with_missing_tag() {
use crate::calcit::{CalcitEnumDef, CalcitStructValue};
use crate::data::cirru::code_to_calcit;
use cirru_edn::EdnTag;
use cirru_parser::Cirru;
let enum_struct = CalcitStructValue {
struct_ref: Arc::new(CalcitStructDef::from_fields(
EdnTag::from("Result"),
vec![EdnTag::from("ok"), EdnTag::from("err")],
)),
values: Arc::new(vec![
Calcit::List(Arc::new(CalcitList::default())),
Calcit::List(Arc::new(CalcitList::default())),
]),
};
let result_enum = CalcitEnumDef::from_struct(enum_struct.clone()).expect("valid result enum");
let expr = Cirru::List(vec![Cirru::leaf("Result")]);
let parsed = code_to_calcit(&expr, "tests.struct", "demo", vec![]).expect("parse enum ctor");
let Calcit::List(parsed_items) = parsed else {
panic!("expected parsed call");
};
let mut code_items = parsed_items.to_vec();
code_items[0] = Calcit::EnumDef(result_enum.clone());
let code = Calcit::from(code_items);
let mut scope_defs: HashSet<Arc<str>> = HashSet::new();
scope_defs.insert(Arc::from("Result"));
let mut scope_types: ScopeTypes = ScopeTypes::new();
scope_types.insert(
Arc::from("Result"),
Arc::new(CalcitTypeAnnotation::Enum(Arc::new(result_enum.clone()), Arc::new(vec![]))),
);
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let result =
preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.struct", &warnings, &stack).expect("preprocess enum head call");
let items = match result {
Calcit::List(xs) => xs.to_vec(),
other => panic!("expected list form, got {other}"),
};
assert!(
!matches!(items.first(), Some(Calcit::Proc(CalcitProc::NativeNamedEnumNew))),
"should not rewrite when enum constructor lacks variant tag"
);
let warnings_vec = warnings.borrow();
assert!(!warnings_vec.is_empty(), "should warn on missing enum variant tag");
assert!(
warnings_vec.iter().any(|w| w.to_string().contains("missing variant tag")),
"warning should mention missing variant tag"
);
}
#[test]
fn rejects_enum_head_call_with_invalid_tag() {
use crate::calcit::{CalcitEnumDef, CalcitStructValue};
use crate::data::cirru::code_to_calcit;
use cirru_edn::EdnTag;
use cirru_parser::Cirru;
let enum_struct = CalcitStructValue {
struct_ref: Arc::new(CalcitStructDef::from_fields(
EdnTag::from("Result"),
vec![EdnTag::from("ok"), EdnTag::from("err")],
)),
values: Arc::new(vec![
Calcit::List(Arc::new(CalcitList::default())),
Calcit::List(Arc::new(CalcitList::default())),
]),
};
let result_enum = CalcitEnumDef::from_struct(enum_struct.clone()).expect("valid result enum");
let expr = Cirru::List(vec![Cirru::leaf("Result"), Cirru::leaf(":bad")]);
let parsed = code_to_calcit(&expr, "tests.struct", "demo", vec![]).expect("parse enum ctor");
let Calcit::List(parsed_items) = parsed else {
panic!("expected parsed call");
};
let mut code_items = parsed_items.to_vec();
code_items[0] = Calcit::EnumDef(result_enum.clone());
let code = Calcit::from(code_items);
let mut scope_defs: HashSet<Arc<str>> = HashSet::new();
scope_defs.insert(Arc::from("Result"));
let mut scope_types: ScopeTypes = ScopeTypes::new();
scope_types.insert(
Arc::from("Result"),
Arc::new(CalcitTypeAnnotation::Enum(Arc::new(result_enum.clone()), Arc::new(vec![]))),
);
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let result =
preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.struct", &warnings, &stack).expect("preprocess enum head call");
let items = match result {
Calcit::List(xs) => xs.to_vec(),
other => panic!("expected list form, got {other}"),
};
assert!(
!matches!(items.first(), Some(Calcit::Proc(CalcitProc::NativeNamedEnumNew))),
"should not rewrite when enum variant is not valid"
);
let warnings_vec = warnings.borrow();
assert!(!warnings_vec.is_empty(), "should warn on invalid enum variant");
assert!(
warnings_vec.iter().any(|w| w.to_string().contains("does not have variant")),
"warning should mention unknown enum variant"
);
}
#[test]
fn rejects_enum_head_call_with_non_tag_first_arg() {
use crate::calcit::{CalcitEnumDef, CalcitStructValue};
use crate::data::cirru::code_to_calcit;
use cirru_edn::EdnTag;
use cirru_parser::Cirru;
let enum_struct = CalcitStructValue {
struct_ref: Arc::new(CalcitStructDef::from_fields(
EdnTag::from("Mode"),
vec![EdnTag::from("dark"), EdnTag::from("light")],
)),
values: Arc::new(vec![
Calcit::List(Arc::new(CalcitList::default())),
Calcit::List(Arc::new(CalcitList::default())),
]),
};
let mode_enum = CalcitEnumDef::from_struct(enum_struct.clone()).expect("valid mode enum");
let expr = Cirru::List(vec![Cirru::leaf("Mode"), Cirru::leaf("dark")]);
let parsed = code_to_calcit(&expr, "tests.struct", "demo", vec![]).expect("parse enum ctor");
let Calcit::List(parsed_items) = parsed else {
panic!("expected parsed call");
};
let mut code_items = parsed_items.to_vec();
code_items[0] = Calcit::EnumDef(mode_enum.clone());
let code = Calcit::from(code_items);
let mut scope_defs: HashSet<Arc<str>> = HashSet::new();
scope_defs.insert(Arc::from("Mode"));
let mut scope_types: ScopeTypes = ScopeTypes::new();
scope_types.insert(
Arc::from("Mode"),
Arc::new(CalcitTypeAnnotation::Enum(Arc::new(mode_enum.clone()), Arc::new(vec![]))),
);
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let result =
preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.struct", &warnings, &stack).expect("preprocess enum head call");
let items = match result {
Calcit::List(xs) => xs.to_vec(),
other => panic!("expected list form, got {other}"),
};
assert!(
!matches!(items.first(), Some(Calcit::Proc(CalcitProc::NativeNamedEnumNew))),
"should not rewrite when enum variant prefix is not a tag"
);
let warnings_vec = warnings.borrow();
assert!(!warnings_vec.is_empty(), "should warn on non-tag first arg");
assert!(
warnings_vec
.iter()
.any(|w| w.to_string().contains("first argument should be a variant tag")),
"warning should mention non-tag first argument"
);
}
#[test]
fn warns_on_invalid_struct_field() {
use cirru_edn::EdnTag;
let test_struct = Arc::new(CalcitTypeAnnotation::StructValue(Arc::new(CalcitStructDef::from_fields(
EdnTag::from("Person"),
vec![EdnTag::from("age"), EdnTag::from("name")],
))));
let expr = Cirru::List(vec![
Cirru::leaf("&struct:get"),
Cirru::leaf("user"),
Cirru::leaf(":email"), ]);
let code = code_to_calcit(&expr, "tests.struct", "demo", vec![]).expect("parse cirru");
let mut scope_defs: HashSet<Arc<str>> = HashSet::new();
scope_defs.insert(Arc::from("user"));
let mut scope_types: ScopeTypes = ScopeTypes::new();
scope_types.insert(Arc::from("user"), test_struct.clone());
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let _resolved =
preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.struct", &warnings, &stack).expect("preprocess should succeed");
let warnings_vec = warnings.borrow();
assert!(!warnings_vec.is_empty(), "should have warning for invalid field");
let warning_msg = warnings_vec[0].to_string();
assert!(
warning_msg.contains("email"),
"warning should mention the invalid field: {warning_msg}"
);
assert!(
warning_msg.contains("Person"),
"warning should mention the struct type: {warning_msg}"
);
}
#[test]
fn rewrites_method_call_when_class_and_method_are_known() {
use cirru_edn::EdnTag;
let expr = Cirru::List(vec![Cirru::leaf(".greet"), Cirru::leaf("user")]);
let code = code_to_calcit(&expr, "tests.method", "demo", vec![]).expect("parse cirru");
let mut scope_defs: HashSet<Arc<str>> = HashSet::new();
scope_defs.insert(Arc::from("user"));
let mut scope_types: ScopeTypes = ScopeTypes::new();
let method_import = Calcit::Import(CalcitImport {
ns: Arc::from("tests.method.ns"),
def: Arc::from("greet"),
info: Arc::new(ImportInfo::SameFile { at_def: Arc::from("demo") }),
def_id: None,
});
let method_impl = CalcitImpl {
name: EdnTag::from("Greeter"),
origin: None,
fields: Arc::new(vec![EdnTag::from("greet")]),
values: Arc::new(vec![method_import.clone()]),
};
let class_struct = CalcitStructValue {
struct_ref: Arc::new(CalcitStructDef {
name: EdnTag::from("Greeter"),
fields: Arc::new(vec![EdnTag::from("greet")]),
field_types: Arc::new(vec![calcit::DYNAMIC_TYPE.clone()]),
generics: Arc::new(vec![]),
where_bounds: Arc::new(vec![]),
impls: vec![Arc::new(method_impl)],
}),
values: Arc::new(vec![method_import.clone()]),
};
scope_types.insert(
Arc::from("user"),
Arc::new(CalcitTypeAnnotation::StructValue(class_struct.struct_ref.clone())),
);
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let resolved =
preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.method", &warnings, &stack).expect("preprocess method call");
let nodes = match resolved {
Calcit::List(xs) => xs.to_vec(),
other => panic!("expected list form, got {other}"),
};
assert!(
matches!(nodes.first(), Some(Calcit::Import(_))),
"method head should be rewritten to import"
);
assert_eq!(nodes.len(), 2, "call should keep receiver argument");
}
#[test]
fn validates_method_field_access() {
use cirru_edn::EdnTag;
let test_struct = Arc::new(CalcitTypeAnnotation::StructValue(Arc::new(CalcitStructDef::from_fields(
EdnTag::from("Person"),
vec![EdnTag::from("age"), EdnTag::from("name")],
))));
let expr = Cirru::List(vec![Cirru::leaf("user.-name")]);
let code = code_to_calcit(&expr, "tests.struct", "demo", vec![]).expect("parse cirru");
let mut scope_defs: HashSet<Arc<str>> = HashSet::new();
scope_defs.insert(Arc::from("user"));
let mut scope_types: ScopeTypes = ScopeTypes::new();
scope_types.insert(Arc::from("user"), test_struct.clone());
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let _resolved =
preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.struct", &warnings, &stack).expect("preprocess should succeed");
let warnings_vec = warnings.borrow();
assert!(
warnings_vec.is_empty(),
"should not have warnings for valid field access, got: {warnings_vec:?}"
);
}
#[test]
fn warns_on_trait_impl_method_tag_syntax() {
let _lock = lock_preprocess_test_state();
let _warn_guard = WarnDynMethodGuard::new(true);
let expr = Cirru::List(vec![
Cirru::leaf("defimpl"),
Cirru::leaf("MyFooImpl"),
Cirru::leaf("MyFoo"),
Cirru::List(vec![Cirru::leaf(":foo"), Cirru::leaf("myfoo:foo")]),
]);
let code = code_to_calcit(&expr, "tests.trait", "demo", vec![]).expect("parse cirru");
let args = match code {
Calcit::List(xs) => xs.drop_left(),
other => panic!("expected list form, got {other}"),
};
let macro_info = CalcitMacro {
name: Arc::from("defimpl"),
def_ns: Arc::from(calcit::CORE_NS),
args: Arc::new(vec![]),
body: Arc::new(vec![]),
};
let warnings = RefCell::new(vec![]);
warn_on_trait_impl_method_tag_syntax(¯o_info, &args, "tests.trait", "demo", &warnings);
let warning_msgs: Vec<String> = warnings.borrow().iter().map(|w| w.to_string()).collect();
assert!(
warning_msgs
.iter()
.any(|msg| msg.contains("defimpl") && msg.contains("legacy tag style") && msg.contains(".foo")),
"expected migration warning for trait/impl method key, got: {warning_msgs:?}"
);
}
#[test]
fn rejects_legacy_hint_fn_clause_syntax() {
use crate::data::cirru::code_to_calcit;
let hint_form = Cirru::List(vec![
Cirru::leaf("hint-fn"),
Cirru::List(vec![Cirru::leaf("return-type"), Cirru::leaf(":string")]),
Cirru::List(vec![
Cirru::leaf("generics"),
Cirru::List(vec![Cirru::leaf("quote"), Cirru::leaf("T")]),
]),
]);
let hint = code_to_calcit(&hint_form, "tests.hint", "demo", vec![]).expect("parse cirru");
let scope_defs: HashSet<Arc<str>> = HashSet::new();
let mut scope_types: ScopeTypes = ScopeTypes::new();
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let err = preprocess_expr(&hint, &scope_defs, &mut scope_types, "tests.hint", &warnings, &stack)
.expect_err("legacy hint-fn clauses should be rejected");
let msg = err.msg;
assert!(
msg.contains("legacy hint-fn clauses are no longer supported") && msg.contains("return-type") && msg.contains("generics"),
"expected hard error for legacy hint-fn clauses, got: {msg}"
);
}
#[test]
fn accepts_schema_hint_fn_syntax() {
use crate::data::cirru::code_to_calcit;
let hint_form = Cirru::List(vec![
Cirru::leaf("hint-fn"),
Cirru::List(vec![
Cirru::leaf("{}"),
Cirru::List(vec![Cirru::leaf(":args"), Cirru::List(vec![Cirru::leaf("[]")])]),
Cirru::List(vec![Cirru::leaf(":return"), Cirru::leaf(":string")]),
]),
]);
let hint = code_to_calcit(&hint_form, "tests.hint", "demo", vec![]).expect("parse cirru");
let scope_defs: HashSet<Arc<str>> = HashSet::new();
let mut scope_types: ScopeTypes = ScopeTypes::new();
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let result = preprocess_expr(&hint, &scope_defs, &mut scope_types, "tests.hint", &warnings, &stack);
assert!(result.is_ok(), "schema hint-fn should preprocess successfully: {result:?}");
}
#[test]
fn local_hint_fn_refines_later_function_references() {
use crate::data::cirru::code_to_calcit;
let hint_form = Cirru::List(vec![
Cirru::leaf("hint-fn"),
Cirru::leaf("f"),
Cirru::List(vec![
Cirru::leaf("{}"),
Cirru::List(vec![
Cirru::leaf(":args"),
Cirru::List(vec![Cirru::leaf("[]"), Cirru::leaf(":number")]),
]),
Cirru::List(vec![Cirru::leaf(":return"), Cirru::leaf(":number")]),
]),
]);
let hint = code_to_calcit(&hint_form, "tests.hint", "demo", vec![]).expect("parse hint-fn");
let mut scope_defs: HashSet<Arc<str>> = HashSet::new();
scope_defs.insert(Arc::from("f"));
let mut scope_types: ScopeTypes = ScopeTypes::new();
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let resolved_hint =
preprocess_expr(&hint, &scope_defs, &mut scope_types, "tests.hint", &warnings, &stack).expect("preprocess local hint-fn");
let annotation = scope_types.get("f").expect("hint-fn should refine the lexical binding");
let CalcitTypeAnnotation::Fn(fn_annotation) = annotation.as_ref() else {
panic!("expected complete function annotation, got {annotation:?}");
};
assert!(matches!(fn_annotation.arg_types.as_slice(), [arg] if matches!(arg.as_ref(), CalcitTypeAnnotation::Number)));
assert!(matches!(fn_annotation.return_type.as_ref(), CalcitTypeAnnotation::Number));
assert!(
matches!(resolved_hint, Calcit::List(ref xs) if matches!(xs.get(1), Some(Calcit::Local(local)) if matches!(local.type_info.as_ref(), CalcitTypeAnnotation::Fn(_)))),
"the annotation target should carry the refined type"
);
let call = code_to_calcit(&Cirru::List(vec![Cirru::leaf("f"), Cirru::leaf("1")]), "tests.hint", "demo", vec![])
.expect("parse local function call");
let resolved_call =
preprocess_expr(&call, &scope_defs, &mut scope_types, "tests.hint", &warnings, &stack).expect("preprocess local function call");
let inferred = infer_type_from_expr(&resolved_call, &scope_types).expect("infer hinted local call return");
assert!(matches!(inferred.as_ref(), CalcitTypeAnnotation::Number));
}
#[test]
fn body_hint_fn_fills_omitted_parameter_types() {
use crate::data::cirru::code_to_calcit;
let schema = Cirru::List(vec![
Cirru::leaf("{}"),
Cirru::List(vec![Cirru::leaf(":return"), Cirru::leaf(":number")]),
]);
let expr = Cirru::List(vec![
Cirru::leaf("&let"),
Cirru::List(vec![
Cirru::leaf("f"),
Cirru::List(vec![
Cirru::leaf("defn"),
Cirru::leaf("f%"),
Cirru::List(vec![Cirru::leaf("x")]),
Cirru::List(vec![Cirru::leaf("hint-fn"), schema]),
Cirru::List(vec![Cirru::leaf("&+"), Cirru::leaf("x"), Cirru::leaf("1")]),
]),
]),
Cirru::List(vec![Cirru::leaf("f"), Cirru::leaf("1")]),
]);
let code = code_to_calcit(&expr, "tests.hint", "demo", vec![]).expect("parse local function");
let scope_defs: HashSet<Arc<str>> = HashSet::new();
let mut scope_types: ScopeTypes = ScopeTypes::new();
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let resolved =
preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.hint", &warnings, &stack).expect("preprocess body hint-fn");
let inferred = infer_type_from_expr(&resolved, &scope_types).expect("infer hinted anonymous function call");
assert!(
matches!(inferred.as_ref(), CalcitTypeAnnotation::Number),
"expected number return from body-hinted function, got {inferred:?}; resolved: {resolved}"
);
assert!(warnings.borrow().is_empty(), "valid body hint should not emit warnings");
}
#[test]
fn named_body_hint_parameter_keeps_its_declared_type() {
let labelled = Arc::new(CalcitTypeAnnotation::TypeRef(
Arc::from("n"),
Arc::new(vec![Arc::new(CalcitTypeAnnotation::Number)]),
));
let parameter = Arc::from("n");
assert!(matches!(
unwrap_named_body_parameter_type(labelled, Some(¶meter)).as_ref(),
CalcitTypeAnnotation::Number
));
}
#[test]
fn warns_on_invalid_method_field_access() {
use cirru_edn::EdnTag;
let test_struct = Arc::new(CalcitTypeAnnotation::StructValue(Arc::new(CalcitStructDef::from_fields(
EdnTag::from("Person"),
vec![EdnTag::from("age"), EdnTag::from("name")],
))));
let expr = Cirru::List(vec![Cirru::leaf("user.-email")]);
let code = code_to_calcit(&expr, "tests.struct", "demo", vec![]).expect("parse cirru");
let mut scope_defs: HashSet<Arc<str>> = HashSet::new();
scope_defs.insert(Arc::from("user"));
let mut scope_types: ScopeTypes = ScopeTypes::new();
scope_types.insert(Arc::from("user"), test_struct.clone());
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let _resolved =
preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.struct", &warnings, &stack).expect("preprocess should succeed");
let warnings_vec = warnings.borrow();
assert!(!warnings_vec.is_empty(), "should have warning for invalid field");
let warning_msg = warnings_vec[0].to_string();
assert!(
warning_msg.contains("email"),
"warning should mention the invalid field: {warning_msg}"
);
assert!(
warning_msg.contains("Person"),
"warning should mention the struct type: {warning_msg}"
);
}
#[test]
fn rejects_method_on_struct_without_field() {
use cirru_edn::EdnTag;
let test_struct = Arc::new(CalcitTypeAnnotation::StructValue(Arc::new(CalcitStructDef::from_fields(
EdnTag::from("Person"),
vec![EdnTag::from("age"), EdnTag::from("name")],
))));
let expr = Cirru::List(vec![
Cirru::leaf(".slice"),
Cirru::leaf("person"),
Cirru::leaf("1"),
Cirru::leaf("3"),
]);
let code = code_to_calcit(&expr, "tests.method", "demo", vec![]).expect("parse cirru");
let mut scope_defs: HashSet<Arc<str>> = HashSet::new();
scope_defs.insert(Arc::from("person"));
let mut scope_types: ScopeTypes = ScopeTypes::new();
scope_types.insert(Arc::from("person"), test_struct.clone());
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let result = preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.method", &warnings, &stack);
assert!(result.is_err(), "preprocess should reject method call on struct without that field");
if let Err(err) = result {
let msg = format!("{err}");
assert!(msg.contains(".slice"), "error should mention the method name: {msg}");
assert!(
msg.contains("Person") || msg.contains("struct"),
"error should mention the struct type: {msg}"
);
}
}
#[test]
fn checks_user_function_arg_types() {
let fn_info = CalcitFn {
name: Arc::from("demo-fn"),
def_ns: Arc::from("tests.user_fn"),
def_ref: None,
usage: crate::calcit::CalcitFnUsageMeta::default(),
scope: Arc::new(CalcitScope::default()),
args: Arc::new(CalcitFnArgs::Args(vec![0, 1])), body: vec![Calcit::Nil],
generics: Arc::new(vec![]),
where_bounds: Arc::new(vec![]),
arg_types: vec![
Arc::new(CalcitTypeAnnotation::from_tag_name("number")),
Arc::new(CalcitTypeAnnotation::from_tag_name("string")),
],
return_type: crate::calcit::DYNAMIC_TYPE.clone(),
rest_type: None,
};
let args = CalcitList::from(
&vec![
Calcit::Str(Arc::from("hello")), Calcit::Number(42.0), ][..],
);
let scope_types: ScopeTypes = ScopeTypes::new();
let warnings = RefCell::new(vec![]);
let dummy_head = Calcit::Import(CalcitImport {
ns: Arc::from("tests.user_fn"),
def: Arc::from("demo-fn"),
info: Arc::new(ImportInfo::NsReferDef {
at_ns: Arc::from("tests.user_fn"),
at_def: Arc::from("demo-fn"),
}),
def_id: None,
});
let call_info = CallTypeCheckInfo {
file_ns: "tests.user_fn",
def_name: "demo",
call_location: None,
};
check_user_fn_arg_types(&fn_info, &dummy_head, &args, &scope_types, &call_info, &warnings);
let warnings_vec = warnings.borrow();
assert!(
warnings_vec.len() >= 2,
"should have at least 2 warnings for arg type mismatches, got {} warnings: {:?}",
warnings_vec.len(),
warnings_vec.iter().map(|w| w.to_string()).collect::<Vec<_>>()
);
let warning1 = warnings_vec.iter().find(|w| w.to_string().contains("arg 1"));
assert!(
warning1.is_some(),
"should have warning for arg 1, warnings: {:?}",
warnings_vec.iter().map(|w| w.to_string()).collect::<Vec<_>>()
);
let msg1 = warning1.unwrap().to_string();
assert!(
msg1.contains("number") || msg1.contains(":number"),
"warning should mention expected type: {msg1}"
);
assert!(
msg1.contains("string") || msg1.contains(":string"),
"warning should mention actual type: {msg1}"
);
let warning2 = warnings_vec.iter().find(|w| w.to_string().contains("arg 2"));
assert!(
warning2.is_some(),
"should have warning for arg 2, warnings: {:?}",
warnings_vec.iter().map(|w| w.to_string()).collect::<Vec<_>>()
);
let msg2 = warning2.unwrap().to_string();
assert!(
msg2.contains("string") || msg2.contains(":string"),
"warning should mention expected type: {msg2}"
);
assert!(
msg2.contains("number") || msg2.contains(":number"),
"warning should mention actual type: {msg2}"
);
}
#[test]
fn user_function_arg_warning_falls_back_to_call_location_for_literal_args() {
let fn_info = CalcitFn {
name: Arc::from("plus1"),
def_ns: Arc::from("tests.user_fn"),
def_ref: None,
usage: CalcitFnUsageMeta::default(),
scope: Arc::new(CalcitScope::default()),
args: Arc::new(CalcitFnArgs::Args(vec![0])),
body: vec![Calcit::Nil],
generics: Arc::new(vec![]),
where_bounds: Arc::new(vec![]),
arg_types: vec![Arc::new(CalcitTypeAnnotation::from_tag_name("number"))],
return_type: crate::calcit::DYNAMIC_TYPE.clone(),
rest_type: None,
};
let expr = Cirru::List(vec![Cirru::leaf("plus1"), Cirru::leaf(":tag")]);
let code = code_to_calcit(&expr, "tests.user_fn", "demo", vec![9]).expect("parse cirru");
let Calcit::List(items) = code else {
panic!("expected list call");
};
let head = items.first().expect("call head");
let args = items.drop_left();
let call_location = derive_call_expr_location(head).expect("source-backed call location");
let warnings = RefCell::new(vec![]);
let dummy_head = Calcit::Import(CalcitImport {
ns: Arc::from("tests.user_fn"),
def: Arc::from("plus1"),
info: Arc::new(ImportInfo::NsReferDef {
at_ns: Arc::from("tests.user_fn"),
at_def: Arc::from("demo"),
}),
def_id: None,
});
let call_info = CallTypeCheckInfo {
file_ns: "tests.user_fn",
def_name: "demo",
call_location: Some(call_location.clone()),
};
check_user_fn_arg_types(&fn_info, &dummy_head, &args, &ScopeTypes::new(), &call_info, &warnings);
let warnings_vec = warnings.borrow();
assert_eq!(warnings_vec.len(), 1, "expected one warning, got: {warnings_vec:?}");
assert_eq!(warnings_vec[0].location(), &call_location);
assert_eq!(warnings_vec[0].location().coord.as_ref(), &vec![9]);
}
#[test]
fn user_function_where_bounds_warn_on_missing_trait_impl() {
let show_trait = Arc::new(crate::calcit::CalcitTrait::new(
EdnTag::new("Renderable"),
vec![EdnTag::new("show")],
vec![crate::calcit::DYNAMIC_TYPE.clone()],
));
let hint_generics = Calcit::List(Arc::new(CalcitList::from(&[
Calcit::Symbol {
sym: Arc::from("[]"),
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("tests.where"),
at_def: Arc::from("print-it"),
}),
location: None,
},
Calcit::Symbol {
sym: Arc::from("T"),
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("tests.where"),
at_def: Arc::from("print-it"),
}),
location: None,
},
])));
let where_map = Calcit::List(Arc::new(CalcitList::from(&[
Calcit::Symbol {
sym: Arc::from("{}"),
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("tests.where"),
at_def: Arc::from("print-it"),
}),
location: None,
},
Calcit::List(Arc::new(CalcitList::from(&[
Calcit::Symbol {
sym: Arc::from("T"),
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("tests.where"),
at_def: Arc::from("print-it"),
}),
location: None,
},
Calcit::Trait((*show_trait).clone()),
]))),
])));
let hint_schema = Calcit::List(Arc::new(CalcitList::from(&[
Calcit::Symbol {
sym: Arc::from("{}"),
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("tests.where"),
at_def: Arc::from("print-it"),
}),
location: None,
},
Calcit::List(Arc::new(CalcitList::from(&[Calcit::tag("generics"), hint_generics]))),
Calcit::List(Arc::new(CalcitList::from(&[Calcit::tag("where"), where_map]))),
])));
let hint_form = Calcit::List(Arc::new(CalcitList::from(&[
Calcit::Syntax(CalcitSyntax::HintFn, Arc::from("tests.where")),
hint_schema,
])));
let fn_info = CalcitFn {
name: Arc::from("print-it"),
def_ns: Arc::from("tests.where"),
def_ref: None,
usage: crate::calcit::CalcitFnUsageMeta::default(),
scope: Arc::new(CalcitScope::default()),
args: Arc::new(CalcitFnArgs::Args(vec![0])),
body: vec![hint_form],
generics: Arc::new(vec![Arc::from("T")]),
where_bounds: Arc::new(vec![crate::calcit::CalcitGenericBound {
name: Arc::from("T"),
traits: Arc::new(vec![show_trait.clone()]),
}]),
arg_types: vec![Arc::new(CalcitTypeAnnotation::TypeVar(Arc::from("T")))],
return_type: crate::calcit::DYNAMIC_TYPE.clone(),
rest_type: None,
};
let arg_local = Calcit::Local(CalcitLocal {
idx: CalcitLocal::track_sym(&Arc::from("value")),
sym: Arc::from("value"),
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("tests.where"),
at_def: Arc::from("demo"),
}),
location: None,
type_info: crate::calcit::DYNAMIC_TYPE.clone(),
});
let args = CalcitList::from(&[arg_local] as &[Calcit]);
let mut shown_struct = crate::calcit::CalcitStructDef::from_fields(EdnTag::new("Shown"), vec![EdnTag::new("name")]);
shown_struct.impls = vec![Arc::new(crate::calcit::CalcitImpl {
name: EdnTag::new("ShowImpl"),
origin: Some(show_trait.clone()),
fields: Arc::new(vec![EdnTag::new("show")]),
values: Arc::new(vec![Calcit::Nil]),
})];
let plain_struct = crate::calcit::CalcitStructDef::from_fields(EdnTag::new("Plain"), vec![EdnTag::new("name")]);
let mut ok_scope_types: ScopeTypes = ScopeTypes::new();
ok_scope_types.insert(
Arc::from("value"),
Arc::new(CalcitTypeAnnotation::Struct(Arc::new(shown_struct), Arc::new(vec![]))),
);
let ok_warnings = RefCell::new(vec![]);
let dummy_head = Calcit::Import(CalcitImport {
ns: Arc::from("tests.where"),
def: Arc::from("print-it"),
info: Arc::new(ImportInfo::NsReferDef {
at_ns: Arc::from("tests.where"),
at_def: Arc::from("demo"),
}),
def_id: None,
});
let call_info = CallTypeCheckInfo {
file_ns: "tests.where",
def_name: "demo",
call_location: None,
};
check_user_fn_arg_types(&fn_info, &dummy_head, &args, &ok_scope_types, &call_info, &ok_warnings);
assert!(
ok_warnings.borrow().is_empty(),
"satisfied where-bound should not warn: {:?}",
ok_warnings.borrow()
);
let mut bad_scope_types: ScopeTypes = ScopeTypes::new();
bad_scope_types.insert(
Arc::from("value"),
Arc::new(CalcitTypeAnnotation::Struct(Arc::new(plain_struct), Arc::new(vec![]))),
);
let bad_warnings = RefCell::new(vec![]);
check_user_fn_arg_types(&fn_info, &dummy_head, &args, &bad_scope_types, &call_info, &bad_warnings);
let warnings_vec = bad_warnings.borrow();
assert_eq!(
warnings_vec.len(),
1,
"missing trait impl should emit one warning: {warnings_vec:?}"
);
let message = warnings_vec[0].to_string();
assert!(
message.contains("trait bound") && message.contains("Renderable"),
"warning should mention missing where-bound: {message}"
);
}
#[test]
fn local_function_where_bounds_warn_on_missing_trait_impl() {
let show_trait = Arc::new(crate::calcit::CalcitTrait::new(
EdnTag::new("Renderable"),
vec![EdnTag::new("show")],
vec![crate::calcit::DYNAMIC_TYPE.clone()],
));
let local_fn = CalcitLocal {
idx: CalcitLocal::track_sym(&Arc::from("printer")),
sym: Arc::from("printer"),
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("tests.where"),
at_def: Arc::from("demo"),
}),
location: None,
type_info: Arc::new(CalcitTypeAnnotation::Fn(Arc::new(CalcitFnTypeAnnotation {
generics: Arc::new(vec![Arc::from("T")]),
where_bounds: Arc::new(vec![crate::calcit::CalcitGenericBound {
name: Arc::from("T"),
traits: Arc::new(vec![show_trait.clone()]),
}]),
arg_types: vec![Arc::new(CalcitTypeAnnotation::TypeVar(Arc::from("T")))],
return_type: crate::calcit::DYNAMIC_TYPE.clone(),
fn_kind: SchemaKind::Fn,
rest_type: None,
features: Arc::new(HashSet::new()),
}))),
};
let head_form = Calcit::Local(local_fn.clone());
let arg_local = Calcit::Local(CalcitLocal {
idx: CalcitLocal::track_sym(&Arc::from("value")),
sym: Arc::from("value"),
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("tests.where"),
at_def: Arc::from("demo"),
}),
location: None,
type_info: crate::calcit::DYNAMIC_TYPE.clone(),
});
let args = CalcitList::from(&[arg_local] as &[Calcit]);
let mut shown_struct = crate::calcit::CalcitStructDef::from_fields(EdnTag::new("Shown"), vec![EdnTag::new("name")]);
shown_struct.impls = vec![Arc::new(CalcitImpl {
name: EdnTag::new("ShowImpl"),
origin: Some(show_trait.clone()),
fields: Arc::new(vec![EdnTag::new("show")]),
values: Arc::new(vec![Calcit::Nil]),
})];
let plain_struct = crate::calcit::CalcitStructDef::from_fields(EdnTag::new("Plain"), vec![EdnTag::new("name")]);
let call_info = CallTypeCheckInfo {
file_ns: "tests.where",
def_name: "demo",
call_location: None,
};
let mut ok_scope_types: ScopeTypes = ScopeTypes::new();
ok_scope_types.insert(
Arc::from("value"),
Arc::new(CalcitTypeAnnotation::Struct(Arc::new(shown_struct), Arc::new(vec![]))),
);
let ok_warnings = RefCell::new(vec![]);
check_local_fn_call_arg_types(&head_form, &local_fn, &args, &ok_scope_types, &call_info, &ok_warnings);
assert!(
ok_warnings.borrow().is_empty(),
"satisfied local fn where-bound should not warn: {:?}",
ok_warnings.borrow()
);
let mut bad_scope_types: ScopeTypes = ScopeTypes::new();
bad_scope_types.insert(
Arc::from("value"),
Arc::new(CalcitTypeAnnotation::Struct(Arc::new(plain_struct), Arc::new(vec![]))),
);
let bad_warnings = RefCell::new(vec![]);
check_local_fn_call_arg_types(&head_form, &local_fn, &args, &bad_scope_types, &call_info, &bad_warnings);
let warnings_vec = bad_warnings.borrow();
assert_eq!(
warnings_vec.len(),
1,
"missing local fn trait impl should emit one warning: {warnings_vec:?}"
);
let message = warnings_vec[0].to_string();
assert!(
message.contains("trait bound") && message.contains("Renderable"),
"local fn warning should mention missing where-bound: {message}"
);
}
#[test]
fn checks_function_return_type() {
use crate::data::cirru::code_to_calcit;
use cirru_parser::Cirru;
let expr = Cirru::List(vec![
Cirru::leaf("defn"),
Cirru::leaf("wrong-ret"),
Cirru::List(vec![]), Cirru::List(vec![
Cirru::leaf("hint-fn"),
Cirru::List(vec![
Cirru::leaf("{}"),
Cirru::List(vec![Cirru::leaf(":return"), Cirru::leaf(":string")]),
]),
]),
Cirru::List(vec![
Cirru::leaf("&+"),
Cirru::leaf("1"),
Cirru::leaf("2"),
]),
]);
let code = code_to_calcit(&expr, "tests.return_type", "demo", vec![]).expect("parse cirru");
let scope_defs: HashSet<Arc<str>> = HashSet::new();
let mut scope_types: ScopeTypes = ScopeTypes::new();
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let _result = preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.return_type", &warnings, &stack);
let warnings_vec = warnings.borrow();
assert!(!warnings_vec.is_empty(), "should have warning for return type mismatch");
let warning_msg = warnings_vec[0].to_string();
assert!(
warning_msg.contains("return") && warning_msg.contains("type"),
"warning should mention return type: {warning_msg}"
);
assert!(
warning_msg.contains("string") || warning_msg.contains(":string"),
"warning should mention declared type: {warning_msg}"
);
assert!(
warning_msg.contains("number") || warning_msg.contains(":number"),
"warning should mention actual type: {warning_msg}"
);
}
#[test]
fn checks_function_return_type_from_if_expression() {
use crate::data::cirru::code_to_calcit;
use cirru_parser::Cirru;
let expr = Cirru::List(vec![
Cirru::leaf("defn"),
Cirru::leaf("wrong-ret-if"),
Cirru::List(vec![]),
Cirru::List(vec![
Cirru::leaf("hint-fn"),
Cirru::List(vec![
Cirru::leaf("{}"),
Cirru::List(vec![Cirru::leaf(":return"), Cirru::leaf(":string")]),
]),
]),
Cirru::List(vec![Cirru::leaf("if"), Cirru::leaf("true"), Cirru::leaf("1"), Cirru::leaf("2")]),
]);
let code = code_to_calcit(&expr, "tests.return_type", "demo", vec![]).expect("parse cirru");
let scope_defs: HashSet<Arc<str>> = HashSet::new();
let mut scope_types: ScopeTypes = ScopeTypes::new();
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let _result = preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.return_type", &warnings, &stack);
let warnings_vec = warnings.borrow();
assert!(
!warnings_vec.is_empty(),
"should have warning for if-expression return type mismatch"
);
let warning_msg = warnings_vec[0].to_string();
assert!(
warning_msg.contains("return") && warning_msg.contains("type"),
"warning should mention return type: {warning_msg}"
);
assert!(
warning_msg.contains("string") || warning_msg.contains(":string"),
"warning should mention declared type: {warning_msg}"
);
assert!(
warning_msg.contains("number") || warning_msg.contains(":number"),
"warning should mention inferred if-branch type: {warning_msg}"
);
}
#[test]
fn checks_struct_method_arg_types() {
use cirru_edn::EdnTag;
let method_fn = Arc::new(CalcitFn {
name: Arc::from("greet"),
def_ns: Arc::from("tests.method"),
def_ref: None,
usage: crate::calcit::CalcitFnUsageMeta::default(),
scope: Arc::new(CalcitScope::default()),
args: Arc::new(CalcitFnArgs::Args(vec![1, 2])), body: vec![Calcit::Nil],
generics: Arc::new(vec![]),
where_bounds: Arc::new(vec![]),
return_type: crate::calcit::DYNAMIC_TYPE.clone(),
arg_types: vec![Arc::new(CalcitTypeAnnotation::String), Arc::new(CalcitTypeAnnotation::Number)],
rest_type: None,
});
let method_value = Calcit::Fn {
id: Arc::from("tests.method/greet"),
info: method_fn.clone(),
};
let method_impl = CalcitImpl {
name: EdnTag::from("Person"),
origin: None,
fields: Arc::new(vec![EdnTag::from("greet")]),
values: Arc::new(vec![method_value.clone()]),
};
let class_struct = CalcitStructValue {
struct_ref: Arc::new(CalcitStructDef {
name: EdnTag::from("Person"),
fields: Arc::new(vec![EdnTag::from("greet")]),
field_types: Arc::new(vec![calcit::DYNAMIC_TYPE.clone()]),
generics: Arc::new(vec![]),
where_bounds: Arc::new(vec![]),
impls: vec![Arc::new(method_impl)],
}),
values: Arc::new(vec![method_value]),
};
let expr = Cirru::List(vec![
Cirru::leaf(".greet"),
Cirru::leaf("user"),
Cirru::leaf("|hello"), ]);
let code = code_to_calcit(&expr, "tests.method", "demo", vec![]).expect("parse cirru");
let mut scope_defs: HashSet<Arc<str>> = HashSet::new();
scope_defs.insert(Arc::from("user"));
let mut scope_types: ScopeTypes = ScopeTypes::new();
scope_types.insert(
Arc::from("user"),
Arc::new(CalcitTypeAnnotation::StructValue(class_struct.struct_ref.clone())),
);
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let _result = preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.method", &warnings, &stack).expect("preprocess");
let warnings_vec = warnings.borrow();
assert!(!warnings_vec.is_empty(), "should have warning for wrong argument type");
let warning_msg = warnings_vec[0].to_string();
assert!(
warning_msg.contains("Method") || warning_msg.contains("greet"),
"warning should mention method: {warning_msg}"
);
assert!(
warning_msg.contains("number") && warning_msg.contains("string"),
"warning should mention type mismatch: {warning_msg}"
);
}
#[test]
fn checks_enum_invalid_variant() {
use crate::calcit::CalcitEnumDef;
use cirru_edn::EdnTag;
let enum_struct = CalcitStructValue {
struct_ref: Arc::new(CalcitStructDef::from_fields(
EdnTag::from("Result"),
vec![EdnTag::from("err"), EdnTag::from("ok")],
)),
values: Arc::new(vec![
Calcit::from(vec![Calcit::tag("string")]), Calcit::from(CalcitList::default()), ]),
};
let enum_proto = CalcitEnumDef::from_struct(enum_struct.clone()).expect("valid enum");
let args = CalcitList::from(
&vec![
Calcit::EnumDef(enum_proto), Calcit::tag("invalid"), ][..],
);
let scope_types: ScopeTypes = ScopeTypes::new();
let warnings = RefCell::new(vec![]);
check_enum_construction(&args, &scope_types, "tests.enum", "demo", &warnings);
let warnings_vec = warnings.borrow();
assert!(!warnings_vec.is_empty(), "should have warning for invalid variant");
let warning_msg = warnings_vec[0].to_string();
assert!(
warning_msg.contains("invalid") && warning_msg.contains("Result"),
"warning should mention invalid variant and enum name: {warning_msg}"
);
assert!(
warning_msg.contains("err") || warning_msg.contains("ok"),
"warning should list available variants: {warning_msg}"
);
}
#[test]
fn checks_enum_wrong_arity() {
use crate::calcit::CalcitEnumDef;
use cirru_edn::EdnTag;
let enum_struct = CalcitStructValue {
struct_ref: Arc::new(CalcitStructDef::from_fields(
EdnTag::from("Result"),
vec![EdnTag::from("err"), EdnTag::from("ok")],
)),
values: Arc::new(vec![
Calcit::from(vec![Calcit::tag("string")]), Calcit::from(CalcitList::default()), ]),
};
let enum_proto = CalcitEnumDef::from_struct(enum_struct.clone()).expect("valid enum");
let args = CalcitList::from(
&vec![
Calcit::EnumDef(enum_proto), Calcit::tag("err"), ][..],
);
let scope_types: ScopeTypes = ScopeTypes::new();
let warnings = RefCell::new(vec![]);
check_enum_construction(&args, &scope_types, "tests.enum", "demo", &warnings);
let warnings_vec = warnings.borrow();
assert!(!warnings_vec.is_empty(), "should have warning for wrong arity");
let warning_msg = warnings_vec[0].to_string();
assert!(
warning_msg.contains("err") && warning_msg.contains("Result"),
"warning should mention variant and enum name: {warning_msg}"
);
assert!(
warning_msg.contains("expects 1") && warning_msg.contains("got 0"),
"warning should mention expected vs actual arity: {warning_msg}"
);
}
#[test]
fn checks_enum_payload_type() {
use crate::calcit::CalcitEnumDef;
use cirru_edn::EdnTag;
let enum_struct = CalcitStructValue {
struct_ref: Arc::new(CalcitStructDef::from_fields(
EdnTag::from("Result"),
vec![EdnTag::from("err"), EdnTag::from("ok")],
)),
values: Arc::new(vec![
Calcit::from(vec![Calcit::tag("string")]), Calcit::from(CalcitList::default()), ]),
};
let enum_proto = CalcitEnumDef::from_struct(enum_struct.clone()).expect("valid enum");
let args = CalcitList::from(
&vec![
Calcit::EnumDef(enum_proto), Calcit::tag("err"), Calcit::Number(42.0), ][..],
);
let scope_types: ScopeTypes = ScopeTypes::new();
let warnings = RefCell::new(vec![]);
check_enum_construction(&args, &scope_types, "tests.enum", "demo", &warnings);
let warnings_vec = warnings.borrow();
assert!(!warnings_vec.is_empty(), "should have warning for payload type mismatch");
let warning_msg = warnings_vec[0].to_string();
assert!(
warning_msg.contains("payload 1"),
"warning should mention payload index: {warning_msg}"
);
assert!(
warning_msg.contains("string") && warning_msg.contains("number"),
"warning should mention expected and actual types: {warning_msg}"
);
}
#[test]
fn checks_enum_nth_out_of_bounds() {
use cirru_edn::EdnTag;
let _ = EdnTag::from("point");
let mut scope_types: ScopeTypes = ScopeTypes::new();
scope_types.insert(Arc::from("my-enum"), Arc::new(CalcitTypeAnnotation::AnonymousEnum));
let args = CalcitList::from(
&vec![
Calcit::Symbol {
sym: Arc::from("my-enum"),
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("tests.enum"),
at_def: Arc::from("demo"),
}),
location: None,
},
Calcit::Number(3.0),
][..],
);
let warnings = RefCell::new(vec![]);
check_enum_nth_bounds(&args, &scope_types, "tests.enum", "demo", &warnings);
let warnings_vec = warnings.borrow();
assert!(
warnings_vec.is_empty(),
"AnonymousEnum: no static bounds checking, should have no warning"
);
}
#[test]
fn checks_enum_nth_valid_index() {
use cirru_edn::EdnTag;
let _ = EdnTag::from("point");
let mut scope_types: ScopeTypes = ScopeTypes::new();
scope_types.insert(Arc::from("my-enum"), Arc::new(CalcitTypeAnnotation::AnonymousEnum));
let args = CalcitList::from(
&vec![
Calcit::Symbol {
sym: Arc::from("my-enum"),
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("tests.enum"),
at_def: Arc::from("demo"),
}),
location: None,
},
Calcit::Number(1.0),
][..],
);
let warnings = RefCell::new(vec![]);
check_enum_nth_bounds(&args, &scope_types, "tests.enum", "demo", &warnings);
let warnings_vec = warnings.borrow();
assert!(warnings_vec.is_empty(), "should have no warnings for valid index");
}
#[test]
fn checks_enum_nth_dynamic_index() {
use cirru_edn::EdnTag;
let _ = EdnTag::from("point");
let mut scope_types: ScopeTypes = ScopeTypes::new();
scope_types.insert(Arc::from("my-enum"), Arc::new(CalcitTypeAnnotation::AnonymousEnum));
let args = CalcitList::from(
&vec![
Calcit::Symbol {
sym: Arc::from("my-enum"),
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("tests.enum"),
at_def: Arc::from("demo"),
}),
location: None,
},
Calcit::Local(CalcitLocal {
idx: CalcitLocal::track_sym(&Arc::from("idx")),
sym: Arc::from("idx"),
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("tests.enum"),
at_def: Arc::from("demo"),
}),
location: None,
type_info: Arc::new(CalcitTypeAnnotation::Number),
}),
][..],
);
let warnings = RefCell::new(vec![]);
check_enum_nth_bounds(&args, &scope_types, "tests.enum", "demo", &warnings);
let warnings_vec = warnings.borrow();
assert!(warnings_vec.is_empty(), "should skip check for dynamic index");
}
#[test]
fn warns_on_dynamic_trait_call() {
let _lock = lock_preprocess_test_state();
let _guard = WarnDynMethodGuard::new(true);
let expr = Cirru::List(vec![Cirru::leaf(".greet"), Cirru::leaf("user")]);
let code = code_to_calcit(&expr, "tests.trait", "demo", vec![]).expect("parse cirru");
let mut scope_defs: HashSet<Arc<str>> = HashSet::new();
scope_defs.insert(Arc::from("user"));
let mut scope_types: ScopeTypes = ScopeTypes::new();
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let _resolved =
preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.trait", &warnings, &stack).expect("preprocess method call");
let warnings_vec = warnings.borrow();
assert!(!warnings_vec.is_empty(), "should warn on dynamic trait call");
let warning_msg = warnings_vec[0].to_string();
assert!(
warning_msg.contains("dynamic trait call") && warning_msg.contains(".greet"),
"warning should mention method: {warning_msg}"
);
}
#[test]
fn fails_fast_on_if_with_too_many_arguments() {
let expr = Cirru::List(vec![
Cirru::leaf("if"),
Cirru::leaf("true"),
Cirru::leaf("1"),
Cirru::leaf("2"),
Cirru::leaf("3"),
]);
let code = code_to_calcit(&expr, "tests.if", "demo", vec![]).expect("parse cirru");
let scope_defs: HashSet<Arc<str>> = HashSet::new();
let mut scope_types: ScopeTypes = ScopeTypes::new();
let warnings = RefCell::new(vec![]);
let stack = CallStackList::default();
let result = preprocess_expr(&code, &scope_defs, &mut scope_types, "tests.if", &warnings, &stack);
assert!(result.is_err(), "preprocess should reject if with too many arguments");
if let Err(err) = result {
let msg = format!("{err}");
assert!(msg.contains("if expects 2 or 3 arguments"), "error should mention if arity: {msg}");
}
}
fn fn_schema_annotation(kind: SchemaKind, arg_count: usize, has_rest: bool) -> Arc<CalcitTypeAnnotation> {
let mut arg_types = Vec::with_capacity(arg_count);
for _ in 0..arg_count {
arg_types.push(Arc::new(CalcitTypeAnnotation::Number));
}
Arc::new(CalcitTypeAnnotation::Fn(Arc::new(crate::calcit::CalcitFnTypeAnnotation {
generics: Arc::new(vec![]),
where_bounds: Arc::new(vec![]),
arg_types,
return_type: Arc::new(CalcitTypeAnnotation::Number),
fn_kind: kind,
rest_type: has_rest.then(|| Arc::new(CalcitTypeAnnotation::Number)),
features: Arc::new(HashSet::new()),
})))
}
#[test]
fn catches_schema_arity_mismatch_during_preprocess() {
let args = CalcitList::from(
&[
Calcit::Symbol {
sym: Arc::from("a"),
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("tests.schema"),
at_def: Arc::from("demo"),
}),
location: None,
},
Calcit::Symbol {
sym: Arc::from("b"),
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("tests.schema"),
at_def: Arc::from("demo"),
}),
location: None,
},
][..],
);
let issues = validate_def_schema_during_preprocess(
&CalcitSyntax::Defn,
"tests.schema",
"demo",
&args,
&fn_schema_annotation(SchemaKind::Fn, 3, false),
);
assert_eq!(issues.len(), 1, "expected 1 issue, got: {issues:?}");
assert!(issues[0].contains("schema has 3 required arg(s) but code has 2"));
}
#[test]
fn warns_on_legacy_optional_in_public_function_schemas() {
let schema = CalcitTypeAnnotation::Fn(Arc::new(CalcitFnTypeAnnotation {
generics: Arc::new(vec![]),
where_bounds: Arc::new(vec![]),
arg_types: vec![Arc::new(CalcitTypeAnnotation::Number)],
return_type: Arc::new(CalcitTypeAnnotation::Optional(Arc::new(CalcitTypeAnnotation::Number))),
fn_kind: SchemaKind::Fn,
rest_type: None,
features: Arc::new(HashSet::new()),
}));
let warnings = RefCell::new(vec![]);
warn_on_legacy_optional_public_schema("tests.schema", "demo", &schema, &warnings);
assert_eq!(warnings.borrow().len(), 1);
assert_eq!(warnings.borrow()[0].code(), Some("W_LEGACY_OPTIONAL_SCHEMA"));
let core_public_warnings = RefCell::new(vec![]);
warn_on_legacy_optional_public_schema(calcit::CORE_NS, "future-public-api", &schema, &core_public_warnings);
assert_eq!(
core_public_warnings.borrow().len(),
1,
"public core APIs must not expose Optional<T>"
);
let core_raw_warnings = RefCell::new(vec![]);
warn_on_legacy_optional_public_schema(calcit::CORE_NS, "&raw-lookup", &schema, &core_raw_warnings);
assert!(core_raw_warnings.borrow().is_empty(), "raw core primitives are semver-private");
let bridge_warnings = RefCell::new(vec![]);
warn_on_legacy_optional_public_schema(calcit::CORE_NS, "optionally", &schema, &bridge_warnings);
assert!(
bridge_warnings.borrow().is_empty(),
"optionally is the explicit nullable-to-nominal bridge"
);
}
#[test]
fn catches_schema_kind_mismatch_during_preprocess() {
let args = CalcitList::from(
&[Calcit::Symbol {
sym: Arc::from("a"),
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("tests.schema"),
at_def: Arc::from("demo"),
}),
location: None,
}][..],
);
let issues = validate_def_schema_during_preprocess(
&CalcitSyntax::Defn,
"tests.schema",
"demo",
&args,
&fn_schema_annotation(SchemaKind::Macro, 1, false),
);
assert_eq!(issues.len(), 1, "expected 1 issue, got: {issues:?}");
assert!(issues[0].contains("schema :kind is :macro but code uses defn"));
}
#[test]
fn rejects_macro_schemas_for_wasm_function_declarations() {
let args = CalcitList::from(
&[Calcit::Symbol {
sym: Arc::from("a"),
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("tests.schema"),
at_def: Arc::from("demo"),
}),
location: None,
}][..],
);
for head in [CalcitSyntax::DefWasmExport, CalcitSyntax::DefWasmImport] {
let issues = validate_def_schema_during_preprocess(
&head,
"tests.schema",
"demo",
&args,
&fn_schema_annotation(SchemaKind::Macro, 1, false),
);
assert_eq!(issues.len(), 1, "expected one issue for {head}");
assert!(issues[0].contains("schema :kind is :macro"));
}
}
#[test]
fn validate_def_schema_skips_rest_binding_name() {
let info = Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("calcit.core"),
at_def: Arc::from("include"),
});
let args = CalcitList::from(&[
Calcit::Local(CalcitLocal {
idx: CalcitLocal::track_sym(&Arc::from("base")),
sym: Arc::from("base"),
info: info.clone(),
location: None,
type_info: crate::calcit::DYNAMIC_TYPE.clone(),
}),
Calcit::Syntax(CalcitSyntax::ArgSpread, Arc::from("test")),
Calcit::Local(CalcitLocal {
idx: CalcitLocal::track_sym(&Arc::from("xs")),
sym: Arc::from("xs"),
info,
location: None,
type_info: crate::calcit::DYNAMIC_TYPE.clone(),
}),
] as &[Calcit]);
let schema = CalcitTypeAnnotation::Fn(Arc::new(crate::calcit::CalcitFnTypeAnnotation {
generics: Arc::new(vec![]),
where_bounds: Arc::new(vec![]),
arg_types: vec![crate::calcit::DYNAMIC_TYPE.clone()],
return_type: crate::calcit::DYNAMIC_TYPE.clone(),
fn_kind: SchemaKind::Fn,
rest_type: Some(crate::calcit::DYNAMIC_TYPE.clone()),
features: Arc::new(HashSet::new()),
}));
let issues = validate_def_schema_during_preprocess(&CalcitSyntax::Defn, "calcit.core", "include", &args, &schema);
assert!(issues.is_empty(), "rest binding should not count as a required arg: {issues:?}");
}
#[test]
fn validate_def_schema_ignores_macro_arity_details() {
let args = CalcitList::from(&[
Calcit::Local(CalcitLocal {
idx: CalcitLocal::track_sym(&Arc::from("args")),
sym: Arc::from("args"),
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("calcit.core"),
at_def: Arc::from("fn"),
}),
location: None,
type_info: crate::calcit::DYNAMIC_TYPE.clone(),
}),
Calcit::Syntax(CalcitSyntax::ArgSpread, Arc::from("test")),
Calcit::Local(CalcitLocal {
idx: CalcitLocal::track_sym(&Arc::from("body")),
sym: Arc::from("body"),
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("calcit.core"),
at_def: Arc::from("fn"),
}),
location: None,
type_info: crate::calcit::DYNAMIC_TYPE.clone(),
}),
] as &[Calcit]);
let schema = CalcitTypeAnnotation::Fn(Arc::new(crate::calcit::CalcitFnTypeAnnotation {
generics: Arc::new(vec![]),
where_bounds: Arc::new(vec![]),
arg_types: vec![crate::calcit::DYNAMIC_TYPE.clone()],
return_type: crate::calcit::DYNAMIC_TYPE.clone(),
fn_kind: SchemaKind::Macro,
rest_type: None,
features: Arc::new(HashSet::new()),
}));
let issues = validate_def_schema_during_preprocess(&CalcitSyntax::Defmacro, "calcit.core", "fn", &args, &schema);
assert!(issues.is_empty(), "macro arity differences should be ignored: {issues:?}");
}
#[test]
fn validate_def_schema_skips_optional_marker() {
let args = CalcitList::from(&[
Calcit::Local(CalcitLocal {
idx: CalcitLocal::track_sym(&Arc::from("xs")),
sym: Arc::from("xs"),
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("calcit.core"),
at_def: Arc::from("slice"),
}),
location: None,
type_info: crate::calcit::DYNAMIC_TYPE.clone(),
}),
Calcit::Local(CalcitLocal {
idx: CalcitLocal::track_sym(&Arc::from("n")),
sym: Arc::from("n"),
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("calcit.core"),
at_def: Arc::from("slice"),
}),
location: None,
type_info: crate::calcit::DYNAMIC_TYPE.clone(),
}),
Calcit::Syntax(CalcitSyntax::ArgOptional, Arc::from("test")),
Calcit::Local(CalcitLocal {
idx: CalcitLocal::track_sym(&Arc::from("m")),
sym: Arc::from("m"),
info: Arc::new(CalcitSymbolInfo {
at_ns: Arc::from("calcit.core"),
at_def: Arc::from("slice"),
}),
location: None,
type_info: crate::calcit::DYNAMIC_TYPE.clone(),
}),
] as &[Calcit]);
let (required_count, has_rest) = analyze_def_schema_param_arity(&args);
assert_eq!(required_count, 3, "optional marker should not count as its own arg");
assert!(!has_rest);
}
}