Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions TEST-NEEDS.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ compliance, 16 tests)
performance, correctness, 13 tests)
* Benchmarks: `+src/ephapax-parser/benches/parse_bench.rs+`,
`+src/ephapax-vram-cache/benches/cache_bench.rs+`
* Total: *621 tests* (`+cargo test --workspace --all-targets+`);
* Total: *625 tests* (`+cargo test --workspace --all-targets+`);
pass/fail enforced by `+rust-ci.yml+`
* Documented all-target tests: 621
* Documented all-target tests: 625

=== CRG Testing Taxonomy — Status

Expand Down Expand Up @@ -83,7 +83,7 @@ eliminator-fork invariant + its gated capstone, per PROOF-NEEDS.adoc §4)
==== Build & Execution

* [x] cargo build — compiles
* [x] cargo test –workspace –all-targets — 621 pass, 0 fail
* [x] cargo test –workspace –all-targets — 625 pass, 0 fail
* [x] idris2/ephapax-parse-tests.ipkg — compiles AND runs green
(2026-07-07, idris2 0.8.0 + zig-built libephapax_tokbuf.so); CI-gated
compile-only by abi-verify.yml `+idris2-parse-front+` (runtime run stays
Expand Down
22 changes: 20 additions & 2 deletions src/ephapax-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,20 @@ struct Cli {
/// Enable verbose output
#[arg(short, long, global = true)]
verbose: bool,

/// Accept the forms of the core language that the proofs do not cover
/// yet (v2 plan rung E9): a lambda with an I64, F32 or F64 parameter
/// or result. Applies to `run`, `check` and `compile`.
#[arg(long, global = true)]
unstable_core: bool,
}

/// The features that the command line asked for. `main` sets it one time.
static FEATURES: std::sync::OnceLock<ephapax_driver::Features> = std::sync::OnceLock::new();

/// The features that the command line asked for.
fn features() -> ephapax_driver::Features {
FEATURES.get().copied().unwrap_or_default()
}

#[derive(Subcommand)]
Expand Down Expand Up @@ -196,6 +210,9 @@ enum PackageCommands {

fn main() -> ExitCode {
let cli = Cli::parse();
FEATURES.get_or_init(|| ephapax_driver::Features {
unstable_core: cli.unstable_core,
});

let result = match cli.command {
Some(Commands::Repl { preload }) => run_repl(preload, cli.verbose),
Expand Down Expand Up @@ -279,7 +296,8 @@ fn load_program(path: &Path, stdlib: Option<PathBuf>) -> Result<ephapax_driver::
return Err(format!("--stdlib {}: not a directory", dir.display()));
}
let stdlib_dir = ephapax_driver::stdlib_dir(stdlib);
ephapax_driver::load_program(path, stdlib_dir.as_deref()).map_err(|e| report_load_error(&e))
ephapax_driver::load_program_with(path, stdlib_dir.as_deref(), features())
.map_err(|e| report_load_error(&e))
}

/// Describe a load failure. A type error is also printed to stderr with
Expand Down Expand Up @@ -446,7 +464,7 @@ fn check_files(files: &[PathBuf], mode: Option<&str>, verbose: bool) -> Result<(
let mut checked = 0;

for path in files {
match ephapax_driver::load_program(path, stdlib_dir.as_deref()) {
match ephapax_driver::load_program_with(path, stdlib_dir.as_deref(), features()) {
Ok(program) => {
if verbose {
println!(
Expand Down
261 changes: 261 additions & 0 deletions src/ephapax-cli/tests/closures.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
// SPDX-License-Identifier: MPL-2.0
// Owner: Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
//
// closures.rs: v2 plan rung E9, closures.
//
// A closure is a cell `(table index, environment)` for a closure function
// `(environment, parameter) -> result`. Each program here runs as wasm and
// in the interpreter, and the 2 must print the expected value.

use std::path::Path;
use std::process::{Command, Output};

/// Run the `ephapax` binary with `args`, in the directory `dir`.
fn ephapax(dir: &Path, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_ephapax"))
.current_dir(dir)
.args(args)
.output()
.expect("spawn ephapax")
}

/// Write each program (name, text, output) and run it as wasm and in the
/// interpreter, with the flags `flags`.
fn each_runs(flags: &[&str], programs: &[(&str, &str, &str)]) {
let tmp = tempfile::tempdir().expect("tempdir");
for (name, text, want) in programs {
let file = format!("{name}.eph");
std::fs::write(tmp.path().join(&file), text).expect("write program");
for mode in [&["run"][..], &["run", "--interp"]] {
let args: Vec<&str> = [mode, flags, &[file.as_str()]].concat();
let out = ephapax(tmp.path(), &args);
assert!(
out.status.success(),
"{name} under {args:?} failed:\n{}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert_eq!(stdout.trim(), *want, "{name} under {args:?}");
}
}
}

/// A function of the module is a value: an argument, a `let` value, or a
/// call with too few arguments. The wasm module had no table for it and
/// was not valid.
#[test]
fn a_named_function_is_a_value() {
let inc = "fn inc(n: I32): I32 = n + 1\n\n";
let apply = "fn apply(f: I32 -> I32, x: I32): I32 = f(x)\n\n";
let add = "fn add(a: I32, b: I32): I32 = a + b\n\n";
let add3 = "fn add3(a: I32, b: I32, c: I32): I32 = a + b + c\n\n";
each_runs(
&[],
&[
(
"argument",
&format!("{inc}{apply}fn main(): I32 = apply(inc, 41)\n"),
"42",
),
(
"let_value",
&format!("{add}fn main(): I32 = let f = add in f(40, 2)\n"),
"42",
),
(
"too_few",
&format!("{add}fn main(): I32 = let f = add(40) in f(2)\n"),
"42",
),
(
"one_at_a_time",
&format!(
"{add3}fn main(): I32 = let f = add3 in let g = f(30) in \
let h = g(10) in h(2)\n"
),
"42",
),
(
"too_few_then_more",
&format!("{add3}fn main(): I32 = add3(30, 10)(2)\n"),
"42",
),
(
"no_parameter",
"fn answer(): I32 = 42\n\nfn call(f: () -> I32): I32 = f(())\n\n\
fn main(): I32 = call(answer)\n",
"42",
),
// The name is free in the lambda and is not a local.
(
"in_a_lambda",
&format!(
"{inc}{apply}fn main(): I32 = let g = fn(x: I32) -> apply(inc, x) in g(41)\n"
),
"42",
),
(
"used_2_times",
&format!("{inc}fn main(): I32 = let f = inc in let g = inc in f(g(40))\n"),
"42",
),
(
"generic",
&format!("fn id<T>(x: T): T = x\n\n{apply}fn main(): I32 = apply(id, 42)\n"),
"42",
),
// Each parameter and the result keep their width.
(
"wide",
"fn pick(a: I64, b: F64): I64 = a + a\n\n\
fn main(): I64 = let f = pick(5000000000) in f(1.5)\n",
"10000000000i64",
),
],
);

// An extern function is a value too. The host of `run` does not have
// these functions, so the program is compiled only.
let tmp = tempfile::tempdir().expect("tempdir");
std::fs::write(
tmp.path().join("ext.eph"),
"extern \"c\" {\n fn host_inc(n: I32): I32\n fn host_now(): I64\n \
fn host_log(n: I32): ()\n}\n\n\
fn apply(f: I32 -> I32, x: I32): I32 = f(x)\n\n\
fn main(): I32 = let t = host_now in let l = host_log in let u = l(1) in \
apply(host_inc, 41)\n",
)
.expect("write program");
let out = ephapax(tmp.path(), &["compile", "ext.eph"]);
assert!(
out.status.success(),
"compile failed:\n{}",
String::from_utf8_lossy(&out.stderr)
);
let wasm = std::fs::read(tmp.path().join("ext.wasm")).expect("read module");
wasmparser::validate(&wasm).expect("the module must be valid wasm");
}

/// The body of a lambda can hold a lambda. The inner closure function was
/// not in the module: "unknown function".
#[test]
fn a_lambda_in_a_lambda_runs() {
each_runs(
&[],
&[
(
"curry",
"fn main(): I32 = let add = fn(a: I32) -> fn(b: I32) -> a + b in add(40)(2)\n",
"42",
),
(
"curry3",
"fn main(): I32 = let add = fn(a: I32) -> fn(b: I32) -> fn(c: I32) -> a + b + c \
in add(30)(10)(2)\n",
"42",
),
(
"compose",
"fn compose(f: I32 -> I32, g: I32 -> I32): I32 -> I32 = fn(x: I32) -> f(g(x))\n\n\
fn main(): I32 = compose(fn(a: I32) -> a + 1, fn(b: I32) -> b * 2)(20) + 1\n",
"42",
),
],
);
}

/// A parameter or a `let` name hides a function of the module that has
/// the same name. The wasm call went to the function of the module.
#[test]
fn a_local_hides_a_function_of_the_same_name() {
let f = "fn f(n: I32): I32 = 0\n\n";
each_runs(
&[],
&[
(
"parameter",
&format!(
"{f}fn apply(f: I32 -> I32, x: I32): I32 = f(x)\n\n\
fn main(): I32 = apply(fn(n: I32) -> n + 1, 41)\n"
),
"42",
),
(
"let_name",
&format!("{f}fn main(): I32 = let f = fn(n: I32) -> n + 1 in f(41)\n"),
"42",
),
],
);
}

/// A lambda with an I64, F32 or F64 parameter or result is behind the
/// flag `--unstable-core`. With the flag, the closure function has the
/// wasm type of its parameter and of its result. (Each closure had the
/// type `(i32, i32) -> i32`, and such a lambda was refused.)
#[test]
fn a_wide_lambda_is_behind_the_flag() {
let programs = [
(
"f64",
"fn main(): F64 = let f = fn(x: F64) -> x + 1.5 in f(40.5)\n",
"42",
),
(
"f32",
"fn main(): F32 = let f = fn(x: F32) -> x + 1.5f32 in f(40.5f32)\n",
"42f32",
),
(
"i64",
"fn main(): I64 = let f = fn(x: I64) -> x + 5000000000 in f(5000000000)\n",
"10000000000i64",
),
(
"result_only",
"fn main(): I64 = let f = fn(x: I32) -> 5000000000 + 5000000000 in f(1)\n",
"10000000000i64",
),
(
"parameter_only",
"fn main(): I32 = let f = fn(x: I64) -> 42 in f(5000000000)\n",
"42",
),
(
"argument",
"fn apply(f: F64 -> F64, x: F64): F64 = f(x)\n\n\
fn main(): F64 = apply(fn(n: F64) -> n * 2.0, 21.0)\n",
"42",
),
(
"captured",
"fn main(): I64 = let k = 5000000000 in let f = fn(x: F64) -> k + k in f(1.5)\n",
"10000000000i64",
),
(
"3_types",
"fn main(): F64 = let f = fn(a: F64) -> fn(b: I64) -> fn(c: F32) -> a \
in f(42.0)(5000000000)(1.5f32)\n",
"42",
),
];
each_runs(&["--unstable-core"], &programs);

// Without the flag, each command refuses the program by name.
let tmp = tempfile::tempdir().expect("tempdir");
for (name, text, _) in programs {
let file = format!("{name}.eph");
std::fs::write(tmp.path().join(&file), text).expect("write program");
for command in [&["check"][..], &["compile"], &["run"], &["run", "--interp"]] {
let args: Vec<&str> = [command, &[file.as_str()]].concat();
let out = ephapax(tmp.path(), &args);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success()
&& stderr.contains("behind the flag `--unstable-core`: a lambda"),
"{name} under {args:?}: exit {:?}, stderr:\n{stderr}",
out.status.code()
);
}
}
}
Loading
Loading