ilo 26.5.0

ilo - the token-minimal programming language AI agents write
Documentation
// Regression tests: suggestion/hint coverage for 7 zero-coverage diagnostic codes.
//
// Codes exercised: ILO-T017, ILO-P007, ILO-P009, ILO-T003, ILO-T008, ILO-T009, ILO-T020
//
// Each test confirms:
//   (a) the correct error code fires
//   (b) the suggestion field is non-empty (not the empty string / not missing)
//
// Parser-layer codes (P007, P009) are exercised via the CLI `--parse` path;
// verifier-layer codes (T003, T008, T009, T017, T020) via `--verify`.
// Both share the same code paths across tree/VM/Cranelift since only the
// front-end changes.

use std::process::Command;

fn ilo() -> Command {
    Command::new(env!("CARGO_BIN_EXE_ilo"))
}

fn stderr_of(args: &[&str]) -> String {
    let out = ilo().args(args).output().expect("failed to run ilo");
    String::from_utf8_lossy(&out.stderr).into_owned()
}

// ---------------------------------------------------------------------------
// ILO-P007: expected type annotation, got token
// e.g. `f x: > n; x` — colon with no type before the arrow
// ---------------------------------------------------------------------------
#[test]
fn ilo_p007_has_suggestion() {
    // `f x:42>n;x` — 42 is not a type
    let src = "f x:42>n;x";
    let stderr = stderr_of(&[src, "--vm", "f", "1"]);
    assert!(
        stderr.contains("ILO-P007"),
        "expected ILO-P007, got: {stderr}"
    );
    assert!(
        stderr.contains("valid types")
            || stderr.contains("type annotation")
            || stderr.contains("n, t, b"),
        "expected suggestion text in ILO-P007 output, got: {stderr}"
    );
}

// ---------------------------------------------------------------------------
// ILO-P009: expected expression, got token
// bare semicolon as function body
// ---------------------------------------------------------------------------
#[test]
fn ilo_p009_generic_has_suggestion() {
    // `f x:n>n;)` — `)` is not an expression
    let src = "f x:n>n;)";
    let stderr = stderr_of(&[src, "--vm", "f", "1"]);
    assert!(
        stderr.contains("ILO-P009"),
        "expected ILO-P009, got: {stderr}"
    );
    // Should have a suggestion (literal, variable name, or function call hint)
    assert!(
        stderr.contains("literal")
            || stderr.contains("variable")
            || stderr.contains("expression")
            || stderr.contains("value"),
        "expected suggestion text in ILO-P009 output, got: {stderr}"
    );
}

#[test]
fn ilo_p009_f_type_no_return_has_suggestion() {
    // `f x:F>n;x` — F type with no return type specified
    let src = "f x:F>n;x";
    let stderr = stderr_of(&[src, "--vm", "f", "1"]);
    assert!(
        stderr.contains("ILO-P009"),
        "expected ILO-P009 for F with no return type, got: {stderr}"
    );
    assert!(
        stderr.contains("return type") || stderr.contains("F n") || stderr.contains("F "),
        "expected F-type suggestion in ILO-P009 output, got: {stderr}"
    );
}

// ---------------------------------------------------------------------------
// ILO-T003: undefined type (in signature) + ternary branch mismatch
// ---------------------------------------------------------------------------
#[test]
fn ilo_t003_undefined_type_has_suggestion() {
    // undefined type 'mytype' in signature (no such type declared)
    let src = "f x:mytype>n;42";
    let stderr = stderr_of(&[src, "--vm", "f"]);
    assert!(
        stderr.contains("ILO-T003"),
        "expected ILO-T003, got: {stderr}"
    );
    // The closest-match path fires when types exist; when none, we just get the undefined type msg.
    // Any output with ILO-T003 is acceptable — presence check is sufficient for code coverage.
    // But if there's a suggestion, it must be non-empty.
    if stderr.contains("suggestion:") || stderr.contains("hint:") {
        assert!(
            !stderr.trim_end().ends_with("suggestion:"),
            "suggestion field is empty in ILO-T003 output: {stderr}"
        );
    }
}

#[test]
fn ilo_t003_ternary_mismatch_has_suggestion() {
    // ternary where then=n, else=t
    let src = "f x:b>n;?h x 1 \"text\"";
    let stderr = stderr_of(&[src, "--vm", "f", "true"]);
    assert!(
        stderr.contains("ILO-T003"),
        "expected ILO-T003 for ternary mismatch, got: {stderr}"
    );
    assert!(
        stderr.contains("same type") || stderr.contains("branches") || stderr.contains("ternary"),
        "expected ternary-mismatch suggestion in ILO-T003 output, got: {stderr}"
    );
}

// ---------------------------------------------------------------------------
// ILO-T008: return type mismatch (fallback path, non n/t swap)
// ---------------------------------------------------------------------------
#[test]
fn ilo_t008_return_mismatch_has_suggestion() {
    // f returns b but declares n - hits the fallback _ => Some(...)
    let src = "f x:n>n;true";
    let stderr = stderr_of(&[src, "--vm", "f", "1"]);
    assert!(
        stderr.contains("ILO-T008"),
        "expected ILO-T008, got: {stderr}"
    );
    assert!(
        stderr.contains("return") || stderr.contains("annotation") || stderr.contains("change"),
        "expected suggestion text in ILO-T008 output, got: {stderr}"
    );
}

// ---------------------------------------------------------------------------
// ILO-T009: arithmetic type error — destructure non-record
// ---------------------------------------------------------------------------
#[test]
fn ilo_t009_destructure_non_record_has_suggestion() {
    // destructuring a number — {a;b}=x where x:n — is a type error
    let src = "f x:n>n;{a;b}=x;+a b";
    let stderr = stderr_of(&[src, "--vm", "f", "1"]);
    assert!(
        stderr.contains("ILO-T009"),
        "expected ILO-T009 for destructure, got: {stderr}"
    );
    assert!(
        stderr.contains("record") || stderr.contains("named") || stderr.contains("destructur"),
        "expected destructure suggestion in ILO-T009 output, got: {stderr}"
    );
}

// ---------------------------------------------------------------------------
// ILO-T017: record field type mismatch
// ---------------------------------------------------------------------------
#[test]
fn ilo_t017_field_type_mismatch_has_suggestion() {
    // type pt { x:n y:n }; providing text for x (which expects n)
    let src = "type pt{x:n;y:n}f>pt;pt x:\"bad\" y:0";
    let stderr = stderr_of(&[src, "--vm", "f"]);
    assert!(
        stderr.contains("ILO-T017"),
        "expected ILO-T017, got: {stderr}"
    );
    assert!(
        stderr.contains("provide") || stderr.contains("value for") || stderr.contains("n "),
        "expected suggestion in ILO-T017 output, got: {stderr}"
    );
}

// ---------------------------------------------------------------------------
// ILO-T020: 'with' on non-record type
// ---------------------------------------------------------------------------
#[test]
fn ilo_t020_with_non_record_has_suggestion() {
    // `with` on a number — `x with field:1` where x:n
    let src = "f x:n>n;y=x with field:1;y";
    let stderr = stderr_of(&[src, "--vm", "f", "1"]);
    assert!(
        stderr.contains("ILO-T020"),
        "expected ILO-T020, got: {stderr}"
    );
    assert!(
        stderr.contains("record") || stderr.contains("named") || stderr.contains("with"),
        "expected suggestion in ILO-T020 output, got: {stderr}"
    );
}