runmat-core 0.5.0

Host-agnostic RunMat execution engine (parser, interpreter, snapshot loader)
Documentation
#![cfg(not(target_arch = "wasm32"))]

use runmat_core::{ExecutionStreamKind, RunError, RunMatSession, SessionExecutionResult};
use runmat_gc::gc_test_context;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};

fn stdout_stream(result: &SessionExecutionResult) -> String {
    result
        .streams
        .iter()
        .filter(|entry| entry.stream == ExecutionStreamKind::Stdout)
        .map(|entry| entry.text.as_str())
        .collect::<String>()
}

fn stderr_stream(result: &SessionExecutionResult) -> String {
    result
        .streams
        .iter()
        .filter(|entry| entry.stream == ExecutionStreamKind::Stderr)
        .map(|entry| entry.text.as_str())
        .collect::<String>()
}

fn unique_temp_path(prefix: &str) -> PathBuf {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_nanos();
    std::env::temp_dir().join(format!("runmat_{prefix}_{nanos}.txt"))
}

#[test]
fn fprintf_rm138_repro_is_stable_end_to_end() {
    let mut engine = gc_test_context(RunMatSession::new).unwrap();
    let script = r#"
        price = 42.5;
        fprintf("Price: $%.2f\n", price);
        x = single(3.14);
        fprintf("Value: %.4f\n", double(x));
    "#;
    let result = runmat_core::execute_text_request_for_testing(&mut engine, script).unwrap();
    assert_eq!(stdout_stream(&result), "Price: $42.50\nValue: 3.1400\n");
}

#[test]
fn disp_inline_cast_argument_is_stable_end_to_end() {
    let mut engine = gc_test_context(RunMatSession::new).unwrap();
    let script = r#"
        x = single(3.14);
        disp(double(x));
    "#;
    let result = runmat_core::execute_text_request_for_testing(&mut engine, script).unwrap();
    let rendered = stdout_stream(&result);
    let parsed: f64 = rendered
        .trim()
        .parse()
        .expect("disp output should parse as f64");
    let expected = f64::from(
        "3.14"
            .parse::<f32>()
            .expect("parse single precision literal"),
    );
    assert!(
        (parsed - expected).abs() < 1e-6,
        "expected {expected}, got {parsed}"
    );
}

#[test]
fn fprintf_stream_routing_is_correct() {
    let mut engine = gc_test_context(RunMatSession::new).unwrap();
    let result = runmat_core::execute_text_request_for_testing(
        &mut engine,
        "fprintf('out'); fprintf(2, 'err');",
    )
    .unwrap();
    assert_eq!(stdout_stream(&result), "out");
    assert_eq!(stderr_stream(&result), "err");
}

#[test]
fn fprintf_grouping_and_i_flag_smoke() {
    let mut engine = gc_test_context(RunMatSession::new).unwrap();
    let result = runmat_core::execute_text_request_for_testing(
        &mut engine,
        "fprintf('%''d|%Id', 12345, 42);",
    )
    .unwrap();
    assert_eq!(stdout_stream(&result), "12,345|42");
}

#[test]
fn fprintf_file_roundtrip_and_count_work_end_to_end() {
    let mut engine = gc_test_context(RunMatSession::new).unwrap();
    let path = unique_temp_path("fprintf_core_roundtrip");
    let path_text = path.to_string_lossy();
    let script = format!(
        "fid = fopen('{}', 'w'); n = fprintf(fid, 'hello-%d', 7); fclose(fid); fprintf('|%d|', n);",
        path_text
    );
    let result = runmat_core::execute_text_request_for_testing(&mut engine, &script).unwrap();
    assert!(
        result.error.is_none(),
        "expected no execution error, got {:?}",
        result.error
    );
    assert_eq!(stdout_stream(&result), "|7|");
    let bytes = std::fs::read(&path).expect("written file should exist");
    assert_eq!(bytes, b"hello-7");
    let _ = std::fs::remove_file(path);
}

#[test]
fn fprintf_encoding_alias_smoke_utf8_underscore() {
    let mut engine = gc_test_context(RunMatSession::new).unwrap();
    let path = unique_temp_path("fprintf_core_utf8_alias");
    let path_text = path.to_string_lossy();
    let script = format!(
        "fid = fopen('{}', 'w', 'native', 'utf_8'); fprintf(fid, '%s', 'é'); fclose(fid);",
        path_text
    );
    let result = runmat_core::execute_text_request_for_testing(&mut engine, &script).unwrap();
    assert!(
        result.error.is_none(),
        "expected no execution error, got {:?}",
        result.error
    );
    let bytes = std::fs::read(&path).expect("utf8 alias output should exist");
    assert_eq!(bytes, "é".as_bytes());
    let _ = std::fs::remove_file(path);
}

#[test]
fn fprintf_format_error_propagates_to_session_boundary() {
    let mut engine = gc_test_context(RunMatSession::new).unwrap();
    let request = runmat_core::abi::ExecutionRequest::for_source(
        runmat_core::abi::SourceInput::Text {
            name: "<test>".to_string(),
            text: "fprintf('%q', 1);".to_string(),
        },
        engine.compat_mode(),
        runmat_core::abi::HostExecutionPolicy::default(),
        engine.workspace_handle(),
    );

    match futures::executor::block_on(engine.execute_request(request)) {
        Ok(outcome) => {
            assert!(
                outcome.diagnostics.iter().any(|d| d.severity
                    == runmat_core::abi::DiagnosticSeverity::Error
                    && d.code == "RunMat:fprintf:InvalidFormat"),
                "expected stable unsupported-format identifier in diagnostics: {:?}",
                outcome.diagnostics
            );
        }
        Err(RunError::Runtime(err)) => assert_eq!(
            err.identifier(),
            Some("RunMat:fprintf:InvalidFormat"),
            "expected stable unsupported-format identifier in runtime error"
        ),
        Err(other) => panic!("expected runtime formatting error, got {other:?}"),
    }
}