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: *600 tests* (`+cargo test --workspace --all-targets+`);
* Total: *605 tests* (`+cargo test --workspace --all-targets+`);
pass/fail enforced by `+rust-ci.yml+`
* Documented all-target tests: 600
* Documented all-target tests: 605

=== 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 — 600 pass, 0 fail
* [x] cargo test –workspace –all-targets — 605 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
4 changes: 2 additions & 2 deletions src/ephapax-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ fn run_file(
} if name == "main" => Some((params.len(), ret_ty.clone())),
_ => None,
});
let module = program.into_merged();
let module = program.into_merged().map_err(|e| report_load_error(&e))?;
let Some((n_params, ret_ty)) = main else {
if verbose {
println!("{} Module loaded (no main function)", "✓".green());
Expand Down Expand Up @@ -516,7 +516,7 @@ fn compile_file(
}

finish_compile(
program.into_merged(),
program.into_merged().map_err(|e| report_load_error(&e))?,
&filename,
path,
output,
Expand Down
189 changes: 189 additions & 0 deletions src/ephapax-cli/tests/generics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
// SPDX-License-Identifier: MPL-2.0
// Owner: Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
//
// generics.rs, v2 plan rung E7 gate (generic functions).
//
// A generic function is checked one time, with each type parameter as an
// opaque linear type. The program then gets one monomorphic copy of the
// function for each set of type arguments it is used with, and the
// generic definition is removed: codegen and the interpreter see
// monomorphic functions only.

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

use ephapax_syntax::Decl;

fn ephapax(dir: &Path, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_ephapax"))
.current_dir(dir)
.args(args)
.output()
.expect("spawn ephapax")
}

fn write(dir: &Path, name: &str, text: &str) {
std::fs::write(dir.join(name), text).expect("write program");
}

/// `id` used at three types, with two inputs for each: every copy
/// returns its input, as wasm and in the interpreter.
#[test]
fn a_generic_function_runs_at_each_type_it_is_used_with() {
let tmp = tempfile::tempdir().expect("tempdir");
for (ty, input, want) in [
("I32", "3", "3"),
("I32", "41", "41"),
("I64", "5000000000", "5000000000i64"),
("I64", "6000000000", "6000000000i64"),
("F64", "2.5", "2.5"),
("F64", "0.25", "0.25"),
] {
write(
tmp.path(),
"g.eph",
&format!("module g\n\nfn id<a>(x: a): a = x\n\nfn main(): {ty} = id({input})\n"),
);
for args in [&["run", "g.eph"][..], &["run", "--interp", "g.eph"]] {
let out = ephapax(tmp.path(), args);
assert_eq!(
String::from_utf8_lossy(&out.stdout).trim(),
want,
"id({input}) at {ty} under {args:?}:\n{}",
String::from_utf8_lossy(&out.stderr)
);
}
}

// An upper-case type parameter, also in an annotation of the body.
write(
tmp.path(),
"upper.eph",
"module upper\n\nfn apply<T>(x: T): T =\n let f = fn(y: T) -> y\n f(x)\n\n\
fn main(): I32 = apply(3)\n",
);
for args in [&["run", "upper.eph"][..], &["run", "--interp", "upper.eph"]] {
let out = ephapax(tmp.path(), args);
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "3", "{args:?}");
}

// A generic function of another module, and one that calls another
// generic function.
write(
tmp.path(),
"lib.eph",
"module lib\n\npub fn id<a>(x: a): a = x\n\npub fn twice<a>(x: a): a = id(id(x))\n",
);
write(
tmp.path(),
"app.eph",
"module app\n\nimport lib\n\nfn main(): F64 = twice(1.5)\n",
);
for args in [&["run", "app.eph"][..], &["run", "--interp", "app.eph"]] {
let out = ephapax(tmp.path(), args);
assert_eq!(
String::from_utf8_lossy(&out.stdout).trim(),
"1.5",
"{args:?}"
);
}
}

/// The merged program has one monomorphic function for each set of type
/// arguments, and no generic function.
#[test]
fn a_program_has_one_copy_for_each_set_of_type_arguments() {
let tmp = tempfile::tempdir().expect("tempdir");
write(
tmp.path(),
"two.eph",
"module two\n\nfn id<a>(x: a): a = x\n\nfn main(): I64 =\n let n = id(7)\n id(6000000000)\n",
);
let merged = ephapax_driver::load_program(&tmp.path().join("two.eph"), None)
.expect("loads")
.into_merged()
.expect("merges");
let mut functions = Vec::new();
for decl in &merged.decls {
if let Decl::Fn {
name, type_params, ..
} = decl
{
assert!(type_params.is_empty(), "`{name}` is still generic");
functions.push(name.as_str());
}
}
assert_eq!(functions, ["main", "id<I32>", "id<I64>"]);
}

/// What the checker refuses in a generic program, each with a named
/// `TypeError`, on `check` and on `compile`.
#[test]
fn generic_programs_the_checker_refuses() {
let tmp = tempfile::tempdir().expect("tempdir");
// (program after the module header, text of the error)
for (program, error) in [
// A value of a type-parameter type can be linear in a use of the
// function: no second use, and no discard.
(
"fn dup<a>(x: a): (a, a) = (x, x)\n\nfn main(): I32 = fst(dup(3))\n",
"Linear variable `x` used more than once",
),
(
"fn k<a>(x: a): I32 = 0\n\nfn main(): I32 = k(3)\n",
"Linear variable `x` not consumed",
),
// Each call needs its own copy of `f`: copies without end.
(
"fn f<a>(x: a): I32 = f((x, 1))\n\nfn main(): I32 = f(3)\n",
"Polymorphic recursion",
),
// Nothing in the call fixes `a`.
(
"fn zero<a>(): I32 = 0\n\nfn main(): I32 = zero()\n",
"Cannot infer a type argument of the generic function `zero`",
),
// No call gives the entry point its type arguments.
(
"fn main<a>(): I32 = 0\n",
"`main` is the entry point of a program and cannot have type parameters",
),
] {
write(tmp.path(), "bad.eph", &format!("module bad\n\n{program}"));
for args in [&["check", "bad.eph"][..], &["compile", "bad.eph"]] {
let out = ephapax(tmp.path(), args);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success() && stderr.contains("Type error") && stderr.contains(error),
"{args:?} on:\n{program}\nexit {:?}, stderr:\n{stderr}",
out.status.code()
);
}
}
}

/// A limit of the typed lowering that only a copy at a wide type shows
/// (here a lambda with an F64 parameter) is a load error on `check` too.
#[test]
fn check_sees_the_limits_of_the_monomorphic_copies() {
let tmp = tempfile::tempdir().expect("tempdir");
let program = |ty: &str, input: &str| {
format!(
"module lam\n\nfn apply<a>(x: a): a =\n let f = fn(y: a) -> y\n f(x)\n\n\
fn main(): {ty} = apply({input})\n"
)
};
write(tmp.path(), "lam.eph", &program("I32", "3"));
assert!(ephapax(tmp.path(), &["check", "lam.eph"]).status.success());

write(tmp.path(), "lam.eph", &program("F64", "2.5"));
for args in [&["check", "lam.eph"][..], &["compile", "lam.eph"]] {
let out = ephapax(tmp.path(), args);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success() && stderr.contains("Codegen error") && stderr.contains("lambda"),
"{args:?}: exit {:?}, stderr:\n{stderr}",
out.status.code()
);
}
}
1 change: 1 addition & 0 deletions src/ephapax-cli/tests/typed_codegen_matrix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ fn load(src: &str) -> ephapax_syntax::Module {
ephapax_driver::load_source(src, "matrix", None, stdlib.as_deref())
.unwrap_or_else(|e| panic!("the matrix module must load: {e}"))
.into_merged()
.expect("the matrix module merges")
}

fn linker(engine: &Engine) -> Linker<()> {
Expand Down
26 changes: 25 additions & 1 deletion src/ephapax-desugar/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,10 @@ pub struct Desugarer {
/// Named types currently being expanded by `desugar_named_type`,
/// innermost last. Re-entering one is a recursive type.
expanding: RefCell<Vec<SmolStr>>,
/// The type parameters of the function being desugared. In its
/// signature and body such a name is a type variable, also when it
/// starts with an upper-case letter (`fn id<T>(x: T): T`).
fn_type_params: Vec<SmolStr>,
}

impl Desugarer {
Expand All @@ -267,6 +271,7 @@ impl Desugarer {
registry: DataRegistry::new(),
module_qualifiers: HashSet::new(),
expanding: RefCell::new(Vec::new()),
fn_type_params: Vec::new(),
}
}

Expand All @@ -276,6 +281,7 @@ impl Desugarer {
registry,
module_qualifiers: HashSet::new(),
expanding: RefCell::new(Vec::new()),
fn_type_params: Vec::new(),
}
}

Expand Down Expand Up @@ -320,20 +326,23 @@ impl Desugarer {
SurfaceDecl::Fn {
name,
visibility,
type_params,
params,
ret_ty,
body,
} => {
self.fn_type_params.clone_from(type_params);
let core_params: Vec<_> = params
.iter()
.map(|(n, t)| Ok((n.clone(), self.desugar_ty(t)?)))
.collect::<Result<_, DesugarError>>()?;
let core_ret = self.desugar_ty(ret_ty)?;
let core_body = self.desugar_expr(body)?;
self.fn_type_params.clear();
core_decls.push(Decl::Fn {
name: name.clone(),
visibility: lower_visibility(*visibility),
type_params: vec![],
type_params: type_params.clone(),
Comment thread
hyperpolymath marked this conversation as resolved.
Resolved
params: core_params,
ret_ty: core_ret,
body: core_body,
Expand Down Expand Up @@ -684,6 +693,18 @@ impl Desugarer {
/// codegen). Two distinct extern types `Window` and `IpcChannel`
/// produce two distinct `Ty::Var` rigids that never unify.
fn desugar_named_type(&self, name: &SmolStr, args: &[SurfaceTy]) -> Result<Ty, DesugarError> {
// A type parameter of the enclosing function shadows every other
// meaning of the name.
if self.fn_type_params.contains(name) {
if !args.is_empty() {
return Err(DesugarError::TypeArityMismatch {
name: name.to_string(),
expected: 0,
got: args.len(),
});
}
return Ok(Ty::Var(name.clone()));
}
// Built-in type aliases that aren't (yet) keywords in the lexer.
// `Unit` is the type-position spelling of the literal `()`;
// bridge.eph and other ML-adjacent corpora write it freely.
Expand Down Expand Up @@ -1795,6 +1816,7 @@ mod tests {
SurfaceDecl::Fn {
name: "unwrap_or".into(),
visibility: SurfaceVisibility::Private,
type_params: vec![],
params: vec![
(
"opt".into(),
Expand Down Expand Up @@ -1879,6 +1901,7 @@ mod tests {
SurfaceDecl::Fn {
name: "test".into(),
visibility: SurfaceVisibility::Private,
type_params: vec![],
params: vec![(
"opt".into(),
SurfaceTy::Named {
Expand Down Expand Up @@ -1944,6 +1967,7 @@ mod tests {
SurfaceDecl::Fn {
name: "test".into(),
visibility: SurfaceVisibility::Private,
type_params: vec![],
params: vec![(
"opt".into(),
SurfaceTy::Named {
Expand Down
Loading
Loading