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

=== 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 — 607 pass, 0 fail
* [x] cargo test –workspace –all-targets — 611 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
2 changes: 1 addition & 1 deletion ephapax-linear/src/affine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ impl AffineChecker {
match &expr.kind {
ExprKind::Lit(_) => {}

ExprKind::Var(name) => {
ExprKind::Var(name) | ExprKind::TypeApp { name, .. } => {
self.consume_var(name);
}

Expand Down
2 changes: 1 addition & 1 deletion ephapax-linear/src/linear.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ impl LinearChecker {
ExprKind::Lit(_) => {}

// --- Variable reference: consume the binding ---
ExprKind::Var(name) => {
ExprKind::Var(name) | ExprKind::TypeApp { name, .. } => {
self.consume_var(name);
}

Expand Down
4 changes: 3 additions & 1 deletion src/ephapax-analysis/src/escape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ impl EscapeAnalysis {

fn analyze_expr(expr: &Expr, escaping: &mut HashSet<SmolStr>, in_escaping_context: bool) {
match &expr.kind {
ExprKind::Var(name) => {
// A generic function at explicit type arguments is a use of the
// function, as a variable is.
ExprKind::Var(name) | ExprKind::TypeApp { name, .. } => {
if in_escaping_context {
escaping.insert(name.clone());
}
Expand Down
4 changes: 3 additions & 1 deletion src/ephapax-analysis/src/free_vars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ impl FreeVarAnalysis {

fn collect(expr: &Expr, free: &mut HashSet<SmolStr>, bound: &mut HashSet<SmolStr>) {
match &expr.kind {
ExprKind::Var(name) => {
// A generic function at explicit type arguments is a use of the
// function, as a variable is.
ExprKind::Var(name) | ExprKind::TypeApp { name, .. } => {
if !bound.contains(name) {
free.insert(name.clone());
}
Expand Down
4 changes: 3 additions & 1 deletion src/ephapax-analysis/src/liveness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ impl LivenessAnalysis {

fn compute(expr: &Expr, live: &mut HashSet<SmolStr>) {
match &expr.kind {
ExprKind::Var(name) => {
// A generic function at explicit type arguments is a use of the
// function, as a variable is.
ExprKind::Var(name) | ExprKind::TypeApp { name, .. } => {
live.insert(name.clone());
}

Expand Down
102 changes: 102 additions & 0 deletions src/ephapax-cli/tests/generics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,94 @@ fn a_generic_function_runs_at_each_type_it_is_used_with() {
}
}

/// A call can give its type arguments: `id[I32](3)`. The checker takes
/// them from the brackets, so a type parameter that no argument fixes
/// has a value.
#[test]
fn a_call_can_give_its_type_arguments() {
let tmp = tempfile::tempdir().expect("tempdir");
let id = "fn id<a>(x: a): a = x\n\n";
// (declarations, type and body of `main`, output)
for (decls, ty, main, want) in [
(id, "I32", "id[I32](3)", "3"),
(id, "I32", "id[I32](41)", "41"),
(id, "F64", "id[F64](2.5)", "2.5"),
// Two type parameters.
(
"fn pair<a, b>(p: (a, b)): (a, b) = p\n\n",
"I64",
"snd(pair[I32, I64]((1, 6000000000)))",
"6000000000i64",
),
// A type parameter of the caller as a type argument, in lower
// case and in upper case. In `g`, `a` is a variable too.
(
"fn id<a>(x: a): a = x\n\nfn g<a>(a: a): a = id[a](a)\n\n\
fn wrap<T>(x: T): T = id[T](x)\n\n",
"I32",
"g(8) * 10 + wrap[I32](5)",
"85",
),
// A region type as a type argument.
(
id,
"I32",
"region r {\n let! s = String.new@r(\"hello\")\n String.len(id[String@r](s))\n }",
"5",
),
// Brackets with one name that is not a type parameter are an
// index, as before: the call is a call of the list element.
(
"",
"I32",
"let fs = [fn(x: I32) -> x + 1, fn(x: I32) -> x + 2] in\n let i = 1 in\n fs[i](3)",
"5",
),
] {
write(
tmp.path(),
"app.eph",
&format!("module app\n\n{decls}fn main(): {ty} =\n {main}\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(),
want,
"{main} under {args:?}:\n{}",
String::from_utf8_lossy(&out.stderr)
);
}
}

// No argument fixes `a`: the brackets do. Wasm only: the interpreter
// cannot call a function with no parameter by its name.
write(
tmp.path(),
"zero.eph",
"module zero\n\nfn zero<a>(): I32 = 7\n\nfn main(): I32 = zero[I64]()\n",
);
let out = ephapax(tmp.path(), &["run", "zero.eph"]);
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "7");

// A type argument of a function of another module, with and without
// the module qualifier.
write(
tmp.path(),
"lib.eph",
"module lib\n\npub fn id<a>(x: a): a = x\n",
);
write(
tmp.path(),
"use.eph",
"module use\n\nimport lib\n\nfn main(): I32 = lib.id[I32](4) + id[I32](5)\n",
);
for args in [&["run", "use.eph"][..], &["run", "--interp", "use.eph"]] {
let out = ephapax(tmp.path(), args);
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "9", "{args:?}");
}
}

/// The merged program has one monomorphic function for each set of type
/// arguments, and no generic function.
#[test]
Expand Down Expand Up @@ -143,6 +231,20 @@ fn generic_programs_the_checker_refuses() {
"fn zero<a>(): I32 = 0\n\nfn main(): I32 = zero()\n",
"Cannot infer a type argument of the generic function `zero`",
),
// Explicit type arguments: the wrong type, a function that is not
// generic, the wrong number.
(
"fn id<a>(x: a): a = x\n\nfn main(): I32 = id[I64](3)\n",
"Type mismatch: expected Base(I64), found Base(I32)",
),
(
"fn inc(x: I32): I32 = x + 1\n\nfn main(): I32 = inc[I32](3)\n",
"`inc` is not a generic function and cannot take type arguments",
),
(
"fn id<a>(x: a): a = x\n\nfn main(): I32 = id[I32, I64](3)\n",
"The generic function `id` has 1 type parameter(s), but the call gives 2 type argument(s)",
),
// No call gives the entry point its type arguments.
(
"fn main<a>(): I32 = 0\n",
Expand Down
Loading
Loading