pd-vm 0.22.2

RustScript bytecode compiler and VM
Documentation
use std::fs;
use std::time::{SystemTime, UNIX_EPOCH};

use vm::{
    ParseError, SourceError, SourceFlavor, SourceMap, SourcePathError, Span, Vm,
    collect_inferred_local_type_hints, compile_source, compile_source_file,
    lint_unknown_inferred_local_types, render_compile_error, render_source_error, render_vm_error,
};

#[test]
fn render_source_error_highlights_exact_range() {
    let source = "let value = 123;\nlet x = ;\n";
    let mut source_map = SourceMap::new();
    let source_id = source_map.add_source("inline.rss", source);
    let lo = source_map
        .line_col_to_offset(source_id, 2, 9)
        .expect("line/col offset should exist");
    let hi = lo + 1;

    let err = ParseError {
        line: 2,
        message: "expected expression".to_string(),
        span: Some(Span::new(source_id, lo, hi)),
        code: Some("E_PARSE".to_string()),
    };

    let rendered = render_source_error(&source_map, &err, false);
    assert!(rendered.contains("expected expression"));
    assert!(rendered.contains("inline.rss:2:9"));
    assert!(rendered.contains("2 | let x = ;"));
    assert!(rendered.contains("^"));
}

#[test]
fn render_compile_error_highlights_if_else_mismatch_line() {
    let source =
        "let cond = 1 == 1;\nlet value = if cond => {\n    1\n} else => {\n    \"x\"\n};\n";

    let err = match compile_source(source) {
        Ok(_) => panic!("compile should reject if/else mismatch"),
        Err(err) => err,
    };
    let compile = match err {
        SourceError::Compile(compile) => compile,
        other => panic!("expected compile error, got {other:?}"),
    };

    let line = compile
        .line()
        .expect("if/else mismatch should report a line");
    let mut source_map = SourceMap::new();
    let source_id = source_map.add_source("inline.rss", source);
    let line_text = source_map
        .file(source_id)
        .and_then(|file| file.line_text(line))
        .expect("line text should exist");
    let rendered = render_compile_error(&source_map, &compile, false);

    assert!(rendered.contains("if/else branches produced incompatible expression result"));
    assert!(rendered.contains("int vs string"));
    assert!(rendered.contains(&format!("inline.rss:{line}:1")));
    assert!(rendered.contains(line_text));
}

#[test]
fn compile_error_from_imported_module_reports_module_path() {
    let unique = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("system time should be monotonic")
        .as_nanos();
    let root = std::env::temp_dir().join(format!("pd_vm_diag_import_{unique}"));
    fs::create_dir_all(&root).expect("temp module root should be writable");

    let module_path = root.join("module.rss");
    let module_source = r#"let cond = 1 == 1;
let broken = if cond => {
    1
} else => {
    "x"
};

pub fn ok() {
    0;
}
"#;
    fs::write(&module_path, module_source).expect("module source should be writable");

    let main_path = root.join("main.rss");
    fs::write(&main_path, "use module;\nok();\n").expect("main source should be writable");

    let result = compile_source_file(main_path.as_path());

    let _ = fs::remove_file(&main_path);
    let _ = fs::remove_file(&module_path);
    let _ = fs::remove_dir(&root);

    match result {
        Err(SourcePathError::Source(SourceError::Compile(compile))) => {
            assert_eq!(
                compile.source_name(),
                Some(module_path.to_string_lossy().as_ref())
            );
            assert_eq!(compile.line(), Some(2));

            let mut source_map = SourceMap::new();
            source_map.add_source(module_path.display().to_string(), module_source);
            let rendered = render_compile_error(&source_map, &compile, false);
            assert!(rendered.contains(&format!("{}:2:1", module_path.display())));
            assert!(rendered.contains("let broken = if cond => {"));
            assert!(rendered.contains("int vs string"));
        }
        _ => panic!("expected compile error"),
    }
}

#[test]
fn render_vm_error_includes_ip_and_source_line() {
    let source = "let value = 1 / 0;\n";
    let compiled = compile_source(source).expect("source should compile");
    let mut vm = Vm::new(compiled.program);
    let err = vm
        .run()
        .expect_err("runtime should fail with division by zero");

    let rendered = render_vm_error(&vm, &err);
    assert!(rendered.contains("runtime error"));
    assert!(rendered.contains("at ip"));
    assert!(rendered.contains("line 1"));
    assert!(rendered.contains("let value = 1 / 0;"));
}

#[test]
fn render_compile_error_highlights_invalid_schema_field_access_line() {
    let source = "struct User { name: string }\nlet user: User = { name: \"Ada\" };\nuser.age;\n";

    let err = match compile_source(source) {
        Ok(_) => panic!("compile should reject invalid schema field access"),
        Err(err) => err,
    };
    let compile = match err {
        SourceError::Compile(compile) => compile,
        other => panic!("expected compile error, got {other:?}"),
    };

    let line = compile
        .line()
        .expect("schema field access should report a line");
    let mut source_map = SourceMap::new();
    let source_id = source_map.add_source("inline.rss", source);
    let line_text = source_map
        .file(source_id)
        .and_then(|file| file.line_text(line))
        .expect("line text should exist");
    let rendered = render_compile_error(&source_map, &compile, false);

    assert!(rendered.contains("field 'age' is not declared"));
    assert!(rendered.contains(&format!("inline.rss:{line}:1")));
    assert!(rendered.contains(line_text));
}

#[test]
fn render_compile_error_highlights_captured_schema_field_access_inside_named_function() {
    let source = r#"struct User { name: string }
let user: User = { name: "Ada" };
fn show_age() {
    user.age;
}
show_age();
"#;

    let err = match compile_source(source) {
        Ok(_) => panic!("compile should reject invalid captured schema field access"),
        Err(err) => err,
    };
    let compile = match err {
        SourceError::Compile(compile) => compile,
        other => panic!("expected compile error, got {other:?}"),
    };

    let line = compile
        .line()
        .expect("captured schema field access should report a line");
    let mut source_map = SourceMap::new();
    let source_id = source_map.add_source("inline.rss", source);
    let line_text = source_map
        .file(source_id)
        .and_then(|file| file.line_text(line))
        .expect("line text should exist");
    let rendered = render_compile_error(&source_map, &compile, false);

    assert!(rendered.contains("field 'age' is not declared"));
    assert!(rendered.contains(&format!("inline.rss:{line}:1")));
    assert!(rendered.contains(line_text));
}

#[test]
fn lint_unknown_inferred_local_types_includes_function_scope_locals() {
    let source = r#"
fn nothing() {
    let a = [1, "2"];
    let idx = 0;
    let b = a[idx];
    0
}
"#;

    let warnings = lint_unknown_inferred_local_types(source, SourceFlavor::RustScript)
        .expect("lint should succeed");
    assert!(
        warnings
            .iter()
            .any(|warning| warning.name == "b" && warning.line == 5),
        "expected function-local unknown inferred warning for 'b', got {warnings:?}"
    );
}

#[test]
fn lint_unknown_inferred_local_types_respects_declared_schema_annotations() {
    let source = r#"
use json;
struct Stats { score: int }
struct Profile { stats: Stats }

let payload_json = json::encode({});
let payload_decoded: Profile = json::decode(payload_json);
payload_decoded.stats.score;
"#;

    let warnings = lint_unknown_inferred_local_types(source, SourceFlavor::RustScript)
        .expect("lint should succeed");
    assert!(
        warnings
            .iter()
            .all(|warning| warning.name != "payload_decoded"),
        "declared schema binding should not be reported as unknown, got {warnings:?}"
    );
}

#[test]
fn collect_inferred_local_type_hints_reports_visible_local_types() {
    let source = r#"
let total = 1;
print(total);
"#;

    let hints = collect_inferred_local_type_hints(source, SourceFlavor::RustScript)
        .expect("type hints should succeed");
    let total = hints
        .iter()
        .find(|hint| hint.name == "total")
        .expect("expected a type hint for total");

    assert_eq!(total.inferred_type, "int");
    assert_eq!(total.declared_line, Some(2));
    assert_eq!(total.last_line, Some(3));
}

#[test]
fn collect_inferred_local_type_hints_include_named_function_parameters() {
    let source = r#"
fn plus_one(amount) {
    amount + 1
}

plus_one(2);
"#;

    let hints = collect_inferred_local_type_hints(source, SourceFlavor::RustScript)
        .expect("type hints should succeed");
    let amount = hints
        .iter()
        .find(|hint| hint.name == "amount")
        .expect("expected a type hint for amount");

    assert_eq!(amount.inferred_type, "int");
    assert_eq!(amount.declared_line, Some(2));
}

#[test]
fn collect_inferred_local_type_hints_preserve_generic_function_schema_labels() {
    let source = r#"
fn myfn<T>(v: T) {
    let b = v;
    b
}

let text = myfn::<string>("hello");
"#;

    let hints = collect_inferred_local_type_hints(source, SourceFlavor::RustScript)
        .expect("type hints should succeed");
    let value = hints
        .iter()
        .find(|hint| hint.name == "v")
        .expect("expected a type hint for v");
    let binding = hints
        .iter()
        .find(|hint| hint.name == "b")
        .expect("expected a type hint for b");
    let text = hints
        .iter()
        .find(|hint| hint.name == "text")
        .expect("expected a type hint for text");

    assert_eq!(value.inferred_type, "T");
    assert_eq!(binding.inferred_type, "T");
    assert_eq!(text.inferred_type, "string");
}

#[test]
fn collect_inferred_local_type_hints_keep_function_body_expr_visibility() {
    let source = r#"
fn plus_one(amount) {
    let total = amount + 1;
    total
}

plus_one(2);
"#;

    let hints = collect_inferred_local_type_hints(source, SourceFlavor::RustScript)
        .expect("type hints should succeed");
    let total = hints
        .iter()
        .find(|hint| hint.name == "total")
        .expect("expected a type hint for total");

    assert_eq!(total.inferred_type, "int");
    assert_eq!(total.declared_line, Some(3));
    assert_eq!(total.last_line, Some(4));
}

#[test]
fn collect_inferred_local_type_hints_use_declared_schema_labels() {
    let source = r#"
use json;
struct Stats { score: int }
struct Profile { stats: Stats }

let payload_json = json::encode({});
let payload_decoded: Profile = json::decode(payload_json);
payload_decoded.stats.score;
"#;

    let hints = collect_inferred_local_type_hints(source, SourceFlavor::RustScript)
        .expect("type hints should succeed");
    let payload_decoded = hints
        .iter()
        .find(|hint| hint.name == "payload_decoded")
        .expect("expected a type hint for payload_decoded");

    assert_eq!(payload_decoded.inferred_type, "Profile");
    assert_eq!(payload_decoded.declared_line, Some(7));
    assert_eq!(payload_decoded.last_line, Some(8));
}

#[test]
fn collect_inferred_local_type_hints_use_host_generic_return_schema_labels() {
    let source = r#"
use json;
struct Stats { score: int }
struct Profile { stats: Stats }

let profile = json::decode::<Profile>("{\"stats\":{\"score\":41}}");
profile.stats.score;
"#;

    let hints = collect_inferred_local_type_hints(source, SourceFlavor::RustScript)
        .expect("type hints should succeed");
    let profile = hints
        .iter()
        .find(|hint| hint.name == "profile")
        .expect("expected a type hint for profile");

    assert_eq!(profile.inferred_type, "Profile");
    assert_eq!(profile.declared_line, Some(6));
    assert_eq!(profile.last_line, Some(7));
}

#[test]
fn lint_unknown_inferred_local_types_skips_generic_function_schema_locals() {
    let source = r#"
fn myfn<T>(v: T) {
    let b = v;
    b
}
"#;

    let warnings = lint_unknown_inferred_local_types(source, SourceFlavor::RustScript)
        .expect("lint should succeed");
    assert!(
        warnings.iter().all(|warning| warning.name != "b"),
        "generic schema local should not be reported as unknown, got {warnings:?}"
    );
}