use std::collections::HashMap;
use crate::ast::*;
#[derive(Debug, Clone, PartialEq)]
pub enum Ty {
Number,
Text,
Bool,
Nil,
Optional(Box<Ty>),
List(Box<Ty>),
Map(Box<Ty>, Box<Ty>),
Result(Box<Ty>, Box<Ty>),
Sum(Vec<String>),
Fn(Vec<Ty>, Box<Ty>),
Named(String),
Unknown,
}
impl std::fmt::Display for Ty {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Ty::Number => write!(f, "n"),
Ty::Text => write!(f, "t"),
Ty::Bool => write!(f, "b"),
Ty::Nil => write!(f, "_"),
Ty::Optional(inner) => write!(f, "O {inner}"),
Ty::List(inner) => write!(f, "L {inner}"),
Ty::Map(k, v) => write!(f, "M {k} {v}"),
Ty::Result(ok, err) => write!(f, "R {ok} {err}"),
Ty::Sum(variants) => {
write!(f, "S")?;
for v in variants { write!(f, " {v}")?; }
Ok(())
}
Ty::Fn(params, ret) => {
write!(f, "F")?;
for p in params { write!(f, " {p}")?; }
write!(f, " {ret}")
}
Ty::Named(name) => write!(f, "{name}"),
Ty::Unknown => write!(f, "?"),
}
}
}
#[derive(Debug, Clone)]
pub struct VerifyError {
pub code: &'static str,
pub function: String,
pub message: String,
pub hint: Option<String>,
pub span: Option<Span>,
pub is_warning: bool,
}
impl std::fmt::Display for VerifyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "verify: {} in '{}'", self.message, self.function)?;
if let Some(hint) = &self.hint {
write!(f, "\n hint: {hint}")?;
}
Ok(())
}
}
struct FuncSig {
params: Vec<(String, Ty)>,
return_type: Ty,
}
#[derive(Clone)]
struct TypeDef {
fields: Vec<(String, Ty)>,
}
struct VerifyContext {
functions: HashMap<String, FuncSig>,
types: HashMap<String, TypeDef>,
aliases: HashMap<String, Ty>,
errors: Vec<VerifyError>,
in_loop: bool,
}
type Scope = Vec<HashMap<String, Ty>>;
fn scope_lookup(scope: &Scope, name: &str) -> Option<Ty> {
for frame in scope.iter().rev() {
if let Some(ty) = frame.get(name) {
return Some(ty.clone());
}
}
None
}
fn scope_insert(scope: &mut Scope, name: String, ty: Ty) {
if let Some(frame) = scope.last_mut() {
frame.insert(name, ty);
}
}
fn collect_named_refs(ty: &Type) -> Vec<String> {
let mut refs = Vec::new();
collect_named_refs_inner(ty, &mut refs);
refs
}
fn collect_named_refs_inner(ty: &Type, refs: &mut Vec<String>) {
match ty {
Type::Named(name) => refs.push(name.clone()),
Type::Optional(inner) => collect_named_refs_inner(inner, refs),
Type::List(inner) => collect_named_refs_inner(inner, refs),
Type::Map(k, v) => {
collect_named_refs_inner(k, refs);
collect_named_refs_inner(v, refs);
}
Type::Result(ok, err) => {
collect_named_refs_inner(ok, refs);
collect_named_refs_inner(err, refs);
}
Type::Fn(params, ret) => {
for p in params { collect_named_refs_inner(p, refs); }
collect_named_refs_inner(ret, refs);
}
Type::Sum(_) | Type::Number | Type::Text | Type::Bool | Type::Nil => {}
}
}
#[allow(dead_code)] fn convert_type(ast_ty: &Type) -> Ty {
convert_type_with_aliases(ast_ty, &HashMap::new())
}
fn convert_type_with_aliases(ast_ty: &Type, aliases: &HashMap<String, Ty>) -> Ty {
match ast_ty {
Type::Number => Ty::Number,
Type::Text => Ty::Text,
Type::Bool => Ty::Bool,
Type::Nil => Ty::Nil,
Type::Optional(inner) => Ty::Optional(Box::new(convert_type_with_aliases(inner, aliases))),
Type::List(inner) => Ty::List(Box::new(convert_type_with_aliases(inner, aliases))),
Type::Map(k, v) => Ty::Map(
Box::new(convert_type_with_aliases(k, aliases)),
Box::new(convert_type_with_aliases(v, aliases)),
),
Type::Result(ok, err) => Ty::Result(
Box::new(convert_type_with_aliases(ok, aliases)),
Box::new(convert_type_with_aliases(err, aliases)),
),
Type::Sum(variants) => Ty::Sum(variants.clone()),
Type::Fn(params, ret) => Ty::Fn(
params.iter().map(|p| convert_type_with_aliases(p, aliases)).collect(),
Box::new(convert_type_with_aliases(ret, aliases)),
),
Type::Named(name) => {
if let Some(resolved) = aliases.get(name) {
resolved.clone()
} else if name.len() == 1
&& name.chars().next().is_some_and(|c| c.is_lowercase())
&& !matches!(name.as_str(), "n" | "t" | "b")
{
Ty::Unknown
} else {
Ty::Named(name.clone())
}
}
}
}
fn compatible(a: &Ty, b: &Ty) -> bool {
match (a, b) {
(Ty::Unknown, _) | (_, Ty::Unknown) => true,
(Ty::Nil, Ty::Optional(_)) | (Ty::Optional(_), Ty::Nil) => true,
(Ty::Optional(a), Ty::Optional(b)) => compatible(a, b),
(inner, Ty::Optional(b)) => compatible(inner, b),
(Ty::Optional(a), inner) => compatible(a, inner),
(Ty::Sum(_), Ty::Text) | (Ty::Text, Ty::Sum(_)) => true,
(Ty::Sum(a), Ty::Sum(b)) => a == b,
(Ty::Number, Ty::Number) => true,
(Ty::Text, Ty::Text) => true,
(Ty::Bool, Ty::Bool) => true,
(Ty::Nil, Ty::Nil) => true,
(Ty::List(a), Ty::List(b)) => compatible(a, b),
(Ty::Map(ak, av), Ty::Map(bk, bv)) => compatible(ak, bk) && compatible(av, bv),
(Ty::Result(ao, ae), Ty::Result(bo, be)) => compatible(ao, bo) && compatible(ae, be),
(Ty::Fn(ap, ar), Ty::Fn(bp, br)) => {
ap.len() == bp.len()
&& ap.iter().zip(bp).all(|(a, b)| compatible(a, b))
&& compatible(ar, br)
}
(Ty::Named(a), Ty::Named(b)) => a == b,
_ => false,
}
}
fn closest_match<'a>(name: &str, candidates: impl Iterator<Item = &'a String>) -> Option<String> {
let mut best: Option<(String, usize)> = None;
for candidate in candidates {
let dist = levenshtein(name, candidate);
if dist <= 3 && best.as_ref().is_none_or(|(_, d)| dist < *d) {
best = Some((candidate.clone(), dist));
}
}
best.map(|(s, _)| s)
}
fn levenshtein(a: &str, b: &str) -> usize {
let a: Vec<char> = a.chars().collect();
let b: Vec<char> = b.chars().collect();
let (m, n) = (a.len(), b.len());
let mut dp = vec![vec![0usize; n + 1]; m + 1];
for (i, row) in dp.iter_mut().enumerate().take(m + 1) { row[0] = i; }
for (j, val) in dp[0].iter_mut().enumerate().take(n + 1) { *val = j; }
for i in 1..=m {
for j in 1..=n {
let cost = if a[i - 1] == b[j - 1] { 0 } else { 1 };
dp[i][j] = (dp[i - 1][j] + 1)
.min(dp[i][j - 1] + 1)
.min(dp[i - 1][j - 1] + cost);
}
}
dp[m][n]
}
const BUILTINS: &[(&str, &[&str], &str)] = &[
("len", &["list_or_text"], "n"),
("str", &["n"], "t"),
("num", &["t"], "R n t"),
("abs", &["n"], "n"),
("flr", &["n"], "n"),
("cel", &["n"], "n"),
("min", &["n", "n"], "n"),
("max", &["n", "n"], "n"),
("mod", &["n", "n"], "n"),
("get", &["t"], "R t t"),
("get", &["t", "M t t"], "R t t"),
("post", &["t", "t"], "R t t"),
("post", &["t", "t", "M t t"], "R t t"),
("rd", &["t"], "R ? t"),
("rd", &["t", "t"], "R ? t"),
("rdl", &["t"], "R (L t) t"),
("rdb", &["t", "t"], "R ? t"),
("wr", &["t", "t"], "R t t"),
("wrl", &["t", "L t"], "R t t"),
("trm", &["t"], "t"),
("spl", &["t", "t"], "L t"),
("cat", &["L t", "t"], "t"),
("has", &["list_or_text", "any"], "b"),
("hd", &["list_or_text"], "any"),
("tl", &["list_or_text"], "list_or_text"),
("rev", &["list_or_text"], "list_or_text"),
("srt", &["list_or_text"], "list_or_text"),
("srt", &["fn", "list"], "list"),
("unq", &["list_or_text"], "list_or_text"),
("slc", &["list_or_text", "n", "n"], "list_or_text"),
("rnd", &[], "n"),
("now", &[], "n"),
("env", &["t"], "R t t"),
("jpth", &["t", "t"], "R t t"),
("jdmp", &["any"], "t"),
("prnt", &["any"], "any"),
("fmt", &["t"], "t"), ("jpar", &["t"], "R ? t"),
("map", &["fn", "list"], "list"),
("flt", &["fn", "list"], "list"),
("fld", &["fn", "list", "any"], "any"),
("grp", &["fn", "list"], "map"),
("flat", &["list"], "list"),
("sum", &["list"], "n"),
("avg", &["list"], "n"),
("rgx", &["t", "t"], "L t"),
("mmap", &[], "map"),
("mget", &["map", "t"], "optional"),
("mset", &["map", "t", "any"], "map"),
("mhas", &["map", "t"], "b"),
("mkeys", &["map"], "L t"),
("mvals", &["map"], "list"),
("mdel", &["map", "t"], "map"),
];
fn builtin_arity(name: &str) -> Option<usize> {
BUILTINS.iter().find(|(n, _, _)| *n == name).map(|(_, params, _)| params.len())
}
fn is_builtin(name: &str) -> bool {
BUILTINS.iter().any(|(n, _, _)| *n == name)
}
fn builtin_check_args(name: &str, arg_types: &[Ty], func_ctx: &str, span: Option<Span>) -> (Ty, Vec<VerifyError>) {
let mut errors = Vec::new();
match name {
"len" => {
if let Some(arg) = arg_types.first() {
match arg {
Ty::List(_) | Ty::Map(_, _) | Ty::Text | Ty::Unknown => {}
other => errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'len' expects a list, map, or text, got {other}"),
hint: None,
span,
is_warning: false,
}),
}
}
(Ty::Number, errors)
}
"str" => {
if let Some(arg) = arg_types.first()
&& !compatible(arg, &Ty::Number)
{
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'str' expects n, got {arg}"),
hint: None,
span,
is_warning: false,
});
}
(Ty::Text, errors)
}
"num" => {
if let Some(arg) = arg_types.first()
&& !compatible(arg, &Ty::Text)
{
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'num' expects t, got {arg}"),
hint: None,
span,
is_warning: false,
});
}
(Ty::Result(Box::new(Ty::Number), Box::new(Ty::Text)), errors)
}
"abs" | "flr" | "cel" => {
if let Some(arg) = arg_types.first()
&& !compatible(arg, &Ty::Number)
{
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'{name}' expects n, got {arg}"),
hint: None,
span,
is_warning: false,
});
}
(Ty::Number, errors)
}
"min" | "max" | "mod" => {
for (i, arg) in arg_types.iter().enumerate() {
if !compatible(arg, &Ty::Number) {
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'{name}' arg {} expects n, got {arg}", i + 1),
hint: None,
span,
is_warning: false,
});
}
}
(Ty::Number, errors)
}
"rnd" => {
for (i, arg) in arg_types.iter().enumerate() {
if !compatible(arg, &Ty::Number) {
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'rnd' arg {} expects n, got {arg}", i + 1),
hint: None,
span,
is_warning: false,
});
}
}
(Ty::Number, errors)
}
"spl" => {
for (i, arg) in arg_types.iter().enumerate() {
if !compatible(arg, &Ty::Text) {
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'spl' arg {} expects t, got {arg}", i + 1),
hint: None,
span,
is_warning: false,
});
}
}
(Ty::List(Box::new(Ty::Text)), errors)
}
"cat" => {
if let Some(arg) = arg_types.first()
&& !compatible(arg, &Ty::List(Box::new(Ty::Text)))
{
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'cat' arg 1 expects L t, got {arg}"),
hint: None,
span,
is_warning: false,
});
}
if let Some(arg) = arg_types.get(1)
&& !compatible(arg, &Ty::Text)
{
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'cat' arg 2 expects t, got {arg}"),
hint: None,
span,
is_warning: false,
});
}
(Ty::Text, errors)
}
"has" => {
if let Some(arg) = arg_types.first() {
match arg {
Ty::List(_) | Ty::Text | Ty::Unknown => {}
other => errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'has' arg 1 expects a list or text, got {other}"),
hint: None,
span,
is_warning: false,
}),
}
}
(Ty::Bool, errors)
}
"hd" => {
if let Some(arg) = arg_types.first() {
match arg {
Ty::List(inner) => return (*inner.clone(), errors),
Ty::Text => return (Ty::Text, errors),
Ty::Unknown => return (Ty::Unknown, errors),
other => errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'hd' expects a list or text, got {other}"),
hint: None,
span,
is_warning: false,
}),
}
}
(Ty::Unknown, errors)
}
"tl" => {
if let Some(arg) = arg_types.first() {
match arg {
Ty::List(inner) => return (Ty::List(inner.clone()), errors),
Ty::Text => return (Ty::Text, errors),
Ty::Unknown => return (Ty::Unknown, errors),
other => errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'tl' expects a list or text, got {other}"),
hint: None,
span,
is_warning: false,
}),
}
}
(Ty::Unknown, errors)
}
"rev" => {
if let Some(arg) = arg_types.first() {
match arg {
Ty::List(_) | Ty::Text | Ty::Unknown => {}
other => errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'rev' expects a list or text, got {other}"),
hint: None,
span,
is_warning: false,
}),
}
}
let ret = match arg_types.first() {
Some(Ty::List(inner)) => Ty::List(inner.clone()),
Some(Ty::Text) => Ty::Text,
_ => Ty::Unknown,
};
(ret, errors)
}
"trm" => {
if let Some(arg) = arg_types.first()
&& !compatible(arg, &Ty::Text)
{
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'trm' expects t, got {arg}"),
hint: None,
span,
is_warning: false,
});
}
(Ty::Text, errors)
}
"unq" => {
if let Some(arg) = arg_types.first() {
match arg {
Ty::List(_) | Ty::Text | Ty::Unknown => {}
other => errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'unq' expects a list or text, got {other}"),
hint: None,
span,
is_warning: false,
}),
}
}
let ret = match arg_types.first() {
Some(Ty::List(inner)) => Ty::List(inner.clone()),
Some(Ty::Text) => Ty::Text,
_ => Ty::Unknown,
};
(ret, errors)
}
"fmt" => {
if let Some(arg) = arg_types.first()
&& !compatible(arg, &Ty::Text)
{
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'fmt' first arg must be a text template, got {arg}"),
hint: None,
span,
is_warning: false,
});
}
(Ty::Text, errors)
}
"srt" => {
if arg_types.len() == 2 {
if let Some(fn_ty) = arg_types.first()
&& !matches!(fn_ty, Ty::Fn(_, _) | Ty::Unknown)
{
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'srt' key arg must be a function (F ...), got {fn_ty}"),
hint: Some("pass a function name: srt key-fn xs".to_string()),
span,
is_warning: false,
});
}
let ret = match arg_types.get(1) {
Some(ty @ Ty::List(_)) => ty.clone(),
_ => Ty::Unknown,
};
return (ret, errors);
}
if let Some(arg) = arg_types.first() {
match arg {
Ty::List(inner) => return (Ty::List(inner.clone()), errors),
Ty::Text => return (Ty::Text, errors),
Ty::Unknown => return (Ty::Unknown, errors),
other => errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'srt' expects a list or text, got {other}"),
hint: None,
span,
is_warning: false,
}),
}
}
(Ty::Unknown, errors)
}
"slc" => {
if let Some(arg) = arg_types.first() {
match arg {
Ty::List(_) | Ty::Text | Ty::Unknown => {}
other => errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'slc' expects a list or text, got {other}"),
hint: None,
span,
is_warning: false,
}),
}
}
for (i, idx) in [1usize, 2].iter().enumerate() {
if let Some(arg) = arg_types.get(*idx)
&& !compatible(arg, &Ty::Number)
{
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'slc' arg {} expects n, got {arg}", i + 2),
hint: None,
span,
is_warning: false,
});
}
}
let ret = match arg_types.first() {
Some(Ty::List(inner)) => Ty::List(inner.clone()),
Some(Ty::Text) => Ty::Text,
_ => Ty::Unknown,
};
(ret, errors)
}
"get" => {
if let Some(arg) = arg_types.first()
&& !compatible(arg, &Ty::Text)
{
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'get' expects t (url), got {arg}"),
hint: None,
span,
is_warning: false,
});
}
if let Some(arg) = arg_types.get(1) {
let map_ty = Ty::Map(Box::new(Ty::Text), Box::new(Ty::Text));
if !compatible(arg, &map_ty) {
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'get' headers arg expects M t t, got {arg}"),
hint: None,
span,
is_warning: false,
});
}
}
(Ty::Result(Box::new(Ty::Text), Box::new(Ty::Text)), errors)
}
"post" => {
for (i, arg) in arg_types.iter().enumerate().take(2) {
if !compatible(arg, &Ty::Text) {
let label = if i == 0 { "url" } else { "body" };
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'post' expects t ({label}), got {arg}"),
hint: None,
span,
is_warning: false,
});
}
}
if let Some(arg) = arg_types.get(2) {
let map_ty = Ty::Map(Box::new(Ty::Text), Box::new(Ty::Text));
if !compatible(arg, &map_ty) {
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'post' headers arg expects M t t, got {arg}"),
hint: None,
span,
is_warning: false,
});
}
}
(Ty::Result(Box::new(Ty::Text), Box::new(Ty::Text)), errors)
}
"rd" | "rdb" => {
if let Some(arg) = arg_types.first()
&& !compatible(arg, &Ty::Text)
{
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'{name}' expects t (path/string), got {arg}"),
hint: None,
span,
is_warning: false,
});
}
if arg_types.len() == 2
&& let Some(fmt) = arg_types.get(1)
&& !compatible(fmt, &Ty::Text)
{
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'{name}' format arg expects t (\"csv\", \"json\", \"raw\"…), got {fmt}"),
hint: None,
span,
is_warning: false,
});
}
(Ty::Result(Box::new(Ty::Unknown), Box::new(Ty::Text)), errors)
}
"rdl" => {
if let Some(arg) = arg_types.first()
&& !compatible(arg, &Ty::Text)
{
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'rdl' expects t (path), got {arg}"),
hint: None,
span,
is_warning: false,
});
}
(Ty::Result(Box::new(Ty::List(Box::new(Ty::Text))), Box::new(Ty::Text)), errors)
}
"wr" | "wrl" => {
if let Some(arg) = arg_types.first()
&& !compatible(arg, &Ty::Text)
{
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'{name}' arg 1 expects t (path), got {arg}"),
hint: None,
span,
is_warning: false,
});
}
if name == "wr"
&& let Some(arg) = arg_types.get(1)
&& !compatible(arg, &Ty::Text)
{
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'wr' arg 2 expects t (content), got {arg}"),
hint: None,
span,
is_warning: false,
});
}
(Ty::Result(Box::new(Ty::Text), Box::new(Ty::Text)), errors)
}
"jpth" => {
for (i, arg) in arg_types.iter().enumerate() {
if !compatible(arg, &Ty::Text) {
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'jpth' arg {} expects t, got {arg}", i + 1),
hint: None,
span,
is_warning: false,
});
}
}
(Ty::Result(Box::new(Ty::Text), Box::new(Ty::Text)), errors)
}
"jdmp" => {
(Ty::Text, errors)
}
"prnt" => {
(arg_types.first().cloned().unwrap_or(Ty::Unknown), errors)
}
"jpar" => {
if let Some(arg) = arg_types.first()
&& !compatible(arg, &Ty::Text)
{
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'jpar' expects t, got {arg}"),
hint: None,
span,
is_warning: false,
});
}
(Ty::Result(Box::new(Ty::Unknown), Box::new(Ty::Text)), errors)
}
"map" => {
if let Some(fn_ty) = arg_types.first()
&& !matches!(fn_ty, Ty::Fn(_, _) | Ty::Unknown) {
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'map' first arg must be a function (F ...), got {fn_ty}"),
hint: Some("pass a function name: map sq xs".to_string()),
span,
is_warning: false,
});
}
let ret_elem = match arg_types.first() {
Some(Ty::Fn(_, ret)) => *ret.clone(),
_ => Ty::Unknown,
};
(Ty::List(Box::new(ret_elem)), errors)
}
"flt" => {
if let Some(fn_ty) = arg_types.first()
&& !matches!(fn_ty, Ty::Fn(_, _) | Ty::Unknown) {
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'flt' first arg must be a function (F ...), got {fn_ty}"),
hint: Some("pass a function name: flt pred xs".to_string()),
span,
is_warning: false,
});
}
let ret = match arg_types.get(1) {
Some(ty @ Ty::List(_)) => ty.clone(),
_ => Ty::Unknown,
};
(ret, errors)
}
"fld" => {
if let Some(fn_ty) = arg_types.first()
&& !matches!(fn_ty, Ty::Fn(_, _) | Ty::Unknown) {
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'fld' first arg must be a function (F ...), got {fn_ty}"),
hint: Some("pass a function name: fld f xs init".to_string()),
span,
is_warning: false,
});
}
let ret = match arg_types.get(2) {
Some(ty) if !matches!(ty, Ty::Unknown) => ty.clone(),
_ => match arg_types.first() {
Some(Ty::Fn(_, ret)) => *ret.clone(),
_ => Ty::Unknown,
},
};
(ret, errors)
}
"grp" => {
if let Some(fn_ty) = arg_types.first()
&& !matches!(fn_ty, Ty::Fn(_, _) | Ty::Unknown) {
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'grp' first arg must be a function (F ...), got {fn_ty}"),
hint: Some("pass a function name: grp key-fn xs".to_string()),
span,
is_warning: false,
});
}
let key_ty = match arg_types.first() {
Some(Ty::Fn(_, ret)) => *ret.clone(),
_ => Ty::Unknown,
};
let elem_ty = match arg_types.get(1) {
Some(Ty::List(inner)) => *inner.clone(),
_ => Ty::Unknown,
};
(Ty::Map(Box::new(key_ty), Box::new(Ty::List(Box::new(elem_ty)))), errors)
}
"flat" => {
let inner = match arg_types.first() {
Some(Ty::List(inner)) => match inner.as_ref() {
Ty::List(elem) => *elem.clone(),
_ => Ty::Unknown,
},
_ => Ty::Unknown,
};
(Ty::List(Box::new(inner)), errors)
}
"mmap" => (Ty::Map(Box::new(Ty::Unknown), Box::new(Ty::Unknown)), errors),
"mget" => {
let val_ty = match arg_types.first() {
Some(Ty::Map(_, v)) => *v.clone(),
_ => Ty::Unknown,
};
if let Some(key_ty) = arg_types.get(1)
&& !compatible(key_ty, &Ty::Text)
{
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'mget' key must be t, got {key_ty}"),
hint: None,
span,
is_warning: false,
});
}
(Ty::Optional(Box::new(val_ty)), errors)
}
"mset" => {
let map_ty = match arg_types.first() {
Some(Ty::Map(k, _)) => {
let val_ty = arg_types.get(2).cloned().unwrap_or(Ty::Unknown);
Ty::Map(k.clone(), Box::new(val_ty))
}
_ => Ty::Map(Box::new(Ty::Unknown), Box::new(Ty::Unknown)),
};
(map_ty, errors)
}
"mhas" => {
if let Some(first) = arg_types.first()
&& !matches!(first, Ty::Map(_, _) | Ty::Unknown)
{
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'mhas' expects a map, got {first}"),
hint: None,
span,
is_warning: false,
});
}
(Ty::Bool, errors)
}
"mkeys" => {
if let Some(first) = arg_types.first()
&& !matches!(first, Ty::Map(_, _) | Ty::Unknown)
{
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'mkeys' expects a map, got {first}"),
hint: None,
span,
is_warning: false,
});
}
(Ty::List(Box::new(Ty::Text)), errors)
}
"mvals" => {
let val_ty = match arg_types.first() {
Some(Ty::Map(_, v)) => *v.clone(),
_ => Ty::Unknown,
};
(Ty::List(Box::new(val_ty)), errors)
}
"mdel" => {
let map_ty = match arg_types.first() {
Some(ty @ Ty::Map(_, _)) => ty.clone(),
_ => Ty::Map(Box::new(Ty::Unknown), Box::new(Ty::Unknown)),
};
(map_ty, errors)
}
_ => (Ty::Unknown, errors),
}
}
impl VerifyContext {
fn new() -> Self {
Self {
functions: HashMap::new(),
types: HashMap::new(),
aliases: HashMap::new(),
errors: Vec::new(),
in_loop: false,
}
}
fn err(&mut self, code: &'static str, function: &str, message: String, hint: Option<String>, span: Option<Span>) {
self.errors.push(VerifyError {
code,
function: function.to_string(),
message,
hint,
span,
is_warning: false,
});
}
fn warn(&mut self, code: &'static str, function: &str, message: String, hint: Option<String>, span: Option<Span>) {
self.errors.push(VerifyError {
code,
function: function.to_string(),
message,
hint,
span,
is_warning: true,
});
}
fn collect_declarations(&mut self, program: &Program) {
let builtin_type_names = ["n", "t", "b", "L", "R"];
let mut raw_aliases: HashMap<String, Type> = HashMap::new();
for decl in &program.declarations {
if let Decl::Alias { name, target, span } = decl {
if builtin_type_names.contains(&name.as_str()) || name == "_" {
self.err("ILO-T031", "<global>",
format!("type alias '{name}' shadows a builtin type"),
Some("choose a different name for the alias".to_string()),
Some(*span));
continue;
}
if raw_aliases.contains_key(name) {
self.err("ILO-T001", "<global>", format!("duplicate type alias '{name}'"), None, Some(*span));
} else {
raw_aliases.insert(name.clone(), target.clone());
}
}
}
self.resolve_aliases(&raw_aliases);
for decl in &program.declarations {
if let Decl::TypeDef { name, fields, .. } = decl {
if self.aliases.contains_key(name) {
self.err("ILO-T001", "<global>", format!("type '{name}' conflicts with type alias of the same name"), None, None);
} else if self.types.contains_key(name) {
self.err("ILO-T001", "<global>", format!("duplicate type definition '{name}'"), None, None);
} else {
let fields: Vec<(String, Ty)> = fields
.iter()
.map(|p| (p.name.clone(), convert_type_with_aliases(&p.ty, &self.aliases)))
.collect();
self.types.insert(name.clone(), TypeDef { fields });
}
}
}
for decl in &program.declarations {
match decl {
Decl::Function { name, params, return_type, .. } => {
if self.functions.contains_key(name) {
self.err("ILO-T002", "<global>", format!("duplicate function definition '{name}'"), None, None);
continue;
}
let params: Vec<(String, Ty)> = params
.iter()
.map(|p| (p.name.clone(), convert_type_with_aliases(&p.ty, &self.aliases)))
.collect();
let ret = convert_type_with_aliases(return_type, &self.aliases);
self.validate_named_types_in_sig(name, ¶ms, &ret);
self.functions.insert(name.clone(), FuncSig { params, return_type: ret });
}
Decl::Tool { name, params, return_type, .. } => {
if self.functions.contains_key(name) {
self.err("ILO-T002", "<global>", format!("duplicate definition '{name}' (tool conflicts with function)"), None, None);
continue;
}
let params: Vec<(String, Ty)> = params
.iter()
.map(|p| (p.name.clone(), convert_type_with_aliases(&p.ty, &self.aliases)))
.collect();
let ret = convert_type_with_aliases(return_type, &self.aliases);
self.validate_named_types_in_sig(name, ¶ms, &ret);
self.functions.insert(name.clone(), FuncSig { params, return_type: ret });
}
Decl::TypeDef { .. } => {} Decl::Alias { .. } => {} Decl::Use { .. } => {} Decl::Error { .. } => {} }
}
for decl in &program.declarations {
if let Decl::TypeDef { name, fields, .. } = decl {
for field in fields {
self.validate_named_type_recursive(&convert_type_with_aliases(&field.ty, &self.aliases), name);
}
}
}
}
fn resolve_aliases(&mut self, raw: &HashMap<String, Type>) {
use std::collections::HashSet;
let deps: HashMap<String, Vec<String>> = raw.iter().map(|(name, target)| {
let refs: Vec<String> = collect_named_refs(target)
.into_iter()
.filter(|r| raw.contains_key(r))
.collect();
(name.clone(), refs)
}).collect();
let mut in_cycle: HashSet<String> = HashSet::new();
for name in raw.keys() {
let mut visited = HashSet::new();
let mut stack = HashSet::new();
if Self::has_cycle(name, &deps, &mut visited, &mut stack) {
for n in &stack {
in_cycle.insert(n.clone());
}
}
}
for name in &in_cycle {
self.err("ILO-T030", "<global>",
format!("circular type alias '{name}'"),
Some("type aliases cannot reference each other in a cycle".to_string()),
None);
}
for name in raw.keys() {
if !in_cycle.contains(name) && !self.aliases.contains_key(name) {
self.resolve_alias_recursive(name, raw);
}
}
}
fn has_cycle(
name: &str,
deps: &HashMap<String, Vec<String>>,
visited: &mut std::collections::HashSet<String>,
stack: &mut std::collections::HashSet<String>,
) -> bool {
if stack.contains(name) {
return true; }
if visited.contains(name) {
return false; }
visited.insert(name.to_string());
stack.insert(name.to_string());
if let Some(neighbors) = deps.get(name) {
for dep in neighbors {
if Self::has_cycle(dep, deps, visited, stack) {
return true;
}
}
}
stack.remove(name);
false
}
fn resolve_alias_recursive(&mut self, name: &str, raw: &HashMap<String, Type>) {
if self.aliases.contains_key(name) {
return;
}
if let Some(target) = raw.get(name) {
let deps = collect_named_refs(target);
for dep in &deps {
if raw.contains_key(dep) && !self.aliases.contains_key(dep) {
self.resolve_alias_recursive(dep, raw);
}
}
let resolved = convert_type_with_aliases(target, &self.aliases);
self.aliases.insert(name.to_string(), resolved);
}
}
fn validate_named_types_in_sig(&mut self, func_name: &str, params: &[(String, Ty)], ret: &Ty) {
for (_, ty) in params {
self.validate_named_type_recursive(ty, func_name);
}
self.validate_named_type_recursive(ret, func_name);
}
fn validate_named_type_recursive(&mut self, ty: &Ty, ctx: &str) {
match ty {
Ty::Named(name) => {
if !self.types.contains_key(name) {
let hint = closest_match(name, self.types.keys())
.map(|s| format!("did you mean '{s}'?"));
self.err("ILO-T003", ctx, format!("undefined type '{name}'"), hint, None);
}
}
Ty::List(inner) => self.validate_named_type_recursive(inner, ctx),
Ty::Result(ok, err) => {
self.validate_named_type_recursive(ok, ctx);
self.validate_named_type_recursive(err, ctx);
}
_ => {}
}
}
fn verify_bodies(&mut self, program: &Program) {
for decl in &program.declarations {
if let Decl::Function { name, params, return_type, body, .. } = decl {
let mut scope: Scope = vec![HashMap::new()];
for p in params {
scope_insert(&mut scope, p.name.clone(), convert_type_with_aliases(&p.ty, &self.aliases));
}
let body_ty = self.verify_body(name, &mut scope, body);
let expected = convert_type_with_aliases(return_type, &self.aliases);
if !compatible(&body_ty, &expected) {
let hint = match (&body_ty, &expected) {
(Ty::Number, Ty::Text) => Some("use 'str' to convert: str <expr>".to_string()),
(Ty::Text, Ty::Number) => Some("use 'num' to parse text (returns R n t)".to_string()),
_ => None,
};
let last_span = body.last().map(|s| s.span);
self.err(
"ILO-T008",
name,
format!("return type mismatch: expected {expected}, got {body_ty}"),
hint,
last_span,
);
}
}
}
}
fn verify_body(&mut self, func: &str, scope: &mut Scope, stmts: &[Spanned<Stmt>]) -> Ty {
let mut last_ty = Ty::Nil;
for (i, spanned) in stmts.iter().enumerate() {
last_ty = self.verify_stmt(func, scope, &spanned.node, spanned.span);
if matches!(spanned.node, Stmt::Return(_) | Stmt::Break(_)) && i + 1 < stmts.len() {
let first_unreachable = stmts[i + 1].span;
let last_unreachable = stmts.last().unwrap().span;
let span = first_unreachable.merge(last_unreachable);
let kind = match &spanned.node {
Stmt::Return(_) => "ret",
Stmt::Break(_) => "brk",
_ => unreachable!(),
};
self.warn(
"ILO-T029",
func,
format!("unreachable code after '{kind}'"),
None,
Some(span),
);
break;
}
}
last_ty
}
fn verify_stmt(&mut self, func: &str, scope: &mut Scope, stmt: &Stmt, span: Span) -> Ty {
match stmt {
Stmt::Let { name, value } => {
let ty = self.infer_expr(func, scope, value, span);
scope_insert(scope, name.clone(), ty);
Ty::Nil
}
Stmt::Destructure { bindings, value } => {
let record_ty = self.infer_expr(func, scope, value, span);
match &record_ty {
Ty::Named(type_name) => {
if let Some(type_def) = self.types.get(type_name).cloned() {
for binding in bindings {
if let Some((_, fty)) = type_def.fields.iter().find(|(n, _)| n == binding) {
scope_insert(scope, binding.clone(), fty.clone());
} else {
let field_names: Vec<String> = type_def.fields.iter().map(|(n, _)| n.clone()).collect();
let hint = closest_match(binding, field_names.iter())
.map(|s| format!("did you mean '{s}'?"));
self.err(
"ILO-T019",
func,
format!("no field '{binding}' on type '{type_name}'"),
hint,
Some(span),
);
scope_insert(scope, binding.clone(), Ty::Unknown);
}
}
} else {
for binding in bindings {
scope_insert(scope, binding.clone(), Ty::Unknown);
}
}
}
Ty::Unknown => {
for binding in bindings {
scope_insert(scope, binding.clone(), Ty::Unknown);
}
}
other => {
self.err(
"ILO-T009",
func,
format!("destructure requires a record type, got {other}"),
None,
Some(span),
);
for binding in bindings {
scope_insert(scope, binding.clone(), Ty::Unknown);
}
}
}
Ty::Nil
}
Stmt::Guard { condition, body, else_body, .. } => {
let _ = self.infer_expr(func, scope, condition, span);
if self.in_loop && else_body.is_none() {
self.warn(
"ILO-W001",
func,
"guard without else inside loop causes early function return, not iteration skip".to_string(),
Some("use ternary form: cond{then}{else} or brk/cnt for loop control".to_string()),
Some(span),
);
}
if body.len() == 1
&& let Stmt::Expr(Expr::Ref(ref name)) = body[0].node
&& (self.functions.contains_key(name) || is_builtin(name))
{
let body_span = body[0].span;
self.err(
"ILO-T027",
func,
format!("braceless guard body '{name}' is a function name — did you mean to call it?"),
Some(format!("use braces for function calls: cond{{{name} args}}")),
Some(body_span),
);
}
scope.push(HashMap::new());
let body_ty = self.verify_body(func, scope, body);
scope.pop();
if let Some(eb) = else_body {
scope.push(HashMap::new());
let _else_ty = self.verify_body(func, scope, eb);
scope.pop();
}
body_ty
}
Stmt::Match { subject, arms } => {
let subject_ty = match subject {
Some(expr) => self.infer_expr(func, scope, expr, span),
None => Ty::Nil,
};
let mut arm_ty = Ty::Unknown;
for arm in arms {
scope.push(HashMap::new());
self.bind_pattern(func, scope, &arm.pattern, &subject_ty);
let body_ty = self.verify_body(func, scope, &arm.body);
if arm_ty == Ty::Unknown {
arm_ty = body_ty;
}
scope.pop();
}
self.check_match_exhaustiveness(func, &subject_ty, arms, span);
arm_ty
}
Stmt::ForEach { binding, collection, body } => {
let coll_ty = self.infer_expr(func, scope, collection, span);
let elem_ty = match &coll_ty {
Ty::List(inner) => *inner.clone(),
Ty::Unknown => Ty::Unknown,
other => {
self.err("ILO-T014", func, format!("foreach expects a list, got {other}"), None, Some(span));
Ty::Unknown
}
};
scope.push(HashMap::new());
scope_insert(scope, binding.clone(), elem_ty);
let prev = self.in_loop;
self.in_loop = true;
let body_ty = self.verify_body(func, scope, body);
self.in_loop = prev;
scope.pop();
body_ty
}
Stmt::ForRange { binding, start, end, body } => {
let start_ty = self.infer_expr(func, scope, start, span);
let end_ty = self.infer_expr(func, scope, end, span);
if !compatible(&start_ty, &Ty::Number) {
self.err("ILO-T014", func, format!("range start must be n, got {start_ty}"), None, Some(span));
}
if !compatible(&end_ty, &Ty::Number) {
self.err("ILO-T014", func, format!("range end must be n, got {end_ty}"), None, Some(span));
}
scope.push(HashMap::new());
scope_insert(scope, binding.clone(), Ty::Number);
let prev = self.in_loop;
self.in_loop = true;
let body_ty = self.verify_body(func, scope, body);
self.in_loop = prev;
scope.pop();
body_ty
}
Stmt::While { condition, body } => {
self.infer_expr(func, scope, condition, span);
let prev = self.in_loop;
self.in_loop = true;
let body_ty = self.verify_body(func, scope, body);
self.in_loop = prev;
body_ty
}
Stmt::Return(expr) => self.infer_expr(func, scope, expr, span),
Stmt::Break(expr) => {
if !self.in_loop {
self.err("ILO-T028", func, "brk can only be used inside a loop (@/wh)".to_string(), None, Some(span));
}
if let Some(e) = expr {
self.infer_expr(func, scope, e, span)
} else {
Ty::Nil
}
}
Stmt::Continue => {
if !self.in_loop {
self.err("ILO-T028", func, "cnt can only be used inside a loop (@/wh)".to_string(), None, Some(span));
}
Ty::Nil
}
Stmt::Expr(expr) => self.infer_expr(func, scope, expr, span),
}
}
fn bind_pattern(&mut self, _func: &str, scope: &mut Scope, pattern: &Pattern, subject_ty: &Ty) {
match pattern {
Pattern::Ok(name) => {
if name != "_" {
let ty = match subject_ty {
Ty::Result(ok, _) => *ok.clone(),
Ty::Unknown => Ty::Unknown,
_ => Ty::Unknown,
};
scope_insert(scope, name.clone(), ty);
}
}
Pattern::Err(name) => {
if name != "_" {
let ty = match subject_ty {
Ty::Result(_, err) => *err.clone(),
Ty::Unknown => Ty::Unknown,
_ => Ty::Unknown,
};
scope_insert(scope, name.clone(), ty);
}
}
Pattern::Literal(_) | Pattern::Wildcard => {}
Pattern::TypeIs { ty, binding } => {
if binding != "_" {
let bound_ty = match ty {
Type::Number => Ty::Number,
Type::Text => Ty::Text,
Type::Bool => Ty::Bool,
Type::List(_) => Ty::List(Box::new(Ty::Unknown)),
_ => Ty::Unknown,
};
scope_insert(scope, binding.clone(), bound_ty);
}
}
}
}
fn infer_expr(&mut self, func: &str, scope: &mut Scope, expr: &Expr, span: Span) -> Ty {
match expr {
Expr::Literal(lit) => match lit {
Literal::Number(_) => Ty::Number,
Literal::Text(_) => Ty::Text,
Literal::Bool(_) => Ty::Bool,
},
Expr::Ref(name) => {
if let Some(ty) = scope_lookup(scope, name) {
ty
} else if let Some(sig) = self.functions.get(name) {
let params: Vec<Ty> = sig.params.iter().map(|(_, t)| t.clone()).collect();
Ty::Fn(params, Box::new(sig.return_type.clone()))
} else {
let mut candidates: Vec<String> = scope.iter()
.flat_map(|frame| frame.keys().cloned())
.collect();
candidates.extend(self.functions.keys().cloned());
let hint = closest_match(name, candidates.iter())
.map(|s| format!("did you mean '{s}'?"));
self.err("ILO-T004", func, format!("undefined variable '{name}'"), hint, Some(span));
Ty::Unknown
}
}
Expr::Call { function: callee, args, unwrap } => {
let arg_types: Vec<Ty> = args.iter().map(|a| self.infer_expr(func, scope, a, span)).collect();
let call_ty = if is_builtin(callee) {
let expected_arity = builtin_arity(callee).unwrap();
let arity_ok = if callee == "rnd" {
args.is_empty() || args.len() == 2
} else if callee == "srt" || callee == "rd" {
args.len() == 1 || args.len() == 2
} else if callee == "wr" {
args.len() == 2 || args.len() == 3
} else if callee == "get" {
args.len() == 1 || args.len() == 2
} else if callee == "post" {
args.len() == 2 || args.len() == 3
} else if callee == "fmt" {
!args.is_empty() } else {
args.len() == expected_arity
};
if !arity_ok {
let arity_desc = if callee == "rnd" {
"0 or 2".to_string()
} else if callee == "srt" || callee == "rd" || callee == "get" {
"1 or 2".to_string()
} else if callee == "post" {
"2 or 3".to_string()
} else {
expected_arity.to_string()
};
self.err(
"ILO-T006",
func,
format!("arity mismatch: '{callee}' expects {arity_desc} args, got {}", args.len()),
None,
Some(span),
);
return Ty::Unknown;
}
let (ret_ty, errors) = builtin_check_args(callee, &arg_types, func, Some(span));
self.errors.extend(errors);
ret_ty
} else if let Some(sig) = self.functions.get(callee) {
let sig_params = sig.params.clone();
let sig_ret = sig.return_type.clone();
if args.len() != sig_params.len() {
let hint = {
let sig_str: String = sig_params.iter()
.map(|(n, t)| format!("{n}:{t}"))
.collect::<Vec<_>>()
.join(" ");
Some(format!("'{callee}' expects: {sig_str}"))
};
self.err(
"ILO-T006",
func,
format!(
"arity mismatch: '{callee}' expects {} args, got {}",
sig_params.len(),
args.len()
),
hint,
Some(span),
);
return sig_ret;
}
for (i, ((param_name, param_ty), arg_ty)) in sig_params.iter().zip(arg_types.iter()).enumerate() {
if !compatible(param_ty, arg_ty) {
let hint = match (param_ty, arg_ty) {
(Ty::Text, Ty::Number) => Some("use 'str' to convert number to text".to_string()),
(Ty::Number, Ty::Text) => Some("use 'num' to parse text as number (returns R n t)".to_string()),
_ => None,
};
self.err(
"ILO-T007",
func,
format!(
"type mismatch: param '{}' of '{}' expects {}, got {}",
param_name, callee, param_ty, arg_ty
),
hint,
Some(span),
);
}
let _ = i;
}
sig_ret
} else if let Some(Ty::Fn(param_types, ret_type)) = scope_lookup(scope, callee) {
if args.len() != param_types.len() {
self.err(
"ILO-T006",
func,
format!("arity mismatch: function parameter '{callee}' expects {} args, got {}", param_types.len(), args.len()),
None,
Some(span),
);
} else {
for (i, (param_ty, arg_ty)) in param_types.iter().zip(arg_types.iter()).enumerate() {
if !compatible(param_ty, arg_ty) {
self.err(
"ILO-T007",
func,
format!("type mismatch: arg {} of '{callee}' expects {param_ty}, got {arg_ty}", i + 1),
None,
Some(span),
);
}
}
}
*ret_type
} else {
let mut candidates: Vec<String> = self.functions.keys().cloned().collect();
for (n, _, _) in BUILTINS {
candidates.push(n.to_string());
}
let hint = closest_match(callee, candidates.iter())
.map(|s| format!("did you mean '{s}'?"));
self.err(
"ILO-T005",
func,
format!("undefined function '{callee}' (called with {} args)", args.len()),
hint,
Some(span),
);
Ty::Unknown
};
if *unwrap {
match &call_ty {
Ty::Result(ok_ty, _err_ty) => {
let enc_rt = self.functions.get(func).map(|sig| sig.return_type.clone());
#[allow(for_loops_over_fallibles)]
for rt in enc_rt {
match rt {
Ty::Result(_, _) => {}
other => {
self.err(
"ILO-T026",
func,
format!("'!' used in function '{func}' which returns {other}, not a Result"),
Some("the enclosing function must return R to propagate errors".to_string()),
Some(span),
);
}
}
}
*ok_ty.clone()
}
Ty::Unknown => Ty::Unknown,
other => {
self.err(
"ILO-T025",
func,
format!("'!' used on call to '{callee}' which returns {other}, not a Result"),
Some("'!' auto-unwraps Result types: Ok(v)→v, Err(e)→propagate".to_string()),
Some(span),
);
Ty::Unknown
}
}
} else {
call_ty
}
}
Expr::BinOp { op, left, right } => {
let lt = self.infer_expr(func, scope, left, span);
let rt = self.infer_expr(func, scope, right, span);
self.check_binop(func, op, <, &rt, span)
}
Expr::UnaryOp { op, operand } => {
let t = self.infer_expr(func, scope, operand, span);
match op {
UnaryOp::Negate => {
if !compatible(&t, &Ty::Number) {
self.err("ILO-T012", func, format!("negate expects n, got {t}"), None, Some(span));
}
Ty::Number
}
UnaryOp::Not => {
Ty::Bool
}
}
}
Expr::Ok(inner) => {
let t = self.infer_expr(func, scope, inner, span);
Ty::Result(Box::new(t), Box::new(Ty::Unknown))
}
Expr::Err(inner) => {
let t = self.infer_expr(func, scope, inner, span);
Ty::Result(Box::new(Ty::Unknown), Box::new(t))
}
Expr::List(items) => {
if items.is_empty() {
Ty::List(Box::new(Ty::Unknown))
} else {
let first_ty = self.infer_expr(func, scope, &items[0], span);
for item in &items[1..] {
let _ = self.infer_expr(func, scope, item, span);
}
Ty::List(Box::new(first_ty))
}
}
Expr::Record { type_name, fields } => {
if let Some(type_def) = self.types.get(type_name) {
let def_fields = type_def.fields.clone();
let provided: HashMap<&str, &Expr> = fields.iter().map(|(n, e)| (n.as_str(), e)).collect();
for (fname, _) in &def_fields {
if !provided.contains_key(fname.as_str()) {
self.err(
"ILO-T015",
func,
format!("missing field '{fname}' in record '{type_name}'"),
None,
Some(span),
);
}
}
let def_field_names: Vec<&str> = def_fields.iter().map(|(n, _)| n.as_str()).collect();
for (fname, _) in fields {
if !def_field_names.contains(&fname.as_str()) {
let def_field_strings: Vec<String> = def_field_names.iter().map(|s| s.to_string()).collect();
let hint = closest_match(fname, def_field_strings.iter())
.map(|s| format!("did you mean '{s}'?"));
self.err(
"ILO-T016",
func,
format!("unknown field '{fname}' in record '{type_name}'"),
hint,
Some(span),
);
}
}
for (fname, fty) in &def_fields {
if let Some(expr) = provided.get(fname.as_str()) {
let actual = self.infer_expr(func, scope, expr, span);
if !compatible(fty, &actual) {
self.err(
"ILO-T017",
func,
format!("field '{fname}' of '{type_name}' expects {fty}, got {actual}"),
None,
Some(span),
);
}
}
}
Ty::Named(type_name.clone())
} else {
let hint = closest_match(type_name, self.types.keys())
.map(|s| format!("did you mean '{s}'?"));
self.err("ILO-T003", func, format!("undefined type '{type_name}'"), hint, Some(span));
Ty::Unknown
}
}
Expr::Field { object, field, safe } => {
let obj_ty = self.infer_expr(func, scope, object, span);
if *safe && obj_ty == Ty::Nil {
return Ty::Nil;
}
match &obj_ty {
Ty::Named(type_name) => {
if let Some(type_def) = self.types.get(type_name) {
if let Some((_, fty)) = type_def.fields.iter().find(|(n, _)| n == field) {
fty.clone()
} else {
let field_names: Vec<String> = type_def.fields.iter().map(|(n, _)| n.clone()).collect();
let hint = closest_match(field, field_names.iter())
.map(|s| format!("did you mean '{s}'?"));
self.err(
"ILO-T019",
func,
format!("no field '{field}' on type '{type_name}'"),
hint,
Some(span),
);
Ty::Unknown
}
} else {
Ty::Unknown
}
}
Ty::Unknown => Ty::Unknown,
other => {
self.err("ILO-T018", func, format!("field access on non-record type {other}"), None, Some(span));
Ty::Unknown
}
}
}
Expr::Index { object, safe, .. } => {
let obj_ty = self.infer_expr(func, scope, object, span);
if *safe && obj_ty == Ty::Nil {
return Ty::Nil;
}
match &obj_ty {
Ty::List(inner) => *inner.clone(),
Ty::Unknown => Ty::Unknown,
other => {
self.err("ILO-T023", func, format!("index access on non-list type {other}"), None, Some(span));
Ty::Unknown
}
}
}
Expr::Match { subject, arms } => {
let subject_ty = match subject {
Some(expr) => self.infer_expr(func, scope, expr, span),
None => Ty::Nil,
};
let mut result_ty = Ty::Unknown;
for arm in arms {
scope.push(HashMap::new());
self.bind_pattern(func, scope, &arm.pattern, &subject_ty);
let body_ty = self.verify_body(func, scope, &arm.body);
if result_ty == Ty::Unknown {
result_ty = body_ty;
}
scope.pop();
}
self.check_match_exhaustiveness(func, &subject_ty, arms, span);
result_ty
}
Expr::NilCoalesce { value, default } => {
let val_ty = self.infer_expr(func, scope, value, span);
let def_ty = self.infer_expr(func, scope, default, span);
match val_ty {
Ty::Nil => def_ty,
Ty::Optional(inner) => *inner,
other => other,
}
}
Expr::With { object, updates } => {
let obj_ty = self.infer_expr(func, scope, object, span);
match &obj_ty {
Ty::Named(type_name) => {
if let Some(type_def) = self.types.get(type_name) {
let def_fields = type_def.fields.clone();
for (fname, expr) in updates {
if let Some((_, fty)) = def_fields.iter().find(|(n, _)| n == fname) {
let actual = self.infer_expr(func, scope, expr, span);
if !compatible(fty, &actual) {
self.err(
"ILO-T022",
func,
format!("'with' field '{fname}' of '{type_name}' expects {fty}, got {actual}"),
None,
Some(span),
);
}
} else {
let def_field_strings: Vec<String> = def_fields.iter().map(|(n, _)| n.clone()).collect();
let hint = closest_match(fname, def_field_strings.iter())
.map(|s| format!("did you mean '{s}'?"));
self.err(
"ILO-T021",
func,
format!("unknown field '{fname}' in 'with' on '{type_name}'"),
hint,
Some(span),
);
}
}
}
obj_ty
}
Ty::Unknown => Ty::Unknown,
other => {
self.err("ILO-T020", func, format!("'with' on non-record type {other}"), None, Some(span));
Ty::Unknown
}
}
}
}
}
fn check_binop(&mut self, func: &str, op: &BinOp, lt: &Ty, rt: &Ty, span: Span) -> Ty {
match op {
BinOp::Add => {
match (lt, rt) {
(Ty::Number, Ty::Number) => Ty::Number,
(Ty::Text, Ty::Text) => Ty::Text,
(Ty::List(a), Ty::List(_)) => Ty::List(a.clone()),
(Ty::Unknown, _) | (_, Ty::Unknown) => Ty::Unknown,
_ => {
let hint = match (lt, rt) {
(Ty::Number, Ty::Text) | (Ty::Text, Ty::Number) =>
Some("convert number to text with 'str' before concatenating".to_string()),
_ => None,
};
self.err("ILO-T009", func, format!("'+' expects matching n, t, or L types, got {lt} and {rt}"), hint, Some(span));
Ty::Unknown
}
}
}
BinOp::Subtract | BinOp::Multiply | BinOp::Divide => {
if !compatible(lt, &Ty::Number) || !compatible(rt, &Ty::Number) {
let sym = match op { BinOp::Subtract => "-", BinOp::Multiply => "*", _ => "/" };
let has_text = matches!(lt, Ty::Text) || matches!(rt, Ty::Text);
let hint = if has_text { Some("parse text as number with 'num' first".to_string()) } else { None };
self.err("ILO-T009", func, format!("'{sym}' expects n and n, got {lt} and {rt}"), hint, Some(span));
}
Ty::Number
}
BinOp::GreaterThan | BinOp::LessThan | BinOp::GreaterOrEqual | BinOp::LessOrEqual => {
match (lt, rt) {
(Ty::Number, Ty::Number) | (Ty::Text, Ty::Text) => {}
(Ty::Unknown, _) | (_, Ty::Unknown) => {}
_ => {
self.err("ILO-T010", func, format!("comparison expects matching n or t, got {lt} and {rt}"), None, Some(span));
}
}
Ty::Bool
}
BinOp::Equals | BinOp::NotEquals => Ty::Bool,
BinOp::And | BinOp::Or => Ty::Bool,
BinOp::Append => {
match lt {
Ty::List(inner) => {
if !compatible(inner, rt) {
self.err("ILO-T011", func, format!("'+=' list element type {inner} doesn't match appended {rt}"), None, Some(span));
}
lt.clone()
}
Ty::Unknown => Ty::Unknown,
_ => {
self.err("ILO-T011", func, format!("'+=' expects a list on the left, got {lt}"), None, Some(span));
Ty::Unknown
}
}
}
}
}
fn check_match_exhaustiveness(&mut self, func: &str, subject_ty: &Ty, arms: &[MatchArm], span: Span) {
let has_wildcard = arms.iter().any(|a| matches!(a.pattern, Pattern::Wildcard | Pattern::TypeIs { .. }));
if has_wildcard {
return;
}
match subject_ty {
Ty::Result(ok_ty, err_ty) => {
let has_ok = arms.iter().any(|a| matches!(a.pattern, Pattern::Ok(_)));
let has_err = arms.iter().any(|a| matches!(a.pattern, Pattern::Err(_)));
if !has_ok || !has_err {
let missing: Vec<&str> = [
if !has_ok { Some("~") } else { None },
if !has_err { Some("^") } else { None },
].into_iter().flatten().collect();
let parts: Vec<String> = [
if !has_ok { Some(format!("~v: <expr> (v is of type {ok_ty})")) } else { None },
if !has_err { Some(format!("^e: <expr> (e is of type {err_ty})")) } else { None },
].into_iter().flatten().collect();
self.err(
"ILO-T024",
func,
format!("non-exhaustive match on Result: missing {}", missing.join(", ")),
Some(format!("add: {}", parts.join(" or "))),
Some(span),
);
}
}
Ty::Bool => {
let has_true = arms.iter().any(|a| matches!(&a.pattern, Pattern::Literal(Literal::Bool(true))));
let has_false = arms.iter().any(|a| matches!(&a.pattern, Pattern::Literal(Literal::Bool(false))));
if !has_true || !has_false {
let missing: Vec<&str> = [
if !has_true { Some("true") } else { None },
if !has_false { Some("false") } else { None },
].into_iter().flatten().collect();
let parts: Vec<&str> = [
if !has_true { Some("true: <expr>") } else { None },
if !has_false { Some("false: <expr>") } else { None },
].into_iter().flatten().collect();
self.err(
"ILO-T024",
func,
format!("non-exhaustive match on Bool: missing {}", missing.join(", ")),
Some(format!("add: {}", parts.join(" or "))),
Some(span),
);
}
}
Ty::Unknown | Ty::Nil => {}
_ => {
self.err(
"ILO-T024",
func,
"non-exhaustive match: no wildcard arm".to_string(),
Some("add a wildcard arm: _: <default-expr>".to_string()),
Some(span),
);
}
}
}
}
#[derive(Debug)]
pub struct VerifyResult {
pub errors: Vec<VerifyError>,
pub warnings: Vec<VerifyError>,
}
pub fn verify(program: &Program) -> VerifyResult {
let mut ctx = VerifyContext::new();
ctx.collect_declarations(program);
ctx.verify_bodies(program);
let (warnings, errors) = ctx.errors.into_iter().partition(|e| e.is_warning);
VerifyResult { errors, warnings }
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_and_verify(code: &str) -> Result<(), Vec<VerifyError>> {
let result = parse_and_verify_full(code);
if result.errors.is_empty() {
Ok(())
} else {
Err(result.errors)
}
}
fn parse_and_verify_full(code: &str) -> VerifyResult {
let tokens = crate::lexer::lex(code).expect("lex failed");
let token_spans: Vec<(crate::lexer::Token, crate::ast::Span)> = tokens
.into_iter()
.map(|(t, r)| (t, crate::ast::Span { start: r.start, end: r.end }))
.collect();
let (program, parse_errors) = crate::parser::parse(token_spans);
assert!(parse_errors.is_empty(), "parse failed: {:?}", parse_errors);
verify(&program)
}
#[test]
fn valid_simple_function() {
assert!(parse_and_verify("f x:n>n;*x 2").is_ok());
}
#[test]
fn valid_multi_param() {
assert!(parse_and_verify("tot p:n q:n r:n>n;s=*p q;t=*s r;+s t").is_ok());
}
#[test]
fn valid_bool_function() {
assert!(parse_and_verify("f x:b>b;!x").is_ok());
}
#[test]
fn valid_text_function() {
assert!(parse_and_verify("f x:t>t;x").is_ok());
}
#[test]
fn undefined_variable() {
let result = parse_and_verify("f x:n>n;*y 2");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("undefined variable 'y'")));
}
#[test]
fn undefined_function() {
let result = parse_and_verify("f x:n>n;foo x");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("undefined function 'foo'")));
}
#[test]
fn arity_mismatch() {
let result = parse_and_verify("g a:n b:n>n;+a b f x:n>n;g x");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("arity mismatch")));
}
#[test]
fn type_mismatch_param() {
let result = parse_and_verify("g x:n>n;*x 2 f x:t>n;g x");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("type mismatch")));
}
#[test]
fn multiply_on_text() {
let result = parse_and_verify("f x:t>n;*x 2");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("'*' expects n and n")));
}
#[test]
fn valid_let_binding() {
assert!(parse_and_verify("f x:n>n;y=*x 2;+y 1").is_ok());
}
#[test]
fn valid_guard() {
assert!(parse_and_verify("f x:n>t;>x 10{\"big\"};\"small\"").is_ok());
}
#[test]
fn valid_list() {
assert!(parse_and_verify("f x:n>L n;[x, *x 2, *x 3]").is_ok());
}
#[test]
fn valid_builtins() {
assert!(parse_and_verify("f x:n>t;str x").is_ok());
assert!(parse_and_verify("f x:t>n;len x").is_ok());
assert!(parse_and_verify("f x:n>n;abs x").is_ok());
}
#[test]
fn builtin_arity_mismatch() {
let result = parse_and_verify("f x:n>n;min x");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("arity mismatch") && e.message.contains("min")));
}
#[test]
fn compatible_types() {
assert!(compatible(&Ty::Number, &Ty::Number));
assert!(compatible(&Ty::Unknown, &Ty::Number));
assert!(compatible(&Ty::Number, &Ty::Unknown));
assert!(!compatible(&Ty::Number, &Ty::Text));
assert!(compatible(
&Ty::List(Box::new(Ty::Number)),
&Ty::List(Box::new(Ty::Number))
));
assert!(!compatible(
&Ty::List(Box::new(Ty::Number)),
&Ty::List(Box::new(Ty::Text))
));
}
#[test]
fn valid_ok_err() {
assert!(parse_and_verify("f x:n>R n t;~x").is_ok());
assert!(parse_and_verify("f x:t>R n t;^x").is_ok());
}
#[test]
fn valid_match() {
assert!(parse_and_verify("f x:R n t>n;?x{^e:0;~v:v;_:1}").is_ok());
}
#[test]
fn valid_foreach() {
assert!(parse_and_verify("f xs:L n>n;s=0;@x xs{s=+s x};s").is_ok());
}
#[test]
fn foreach_on_non_list() {
let result = parse_and_verify("f x:n>n;@i x{i};0");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("foreach expects a list")));
}
#[test]
fn duplicate_function() {
let result = parse_and_verify("dup x:n>n;*x 2 dup x:n>n;+x 1");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("duplicate function")));
}
#[test]
fn valid_nested_prefix() {
assert!(parse_and_verify("f a:n b:n c:n>n;+*a b c").is_ok());
}
#[test]
fn valid_multi_function_calls() {
assert!(parse_and_verify("dbl x:n>n;*x 2 apply x:n>n;dbl x").is_ok());
}
#[test]
fn return_type_mismatch() {
let result = parse_and_verify("f x:n>t;*x 2");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("return type mismatch")));
}
#[test]
fn valid_negated_guard() {
assert!(parse_and_verify("f x:b>t;!x{\"yes\"};\"no\"").is_ok());
}
#[test]
fn index_on_non_list() {
let result = parse_and_verify("f x:n>n;x.0");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("index access on non-list")));
}
#[test]
fn did_you_mean_hint() {
let result = parse_and_verify("calc x:n>n;*x 2 f x:n>n;calx x");
assert!(result.is_err());
let errors = result.unwrap_err();
let err = errors.iter().find(|e| e.message.contains("undefined function 'calx'")).unwrap();
assert!(err.hint.as_ref().is_some_and(|h| h.contains("did you mean 'calc'?")));
}
#[test]
fn exhaustive_result_match_with_both_arms() {
assert!(parse_and_verify("f x:R n t>n;?x{~v:v;^e:0}").is_ok());
}
#[test]
fn exhaustive_result_match_with_wildcard() {
assert!(parse_and_verify("f x:R n t>n;?x{~v:v;_:0}").is_ok());
}
#[test]
fn non_exhaustive_result_missing_err() {
let result = parse_and_verify("f x:R n t>n;?x{~v:v}");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("non-exhaustive") && e.message.contains("^")));
}
#[test]
fn non_exhaustive_result_missing_ok() {
let result = parse_and_verify("f x:R n t>n;?x{^e:0}");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("non-exhaustive") && e.message.contains("~")));
}
#[test]
fn exhaustive_bool_match() {
assert!(parse_and_verify("f x:b>n;?x{true:1;false:0}").is_ok());
}
#[test]
fn non_exhaustive_bool_missing_false() {
let result = parse_and_verify("f x:b>n;?x{true:1}");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("non-exhaustive") && e.message.contains("false")));
}
#[test]
fn non_exhaustive_number_no_wildcard() {
let result = parse_and_verify("f x:n>t;?x{1:\"one\";2:\"two\"}");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("non-exhaustive") && e.message.contains("no wildcard")));
}
#[test]
fn exhaustive_number_with_wildcard() {
assert!(parse_and_verify("f x:n>t;?x{1:\"one\";_:\"other\"}").is_ok());
}
#[test]
fn subjectless_match_no_false_positive() {
assert!(parse_and_verify("f x:R n t>n;?x{~v:v;^e:0}").is_ok());
}
#[test]
fn ty_display_bool() {
assert_eq!(format!("{}", Ty::Bool), "b");
}
#[test]
fn ty_display_nil() {
assert_eq!(format!("{}", Ty::Nil), "_");
}
#[test]
fn ty_display_list() {
assert_eq!(format!("{}", Ty::List(Box::new(Ty::Number))), "L n");
}
#[test]
fn ty_display_result() {
assert_eq!(format!("{}", Ty::Result(Box::new(Ty::Number), Box::new(Ty::Text))), "R n t");
}
#[test]
fn ty_display_named() {
assert_eq!(format!("{}", Ty::Named("point".to_string())), "point");
}
#[test]
fn ty_display_unknown() {
assert_eq!(format!("{}", Ty::Unknown), "?");
}
#[test]
fn verify_error_display_no_hint() {
let e = VerifyError {
code: "ILO-T004",
function: "f".to_string(),
message: "undefined variable 'x'".to_string(),
hint: None,
span: None,
is_warning: false,
};
let s = format!("{e}");
assert!(s.contains("undefined variable 'x'"));
assert!(s.contains("'f'"));
assert!(!s.contains("hint"));
}
#[test]
fn verify_error_display_with_hint() {
let e = VerifyError {
code: "ILO-T004",
function: "f".to_string(),
message: "undefined variable 'x'".to_string(),
hint: Some("did you mean 'y'?".to_string()),
span: None,
is_warning: false,
};
let s = format!("{e}");
assert!(s.contains("hint: did you mean 'y'?"));
}
#[test]
fn compatible_nil_nil() {
assert!(compatible(&Ty::Nil, &Ty::Nil));
}
#[test]
fn compatible_named_same() {
assert!(compatible(&Ty::Named("point".to_string()), &Ty::Named("point".to_string())));
}
#[test]
fn compatible_named_different() {
assert!(!compatible(&Ty::Named("point".to_string()), &Ty::Named("rect".to_string())));
}
#[test]
fn compatible_list_unknown() {
assert!(compatible(
&Ty::List(Box::new(Ty::Unknown)),
&Ty::List(Box::new(Ty::Number))
));
}
#[test]
fn compatible_result_unknown() {
assert!(compatible(
&Ty::Result(Box::new(Ty::Unknown), Box::new(Ty::Unknown)),
&Ty::Result(Box::new(Ty::Number), Box::new(Ty::Text))
));
}
#[test]
fn convert_type_nil_and_named() {
let _ = parse_and_verify("f x:n>_;x");
}
#[test]
fn convert_type_named_in_signature() {
assert!(parse_and_verify("type point{x:n;y:n} f p:point>point;p").is_ok());
}
#[test]
fn builtin_str_wrong_type() {
let result = parse_and_verify("f x:t>t;str x");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("'str' expects n, got t")));
}
#[test]
fn builtin_num_wrong_type() {
let result = parse_and_verify("f x:n>R n t;num x");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("'num' expects t, got n")));
}
#[test]
fn builtin_min_wrong_type() {
let result = parse_and_verify("f x:t y:n>n;min x y");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("'min' arg 1 expects n, got t")));
}
#[test]
fn builtin_max_wrong_type() {
let result = parse_and_verify("f x:n y:t>n;max x y");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("'max' arg 2 expects n, got t")));
}
#[test]
fn tool_declaration_processed() {
let result = parse_and_verify(
r#"tool my-tool "desc" x:n>n f y:n>n;my-tool y"#
);
assert!(result.is_ok());
}
#[test]
fn tool_conflicts_with_function_name() {
let result = parse_and_verify(
r#"f x:n>n;*x 2 tool f "desc" y:n>n"#
);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("duplicate definition") || e.message.contains("duplicate function")));
}
#[test]
fn typedef_field_with_undefined_named_type() {
let result = parse_and_verify("type edge{from:node;to:node} f x:n>n;x");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("undefined type 'node'")));
}
#[test]
fn undefined_type_in_function_param() {
let result = parse_and_verify("f x:ghost>n;x");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("undefined type 'ghost'")));
}
#[test]
fn record_missing_field() {
let result = parse_and_verify("type point{x:n;y:n} f>point;point x:1");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("missing field 'y'")));
}
#[test]
fn record_extra_field() {
let result = parse_and_verify("type point{x:n;y:n} f>point;point x:1 y:2 z:3");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("unknown field 'z'")));
}
#[test]
fn record_field_type_mismatch() {
let result = parse_and_verify("type point{x:n;y:n} f>point;point x:1 y:\"bad\"");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("field 'y' of 'point' expects n, got t")));
}
#[test]
fn record_undefined_type() {
let result = parse_and_verify("f>n;x=ghost a:1;0");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("undefined type 'ghost'")));
}
#[test]
fn field_not_found_on_type() {
let result = parse_and_verify("type point{x:n;y:n} f p:point>n;p.z");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("no field 'z' on type 'point'")));
}
#[test]
fn field_access_on_non_record_type() {
let result = parse_and_verify("f x:n>n;x.foo");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("field access on non-record type n")));
}
#[test]
fn with_on_non_record() {
let result = parse_and_verify("f x:n>n;y=x with foo:1;0");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("'with' on non-record type n")));
}
#[test]
fn with_field_not_found() {
let result = parse_and_verify("type point{x:n;y:n} f p:point>point;p with z:1");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("unknown field 'z' in 'with'")));
}
#[test]
fn with_field_type_mismatch() {
let result = parse_and_verify("type point{x:n;y:n} f p:point>point;p with x:\"bad\"");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("'with' field 'x' of 'point' expects n, got t")));
}
#[test]
fn binop_comparison_wrong_types() {
let result = parse_and_verify("f x:n y:b>b;>x y");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("comparison expects matching n or t, got n and b")));
}
#[test]
fn binop_append_non_list() {
let result = parse_and_verify("f x:n>n;y=+=x 1;0");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("'+=' expects a list on the left, got n")));
}
#[test]
fn binop_append_wrong_element_type() {
let result = parse_and_verify("f xs:L n>L n;+=xs \"bad\"");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("'+=' list element type n doesn't match appended t")));
}
#[test]
fn match_as_expression_in_let() {
assert!(parse_and_verify("f x:R n t>n;y=?x{~v:v;^e:0};y").is_ok());
}
#[test]
fn non_exhaustive_text_no_wildcard() {
let result = parse_and_verify("f x:t>n;?x{\"a\":1;\"b\":2}");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("non-exhaustive") && e.message.contains("no wildcard")));
}
#[test]
fn index_access_on_non_list_bool() {
let result = parse_and_verify("f x:b>b;x.0");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("index access on non-list")));
}
#[test]
fn builtin_len_wrong_type() {
let result = parse_and_verify("f x:n>n;len x");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("'len' expects a list, map, or text, got n")));
}
#[test]
fn builtin_abs_wrong_type() {
let result = parse_and_verify("f x:t>n;abs x");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("'abs' expects n, got t")));
}
#[test]
fn duplicate_type_definition() {
let result = parse_and_verify("type point{x:n;y:n} type point{a:n;b:n} f x:n>n;x");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("duplicate type definition 'point'")));
}
#[test]
fn bool_literal_in_function_body() {
assert!(parse_and_verify("f>b;true").is_ok());
}
#[test]
fn empty_list_type_is_unknown() {
assert!(parse_and_verify("f>L n;[]").is_ok());
}
#[test]
fn negate_wrong_type() {
let result = parse_and_verify("f x:t>n;-x");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("negate expects n, got t")));
}
#[test]
fn binop_add_text_text() {
assert!(parse_and_verify("f a:t b:t>t;+a b").is_ok());
}
#[test]
fn binop_add_list_list() {
assert!(parse_and_verify("f a:L n b:L n>L n;+a b").is_ok());
}
#[test]
fn binop_add_wrong_types() {
let result = parse_and_verify("f x:n y:b>n;+x y");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("'+' expects matching n, t, or L types")));
}
#[test]
fn binop_equals_returns_bool() {
assert!(parse_and_verify("f x:n y:n>b;=x y").is_ok());
}
#[test]
fn binop_and_returns_bool() {
assert!(parse_and_verify("f x:b y:b>b;&x y").is_ok());
}
#[test]
fn with_on_unknown_type_is_passthrough() {
let result = parse_and_verify("f x:n>n;y=undefined x;z=y with foo:1;0");
assert!(result.is_err());
}
#[test]
fn field_access_on_named_type_not_in_types() {
let result = parse_and_verify("f p:ghost>n;p.x");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("undefined type 'ghost'")));
}
#[test]
fn match_stmt_with_subject() {
assert!(parse_and_verify("f x:n>t;?x{1:\"one\";_:\"other\"}").is_ok());
}
#[test]
fn match_stmt_no_subject() {
assert!(parse_and_verify("f x:n>n;?{_:x}").is_ok());
}
#[test]
fn foreach_unknown_collection() {
let result = parse_and_verify("f x:n>n;@i z{i};0");
let _ = result;
}
#[test]
fn ok_pattern_on_unknown_subject() {
let result = parse_and_verify("f x:n>n;r=?z{~v:0;_:0};r");
let _ = result;
}
#[test]
fn ok_pattern_on_non_result() {
let result = parse_and_verify("f x:n>n;r=?x{~v:0;_:0};r");
let _ = result;
}
#[test]
fn err_pattern_on_unknown_subject() {
let result = parse_and_verify("f x:n>n;r=?z{^v:0;_:0};r");
let _ = result;
}
#[test]
fn err_pattern_on_non_result() {
let result = parse_and_verify("f x:n>n;r=?x{^v:0;_:0};r");
let _ = result;
}
#[test]
fn field_access_on_named_type_found() {
assert!(parse_and_verify("type point{x:n;y:n} f p:point>n;p.x").is_ok());
}
#[test]
fn field_access_on_unknown_type() {
let result = parse_and_verify("f x:n>n;z.field");
let _ = result;
}
#[test]
fn index_access_on_unknown_type() {
let result = parse_and_verify("f x:n>n;z.0");
let _ = result;
}
#[test]
fn expr_match_no_subject() {
assert!(parse_and_verify("f x:n>n;r=?{_:x};r").is_ok());
}
#[test]
fn add_with_unknown_operand() {
let result = parse_and_verify("f x:n>n;+z 1");
let _ = result;
}
#[test]
fn compare_with_unknown_operand() {
let result = parse_and_verify("f x:n>n;>z 0");
let _ = result;
}
#[test]
fn append_with_unknown_left() {
let result = parse_and_verify("f x:n>n;+=z 1");
let _ = result;
}
#[test]
fn match_exhaustiveness_unknown_subject() {
let result = parse_and_verify("f x:n>n;?z{1:0};0");
let _ = result;
}
#[test]
fn match_exhaustiveness_nil_subject() {
let result = parse_and_verify("f x:n>n;?{1:0};0");
let _ = result;
}
#[test]
fn builtin_check_args_len_no_args() {
let (ty, errors) = builtin_check_args("len", &[], "test_func", None);
assert!(errors.is_empty());
assert_eq!(ty, Ty::Number);
}
#[test]
fn builtin_check_args_unknown_name() {
let (ty, errors) = builtin_check_args("unknown_builtin", &[], "test_func", None);
assert!(errors.is_empty());
assert_eq!(ty, Ty::Unknown);
}
#[test]
fn ok_pattern_wildcard_binding() {
let result = parse_and_verify("f x:R n t>n;r=?x{~_:0;_:1};r");
assert!(result.is_ok());
}
#[test]
fn err_pattern_wildcard_binding() {
let result = parse_and_verify("f x:R n t>n;r=?x{^_:1;_:0};r");
assert!(result.is_ok());
}
#[test]
fn with_on_undefined_named_type() {
let result = parse_and_verify("f x:ghost>ghost;x with name:\"bob\"");
let _ = result;
}
#[test]
fn suggestion_t008_number_body_text_expected() {
let result = parse_and_verify("f x:n>t;*x 2");
let errors = result.unwrap_err();
let e = errors.iter().find(|e| e.code == "ILO-T008").unwrap();
assert!(e.hint.as_ref().is_some_and(|h| h.contains("str")));
}
#[test]
fn suggestion_t008_text_body_number_expected() {
let result = parse_and_verify("f x:t>n;x");
let errors = result.unwrap_err();
let e = errors.iter().find(|e| e.code == "ILO-T008").unwrap();
assert!(e.hint.as_ref().is_some_and(|h| h.contains("num")));
}
#[test]
fn suggestion_t008_unrelated_mismatch_no_hint() {
let result = parse_and_verify("f x:b>n;x");
let errors = result.unwrap_err();
let e = errors.iter().find(|e| e.code == "ILO-T008").unwrap();
assert!(e.hint.is_none());
}
#[test]
fn suggestion_t006_user_defined_shows_signature() {
let result = parse_and_verify("g a:n b:n>n;+a b f x:n>n;g x");
let errors = result.unwrap_err();
let e = errors.iter().find(|e| e.code == "ILO-T006" && e.message.contains("'g'")).unwrap();
let hint = e.hint.as_ref().unwrap();
assert!(hint.contains("'g' expects:"));
assert!(hint.contains("a:n"));
assert!(hint.contains("b:n"));
}
#[test]
fn suggestion_t007_param_number_expected_text_given() {
let result = parse_and_verify("g x:n>n;*x 2 f x:t>n;g x");
let errors = result.unwrap_err();
let e = errors.iter().find(|e| e.code == "ILO-T007").unwrap();
assert!(e.hint.as_ref().is_some_and(|h| h.contains("num")));
}
#[test]
fn suggestion_t007_param_text_expected_number_given() {
let result = parse_and_verify("g x:t>t;+x x f y:n>t;g y");
let errors = result.unwrap_err();
let e = errors.iter().find(|e| e.code == "ILO-T007").unwrap();
assert!(e.hint.as_ref().is_some_and(|h| h.contains("str")));
}
#[test]
fn suggestion_t009_add_mixed_nt_hint() {
let result = parse_and_verify("f x:n y:t>n;+x y");
let errors = result.unwrap_err();
let e = errors.iter().find(|e| e.code == "ILO-T009" && e.message.contains("'+'")).unwrap();
assert!(e.hint.as_ref().is_some_and(|h| h.contains("str")));
}
#[test]
fn suggestion_t009_multiply_text_hint() {
let result = parse_and_verify("f x:t y:n>n;*x y");
let errors = result.unwrap_err();
let e = errors.iter().find(|e| e.code == "ILO-T009" && e.message.contains("'*'")).unwrap();
assert!(e.hint.as_ref().is_some_and(|h| h.contains("num")));
}
#[test]
fn suggestion_t016_closest_match() {
let result = parse_and_verify("type point{x:n;y:n} f>point;point x:1 y:2 z:3");
let errors = result.unwrap_err();
let e = errors.iter().find(|e| e.code == "ILO-T016").unwrap();
let _ = &e.hint;
}
#[test]
fn suggestion_t019_closest_match() {
let result = parse_and_verify("type person{name:t;age:n} f p:person>t;p.nam");
let errors = result.unwrap_err();
let e = errors.iter().find(|e| e.code == "ILO-T019").unwrap();
assert!(e.hint.as_ref().is_some_and(|h| h.contains("name")));
}
#[test]
fn suggestion_t021_closest_match() {
let result = parse_and_verify("type person{name:t;age:n} f p:person>person;p with nam:\"bob\"");
let errors = result.unwrap_err();
let e = errors.iter().find(|e| e.code == "ILO-T021").unwrap();
assert!(e.hint.as_ref().is_some_and(|h| h.contains("name")));
}
#[test]
fn suggestion_t024_result_missing_err() {
let result = parse_and_verify("f x:R n t>n;?x{~v:v}");
let errors = result.unwrap_err();
let e = errors.iter().find(|e| e.code == "ILO-T024").unwrap();
let hint = e.hint.as_ref().unwrap();
assert!(hint.contains("^e: <expr>"));
assert!(hint.contains("t")); }
#[test]
fn suggestion_t024_result_missing_ok() {
let result = parse_and_verify("f x:R n t>n;?x{^e:0}");
let errors = result.unwrap_err();
let e = errors.iter().find(|e| e.code == "ILO-T024").unwrap();
let hint = e.hint.as_ref().unwrap();
assert!(hint.contains("~v: <expr>"));
assert!(hint.contains("n")); }
#[test]
fn suggestion_t024_bool_missing_false() {
let result = parse_and_verify("f x:b>n;?x{true:1}");
let errors = result.unwrap_err();
let e = errors.iter().find(|e| e.code == "ILO-T024").unwrap();
let hint = e.hint.as_ref().unwrap();
assert!(hint.contains("false: <expr>"));
}
#[test]
fn suggestion_t024_bool_missing_true() {
let result = parse_and_verify("f x:b>n;?x{false:0}");
let errors = result.unwrap_err();
let e = errors.iter().find(|e| e.code == "ILO-T024").unwrap();
let hint = e.hint.as_ref().unwrap();
assert!(hint.contains("true: <expr>"));
}
#[test]
fn suggestion_t024_generic_wildcard() {
let result = parse_and_verify("f x:n>t;?x{1:\"one\";2:\"two\"}");
let errors = result.unwrap_err();
let e = errors.iter().find(|e| e.code == "ILO-T024").unwrap();
let hint = e.hint.as_ref().unwrap();
assert!(hint.contains("_: <default-expr>"));
}
#[test]
fn unwrap_valid_result_call() {
use crate::ast::*;
let rnt = Type::Result(Box::new(Type::Number), Box::new(Type::Text));
let prog = Program {
declarations: vec![
Decl::Function {
name: "inner".to_string(),
params: vec![Param { name: "x".to_string(), ty: Type::Number }],
return_type: rnt.clone(),
body: vec![Spanned::unknown(Stmt::Expr(Expr::Ok(Box::new(Expr::Ref("x".to_string())))))],
span: Span::UNKNOWN,
},
Decl::Function {
name: "outer".to_string(),
params: vec![Param { name: "x".to_string(), ty: Type::Number }],
return_type: rnt,
body: vec![
Spanned::unknown(Stmt::Let {
name: "d".to_string(),
value: Expr::Call { function: "inner".to_string(), args: vec![Expr::Ref("x".to_string())], unwrap: true },
}),
Spanned::unknown(Stmt::Expr(Expr::Ok(Box::new(Expr::Ref("d".to_string()))))),
],
span: Span::UNKNOWN,
},
],
source: None,
};
let result = verify(&prog);
assert!(result.errors.is_empty(), "expected valid, got: {:?}", result);
}
#[test]
fn unwrap_t025_non_result_callee() {
use crate::ast::*;
let prog = Program {
declarations: vec![
Decl::Function {
name: "inner".to_string(),
params: vec![Param { name: "x".to_string(), ty: Type::Number }],
return_type: Type::Number,
body: vec![Spanned::unknown(Stmt::Expr(Expr::Ref("x".to_string())))],
span: Span::UNKNOWN,
},
Decl::Function {
name: "outer".to_string(),
params: vec![Param { name: "x".to_string(), ty: Type::Number }],
return_type: Type::Result(Box::new(Type::Number), Box::new(Type::Text)),
body: vec![Spanned::unknown(Stmt::Expr(
Expr::Call { function: "inner".to_string(), args: vec![Expr::Ref("x".to_string())], unwrap: true },
))],
span: Span::UNKNOWN,
},
],
source: None,
};
let errors = &verify(&prog).errors;
assert!(errors.iter().any(|e| e.code == "ILO-T025"), "expected T025, got: {:?}", errors);
}
#[test]
fn unwrap_t026_non_result_enclosing() {
use crate::ast::*;
let rnt = Type::Result(Box::new(Type::Number), Box::new(Type::Text));
let prog = Program {
declarations: vec![
Decl::Function {
name: "inner".to_string(),
params: vec![Param { name: "x".to_string(), ty: Type::Number }],
return_type: rnt,
body: vec![Spanned::unknown(Stmt::Expr(Expr::Ok(Box::new(Expr::Ref("x".to_string())))))],
span: Span::UNKNOWN,
},
Decl::Function {
name: "outer".to_string(),
params: vec![Param { name: "x".to_string(), ty: Type::Number }],
return_type: Type::Number,
body: vec![Spanned::unknown(Stmt::Expr(
Expr::Call { function: "inner".to_string(), args: vec![Expr::Ref("x".to_string())], unwrap: true },
))],
span: Span::UNKNOWN,
},
],
source: None,
};
let errors = &verify(&prog).errors;
assert!(errors.iter().any(|e| e.code == "ILO-T026"), "expected T026, got: {:?}", errors);
}
#[test]
fn builtin_get_valid() {
assert!(parse_and_verify(r#"f url:t>R t t;get url"#).is_ok());
}
#[test]
fn builtin_get_wrong_type() {
let result = parse_and_verify("f x:n>R t t;get x");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("'get' expects t")));
}
#[test]
fn builtin_get_wrong_arity() {
let result = parse_and_verify(r#"f x:t y:M t t z:t>R t t;get x y z"#);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("arity")));
}
#[test]
fn builtin_get_with_headers_ok() {
assert!(parse_and_verify(r#"f url:t hdrs:M t t>R t t;get url hdrs"#).is_ok());
}
#[test]
fn builtin_post_with_headers_ok() {
assert!(parse_and_verify(r#"f url:t body:t hdrs:M t t>R t t;post url body hdrs"#).is_ok());
}
#[test]
fn dollar_desugars_to_get() {
assert!(parse_and_verify(r#"f url:t>R t t;$url"#).is_ok());
}
#[test]
fn braceless_guard_body_is_function_name() {
let result = parse_and_verify("classify n:n>t;\"done\"\ncls sp:n>t;>=sp 1000 classify;\"fallback\"");
let errors = result.unwrap_err();
assert!(
errors.iter().any(|e| e.code == "ILO-T027" && e.message.contains("classify")),
"expected ILO-T027 for function name in braceless guard body, got: {:?}", errors
);
assert!(
errors.iter().any(|e| e.hint.as_ref().is_some_and(|h| h.contains("braces"))),
"expected hint about braces, got: {:?}", errors
);
}
#[test]
fn braceless_guard_body_is_variable_no_warning() {
assert!(parse_and_verify("f x:n>n;>=x 10 x").is_ok());
}
#[test]
fn braceless_guard_body_is_builtin_name() {
let result = parse_and_verify("f x:n>n;>=x 0 len;x");
let errors = result.unwrap_err();
assert!(
errors.iter().any(|e| e.code == "ILO-T027" && e.message.contains("len")),
"expected ILO-T027 for builtin name in braceless guard body, got: {:?}", errors
);
}
#[test]
fn spl_valid() {
let result = parse_and_verify(r#"f s:t sep:t>L t;spl s sep"#);
assert!(result.is_ok(), "spl with two text args should verify: {:?}", result);
}
#[test]
fn spl_wrong_type() {
let result = parse_and_verify(r#"f s:t n:n>L t;spl s n"#);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.code == "ILO-T013" && e.message.contains("spl")));
}
#[test]
fn spl_wrong_arity() {
let result = parse_and_verify(r#"f s:t>L t;spl s"#);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("spl")));
}
#[test]
fn cat_valid_call() {
assert!(parse_and_verify("f items:L t>t;cat items \",\"").is_ok());
}
#[test]
fn cat_wrong_type_arg1() {
let result = parse_and_verify("f x:n>t;cat x \",\"");
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.code == "ILO-T013" && e.message.contains("cat")));
}
#[test]
fn cat_wrong_arity() {
let result = parse_and_verify("f items:L t>t;cat items");
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("cat") && e.message.contains("2")));
}
#[test]
fn has_valid_list() {
assert!(parse_and_verify("f xs:L n x:n>b;has xs x").is_ok());
}
#[test]
fn has_valid_text() {
assert!(parse_and_verify(r#"f s:t needle:t>b;has s needle"#).is_ok());
}
#[test]
fn has_wrong_type_arg1() {
let result = parse_and_verify("f x:n y:n>b;has x y");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("'has' arg 1 expects a list or text")));
}
#[test]
fn hd_valid_list() {
assert!(parse_and_verify("f xs:L n>n;hd xs").is_ok());
}
#[test]
fn tl_valid_list() {
assert!(parse_and_verify("f xs:L n>L n;tl xs").is_ok());
}
#[test]
fn hd_valid_text() {
assert!(parse_and_verify("f s:t>t;hd s").is_ok());
}
#[test]
fn hd_wrong_type() {
let result = parse_and_verify("f x:n>n;hd x");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("'hd' expects a list or text, got n")));
}
#[test]
fn tl_wrong_type() {
let result = parse_and_verify("f x:n>n;tl x");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("'tl' expects a list or text, got n")));
}
#[test]
fn rev_valid_list() {
assert!(parse_and_verify("f xs:L n>L n;rev xs").is_ok());
}
#[test]
fn rev_valid_text() {
assert!(parse_and_verify("f s:t>t;rev s").is_ok());
}
#[test]
fn rev_wrong_type() {
let result = parse_and_verify("f n:n>L n;rev n");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.code == "ILO-T013" && e.message.contains("rev")));
}
#[test]
fn srt_valid_list() {
assert!(parse_and_verify("f>L n;xs=[3, 1, 2];srt xs").is_ok());
}
#[test]
fn srt_wrong_type() {
let result = parse_and_verify("f x:n>n;srt x");
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.code == "ILO-T013" && e.message.contains("srt")));
}
#[test]
fn slc_valid_list() {
assert!(parse_and_verify("f x:L n>L n;slc x 0 2").is_ok());
}
#[test]
fn slc_valid_text() {
assert!(parse_and_verify("f x:t>t;slc x 0 2").is_ok());
}
#[test]
fn slc_wrong_collection_type() {
let result = parse_and_verify("f x:n>n;slc x 0 2");
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.code == "ILO-T013" && e.message.contains("slc")));
}
#[test]
fn slc_wrong_index_type() {
let result = parse_and_verify("f x:L n s:t>L n;slc x s 2");
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.code == "ILO-T013" && e.message.contains("slc")));
}
#[test]
fn while_valid() {
assert!(parse_and_verify("f>n;i=0;wh <i 5{i=+i 1};i").is_ok());
}
#[test]
fn ret_valid() {
assert!(parse_and_verify("f x:n>n;ret +x 1").is_ok());
}
#[test]
fn ret_in_guard() {
assert!(parse_and_verify(r#"f x:n>t;>x 0{ret "pos"};"neg""#).is_ok());
}
#[test]
fn brk_outside_loop() {
let errors = parse_and_verify("f>n;brk").unwrap_err();
assert!(errors.iter().any(|e| e.code == "ILO-T028" && e.message.contains("brk")));
}
#[test]
fn cnt_outside_loop() {
let errors = parse_and_verify("f>n;cnt").unwrap_err();
assert!(errors.iter().any(|e| e.code == "ILO-T028" && e.message.contains("cnt")));
}
#[test]
fn brk_inside_foreach() {
assert!(parse_and_verify("f xs:L n>_;@x xs{brk}").is_ok());
}
#[test]
fn brk_inside_while() {
assert!(parse_and_verify("f>_;i=0;wh <i 5{brk}").is_ok());
}
#[test]
fn cnt_inside_foreach() {
assert!(parse_and_verify("f xs:L n>_;@x xs{cnt}").is_ok());
}
#[test]
fn brk_inside_guard_inside_loop() {
assert!(parse_and_verify("f>_;i=0;wh <i 5{>i 3{brk};i=+i 1}").is_ok());
}
#[test]
fn guard_in_foreach_warns() {
let result = parse_and_verify_full("f xs:L n>n;r=0;@x xs{>=x 10{r= +r x}};r");
assert!(result.errors.is_empty());
let w001: Vec<_> = result.warnings.iter().filter(|w| w.code == "ILO-W001").collect();
assert_eq!(w001.len(), 1);
assert!(w001[0].message.contains("guard without else inside loop"));
}
#[test]
fn guard_in_while_warns() {
let result = parse_and_verify_full("f>n;i=0;wh <i 10{>i 5{ret i};i= +i 1};i");
assert!(result.errors.is_empty());
let w001: Vec<_> = result.warnings.iter().filter(|w| w.code == "ILO-W001").collect();
assert_eq!(w001.len(), 1);
}
#[test]
fn guard_in_range_warns() {
let result = parse_and_verify_full("f>n;r=0;@i 0..10{>=i 5{r= +r i}};r");
assert!(result.errors.is_empty());
let w001: Vec<_> = result.warnings.iter().filter(|w| w.code == "ILO-W001").collect();
assert_eq!(w001.len(), 1);
}
#[test]
fn guard_with_else_in_loop_no_warning() {
let result = parse_and_verify_full("f xs:L n>n;r=0;@x xs{>=x 10{r= +r x}{r}};r");
let w001: Vec<_> = result.warnings.iter().filter(|w| w.code == "ILO-W001").collect();
assert_eq!(w001.len(), 0);
}
#[test]
fn guard_outside_loop_no_warning() {
let result = parse_and_verify_full("f x:n>n;>=x 0{x};-x");
let w001: Vec<_> = result.warnings.iter().filter(|w| w.code == "ILO-W001").collect();
assert_eq!(w001.len(), 0);
}
#[test]
fn unreachable_after_ret() {
let result = parse_and_verify_full("f x:n>n;ret x;*x 2");
assert!(result.errors.is_empty());
assert_eq!(result.warnings.len(), 1);
assert_eq!(result.warnings[0].code, "ILO-T029");
assert!(result.warnings[0].message.contains("ret"));
}
#[test]
fn unreachable_after_brk() {
let result = parse_and_verify_full("f x:n>n;wh true{brk 1;x=2;x};x");
assert!(result.errors.is_empty());
assert_eq!(result.warnings.len(), 1);
assert_eq!(result.warnings[0].code, "ILO-T029");
assert!(result.warnings[0].message.contains("brk"));
}
#[test]
fn ret_as_last_no_warning() {
let result = parse_and_verify_full("f x:n>n;y=*x 2;ret y");
assert!(result.errors.is_empty());
assert!(result.warnings.is_empty());
}
#[test]
fn ret_in_guard_body_no_warning_for_outer() {
let result = parse_and_verify_full(r#"f x:n>t;>x 0{ret "pos"};"neg""#);
assert!(result.errors.is_empty());
assert!(result.warnings.is_empty());
}
#[test]
fn multiple_stmts_after_ret_one_warning() {
let result = parse_and_verify_full("f x:n>n;ret x;y=*x 2;+y 1");
assert!(result.errors.is_empty());
assert_eq!(result.warnings.len(), 1);
assert_eq!(result.warnings[0].code, "ILO-T029");
}
#[test]
fn rnd_zero_args_valid() {
assert!(parse_and_verify("f>n;rnd").is_ok());
}
#[test]
fn rnd_two_args_valid() {
assert!(parse_and_verify("f>n;rnd 1 10").is_ok());
}
#[test]
fn rnd_one_arg_arity_error() {
let result = parse_and_verify("f x:n>n;rnd x");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("arity mismatch") && e.message.contains("rnd")));
}
#[test]
fn rnd_type_error() {
let result = parse_and_verify(r#"f>n;rnd "hello" 5"#);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.code == "ILO-T013" && e.message.contains("rnd")));
}
#[test]
fn now_zero_args_valid() {
assert!(parse_and_verify("f>n;now").is_ok());
}
#[test]
fn now_with_args_arity_error() {
let result = parse_and_verify("f x:n>n;now x");
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("arity mismatch") && e.message.contains("now")));
}
#[test]
fn range_basic_ok() {
assert!(parse_and_verify("f>n;@i 0..3{i}").is_ok());
}
#[test]
fn range_binding_is_number() {
assert!(parse_and_verify("f>n;@i 0..3{+i 1}").is_ok());
}
#[test]
fn range_start_must_be_number() {
let result = parse_and_verify(r#"f>n;@i "a"..3{i}"#);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("range start must be n")));
}
#[test]
fn range_end_must_be_number() {
let result = parse_and_verify(r#"f>n;@i 0.."b"{i}"#);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("range end must be n")));
}
#[test]
fn range_brk_cnt_allowed() {
assert!(parse_and_verify("f>n;@i 0..5{>=i 3{brk i};i}").is_ok());
assert!(parse_and_verify("f>n;@i 0..5{=i 2{cnt};i}").is_ok());
}
#[test]
fn alias_basic_return_type() {
assert!(parse_and_verify("alias res R n t\nf x:n>res;~x").is_ok());
}
#[test]
fn alias_in_param_type() {
assert!(parse_and_verify("alias num n\nf x:num>n;x").is_ok());
}
#[test]
fn alias_nested() {
assert!(parse_and_verify("alias ids L n\nalias idres R ids t\nf>idres;~[1, 2, 3]").is_ok());
}
#[test]
fn alias_circular_detected() {
let errs = parse_and_verify("alias foo bar\nalias bar foo\nf>n;1").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T030"));
}
#[test]
fn alias_of_alias_chain() {
assert!(parse_and_verify("alias x n\nalias y x\nf a:y>y;a").is_ok());
}
#[test]
fn alias_shadows_builtin_type_error() {
let result = parse_and_verify_full("alias n t\nf>n;1");
assert!(result.errors.iter().any(|e| e.code == "ILO-T031"));
}
#[test]
fn alias_duplicate_error() {
let errs = parse_and_verify("alias res R n t\nalias res L n\nf>n;1").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T001" && e.message.contains("duplicate type alias")));
}
#[test]
fn alias_in_type_def_field() {
assert!(parse_and_verify("alias id n\ntype user{name:t;id:id}\nf u:user>id;u.id").is_ok());
}
#[test]
fn alias_conflicts_with_type_def() {
let errs = parse_and_verify("alias pt n\ntype pt{x:n;y:n}\nf>n;1").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T001" && e.message.contains("conflicts with type alias")));
}
#[test]
fn alias_complex_type() {
assert!(parse_and_verify("alias deep L R n t\nf>deep;[~1, ~2]").is_ok());
}
#[test]
fn destructure_ok() {
assert!(parse_and_verify("type pt{x:n;y:n}\nf p:pt>n;{x;y}=p;+x y").is_ok());
}
#[test]
fn destructure_infers_types() {
assert!(parse_and_verify("type pt{x:n;y:n}\nf p:pt>n;{x;y}=p;*x y").is_ok());
}
#[test]
fn destructure_wrong_field() {
let errs = parse_and_verify("type pt{x:n;y:n}\nf p:pt>n;{x;z}=p;x").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T019" && e.message.contains("no field 'z'")));
}
#[test]
fn destructure_non_record() {
let errs = parse_and_verify("f x:n>n;{a}=x;a").unwrap_err();
assert!(errs.iter().any(|e| e.message.contains("destructure requires a record")));
}
#[test]
fn destructure_text_type_error() {
let errs = parse_and_verify("f x:t>n;{a}=x;a").unwrap_err();
assert!(errs.iter().any(|e| e.message.contains("destructure requires a record")));
}
#[test]
fn mkeys_non_map_arg_error() {
let errs = parse_and_verify("f x:n>L t;mkeys x").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T013" && e.message.contains("mkeys")));
}
#[test]
fn match_exhaustive_on_number_no_wildcard_error() {
let errs = parse_and_verify("f x:n>t;?x{1:\"one\";2:\"two\"}").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T024"));
}
#[test]
fn match_exhaustive_on_text_no_wildcard_error() {
let errs = parse_and_verify(r#"f x:t>n;?x{"a":1;"b":2}"#).unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T024"));
}
#[test]
fn match_result_missing_err_arm() {
let errs = parse_and_verify("f x:R n t>n;?x{~v:v}").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T024" && e.message.contains("missing") && e.message.contains("^")));
}
#[test]
fn match_result_missing_ok_arm() {
let errs = parse_and_verify("f x:R n t>t;?x{^e:e}").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T024" && e.message.contains("missing") && e.message.contains("~")));
}
#[test]
fn match_bool_missing_false_arm() {
let errs = parse_and_verify("f x:b>n;?x{true:1}").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T024" && e.message.contains("false")));
}
#[test]
fn match_bool_missing_true_arm() {
let errs = parse_and_verify("f x:b>n;?x{false:0}").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T024" && e.message.contains("true")));
}
#[test]
fn trm_wrong_type() {
let errs = parse_and_verify("f x:n>t;trm x").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T013" && e.message.contains("trm")));
}
#[test]
fn unq_with_list_ok() {
assert!(parse_and_verify("f xs:L n>L n;unq xs").is_ok());
}
#[test]
fn fmt_non_text_template() {
let errs = parse_and_verify("f x:n>t;fmt x").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T013" && e.message.contains("fmt")));
}
#[test]
fn jpth_wrong_first_arg() {
let errs = parse_and_verify(r#"f x:n>R t t;jpth x "path""#).unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T013" && e.message.contains("jpth")));
}
#[test]
fn jpth_wrong_second_arg() {
let errs = parse_and_verify(r#"f x:t y:n>R t t;jpth x y"#).unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T013" && e.message.contains("jpth")));
}
#[test]
fn jpar_wrong_type() {
let errs = parse_and_verify("f x:n>R n t;jpar x").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T013" && e.message.contains("jpar")));
}
#[test]
fn jdmp_any_type_ok() {
assert!(parse_and_verify("f x:n>t;jdmp x").is_ok());
}
#[test]
fn prnt_passthrough_number() {
assert!(parse_and_verify("f x:n>n;prnt x").is_ok());
}
#[test]
fn map_non_function_first_arg() {
let errs = parse_and_verify("f xs:L n>L n;map 123 xs").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T013" && e.message.contains("map")));
}
#[test]
fn flt_non_function_first_arg() {
let errs = parse_and_verify("f xs:L n>L n;flt 123 xs").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T013" && e.message.contains("flt")));
}
#[test]
fn fld_non_function_first_arg() {
let errs = parse_and_verify("f xs:L n>n;fld 123 xs 0").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T013" && e.message.contains("fld")));
}
#[test]
fn grp_non_function_first_arg() {
let errs = parse_and_verify("f xs:L n>M t L n;grp 123 xs").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T013" && e.message.contains("grp")));
}
#[test]
fn mget_key_non_text() {
let errs = parse_and_verify("f m:M t n>O n;mget m 123").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T013" && e.message.contains("mget")));
}
#[test]
fn mhas_non_map_first_arg() {
let errs = parse_and_verify(r#"f>b;mhas 123 "k""#).unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T013" && e.message.contains("mhas")));
}
#[test]
fn mset_returns_map() {
assert!(parse_and_verify(r#"f m:M t t>M t t;mset m "k" "v""#).is_ok());
}
#[test]
fn mvals_returns_list() {
assert!(parse_and_verify("f m:M t n>L n;mvals m").is_ok());
}
#[test]
fn mdel_returns_map() {
assert!(parse_and_verify(r#"f m:M t n>M t n;mdel m "k""#).is_ok());
}
#[test]
fn rd_wrong_type_path() {
let errs = parse_and_verify("f x:n>R n t;rd x").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T013" && e.message.contains("rd")));
}
#[test]
fn wr_wrong_content_type() {
let errs = parse_and_verify(r#"f x:t>R t t;wr x 123"#).unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T013" && e.message.contains("wr")));
}
#[test]
fn wrl_wrong_second_arg() {
assert!(parse_and_verify("f x:t xs:n>R t t;wrl x xs").is_ok());
}
#[test]
fn ty_display_optional_type() {
let ty = Ty::Optional(Box::new(Ty::Number));
assert_eq!(format!("{ty}"), "O n");
}
#[test]
fn ty_display_map_type() {
let ty = Ty::Map(Box::new(Ty::Text), Box::new(Ty::Number));
assert_eq!(format!("{ty}"), "M t n");
}
#[test]
fn ty_display_sum_type() {
let ty = Ty::Sum(vec!["a".into(), "b".into()]);
assert_eq!(format!("{ty}"), "S a b");
}
#[test]
fn ty_display_fn_type() {
let ty = Ty::Fn(vec![Ty::Number], Box::new(Ty::Text));
assert_eq!(format!("{ty}"), "F n t");
}
#[test]
fn verify_named_type_in_optional_param() {
let errs = parse_and_verify("f x:O mytype>n;0");
let _ = errs;
}
#[test]
fn verify_named_type_in_map_param() {
let errs = parse_and_verify("f x:M mytype n>n;0");
let _ = errs;
}
#[test]
fn verify_named_type_in_fn_param() {
let errs = parse_and_verify("f cb:F n mytype>n;0");
let _ = errs;
}
#[test]
fn verify_sum_type_param() {
assert!(parse_and_verify(r#"f x:S foo bar>t;"ok""#).is_ok());
}
#[test]
fn verify_fn_type_param() {
assert!(parse_and_verify("f cb:F n t x:n>t;cb x").is_ok());
}
#[test]
fn verify_type_variable_in_param() {
let errs = parse_and_verify("f x:z>n;0");
let _ = errs;
}
#[test]
fn verify_cat_arg2_wrong_type() {
let errs = parse_and_verify("f a:t>t;cat a 42").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T013" && e.message.contains("cat")));
}
#[test]
fn verify_hd_on_text_returns_text() {
assert!(parse_and_verify("f s:t>t;hd s").is_ok());
}
#[test]
fn verify_hd_wrong_type() {
let errs = parse_and_verify("f x:n>t;hd x").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T013" && e.message.contains("hd")));
}
#[test]
fn verify_tl_on_text_returns_text() {
assert!(parse_and_verify("f s:t>t;tl s").is_ok());
}
#[test]
fn verify_tl_wrong_type() {
let errs = parse_and_verify("f x:n>t;tl x").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T013" && e.message.contains("tl")));
}
#[test]
fn verify_rev_wrong_type() {
let errs = parse_and_verify("f x:n>L t;rev x").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T013" && e.message.contains("rev")));
}
#[test]
fn verify_unq_wrong_type() {
let errs = parse_and_verify("f x:n>L t;unq x").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T013" && e.message.contains("unq")));
}
#[test]
fn verify_unq_on_text_returns_text() {
assert!(parse_and_verify("f s:t>t;unq s").is_ok());
}
#[test]
fn verify_srt_with_key_fn() {
assert!(parse_and_verify("key x:n>n;*x 2 f xs:L n>L n;srt key xs").is_ok());
}
#[test]
fn verify_srt_wrong_key_fn() {
let errs = parse_and_verify("f xs:L n>L n;srt 42 xs").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T013" && e.message.contains("srt")));
}
#[test]
fn verify_srt_on_text() {
assert!(parse_and_verify("f s:t>t;srt s").is_ok());
}
#[test]
fn verify_srt_wrong_single_arg() {
let errs = parse_and_verify("f x:n>t;srt x").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T013" && e.message.contains("srt")));
}
#[test]
fn verify_slc_wrong_type() {
let errs = parse_and_verify("f x:n>L n;slc x 0 1").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T013" && e.message.contains("slc")));
}
#[test]
fn verify_slc_on_text() {
assert!(parse_and_verify("f s:t>t;slc s 0 2").is_ok());
}
#[test]
fn collect_named_refs_inner_optional_type() {
use crate::ast::Type;
let mut refs = Vec::new();
collect_named_refs_inner(
&Type::Optional(Box::new(Type::Named("mytype".to_string()))),
&mut refs,
);
assert_eq!(refs, vec!["mytype".to_string()]);
}
#[test]
fn collect_named_refs_inner_map_type() {
use crate::ast::Type;
let mut refs = Vec::new();
collect_named_refs_inner(
&Type::Map(
Box::new(Type::Named("keytype".to_string())),
Box::new(Type::Named("valtype".to_string())),
),
&mut refs,
);
assert!(refs.contains(&"keytype".to_string()));
assert!(refs.contains(&"valtype".to_string()));
}
#[test]
fn collect_named_refs_inner_fn_type() {
use crate::ast::Type;
let mut refs = Vec::new();
collect_named_refs_inner(
&Type::Fn(
vec![Type::Named("myarg".to_string())],
Box::new(Type::Named("myret".to_string())),
),
&mut refs,
);
assert!(refs.contains(&"myarg".to_string()));
assert!(refs.contains(&"myret".to_string()));
}
#[test]
fn convert_type_number() {
use crate::ast::Type;
let ty = convert_type(&Type::Number);
assert_eq!(ty, Ty::Number);
}
#[test]
fn convert_type_fn_type() {
use crate::ast::Type;
let ty = convert_type(&Type::Fn(vec![Type::Number], Box::new(Type::Text)));
assert_eq!(ty, Ty::Fn(vec![Ty::Number], Box::new(Ty::Text)));
}
#[test]
fn compat_nil_to_optional_via_assign_body() {
assert!(parse_and_verify("f>O n;x=0").is_ok());
}
#[test]
fn compat_optional_to_optional_return() {
assert!(parse_and_verify("f x:O n>O n;x").is_ok());
}
#[test]
fn compat_inner_to_optional_return() {
assert!(parse_and_verify("f x:n>O n;x").is_ok());
}
#[test]
fn compat_optional_to_inner_return() {
assert!(parse_and_verify("f x:O n>n;x").is_ok());
}
#[test]
fn compat_sum_to_text_return() {
assert!(parse_and_verify(r#"f x:S a b>t;x"#).is_ok());
}
#[test]
fn compat_text_to_sum_param() {
assert!(parse_and_verify(r#"f x:S a b>n;0 g y:S a b>n;g "hello""#).is_ok());
}
#[test]
fn compat_fn_to_fn_param() {
assert!(parse_and_verify("double x:n>n;*x 2 apply cb:F n n x:n>n;cb x h>n;apply double 5").is_ok());
}
#[test]
fn hd_with_unknown_type_arg() {
assert!(parse_and_verify("f x:z>n;hd x").is_ok());
}
#[test]
fn tl_with_unknown_type_arg() {
assert!(parse_and_verify("f x:z>n;tl x").is_ok());
}
#[test]
fn srt_single_unknown_type_arg() {
assert!(parse_and_verify("f x:z>n;srt x").is_ok());
}
#[test]
fn srt_two_arg_second_not_list_returns_unknown() {
assert!(parse_and_verify("double x:n>n;*x 2 f x:n>n;srt double x").is_ok());
}
#[test]
fn get_wrong_headers_type_error() {
let errs = parse_and_verify("f url:t hdrs:n>R t t;get url hdrs").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T013" && e.message.contains("get") && e.message.contains("headers")));
}
#[test]
fn post_wrong_headers_type_error() {
let errs = parse_and_verify("f url:t body:t hdrs:n>R t t;post url body hdrs").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T013" && e.message.contains("post") && e.message.contains("headers")));
}
#[test]
fn rd_wrong_format_arg_type() {
let errs = parse_and_verify("f p:t fmt:n>R n t;rd p fmt").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T013" && e.message.contains("rd")));
}
#[test]
fn rdl_wrong_path_type() {
let errs = parse_and_verify("f x:n>R L t t;rdl x").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T013" && e.message.contains("rdl")));
}
#[test]
fn wr_wrong_path_type() {
let errs = parse_and_verify(r#"f x:n>R t t;wr x "content""#).unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T013" && e.message.contains("wr") && e.message.contains("arg 1")));
}
#[test]
fn map_infers_fn_return_type() {
assert!(parse_and_verify("double x:n>n;*x 2 f xs:L n>L n;map double xs").is_ok());
}
#[test]
fn flt_infers_list_type_from_second_arg() {
assert!(parse_and_verify("gt x:n>b;>x 0 f xs:L n>L n;flt gt xs").is_ok());
}
#[test]
fn fld_infers_fn_return_type_when_third_unknown() {
assert!(parse_and_verify("add x:n y:n>n;+x y f xs:L n init:z>n;fld add xs init").is_ok());
}
#[test]
fn mvals_non_map_returns_unknown_list() {
assert!(parse_and_verify("f x:n>L n;mvals x").is_ok());
}
#[test]
fn mdel_non_map_returns_generic_map() {
assert!(parse_and_verify(r#"f x:n>M t n;mdel x "k""#).is_ok());
}
#[test]
fn decl_use_skipped_in_verify() {
use crate::ast::{Decl, Span};
let tokens = crate::lexer::lex("f>n;1").expect("lex failed");
let token_spans: Vec<(crate::lexer::Token, crate::ast::Span)> = tokens
.into_iter()
.map(|(t, r)| (t, crate::ast::Span { start: r.start, end: r.end }))
.collect();
let (mut program, _) = crate::parser::parse(token_spans);
program.declarations.push(Decl::Use { path: "x.ilo".into(), only: None, span: Span::UNKNOWN });
let result = verify(&program);
assert!(result.errors.is_empty());
}
#[test]
fn alias_diamond_dep_exercises_has_cycle_false() {
assert!(parse_and_verify("alias inner n\nalias a R inner inner\nf>n;1").is_ok());
}
#[test]
fn alias_shared_dep_resolved_once() {
let result = parse_and_verify_full("alias shared n\nalias a L shared\nalias b L shared\nf x:a y:b>n;0");
let _ = result;
}
#[test]
fn destructure_with_unknown_type_binds_unknown() {
assert!(parse_and_verify("f x:z>n;{a}=x;0").is_ok());
}
#[test]
fn guard_stmt_with_else_body_verified() {
assert!(parse_and_verify("f x:n>n;>x 0{x}{0}").is_ok());
}
#[test]
fn rd_three_args_arity_error_description() {
let errs = parse_and_verify("f p:t fmt:t extra:t>R n t;rd p fmt extra").unwrap_err();
assert!(errs.iter().any(|e| e.message.contains("arity") && e.message.contains("rd")));
}
#[test]
fn post_one_arg_arity_error_description() {
let errs = parse_and_verify(r#"f url:t>R t t;post url"#).unwrap_err();
assert!(errs.iter().any(|e| e.message.contains("arity") && e.message.contains("post")));
}
#[test]
fn type_mismatch_bool_to_number_no_hint() {
let errs = parse_and_verify("f x:b>n;0 g y:n>n;y h x:b>n;g x").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T007"));
}
#[test]
fn dynamic_dispatch_wrong_arity() {
let errs = parse_and_verify("f cb:F n n>n;cb 1 2 3").unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T006" && e.message.contains("cb")));
}
#[test]
fn dynamic_dispatch_wrong_type() {
let errs = parse_and_verify(r#"f cb:F n n>n;cb "text""#).unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T007" && e.message.contains("cb")));
}
#[test]
fn unwrap_in_non_result_enclosing_fn() {
let errs = parse_and_verify(r#"f url:t>t;x=get! url;x"#).unwrap_err();
assert!(errs.iter().any(|e| e.code == "ILO-T026" && e.message.contains("t")));
}
#[test]
fn nil_coalesce_optional_to_inner() {
assert!(parse_and_verify("f x:O n>n;y=x??0;y").is_ok());
}
#[test]
fn nil_coalesce_non_optional_passthrough() {
assert!(parse_and_verify("f x:n>n;y=x??0;y").is_ok());
}
#[test]
fn mget_non_map_first_arg_returns_unknown() {
assert!(parse_and_verify(r#"f x:n>O n;mget x "k""#).is_ok());
}
#[test]
fn mset_non_map_first_arg_returns_generic_map() {
assert!(parse_and_verify(r#"f x:n>M t t;mset x "k" "v""#).is_ok());
}
#[test]
fn rev_unknown_type_returns_unknown() {
assert!(parse_and_verify("f x:z>n;rev x").is_ok());
}
#[test]
fn unq_unknown_type_returns_unknown() {
assert!(parse_and_verify("f x:z>n;unq x").is_ok());
}
#[test]
fn compat_sum_to_sum_same_variants_ok() {
assert!(parse_and_verify(r#"f x:S a b>S a b;x"#).is_ok());
}
#[test]
fn flt_second_arg_not_list_returns_unknown() {
assert!(parse_and_verify("gt x:n>b;>x 0\nf xs:n>L n;flt gt xs").is_ok());
}
#[test]
fn fld_unknown_fn_and_unknown_init_returns_unknown() {
assert!(parse_and_verify("f cb:z xs:L n init:z>n;fld cb xs init").is_ok());
}
#[test]
fn destructure_named_type_not_in_types_binds_unknown() {
let result = parse_and_verify_full("f x:foo>n;{a}=x;a");
assert!(result.errors.iter().any(|e| e.code == "ILO-T003"), "expected ILO-T003");
}
#[test]
fn match_stmt_type_is_pattern_binds_var() {
assert!(parse_and_verify(r#"f x:n>t;?x{n v:"num";_:"other"}"#).is_ok());
}
#[test]
fn match_stmt_type_is_list_pattern_binds_var() {
use crate::ast::{Decl, Expr, MatchArm, Pattern, Program, Span, Spanned, Stmt, Type};
let lit_list = Expr::Literal(crate::ast::Literal::Text("list".to_string()));
let lit_other = Expr::Literal(crate::ast::Literal::Text("other".to_string()));
let arm_list = MatchArm {
pattern: Pattern::TypeIs { ty: Type::List(Box::new(Type::Text)), binding: "v".to_string() },
body: vec![Spanned::unknown(Stmt::Expr(lit_list))],
};
let arm_wild = MatchArm {
pattern: Pattern::Wildcard,
body: vec![Spanned::unknown(Stmt::Expr(lit_other))],
};
let prog = Program {
declarations: vec![Decl::Function {
name: "f".to_string(),
params: vec![crate::ast::Param { name: "x".to_string(), ty: Type::List(Box::new(Type::Text)) }],
return_type: Type::Text,
body: vec![Spanned::unknown(Stmt::Match {
subject: Some(Expr::Ref("x".to_string())),
arms: vec![arm_list, arm_wild],
})],
span: Span::UNKNOWN,
}],
source: None,
};
let result = verify(&prog);
assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
}
#[test]
fn srt_unknown_type_two_arg_second_unknown() {
assert!(parse_and_verify("double x:n>n;*x 2\nf xs:z>n;srt double xs").is_ok());
}
#[test]
fn flat_on_non_nested_list_hits_unknown_inner() {
let result = parse_and_verify_full("f xs:L n>_; flat xs");
let _ = result;
}
#[test]
fn flat_on_non_list_hits_unknown_fallback() {
let result = parse_and_verify_full("f x:n>_; flat x");
let _ = result;
}
#[test]
fn match_type_is_bool_binds_bool_type() {
assert!(parse_and_verify(r#"f x:_>t;?x{b y:"bool";t z:"other"}"#).is_ok());
}
#[test]
fn alias_diamond_dependency_hits_early_return() {
assert!(parse_and_verify("alias myint n\nalias mynum myint\nalias mycount myint\nf x:mynum y:mycount>n;+x y").is_ok());
}
#[test]
fn grp_with_unknown_key_type_falls_back() {
let result = parse_and_verify_full("f xs:L n>_;grp 42 xs");
let _ = result; }
#[test]
fn grp_with_non_list_second_arg() {
let result = parse_and_verify_full("f x:n>_;grp 42 x");
let _ = result;
}
#[test]
fn bang_on_undefined_callee_gives_unknown() {
let result = parse_and_verify("f>R t t;unk! 1");
assert!(result.is_err()); }
#[test]
fn bang_on_result_callee_with_result_enclosing() {
assert!(parse_and_verify(r#"f>R t t;rd! "/tmp/x""#).is_ok());
}
#[test]
fn srt_on_number_errors() {
let result = parse_and_verify("f x:n>_;srt x");
assert!(result.is_err());
}
#[test]
fn slc_on_number_errors() {
let result = parse_and_verify("f x:n>_;slc x 0 1");
assert!(result.is_err());
}
#[test]
fn hd_on_number_errors() {
let result = parse_and_verify("f x:n>_;hd x");
assert!(result.is_err());
}
#[test]
fn tl_on_number_errors() {
let result = parse_and_verify("f x:n>_;tl x");
assert!(result.is_err());
}
#[test]
fn rev_on_number_errors() {
let result = parse_and_verify("f x:n>_;rev x");
assert!(result.is_err());
}
#[test]
fn unq_on_number_errors() {
let result = parse_and_verify("f x:n>_;unq x");
assert!(result.is_err());
}
#[test]
fn has_on_number_errors() {
let result = parse_and_verify("f x:n>b;has x x");
assert!(result.is_err());
}
#[test]
fn alias_chain_covers_early_return() {
let result = parse_and_verify("alias a2 b2 alias b2 n f x:a2>n;x");
assert!(result.is_ok(), "expected ok, got: {:?}", result.unwrap_err());
}
#[test]
fn safe_field_access_on_nil_type_returns_nil() {
let result = parse_and_verify("type p{x:n} f r:_>n;s=r.?x;0");
let _ = result;
}
#[test]
fn safe_index_access_on_nil_type_returns_nil() {
let result = parse_and_verify("f r:_>n;s=r.?0;0");
let _ = result;
}
#[test]
fn nil_coalesce_on_nil_type() {
let result = parse_and_verify("f r:_>n;r??0");
let _ = result;
}
}