use super::*;
use crate::Parser;
use harn_lexer::Lexer;
#[test]
fn top_level_bindings_replace_forward_placeholders_with_real_types() {
let errs = errors(
r"
let x: int = 1
let y: string = x
",
);
assert_eq!(errs.len(), 1, "expected top-level mismatch, got: {errs:?}");
assert!(errs[0].contains("expected string, found int"), "{errs:?}");
}
#[test]
fn unconstrained_generic_param_cannot_flow_to_concrete_return() {
let errs = errors(
r"
fn bad<T>(x: T) -> int {
return x
}
",
);
assert_eq!(
errs.len(),
1,
"expected generic return mismatch, got: {errs:?}"
);
assert!(errs[0].contains("expected int, found T"), "{errs:?}");
}
#[test]
fn concrete_value_cannot_flow_to_unconstrained_generic_return() {
let errs = errors(
r"
fn bad<T>() -> T {
return 1
}
",
);
assert_eq!(
errs.len(),
1,
"expected generic return mismatch, got: {errs:?}"
);
assert!(errs[0].contains("expected T, found int"), "{errs:?}");
}
#[test]
fn bare_return_rejects_non_nil_return_type() {
let errs = errors(
r"
fn bad() -> int {
return
}
",
);
assert_eq!(
errs.len(),
1,
"expected bare-return mismatch, got: {errs:?}"
);
assert!(errs[0].contains("expected int, found nil"), "{errs:?}");
}
#[test]
fn non_nil_return_type_rejects_fallthrough() {
let errs = errors(
r"
fn bad() -> int {
let x = 1
}
",
);
assert_eq!(
errs.len(),
1,
"expected missing-return error, got: {errs:?}"
);
assert!(
errs[0].contains("can fall through without returning int"),
"{errs:?}"
);
}
#[test]
fn partial_return_path_rejects_fallthrough() {
let errs = errors(
r"
fn bad(flag: bool) -> int {
if flag {
return 1
}
}
",
);
assert_eq!(
errs.len(),
1,
"expected missing-return error, got: {errs:?}"
);
assert!(
errs[0].contains("can fall through without returning int"),
"{errs:?}"
);
}
#[test]
fn typed_pipeline_checks_final_expression_type() {
let errs = errors(
r#"
pipeline test(task) -> int {
"wrong"
}
"#,
);
assert_eq!(
errs.len(),
1,
"expected pipeline result mismatch, got: {errs:?}"
);
assert!(
errs[0].contains("pipeline result: expected int, found string"),
"{errs:?}"
);
}
#[test]
fn typed_pipeline_rejects_nil_fallthrough() {
let errs = errors(
r"
pipeline test(task) -> int {
let x = 1
}
",
);
assert_eq!(
errs.len(),
1,
"expected pipeline nil result mismatch, got: {errs:?}"
);
assert!(
errs[0].contains("pipeline result: expected int, found nil"),
"{errs:?}"
);
}
#[test]
fn tool_body_final_expression_satisfies_return_type() {
let errs = errors(
r#"
pipeline test(task) {
tool greet(name: string) -> string {
"Hello, " + name
}
}
"#,
);
assert!(errs.is_empty(), "unexpected tool result error: {errs:?}");
}
#[test]
fn tool_body_final_expression_type_is_checked() {
let errs = errors(
r#"
pipeline test(task) {
tool bad() -> int {
"wrong"
}
}
"#,
);
assert_eq!(
errs.len(),
1,
"expected tool result mismatch, got: {errs:?}"
);
assert!(
errs[0].contains("tool result: expected int, found string"),
"{errs:?}"
);
}
#[test]
fn return_type_checks_recurse_into_match_arms() {
let errs = errors(
r#"
fn bad(value: string) -> int {
match value {
_ -> { return "wrong" }
}
}
"#,
);
assert_eq!(
errs.len(),
1,
"expected match-arm return mismatch, got: {errs:?}"
);
assert!(errs[0].contains("expected int, found string"), "{errs:?}");
}
#[test]
fn return_type_checks_recurse_into_try_expr_body() {
let errs = errors(
r#"
fn bad() -> int {
try {
return "wrong"
}
}
"#,
);
assert_eq!(
errs.len(),
1,
"expected try-body return mismatch, got: {errs:?}"
);
assert!(errs[0].contains("expected int, found string"), "{errs:?}");
}
#[test]
fn exhaustive_match_return_arms_satisfy_return_type() {
let errs = errors(
r#"
type Verdict = "pass" | "fail"
fn classify(v: Verdict) -> string {
match v {
"pass" -> { return "ok" }
"fail" -> { return "no" }
}
}
"#,
);
assert!(
errs.is_empty(),
"unexpected exhaustive-match error: {errs:?}"
);
}
#[test]
fn exhaustive_match_return_arms_prevent_later_fallthrough() {
let errs = errors(
r#"
type Verdict = "pass" | "fail"
fn classify(v: Verdict) -> string {
match v {
"pass" -> { return "ok" }
"fail" -> { return "no" }
}
log("unreachable")
}
"#,
);
assert!(
errs.is_empty(),
"unexpected fallthrough after exhaustive match: {errs:?}"
);
}
#[test]
fn non_exhaustive_match_return_arms_do_not_hide_fallthrough() {
let errs = errors(
r#"
fn classify(v: string) -> int {
match v {
"one" -> { return 1 }
}
}
"#,
);
assert_eq!(
errs.len(),
1,
"expected missing-return error, got: {errs:?}"
);
assert!(
errs[0].contains("can fall through without returning int"),
"{errs:?}"
);
}
#[test]
fn generic_enum_match_requires_all_variants() {
let errs = errors(
r"
fn unwrap_ok<T, E>(result: Result<T, E>) -> T {
match result {
Result.Ok(value) -> { return value }
}
}
",
);
assert!(
errs.iter()
.any(|err| err.contains("Non-exhaustive match") && err.contains("\"Err\"")),
"expected generic enum exhaustiveness error, got: {errs:?}"
);
}
#[test]
fn never_tail_expression_satisfies_return_type() {
let errs = errors(
r#"
fn exhaustive(x: string | int) -> string {
if type_of(x) == "string" {
return "string"
}
if type_of(x) == "int" {
return "int"
}
unreachable(x)
}
"#,
);
assert!(errs.is_empty(), "unexpected never-tail error: {errs:?}");
}
#[test]
fn schema_typed_llm_call_data_flows_through_generic_wrapper() {
let errs = errors(
r#"
type GraderOut = {verdict: "pass" | "fail", summary: string}
fn grade<T>(schema: Schema<T>) -> T {
let r = llm_call("Grade this", nil, {output_schema: schema, output_validation: "error"})
return r.data
}
fn use_grade() -> GraderOut {
return grade(schema_of(GraderOut))
}
"#,
);
assert!(errs.is_empty(), "unexpected schema wrapper error: {errs:?}");
}
#[test]
fn schema_typed_llm_call_data_stays_optional_without_error_validation() {
let errs = errors(
r#"
type GraderOut = {verdict: "pass" | "fail", summary: string}
fn grade<T>(schema: Schema<T>) -> T {
let r = llm_call("Grade this", nil, {output_schema: schema})
return r.data
}
"#,
);
assert!(
errs.iter().any(|err| err.contains("expected T, found T?")),
"expected optional data mismatch, got: {errs:?}"
);
}
#[test]
fn check_strict_with_source_enables_strict_mode() {
let source = r#"pipeline t(task) {
let data = json_parse("{}")
log(data.name)
}"#;
let mut lexer = Lexer::new(source);
let tokens = lexer.tokenize().unwrap();
let mut parser = Parser::new(tokens);
let program = parser.parse().unwrap();
let diagnostics = TypeChecker::new().check_strict_with_source(&program, source);
assert!(
diagnostics
.iter()
.any(|diagnostic| diagnostic.message.contains("unvalidated")),
"expected strict unvalidated warning, got: {diagnostics:?}"
);
}