use crate::ast::*;
use crate::lexer::Token;
pub struct Parser {
tokens: Vec<(Token, Span)>,
pos: usize,
}
#[derive(Debug, thiserror::Error)]
#[error("Parse error at token {position}: {message}")]
pub struct ParseError {
pub code: &'static str,
pub position: usize,
pub span: Span,
pub message: String,
pub hint: Option<String>,
}
type Result<T> = std::result::Result<T, ParseError>;
impl Parser {
pub fn new(tokens: Vec<(Token, Span)>) -> Self {
let tokens: Vec<(Token, Span)> = tokens
.into_iter()
.filter(|(t, _)| *t != Token::Newline)
.collect();
Parser { tokens, pos: 0 }
}
fn peek(&self) -> Option<&Token> {
self.tokens.get(self.pos).map(|(t, _)| t)
}
fn peek_span(&self) -> Span {
self.tokens
.get(self.pos)
.map(|(_, s)| *s)
.unwrap_or(Span::UNKNOWN)
}
fn advance(&mut self) -> Option<&Token> {
let tok = self.tokens.get(self.pos).map(|(t, _)| t);
if tok.is_some() {
self.pos += 1;
}
tok
}
fn expect(&mut self, expected: &Token) -> Result<Span> {
match self.peek() {
Some(tok) if tok == expected => {
let span = self.peek_span();
self.advance();
Ok(span)
}
Some(tok) => {
let hint = if *expected == Token::Greater
&& *tok == Token::Minus
&& self.token_at(self.pos + 1) == Some(&Token::Greater)
{
Some("ilo uses '>' not '->' for the return type separator".to_string())
} else {
None
};
let mut err = self.error(
"ILO-P003",
format!("expected {:?}, got {:?}", expected, tok),
);
err.hint = hint;
Err(err)
}
None => Err(self.error("ILO-P004", format!("expected {:?}, got EOF", expected))),
}
}
fn expect_ident(&mut self) -> Result<String> {
match self.peek().cloned() {
Some(Token::Ident(name)) => {
self.advance();
Ok(name)
}
Some(tok) => {
if let Some((msg, hint)) = reserved_keyword_message(&tok) {
Err(self.error_hint("ILO-P011", msg, hint))
} else {
Err(self.error("ILO-P005", format!("expected identifier, got {:?}", tok)))
}
}
None => Err(self.error("ILO-P006", "expected identifier, got EOF".into())),
}
}
fn error(&self, code: &'static str, message: String) -> ParseError {
ParseError {
code,
position: self.pos,
span: self.peek_span(),
message,
hint: None,
}
}
fn error_hint(&self, code: &'static str, message: String, hint: String) -> ParseError {
ParseError {
code,
position: self.pos,
span: self.peek_span(),
message,
hint: Some(hint),
}
}
fn at_end(&self) -> bool {
self.pos >= self.tokens.len()
}
fn at_body_end(&self) -> bool {
matches!(self.peek(), None | Some(Token::RBrace))
}
fn token_at(&self, idx: usize) -> Option<&Token> {
self.tokens.get(idx).map(|(t, _)| t)
}
pub fn parse_program(&mut self) -> (Program, Vec<ParseError>) {
let mut declarations = Vec::new();
let mut errors: Vec<ParseError> = Vec::new();
const MAX_ERRORS: usize = 20;
while !self.at_end() {
if errors.len() >= MAX_ERRORS {
break;
}
match self.parse_decl() {
Ok(decl) => declarations.push(decl),
Err(e) => {
let err_span = e.span;
errors.push(e);
let end_span = self.sync_to_decl_boundary();
declarations.push(Decl::Error {
span: err_span.merge(end_span),
});
}
}
}
(
Program {
declarations,
source: None,
},
errors,
)
}
fn is_fn_decl_start(&self, pos: usize) -> bool {
if !matches!(self.token_at(pos), Some(Token::Ident(_))) {
return false;
}
match self.token_at(pos + 1) {
Some(Token::Greater) => true,
Some(Token::Ident(_)) => matches!(self.token_at(pos + 2), Some(Token::Colon)),
_ => false,
}
}
fn sync_to_decl_boundary(&mut self) -> Span {
let mut depth: usize = 0;
let mut last_span = self.peek_span();
loop {
match self.peek() {
None => break,
Some(Token::LBrace) => {
depth += 1;
last_span = self.peek_span();
self.advance();
}
Some(Token::RBrace) => {
if depth == 0 {
break;
}
depth -= 1;
last_span = self.peek_span();
self.advance();
}
Some(Token::Type) | Some(Token::Tool) if depth == 0 => break,
_ if depth == 0 && self.is_fn_decl_start(self.pos) => break,
_ => {
last_span = self.peek_span();
self.advance();
}
}
}
last_span
}
fn parse_decl(&mut self) -> Result<Decl> {
if self.token_at(self.pos + 1) == Some(&Token::Eq)
&& let Some(tok) = self.peek()
&& let Some((msg, _)) = reserved_keyword_message(tok)
{
return Err(self.error_hint(
"ILO-P011",
msg,
"use `name=expr` for bindings (e.g. `count=5`)".to_string(),
));
}
if let Some(Token::Ident(name)) = self.peek()
&& (name == "cnt" || name == "brk")
&& self.token_at(self.pos + 1) == Some(&Token::Eq)
{
let (word, role, alt) = if name == "cnt" {
("cnt", "continue", "count")
} else {
("brk", "break", "brake")
};
return Err(self.error_hint(
"ILO-P011",
format!("`{word}` is reserved for {role} (loop control) and cannot be used as an identifier"),
format!("pick a different name like `{alt}` or `{}`", &word[..1]),
));
}
match self.peek() {
Some(Token::Type) => self.parse_type_decl(),
Some(Token::Tool) => self.parse_tool_decl(),
Some(Token::Use) => self.parse_use_decl(),
Some(Token::Ident(_)) => {
let ident_str = match self.peek() {
Some(Token::Ident(s)) => s.as_str(),
_ => unreachable!(),
};
if ident_str == "alias" {
return self.parse_alias_decl();
}
let hint = match ident_str {
"function" | "def" | "fn" =>
Some("ilo function syntax: name param:type > return-type; body".to_string()),
"let" | "var" | "const" =>
Some("ilo uses assignment syntax: name = expr".to_string()),
"return" =>
Some("the last expression in a function body is the return value — no 'return' keyword".to_string()),
"if" =>
Some("ilo uses match for conditionals: ?expr{true:... false:...}".to_string()),
_ => None,
};
if let Some(hint_msg) = hint {
let mut err = self.error(
"ILO-P001",
format!("expected declaration, got Ident({ident_str:?})"),
);
err.hint = Some(hint_msg);
return Err(err);
}
self.parse_fn_decl()
}
Some(tok) => {
let msg = format!("expected declaration, got {:?}", tok);
let hint = match tok {
Token::Plus | Token::Minus | Token::Star | Token::Slash
| Token::Greater | Token::Less | Token::GreaterEq | Token::LessEq
| Token::Eq | Token::NotEq | Token::Amp | Token::Pipe
| Token::Bang | Token::Tilde | Token::Caret =>
Some("prefix operators can't start a declaration. Bind call results to variables: r=fac -n 1;*n r".to_string()),
Token::KwFn | Token::KwDef =>
Some("ilo function syntax: name param:type > return-type; body".to_string()),
Token::KwLet | Token::KwVar | Token::KwConst =>
Some("ilo uses assignment syntax: name = expr".to_string()),
Token::KwReturn =>
Some("the last expression in a function body is the return value — no 'return' keyword".to_string()),
Token::KwIf =>
Some("ilo uses match for conditionals: ?expr{true:... false:...}".to_string()),
_ => None,
};
let mut err = self.error("ILO-P001", msg);
err.hint = hint;
Err(err)
}
None => Err(self.error("ILO-P002", "expected declaration, got EOF".into())),
}
}
fn parse_use_decl(&mut self) -> Result<Decl> {
let start = self.peek_span();
self.expect(&Token::Use)?;
let path = match self.peek().cloned() {
Some(Token::Text(p)) => {
self.advance();
p
}
Some(tok) => {
return Err(self.error(
"ILO-P016",
format!("expected a string path after `use`, got {:?}", tok),
));
}
None => {
return Err(self.error(
"ILO-P016",
"expected a string path after `use`, got EOF".into(),
));
}
};
let only = if self.peek() == Some(&Token::LBracket) {
self.advance(); let mut names = Vec::new();
while self.peek() != Some(&Token::RBracket) {
match self.peek() {
None => {
return Err(self.error("ILO-P016", "unclosed `[` in use statement".into()));
}
_ => names.push(self.expect_ident()?),
}
}
self.expect(&Token::RBracket)?;
if names.is_empty() {
return Err(self.error(
"ILO-P016",
"use `[...]` list must not be empty — omit brackets to import all".into(),
));
}
Some(names)
} else {
None
};
let end = self.peek_span();
Ok(Decl::Use {
path,
only,
span: start.merge(end),
})
}
fn parse_type_decl(&mut self) -> Result<Decl> {
let start = self.peek_span();
self.expect(&Token::Type)?;
let name = self.expect_ident()?;
self.expect(&Token::LBrace)?;
let mut fields = Vec::new();
while self.peek() != Some(&Token::RBrace) {
if !fields.is_empty() {
self.expect(&Token::Semi)?;
}
let fname = self.expect_ident()?;
self.expect(&Token::Colon)?;
let ty = self.parse_type()?;
fields.push(Param { name: fname, ty });
}
let end = self.peek_span();
self.expect(&Token::RBrace)?;
Ok(Decl::TypeDef {
name,
fields,
span: start.merge(end),
})
}
fn parse_tool_decl(&mut self) -> Result<Decl> {
let start = self.peek_span();
self.expect(&Token::Tool)?;
let name = self.expect_ident()?;
let description = match self.peek().cloned() {
Some(Token::Text(s)) => {
self.advance();
s
}
_ => return Err(self.error("ILO-P015", "expected tool description string".into())),
};
let params = self.parse_params()?;
self.expect(&Token::Greater)?;
let return_type = self.parse_type()?;
let mut timeout = None;
let mut retry = None;
while matches!(self.peek(), Some(Token::Timeout) | Some(Token::Retry)) {
match self.peek() {
Some(Token::Timeout) => {
self.advance();
self.expect(&Token::Colon)?;
timeout = Some(self.parse_number()?);
}
Some(Token::Retry) => {
self.advance();
self.expect(&Token::Colon)?;
retry = Some(self.parse_number()?);
}
_ => break,
}
if self.peek() == Some(&Token::Comma) {
self.advance();
}
}
let end_span = self.prev_span();
Ok(Decl::Tool {
name,
description,
params,
return_type,
timeout,
retry,
span: start.merge(end_span),
})
}
fn parse_alias_decl(&mut self) -> Result<Decl> {
let start = self.peek_span();
self.advance();
let name = self.expect_ident()?;
let target = self.parse_type()?;
let end = self.prev_span();
Ok(Decl::Alias {
name,
target,
span: start.merge(end),
})
}
fn parse_fn_decl(&mut self) -> Result<Decl> {
let start = self.peek_span();
let name = self.expect_ident()?;
let params = self.parse_params()?;
self.expect(&Token::Greater)?;
let return_type = self.parse_type()?;
self.expect(&Token::Semi)?;
let body = self.parse_body()?;
let end = self.prev_span();
Ok(Decl::Function {
name,
params,
return_type,
body,
span: start.merge(end),
})
}
fn prev_span(&self) -> Span {
if self.pos > 0 {
self.tokens[self.pos - 1].1
} else {
Span::UNKNOWN
}
}
fn parse_type(&mut self) -> Result<Type> {
match self.peek().cloned() {
Some(Token::Ident(ref s)) if s == "n" => {
self.advance();
Ok(Type::Number)
}
Some(Token::Ident(ref s)) if s == "t" => {
self.advance();
Ok(Type::Text)
}
Some(Token::Ident(ref s)) if s == "b" => {
self.advance();
Ok(Type::Bool)
}
Some(Token::Underscore) => {
self.advance();
Ok(Type::Any)
}
Some(Token::OptType) => {
self.advance();
let inner = self.parse_type()?;
Ok(Type::Optional(Box::new(inner)))
}
Some(Token::ListType) => {
self.advance();
let inner = self.parse_type()?;
Ok(Type::List(Box::new(inner)))
}
Some(Token::MapType) => {
self.advance();
let key_type = self.parse_type()?;
let val_type = self.parse_type()?;
Ok(Type::Map(Box::new(key_type), Box::new(val_type)))
}
Some(Token::ResultType) => {
self.advance();
let ok_type = self.parse_type()?;
let err_type = self.parse_type()?;
Ok(Type::Result(Box::new(ok_type), Box::new(err_type)))
}
Some(Token::SumType) => {
self.advance();
let mut variants = Vec::new();
while let Some(Token::Ident(_)) = self.peek() {
if self.token_at(self.pos + 1) == Some(&Token::Colon) {
break;
}
if let Some(Token::Ident(name)) = self.peek().cloned() {
variants.push(name);
self.advance();
}
}
if variants.is_empty() {
return Err(
self.error("ILO-P010", "S type requires at least one variant".into())
);
}
Ok(Type::Sum(variants))
}
Some(Token::FnType) => {
self.advance();
let mut types = Vec::new();
loop {
if !self.can_start_type() {
break;
}
if matches!(self.peek(), Some(Token::Ident(_)))
&& self.token_at(self.pos + 1) == Some(&Token::Colon)
{
break;
}
types.push(self.parse_type()?);
}
if types.is_empty() {
return Err(
self.error("ILO-P009", "F type requires at least a return type".into())
);
}
let return_type = types.pop().expect("F type requires at least a return type");
Ok(Type::Fn(types, Box::new(return_type)))
}
Some(Token::Ident(name)) => {
self.advance();
Ok(Type::Named(name))
}
Some(tok) => Err(self.error("ILO-P007", format!("expected type, got {:?}", tok))),
None => Err(self.error("ILO-P008", "expected type, got EOF".into())),
}
}
fn can_start_type(&self) -> bool {
match self.peek() {
Some(Token::Ident(s)) => {
matches!(s.as_str(), "n" | "t" | "b")
|| self.token_at(self.pos + 1) != Some(&Token::Colon)
}
Some(Token::Underscore) => true,
Some(Token::OptType) => true,
Some(Token::ListType) => true,
Some(Token::MapType) => true,
Some(Token::ResultType) => true,
Some(Token::SumType) => true,
Some(Token::FnType) => true,
_ => false,
}
}
fn parse_params(&mut self) -> Result<Vec<Param>> {
let mut params = Vec::new();
while let Some(Token::Ident(_)) = self.peek() {
if self.pos + 1 < self.tokens.len()
&& self.token_at(self.pos + 1) == Some(&Token::Colon)
{
let name = self.expect_ident()?;
self.expect(&Token::Colon)?;
let ty = self.parse_type()?;
params.push(Param { name, ty });
} else {
break;
}
}
Ok(params)
}
fn parse_body(&mut self) -> Result<Vec<Spanned<Stmt>>> {
let mut stmts = Vec::new();
if !self.at_body_end() {
let span_start = self.peek_span();
let stmt = self.parse_stmt()?;
stmts.push(Spanned {
node: stmt,
span: span_start.merge(self.prev_span()),
});
while self.peek() == Some(&Token::Semi) {
self.advance();
if self.at_body_end() {
break;
}
let span_start = self.peek_span();
let stmt = self.parse_stmt()?;
stmts.push(Spanned {
node: stmt,
span: span_start.merge(self.prev_span()),
});
}
}
Ok(stmts)
}
fn parse_stmt(&mut self) -> Result<Stmt> {
if self.token_at(self.pos + 1) == Some(&Token::Eq)
&& let Some(tok) = self.peek()
&& let Some((msg, _)) = reserved_keyword_message(tok)
{
return Err(self.error_hint(
"ILO-P011",
msg,
"use `name=expr` for bindings (e.g. `count=5`)".to_string(),
));
}
match self.peek() {
Some(Token::Question) => {
if self.is_prefix_ternary() {
let expr = self.parse_prefix_ternary()?;
Ok(Stmt::Expr(expr))
} else {
self.parse_match_stmt()
}
}
Some(Token::At) => self.parse_foreach(),
Some(Token::Ident(name)) if name == "ret" => {
self.advance(); let value = self.parse_expr()?;
Ok(Stmt::Return(value))
}
Some(Token::Ident(name)) if name == "brk" => {
if self.token_at(self.pos + 1) == Some(&Token::Eq) {
return Err(self.error_hint(
"ILO-P011",
"`brk` is reserved for break (loop control) and cannot be used as an identifier".into(),
"pick a different name like `brake` or `b`".into(),
));
}
self.advance(); let value = if self.at_body_end() {
None
} else {
Some(self.parse_expr()?)
};
Ok(Stmt::Break(value))
}
Some(Token::Ident(name)) if name == "cnt" => {
if self.token_at(self.pos + 1) == Some(&Token::Eq) {
return Err(self.error_hint(
"ILO-P011",
"`cnt` is reserved for continue (loop control) and cannot be used as an identifier".into(),
"pick a different name like `count` or `c`".into(),
));
}
self.advance(); Ok(Stmt::Continue)
}
Some(Token::Ident(name)) if name == "wh" => {
self.advance(); let condition = self.parse_expr()?;
self.expect(&Token::LBrace)?;
let body = self.parse_body()?;
self.expect(&Token::RBrace)?;
Ok(Stmt::While { condition, body })
}
Some(Token::LBrace) if self.is_destructure_pattern() => self.parse_destructure(),
Some(Token::Ident(_)) => {
if self.pos + 1 < self.tokens.len()
&& self.token_at(self.pos + 1) == Some(&Token::Eq)
{
self.parse_let()
} else {
self.parse_expr_or_guard()
}
}
Some(Token::Bang) => {
self.parse_bang_stmt()
}
Some(Token::Caret) => {
self.parse_caret_stmt()
}
_ => {
let expr = self.parse_expr()?;
if self.peek() == Some(&Token::LBrace) {
let body = self.parse_brace_body()?;
let else_body = if self.peek() == Some(&Token::LBrace) {
Some(self.parse_brace_body()?)
} else {
None
};
Ok(Stmt::Guard {
condition: expr,
negated: false,
body,
else_body,
braceless: false,
})
} else if is_guard_eligible_condition(&expr) && self.can_start_operand() {
Ok(self.parse_braceless_guard_body(expr, false)?)
} else {
Ok(Stmt::Expr(expr))
}
}
}
}
fn parse_let(&mut self) -> Result<Stmt> {
let name = self.expect_ident()?;
self.expect(&Token::Eq)?;
let value = self.parse_expr()?;
if self.peek() == Some(&Token::LBrace) && is_guard_eligible_condition(&value) {
let then_body = self.parse_brace_body()?;
if self.peek() == Some(&Token::LBrace) {
let else_body = self.parse_brace_body()?;
let then_expr = body_to_expr(then_body);
let else_expr = body_to_expr(else_body);
Ok(Stmt::Let {
name,
value: Expr::Ternary {
condition: Box::new(value),
then_expr: Box::new(then_expr),
else_expr: Box::new(else_expr),
},
})
} else {
let body_with_let = wrap_body_as_let(&name, then_body);
Ok(Stmt::Guard {
condition: value,
negated: false,
body: body_with_let,
else_body: None,
braceless: false,
})
}
} else {
Ok(Stmt::Let { name, value })
}
}
fn is_destructure_pattern(&self) -> bool {
let mut pos = self.pos + 1; loop {
match self.token_at(pos) {
Some(Token::Ident(_)) => pos += 1,
Some(Token::Semi) => pos += 1,
Some(Token::RBrace) => {
return self.token_at(pos + 1) == Some(&Token::Eq);
}
_ => return false,
}
}
}
fn parse_destructure(&mut self) -> Result<Stmt> {
self.expect(&Token::LBrace)?;
let mut bindings = Vec::new();
loop {
let name = self.expect_ident()?;
bindings.push(name);
if self.peek() == Some(&Token::Semi) {
self.advance(); } else {
break;
}
}
self.expect(&Token::RBrace)?;
self.expect(&Token::Eq)?;
let value = self.parse_expr()?;
Ok(Stmt::Destructure { bindings, value })
}
fn parse_match_stmt(&mut self) -> Result<Stmt> {
self.expect(&Token::Question)?;
let subject = if self.peek() == Some(&Token::LBrace) {
None
} else {
Some(self.parse_atom()?)
};
self.expect(&Token::LBrace)?;
let arms = self.parse_match_arms()?;
self.expect(&Token::RBrace)?;
Ok(Stmt::Match { subject, arms })
}
fn parse_match_arms(&mut self) -> Result<Vec<MatchArm>> {
let mut arms = Vec::new();
while self.peek() != Some(&Token::RBrace) {
if !arms.is_empty() {
self.expect(&Token::Semi)?;
if self.peek() == Some(&Token::RBrace) {
break;
}
}
arms.push(self.parse_match_arm()?);
}
Ok(arms)
}
fn parse_match_arm(&mut self) -> Result<MatchArm> {
let pattern = self.parse_pattern()?;
self.expect(&Token::Colon)?;
let body = self.parse_arm_body()?;
Ok(MatchArm { pattern, body })
}
fn parse_arm_body(&mut self) -> Result<Vec<Spanned<Stmt>>> {
let mut stmts = Vec::new();
if !self.at_arm_end() {
let span_start = self.peek_span();
let stmt = self.parse_stmt()?;
stmts.push(Spanned {
node: stmt,
span: span_start.merge(self.prev_span()),
});
while self.peek() == Some(&Token::Semi) && !self.semi_starts_new_arm() {
self.advance(); if self.at_arm_end() {
break;
}
let span_start = self.peek_span();
let stmt = self.parse_stmt()?;
stmts.push(Spanned {
node: stmt,
span: span_start.merge(self.prev_span()),
});
}
}
Ok(stmts)
}
fn semi_starts_new_arm(&self) -> bool {
if self.peek() != Some(&Token::Semi) {
return false;
}
let after_semi = self.pos + 1;
if after_semi >= self.tokens.len() {
return false;
}
match self.token_at(after_semi) {
Some(Token::Caret) => {
if after_semi + 2 < self.tokens.len() {
matches!(
(self.token_at(after_semi + 1), self.token_at(after_semi + 2)),
(
Some(Token::Ident(_) | Token::Underscore),
Some(Token::Colon)
)
)
} else {
false
}
}
Some(Token::Tilde) => {
if after_semi + 2 < self.tokens.len() {
matches!(
(self.token_at(after_semi + 1), self.token_at(after_semi + 2)),
(
Some(Token::Ident(_) | Token::Underscore),
Some(Token::Colon)
)
)
} else {
false
}
}
Some(Token::Underscore) => {
after_semi + 1 < self.tokens.len()
&& self.token_at(after_semi + 1) == Some(&Token::Colon)
}
Some(Token::Number(_) | Token::Text(_) | Token::True | Token::False | Token::Nil) => {
after_semi + 1 < self.tokens.len()
&& self.token_at(after_semi + 1) == Some(&Token::Colon)
}
Some(Token::Ident(ty_name)) if matches!(ty_name.as_str(), "n" | "t" | "b" | "l") => {
if after_semi + 2 < self.tokens.len() {
matches!(
(self.token_at(after_semi + 1), self.token_at(after_semi + 2)),
(
Some(Token::Ident(_) | Token::Underscore),
Some(Token::Colon)
)
)
} else {
false
}
}
_ => false,
}
}
fn at_arm_end(&self) -> bool {
matches!(self.peek(), None | Some(Token::RBrace) | Some(Token::Semi))
}
fn parse_pattern(&mut self) -> Result<Pattern> {
match self.peek() {
Some(Token::Caret) => {
self.advance();
let name = match self.peek() {
Some(Token::Underscore) => {
self.advance();
"_".to_string()
}
_ => self.expect_ident()?,
};
Ok(Pattern::Err(name))
}
Some(Token::Tilde) => {
self.advance();
let name = match self.peek() {
Some(Token::Underscore) => {
self.advance();
"_".to_string()
}
_ => self.expect_ident()?,
};
Ok(Pattern::Ok(name))
}
Some(Token::Underscore) => {
self.advance();
Ok(Pattern::Wildcard)
}
Some(Token::Number(_)) => {
if let Some(Token::Number(n)) = self.advance().cloned() {
Ok(Pattern::Literal(Literal::Number(n)))
} else {
unreachable!()
}
}
Some(Token::Text(_)) => {
if let Some(Token::Text(s)) = self.advance().cloned() {
Ok(Pattern::Literal(Literal::Text(s)))
} else {
unreachable!()
}
}
Some(Token::True) => {
self.advance();
Ok(Pattern::Literal(Literal::Bool(true)))
}
Some(Token::False) => {
self.advance();
Ok(Pattern::Literal(Literal::Bool(false)))
}
Some(Token::Nil) => {
self.advance();
Ok(Pattern::Literal(Literal::Nil))
}
Some(Token::Ident(name)) if matches!(name.as_str(), "n" | "t" | "b" | "l") => {
let ty = match name.as_str() {
"n" => Type::Number,
"t" => Type::Text,
"b" => Type::Bool,
"l" => Type::List(Box::new(Type::Text)),
_ => unreachable!(),
};
self.advance();
let binding = match self.peek() {
Some(Token::Underscore) => {
self.advance();
"_".to_string()
}
_ => self.expect_ident()?,
};
Ok(Pattern::TypeIs { ty, binding })
}
Some(tok) => Err(self.error("ILO-P011", format!("expected pattern, got {:?}", tok))),
None => Err(self.error("ILO-P012", "expected pattern, got EOF".into())),
}
}
fn parse_foreach(&mut self) -> Result<Stmt> {
self.expect(&Token::At)?;
let binding = self.expect_ident()?;
let start_expr = self.parse_atom()?;
if self.peek() == Some(&Token::DotDot) {
self.advance(); let end_expr = self.parse_atom()?;
let body = self.parse_brace_body()?;
return Ok(Stmt::ForRange {
binding,
start: start_expr,
end: end_expr,
body,
});
}
let body = self.parse_brace_body()?;
Ok(Stmt::ForEach {
binding,
collection: start_expr,
body,
})
}
fn parse_bang_stmt(&mut self) -> Result<Stmt> {
self.expect(&Token::Bang)?;
let inner = self.parse_expr_inner()?;
if self.peek() == Some(&Token::LBrace) {
let body = self.parse_brace_body()?;
let else_body = if self.peek() == Some(&Token::LBrace) {
Some(self.parse_brace_body()?)
} else {
None
};
Ok(Stmt::Guard {
condition: inner,
negated: true,
body,
else_body,
braceless: false,
})
} else if is_guard_eligible_condition(&inner) && self.can_start_operand() {
Ok(self.parse_braceless_guard_body(inner, true)?)
} else {
Ok(Stmt::Expr(Expr::UnaryOp {
op: UnaryOp::Not,
operand: Box::new(inner),
}))
}
}
fn parse_caret_stmt(&mut self) -> Result<Stmt> {
self.expect(&Token::Caret)?;
let inner = self.parse_expr_inner()?;
Ok(Stmt::Expr(Expr::Err(Box::new(inner))))
}
fn parse_expr_or_guard(&mut self) -> Result<Stmt> {
let expr = self.parse_expr()?;
if self.peek() == Some(&Token::LBrace) {
let body = self.parse_brace_body()?;
let else_body = if self.peek() == Some(&Token::LBrace) {
Some(self.parse_brace_body()?)
} else {
None
};
Ok(Stmt::Guard {
condition: expr,
negated: false,
body,
else_body,
braceless: false,
})
} else if is_guard_eligible_condition(&expr) && self.can_start_operand() {
Ok(self.parse_braceless_guard_body(expr, false)?)
} else {
Ok(Stmt::Expr(expr))
}
}
fn parse_braceless_guard_body(&mut self, condition: Expr, negated: bool) -> Result<Stmt> {
let body_start = self.peek_span();
let body_expr = self.parse_operand()?;
let body_span = body_start.merge(self.prev_span());
if !matches!(self.peek(), None | Some(Token::Semi) | Some(Token::RBrace)) {
return Err(self.error_hint(
"ILO-P016",
"unexpected token after braceless guard body".to_string(),
"function calls in braceless guards need braces: >=cond val{func args}".to_string(),
));
}
Ok(Stmt::Guard {
condition,
negated,
body: vec![Spanned::new(Stmt::Expr(body_expr), body_span)],
else_body: None,
braceless: true,
})
}
fn parse_brace_body(&mut self) -> Result<Vec<Spanned<Stmt>>> {
self.expect(&Token::LBrace)?;
let body = self.parse_body()?;
self.expect(&Token::RBrace)?;
Ok(body)
}
fn parse_expr(&mut self) -> Result<Expr> {
let expr = match self.peek() {
Some(Token::Tilde) => {
self.advance();
let inner = self.parse_expr_inner()?;
Expr::Ok(Box::new(inner))
}
Some(Token::Caret) => {
self.advance();
let inner = self.parse_expr_inner()?;
Expr::Err(Box::new(inner))
}
_ => self.parse_expr_inner()?,
};
let expr = self.maybe_with(expr)?;
let expr = self.maybe_nil_coalesce(expr)?;
self.maybe_pipe(expr)
}
fn maybe_with(&mut self, expr: Expr) -> Result<Expr> {
if matches!(self.peek(), Some(Token::With)) {
self.advance();
let mut updates = Vec::new();
while let Some(Token::Ident(_)) = self.peek() {
if self.pos + 1 < self.tokens.len()
&& self.token_at(self.pos + 1) == Some(&Token::Colon)
{
let name = self.expect_ident()?;
self.expect(&Token::Colon)?;
let value = self.parse_atom()?;
updates.push((name, value));
} else {
break;
}
}
Ok(Expr::With {
object: Box::new(expr),
updates,
})
} else {
Ok(expr)
}
}
fn maybe_nil_coalesce(&mut self, mut expr: Expr) -> Result<Expr> {
while matches!(self.peek(), Some(Token::NilCoalesce)) {
self.advance(); let default = self.parse_expr_inner()?;
expr = Expr::NilCoalesce {
value: Box::new(expr),
default: Box::new(default),
};
}
Ok(expr)
}
fn maybe_pipe(&mut self, mut expr: Expr) -> Result<Expr> {
while matches!(self.peek(), Some(Token::PipeOp)) {
self.advance(); let func_name = self.expect_ident()?;
let unwrap = self.peek() == Some(&Token::Bang) && {
let prev = self.prev_span();
let bang = self.peek_span();
prev.end > 0 && bang.start == prev.end
};
if unwrap {
self.advance(); }
let mut args = Vec::new();
while self.can_start_operand() {
args.push(self.parse_operand()?);
}
args.push(expr);
expr = Expr::Call {
function: func_name,
args,
unwrap,
};
}
Ok(expr)
}
fn infix_binding_power(token: &Token) -> Option<(u8, u8, BinOp)> {
match token {
Token::Pipe => Some((1, 2, BinOp::Or)),
Token::Amp => Some((3, 4, BinOp::And)),
Token::Eq => Some((5, 6, BinOp::Equals)),
Token::NotEq => Some((5, 6, BinOp::NotEquals)),
Token::Less => Some((7, 8, BinOp::LessThan)),
Token::Greater => Some((7, 8, BinOp::GreaterThan)),
Token::LessEq => Some((7, 8, BinOp::LessOrEqual)),
Token::GreaterEq => Some((7, 8, BinOp::GreaterOrEqual)),
Token::PlusEq => Some((9, 10, BinOp::Append)),
Token::Plus => Some((9, 10, BinOp::Add)),
Token::Minus => Some((9, 10, BinOp::Subtract)),
Token::Star => Some((11, 12, BinOp::Multiply)),
Token::Slash => Some((11, 12, BinOp::Divide)),
_ => None,
}
}
fn parse_infix(&mut self, mut left: Expr, min_bp: u8) -> Result<Expr> {
while let Some(token) = self.peek() {
let Some((l_bp, r_bp, op)) = Self::infix_binding_power(token) else {
break;
};
if l_bp < min_bp {
break;
}
self.advance(); let right = self.parse_operand()?;
let right = self.parse_infix(right, r_bp)?;
left = Expr::BinOp {
op,
left: Box::new(left),
right: Box::new(right),
};
}
Ok(left)
}
fn parse_list_element(&mut self) -> Result<Expr> {
match self.peek() {
Some(Token::Tilde) => {
self.advance();
let inner = self.parse_expr_inner()?;
Ok(Expr::Ok(Box::new(inner)))
}
Some(Token::Caret) => {
self.advance();
let inner = self.parse_expr_inner()?;
Ok(Expr::Err(Box::new(inner)))
}
_ => self.parse_expr_inner(),
}
}
fn parse_expr_inner(&mut self) -> Result<Expr> {
match self.peek() {
Some(Token::Minus) => self.parse_minus(),
Some(Token::Bang) => {
self.advance();
let operand = self.parse_operand()?;
Ok(Expr::UnaryOp {
op: UnaryOp::Not,
operand: Box::new(operand),
})
}
Some(Token::Dollar) => self.parse_dollar(),
Some(Token::Plus)
| Some(Token::Star)
| Some(Token::Slash)
| Some(Token::Greater)
| Some(Token::Less)
| Some(Token::GreaterEq)
| Some(Token::LessEq)
| Some(Token::Eq)
| Some(Token::NotEq)
| Some(Token::Amp)
| Some(Token::Pipe)
| Some(Token::PlusEq) => self.parse_prefix_binop(),
Some(Token::Question) => self.parse_question_expr(),
_ => {
let primary = self.parse_call_or_atom()?;
self.parse_infix(primary, 0)
}
}
}
fn parse_dollar(&mut self) -> Result<Expr> {
self.advance(); let unwrap = self.peek() == Some(&Token::Bang) && {
let prev = self.prev_span();
let bang = self.peek_span();
prev.end > 0 && bang.start == prev.end
};
if unwrap {
self.advance(); }
let arg = self.parse_operand()?;
Ok(Expr::Call {
function: "get".to_string(),
args: vec![arg],
unwrap,
})
}
fn is_prefix_ternary(&self) -> bool {
matches!(
self.token_at(self.pos + 1),
Some(
Token::Eq
| Token::Greater
| Token::Less
| Token::GreaterEq
| Token::LessEq
| Token::NotEq
)
)
}
fn parse_question_expr(&mut self) -> Result<Expr> {
if self.is_prefix_ternary() {
return self.parse_prefix_ternary();
}
self.parse_match_expr()
}
fn parse_prefix_ternary(&mut self) -> Result<Expr> {
self.advance(); let condition = self.parse_prefix_binop()?;
let then_expr = self.parse_operand()?;
let else_expr = self.parse_operand()?;
Ok(Expr::Ternary {
condition: Box::new(condition),
then_expr: Box::new(then_expr),
else_expr: Box::new(else_expr),
})
}
fn parse_match_expr(&mut self) -> Result<Expr> {
self.expect(&Token::Question)?;
let subject = if self.peek() == Some(&Token::LBrace) {
None
} else {
Some(Box::new(self.parse_atom()?))
};
self.expect(&Token::LBrace)?;
let arms = self.parse_match_arms()?;
self.expect(&Token::RBrace)?;
Ok(Expr::Match { subject, arms })
}
fn parse_minus(&mut self) -> Result<Expr> {
self.advance(); let first = self.parse_operand()?;
if self.can_start_operand() {
let second = self.parse_operand()?;
Ok(Expr::BinOp {
op: BinOp::Subtract,
left: Box::new(first),
right: Box::new(second),
})
} else {
Ok(Expr::UnaryOp {
op: UnaryOp::Negate,
operand: Box::new(first),
})
}
}
fn parse_prefix_binop(&mut self) -> Result<Expr> {
let op = match self.advance() {
Some(Token::Plus) => BinOp::Add,
Some(Token::Star) => BinOp::Multiply,
Some(Token::Slash) => BinOp::Divide,
Some(Token::Greater) => BinOp::GreaterThan,
Some(Token::Less) => BinOp::LessThan,
Some(Token::GreaterEq) => BinOp::GreaterOrEqual,
Some(Token::LessEq) => BinOp::LessOrEqual,
Some(Token::Eq) => BinOp::Equals,
Some(Token::NotEq) => BinOp::NotEquals,
Some(Token::Amp) => {
if self.peek() == Some(&Token::Amp) {
return Err(self.error_hint(
"ILO-P003",
"unexpected '&&': ilo uses single '&' for AND".to_string(),
"ilo uses single '&' for AND, '|' for OR".to_string(),
));
}
BinOp::And
}
Some(Token::Pipe) => {
if self.peek() == Some(&Token::Pipe) {
return Err(self.error_hint(
"ILO-P003",
"unexpected '||': ilo uses single '|' for OR".to_string(),
"ilo uses single '&' for AND, '|' for OR".to_string(),
));
}
BinOp::Or
}
Some(Token::PlusEq) => BinOp::Append,
_ => unreachable!(),
};
let left = self.parse_operand()?;
let right = self.parse_operand()?;
Ok(Expr::BinOp {
op,
left: Box::new(left),
right: Box::new(right),
})
}
fn parse_call_or_atom(&mut self) -> Result<Expr> {
let atom = self.parse_atom()?;
if let Expr::Ref(ref name) = atom {
let name = name.clone();
let unwrap = self.peek() == Some(&Token::Bang) && {
let prev = self.prev_span();
let bang = self.peek_span();
prev.end > 0 && bang.start == prev.end
};
if unwrap {
self.advance(); }
if self.peek() == Some(&Token::LParen)
&& self.pos + 1 < self.tokens.len()
&& self.token_at(self.pos + 1) == Some(&Token::RParen)
{
self.advance(); self.advance(); return Ok(Expr::Call {
function: name,
args: vec![],
unwrap,
});
}
if unwrap {
let mut args = Vec::new();
while self.can_start_operand() {
args.push(self.parse_operand()?);
}
return Ok(Expr::Call {
function: name,
args,
unwrap: true,
});
}
if self.is_named_field_ahead() {
return self.parse_record(name);
}
if (name == "rnd" || name == "now" || name == "mmap") && !self.can_start_operand() {
return Ok(Expr::Call {
function: name,
args: vec![],
unwrap: false,
});
}
if self.can_start_operand() {
if let Some(tok) = self.peek()
&& Self::infix_binding_power(tok).is_some()
&& !self.looks_like_prefix_binary(self.pos)
{
return Ok(atom);
}
let mut args = Vec::new();
while self.can_start_operand() {
args.push(self.parse_operand()?);
if let Some(tok) = self.peek()
&& Self::infix_binding_power(tok).is_some()
{
break;
}
}
return Ok(Expr::Call {
function: name,
args,
unwrap: false,
});
}
}
Ok(atom)
}
fn is_named_field_ahead(&self) -> bool {
if let Some(Token::Ident(_)) = self.peek()
&& self.pos + 1 < self.tokens.len()
&& self.token_at(self.pos + 1) == Some(&Token::Colon)
{
return true;
}
false
}
fn parse_record(&mut self, type_name: String) -> Result<Expr> {
let mut fields = Vec::new();
while self.is_named_field_ahead() {
let fname = self.expect_ident()?;
self.expect(&Token::Colon)?;
let value = self.parse_atom()?;
fields.push((fname, value));
}
Ok(Expr::Record { type_name, fields })
}
fn looks_like_prefix_binary(&self, pos: usize) -> bool {
if pos >= self.tokens.len() {
return false;
}
let mut count = 0;
let mut look = pos + 1;
while look < self.tokens.len() {
if self.is_fn_decl_start(look) {
break;
}
let t = &self.tokens[look].0;
match t {
Token::Ident(_)
| Token::Number(_)
| Token::Text(_)
| Token::True
| Token::False
| Token::Nil
| Token::Underscore => {
count += 1;
look += 1;
}
Token::LParen | Token::LBracket => {
count += 1;
let close = if *t == Token::LParen {
Token::RParen
} else {
Token::RBracket
};
let mut depth = 1;
look += 1;
while look < self.tokens.len() && depth > 0 {
let inner = &self.tokens[look].0;
if *inner == *t {
depth += 1;
}
if *inner == close {
depth -= 1;
}
look += 1;
}
}
_ => break,
}
}
count >= 2
}
fn can_start_atom(&self) -> bool {
matches!(
self.peek(),
Some(Token::Ident(_))
| Some(Token::Number(_))
| Some(Token::Text(_))
| Some(Token::True)
| Some(Token::False)
| Some(Token::Nil)
| Some(Token::Underscore)
| Some(Token::LParen)
| Some(Token::LBracket)
)
}
fn can_start_operand(&self) -> bool {
if self.is_fn_decl_start(self.pos) {
return false;
}
self.can_start_atom()
|| matches!(
self.peek(),
Some(Token::Plus)
| Some(Token::Minus)
| Some(Token::Star)
| Some(Token::Slash)
| Some(Token::Greater)
| Some(Token::Less)
| Some(Token::GreaterEq)
| Some(Token::LessEq)
| Some(Token::Eq)
| Some(Token::NotEq)
| Some(Token::Amp)
| Some(Token::Pipe)
| Some(Token::PlusEq)
| Some(Token::Bang)
| Some(Token::Tilde)
| Some(Token::Caret)
| Some(Token::Dollar)
)
}
fn parse_operand(&mut self) -> Result<Expr> {
match self.peek() {
Some(Token::Plus)
| Some(Token::Star)
| Some(Token::Slash)
| Some(Token::Greater)
| Some(Token::Less)
| Some(Token::GreaterEq)
| Some(Token::LessEq)
| Some(Token::Eq)
| Some(Token::NotEq)
| Some(Token::Amp)
| Some(Token::Pipe)
| Some(Token::PlusEq) => self.parse_prefix_binop(),
Some(Token::Minus) => self.parse_minus(),
Some(Token::Bang) => {
self.advance();
let operand = self.parse_operand()?;
Ok(Expr::UnaryOp {
op: UnaryOp::Not,
operand: Box::new(operand),
})
}
Some(Token::Tilde) => {
self.advance();
let inner = self.parse_operand()?;
Ok(Expr::Ok(Box::new(inner)))
}
Some(Token::Caret) => {
self.advance();
let inner = self.parse_operand()?;
Ok(Expr::Err(Box::new(inner)))
}
Some(Token::Dollar) => self.parse_dollar(),
_ => self.parse_atom(),
}
}
fn parse_atom(&mut self) -> Result<Expr> {
match self.peek().cloned() {
Some(Token::Number(n)) => {
self.advance();
Ok(Expr::Literal(Literal::Number(n)))
}
Some(Token::Text(s)) => {
self.advance();
Ok(Expr::Literal(Literal::Text(s)))
}
Some(Token::True) => {
self.advance();
Ok(Expr::Literal(Literal::Bool(true)))
}
Some(Token::False) => {
self.advance();
Ok(Expr::Literal(Literal::Bool(false)))
}
Some(Token::Nil) => {
self.advance();
Ok(Expr::Literal(Literal::Nil))
}
Some(Token::Underscore) => {
self.advance();
Ok(Expr::Ref("_".to_string()))
}
Some(Token::LParen) => {
self.advance();
let expr = self.parse_expr()?;
self.expect(&Token::RParen)?;
Ok(expr)
}
Some(Token::LBracket) => {
self.advance();
let mut items = Vec::new();
while self.peek() != Some(&Token::RBracket) {
items.push(self.parse_list_element()?);
if self.peek() == Some(&Token::Comma) {
self.advance();
}
}
self.expect(&Token::RBracket)?;
Ok(Expr::List(items))
}
Some(Token::Ident(name)) => {
self.advance();
if name == "mmap" {
return Ok(Expr::Call {
function: name,
args: vec![],
unwrap: false,
});
}
let mut expr = Expr::Ref(name);
while matches!(self.peek(), Some(Token::Dot) | Some(Token::DotQuestion)) {
let safe = self.peek() == Some(&Token::DotQuestion);
self.advance();
match self.peek().cloned() {
Some(Token::Number(n)) if n.fract() == 0.0 && n >= 0.0 => {
self.advance();
expr = Expr::Index {
object: Box::new(expr),
index: n as usize,
safe,
};
}
_ => {
let field = self.expect_ident()?;
expr = Expr::Field {
object: Box::new(expr),
field,
safe,
};
}
}
}
Ok(expr)
}
Some(tok) => Err(self.error("ILO-P009", format!("expected expression, got {:?}", tok))),
None => Err(self.error("ILO-P010", "expected expression, got EOF".into())),
}
}
fn parse_number(&mut self) -> Result<f64> {
match self.peek().cloned() {
Some(Token::Number(n)) => {
self.advance();
Ok(n)
}
Some(tok) => Err(self.error("ILO-P013", format!("expected number, got {:?}", tok))),
None => Err(self.error("ILO-P014", "expected number, got EOF".into())),
}
}
}
fn body_to_expr(body: Vec<Spanned<Stmt>>) -> Expr {
if body.is_empty() {
return Expr::Literal(Literal::Nil);
}
match body.into_iter().last().unwrap().node {
Stmt::Expr(e) => e,
_ => Expr::Literal(Literal::Nil),
}
}
fn wrap_body_as_let(name: &str, mut body: Vec<Spanned<Stmt>>) -> Vec<Spanned<Stmt>> {
if body.is_empty() {
return vec![Spanned::unknown(Stmt::Let {
name: name.to_string(),
value: Expr::Literal(Literal::Nil),
})];
}
let last_idx = body.len() - 1;
let last = &mut body[last_idx];
let span = last.span;
match &last.node {
Stmt::Expr(expr) => {
body[last_idx] = Spanned::new(
Stmt::Let {
name: name.to_string(),
value: expr.clone(),
},
span,
);
}
_ => {
}
}
body
}
fn reserved_keyword_message(tok: &Token) -> Option<(String, String)> {
let (name, hint) = match tok {
Token::KwIf => ("if", "ilo uses `cond{body}` for conditional branches"),
Token::KwReturn => ("return", "ilo uses `ret expr` for early returns"),
Token::KwLet => ("let", "ilo uses `name=expr` for bindings"),
Token::KwFn => ("fn", "ilo defines functions as `name params>return;body`"),
Token::KwDef => ("def", "ilo defines functions as `name params>return;body`"),
Token::KwVar => ("var", "ilo uses `name=expr` for bindings"),
Token::KwConst => ("const", "ilo uses `name=expr` for bindings"),
_ => return None,
};
Some((
format!("`{name}` is a reserved word and cannot be used as an identifier"),
hint.to_string(),
))
}
fn is_guard_eligible_condition(expr: &Expr) -> bool {
matches!(
expr,
Expr::BinOp { op, .. } if matches!(
op,
BinOp::Equals | BinOp::NotEquals
| BinOp::GreaterThan | BinOp::LessThan
| BinOp::GreaterOrEqual | BinOp::LessOrEqual
| BinOp::And | BinOp::Or
)
)
}
pub fn parse(tokens: Vec<(Token, Span)>) -> (Program, Vec<ParseError>) {
let mut parser = Parser::new(tokens);
parser.parse_program()
}
#[cfg(test)]
pub fn parse_tokens(tokens: Vec<Token>) -> std::result::Result<Program, Vec<ParseError>> {
let pairs: Vec<(Token, Span)> = tokens.into_iter().map(|t| (t, Span::UNKNOWN)).collect();
let (prog, errors) = parse(pairs);
if errors.is_empty() {
Ok(prog)
} else {
Err(errors)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lexer;
fn parse_str(source: &str) -> Program {
let tokens = lexer::lex(source).unwrap();
let token_spans: Vec<(Token, Span)> = tokens
.into_iter()
.map(|(t, r)| {
(
t,
Span {
start: r.start,
end: r.end,
},
)
})
.collect();
let (prog, errors) = parse(token_spans);
assert!(errors.is_empty(), "parse errors: {:?}", errors);
prog
}
fn parse_str_errors(source: &str) -> (Program, Vec<ParseError>) {
let tokens = lexer::lex(source).unwrap();
let token_spans: Vec<(Token, Span)> = tokens
.into_iter()
.map(|(t, r)| {
(
t,
Span {
start: r.start,
end: r.end,
},
)
})
.collect();
parse(token_spans)
}
fn parse_file(path: &str) -> Program {
let source =
std::fs::read_to_string(path).unwrap_or_else(|e| panic!("cannot read {}: {}", path, e));
parse_str(&source)
}
#[test]
fn parse_simple_function() {
let prog = parse_str("tot p:n q:n r:n>n;s=*p q;t=*s r;+s t");
assert_eq!(prog.declarations.len(), 1);
let Decl::Function {
name, params, body, ..
} = &prog.declarations[0]
else {
panic!("expected function")
};
assert_eq!(name, "tot");
assert_eq!(params.len(), 3);
assert_eq!(body.len(), 3); }
#[test]
fn parse_let_binding() {
let prog = parse_str("f x:n>n;y=+x 1;y");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert_eq!(body.len(), 2);
let Stmt::Let { name, .. } = &body[0].node else {
panic!("expected let")
};
assert_eq!(name, "y");
}
#[test]
fn parse_type_def() {
let prog = parse_str("type point{x:n;y:n}");
let Decl::TypeDef { name, fields, .. } = &prog.declarations[0] else {
panic!("expected type def")
};
assert_eq!(name, "point");
assert_eq!(fields.len(), 2);
}
#[test]
fn parse_guard() {
let prog = parse_str(r#"cls sp:n>t;>=sp 1000{"gold"};"bronze""#);
let Decl::Function { name, body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert_eq!(name, "cls");
assert!(body.len() >= 2);
let Stmt::Guard { negated, .. } = &body[0].node else {
panic!("expected guard")
};
assert!(!negated);
}
#[test]
fn parse_match_stmt() {
let prog = parse_str(r#"f x:n>t;?{^e:^"error";~v:v;_:"default"}"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Match { subject, arms } = &body[0].node else {
panic!("expected match")
};
assert!(subject.is_none());
assert_eq!(arms.len(), 3);
}
#[test]
fn parse_prefix_ternary() {
let prog = parse_str("f x:n>n;?=x 0 10 20");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Expr(Expr::Ternary {
condition,
then_expr,
else_expr,
}) = &body[0].node
else {
panic!("expected ternary, got {:?}", body[0])
};
assert!(matches!(
condition.as_ref(),
Expr::BinOp {
op: BinOp::Equals,
..
}
));
assert!(matches!(then_expr.as_ref(), Expr::Literal(Literal::Number(n)) if *n == 10.0));
assert!(matches!(else_expr.as_ref(), Expr::Literal(Literal::Number(n)) if *n == 20.0));
}
#[test]
fn parse_prefix_ternary_gt() {
let prog = parse_str("f x:n>n;?>x 3 1 0");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Expr(Expr::Ternary { condition, .. }) = &body[0].node else {
panic!("expected ternary, got {:?}", body[0])
};
assert!(matches!(
condition.as_ref(),
Expr::BinOp {
op: BinOp::GreaterThan,
..
}
));
}
#[test]
fn parse_prefix_ternary_assignment() {
let prog = parse_str("f x:n>n;v=?=x 0 10 20;v");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Let { name, value, .. } = &body[0].node else {
panic!("expected let, got {:?}", body[0])
};
assert_eq!(name, "v");
assert!(matches!(value, Expr::Ternary { .. }));
}
#[test]
fn parse_ok_err_exprs() {
let prog = parse_str("f x:n>R n t;~x");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert!(
matches!(&body[0].node, Stmt::Expr(Expr::Ok(_))),
"expected Ok expr, got {:?}",
body[0]
);
}
#[test]
fn parse_foreach() {
let prog = parse_str("f xs:L n>n;s=0;@x xs{s=+s x};s");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert!(body.len() >= 3);
let Stmt::ForEach { binding, .. } = &body[1].node else {
panic!("expected foreach")
};
assert_eq!(binding, "x");
}
#[test]
fn parse_for_range() {
let prog = parse_str("f>n;@i 0..3{i}");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::ForRange {
binding,
start,
end,
..
} = &body[0].node
else {
panic!("expected ForRange")
};
assert_eq!(binding, "i");
assert_eq!(*start, Expr::Literal(Literal::Number(0.0)));
assert_eq!(*end, Expr::Literal(Literal::Number(3.0)));
}
#[test]
fn parse_for_range_with_expr_end() {
let prog = parse_str("f n:n>n;@i 0..n{i}");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::ForRange { binding, end, .. } = &body[0].node else {
panic!("expected ForRange")
};
assert_eq!(binding, "i");
assert_eq!(*end, Expr::Ref("n".to_string()));
}
#[test]
fn parse_multi_decl() {
let prog = parse_str("f x:n>n;*x 2 g x:n>n;+x 1");
assert_eq!(prog.declarations.len(), 2);
}
#[test]
fn parse_nested_prefix() {
let prog = parse_str("f a:n b:n c:n>n;+*a b c");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Expr(Expr::BinOp {
op: BinOp::Add,
left,
..
}) = &body[0].node
else {
panic!("expected binop")
};
assert!(matches!(
**left,
Expr::BinOp {
op: BinOp::Multiply,
..
}
));
}
#[test]
fn parse_list_literal() {
let prog = parse_str("f x:n>L n;[x, *x 2, *x 3]");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Expr(Expr::List(items)) = &body[0].node else {
panic!("expected list")
};
assert_eq!(items.len(), 3);
}
#[test]
fn parse_field_access() {
let prog = parse_str("f p:point>n;p.x");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Expr(Expr::Field { field, .. }) = &body[0].node else {
panic!("expected field access")
};
assert_eq!(field, "x");
}
#[test]
fn parse_index_access() {
let prog = parse_str("f xs:L n>n;xs.0");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Expr(Expr::Index { index, .. }) = &body[0].node else {
panic!("expected index access")
};
assert_eq!(*index, 0);
}
#[test]
fn parse_safe_field_access() {
let prog = parse_str("f p:point>n;p.?x");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Expr(Expr::Field { field, safe, .. }) = &body[0].node else {
panic!("expected safe field access")
};
assert_eq!(field, "x");
assert!(*safe);
}
#[test]
fn parse_negated_guard() {
let prog = parse_str(r#"f x:b>t;!x{"yes"};"no""#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Guard { negated, .. } = &body[0].node else {
panic!("expected guard")
};
assert!(negated);
}
#[test]
fn parse_record_construction() {
let prog = parse_str("type point{x:n;y:n} f a:n b:n>point;point x:a y:b");
let Decl::Function { body, .. } = &prog.declarations[1] else {
panic!("expected function")
};
let Stmt::Expr(Expr::Record { type_name, fields }) = &body[0].node else {
panic!("expected record")
};
assert_eq!(type_name, "point");
assert_eq!(fields.len(), 2);
}
#[test]
fn parse_with_expr() {
let prog = parse_str("type point{x:n;y:n} f p:point>point;p with x:1 y:2");
let Decl::Function { body, .. } = &prog.declarations[1] else {
panic!("expected function")
};
let Stmt::Expr(Expr::With { updates, .. }) = &body[0].node else {
panic!("expected with expr")
};
assert_eq!(updates.len(), 2);
}
#[test]
fn parse_tool_decl() {
let prog = parse_str(r#"tool fetch"http get" url:t>t timeout:30,retry:3"#);
let Decl::Tool {
name,
description,
timeout,
retry,
..
} = &prog.declarations[0]
else {
panic!("expected tool")
};
assert_eq!(name, "fetch");
assert_eq!(description, "http get");
assert_eq!(*timeout, Some(30.0));
assert_eq!(*retry, Some(3.0));
}
#[test]
fn parse_match_with_subject() {
let prog = parse_str("f x:R n t>n;?x{~v:v;^e:0}");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Match { subject, arms } = &body[0].node else {
panic!("expected match stmt")
};
assert!(subject.is_some());
assert_eq!(arms.len(), 2);
}
#[test]
fn parse_match_expr_in_let() {
let prog = parse_str(r#"f x:R n t>n;r=?x{~v:v;^e:0};r"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert_eq!(body.len(), 2);
assert!(
matches!(
&body[0].node,
Stmt::Let {
value: Expr::Match { .. },
..
}
),
"expected let with match expr, got {:?}",
body[0]
);
}
#[test]
fn parse_call_with_prefix_arg() {
let prog = parse_str("fac n:n>n;r=fac -n 1;*n r");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Let {
value: Expr::Call { function, args, .. },
..
} = &body[0].node
else {
panic!("expected call with prefix arg")
};
assert_eq!(function, "fac");
assert_eq!(args.len(), 1);
assert!(matches!(
&args[0],
Expr::BinOp {
op: BinOp::Subtract,
..
}
));
}
#[test]
fn infix_add() {
let prog = parse_str("f x:n>n;x + 1");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!()
};
let Stmt::Expr(Expr::BinOp { op: BinOp::Add, .. }) = &body[0].node else {
panic!("expected infix add")
};
}
#[test]
fn infix_subtract() {
let prog = parse_str("f x:n>n;x - 3");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!()
};
let Stmt::Expr(Expr::BinOp {
op: BinOp::Subtract,
..
}) = &body[0].node
else {
panic!("expected infix subtract")
};
}
#[test]
fn infix_multiply() {
let prog = parse_str("f x:n>n;x * 2");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!()
};
let Stmt::Expr(Expr::BinOp {
op: BinOp::Multiply,
..
}) = &body[0].node
else {
panic!("expected infix multiply")
};
}
#[test]
fn infix_divide() {
let prog = parse_str("f x:n>n;x / 2");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!()
};
let Stmt::Expr(Expr::BinOp {
op: BinOp::Divide, ..
}) = &body[0].node
else {
panic!("expected infix divide")
};
}
#[test]
fn infix_precedence_mul_over_add() {
let prog = parse_str("f x:n y:n>n;x + y * 2");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!()
};
let Stmt::Expr(Expr::BinOp {
op: BinOp::Add,
left,
right,
}) = &body[0].node
else {
panic!("expected add")
};
assert!(matches!(left.as_ref(), Expr::Ref(_)));
assert!(matches!(
right.as_ref(),
Expr::BinOp {
op: BinOp::Multiply,
..
}
));
}
#[test]
fn infix_parens_override_precedence() {
let prog = parse_str("f x:n y:n>n;(x + y) * 2");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!()
};
let Stmt::Expr(Expr::BinOp {
op: BinOp::Multiply,
left,
..
}) = &body[0].node
else {
panic!("expected multiply")
};
assert!(matches!(left.as_ref(), Expr::BinOp { op: BinOp::Add, .. }));
}
#[test]
fn infix_call_binds_tighter() {
let prog = parse_str("f x:n>n;x g x:n>n;f x + 1");
let Decl::Function { body, .. } = &prog.declarations[1] else {
panic!()
};
let Stmt::Expr(Expr::BinOp {
op: BinOp::Add,
left,
..
}) = &body[0].node
else {
panic!("expected infix add")
};
assert!(matches!(left.as_ref(), Expr::Call { .. }));
}
#[test]
fn infix_comparison() {
let prog = parse_str("f x:n y:n>b;x > y");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!()
};
let Stmt::Expr(Expr::BinOp {
op: BinOp::GreaterThan,
..
}) = &body[0].node
else {
panic!("expected gt")
};
}
#[test]
fn infix_and_or() {
let prog = parse_str("f a:b b:b>b;a & b");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!()
};
let Stmt::Expr(Expr::BinOp { op: BinOp::And, .. }) = &body[0].node else {
panic!("expected and")
};
}
#[test]
fn infix_left_associative() {
let prog = parse_str("f a:n b:n c:n>n;a - b - c");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!()
};
let Stmt::Expr(Expr::BinOp {
op: BinOp::Subtract,
left,
..
}) = &body[0].node
else {
panic!("expected sub")
};
assert!(matches!(
left.as_ref(),
Expr::BinOp {
op: BinOp::Subtract,
..
}
));
}
#[test]
fn prefix_still_works_alongside_infix() {
let prog = parse_str("f x:n>n;+x 1");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!()
};
let Stmt::Expr(Expr::BinOp { op: BinOp::Add, .. }) = &body[0].node else {
panic!("expected prefix add")
};
}
#[test]
fn prefix_call_arg_still_works() {
let prog = parse_str("fac n:n>n;r=fac -n 1;*n r");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!()
};
let Stmt::Let {
value: Expr::Call { function, args, .. },
..
} = &body[0].node
else {
panic!("expected call")
};
assert_eq!(function, "fac");
assert_eq!(args.len(), 1);
assert!(matches!(
&args[0],
Expr::BinOp {
op: BinOp::Subtract,
..
}
));
}
#[test]
fn parse_zero_arg_call() {
let prog = parse_str("f>n;g() g>n;42");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Expr(Expr::Call { function, args, .. }) = &body[0].node else {
panic!("expected zero-arg call")
};
assert_eq!(function, "g");
assert!(args.is_empty());
}
#[test]
fn parse_paren_expr() {
let prog = parse_str("f x:n>n;*(+x 1) 2");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Expr(Expr::BinOp {
op: BinOp::Multiply,
left,
..
}) = &body[0].node
else {
panic!("expected binop")
};
assert!(matches!(**left, Expr::BinOp { op: BinOp::Add, .. }));
}
#[test]
fn parse_list_append() {
let prog = parse_str("f xs:L n x:n>L n;+=xs x");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert!(
matches!(
&body[0].node,
Stmt::Expr(Expr::BinOp {
op: BinOp::Append,
..
})
),
"expected append, got {:?}",
body[0]
);
}
#[test]
fn parse_trailing_comma_in_list() {
let prog = parse_str("f>L n;[1, 2, 3,]");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Expr(Expr::List(items)) = &body[0].node else {
panic!("expected list")
};
assert_eq!(items.len(), 3);
}
#[test]
fn parse_empty_list() {
let prog = parse_str("f>L n;[]");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Expr(Expr::List(items)) = &body[0].node else {
panic!("expected list")
};
assert!(items.is_empty());
}
#[test]
fn parse_list_space_separated() {
let prog = parse_str("f>L n;[1 2 3]");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Expr(Expr::List(items)) = &body[0].node else {
panic!("expected list")
};
assert_eq!(items.len(), 3);
}
#[test]
fn parse_list_with_variables() {
let prog = parse_str(r#"f w:t>L t;["hi" w]"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Expr(Expr::List(items)) = &body[0].node else {
panic!("expected list")
};
assert_eq!(items.len(), 2);
}
#[test]
fn parse_list_mixed_types() {
let prog = parse_str(r#"f>L a;["search" 10 true]"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Expr(Expr::List(items)) = &body[0].node else {
panic!("expected list")
};
assert_eq!(items.len(), 3);
}
#[test]
fn parse_list_ok_err_elements() {
let prog = parse_str("f>L R n t;[~1 ~2 ~3]");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Expr(Expr::List(items)) = &body[0].node else {
panic!("expected list")
};
assert_eq!(items.len(), 3);
}
#[test]
fn parse_caret_stmt_in_match() {
let prog = parse_str(r#"f x:R n t>n;?x{^e:^"error";~v:v;_:0}"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Match { arms, .. } = &body[0].node else {
panic!("expected match")
};
assert!(
matches!(&arms[0].body[0].node, Stmt::Expr(Expr::Err(_))),
"expected Err expr in first arm"
);
}
#[test]
fn parse_chained_field_access() {
let prog = parse_str("type inner{v:n} type outer{i:inner} f o:outer>n;o.i.v");
let Decl::Function { body, .. } = &prog.declarations[2] else {
panic!("expected function")
};
let Stmt::Expr(Expr::Field { object, field, .. }) = &body[0].node else {
panic!("expected chained field")
};
assert_eq!(field, "v");
assert!(matches!(**object, Expr::Field { .. }));
}
#[test]
fn parse_multi_stmt_match_arm() {
let prog = parse_str("f x:R n t>n;?x{~v:y=+v 1;*y 2;^e:0}");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Match { arms, .. } = &body[0].node else {
panic!("expected match")
};
assert_eq!(arms[0].body.len(), 2); }
#[test]
fn parse_negated_guard_vs_not_expr() {
let prog = parse_str("f x:b>b;!x");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert!(
matches!(
&body[0].node,
Stmt::Expr(Expr::UnaryOp {
op: UnaryOp::Not,
..
})
),
"expected NOT expr, got {:?}",
body[0]
);
}
#[test]
fn parse_match_bool_literals() {
let prog = parse_str("f x:b>n;?x{true:1;false:0}");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Match { arms, .. } = &body[0].node else {
panic!("expected match")
};
assert!(matches!(
arms[0].pattern,
Pattern::Literal(Literal::Bool(true))
));
assert!(matches!(
arms[1].pattern,
Pattern::Literal(Literal::Bool(false))
));
}
#[test]
fn parse_match_number_with_wildcard() {
let prog = parse_str(r#"f x:n>t;?x{1:"one";2:"two";_:"other"}"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Match { arms, .. } = &body[0].node else {
panic!("expected match")
};
assert_eq!(arms.len(), 3);
assert!(matches!(arms[2].pattern, Pattern::Wildcard));
}
#[test]
fn parse_match_string_patterns() {
let prog = parse_str(r#"f x:t>n;?x{"a":1;"b":2;_:0}"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Match { arms, .. } = &body[0].node else {
panic!("expected match")
};
assert_eq!(arms.len(), 3);
assert!(matches!(&arms[0].pattern, Pattern::Literal(Literal::Text(s)) if s == "a"));
}
#[test]
fn parse_all_comparison_ops() {
let tests = vec![
(">=a b", BinOp::GreaterOrEqual),
("<=a b", BinOp::LessOrEqual),
("!=a b", BinOp::NotEquals),
("=a b", BinOp::Equals),
(">a b", BinOp::GreaterThan),
("<a b", BinOp::LessThan),
("&a b", BinOp::And),
("|a b", BinOp::Or),
];
for (expr_str, expected_op) in tests {
let code = format!("f a:b b:b>b;{}", expr_str);
let prog = parse_str(&code);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Expr(Expr::BinOp { op, .. }) = &body[0].node else {
panic!("expected binop for {}", expr_str)
};
assert_eq!(*op, expected_op, "failed for expr: {}", expr_str);
}
}
#[test]
fn parse_error_has_span() {
let source = "f x:n>n;+";
let tokens = lexer::lex(source).unwrap();
let token_spans: Vec<(Token, Span)> = tokens
.into_iter()
.map(|(t, r)| {
(
t,
Span {
start: r.start,
end: r.end,
},
)
})
.collect();
let (_prog, errors) = parse(token_spans);
let err = errors.into_iter().next().expect("expected parse error");
assert!(!err.message.is_empty());
assert!(err.position > 0, "error position should be > 0");
}
#[test]
fn fn_decl_span_covers_full_declaration() {
let prog = parse_str("f x:n>n;*x 2");
let Decl::Function { span, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert_eq!(span.start, 0);
assert!(span.end > 0, "function span end should be > 0");
}
#[test]
fn type_decl_span_covers_full_declaration() {
let prog = parse_str("type point{x:n;y:n}");
let Decl::TypeDef { span, .. } = &prog.declarations[0] else {
panic!("expected type def")
};
assert_eq!(span.start, 0);
assert!(
span.end >= 18,
"type span end should cover closing brace, got {}",
span.end
);
}
#[test]
fn multi_decl_spans_are_distinct() {
let prog = parse_str("f x:n>n;*x 2 g y:n>n;+y 1");
assert_eq!(prog.declarations.len(), 2);
let Decl::Function { span: span_f, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let span_f = *span_f;
let Decl::Function { span: span_g, .. } = &prog.declarations[1] else {
panic!("expected function")
};
let span_g = *span_g;
assert_eq!(span_f.start, 0);
assert!(span_g.start > span_f.start, "g should start after f");
assert!(
span_g.start >= span_f.end,
"g span should not overlap f span"
);
}
#[test]
fn tool_decl_has_span() {
let prog = parse_str(r#"tool fetch"http get" url:t>t"#);
let Decl::Tool { span, .. } = &prog.declarations[0] else {
panic!("expected tool")
};
assert_eq!(span.start, 0);
assert!(span.end > 0);
}
#[test]
fn parse_example_01_simple_function() {
let prog = parse_file("examples/01-simple-function.ilo");
assert_eq!(prog.declarations.len(), 1);
let Decl::Function {
name,
params,
return_type,
body,
..
} = &prog.declarations[0]
else {
panic!("expected function")
};
assert_eq!(name, "tot");
assert_eq!(params.len(), 3);
assert_eq!(*return_type, Type::Number);
assert_eq!(body.len(), 3);
}
#[test]
fn parse_example_02_with_dependencies() {
let prog = parse_file("examples/02-with-dependencies.ilo");
assert_eq!(prog.declarations.len(), 1);
let Decl::Function {
name, return_type, ..
} = &prog.declarations[0]
else {
panic!("expected function")
};
assert_eq!(name, "prc");
assert!(matches!(return_type, Type::Result(_, _)));
}
#[test]
fn parse_error_messages() {
let bad = "42 x:n>n;x";
let tokens = lexer::lex(bad).unwrap();
let token_spans: Vec<(Token, Span)> = tokens
.into_iter()
.map(|(t, r)| {
(
t,
Span {
start: r.start,
end: r.end,
},
)
})
.collect();
let (_prog, errors) = parse(token_spans);
let err = errors.into_iter().next().expect("expected parse error");
assert!(
err.message.contains("expected declaration"),
"got: {}",
err.message
);
}
#[test]
fn parse_complex_match_patterns() {
let prog = parse_str(r#"f x:R n t>n;?x{^e:0;~v:?v{1:100;2:200;_:v}}"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert_eq!(body.len(), 1);
let Stmt::Match { arms, .. } = &body[0].node else {
panic!("expected match")
};
assert_eq!(arms.len(), 2);
assert!(matches!(&arms[1].body[0].node, Stmt::Match { .. }));
}
#[test]
fn parse_deeply_nested_prefix() {
let prog = parse_str("f x:n>n;+*+x 1 2 3");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Expr(Expr::BinOp {
op: BinOp::Add,
left,
..
}) = &body[0].node
else {
panic!("expected add")
};
let Expr::BinOp {
op: BinOp::Multiply,
left: inner,
..
} = &**left
else {
panic!("expected nested multiply")
};
assert!(matches!(&**inner, Expr::BinOp { op: BinOp::Add, .. }));
}
#[test]
fn parse_tokens_legacy_api() {
let source = "f x:n>n;*x 2";
let tokens: Vec<Token> = lexer::lex(source)
.unwrap()
.into_iter()
.map(|(t, _)| t)
.collect();
let prog = parse_tokens(tokens).unwrap();
assert_eq!(prog.declarations.len(), 1);
}
#[test]
fn recovery_second_function_parsed_after_first_error() {
let (prog, errors) = parse_str_errors("f x:n n;bad g y:n>n;y");
assert!(!errors.is_empty(), "expected parse error from f");
let valid: Vec<_> = prog
.declarations
.iter()
.filter(|d| !matches!(d, Decl::Error { .. }))
.collect();
assert_eq!(valid.len(), 1, "g should parse successfully");
let Decl::Function { name, .. } = valid[0] else {
panic!("expected function g")
};
assert_eq!(name, "g");
}
#[test]
fn recovery_error_node_in_declarations() {
let (prog, errors) = parse_str_errors("f x:n n;bad g y:n>n;y");
assert!(!errors.is_empty());
assert_eq!(prog.declarations.len(), 2);
assert!(matches!(prog.declarations[0], Decl::Error { .. }));
assert!(matches!(prog.declarations[1], Decl::Function { .. }));
}
#[test]
fn recovery_two_errors_both_reported() {
let (prog, errors) = parse_str_errors("f x:n n;bad g y:n n;bad");
assert_eq!(errors.len(), 2, "expected two errors");
assert_eq!(prog.declarations.len(), 2);
assert!(
prog.declarations
.iter()
.all(|d| matches!(d, Decl::Error { .. }))
);
}
#[test]
fn recovery_error_node_not_in_json() {
let (prog, _errors) = parse_str_errors("f x:n n;bad g y:n>n;y");
let json = serde_json::to_string(&prog).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let decls = parsed["declarations"].as_array().unwrap();
assert_eq!(
decls.len(),
1,
"only valid declarations should appear in JSON"
);
}
#[test]
fn recovery_stops_at_20_errors() {
let bad: String = (0..25).map(|i| format!("f{i} x:n n;bad ")).collect();
let good = "g y:n>n;y";
let source = format!("{bad}{good}");
let (_prog, errors) = parse_str_errors(&source);
assert!(
errors.len() <= 20,
"should cap at 20 errors, got {}",
errors.len()
);
}
#[test]
fn recovery_type_decl_after_error() {
let (prog, errors) = parse_str_errors("f x:n n;bad type point{x:n;y:n}");
assert!(!errors.is_empty());
let valid: Vec<_> = prog
.declarations
.iter()
.filter(|d| !matches!(d, Decl::Error { .. }))
.collect();
assert_eq!(valid.len(), 1);
assert!(matches!(valid[0], Decl::TypeDef { .. }));
}
#[test]
fn eof_while_expecting_type() {
let (_, errors) = parse_str_errors("f x:");
assert!(!errors.is_empty(), "expected parse error");
assert!(
errors
.iter()
.any(|e| e.message.contains("EOF") || e.message.contains("expected")),
"unexpected error messages: {:?}",
errors
);
}
#[test]
fn eof_while_expecting_identifier() {
let (_, errors) = parse_str_errors("f");
assert!(!errors.is_empty(), "expected parse error");
assert!(
errors
.iter()
.any(|e| e.message.contains("EOF") || e.message.contains("expected")),
"unexpected error messages: {:?}",
errors
);
}
#[test]
fn eof_while_expecting_expression() {
let (_, errors) = parse_str_errors("f x:n>n;+x");
assert!(
!errors.is_empty(),
"expected parse error for EOF expression"
);
}
#[test]
fn eof_expecting_gt_in_signature() {
let (_, errors) = parse_str_errors("f x:n");
assert!(!errors.is_empty(), "expected parse error");
}
#[test]
fn tool_missing_description() {
let (_, errors) = parse_str_errors("tool my-tool x:n>n");
assert!(
!errors.is_empty(),
"expected parse error for missing description"
);
assert!(
errors.iter().any(|e| e.code == "ILO-P015"),
"expected ILO-P015 error, got: {:?}",
errors
);
}
#[test]
fn unexpected_token_as_expression() {
let (_, errors) = parse_str_errors("f x:n>n;>x 0{}};x");
assert!(!errors.is_empty(), "expected parse error");
}
#[test]
fn unexpected_token_as_pattern() {
let (_, errors) = parse_str_errors("f x:n>n;?x{+:1;_:0}");
assert!(!errors.is_empty(), "expected parse error for bad pattern");
}
#[test]
fn eof_while_expecting_declaration() {
let (prog, errors) = parse_str_errors("");
let _ = (prog, errors);
}
#[test]
fn expect_ident_got_non_ident() {
let (_, errors) = parse_str_errors("type 123{x:n}");
assert!(!errors.is_empty(), "expected parse error");
assert!(
errors
.iter()
.any(|e| e.code == "ILO-P005" || e.message.contains("expected identifier")),
"unexpected errors: {:?}",
errors
);
}
#[test]
fn expect_ident_got_eof() {
let (_, errors) = parse_str_errors("type");
assert!(!errors.is_empty(), "expected parse error");
assert!(
errors
.iter()
.any(|e| e.code == "ILO-P006" || e.message.contains("EOF")),
"unexpected errors: {:?}",
errors
);
}
#[test]
fn parse_ok_expr_as_operand() {
let prog = parse_str("f x:n>R n t;g ~x");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Expr(Expr::Call { function, args, .. }) = &body[0].node else {
panic!("expected call")
};
assert_eq!(function, "g");
assert!(matches!(&args[0], Expr::Ok(_)));
}
#[test]
fn parse_err_expr_as_operand() {
let prog = parse_str("f x:n>R n t;g ^x");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Expr(Expr::Call { function, args, .. }) = &body[0].node else {
panic!("expected call")
};
assert_eq!(function, "g");
assert!(matches!(&args[0], Expr::Err(_)));
}
#[test]
fn declaration_starts_with_prefix_op_gets_hint() {
let (_, errors) = parse_str_errors("+x 1");
assert!(!errors.is_empty(), "expected parse error");
}
#[test]
fn nested_brace_body_recovery() {
let (prog, errors) = parse_str_errors("f x:n>n;>x 0{{inner}};x g y:n>n;y");
assert!(!errors.is_empty(), "should have errors from nested braces");
let valid: Vec<_> = prog
.declarations
.iter()
.filter(|d| matches!(d, Decl::Function { name, .. } if name == "g"))
.collect();
assert!(
!valid.is_empty() || !prog.declarations.is_empty(),
"should recover at least something"
);
}
#[test]
fn parse_ident_guard_expr_or_guard() {
let prog = parse_str("f x:b>n;x{42}");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert!(
matches!(&body[0].node, Stmt::Guard { negated: false, .. }),
"expected non-negated guard, got {:?}",
body[0]
);
}
#[test]
fn parse_eof_in_pattern() {
let tokens: Vec<(Token, Span)> = vec![
(Token::Ident("f".to_string()), Span::UNKNOWN),
(Token::Greater, Span::UNKNOWN),
(Token::Ident("n".to_string()), Span::UNKNOWN),
(Token::Semi, Span::UNKNOWN),
(Token::Question, Span::UNKNOWN),
(Token::Number(1.0), Span::UNKNOWN),
(Token::LBrace, Span::UNKNOWN),
];
let (_, errors) = parse(tokens);
assert!(
!errors.is_empty(),
"expected parse error for EOF in pattern"
);
let found = errors
.iter()
.any(|e| e.code == "ILO-P012" || e.message.contains("EOF"));
assert!(found, "expected ILO-P012 error, got: {:?}", errors);
}
#[test]
fn parse_body_trailing_semicolon() {
let prog = parse_str("f>n;42;");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert_eq!(body.len(), 1);
}
#[test]
fn parse_match_arms_trailing_semi() {
let prog = parse_str("f>n;?{1:;}");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Match { arms, .. } = &body[0].node else {
panic!("expected match")
};
assert_eq!(arms.len(), 1);
assert_eq!(arms[0].body.len(), 0); }
#[test]
fn parse_arm_body_trailing_semi() {
let prog = parse_str("f>n;?0{_:1;}");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Match { arms, .. } = &body[0].node else {
panic!("expected match")
};
assert_eq!(arms.len(), 1);
assert_eq!(arms[0].body.len(), 1);
}
#[test]
fn parse_incomplete_match_arm_eof_after_semi() {
let (_, errors) = parse_str_errors("f x:n>n;?x{1:42;");
assert!(
!errors.is_empty(),
"expected parse error for unclosed match"
);
}
#[test]
fn parse_with_ident_no_colon() {
let (_, errors) = parse_str_errors("f x:n>n;x with a");
let _ = errors;
}
#[test]
fn parse_tool_timeout_non_numeric() {
let (_, errors) = parse_str_errors(r#"tool f "desc" x:n>n timeout:foo"#);
assert!(
!errors.is_empty(),
"expected parse error for non-numeric timeout"
);
let found = errors
.iter()
.any(|e| e.code == "ILO-P013" || e.message.contains("expected number"));
assert!(found, "expected ILO-P013, got: {:?}", errors);
}
#[test]
fn parse_tool_timeout_eof() {
let (_, errors) = parse_str_errors(r#"tool f "desc" x:n>n timeout:"#);
assert!(!errors.is_empty(), "expected parse error for EOF timeout");
let found = errors
.iter()
.any(|e| e.code == "ILO-P014" || e.message.contains("EOF"));
assert!(found, "expected ILO-P014, got: {:?}", errors);
}
#[test]
fn parse_semi_starts_new_arm_caret_eof() {
let (_, errors) = parse_str_errors("f x:n>n;?x{1:2;^v");
let _ = errors; }
#[test]
fn parse_semi_starts_new_arm_tilde_eof() {
let (_, errors) = parse_str_errors("f x:n>n;?x{1:2;~v");
let _ = errors;
}
#[test]
fn parse_decl_eof() {
let (prog, _) = parse_str_errors("f>n;42;");
let _ = prog;
}
#[test]
fn parse_prev_span_at_zero() {
let (_, errors) = parse_str_errors("");
let _ = errors;
}
#[test]
fn parse_decl_with_empty_tokens() {
let mut parser = Parser::new(vec![]);
let result = parser.parse_decl();
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, "ILO-P002");
}
#[test]
fn prev_span_at_position_zero() {
let parser = Parser::new(vec![(Token::Ident("x".into()), Span { start: 1, end: 2 })]);
assert_eq!(parser.prev_span(), Span::UNKNOWN);
}
#[test]
fn semi_starts_new_arm_non_semi_token() {
let parser = Parser::new(vec![(Token::Ident("x".into()), Span::UNKNOWN)]);
assert!(!parser.semi_starts_new_arm());
}
#[test]
fn hint_p001_function_keyword() {
let (_, errors) = parse_str_errors("function foo() {}");
assert!(!errors.is_empty());
let e = errors.iter().find(|e| e.code == "ILO-P001").unwrap();
let hint = e.hint.as_ref().unwrap();
assert!(hint.contains("ilo function syntax"));
}
#[test]
fn hint_p001_let_keyword() {
let (_, errors) = parse_str_errors("let x = 5");
assert!(!errors.is_empty());
let e = errors.iter().find(|e| e.code == "ILO-P001").unwrap();
let hint = e.hint.as_ref().unwrap();
assert!(hint.contains("assignment syntax"));
}
#[test]
fn hint_p001_return_keyword() {
let (_, errors) = parse_str_errors("return x");
assert!(!errors.is_empty());
let e = errors.iter().find(|e| e.code == "ILO-P001").unwrap();
let hint = e.hint.as_ref().unwrap();
assert!(hint.contains("return value"));
}
#[test]
fn hint_p001_if_keyword() {
let (_, errors) = parse_str_errors("if x > 0 { true }");
assert!(!errors.is_empty());
let e = errors.iter().find(|e| e.code == "ILO-P001").unwrap();
let hint = e.hint.as_ref().unwrap();
assert!(hint.contains("match"));
}
#[test]
fn hint_p001_operator_at_decl_level() {
let tokens = vec![
(Token::Plus, Span::UNKNOWN),
(Token::Ident("x".into()), Span::UNKNOWN),
];
let (_, errors) = parse(tokens);
assert!(!errors.is_empty());
let e = errors.iter().find(|e| e.code == "ILO-P001").unwrap();
let hint = e.hint.as_ref().unwrap();
assert!(hint.contains("prefix operators"));
}
#[test]
fn hint_p003_arrow_instead_of_greater() {
let (_, errors) = parse_str_errors("f x:n->n;x");
assert!(!errors.is_empty());
let e = errors.iter().find(|e| e.code == "ILO-P003").unwrap();
let hint = e.hint.as_ref().unwrap();
assert!(hint.contains("->"));
assert!(hint.contains(">"));
}
#[test]
fn hint_p003_double_amp() {
let (_, errors) = parse_str_errors("f x:b y:b>b;&&x y");
let e = errors.iter().find(|e| e.code == "ILO-P003").unwrap();
let hint = e.hint.as_ref().unwrap();
assert!(hint.contains("'&'"));
assert!(hint.contains("'|'"));
}
#[test]
fn hint_p003_double_pipe() {
let (_, errors) = parse_str_errors("f x:b y:b>b;||x y");
let e = errors.iter().find(|e| e.code == "ILO-P003").unwrap();
let hint = e.hint.as_ref().unwrap();
assert!(hint.contains("'|'"));
}
#[test]
fn no_hint_p001_unrecognized_token() {
let tokens = vec![(Token::Number(42.0), Span::UNKNOWN)];
let (_, errors) = parse(tokens);
assert!(!errors.is_empty());
let e = errors.iter().find(|e| e.code == "ILO-P001").unwrap();
assert!(e.hint.is_none());
}
#[test]
fn parse_unwrap_call() {
let prog = parse_str("f x:n>R n t;d=g! x;~d");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Let {
value:
Expr::Call {
function,
args,
unwrap,
},
..
} = &body[0].node
else {
panic!("expected unwrap call")
};
assert_eq!(function, "g");
assert!(unwrap);
assert_eq!(args.len(), 1);
assert!(matches!(&args[0], Expr::Ref(n) if n == "x"));
}
#[test]
fn parse_unwrap_zero_arg() {
let prog = parse_str("f>R t t;d=g!();~d");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Let {
value:
Expr::Call {
function,
args,
unwrap,
},
..
} = &body[0].node
else {
panic!("expected unwrap zero-arg call")
};
assert_eq!(function, "g");
assert!(unwrap);
assert!(args.is_empty());
}
#[test]
fn parse_bang_not_is_not_unwrap() {
let prog = parse_str("f x:b>b;g !x");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Expr(Expr::Call {
function,
args,
unwrap,
..
}) = &body[0].node
else {
panic!("expected call with NOT arg")
};
assert_eq!(function, "g");
assert!(!unwrap);
assert_eq!(args.len(), 1);
assert!(matches!(
&args[0],
Expr::UnaryOp {
op: UnaryOp::Not,
..
}
));
}
#[test]
fn parse_unwrap_multi_arg() {
let prog = parse_str("f a:n b:n>R n t;d=g! a b;~d");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Let {
value:
Expr::Call {
function,
args,
unwrap,
},
..
} = &body[0].node
else {
panic!("expected unwrap multi-arg call")
};
assert_eq!(function, "g");
assert!(unwrap);
assert_eq!(args.len(), 2);
}
#[test]
fn parse_unwrap_as_last_expr() {
let prog = parse_str("f x:n>R n t;g! x");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Expr(Expr::Call {
function, unwrap, ..
}) = &body[0].node
else {
panic!("expected unwrap call expr")
};
assert_eq!(function, "g");
assert!(unwrap);
}
#[test]
fn braceless_guard_comparison_literal() {
let prog = parse_str(r#"cls sp:n>t;>=sp 1000 "gold";"bronze""#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert_eq!(
body.len(),
2,
"expected 2 stmts (guard + expr), got {:?}",
body
);
let Stmt::Guard {
condition,
negated,
body: guard_body,
..
} = &body[0].node
else {
panic!("expected guard")
};
assert!(!negated);
assert!(matches!(
condition,
Expr::BinOp {
op: BinOp::GreaterOrEqual,
..
}
));
assert_eq!(guard_body.len(), 1);
let Stmt::Expr(Expr::Literal(Literal::Text(s))) = &guard_body[0].node else {
panic!("expected text literal body")
};
assert_eq!(s, "gold");
}
#[test]
fn braceless_guard_variable_body() {
let prog = parse_str("fib n:n>n;<=n 1 n;+n 1");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert_eq!(body.len(), 2);
let Stmt::Guard {
condition,
negated,
body: guard_body,
..
} = &body[0].node
else {
panic!("expected guard")
};
assert!(!negated);
assert!(matches!(
condition,
Expr::BinOp {
op: BinOp::LessOrEqual,
..
}
));
assert_eq!(guard_body.len(), 1);
assert!(matches!(&guard_body[0].node, Stmt::Expr(Expr::Ref(n)) if n == "n"));
}
#[test]
fn braceless_guard_ok_body() {
let prog = parse_str("f x:n>R n t;>=x 0 ~x;^\"negative\"");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Guard {
body: guard_body, ..
} = &body[0].node
else {
panic!("expected guard")
};
assert_eq!(guard_body.len(), 1);
assert!(matches!(&guard_body[0].node, Stmt::Expr(Expr::Ok(_))));
}
#[test]
fn braceless_guard_err_body() {
let prog = parse_str(r#"f x:n>R n t;<x 0 ^"negative";~x"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Guard {
body: guard_body, ..
} = &body[0].node
else {
panic!("expected guard")
};
assert_eq!(guard_body.len(), 1);
assert!(matches!(&guard_body[0].node, Stmt::Expr(Expr::Err(_))));
}
#[test]
fn braceless_guard_operator_body() {
let prog = parse_str("f x:n>n;>=x 10 +x 1;*x 2");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert_eq!(body.len(), 2);
let Stmt::Guard {
body: guard_body, ..
} = &body[0].node
else {
panic!("expected guard")
};
assert_eq!(guard_body.len(), 1);
assert!(matches!(
&guard_body[0].node,
Stmt::Expr(Expr::BinOp { op: BinOp::Add, .. })
));
}
#[test]
fn braceless_guard_multi_guard_program() {
let prog = parse_str(r#"cls sp:n>t;>=sp 1000 "gold";>=sp 500 "silver";"bronze""#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert_eq!(body.len(), 3, "expected 3 stmts, got {:?}", body);
assert!(matches!(&body[0].node, Stmt::Guard { .. }));
assert!(matches!(&body[1].node, Stmt::Guard { .. }));
assert!(matches!(
&body[2].node,
Stmt::Expr(Expr::Literal(Literal::Text(_)))
));
}
#[test]
fn braceless_guard_negated() {
let prog = parse_str(r#"f x:n>t;!>=x 10 "small";"big""#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert_eq!(body.len(), 2);
let Stmt::Guard {
condition,
negated,
body: guard_body,
..
} = &body[0].node
else {
panic!("expected negated guard")
};
assert!(negated);
assert!(matches!(
condition,
Expr::BinOp {
op: BinOp::GreaterOrEqual,
..
}
));
assert_eq!(guard_body.len(), 1);
let Stmt::Expr(Expr::Literal(Literal::Text(s))) = &guard_body[0].node else {
panic!("expected text body")
};
assert_eq!(s, "small");
}
#[test]
fn braceless_guard_non_comparison_not_triggered() {
let prog = parse_str(r#"f x:n y:n>t;+x y;"result""#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert!(
matches!(
&body[0].node,
Stmt::Expr(Expr::BinOp { op: BinOp::Add, .. })
),
"non-comparison should not trigger braceless guard, got {:?}",
body[0]
);
}
#[test]
fn braceless_guard_braced_still_works() {
let prog = parse_str(r#"cls sp:n>t;>=sp 1000{"gold"};"bronze""#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert_eq!(body.len(), 2);
let Stmt::Guard { negated, .. } = &body[0].node else {
panic!("expected guard")
};
assert!(!negated);
}
#[test]
fn braceless_guard_equality() {
let prog = parse_str(r#"f x:t>R t t;=x "admin" ~x;^"denied""#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Guard { condition, .. } = &body[0].node else {
panic!("expected guard")
};
assert!(matches!(
condition,
Expr::BinOp {
op: BinOp::Equals,
..
}
));
}
#[test]
fn braceless_guard_logical_and() {
let prog = parse_str(r#"f a:b b:b>t;&a b "both";"nope""#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Guard { condition, .. } = &body[0].node else {
panic!("expected guard")
};
assert!(matches!(condition, Expr::BinOp { op: BinOp::And, .. }));
}
#[test]
fn braceless_guard_at_end_no_body() {
let prog = parse_str("f x:n>b;>=x 10");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert_eq!(body.len(), 1);
assert!(matches!(
&body[0].node,
Stmt::Expr(Expr::BinOp {
op: BinOp::GreaterOrEqual,
..
})
));
}
#[test]
fn braceless_guard_factorial() {
let prog = parse_str("fac n:n>n;<=n 1 1;r=fac -n 1;*n r");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert_eq!(
body.len(),
3,
"expected 3 stmts (guard + let + expr), got {:?}",
body
);
let Stmt::Guard {
condition,
body: guard_body,
..
} = &body[0].node
else {
panic!("expected guard")
};
assert!(matches!(
condition,
Expr::BinOp {
op: BinOp::LessOrEqual,
..
}
));
assert_eq!(guard_body.len(), 1);
assert!(
matches!(&guard_body[0].node, Stmt::Expr(Expr::Literal(Literal::Number(n))) if *n == 1.0)
);
}
#[test]
fn braceless_guard_dangling_token_error() {
let (_, errors) = parse_str_errors("cls sp:n>t;>=sp 1000 classify sp");
assert!(
errors.iter().any(|e| e.code == "ILO-P016"),
"expected ILO-P016 error, 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_valid_semicolon_terminates() {
let prog = parse_str("cls sp:n>t;>=sp 1000 classify;\"fallback\"");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert!(matches!(&body[0].node, Stmt::Guard { .. }));
}
#[test]
fn parse_dollar_desugars_to_get() {
let prog = parse_str(r#"f url:t>R t t;$url"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Expr(Expr::Call {
function,
args,
unwrap,
}) = &body[0].node
else {
panic!("expected get call")
};
assert_eq!(function, "get");
assert_eq!(args.len(), 1);
assert!(!unwrap);
}
#[test]
fn parse_dollar_bang_desugars_to_get_unwrap() {
let prog = parse_str(r#"f url:t>t;$!url"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Expr(Expr::Call {
function,
args,
unwrap,
}) = &body[0].node
else {
panic!("expected get! call")
};
assert_eq!(function, "get");
assert_eq!(args.len(), 1);
assert!(unwrap);
}
#[test]
fn parse_dollar_with_string_literal() {
let prog = parse_str(r#"f>R t t;$"http://example.com""#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Expr(Expr::Call { function, args, .. }) = &body[0].node else {
panic!("expected get call")
};
assert_eq!(function, "get");
assert!(matches!(&args[0], Expr::Literal(Literal::Text(_))));
}
#[test]
fn parse_ternary_guard_else() {
let source = r#"f x:n>t;=x 1{"yes"}{"no"}"#;
let (program, errors) = parse_str_errors(source);
assert!(errors.is_empty(), "parse errors: {:?}", errors);
let Decl::Function { body, .. } = &program.declarations[0] else {
panic!("expected function")
};
assert_eq!(body.len(), 1, "expected 1 stmt (ternary), got {:?}", body);
let Stmt::Guard { else_body, .. } = &body[0].node else {
panic!("expected guard with else")
};
assert!(else_body.is_some(), "expected else_body in ternary");
let eb = else_body.as_ref().unwrap();
assert_eq!(eb.len(), 1);
}
#[test]
fn parse_while_loop() {
let prog = parse_str("f>n;wh true{42}");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::While { condition, body } = &body[0].node else {
panic!("expected While")
};
assert!(matches!(condition, Expr::Literal(Literal::Bool(true))));
assert_eq!(body.len(), 1);
}
#[test]
fn parse_ret_statement() {
let prog = parse_str("f x:n>n;ret +x 1");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert_eq!(body.len(), 1);
assert!(
matches!(
&body[0].node,
Stmt::Return(Expr::BinOp { op: BinOp::Add, .. })
),
"expected Return(BinOp::Add), got {:?}",
body[0]
);
}
#[test]
fn parse_pipe_simple() {
let prog = parse_str("add a:n b:n>n;+a b\nf x:n>n;add x 1>>add 2");
let Decl::Function { body, .. } = &prog.declarations[1] else {
panic!("expected function")
};
let Stmt::Expr(Expr::Call { function, args, .. }) = &body[0].node else {
panic!("expected Call")
};
assert_eq!(function, "add");
assert_eq!(args.len(), 2); }
#[test]
fn parse_pipe_chain() {
let prog = parse_str("f x:n>n;str x>>len");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Expr(Expr::Call { function, args, .. }) = &body[0].node else {
panic!("expected Call")
};
assert_eq!(function, "len");
assert_eq!(args.len(), 1);
let Expr::Call { function, .. } = &args[0] else {
panic!("expected Call(str)")
};
assert_eq!(function, "str");
}
#[test]
fn parse_ret_in_guard() {
let prog = parse_str(r#"f x:n>t;>x 0{ret "pos"};"neg""#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert_eq!(body.len(), 2);
let Stmt::Guard {
body: guard_body, ..
} = &body[0].node
else {
panic!("expected guard")
};
let Stmt::Return(Expr::Literal(Literal::Text(s))) = &guard_body[0].node else {
panic!("expected Return")
};
assert_eq!(s, "pos");
}
#[test]
fn parse_brk_no_value() {
let prog = parse_str("f>n;wh true{brk}");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::While { body, .. } = &body[0].node else {
panic!("expected While")
};
assert!(matches!(&body[0].node, Stmt::Break(None)));
}
#[test]
fn parse_brk_with_value() {
let prog = parse_str("f>n;wh true{brk 42}");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::While { body, .. } = &body[0].node else {
panic!("expected While")
};
assert!(
matches!(&body[0].node, Stmt::Break(Some(Expr::Literal(Literal::Number(n)))) if *n == 42.0)
);
}
#[test]
fn parse_cnt() {
let prog = parse_str("f>n;wh true{cnt}");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::While { body, .. } = &body[0].node else {
panic!("expected While")
};
assert!(matches!(&body[0].node, Stmt::Continue));
}
#[test]
fn parse_dollar_in_operand() {
let prog = parse_str(r#"f url:t>R t t;cat [$url] ",""#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Expr(Expr::Call { function, .. }) = &body[0].node else {
panic!("expected Call")
};
assert_eq!(function, "cat");
}
#[test]
fn parse_destructure_two_fields() {
let prog = parse_str("type pt{x:n;y:n} f p:pt>n;{x;y}=p;+x y");
let Decl::Function { body: func, .. } = &prog.declarations[1] else {
panic!("expected function")
};
let Stmt::Destructure { bindings, value } = &func[0].node else {
panic!("expected Destructure")
};
assert_eq!(bindings, &["x", "y"]);
assert!(matches!(value, Expr::Ref(name) if name == "p"));
}
#[test]
fn parse_destructure_single_field() {
let prog = parse_str("type pt{x:n} f p:pt>n;{x}=p;x");
let Decl::Function { body: func, .. } = &prog.declarations[1] else {
panic!("expected function")
};
let Stmt::Destructure { bindings, .. } = &func[0].node else {
panic!("expected Destructure")
};
assert_eq!(bindings, &["x"]);
}
#[test]
fn parse_destructure_three_fields() {
let prog = parse_str("type pt{a:n;b:t;c:b} f p:pt>n;{a;b;c}=p;a");
let Decl::Function { body: func, .. } = &prog.declarations[1] else {
panic!("expected function")
};
let Stmt::Destructure { bindings, .. } = &func[0].node else {
panic!("expected Destructure")
};
assert_eq!(bindings, &["a", "b", "c"]);
}
#[test]
fn greedy_arg_stops_at_zero_param_decl() {
let prog = parse_str("f xs:n>n;len xs g>n;2");
assert_eq!(
prog.declarations.len(),
2,
"expected exactly 2 declarations"
);
let Decl::Function { name, body, .. } = &prog.declarations[0] else {
panic!("expected function f")
};
assert_eq!(name, "f");
let Stmt::Expr(Expr::Call { function, args, .. }) = &body[0].node else {
panic!("expected Call(len, [xs])")
};
assert_eq!(function, "len");
assert_eq!(
args.len(),
1,
"len should have exactly 1 arg, not consume `g`"
);
assert!(matches!(&args[0], Expr::Ref(n) if n == "xs"));
let Decl::Function { name, .. } = &prog.declarations[1] else {
panic!("expected function g")
};
assert_eq!(name, "g");
}
#[test]
fn greedy_arg_stops_at_parameterised_decl() {
let prog = parse_str("f xs:n>n;len xs g y:n>n;*y 2");
assert_eq!(
prog.declarations.len(),
2,
"expected exactly 2 declarations"
);
let Decl::Function { name, body, .. } = &prog.declarations[0] else {
panic!("expected function f")
};
assert_eq!(name, "f");
let Stmt::Expr(Expr::Call { function, args, .. }) = &body[0].node else {
panic!("expected Call(len, [xs])")
};
assert_eq!(function, "len");
assert_eq!(
args.len(),
1,
"len should have exactly 1 arg, not consume `g`"
);
let Decl::Function { name, params, .. } = &prog.declarations[1] else {
panic!("expected function g")
};
assert_eq!(name, "g");
assert_eq!(params.len(), 1);
assert_eq!(params[0].name, "y");
}
#[test]
fn greedy_arg_three_functions_middle_ends_with_call() {
let prog = parse_str("f xs:n>n;len xs g y:n>n;*y 2 h z:n>n;+z 1");
assert_eq!(prog.declarations.len(), 3, "expected 3 declarations");
let Decl::Function { name, .. } = &prog.declarations[0] else {
panic!("expected function f")
};
assert_eq!(name, "f");
let Decl::Function { name, .. } = &prog.declarations[1] else {
panic!("expected function g")
};
assert_eq!(name, "g");
let Decl::Function { name, .. } = &prog.declarations[2] else {
panic!("expected function h")
};
assert_eq!(name, "h");
}
#[test]
fn greedy_arg_still_collects_multiple_args_within_single_function() {
let prog = parse_str("f>n;tot 1 2 3");
assert_eq!(prog.declarations.len(), 1);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Expr(Expr::Call { function, args, .. }) = &body[0].node else {
panic!("expected Call(tot, [1,2,3])")
};
assert_eq!(function, "tot");
assert_eq!(args.len(), 3);
}
#[test]
fn parse_type_is_pattern_in_match() {
let prog = parse_str(r#"f x:t>t;?x{n v:"num";t v:v;_:"other"}"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Match { arms, .. } = &body[0].node else {
panic!("expected match")
};
assert_eq!(arms.len(), 3);
assert!(
matches!(&arms[0].pattern, Pattern::TypeIs { ty: Type::Number, binding } if binding == "v"),
"arm0: {:?}",
arms[0].pattern
);
assert!(
matches!(&arms[1].pattern, Pattern::TypeIs { ty: Type::Text, binding } if binding == "v"),
"arm1: {:?}",
arms[1].pattern
);
assert!(
matches!(&arms[2].pattern, Pattern::Wildcard),
"arm2: {:?}",
arms[2].pattern
);
}
#[test]
fn parse_use_basic() {
let prog = parse_str(r#"use "lib.ilo""#);
let Decl::Use { path, only, .. } = &prog.declarations[0] else {
panic!("expected Use")
};
assert_eq!(path, "lib.ilo");
assert!(only.is_none());
}
#[test]
fn parse_use_with_scoped_imports() {
let prog = parse_str(r#"use "lib.ilo" [foo bar]"#);
let Decl::Use { path, only, .. } = &prog.declarations[0] else {
panic!("expected Use")
};
assert_eq!(path, "lib.ilo");
let names = only.as_ref().unwrap();
assert_eq!(names, &["foo", "bar"]);
}
#[test]
fn parse_use_missing_path_error() {
let (_, errors) = parse_str_errors("use 42");
assert!(!errors.is_empty());
assert!(
errors.iter().any(|e| e.code == "ILO-P016"),
"got: {:?}",
errors
);
}
#[test]
fn parse_use_empty_bracket_list_error() {
let (_, errors) = parse_str_errors(r#"use "lib.ilo" []"#);
assert!(!errors.is_empty());
assert!(
errors
.iter()
.any(|e| e.code == "ILO-P016" && e.message.contains("must not be empty")),
"got: {:?}",
errors
);
}
#[test]
fn parse_alias_basic() {
let prog = parse_str("alias mynum n");
let Decl::Alias { name, target, .. } = &prog.declarations[0] else {
panic!("expected Alias")
};
assert_eq!(name, "mynum");
assert!(matches!(target, Type::Number));
}
#[test]
fn parse_alias_complex_type() {
let prog = parse_str("alias res R n t");
let Decl::Alias { name, target, .. } = &prog.declarations[0] else {
panic!("expected Alias")
};
assert_eq!(name, "res");
assert!(matches!(target, Type::Result(_, _)));
}
#[test]
fn parse_tool_retry_option() {
let prog = parse_str(r#"tool fetch"Get a URL" url:t>R t t retry:3"#);
let Decl::Tool {
name,
retry,
timeout,
..
} = &prog.declarations[0]
else {
panic!("expected Tool")
};
assert_eq!(name, "fetch");
assert_eq!(*retry, Some(3.0));
assert!(timeout.is_none());
}
#[test]
fn parse_tool_timeout_and_retry() {
let prog = parse_str(r#"tool fetch"Get a URL" url:t>R t t timeout:5,retry:3"#);
let Decl::Tool { timeout, retry, .. } = &prog.declarations[0] else {
panic!("expected Tool")
};
assert_eq!(*timeout, Some(5.0));
assert_eq!(*retry, Some(3.0));
}
#[test]
fn parse_nil_coalesce_basic() {
let prog = parse_str("f x:n>n;x??99");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Expr(Expr::NilCoalesce { default, .. }) = &body[0].node else {
panic!("expected NilCoalesce")
};
let Expr::Literal(Literal::Number(n)) = default.as_ref() else {
panic!("expected 99")
};
assert_eq!(*n, 99.0);
}
#[test]
fn reserved_word_if_as_identifier_errors_with_hint() {
let tokens = vec![
(Token::Type, Span::UNKNOWN),
(Token::KwIf, Span::UNKNOWN),
(Token::LBrace, Span::UNKNOWN),
(Token::RBrace, Span::UNKNOWN),
];
let (_, errors) = parse(tokens);
assert!(!errors.is_empty(), "expected parse error");
let e = errors
.iter()
.find(|e| e.code == "ILO-P011")
.expect("expected ILO-P011");
assert!(
e.message.contains("`if` is a reserved word"),
"message: {}",
e.message
);
let hint = e.hint.as_ref().expect("expected hint");
assert!(
hint.contains("cond"),
"hint should mention cond syntax, got: {}",
hint
);
}
#[test]
fn reserved_word_return_as_identifier_errors_with_hint() {
let tokens = vec![
(Token::Type, Span::UNKNOWN),
(Token::KwReturn, Span::UNKNOWN),
(Token::LBrace, Span::UNKNOWN),
(Token::RBrace, Span::UNKNOWN),
];
let (_, errors) = parse(tokens);
assert!(!errors.is_empty(), "expected parse error");
let e = errors
.iter()
.find(|e| e.code == "ILO-P011")
.expect("expected ILO-P011");
assert!(
e.message.contains("`return` is a reserved word"),
"message: {}",
e.message
);
let hint = e.hint.as_ref().expect("expected hint");
assert!(
hint.contains("ret"),
"hint should mention `ret`, got: {}",
hint
);
}
#[test]
fn reserved_word_let_as_identifier_errors_with_hint() {
let tokens = vec![
(Token::Type, Span::UNKNOWN),
(Token::KwLet, Span::UNKNOWN),
(Token::LBrace, Span::UNKNOWN),
(Token::RBrace, Span::UNKNOWN),
];
let (_, errors) = parse(tokens);
assert!(!errors.is_empty(), "expected parse error");
let e = errors
.iter()
.find(|e| e.code == "ILO-P011")
.expect("expected ILO-P011");
assert!(
e.message.contains("`let` is a reserved word"),
"message: {}",
e.message
);
let hint = e.hint.as_ref().expect("expected hint");
assert!(
hint.contains("name=expr") || hint.contains("bindings"),
"hint: {}",
hint
);
}
#[test]
fn reserved_word_fn_as_identifier_errors_with_hint() {
let tokens = vec![
(Token::Type, Span::UNKNOWN),
(Token::KwFn, Span::UNKNOWN),
(Token::LBrace, Span::UNKNOWN),
(Token::RBrace, Span::UNKNOWN),
];
let (_, errors) = parse(tokens);
assert!(!errors.is_empty(), "expected parse error");
let e = errors
.iter()
.find(|e| e.code == "ILO-P011")
.expect("expected ILO-P011");
assert!(
e.message.contains("`fn` is a reserved word"),
"message: {}",
e.message
);
let hint = e.hint.as_ref().expect("expected hint");
assert!(hint.contains("name params>return"), "hint: {}", hint);
}
#[test]
fn reserved_word_def_as_identifier_errors_with_hint() {
let tokens = vec![
(Token::Type, Span::UNKNOWN),
(Token::KwDef, Span::UNKNOWN),
(Token::LBrace, Span::UNKNOWN),
(Token::RBrace, Span::UNKNOWN),
];
let (_, errors) = parse(tokens);
assert!(!errors.is_empty(), "expected parse error");
let e = errors
.iter()
.find(|e| e.code == "ILO-P011")
.expect("expected ILO-P011");
assert!(
e.message.contains("`def` is a reserved word"),
"message: {}",
e.message
);
}
#[test]
fn reserved_word_var_as_identifier_errors_with_hint() {
let tokens = vec![
(Token::Type, Span::UNKNOWN),
(Token::KwVar, Span::UNKNOWN),
(Token::LBrace, Span::UNKNOWN),
(Token::RBrace, Span::UNKNOWN),
];
let (_, errors) = parse(tokens);
assert!(!errors.is_empty(), "expected parse error");
let e = errors
.iter()
.find(|e| e.code == "ILO-P011")
.expect("expected ILO-P011");
assert!(
e.message.contains("`var` is a reserved word"),
"message: {}",
e.message
);
let hint = e.hint.as_ref().expect("expected hint");
assert!(
hint.contains("name=expr") || hint.contains("bindings"),
"hint: {}",
hint
);
}
#[test]
fn reserved_word_const_as_identifier_errors_with_hint() {
let tokens = vec![
(Token::Type, Span::UNKNOWN),
(Token::KwConst, Span::UNKNOWN),
(Token::LBrace, Span::UNKNOWN),
(Token::RBrace, Span::UNKNOWN),
];
let (_, errors) = parse(tokens);
assert!(!errors.is_empty(), "expected parse error");
let e = errors
.iter()
.find(|e| e.code == "ILO-P011")
.expect("expected ILO-P011");
assert!(
e.message.contains("`const` is a reserved word"),
"message: {}",
e.message
);
let hint = e.hint.as_ref().expect("expected hint");
assert!(
hint.contains("name=expr") || hint.contains("bindings"),
"hint: {}",
hint
);
}
#[test]
fn foreign_syntax_fn_keyword_at_decl_level_gets_hint() {
let tokens = vec![
(Token::KwFn, Span::UNKNOWN),
(Token::Ident("foo".into()), Span::UNKNOWN),
];
let (_, errors) = parse(tokens);
assert!(!errors.is_empty(), "expected parse error");
let e = errors
.iter()
.find(|e| e.code == "ILO-P001")
.expect("expected ILO-P001");
let hint = e.hint.as_ref().expect("expected hint on fn at decl level");
assert!(hint.contains("ilo function syntax"), "hint: {}", hint);
}
#[test]
fn foreign_syntax_def_keyword_at_decl_level_gets_hint() {
let tokens = vec![
(Token::KwDef, Span::UNKNOWN),
(Token::Ident("foo".into()), Span::UNKNOWN),
];
let (_, errors) = parse(tokens);
assert!(!errors.is_empty(), "expected parse error");
let e = errors
.iter()
.find(|e| e.code == "ILO-P001")
.expect("expected ILO-P001");
let hint = e.hint.as_ref().expect("expected hint");
assert!(hint.contains("ilo function syntax"), "hint: {}", hint);
}
#[test]
fn foreign_syntax_let_keyword_at_decl_level_gets_hint() {
let tokens = vec![
(Token::KwLet, Span::UNKNOWN),
(Token::Ident("x".into()), Span::UNKNOWN),
];
let (_, errors) = parse(tokens);
assert!(!errors.is_empty(), "expected parse error");
let e = errors
.iter()
.find(|e| e.code == "ILO-P001")
.expect("expected ILO-P001");
let hint = e.hint.as_ref().expect("expected hint");
assert!(hint.contains("assignment syntax"), "hint: {}", hint);
}
#[test]
fn foreign_syntax_var_keyword_at_decl_level_gets_hint() {
let tokens = vec![
(Token::KwVar, Span::UNKNOWN),
(Token::Ident("x".into()), Span::UNKNOWN),
];
let (_, errors) = parse(tokens);
assert!(!errors.is_empty(), "expected parse error");
let e = errors
.iter()
.find(|e| e.code == "ILO-P001")
.expect("expected ILO-P001");
let hint = e.hint.as_ref().expect("expected hint");
assert!(hint.contains("assignment syntax"), "hint: {}", hint);
}
#[test]
fn foreign_syntax_const_keyword_at_decl_level_gets_hint() {
let tokens = vec![
(Token::KwConst, Span::UNKNOWN),
(Token::Ident("x".into()), Span::UNKNOWN),
];
let (_, errors) = parse(tokens);
assert!(!errors.is_empty(), "expected parse error");
let e = errors
.iter()
.find(|e| e.code == "ILO-P001")
.expect("expected ILO-P001");
let hint = e.hint.as_ref().expect("expected hint");
assert!(hint.contains("assignment syntax"), "hint: {}", hint);
}
#[test]
fn foreign_syntax_return_keyword_at_decl_level_gets_hint() {
let tokens = vec![
(Token::KwReturn, Span::UNKNOWN),
(Token::Ident("x".into()), Span::UNKNOWN),
];
let (_, errors) = parse(tokens);
assert!(!errors.is_empty(), "expected parse error");
let e = errors
.iter()
.find(|e| e.code == "ILO-P001")
.expect("expected ILO-P001");
let hint = e.hint.as_ref().expect("expected hint");
assert!(hint.contains("return value"), "hint: {}", hint);
}
#[test]
fn foreign_syntax_if_keyword_at_decl_level_gets_hint() {
let tokens = vec![
(Token::KwIf, Span::UNKNOWN),
(Token::Ident("x".into()), Span::UNKNOWN),
];
let (_, errors) = parse(tokens);
assert!(!errors.is_empty(), "expected parse error");
let e = errors
.iter()
.find(|e| e.code == "ILO-P001")
.expect("expected ILO-P001");
let hint = e.hint.as_ref().expect("expected hint");
assert!(
hint.contains("match") || hint.contains("conditionals"),
"hint: {}",
hint
);
}
#[test]
fn foreign_ident_let_at_decl_level_gets_hint() {
let (_, errors) = parse_str_errors("let x = 5");
assert!(!errors.is_empty(), "expected parse error");
let e = errors
.iter()
.find(|e| e.code == "ILO-P001")
.expect("expected ILO-P001");
let hint = e.hint.as_ref().expect("expected hint");
assert!(hint.contains("assignment syntax"), "hint: {}", hint);
}
#[test]
fn foreign_ident_return_at_decl_level_gets_hint() {
let (_, errors) = parse_str_errors("return x");
assert!(!errors.is_empty(), "expected parse error");
let e = errors
.iter()
.find(|e| e.code == "ILO-P001")
.expect("expected ILO-P001");
let hint = e.hint.as_ref().expect("expected hint");
assert!(hint.contains("return value"), "hint: {}", hint);
}
#[test]
fn foreign_ident_if_at_decl_level_gets_hint() {
let (_, errors) = parse_str_errors("if x > 0 {}");
assert!(!errors.is_empty(), "expected parse error");
let e = errors
.iter()
.find(|e| e.code == "ILO-P001")
.expect("expected ILO-P001");
let hint = e.hint.as_ref().expect("expected hint");
assert!(hint.contains("match"), "hint: {}", hint);
}
#[test]
fn foreign_ident_fn_at_decl_level_gets_hint() {
let (_, errors) = parse_str_errors("fn foo() {}");
assert!(!errors.is_empty(), "expected parse error");
let e = errors
.iter()
.find(|e| e.code == "ILO-P001")
.expect("expected ILO-P001");
let hint = e.hint.as_ref().expect("expected hint");
assert!(hint.contains("ilo function syntax"), "hint: {}", hint);
}
#[test]
fn sum_type_requires_at_least_one_variant() {
let (_, errors) = parse_str_errors("f x:S>n;x");
assert!(!errors.is_empty(), "expected parse error for empty S type");
assert!(
errors
.iter()
.any(|e| e.code == "ILO-P010" || e.message.contains("S type requires")),
"expected ILO-P010, got: {:?}",
errors
);
}
#[test]
fn fn_type_requires_at_least_return_type() {
let (_, errors) = parse_str_errors("f x:F>n;x");
assert!(!errors.is_empty(), "expected parse error for empty F type");
assert!(
errors
.iter()
.any(|e| e.code == "ILO-P009" || e.message.contains("F type requires")),
"expected ILO-P009, got: {:?}",
errors
);
}
#[test]
fn nil_type_underscore_in_param() {
let prog = parse_str("f x:_>_;x");
let Decl::Function {
params,
return_type,
..
} = &prog.declarations[0]
else {
panic!("expected function")
};
assert_eq!(params[0].ty, Type::Any);
assert_eq!(*return_type, Type::Any);
}
#[test]
fn optional_type_in_param() {
let prog = parse_str("f x:O t>O t;x");
let Decl::Function {
params,
return_type,
..
} = &prog.declarations[0]
else {
panic!("expected function")
};
assert!(matches!(params[0].ty, Type::Optional(_)));
assert!(matches!(*return_type, Type::Optional(_)));
}
#[test]
fn list_type_in_param() {
let prog = parse_str("f x:L n>L n;x");
let Decl::Function {
params,
return_type,
..
} = &prog.declarations[0]
else {
panic!("expected function")
};
assert!(matches!(¶ms[0].ty, Type::List(inner) if **inner == Type::Number));
assert!(matches!(return_type, Type::List(inner) if **inner == Type::Number));
}
#[test]
fn map_type_in_param() {
let prog = parse_str("f x:M t n>M t n;x");
let Decl::Function {
params,
return_type,
..
} = &prog.declarations[0]
else {
panic!("expected function")
};
assert!(matches!(¶ms[0].ty, Type::Map(_, _)));
assert!(matches!(return_type, Type::Map(_, _)));
}
#[test]
fn result_type_in_param() {
let prog = parse_str("f x:R t t>R t t;x");
let Decl::Function {
params,
return_type,
..
} = &prog.declarations[0]
else {
panic!("expected function")
};
assert!(matches!(¶ms[0].ty, Type::Result(_, _)));
assert!(matches!(return_type, Type::Result(_, _)));
}
#[test]
fn sum_type_in_param() {
let prog = parse_str("f x:S ok err>S ok err;x");
let Decl::Function {
params,
return_type,
..
} = &prog.declarations[0]
else {
panic!("expected function")
};
assert!(matches!(¶ms[0].ty, Type::Sum(variants) if variants.len() == 2));
assert!(matches!(return_type, Type::Sum(variants) if variants.len() == 2));
}
#[test]
fn fn_type_in_param() {
let prog = parse_str("f x:F n n>F n n;x");
let Decl::Function {
params,
return_type,
..
} = &prog.declarations[0]
else {
panic!("expected function")
};
assert!(matches!(¶ms[0].ty, Type::Fn(param_types, _) if param_types.len() == 1));
assert!(matches!(return_type, Type::Fn(param_types, _) if param_types.len() == 1));
}
#[test]
fn match_arm_multiple_type_is_patterns() {
let prog = parse_str(r#"f x:t>t;?x{n v:"num";t v:v;b v:"bool"}"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Match { arms, .. } = &body[0].node else {
panic!("expected match")
};
assert_eq!(arms.len(), 3, "expected 3 arms");
assert!(
matches!(&arms[0].pattern, Pattern::TypeIs { ty: Type::Number, binding } if binding == "v")
);
assert!(
matches!(&arms[1].pattern, Pattern::TypeIs { ty: Type::Text, binding } if binding == "v")
);
assert!(
matches!(&arms[2].pattern, Pattern::TypeIs { ty: Type::Bool, binding } if binding == "v")
);
}
#[test]
fn match_arm_type_is_with_wildcard_binding() {
let prog = parse_str(r#"f x:t>t;?x{n _:"num";_:"other"}"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Match { arms, .. } = &body[0].node else {
panic!("expected match")
};
assert_eq!(arms.len(), 2);
assert!(
matches!(&arms[0].pattern, Pattern::TypeIs { ty: Type::Number, binding } if binding == "_")
);
assert!(matches!(&arms[1].pattern, Pattern::Wildcard));
}
#[test]
fn use_missing_path_eof_error() {
let (_, errors) = parse_str_errors("use");
assert!(!errors.is_empty(), "expected parse error");
assert!(
errors
.iter()
.any(|e| e.code == "ILO-P016" || e.message.contains("expected a string path")),
"expected ILO-P016, got: {:?}",
errors
);
}
#[test]
fn use_unclosed_bracket_list_error() {
let (_, errors) = parse_str_errors(r#"use "file.ilo" [foo"#);
assert!(!errors.is_empty(), "expected parse error for unclosed [");
assert!(
errors
.iter()
.any(|e| e.code == "ILO-P016" || e.message.contains("unclosed")),
"expected ILO-P016 for unclosed bracket, got: {:?}",
errors
);
}
#[test]
fn use_bracket_list_with_reserved_word_errors() {
let tokens = vec![
(Token::Use, Span::UNKNOWN),
(Token::Text("file.ilo".into()), Span::UNKNOWN),
(Token::LBracket, Span::UNKNOWN),
(Token::KwIf, Span::UNKNOWN),
(Token::RBracket, Span::UNKNOWN),
];
let (_, errors) = parse(tokens);
assert!(!errors.is_empty(), "expected parse error");
assert!(
errors.iter().any(|e| e.code == "ILO-P011"),
"expected ILO-P011 for reserved word in use list, got: {:?}",
errors
);
}
#[test]
fn parse_return_at_decl_level_gives_hint() {
let (_, errors) = parse_str_errors("return x");
assert!(!errors.is_empty(), "expected parse error");
let hint_found = errors
.iter()
.any(|e| e.hint.as_deref().unwrap_or("").contains("return value"));
assert!(
hint_found,
"expected 'return value' hint, got: {:?}",
errors
);
}
#[test]
fn parse_if_at_decl_level_gives_hint() {
let (_, errors) = parse_str_errors("if x > 0");
assert!(!errors.is_empty(), "expected parse error");
let hint_found = errors
.iter()
.any(|e| e.hint.as_deref().unwrap_or("").contains("match"));
assert!(
hint_found,
"expected 'match' hint for 'if', got: {:?}",
errors
);
}
#[test]
fn parse_tool_decl_stops_at_non_option_token() {
let prog = parse_str(r#"tool ping "ping server" url:t>t"#);
let Decl::Tool { name, .. } = &prog.declarations[0] else {
panic!("expected tool decl")
};
assert_eq!(name, "ping");
}
#[test]
fn parse_sum_type_with_trailing_param_breaks_correctly() {
let prog = parse_str(r#"f x:S foo bar>t;"ok""#);
let Decl::Function { params, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert_eq!(params.len(), 1);
let Type::Sum(variants) = ¶ms[0].ty else {
panic!("expected Sum type")
};
assert_eq!(variants, &["foo".to_string(), "bar".to_string()]);
}
#[test]
fn parse_fn_type_in_param_breaks_at_colon() {
let prog = parse_str(r#"f cb:F n t x:n>n;x"#);
let Decl::Function { params, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert_eq!(params.len(), 2);
let Type::Fn(arg_types, ret) = ¶ms[0].ty else {
panic!("expected Fn type")
};
assert_eq!(arg_types.len(), 1);
assert!(matches!(**ret, Type::Text));
}
#[test]
fn parse_underscore_type_in_param() {
let (_, errors) = parse_str_errors("f x:_>n;0");
let _ = errors;
}
#[test]
fn parse_opt_type_in_param() {
let prog = parse_str("f x:O t>n;0");
let Decl::Function { params, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert_eq!(params.len(), 1);
assert!(matches!(¶ms[0].ty, Type::Optional(_)));
}
#[test]
fn parse_list_type_in_param() {
let prog = parse_str("f xs:L n>n;0");
let Decl::Function { params, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert!(matches!(¶ms[0].ty, Type::List(_)));
}
#[test]
fn parse_map_type_in_param() {
let prog = parse_str("f m:M t n>n;0");
let Decl::Function { params, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert!(matches!(¶ms[0].ty, Type::Map(_, _)));
}
#[test]
fn parse_result_type_in_param() {
let prog = parse_str("f r:R n t>n;0");
let Decl::Function { params, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert!(matches!(¶ms[0].ty, Type::Result(_, _)));
}
#[test]
fn guard_with_non_eligible_condition_parses_as_stmt() {
let prog = parse_str(r#"f x:n>n;x{x}"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert!(!body.is_empty());
let _ = body;
}
#[test]
fn guard_with_literal_condition_hits_non_eligible_branch() {
let (prog, _errors) = parse_str_errors(r#"f x:n>n; 1{x}"#);
let _ = prog;
}
#[test]
fn match_with_type_pattern_at_end_of_tokens() {
let prog = parse_str(r#"f x:n>t;?x{~v:"ok";^_:"err"}"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert!(!body.is_empty());
}
#[test]
fn parse_negated_guard_with_else_body() {
let prog = parse_str(r#"f x:n>n;!>x 0{-1}{1}"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert!(!body.is_empty());
let Stmt::Guard {
negated, else_body, ..
} = &body[0].node
else {
panic!("expected Guard")
};
assert!(negated, "expected negated guard");
assert!(else_body.is_some(), "expected else body");
}
#[test]
fn parse_guard_with_else_body() {
let prog = parse_str(r#"f x:n>n;>x 0{1}{-1}"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert!(!body.is_empty());
let Stmt::Guard {
negated, else_body, ..
} = &body[0].node
else {
panic!("expected Guard")
};
assert!(!negated, "expected non-negated guard");
assert!(else_body.is_some(), "expected else body");
}
#[test]
fn parse_braceless_negated_guard() {
let prog = parse_str(r#"f x:n>n;!>x 0 99;x"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert!(body.len() >= 2);
let Stmt::Guard { negated, .. } = &body[0].node else {
panic!("expected Guard")
};
assert!(negated);
}
#[test]
fn parse_pipe_with_bang_unwrap() {
let prog = parse_str(r#"dbl x:n>n;*x 2 f s:t>n;s>>num!"#);
let Some(Decl::Function { body, .. }) = prog.declarations.last() else {
panic!("expected function")
};
assert!(!body.is_empty());
let Stmt::Expr(Expr::Call { unwrap, .. }) = &body[0].node else {
panic!("expected Call expr")
};
assert!(unwrap, "expected unwrap=true on piped call");
}
#[test]
fn parse_dollar_as_operand_in_let() {
let prog = parse_str(r#"f url:t>R t t;r=$url;r"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert!(!body.is_empty());
let Stmt::Let { value, .. } = &body[0].node else {
panic!("expected let")
};
let Expr::Call {
function, unwrap, ..
} = value
else {
panic!("expected get call")
};
assert_eq!(function, "get");
assert!(!unwrap);
}
#[test]
fn parse_sum_type_stops_at_named_param() {
let prog = parse_str("f x:S a n:n>n;0");
let Decl::Function { params, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert_eq!(params.len(), 2);
let Type::Sum(variants) = ¶ms[0].ty else {
panic!("expected Sum type")
};
assert_eq!(variants, &["a"]);
assert_eq!(params[1].name, "n");
}
#[test]
fn parse_fn_type_stops_at_named_param() {
let prog = parse_str("f x:F n n:n>n;0");
let Decl::Function { params, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert_eq!(params.len(), 2);
let Type::Fn(param_types, ret) = ¶ms[0].ty else {
panic!("expected Fn type")
};
assert!(param_types.is_empty(), "F n should have no param types");
assert!(matches!(ret.as_ref(), Type::Number));
}
#[test]
fn parse_fn_type_with_underscore_param() {
let prog = parse_str("f cb:F _ n>n;0");
let Decl::Function { params, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert_eq!(params.len(), 1);
assert!(matches!(¶ms[0].ty, Type::Fn(..)));
}
#[test]
fn parse_fn_type_with_opt_param() {
let prog = parse_str("f cb:F O n n>n;0");
let Decl::Function { params, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert_eq!(params.len(), 1);
assert!(matches!(¶ms[0].ty, Type::Fn(..)));
}
#[test]
fn parse_fn_type_with_list_param() {
let prog = parse_str("f cb:F L n n>n;0");
let Decl::Function { params, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert_eq!(params.len(), 1);
assert!(matches!(¶ms[0].ty, Type::Fn(..)));
}
#[test]
fn parse_fn_type_with_map_param() {
let prog = parse_str("f cb:F M t n n>n;0");
let Decl::Function { params, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert_eq!(params.len(), 1);
assert!(matches!(¶ms[0].ty, Type::Fn(..)));
}
#[test]
fn parse_fn_type_with_result_param() {
let prog = parse_str("f cb:F R n t n>n;0");
let Decl::Function { params, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert_eq!(params.len(), 1);
assert!(matches!(¶ms[0].ty, Type::Fn(..)));
}
#[test]
fn parse_fn_type_with_sum_param() {
let prog = parse_str("f cb:F S a n>n;0");
let Decl::Function { params, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert_eq!(params.len(), 1);
assert!(matches!(¶ms[0].ty, Type::Fn(..)));
}
#[test]
fn parse_fn_type_with_nested_fn_param() {
let prog = parse_str("f cb:F F n n>n;0");
let Decl::Function { params, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert_eq!(params.len(), 1);
assert!(matches!(¶ms[0].ty, Type::Fn(..)));
}
#[test]
fn parse_non_ident_inside_brace_is_not_destructure() {
let (_prog, errs) = parse_str_errors("f x:n>n;{42}=x");
assert!(
!errs.is_empty(),
"expected parse error for non-destructure brace"
);
}
#[test]
fn parse_match_type_is_two_arms() {
let prog = parse_str(r#"f x:n>n;?x{n y: +y 1; n z: *z 2}"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert!(!body.is_empty());
let Stmt::Match { arms, .. } = &body[0].node else {
panic!("expected Match")
};
assert_eq!(arms.len(), 2);
}
#[test]
fn parse_match_type_is_incomplete_at_eof() {
let (_prog, errs) = parse_str_errors("f x:n>n;?x{n y:1;n");
assert!(
!errs.is_empty(),
"expected parse error for incomplete TypeIs arm"
);
}
#[test]
fn parse_dollar_as_function_argument() {
let prog = parse_str(r#"f url:t>t;fetch $url"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert!(!body.is_empty());
let Stmt::Expr(Expr::Call { function, args, .. }) = &body[0].node else {
panic!("expected Call stmt")
};
assert_eq!(function, "fetch");
assert_eq!(args.len(), 1);
let Expr::Call {
function: inner_fn, ..
} = &args[0]
else {
panic!("expected get call as arg")
};
assert_eq!(inner_fn, "get");
}
#[test]
fn match_literal_pattern_at_end_of_tokens() {
let (prog, _errors) = parse_str_errors(r#"f x:n>t;?x{1:"one";2"#);
let _ = prog; }
#[test]
fn foreign_ident_let_raw_token_hint() {
let tokens = vec![
(Token::Ident("let".into()), Span::UNKNOWN),
(Token::Ident("x".into()), Span::UNKNOWN),
];
let (_, errors) = parse(tokens);
assert!(!errors.is_empty());
let e = errors
.iter()
.find(|e| e.code == "ILO-P001")
.expect("expected ILO-P001");
assert!(e.hint.as_ref().unwrap().contains("assignment syntax"));
}
#[test]
fn foreign_ident_var_raw_token_hint() {
let tokens = vec![
(Token::Ident("var".into()), Span::UNKNOWN),
(Token::Ident("x".into()), Span::UNKNOWN),
];
let (_, errors) = parse(tokens);
assert!(!errors.is_empty());
let e = errors
.iter()
.find(|e| e.code == "ILO-P001")
.expect("expected ILO-P001");
assert!(e.hint.as_ref().unwrap().contains("assignment syntax"));
}
#[test]
fn foreign_ident_const_raw_token_hint() {
let tokens = vec![
(Token::Ident("const".into()), Span::UNKNOWN),
(Token::Ident("x".into()), Span::UNKNOWN),
];
let (_, errors) = parse(tokens);
assert!(!errors.is_empty());
let e = errors
.iter()
.find(|e| e.code == "ILO-P001")
.expect("expected ILO-P001");
assert!(e.hint.as_ref().unwrap().contains("assignment syntax"));
}
#[test]
fn foreign_ident_return_raw_token_hint() {
let tokens = vec![
(Token::Ident("return".into()), Span::UNKNOWN),
(Token::Ident("x".into()), Span::UNKNOWN),
];
let (_, errors) = parse(tokens);
assert!(!errors.is_empty());
let e = errors
.iter()
.find(|e| e.code == "ILO-P001")
.expect("expected ILO-P001");
assert!(e.hint.as_ref().unwrap().contains("return value"));
}
#[test]
fn foreign_ident_if_raw_token_hint() {
let tokens = vec![
(Token::Ident("if".into()), Span::UNKNOWN),
(Token::Ident("x".into()), Span::UNKNOWN),
];
let (_, errors) = parse(tokens);
assert!(!errors.is_empty());
let e = errors
.iter()
.find(|e| e.code == "ILO-P001")
.expect("expected ILO-P001");
assert!(e.hint.as_ref().unwrap().contains("match"));
}
#[test]
fn parse_match_nil_literal_pattern() {
let prog = parse_str("f x:n>n;?x{nil:0;_:1}");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Match { arms, .. } = &body[0].node else {
panic!("expected match")
};
assert!(matches!(&arms[0].pattern, Pattern::Literal(Literal::Nil)));
}
#[test]
fn parse_expr_or_guard_with_else_body() {
let source = r#"f x:n>n;=x 1{10}{20}"#;
let prog = parse_str(source);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Guard { else_body, .. } = &body[0].node else {
panic!("expected guard")
};
assert!(else_body.is_some(), "expected else body");
}
#[test]
fn parse_expr_or_guard_braceless() {
let prog = parse_str("f x:n>n;=x 0 99;x");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert!(body.len() >= 2);
assert!(
matches!(&body[0].node, Stmt::Guard { .. }),
"expected braceless guard, got {:?}",
body[0]
);
}
#[test]
fn infix_or_operator() {
let prog = parse_str("f a:b b:b>b;a | b");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!()
};
let Stmt::Expr(Expr::BinOp { op: BinOp::Or, .. }) = &body[0].node else {
panic!("expected infix or")
};
}
#[test]
fn infix_equals_operator() {
let prog = parse_str("f a:n b:n>b;(a == b)");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!()
};
let Stmt::Expr(Expr::BinOp {
op: BinOp::Equals, ..
}) = &body[0].node
else {
panic!("expected infix equals, got {:?}", body[0])
};
}
#[test]
fn infix_not_equals_operator() {
let prog = parse_str("f a:n b:n>b;a != b");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!()
};
let Stmt::Expr(Expr::BinOp {
op: BinOp::NotEquals,
..
}) = &body[0].node
else {
panic!("expected infix not-equals")
};
}
#[test]
fn infix_less_than_operator() {
let prog = parse_str("f a:n b:n>b;a < b");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!()
};
let Stmt::Expr(Expr::BinOp {
op: BinOp::LessThan,
..
}) = &body[0].node
else {
panic!("expected infix less-than")
};
}
#[test]
fn infix_less_or_equal_operator() {
let prog = parse_str("f a:n b:n>b;a <= b");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!()
};
let Stmt::Expr(Expr::BinOp {
op: BinOp::LessOrEqual,
..
}) = &body[0].node
else {
panic!("expected infix <=")
};
}
#[test]
fn infix_greater_or_equal_operator() {
let prog = parse_str("f a:n b:n>b;a >= b");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!()
};
let Stmt::Expr(Expr::BinOp {
op: BinOp::GreaterOrEqual,
..
}) = &body[0].node
else {
panic!("expected infix >=")
};
}
#[test]
fn infix_append_operator() {
let prog = parse_str("f xs:L n x:n>L n;xs += x");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!()
};
let Stmt::Expr(Expr::BinOp {
op: BinOp::Append, ..
}) = &body[0].node
else {
panic!("expected infix +=")
};
}
#[test]
fn looks_like_prefix_with_paren_group() {
let prog = parse_str("fac n:n>n;r=fac -(n) 1;*n r");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Let {
value: Expr::Call { function, args, .. },
..
} = &body[0].node
else {
panic!("expected call")
};
assert_eq!(function, "fac");
assert_eq!(args.len(), 1);
}
#[test]
fn looks_like_prefix_with_bracket_group() {
let prog = parse_str("foo a:L n b:n>n;0 f x:n>n;r=foo -[1, 2] x;r");
let Decl::Function { body, .. } = &prog.declarations[1] else {
panic!("expected function")
};
let Stmt::Let {
value: Expr::Call { function, args, .. },
..
} = &body[0].node
else {
panic!("expected call")
};
assert_eq!(function, "foo");
assert_eq!(args.len(), 1);
}
#[test]
fn parse_nil_literal_operand() {
let prog = parse_str("f>_;nil");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert!(matches!(
&body[0].node,
Stmt::Expr(Expr::Literal(Literal::Nil))
));
}
#[test]
fn eq_prefix_is_equality_check() {
let prog = parse_str("f x:n y:n>b;=x y");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected fn")
};
let Stmt::Expr(Expr::BinOp { op, .. }) = &body[0].node else {
panic!("expected equality binop, got {:?}", body[0].node)
};
assert_eq!(*op, BinOp::Equals);
}
#[test]
fn eq_after_ident_is_let_binding() {
let prog = parse_str("f>n;x=1;x");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected fn")
};
let Stmt::Let { name, .. } = &body[0].node else {
panic!("expected let binding, got {:?}", body[0].node)
};
assert_eq!(name, "x");
}
#[test]
fn eq_double_equals_is_equality() {
let prog = parse_str("f x:n>b;==x 1");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected fn")
};
let Stmt::Expr(Expr::BinOp { op, .. }) = &body[0].node else {
panic!("expected equality binop, got {:?}", body[0].node)
};
assert_eq!(*op, BinOp::Equals);
}
#[test]
fn eq_infix_is_equality() {
let prog = parse_str("f x:n>b;r=+x 0;=r 0");
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected fn")
};
let Stmt::Expr(Expr::BinOp {
op: BinOp::Equals, ..
}) = &body[1].node
else {
panic!("expected equality, got {:?}", body[1].node)
};
}
#[test]
fn eq_prefix_ternary_uses_equality() {
let prog = parse_str(r#"f x:n>t;?=x 0 "zero" "nonzero""#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected fn")
};
let Stmt::Expr(Expr::Ternary { condition, .. }) = &body[0].node else {
panic!("expected ternary, got {:?}", body[0].node)
};
let Expr::BinOp { op, .. } = condition.as_ref() else {
panic!("expected equality condition, got {:?}", condition)
};
assert_eq!(*op, BinOp::Equals);
}
#[test]
fn eq_guard_with_equality_condition() {
let prog = parse_str(r#"f x:n>t;=x 1{"one"};"other""#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected fn")
};
let Stmt::Guard { condition, .. } = &body[0].node else {
panic!("expected guard, got {:?}", body[0].node)
};
let Expr::BinOp { op, .. } = condition else {
panic!("expected equality condition, got {:?}", condition)
};
assert_eq!(*op, BinOp::Equals);
}
#[test]
fn type_is_pattern_bounds_check_in_semi_starts_new_arm() {
let prog = parse_str(r#"f x:n>n;?x{n v:v;_:0}"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Match { arms, .. } = &body[0].node else {
panic!("expected match")
};
assert_eq!(arms.len(), 2);
assert!(matches!(
&arms[0].pattern,
Pattern::TypeIs {
ty: Type::Number,
..
}
));
}
#[test]
fn cov_guard_with_else_braces() {
let prog = parse_str(r#"f x:n>n;>=x 0{x}{0}"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
match &body[0].node {
Stmt::Guard { else_body, .. } => {
assert!(else_body.is_some(), "should have else body");
}
other => panic!("expected Guard, got {:?}", other),
}
}
#[test]
fn cov_braceless_guard() {
let prog = parse_str(r#"f x:n>n;>=x 0 x"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert!(matches!(&body[0].node, Stmt::Guard { .. }));
}
#[test]
fn cov_err_expression() {
let prog = parse_str(r#"f>R n t;^"oops""#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
match &body[0].node {
Stmt::Expr(Expr::Err(_)) => {}
other => panic!("expected Err expression, got {:?}", other),
}
}
#[test]
fn cov_parse_tokens_error() {
use crate::lexer::Token;
let tokens = vec![Token::Greater]; let result = super::parse_tokens(tokens);
assert!(
result.is_err(),
"incomplete tokens should produce parse error"
);
}
#[test]
fn cov_type_is_bool_pattern() {
let prog = parse_str(r#"f x:n>n;?x{b v:1;_:0}"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Match { arms, .. } = &body[0].node else {
panic!("expected match")
};
assert!(matches!(
&arms[0].pattern,
Pattern::TypeIs { ty: Type::Bool, .. }
));
}
#[test]
fn cov_type_is_list_pattern() {
let prog = parse_str(r#"f x:n>n;?x{l v:1;_:0}"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Match { arms, .. } = &body[0].node else {
panic!("expected match")
};
assert!(matches!(
&arms[0].pattern,
Pattern::TypeIs {
ty: Type::List(_),
..
}
));
}
#[test]
fn cov_cascading_braceless_guards() {
let prog = parse_str(r#"cls sp:n>t;>=sp 1000 "gold";>=sp 500 "silver";"bronze""#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert!(body.len() >= 2, "should have multiple statements");
}
#[test]
fn cov_nil_literal_pattern() {
let prog = parse_str(r#"f x:n>n;?x{nil:0;_:1}"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Match { arms, .. } = &body[0].node else {
panic!("expected match")
};
assert!(matches!(&arms[0].pattern, Pattern::Literal(Literal::Nil)));
}
#[test]
fn cov_parse_let_single_brace_guard() {
let prog = parse_str(r#"f x:n>n;v=>=x 0{42};v"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert!(
matches!(
&body[0].node,
Stmt::Guard {
negated: false,
else_body: None,
braceless: false,
..
}
),
"expected Guard from single-brace let desugar, got {:?}",
body[0].node
);
}
#[test]
fn cov_wrap_body_as_let_empty_body() {
let prog = parse_str(r#"f x:n>n;v=>=x 0{};v"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Guard {
body: guard_body, ..
} = &body[0].node
else {
panic!("expected Guard, got {:?}", body[0].node)
};
assert_eq!(guard_body.len(), 1);
assert!(
matches!(
&guard_body[0].node,
Stmt::Let {
value: Expr::Literal(Literal::Nil),
..
}
),
"expected Let{{Nil}} in guard body, got {:?}",
guard_body[0].node
);
}
#[test]
fn cov_wrap_body_as_let_non_expr_last() {
let prog = parse_str(r#"f x:n>n;v=>=x 0{w=1};v"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Guard {
body: guard_body, ..
} = &body[0].node
else {
panic!("expected Guard, got {:?}", body[0].node)
};
assert!(!guard_body.is_empty());
assert!(
matches!(&guard_body[0].node, Stmt::Let { name, .. } if name == "w"),
"expected inner Let{{w}} untouched, got {:?}",
guard_body[0].node
);
}
#[test]
fn cov_list_element_caret_err() {
let prog = parse_str(r#"f x:n>R n t;[^"bad"]"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Expr(Expr::List(elems)) = &body[0].node else {
panic!("expected List expr, got {:?}", body[0].node)
};
assert_eq!(elems.len(), 1);
assert!(
matches!(&elems[0], Expr::Err(_)),
"expected Err element, got {:?}",
elems[0]
);
}
#[test]
fn cov_body_to_expr_empty() {
let prog = parse_str(r#"f x:n>n;v=>=x 0{}{};v"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Let { value, .. } = &body[0].node else {
panic!("expected Let, got {:?}", body[0].node)
};
assert!(
matches!(value, Expr::Ternary { then_expr, else_expr, .. }
if matches!(then_expr.as_ref(), Expr::Literal(Literal::Nil))
&& matches!(else_expr.as_ref(), Expr::Literal(Literal::Nil))
),
"expected Ternary{{Nil, Nil}}, got {:?}",
value
);
}
#[test]
fn cov_body_to_expr_non_expr_last() {
let prog = parse_str(r#"f x:n>n;v=>=x 0{w=1}{w=2};v"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Let { value, .. } = &body[0].node else {
panic!("expected Let, got {:?}", body[0].node)
};
assert!(
matches!(value, Expr::Ternary { then_expr, else_expr, .. }
if matches!(then_expr.as_ref(), Expr::Literal(Literal::Nil))
&& matches!(else_expr.as_ref(), Expr::Literal(Literal::Nil))
),
"expected Ternary fallback to Nil for non-Expr branches, got {:?}",
value
);
}
#[test]
fn cov_semi_starts_new_arm_type_is() {
let prog = parse_str(r#"f x:n>n;?x{1:x;n v:v;_:0}"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Match { arms, .. } = &body[0].node else {
panic!("expected match")
};
assert_eq!(arms.len(), 3, "expected 3 match arms");
assert!(
matches!(
&arms[1].pattern,
Pattern::TypeIs {
ty: Type::Number,
..
}
),
"expected TypeIs Number arm, got {:?}",
arms[1].pattern
);
}
#[test]
fn cov_semi_starts_new_arm_type_is_false() {
let prog = parse_str(r#"f x:n>n;?x{1:x;n;_:0}"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
let Stmt::Match { arms, .. } = &body[0].node else {
panic!("expected match")
};
assert!(arms.len() >= 2, "expected at least 2 arms");
}
#[test]
fn cov_looks_like_prefix_binary_paren_group() {
let prog = parse_str(r#"f x:n>n;+(x) 1"#);
let Decl::Function { body, .. } = &prog.declarations[0] else {
panic!("expected function")
};
assert!(
matches!(
&body[0].node,
Stmt::Expr(Expr::BinOp { op: BinOp::Add, .. })
),
"expected Add BinOp, got {:?}",
body[0].node
);
}
#[test]
fn cov_looks_like_prefix_binary_nested_parens() {
let prog = parse_str(r#"dbl x:n>n;*x 2 f y:n>n;dbl -((y)) 1"#);
let Decl::Function { body, .. } = &prog.declarations[1] else {
panic!("expected second function")
};
assert!(
matches!(&body[0].node, Stmt::Expr(Expr::Call { function, .. }) if function == "dbl"),
"expected Call to dbl, got {:?}",
body[0].node
);
}
}