use super::*;
use crate::ast::{Program, Statement, WordDef};
use crate::types::{Effect, StackType, Type};
#[test]
fn test_simple_literal() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty,
StackType::singleton(Type::Int),
)),
body: vec![Statement::IntLiteral(42)],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_simple_operation() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty.push(Type::Int).push(Type::Int),
StackType::singleton(Type::Int),
)),
body: vec![Statement::WordCall {
name: "i.add".to_string(),
span: None,
}],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_type_mismatch() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::String),
StackType::Empty,
)),
body: vec![
Statement::IntLiteral(42), Statement::WordCall {
name: "io.write-line".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Type mismatch"));
}
#[test]
fn test_polymorphic_dup() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "my-dup".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Int),
StackType::Empty.push(Type::Int).push(Type::Int),
)),
body: vec![Statement::WordCall {
name: "dup".to_string(),
span: None,
}],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_conditional_branches() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty.push(Type::Int).push(Type::Int),
StackType::singleton(Type::String),
)),
body: vec![
Statement::WordCall {
name: "i.>".to_string(),
span: None,
},
Statement::If {
then_branch: vec![Statement::StringLiteral(b"greater".to_vec())],
else_branch: Some(vec![Statement::StringLiteral(b"not greater".to_vec())]),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_mismatched_branches() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty,
StackType::singleton(Type::Int),
)),
body: vec![
Statement::BoolLiteral(true),
Statement::If {
then_branch: vec![Statement::IntLiteral(42)],
else_branch: Some(vec![Statement::StringLiteral(b"string".to_vec())]),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(result.is_err());
assert!(result.unwrap_err().contains("incompatible"));
}
#[test]
fn test_user_defined_word_call() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![
WordDef {
name: "helper".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Int),
StackType::singleton(Type::String),
)),
body: vec![Statement::WordCall {
name: "int->string".to_string(),
span: None,
}],
source: None,
allowed_lints: vec![],
},
WordDef {
name: "main".to_string(),
effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
body: vec![
Statement::IntLiteral(42),
Statement::WordCall {
name: "helper".to_string(),
span: None,
},
Statement::WordCall {
name: "io.write-line".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
},
],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_arithmetic_chain() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty
.push(Type::Int)
.push(Type::Int)
.push(Type::Int),
StackType::singleton(Type::Int),
)),
body: vec![
Statement::WordCall {
name: "i.add".to_string(),
span: None,
},
Statement::WordCall {
name: "i.multiply".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_write_line_type_error() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Int),
StackType::Empty,
)),
body: vec![Statement::WordCall {
name: "io.write-line".to_string(),
span: None,
}],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Type mismatch"));
}
#[test]
fn test_stack_underflow_drop() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
body: vec![Statement::WordCall {
name: "drop".to_string(),
span: None,
}],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(result.is_err());
assert!(result.unwrap_err().contains("mismatch"));
}
#[test]
fn test_stack_underflow_add() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Int),
StackType::singleton(Type::Int),
)),
body: vec![Statement::WordCall {
name: "i.add".to_string(),
span: None,
}],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(result.is_err());
assert!(result.unwrap_err().contains("mismatch"));
}
#[test]
fn test_stack_underflow_rot_issue_169() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::RowVar("rest".to_string()),
StackType::RowVar("rest".to_string()),
)),
body: vec![
Statement::IntLiteral(3),
Statement::IntLiteral(4),
Statement::WordCall {
name: "rot".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(result.is_err(), "rot with 2 values should fail");
let err = result.unwrap_err();
assert!(
err.contains("stack underflow") || err.contains("requires 3"),
"Error should mention underflow: {}",
err
);
}
#[test]
fn test_csp_operations() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
body: vec![
Statement::WordCall {
name: "chan.make".to_string(),
span: None,
},
Statement::IntLiteral(42),
Statement::WordCall {
name: "swap".to_string(),
span: None,
},
Statement::WordCall {
name: "chan.send".to_string(),
span: None,
},
Statement::WordCall {
name: "drop".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_complex_stack_shuffling() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty
.push(Type::Int)
.push(Type::Int)
.push(Type::Int),
StackType::singleton(Type::Int),
)),
body: vec![
Statement::WordCall {
name: "rot".to_string(),
span: None,
},
Statement::WordCall {
name: "i.add".to_string(),
span: None,
},
Statement::WordCall {
name: "i.add".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_empty_program() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_word_without_effect_declaration() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "helper".to_string(),
effect: None,
body: vec![Statement::IntLiteral(42)],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.contains("missing a stack effect declaration")
);
}
#[test]
fn test_nested_conditionals() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty
.push(Type::Int)
.push(Type::Int)
.push(Type::Int)
.push(Type::Int),
StackType::singleton(Type::String),
)),
body: vec![
Statement::WordCall {
name: "i.>".to_string(),
span: None,
},
Statement::If {
then_branch: vec![
Statement::WordCall {
name: "i.>".to_string(),
span: None,
},
Statement::If {
then_branch: vec![Statement::StringLiteral(b"both true".to_vec())],
else_branch: Some(vec![Statement::StringLiteral(
b"first true".to_vec(),
)]),
span: None,
},
],
else_branch: Some(vec![
Statement::WordCall {
name: "drop".to_string(),
span: None,
},
Statement::WordCall {
name: "drop".to_string(),
span: None,
},
Statement::StringLiteral(b"first false".to_vec()),
]),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
match checker.check_program(&program) {
Ok(_) => {}
Err(e) => panic!("Type check failed: {}", e),
}
}
#[test]
fn test_conditional_without_else() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty.push(Type::Int).push(Type::Int),
StackType::singleton(Type::Int),
)),
body: vec![
Statement::WordCall {
name: "i.>".to_string(),
span: None,
},
Statement::If {
then_branch: vec![Statement::IntLiteral(100)],
else_branch: None, span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(result.is_err());
}
#[test]
fn test_multiple_word_chain() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![
WordDef {
name: "helper1".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Int),
StackType::singleton(Type::String),
)),
body: vec![Statement::WordCall {
name: "int->string".to_string(),
span: None,
}],
source: None,
allowed_lints: vec![],
},
WordDef {
name: "helper2".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::String),
StackType::Empty,
)),
body: vec![Statement::WordCall {
name: "io.write-line".to_string(),
span: None,
}],
source: None,
allowed_lints: vec![],
},
WordDef {
name: "main".to_string(),
effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
body: vec![
Statement::IntLiteral(42),
Statement::WordCall {
name: "helper1".to_string(),
span: None,
},
Statement::WordCall {
name: "helper2".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
},
],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_all_stack_ops() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty
.push(Type::Int)
.push(Type::Int)
.push(Type::Int),
StackType::Empty
.push(Type::Int)
.push(Type::Int)
.push(Type::Int)
.push(Type::Int),
)),
body: vec![
Statement::WordCall {
name: "over".to_string(),
span: None,
},
Statement::WordCall {
name: "nip".to_string(),
span: None,
},
Statement::WordCall {
name: "tuck".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_mixed_types_complex() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
body: vec![
Statement::IntLiteral(42),
Statement::WordCall {
name: "int->string".to_string(),
span: None,
},
Statement::IntLiteral(100),
Statement::IntLiteral(200),
Statement::WordCall {
name: "i.>".to_string(),
span: None,
},
Statement::If {
then_branch: vec![Statement::WordCall {
name: "io.write-line".to_string(),
span: None,
}],
else_branch: Some(vec![Statement::WordCall {
name: "io.write-line".to_string(),
span: None,
}]),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_string_literal() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty,
StackType::singleton(Type::String),
)),
body: vec![Statement::StringLiteral(b"hello".to_vec())],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_bool_literal() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty,
StackType::singleton(Type::Bool),
)),
body: vec![Statement::BoolLiteral(true)],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_type_error_in_nested_conditional() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
body: vec![
Statement::IntLiteral(10),
Statement::IntLiteral(20),
Statement::WordCall {
name: "i.>".to_string(),
span: None,
},
Statement::If {
then_branch: vec![
Statement::IntLiteral(42),
Statement::WordCall {
name: "io.write-line".to_string(),
span: None,
},
],
else_branch: Some(vec![
Statement::StringLiteral(b"ok".to_vec()),
Statement::WordCall {
name: "io.write-line".to_string(),
span: None,
},
]),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Type mismatch"));
}
#[test]
fn test_read_line_operation() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty,
StackType::from_vec(vec![Type::String, Type::Bool]),
)),
body: vec![Statement::WordCall {
name: "io.read-line".to_string(),
span: None,
}],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_comparison_operations() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty.push(Type::Int).push(Type::Int),
StackType::singleton(Type::Bool),
)),
body: vec![Statement::WordCall {
name: "i.<=".to_string(),
span: None,
}],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_recursive_word_definitions() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![
WordDef {
name: "is-even".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Int),
StackType::singleton(Type::Int),
)),
body: vec![
Statement::WordCall {
name: "dup".to_string(),
span: None,
},
Statement::IntLiteral(0),
Statement::WordCall {
name: "i.=".to_string(),
span: None,
},
Statement::If {
then_branch: vec![
Statement::WordCall {
name: "drop".to_string(),
span: None,
},
Statement::IntLiteral(1),
],
else_branch: Some(vec![
Statement::IntLiteral(1),
Statement::WordCall {
name: "i.subtract".to_string(),
span: None,
},
Statement::WordCall {
name: "is-odd".to_string(),
span: None,
},
]),
span: None,
},
],
source: None,
allowed_lints: vec![],
},
WordDef {
name: "is-odd".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Int),
StackType::singleton(Type::Int),
)),
body: vec![
Statement::WordCall {
name: "dup".to_string(),
span: None,
},
Statement::IntLiteral(0),
Statement::WordCall {
name: "i.=".to_string(),
span: None,
},
Statement::If {
then_branch: vec![
Statement::WordCall {
name: "drop".to_string(),
span: None,
},
Statement::IntLiteral(0),
],
else_branch: Some(vec![
Statement::IntLiteral(1),
Statement::WordCall {
name: "i.subtract".to_string(),
span: None,
},
Statement::WordCall {
name: "is-even".to_string(),
span: None,
},
]),
span: None,
},
],
source: None,
allowed_lints: vec![],
},
],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_word_calling_word_with_row_polymorphism() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![
WordDef {
name: "apply-twice".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Int),
StackType::singleton(Type::Int),
)),
body: vec![
Statement::WordCall {
name: "dup".to_string(),
span: None,
},
Statement::WordCall {
name: "i.add".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
},
WordDef {
name: "quad".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Int),
StackType::singleton(Type::Int),
)),
body: vec![
Statement::WordCall {
name: "apply-twice".to_string(),
span: None,
},
Statement::WordCall {
name: "apply-twice".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
},
],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_deep_stack_types() {
let mut stack_type = StackType::Empty;
for _ in 0..10 {
stack_type = stack_type.push(Type::Int);
}
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(stack_type, StackType::singleton(Type::Int))),
body: vec![
Statement::WordCall {
name: "i.add".to_string(),
span: None,
},
Statement::WordCall {
name: "i.add".to_string(),
span: None,
},
Statement::WordCall {
name: "i.add".to_string(),
span: None,
},
Statement::WordCall {
name: "i.add".to_string(),
span: None,
},
Statement::WordCall {
name: "i.add".to_string(),
span: None,
},
Statement::WordCall {
name: "i.add".to_string(),
span: None,
},
Statement::WordCall {
name: "i.add".to_string(),
span: None,
},
Statement::WordCall {
name: "i.add".to_string(),
span: None,
},
Statement::WordCall {
name: "i.add".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_simple_quotation() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty,
StackType::singleton(Type::Quotation(Box::new(Effect::new(
StackType::RowVar("input".to_string()).push(Type::Int),
StackType::RowVar("input".to_string()).push(Type::Int),
)))),
)),
body: vec![Statement::Quotation {
span: None,
id: 0,
body: vec![
Statement::IntLiteral(1),
Statement::WordCall {
name: "i.add".to_string(),
span: None,
},
],
}],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
match checker.check_program(&program) {
Ok(_) => {}
Err(e) => panic!("Type check failed: {}", e),
}
}
#[test]
fn test_empty_quotation() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty,
StackType::singleton(Type::Quotation(Box::new(Effect::new(
StackType::RowVar("input".to_string()),
StackType::RowVar("input".to_string()),
)))),
)),
body: vec![Statement::Quotation {
span: None,
id: 1,
body: vec![],
}],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_nested_quotation() {
let inner_quot_type = Type::Quotation(Box::new(Effect::new(
StackType::RowVar("input".to_string()).push(Type::Int),
StackType::RowVar("input".to_string()).push(Type::Int),
)));
let outer_quot_type = Type::Quotation(Box::new(Effect::new(
StackType::RowVar("input".to_string()),
StackType::RowVar("input".to_string()).push(inner_quot_type.clone()),
)));
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty,
StackType::singleton(outer_quot_type),
)),
body: vec![Statement::Quotation {
span: None,
id: 2,
body: vec![Statement::Quotation {
span: None,
id: 3,
body: vec![
Statement::IntLiteral(1),
Statement::WordCall {
name: "i.add".to_string(),
span: None,
},
],
}],
}],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_invalid_field_type_error() {
use crate::ast::{UnionDef, UnionField, UnionVariant};
let program = Program {
includes: vec![],
unions: vec![UnionDef {
name: "Message".to_string(),
variants: vec![UnionVariant {
name: "Get".to_string(),
fields: vec![UnionField {
name: "chan".to_string(),
type_name: "InvalidType".to_string(),
}],
source: None,
}],
source: None,
}],
words: vec![],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.contains("Unknown type 'InvalidType'"));
assert!(err.contains("chan"));
assert!(err.contains("Get"));
assert!(err.contains("Message"));
}
#[test]
fn test_roll_inside_conditional_with_concrete_stack() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty
.push(Type::Int)
.push(Type::Int)
.push(Type::Int)
.push(Type::Int),
StackType::Empty
.push(Type::Int)
.push(Type::Int)
.push(Type::Int)
.push(Type::Int),
)),
body: vec![
Statement::WordCall {
name: "dup".to_string(),
span: None,
},
Statement::IntLiteral(0),
Statement::WordCall {
name: "i.>".to_string(),
span: None,
},
Statement::If {
then_branch: vec![
Statement::IntLiteral(3),
Statement::WordCall {
name: "roll".to_string(),
span: None,
},
],
else_branch: Some(vec![
Statement::WordCall {
name: "rot".to_string(),
span: None,
},
Statement::WordCall {
name: "rot".to_string(),
span: None,
},
]),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
match checker.check_program(&program) {
Ok(_) => {}
Err(e) => panic!("Type check failed: {}", e),
}
}
#[test]
fn test_roll_inside_match_arm_with_concrete_stack() {
use crate::ast::{MatchArm, Pattern, UnionDef, UnionVariant};
let union_def = UnionDef {
name: "Result".to_string(),
variants: vec![
UnionVariant {
name: "Ok".to_string(),
fields: vec![],
source: None,
},
UnionVariant {
name: "Err".to_string(),
fields: vec![],
source: None,
},
],
source: None,
};
let program = Program {
includes: vec![],
unions: vec![union_def],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty
.push(Type::Int)
.push(Type::Int)
.push(Type::Int)
.push(Type::Int)
.push(Type::Union("Result".to_string())),
StackType::Empty
.push(Type::Int)
.push(Type::Int)
.push(Type::Int)
.push(Type::Int),
)),
body: vec![Statement::Match {
arms: vec![
MatchArm {
pattern: Pattern::Variant("Ok".to_string()),
body: vec![
Statement::IntLiteral(3),
Statement::WordCall {
name: "roll".to_string(),
span: None,
},
],
span: None,
},
MatchArm {
pattern: Pattern::Variant("Err".to_string()),
body: vec![
Statement::WordCall {
name: "rot".to_string(),
span: None,
},
Statement::WordCall {
name: "rot".to_string(),
span: None,
},
],
span: None,
},
],
span: None,
}],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
match checker.check_program(&program) {
Ok(_) => {}
Err(e) => panic!("Type check failed: {}", e),
}
}
#[test]
fn test_roll_with_row_polymorphic_input() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty
.push(Type::Var("T".to_string()))
.push(Type::Var("U".to_string()))
.push(Type::Var("V".to_string()))
.push(Type::Var("W".to_string())),
StackType::Empty
.push(Type::Var("U".to_string()))
.push(Type::Var("V".to_string()))
.push(Type::Var("W".to_string()))
.push(Type::Var("T".to_string())),
)),
body: vec![
Statement::IntLiteral(3),
Statement::WordCall {
name: "roll".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(result.is_ok(), "roll test failed: {:?}", result.err());
}
#[test]
fn test_pick_with_row_polymorphic_input() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty
.push(Type::Var("T".to_string()))
.push(Type::Var("U".to_string()))
.push(Type::Var("V".to_string())),
StackType::Empty
.push(Type::Var("T".to_string()))
.push(Type::Var("U".to_string()))
.push(Type::Var("V".to_string()))
.push(Type::Var("T".to_string())),
)),
body: vec![
Statement::IntLiteral(2),
Statement::WordCall {
name: "pick".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_valid_union_reference_in_field() {
use crate::ast::{UnionDef, UnionField, UnionVariant};
let program = Program {
includes: vec![],
unions: vec![
UnionDef {
name: "Inner".to_string(),
variants: vec![UnionVariant {
name: "Val".to_string(),
fields: vec![UnionField {
name: "x".to_string(),
type_name: "Int".to_string(),
}],
source: None,
}],
source: None,
},
UnionDef {
name: "Outer".to_string(),
variants: vec![UnionVariant {
name: "Wrap".to_string(),
fields: vec![UnionField {
name: "inner".to_string(),
type_name: "Inner".to_string(), }],
source: None,
}],
source: None,
},
],
words: vec![],
};
let mut checker = TypeChecker::new();
assert!(
checker.check_program(&program).is_ok(),
"Union reference in field should be valid"
);
}
#[test]
fn test_divergent_recursive_tail_call() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "store-loop".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Channel), StackType::Empty,
)),
body: vec![
Statement::WordCall {
name: "dup".to_string(),
span: None,
},
Statement::WordCall {
name: "chan.receive".to_string(),
span: None,
},
Statement::WordCall {
name: "not".to_string(),
span: None,
},
Statement::If {
then_branch: vec![
Statement::WordCall {
name: "drop".to_string(),
span: None,
},
Statement::WordCall {
name: "store-loop".to_string(), span: None,
},
],
else_branch: None, span: None,
},
Statement::WordCall {
name: "drop".to_string(),
span: None,
},
Statement::WordCall {
name: "drop".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(
result.is_ok(),
"Divergent recursive tail call should be accepted: {:?}",
result.err()
);
}
#[test]
fn test_divergent_else_branch() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "process-loop".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Channel), StackType::Empty,
)),
body: vec![
Statement::WordCall {
name: "dup".to_string(),
span: None,
},
Statement::WordCall {
name: "chan.receive".to_string(),
span: None,
},
Statement::If {
then_branch: vec![
Statement::WordCall {
name: "drop".to_string(),
span: None,
},
Statement::WordCall {
name: "drop".to_string(),
span: None,
},
],
else_branch: Some(vec![
Statement::WordCall {
name: "drop".to_string(),
span: None,
},
Statement::WordCall {
name: "process-loop".to_string(), span: None,
},
]),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(
result.is_ok(),
"Divergent else branch should be accepted: {:?}",
result.err()
);
}
#[test]
fn test_non_tail_call_recursion_not_divergent() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "bad-loop".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Int),
StackType::singleton(Type::Int),
)),
body: vec![
Statement::WordCall {
name: "dup".to_string(),
span: None,
},
Statement::IntLiteral(0),
Statement::WordCall {
name: "i.>".to_string(),
span: None,
},
Statement::If {
then_branch: vec![
Statement::IntLiteral(1),
Statement::WordCall {
name: "i.subtract".to_string(),
span: None,
},
Statement::WordCall {
name: "bad-loop".to_string(), span: None,
},
Statement::IntLiteral(1),
Statement::WordCall {
name: "i.add".to_string(), span: None,
},
],
else_branch: None,
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(
result.is_ok(),
"Non-tail recursion should type check normally: {:?}",
result.err()
);
}
#[test]
fn test_call_yield_quotation_error() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "bad".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Var("W".to_string())),
StackType::singleton(Type::Var("W".to_string())),
)),
body: vec![
Statement::IntLiteral(42),
Statement::Quotation {
span: None,
id: 0,
body: vec![Statement::WordCall {
name: "yield".to_string(),
span: None,
}],
},
Statement::WordCall {
name: "call".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(
result.is_err(),
"Calling yield quotation directly should fail"
);
let err = result.unwrap_err();
assert!(
err.contains("Yield") || err.contains("strand.weave"),
"Error should mention Yield or strand.weave: {}",
err
);
}
#[test]
fn test_strand_weave_yield_quotation_ok() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "good".to_string(),
effect: Some(Effect::new(
StackType::Empty,
StackType::Empty
.push(Type::Int)
.push(Type::Var("H".to_string())),
)),
body: vec![
Statement::IntLiteral(42),
Statement::Quotation {
span: None,
id: 0,
body: vec![Statement::WordCall {
name: "yield".to_string(),
span: None,
}],
},
Statement::WordCall {
name: "strand.weave".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(
result.is_ok(),
"strand.weave on yield quotation should pass: {:?}",
result.err()
);
}
#[test]
fn test_call_pure_quotation_ok() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "ok".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Int),
StackType::singleton(Type::Int),
)),
body: vec![
Statement::Quotation {
span: None,
id: 0,
body: vec![
Statement::IntLiteral(1),
Statement::WordCall {
name: "i.add".to_string(),
span: None,
},
],
},
Statement::WordCall {
name: "call".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(
result.is_ok(),
"Calling pure quotation should pass: {:?}",
result.err()
);
}
#[test]
fn test_pollution_extra_push() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Int),
StackType::singleton(Type::Int),
)),
body: vec![Statement::IntLiteral(42)],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(
result.is_err(),
"Should reject: declares ( Int -- Int ) but leaves 2 values on stack"
);
}
#[test]
fn test_pollution_extra_dup() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Int),
StackType::singleton(Type::Int),
)),
body: vec![Statement::WordCall {
name: "dup".to_string(),
span: None,
}],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(
result.is_err(),
"Should reject: declares ( Int -- Int ) but dup produces 2 values"
);
}
#[test]
fn test_pollution_consumes_extra() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Int),
StackType::singleton(Type::Int),
)),
body: vec![
Statement::WordCall {
name: "drop".to_string(),
span: None,
},
Statement::WordCall {
name: "drop".to_string(),
span: None,
},
Statement::IntLiteral(42),
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(
result.is_err(),
"Should reject: declares ( Int -- Int ) but consumes 2 values"
);
}
#[test]
fn test_pollution_in_then_branch() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Bool),
StackType::singleton(Type::Int),
)),
body: vec![Statement::If {
then_branch: vec![
Statement::IntLiteral(1),
Statement::IntLiteral(2), ],
else_branch: Some(vec![Statement::IntLiteral(3)]),
span: None,
}],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(
result.is_err(),
"Should reject: then branch pushes 2 values, else pushes 1"
);
}
#[test]
fn test_pollution_in_else_branch() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Bool),
StackType::singleton(Type::Int),
)),
body: vec![Statement::If {
then_branch: vec![Statement::IntLiteral(1)],
else_branch: Some(vec![
Statement::IntLiteral(2),
Statement::IntLiteral(3), ]),
span: None,
}],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(
result.is_err(),
"Should reject: then branch pushes 1 value, else pushes 2"
);
}
#[test]
fn test_pollution_both_branches_extra() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Bool),
StackType::singleton(Type::Int),
)),
body: vec![Statement::If {
then_branch: vec![Statement::IntLiteral(1), Statement::IntLiteral(2)],
else_branch: Some(vec![Statement::IntLiteral(3), Statement::IntLiteral(4)]),
span: None,
}],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(
result.is_err(),
"Should reject: both branches push 2 values, but declared output is 1"
);
}
#[test]
fn test_pollution_branch_consumes_extra() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty.push(Type::Bool).push(Type::Int),
StackType::singleton(Type::Int),
)),
body: vec![Statement::If {
then_branch: vec![
Statement::WordCall {
name: "drop".to_string(),
span: None,
},
Statement::WordCall {
name: "drop".to_string(),
span: None,
},
Statement::IntLiteral(1),
],
else_branch: Some(vec![]),
span: None,
}],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(
result.is_err(),
"Should reject: then branch consumes Bool (should only have Int after if)"
);
}
#[test]
fn test_pollution_quotation_wrong_arity_output() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Int),
StackType::singleton(Type::Int),
)),
body: vec![
Statement::Quotation {
span: None,
id: 0,
body: vec![Statement::WordCall {
name: "dup".to_string(),
span: None,
}],
},
Statement::WordCall {
name: "call".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(
result.is_err(),
"Should reject: quotation [dup] produces 2 values, declared output is 1"
);
}
#[test]
fn test_pollution_quotation_wrong_arity_input() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Int),
StackType::singleton(Type::Int),
)),
body: vec![
Statement::Quotation {
span: None,
id: 0,
body: vec![
Statement::WordCall {
name: "drop".to_string(),
span: None,
},
Statement::WordCall {
name: "drop".to_string(),
span: None,
},
Statement::IntLiteral(42),
],
},
Statement::WordCall {
name: "call".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(
result.is_err(),
"Should reject: quotation [drop drop 42] consumes 2 values, only 1 available"
);
}
#[test]
fn test_missing_effect_provides_helpful_error() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "myword".to_string(),
effect: None, body: vec![Statement::IntLiteral(42)],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.contains("myword"), "Error should mention word name");
assert!(
err.contains("stack effect"),
"Error should mention stack effect"
);
}
#[test]
fn test_valid_effect_exact_match() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty.push(Type::Int).push(Type::Int),
StackType::singleton(Type::Int),
)),
body: vec![Statement::WordCall {
name: "i.add".to_string(),
span: None,
}],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(result.is_ok(), "Should accept: effect matches exactly");
}
#[test]
fn test_valid_polymorphic_passthrough() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Cons {
rest: Box::new(StackType::RowVar("rest".to_string())),
top: Type::Var("a".to_string()),
},
StackType::Cons {
rest: Box::new(StackType::RowVar("rest".to_string())),
top: Type::Var("a".to_string()),
},
)),
body: vec![], source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(result.is_ok(), "Should accept: polymorphic identity");
}
#[test]
fn test_closure_basic_capture() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "make-adder".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Int),
StackType::singleton(Type::Closure {
effect: Box::new(Effect::new(
StackType::RowVar("r".to_string()).push(Type::Int),
StackType::RowVar("r".to_string()).push(Type::Int),
)),
captures: vec![Type::Int], }),
)),
body: vec![Statement::Quotation {
span: None,
id: 0,
body: vec![Statement::WordCall {
name: "i.add".to_string(),
span: None,
}],
}],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(
result.is_ok(),
"Basic closure capture should work: {:?}",
result.err()
);
}
#[test]
fn test_closure_nested_two_levels() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "outer".to_string(),
effect: Some(Effect::new(
StackType::Empty,
StackType::singleton(Type::Quotation(Box::new(Effect::new(
StackType::RowVar("r".to_string()),
StackType::RowVar("r".to_string()).push(Type::Quotation(Box::new(
Effect::new(
StackType::RowVar("s".to_string()).push(Type::Int),
StackType::RowVar("s".to_string()).push(Type::Int),
),
))),
)))),
)),
body: vec![Statement::Quotation {
span: None,
id: 0,
body: vec![Statement::Quotation {
span: None,
id: 1,
body: vec![
Statement::IntLiteral(1),
Statement::WordCall {
name: "i.add".to_string(),
span: None,
},
],
}],
}],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(
result.is_ok(),
"Two-level nested quotations should work: {:?}",
result.err()
);
}
#[test]
fn test_closure_nested_three_levels() {
let inner_effect = Effect::new(
StackType::RowVar("a".to_string()).push(Type::Int),
StackType::RowVar("a".to_string()).push(Type::Int),
);
let middle_effect = Effect::new(
StackType::RowVar("b".to_string()),
StackType::RowVar("b".to_string()).push(Type::Quotation(Box::new(inner_effect))),
);
let outer_effect = Effect::new(
StackType::RowVar("c".to_string()),
StackType::RowVar("c".to_string()).push(Type::Quotation(Box::new(middle_effect))),
);
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "deep".to_string(),
effect: Some(Effect::new(
StackType::Empty,
StackType::singleton(Type::Quotation(Box::new(outer_effect))),
)),
body: vec![Statement::Quotation {
span: None,
id: 0,
body: vec![Statement::Quotation {
span: None,
id: 1,
body: vec![Statement::Quotation {
span: None,
id: 2,
body: vec![
Statement::IntLiteral(1),
Statement::WordCall {
name: "i.add".to_string(),
span: None,
},
],
}],
}],
}],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(
result.is_ok(),
"Three-level nested quotations should work: {:?}",
result.err()
);
}
#[test]
fn test_closure_use_after_creation() {
let adder_type = Type::Closure {
effect: Box::new(Effect::new(
StackType::RowVar("r".to_string()).push(Type::Int),
StackType::RowVar("r".to_string()).push(Type::Int),
)),
captures: vec![Type::Int],
};
let program = Program {
includes: vec![],
unions: vec![],
words: vec![
WordDef {
name: "make-adder".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Int),
StackType::singleton(adder_type.clone()),
)),
body: vec![Statement::Quotation {
span: None,
id: 0,
body: vec![Statement::WordCall {
name: "i.add".to_string(),
span: None,
}],
}],
source: None,
allowed_lints: vec![],
},
WordDef {
name: "use-adder".to_string(),
effect: Some(Effect::new(
StackType::Empty,
StackType::singleton(Type::Int),
)),
body: vec![
Statement::IntLiteral(5),
Statement::WordCall {
name: "make-adder".to_string(),
span: None,
},
Statement::IntLiteral(10),
Statement::WordCall {
name: "swap".to_string(),
span: None,
},
Statement::WordCall {
name: "call".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
},
],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(
result.is_ok(),
"Closure usage after creation should work: {:?}",
result.err()
);
}
#[test]
fn test_closure_wrong_call_type() {
let adder_type = Type::Closure {
effect: Box::new(Effect::new(
StackType::RowVar("r".to_string()).push(Type::Int),
StackType::RowVar("r".to_string()).push(Type::Int),
)),
captures: vec![Type::Int],
};
let program = Program {
includes: vec![],
unions: vec![],
words: vec![
WordDef {
name: "make-adder".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Int),
StackType::singleton(adder_type.clone()),
)),
body: vec![Statement::Quotation {
span: None,
id: 0,
body: vec![Statement::WordCall {
name: "i.add".to_string(),
span: None,
}],
}],
source: None,
allowed_lints: vec![],
},
WordDef {
name: "bad-use".to_string(),
effect: Some(Effect::new(
StackType::Empty,
StackType::singleton(Type::Int),
)),
body: vec![
Statement::IntLiteral(5),
Statement::WordCall {
name: "make-adder".to_string(),
span: None,
},
Statement::StringLiteral(b"hello".to_vec()), Statement::WordCall {
name: "swap".to_string(),
span: None,
},
Statement::WordCall {
name: "call".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
},
],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(
result.is_err(),
"Calling Int closure with String should fail"
);
}
#[test]
fn test_closure_multiple_captures() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "make-between".to_string(),
effect: Some(Effect::new(
StackType::Empty.push(Type::Int).push(Type::Int),
StackType::singleton(Type::Quotation(Box::new(Effect::new(
StackType::RowVar("r".to_string()).push(Type::Int),
StackType::RowVar("r".to_string()).push(Type::Bool),
)))),
)),
body: vec![Statement::Quotation {
span: None,
id: 0,
body: vec![
Statement::WordCall {
name: "i.>=".to_string(),
span: None,
},
],
}],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(
result.is_ok() || result.is_err(),
"Multiple captures should be handled (pass or fail gracefully)"
);
}
#[test]
fn test_quotation_type_preserved_through_word() {
let quot_type = Type::Quotation(Box::new(Effect::new(
StackType::RowVar("r".to_string()).push(Type::Int),
StackType::RowVar("r".to_string()).push(Type::Int),
)));
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "identity-quot".to_string(),
effect: Some(Effect::new(
StackType::singleton(quot_type.clone()),
StackType::singleton(quot_type.clone()),
)),
body: vec![], source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(
result.is_ok(),
"Quotation type should be preserved through identity word: {:?}",
result.err()
);
}
#[test]
fn test_closure_captures_value_for_inner_quotation() {
let closure_effect = Effect::new(
StackType::RowVar("r".to_string()).push(Type::Int),
StackType::RowVar("r".to_string()).push(Type::Int),
);
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "make-inner-adder".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Int),
StackType::singleton(Type::Closure {
effect: Box::new(closure_effect),
captures: vec![Type::Int],
}),
)),
body: vec![Statement::Quotation {
span: None,
id: 0,
body: vec![
Statement::WordCall {
name: "i.add".to_string(),
span: None,
},
],
}],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(
result.is_ok(),
"Closure with capture for inner work should pass: {:?}",
result.err()
);
}
#[test]
fn test_union_type_mismatch_should_fail() {
use crate::ast::{UnionDef, UnionField, UnionVariant};
let mut program = Program {
includes: vec![],
unions: vec![
UnionDef {
name: "UnionA".to_string(),
variants: vec![UnionVariant {
name: "AVal".to_string(),
fields: vec![UnionField {
name: "x".to_string(),
type_name: "Int".to_string(),
}],
source: None,
}],
source: None,
},
UnionDef {
name: "UnionB".to_string(),
variants: vec![UnionVariant {
name: "BVal".to_string(),
fields: vec![UnionField {
name: "y".to_string(),
type_name: "Int".to_string(),
}],
source: None,
}],
source: None,
},
],
words: vec![
WordDef {
name: "takes-a".to_string(),
effect: Some(Effect::new(
StackType::RowVar("rest".to_string()).push(Type::Union("UnionA".to_string())),
StackType::RowVar("rest".to_string()),
)),
body: vec![Statement::WordCall {
name: "drop".to_string(),
span: None,
}],
source: None,
allowed_lints: vec![],
},
WordDef {
name: "main".to_string(),
effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
body: vec![
Statement::IntLiteral(99),
Statement::WordCall {
name: "Make-BVal".to_string(),
span: None,
},
Statement::WordCall {
name: "takes-a".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
},
],
};
program.generate_constructors().unwrap();
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(
result.is_err(),
"Passing UnionB to function expecting UnionA should fail, but got: {:?}",
result
);
let err = result.unwrap_err();
assert!(
err.contains("Union") || err.contains("mismatch"),
"Error should mention union type mismatch, got: {}",
err
);
}
fn make_word_call(name: &str) -> Statement {
Statement::WordCall {
name: name.to_string(),
span: None,
}
}
#[test]
fn test_aux_basic_round_trip() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Int),
StackType::singleton(Type::Int),
)),
body: vec![make_word_call(">aux"), make_word_call("aux>")],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_aux_preserves_type() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::String),
StackType::singleton(Type::String),
)),
body: vec![make_word_call(">aux"), make_word_call("aux>")],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_aux_unbalanced_error() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Int),
StackType::Empty,
)),
body: vec![make_word_call(">aux")],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.contains("aux stack is not empty"),
"Expected aux stack balance error, got: {}",
err
);
}
#[test]
fn test_aux_pop_empty_error() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty,
StackType::singleton(Type::Int),
)),
body: vec![make_word_call("aux>")],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.contains("aux stack is empty"),
"Expected aux empty error, got: {}",
err
);
}
#[test]
fn test_aux_multiple_values() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty.push(Type::Int).push(Type::String),
StackType::Empty.push(Type::Int).push(Type::String),
)),
body: vec![
make_word_call(">aux"),
make_word_call(">aux"),
make_word_call("aux>"),
make_word_call("aux>"),
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_aux_max_depths_tracked() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Int),
StackType::singleton(Type::Int),
)),
body: vec![make_word_call(">aux"), make_word_call("aux>")],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
checker.check_program(&program).unwrap();
let depths = checker.take_aux_max_depths();
assert_eq!(depths.get("test"), Some(&1));
}
#[test]
fn test_aux_in_match_balanced() {
use crate::ast::{MatchArm, Pattern, UnionDef, UnionVariant};
let union_def = UnionDef {
name: "Choice".to_string(),
variants: vec![
UnionVariant {
name: "Left".to_string(),
fields: vec![],
source: None,
},
UnionVariant {
name: "Right".to_string(),
fields: vec![],
source: None,
},
],
source: None,
};
let program = Program {
includes: vec![],
unions: vec![union_def],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty
.push(Type::Int)
.push(Type::Union("Choice".to_string())),
StackType::singleton(Type::Int),
)),
body: vec![
make_word_call("swap"),
make_word_call(">aux"),
Statement::Match {
arms: vec![
MatchArm {
pattern: Pattern::Variant("Left".to_string()),
body: vec![make_word_call("aux>")],
span: None,
},
MatchArm {
pattern: Pattern::Variant("Right".to_string()),
body: vec![make_word_call("aux>")],
span: None,
},
],
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_aux_in_match_unbalanced_error() {
use crate::ast::{MatchArm, Pattern, UnionDef, UnionVariant};
let union_def = UnionDef {
name: "Choice".to_string(),
variants: vec![
UnionVariant {
name: "Left".to_string(),
fields: vec![],
source: None,
},
UnionVariant {
name: "Right".to_string(),
fields: vec![],
source: None,
},
],
source: None,
};
let program = Program {
includes: vec![],
unions: vec![union_def],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty
.push(Type::Int)
.push(Type::Union("Choice".to_string())),
StackType::singleton(Type::Int),
)),
body: vec![
make_word_call("swap"),
make_word_call(">aux"),
Statement::Match {
arms: vec![
MatchArm {
pattern: Pattern::Variant("Left".to_string()),
body: vec![make_word_call("aux>")],
span: None,
},
MatchArm {
pattern: Pattern::Variant("Right".to_string()),
body: vec![],
span: None,
},
],
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.contains("aux stack"),
"Expected aux stack mismatch error, got: {}",
err
);
}
#[test]
fn test_aux_in_quotation_balanced_accepted() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty,
StackType::singleton(Type::Quotation(Box::new(Effect::new(
StackType::singleton(Type::Int),
StackType::singleton(Type::Int),
)))),
)),
body: vec![Statement::Quotation {
span: None,
id: 0,
body: vec![make_word_call(">aux"), make_word_call("aux>")],
}],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(
result.is_ok(),
"should accept balanced aux in quotation: {:?}",
result.err()
);
}
#[test]
fn test_aux_in_quotation_unbalanced_rejected() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty,
StackType::singleton(Type::Quotation(Box::new(Effect::new(
StackType::singleton(Type::Int),
StackType::Empty,
)))),
)),
body: vec![Statement::Quotation {
span: None,
id: 0,
body: vec![make_word_call(">aux")],
}],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.contains("unbalanced aux stack"),
"Expected unbalanced aux error, got: {}",
err
);
}
#[test]
fn test_dip_basic() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty.push(Type::Int).push(Type::Int),
StackType::Empty.push(Type::Int).push(Type::Int),
)),
body: vec![
Statement::Quotation {
id: 0,
body: vec![
Statement::IntLiteral(1),
Statement::WordCall {
name: "i.+".to_string(),
span: None,
},
],
span: None,
},
Statement::WordCall {
name: "dip".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_dip_type_mismatch() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty.push(Type::String).push(Type::Int),
StackType::Empty.push(Type::Int).push(Type::Int),
)),
body: vec![
Statement::Quotation {
id: 0,
body: vec![
Statement::IntLiteral(1),
Statement::WordCall {
name: "i.+".to_string(),
span: None,
},
],
span: None,
},
Statement::WordCall {
name: "dip".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_err());
}
#[test]
fn test_keep_basic() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Int),
StackType::Empty.push(Type::Int).push(Type::Int),
)),
body: vec![
Statement::Quotation {
id: 0,
body: vec![
Statement::WordCall {
name: "dup".to_string(),
span: None,
},
Statement::WordCall {
name: "i.*".to_string(),
span: None,
},
],
span: None,
},
Statement::WordCall {
name: "keep".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_bi_basic() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Int),
StackType::Empty.push(Type::Int).push(Type::Int),
)),
body: vec![
Statement::Quotation {
id: 0,
body: vec![
Statement::IntLiteral(2),
Statement::WordCall {
name: "i.*".to_string(),
span: None,
},
],
span: None,
},
Statement::Quotation {
id: 1,
body: vec![
Statement::IntLiteral(3),
Statement::WordCall {
name: "i.*".to_string(),
span: None,
},
],
span: None,
},
Statement::WordCall {
name: "bi".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_keep_type_mismatch() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::String),
StackType::Empty.push(Type::Int).push(Type::String),
)),
body: vec![
Statement::Quotation {
id: 0,
body: vec![
Statement::IntLiteral(1),
Statement::WordCall {
name: "i.+".to_string(),
span: None,
},
],
span: None,
},
Statement::WordCall {
name: "keep".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_err());
}
#[test]
fn test_bi_type_mismatch() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::String),
StackType::Empty.push(Type::Int).push(Type::Int),
)),
body: vec![
Statement::Quotation {
id: 0,
body: vec![Statement::WordCall {
name: "string.length".to_string(),
span: None,
}],
span: None,
},
Statement::Quotation {
id: 1,
body: vec![
Statement::IntLiteral(1),
Statement::WordCall {
name: "i.+".to_string(),
span: None,
},
],
span: None,
},
Statement::WordCall {
name: "bi".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_err());
}
#[test]
fn test_dip_underflow() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty,
StackType::singleton(Type::Int),
)),
body: vec![
Statement::Quotation {
id: 0,
body: vec![Statement::IntLiteral(1)],
span: None,
},
Statement::WordCall {
name: "dip".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.contains("stack underflow"),
"Expected underflow error, got: {}",
err
);
}
#[test]
fn test_dip_preserves_type() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty.push(Type::Int).push(Type::String),
StackType::Empty.push(Type::Int).push(Type::String),
)),
body: vec![
Statement::Quotation {
id: 0,
body: vec![
Statement::IntLiteral(1),
Statement::WordCall {
name: "i.+".to_string(),
span: None,
},
],
span: None,
},
Statement::WordCall {
name: "dip".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_keep_underflow() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
body: vec![
Statement::Quotation {
id: 0,
body: vec![Statement::WordCall {
name: "drop".to_string(),
span: None,
}],
span: None,
},
Statement::WordCall {
name: "keep".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.contains("stack underflow") || err.contains("underflow"),
"Expected underflow error, got: {}",
err
);
}
#[test]
fn test_bi_underflow() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
body: vec![
Statement::Quotation {
id: 0,
body: vec![Statement::IntLiteral(1)],
span: None,
},
Statement::Quotation {
id: 1,
body: vec![Statement::IntLiteral(2)],
span: None,
},
Statement::WordCall {
name: "bi".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.contains("stack underflow") || err.contains("underflow"),
"Expected underflow error, got: {}",
err
);
}
fn check_word_with_body(
name: &str,
inputs: StackType,
outputs: StackType,
body: Vec<Statement>,
) -> Result<(), String> {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: name.to_string(),
effect: Some(Effect::new(inputs, outputs)),
body,
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
checker.check_program(&program).map(|_| ())
}
fn variant_call(name: &str) -> Statement {
Statement::WordCall {
name: name.to_string(),
span: None,
}
}
#[test]
fn test_variant_field_at_rejects_string() {
let result = check_word_with_body(
"bad",
StackType::Empty,
StackType::singleton(Type::Int),
vec![
Statement::StringLiteral(b"alpha beta".to_vec()),
Statement::IntLiteral(0),
variant_call("variant.field-at"),
],
);
let err = result.expect_err("expected type error for String -> variant.field-at");
assert!(
err.contains("variant.field-at") && err.contains("String"),
"error should name variant.field-at and String, got: {err}"
);
}
#[test]
fn test_variant_tag_rejects_int() {
let result = check_word_with_body(
"bad",
StackType::Empty,
StackType::singleton(Type::Symbol),
vec![Statement::IntLiteral(7), variant_call("variant.tag")],
);
let err = result.expect_err("expected type error for Int -> variant.tag");
assert!(err.contains("variant.tag"));
}
#[test]
fn test_variant_field_count_rejects_string() {
let result = check_word_with_body(
"bad",
StackType::Empty,
StackType::singleton(Type::Int),
vec![
Statement::StringLiteral(b"x".to_vec()),
variant_call("variant.field-count"),
],
);
let err = result.expect_err("expected type error for String -> variant.field-count");
assert!(
err.contains("variant.field-count"),
"error should name variant.field-count, got: {err}"
);
}
#[test]
fn test_variant_init_rejects_string() {
let result = check_word_with_body(
"bad",
StackType::Empty,
StackType::singleton(Type::Variant),
vec![
Statement::StringLiteral(b"x".to_vec()),
variant_call("variant.init"),
],
);
let err = result.expect_err("expected type error for String -> variant.init");
assert!(
err.contains("variant.init"),
"error should name variant.init, got: {err}"
);
}
#[test]
fn test_variant_append_rejects_string_base() {
let result = check_word_with_body(
"bad",
StackType::Empty,
StackType::singleton(Type::Variant),
vec![
Statement::StringLiteral(b"x".to_vec()),
Statement::IntLiteral(1),
variant_call("variant.append"),
],
);
let err = result.expect_err("expected type error for String -> variant.append");
assert!(
err.contains("variant.append"),
"error should name variant.append, got: {err}"
);
}
#[test]
fn test_variant_last_rejects_string() {
let result = check_word_with_body(
"bad",
StackType::Empty,
StackType::Empty.push(Type::Var("T".to_string())),
vec![
Statement::StringLiteral(b"x".to_vec()),
variant_call("variant.last"),
],
);
let err = result.expect_err("expected type error for String -> variant.last");
assert!(
err.contains("variant.last"),
"error should name variant.last, got: {err}"
);
}
#[test]
fn test_union_value_accepted_by_variant_field_at() {
let union_def = crate::ast::UnionDef {
name: "Box".to_string(),
variants: vec![crate::ast::UnionVariant {
name: "Cell".to_string(),
fields: vec![crate::ast::UnionField {
name: "x".to_string(),
type_name: "Int".to_string(),
}],
source: None,
}],
source: None,
};
let program = Program {
includes: vec![],
unions: vec![union_def],
words: vec![WordDef {
name: "first".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Union("Box".to_string())),
StackType::singleton(Type::Int),
)),
body: vec![Statement::IntLiteral(0), variant_call("variant.field-at")],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
checker
.check_program(&program)
.expect("Union(Box) should be accepted by variant.field-at");
}
#[test]
fn test_variant_make_then_field_at_typechecks() {
let result = check_word_with_body(
"ok",
StackType::Empty,
StackType::singleton(Type::Int),
vec![
Statement::IntLiteral(42),
Statement::Symbol("Foo".to_string()),
variant_call("variant.make-1"),
Statement::IntLiteral(0),
variant_call("variant.field-at"),
],
);
assert!(
result.is_ok(),
"variant.make-1 -> variant.field-at should typecheck, got: {:?}",
result
);
}
#[test]
fn test_bi_polymorphic_quotations() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Int),
StackType::Empty.push(Type::Int).push(Type::String),
)),
body: vec![
Statement::Quotation {
id: 0,
body: vec![
Statement::IntLiteral(2),
Statement::WordCall {
name: "i.*".to_string(),
span: None,
},
],
span: None,
},
Statement::Quotation {
id: 1,
body: vec![Statement::WordCall {
name: "int->string".to_string(),
span: None,
}],
span: None,
},
Statement::WordCall {
name: "bi".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_if_combinator_basic() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Bool),
StackType::singleton(Type::Int),
)),
body: vec![
Statement::Quotation {
id: 0,
body: vec![Statement::IntLiteral(1)],
span: None,
},
Statement::Quotation {
id: 1,
body: vec![Statement::IntLiteral(2)],
span: None,
},
Statement::WordCall {
name: "if".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_if_combinator_branch_mismatch() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Bool),
StackType::singleton(Type::Int),
)),
body: vec![
Statement::Quotation {
id: 0,
body: vec![Statement::IntLiteral(1)],
span: None,
},
Statement::Quotation {
id: 1,
body: vec![Statement::StringLiteral(b"string".to_vec())],
span: None,
},
Statement::WordCall {
name: "if".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.contains("incompatible") || err.contains("unify"),
"Expected branch-mismatch error, got: {}",
err
);
}
#[test]
fn test_nested_dip_in_dip_body() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty
.push(Type::Int)
.push(Type::Int)
.push(Type::Int),
StackType::Empty
.push(Type::Int)
.push(Type::Int)
.push(Type::Int),
)),
body: vec![
Statement::Quotation {
id: 0,
body: vec![
Statement::Quotation {
id: 1,
body: vec![
Statement::IntLiteral(1),
Statement::WordCall {
name: "i.+".to_string(),
span: None,
},
],
span: None,
},
Statement::WordCall {
name: "dip".to_string(),
span: None,
},
],
span: None,
},
Statement::WordCall {
name: "dip".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_keep_inside_dip_body() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty.push(Type::Int).push(Type::Int),
StackType::Empty
.push(Type::Int)
.push(Type::Int)
.push(Type::Int),
)),
body: vec![
Statement::Quotation {
id: 0,
body: vec![
Statement::Quotation {
id: 1,
body: vec![
Statement::IntLiteral(1),
Statement::WordCall {
name: "i.+".to_string(),
span: None,
},
],
span: None,
},
Statement::WordCall {
name: "keep".to_string(),
span: None,
},
],
span: None,
},
Statement::WordCall {
name: "dip".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_dip_inside_keep_body() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty.push(Type::Int).push(Type::Int),
StackType::Empty
.push(Type::Int)
.push(Type::Int)
.push(Type::Int),
)),
body: vec![
Statement::Quotation {
id: 0,
body: vec![
Statement::Quotation {
id: 1,
body: vec![
Statement::IntLiteral(1),
Statement::WordCall {
name: "i.+".to_string(),
span: None,
},
],
span: None,
},
Statement::WordCall {
name: "dip".to_string(),
span: None,
},
],
span: None,
},
Statement::WordCall {
name: "keep".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_bi_inside_dip_body() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::Empty.push(Type::Int).push(Type::Int),
StackType::Empty
.push(Type::Int)
.push(Type::Int)
.push(Type::Int),
)),
body: vec![
Statement::Quotation {
id: 0,
body: vec![
Statement::Quotation {
id: 1,
body: vec![
Statement::IntLiteral(1),
Statement::WordCall {
name: "i.+".to_string(),
span: None,
},
],
span: None,
},
Statement::Quotation {
id: 2,
body: vec![
Statement::IntLiteral(2),
Statement::WordCall {
name: "i.+".to_string(),
span: None,
},
],
span: None,
},
Statement::WordCall {
name: "bi".to_string(),
span: None,
},
],
span: None,
},
Statement::WordCall {
name: "dip".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
assert!(checker.check_program(&program).is_ok());
}
#[test]
fn test_dip_rigid_rest_underflow_still_rejected() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(StackType::Empty, StackType::Empty)),
body: vec![
Statement::Quotation {
id: 0,
body: vec![],
span: None,
},
Statement::WordCall {
name: "dip".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.contains("stack underflow"),
"Expected underflow error at rigid signature boundary, got: {}",
err
);
}
#[test]
fn test_if_combinator_non_bool_condition() {
let program = Program {
includes: vec![],
unions: vec![],
words: vec![WordDef {
name: "test".to_string(),
effect: Some(Effect::new(
StackType::singleton(Type::Int),
StackType::singleton(Type::Int),
)),
body: vec![
Statement::Quotation {
id: 0,
body: vec![Statement::IntLiteral(1)],
span: None,
},
Statement::Quotation {
id: 1,
body: vec![Statement::IntLiteral(2)],
span: None,
},
Statement::WordCall {
name: "if".to_string(),
span: None,
},
],
source: None,
allowed_lints: vec![],
}],
};
let mut checker = TypeChecker::new();
let result = checker.check_program(&program);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.contains("Bool") || err.contains("condition"),
"Expected non-Bool condition error, got: {}",
err
);
}