simplicityhl 0.7.0

Rust-like language that compiles to Simplicity bytecode.
Documentation
use std::path::{Path, PathBuf};
use std::process::{Command, Output};

fn repo_path(path: &str) -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR")).join(path)
}

/// Write `content` to a uniquely named `.simf` file in the test temp dir and run
/// `simc` on it, returning the process output. Used by the version-directive tests
/// to exercise the real binary on standalone files.
fn run_simc_on_source(name: &str, content: &str) -> Output {
    let file = Path::new(env!("CARGO_TARGET_TMPDIR")).join(format!("{name}.simf"));
    std::fs::write(&file, content).expect("failed to write source file");
    Command::new(env!("CARGO_BIN_EXE_simc"))
        .arg(file)
        .output()
        .expect("failed to run simc")
}

/// Write each `(relative path, content)` under a unique temp project root (creating
/// parent directories) and return the root. Used to drive multi-file `--dep` builds.
fn setup_project(name: &str, files: &[(&str, &str)]) -> PathBuf {
    let root = Path::new(env!("CARGO_TARGET_TMPDIR")).join(name);
    for (rel, content) in files {
        let path = root.join(rel);
        std::fs::create_dir_all(path.parent().unwrap()).expect("failed to create project dirs");
        std::fs::write(&path, content).expect("failed to write project file");
    }
    root
}

#[test]
fn cli_dependency_can_use_crate_root() {
    let root = repo_path("functional-tests/valid-test-cases/external-library-uses-crate");
    let main = root.join("main.simf");
    let ext_lib = root.join("ext_lib");
    let dep_arg = format!("{}:ext_lib={}", root.display(), ext_lib.display());

    let output = Command::new(env!("CARGO_BIN_EXE_simc"))
        .arg(main)
        .arg("-Z")
        .arg("imports")
        .arg("--dep")
        .arg(dep_arg)
        .output()
        .expect("failed to run simc");

    assert!(
        output.status.success(),
        "simc failed\nstatus: {:?}\nstdout:\n{}\nstderr:\n{}",
        output.status.code(),
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr),
    );
}

#[test]
fn cli_import_program_rejected_without_unstable_flag() {
    let root = repo_path("functional-tests/valid-test-cases/external-library-uses-crate");
    let main = root.join("main.simf");
    let ext_lib = root.join("ext_lib");
    let dep_arg = format!("{}:ext_lib={}", root.display(), ext_lib.display());

    // Same invocation as `cli_dependency_can_use_crate_root`, but without `-Z imports`:
    // the import syntax must be gated, so the binary exits non-zero and points at
    // the missing feature flag.
    let output = Command::new(env!("CARGO_BIN_EXE_simc"))
        .arg(main)
        .arg("--dep")
        .arg(dep_arg)
        .output()
        .expect("failed to run simc");

    assert!(
        !output.status.success(),
        "simc must reject an import program without -Z imports"
    );

    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("imports") && stderr.contains("-Z"),
        "error should name the feature and the flag, got:\n{stderr}"
    );
}

#[test]
fn cli_reserved_crate_mapping_fails() {
    let root = repo_path("functional-tests/valid-test-cases/external-library-uses-crate");
    let main = root.join("main.simf");
    let ext_lib = root.join("ext_lib");

    // Attempt to maliciously override the `crate` keyword
    let dep_arg = format!("crate={}", ext_lib.display());

    let output = Command::new(env!("CARGO_BIN_EXE_simc"))
        .arg(main)
        .arg("--dep")
        .arg(dep_arg)
        .output()
        .expect("failed to run simc");

    assert!(
        !output.status.success(),
        "simc unexpectedly succeeded when overriding the 'crate' dependency"
    );

    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("keyword is reserved"),
        "Expected 'keyword is reserved' error, got:\n{}",
        stderr
    );
}

/// The output carries the version of the compiler that produced it, so the exact
/// compiler can be identified from a shared artifact (different versions can
/// produce different CMRs from the same source). JSON output requires `serde`.
#[cfg(feature = "serde")]
#[test]
fn cli_output_includes_compiler_version() {
    let file = Path::new(env!("CARGO_TARGET_TMPDIR")).join("output_version.simf");
    std::fs::write(&file, "simc \"*\";\nfn main() {}\n").expect("failed to write source file");
    let output = Command::new(env!("CARGO_BIN_EXE_simc"))
        .arg(&file)
        .arg("--json")
        .output()
        .expect("failed to run simc");
    assert!(output.status.success(), "simc --json must succeed");

    let stdout = String::from_utf8_lossy(&output.stdout);
    let expected = format!("\"compiler_version\":\"{}\"", env!("CARGO_PKG_VERSION"));
    assert!(
        stdout.contains(&expected),
        "expected {expected} in the JSON output, got:\n{stdout}"
    );
}

/// A compatible version directive compiles from the command line, with no
/// missing-directive warning. `*` matches any compiler, so this stays valid across
/// version bumps and acts as the positive control for the rejection tests below.
#[test]
fn cli_version_compatible_accepted() {
    let output = run_simc_on_source("version_ok", "simc \"*\";\nfn main() {}\n");
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        output.status.success(),
        "simc should accept a compatible directive\nstderr:\n{stderr}",
    );
    assert!(
        !stderr.contains("no compiler version directive"),
        "a present directive must not trigger the missing-directive warning, got:\n{stderr}"
    );
}

/// A directive the running compiler cannot satisfy is rejected. `>99.0.0` is
/// permanently too new, so the build aborts with a non-zero exit and a clear message.
#[test]
fn cli_version_incompatible_rejected() {
    let output = run_simc_on_source("version_incompatible", "simc \">99.0.0\";\nfn main() {}\n");
    assert!(
        !output.status.success(),
        "simc must reject an incompatible directive"
    );

    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("Incompatible compiler version"),
        "Expected 'Incompatible compiler version', got:\n{stderr}"
    );
}

/// A directive is optional: a file with none still compiles, but the CLI prints a
/// warning suggesting one be added.
#[test]
fn cli_version_missing_warns_but_compiles() {
    let output = run_simc_on_source("version_missing", "fn main() {}\n");
    assert!(
        output.status.success(),
        "simc must accept a file with no version directive"
    );

    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("no compiler version directive"),
        "Expected a missing-directive warning, got:\n{stderr}"
    );
}

/// A directive whose requirement is not valid semver is a syntax error, not a
/// version mismatch.
#[test]
fn cli_version_invalid_syntax_rejected() {
    let output = run_simc_on_source(
        "version_bad_syntax",
        "simc \"not-a-version\";\nfn main() {}\n",
    );
    assert!(
        !output.status.success(),
        "simc must reject a malformed version requirement"
    );

    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("Invalid version requirement in `simc` directive"),
        "Expected 'Invalid version requirement in `simc` directive', got:\n{stderr}"
    );
}

/// A directive that is structurally broken (here a missing semicolon) is rejected
/// before the requirement is even parsed — a different path than an invalid semver
/// string above.
#[test]
fn cli_version_malformed_directive_rejected() {
    let output = run_simc_on_source("version_malformed", "simc \"1.0\"\nfn main() {}\n");
    assert!(
        !output.status.success(),
        "simc must reject a directive with a missing semicolon"
    );

    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("Malformed compiler version directive"),
        "Expected 'Malformed compiler version directive', got:\n{stderr}"
    );
}

/// The fail-fast promise: an incompatible directive is reported even when the file's
/// body uses syntax this compiler cannot lex (here a string literal), and the version
/// error is the *only* diagnostic — not buried under `Cannot parse` noise.
#[test]
fn cli_version_incompatible_preempts_lex_errors() {
    let output = run_simc_on_source(
        "version_future_syntax",
        "simc \">99.0.0\";\nfn main() { let s = \"future string literal\"; }\n",
    );
    assert!(
        !output.status.success(),
        "simc must reject an incompatible directive even when the body does not lex"
    );

    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("Incompatible compiler version"),
        "Expected 'Incompatible compiler version', got:\n{stderr}"
    );
    assert!(
        !stderr.contains("Cannot parse"),
        "the version error must preempt lex errors, got:\n{stderr}"
    );
}

/// An incompatible directive in a *dependency* (reached via `--dep`), not the entry
/// file, also aborts the build and the diagnostic points at the dependency.
#[test]
fn cli_dependency_version_mismatch_rejected() {
    let root = setup_project(
        "version_dep_mismatch",
        &[
            (
                "main.simf",
                "simc \"*\";\nuse lib::module::add;\nfn main() {}\n",
            ),
            ("lib/module.simf", "simc \">99.0.0\";\npub fn add() {}\n"),
        ],
    );
    let dep_arg = format!("{}:lib={}", root.display(), root.join("lib").display());

    let output = Command::new(env!("CARGO_BIN_EXE_simc"))
        .arg(root.join("main.simf"))
        .arg("-Z")
        .arg("imports")
        .arg("--dep")
        .arg(dep_arg)
        .output()
        .expect("failed to run simc");

    assert!(
        !output.status.success(),
        "simc must reject an incompatible directive in a dependency"
    );

    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("Incompatible compiler version") && stderr.contains("module.simf"),
        "expected an incompatible-version error pointing at the dependency, got:\n{stderr}"
    );
}

#[test]
fn cli_diagnostics_respect_no_color_env() {
    const ESC: u8 = 0x1b;

    let file = Path::new(env!("CARGO_TARGET_TMPDIR")).join("no_color.simf");
    std::fs::write(&file, "fn main() { let x: u32 = true; }\n").expect("write source file");

    let output = Command::new(env!("CARGO_BIN_EXE_simc"))
        .arg(&file)
        .env("NO_COLOR", "1")
        .output()
        .expect("run simc");

    assert!(
        !output.status.success(),
        "compilation of an ill-typed program must fail; got {:?}",
        output.status,
    );

    // Guard against the vacuous case: empty stderr also contains no
    // escape bytes. We want to prove a real diagnostic came out AND
    // that it's clean.
    assert!(
        !output.stderr.is_empty(),
        "expected a diagnostic on stderr; got nothing.\nstdout: {}",
        String::from_utf8_lossy(&output.stdout),
    );

    assert!(
        !output.stderr.contains(&ESC),
        "NO_COLOR=1 was set but stderr contains ANSI escape bytes:\n{}",
        String::from_utf8_lossy(&output.stderr),
    );
}