hara-native 0.1.8

HAL-free native host runtime and package launcher for Hara
Documentation
use super::{
    import_wit, inspect_direct, project_wit, WasmInterface, WasmValueType, WitImportOptions,
    WitProjectionOptions, WitRoute,
};

const START_SENTINEL: &[u8] = b"\0asm\x01\0\0\0\x08\x01\0";

const SCALAR_INTERFACE: &str = r#"
  (wasm/interface
   {:schema "hara.wasm-interface/0-alpha"
    :namespace math.scalar
    :module "modules/math.wasm"
    :exports
    {add {:wasm/export "add_i64"
          :arguments [{:name left :hara/type :i64 :wasm/type :i64}
                      {:name right :hara/type :i64 :wasm/type :i64}]
          :returns {:hara/type :i64 :wasm/type :i64}}}})"#;

const MEMORY_INTERFACE: &str = r#"
  (wasm/interface
   {:schema "hara.wasm-interface/0-alpha"
    :namespace codec.echo
    :module "echo.wasm"
    :memory {:export "memory" :allocate "alloc" :release "free"}
    :exports
    {echo {:wasm/export "echo_bytes"
           :arguments [{:name input
                        :hara/type :bytes
                        :wasm/type :i32
                        :lower [:pointer :length]
                        :ownership :borrowed}]
           :returns {:hara/type :bytes
                     :wasm/type :i64
                     :lift :packed-i64
                     :ownership :caller}}}})"#;

#[test]
fn parses_scalar_interface_without_evaluation() {
    let interface = WasmInterface::parse(SCALAR_INTERFACE, "fixture").unwrap();
    assert_eq!(interface.namespace, "math.scalar");
    assert_eq!(interface.module, "modules/math.wasm");
    assert_eq!(interface.exports[0].name, "add");
    assert_eq!(interface.exports[0].wasm_export, "add_i64");
    assert_eq!(
        interface.exports[0].arguments[0].wasm_type,
        WasmValueType::I64
    );
    assert_eq!(interface.direct_exports()[0].0, "add_i64");
    assert_eq!(interface.digest().len(), 71);
    assert!(interface.digest().starts_with("sha256:"));
    assert_eq!(
        WasmInterface::parse(&interface.canonical_source(), "canonical").unwrap(),
        interface
    );
}

#[test]
fn parses_explicit_memory_semantics_without_executing_them() {
    let interface = WasmInterface::parse(MEMORY_INTERFACE, "fixture").unwrap();
    let memory = interface.memory.as_ref().unwrap();
    assert_eq!(memory.export, "memory");
    assert_eq!(memory.allocate.as_deref(), Some("alloc"));
    assert_eq!(memory.release.as_deref(), Some("free"));
    assert_eq!(
        WasmInterface::parse(&interface.canonical_source(), "canonical").unwrap(),
        interface
    );
}

#[test]
fn static_inspection_records_a_start_function_without_running_it() {
    let inspection = inspect_direct(START_SENTINEL).unwrap();
    assert_eq!(inspection.start, Some(0));
}

#[test]
fn canonicalizes_map_and_set_order() {
    let left = r#"
      {:schema "hara.wasm-interface/0-alpha"
       :namespace math.scalar
       :module "modules/math.wasm"
       :capabilities [:random :clock]
       :exports
       {subtract {:wasm/export "sub"
                  :arguments [{:name left :hara/type :i64 :wasm/type :i64}
                              {:name right :hara/type :i64 :wasm/type :i64}]
                  :returns {:hara/type :i64 :wasm/type :i64}}
        add {:wasm/export "add_i64"
             :arguments [{:name left :hara/type :i64 :wasm/type :i64}
                         {:name right :hara/type :i64 :wasm/type :i64}]
             :returns {:hara/type :i64 :wasm/type :i64}
             :capabilities [:clock :random]}}}"#;
    let right = r#"
      {:exports
       {add {:capabilities [:random :clock]
             :returns {:wasm/type :i64 :hara/type :i64}
             :arguments [{:wasm/type :i64 :hara/type :i64 :name left}
                         {:hara/type :i64 :name right :wasm/type :i64}]
             :wasm/export "add_i64"}
        subtract {:returns {:wasm/type :i64 :hara/type :i64}
                  :wasm/export "sub"
                  :arguments [{:wasm/type :i64 :name left :hara/type :i64}
                              {:name right :wasm/type :i64 :hara/type :i64}]}}
       :module "modules/math.wasm"
       :capabilities [:clock :random]
       :namespace math.scalar
       :schema "hara.wasm-interface/0-alpha"}"#;
    let left = WasmInterface::parse(left, "left").unwrap();
    let right = WasmInterface::parse(right, "right").unwrap();
    assert_eq!(left, right);
    assert_eq!(left.canonical_source(), right.canonical_source());
    assert_eq!(left.digest(), right.digest());
}

#[test]
fn rejects_executable_unknown_duplicate_and_unsafe_sources() {
    for source in [
        "(do (println \"not data\"))".to_owned(),
        SCALAR_INTERFACE.replace(":module \"modules/math.wasm\"", ":module \"../math.wasm\""),
        SCALAR_INTERFACE.replace(
            ":schema \"hara.wasm-interface/0-alpha\"",
            ":schema \"hara.wasm-interface/9\"",
        ),
        SCALAR_INTERFACE.replace(":namespace math.scalar", ":namespace Math.scalar"),
        SCALAR_INTERFACE.replace(":exports", ":unknown true :exports"),
        SCALAR_INTERFACE.replace(":name left", ":name left :name duplicate"),
    ] {
        let error = WasmInterface::parse(&source, "fixture").unwrap_err();
        assert!(error.starts_with("wasm-interface/"));
    }
}

#[test]
fn rejects_ambiguous_and_future_semantics() {
    let mismatch = SCALAR_INTERFACE.replace(
        ":name left :hara/type :i64 :wasm/type :i64",
        ":name left :hara/type :i32 :wasm/type :i64",
    );
    assert!(WasmInterface::parse(&mismatch, "mismatch")
        .unwrap_err()
        .contains("maps :i32 to :i64"));

    let missing_ownership = SCALAR_INTERFACE.replace(
        ":name left :hara/type :i64 :wasm/type :i64",
        ":name left :hara/type :bytes :wasm/type :i32 :lower [:pointer :length]",
    );
    assert!(WasmInterface::parse(&missing_ownership, "bytes")
        .unwrap_err()
        .contains("requires :ownership"));

    let missing_memory = MEMORY_INTERFACE.replace(
        ":memory {:export \"memory\" :allocate \"alloc\" :release \"free\"}",
        "",
    );
    assert!(WasmInterface::parse(&missing_memory, "bytes")
        .unwrap_err()
        .contains("require an explicit :memory contract"));

    let asynchronous = SCALAR_INTERFACE.replace(
        ":returns {:hara/type :i64 :wasm/type :i64}",
        ":returns {:hara/type :i64 :wasm/type :i64} :async true",
    );
    assert!(WasmInterface::parse(&asynchronous, "async")
        .unwrap()
        .exports
        .first()
        .is_some_and(|export| export.asynchronous));

    let handles = SCALAR_INTERFACE.replace(
        ":exports",
        ":handles {stream {:tag stream :release \"stream_drop\"}} :exports",
    );
    let handles = WasmInterface::parse(&handles, "handles").unwrap();
    assert_eq!(handles.handles["stream"].tag, "stream");
    assert_eq!(
        handles.handles["stream"].release.as_deref(),
        Some("stream_drop")
    );
}

const WIT_SCALAR: &str = r#"
package demo:calculator;

interface calculator {
  add: func(left: s64, right: s64) -> s64;
}

world calculator-world {
  export calculator;
}
"#;

const WIT_RICH: &str = r#"
package demo:rich;

interface rich {
  record point {
    x: s32,
    y: s32,
  }
  variant choice {
    point(point),
    none,
  }
  resource stream;
  transform: func(value: option<point>, data: list<string>) -> result<choice, string>;
}

world rich-world {
  import host;
  export rich;
}
"#;

#[test]
fn imports_scalar_wit_deterministically_and_keeps_the_direct_route() {
    let options = WitImportOptions {
        module: Some("fixtures/calculator.wasm".into()),
        ..WitImportOptions::default()
    };
    let left = import_wit(WIT_SCALAR, "calculator.wit", &options).unwrap();
    let right = import_wit(WIT_SCALAR, "calculator.wit", &options).unwrap();
    assert_eq!(left, right);
    assert_eq!(left.route, WitRoute::DirectImport);
    assert!(left.diagnostics.is_empty());
    assert!(left.interface_source.contains(":wasm/type :i64"));
    let interface = WasmInterface::parse(&left.interface_source, "wit skeleton").unwrap();
    let projection = project_wit(
        &interface,
        &WitProjectionOptions {
            strict: true,
            ..WitProjectionOptions::default()
        },
    )
    .unwrap();
    assert!(projection
        .source
        .contains("add: func(left: s64, right: s64) -> s64;"));
    let round_trip = import_wit(&projection.source, "projection.wit", &options).unwrap();
    assert_eq!(
        interface,
        WasmInterface::parse(&round_trip.interface_source, "round trip").unwrap()
    );
}

#[test]
fn rich_wit_reports_lossy_features_and_strict_mode_rejects_them() {
    let permissive = import_wit(WIT_RICH, "rich.wit", &WitImportOptions::default()).unwrap();
    assert_eq!(permissive.route, WitRoute::HtaRequire);
    assert!(permissive
        .diagnostics
        .iter()
        .any(|diagnostic| diagnostic.code == "option"));
    assert!(permissive
        .diagnostics
        .iter()
        .any(|diagnostic| diagnostic.code == "resource"));
    assert!(permissive.normalized_ir.contains(":provenance"));

    let strict = import_wit(
        WIT_RICH,
        "rich.wit",
        &WitImportOptions {
            strict: true,
            ..WitImportOptions::default()
        },
    )
    .unwrap_err();
    assert!(strict.starts_with("wasm-wit/strict"));
    assert!(strict.contains("option"));
    assert!(strict.contains("world-import"));
}