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
41 changes: 41 additions & 0 deletions .github/actions-allowlist.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# SPDX-License-Identifier: MPL-2.0
# Owner: Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
#
# Reviewed copy of the Actions allow-list of this repository:
# Settings > Actions > General > "Allow ... specified actions and reusable
# workflows". GitHub does not read this file; the owner changes the setting
# by hand. When a workflow needs a new action, add the entry here in the
# same pull request, then update the setting.
#
# Also required on that page: "Allow actions created by GitHub"
# (actions/*, github/codeql-action).
#
# Source of the entries: each `uses:` in .github/workflows/*.yml, and the
# `uses` list of metadatastician/standards in .github/workflows/actions.lock
# (what the reusable workflows use). To paste the list into the settings
# page, take the lines that do not start with `#`.

# This repository's own workflows.
Swatinem/rust-cache@*,
swatinem/rust-cache@*,
dawidd6/action-send-mail@*,
dtolnay/rust-toolchain@*,
mlugg/setup-zig@*,
peter-evans/repository-dispatch@*,

# The reusable workflows of the standards repository, and what they use.
metadatastician/standards/.github/workflows/*@*,
denoland/setup-deno@*,
editorconfig-checker/action-editorconfig-checker@*,
erlef/setup-beam@*,
goto-bus-stop/setup-zig@*,
ossf/scorecard-action@*,
oven-sh/setup-bun@*,
webfactory/ssh-agent@*,

# The owner's own repositories.
hyperpolymath/*,

# No workflow uses these yet. Kept by owner decision (2026-11-10).
extractions/setup-just@*,
ludeeus/action-shellcheck@*
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: *590 tests* (`+cargo test --workspace --all-targets+`);
* Total: *600 tests* (`+cargo test --workspace --all-targets+`);
pass/fail enforced by `+rust-ci.yml+`
* Documented all-target tests: 596
* Documented all-target tests: 600

=== 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 — 467 pass, 0 fail
* [x] cargo test –workspace –all-targets — 600 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
30 changes: 30 additions & 0 deletions src/ephapax-cli/tests/run_wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,3 +248,33 @@ fn pair_patterns_bind_each_component_at_its_type() {
}
}
}

/// The binder of a `case` arm is in scope in that arm only. It used to
/// stay bound after the `case`, so a later use of an outer variable of
/// the same name read the binder.
#[test]
fn an_arm_binder_does_not_replace_an_outer_variable() {
let tmp = tempfile::tempdir().expect("tempdir");
for (payload, value, arm, want) in [
("I32", "7", "x", "12"),
// The binder has another wasm type than the outer variable.
("I64", "7000000000", "1", "6"),
] {
std::fs::write(
tmp.path().join("scope.eph"),
format!(
"module scope\n\ndata Opt = Yes({payload}) | No\n\nfn main(): I32 =\n \
let x = 5\n let r = match Yes({value}) of\n | Yes(x) => {arm}\n \
| No => 0\n end\n r + x\n"
),
)
.expect("write program");
for args in [&["run", "scope.eph"][..], &["run", "--interp", "scope.eph"]] {
assert_eq!(
stdout_of(tmp.path(), args),
want,
"{payload} under {args:?}"
);
}
}
}
109 changes: 109 additions & 0 deletions src/ephapax-cli/tests/wasm_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,112 @@ fn e2e_wasm_header_valid() {
assert_eq!(&wasm[0..4], b"\x00asm", "bad WASM magic");
assert_eq!(&wasm[4..8], &[1, 0, 0, 0], "bad WASM version");
}

// =========================================================================
// Core `match` on a hand-built module: the result of `main` is checked
// =========================================================================

/// Compile a module whose only function is `main(): <ret_ty> = body`, run
/// it, and return the result of `main`.
fn run_main<R: wasmtime::WasmResults>(ret_ty: ephapax_syntax::Ty, body: ephapax_syntax::Expr) -> R {
use ephapax_syntax::{Decl, Module, Visibility};
let module = Module {
name: "t".into(),
imports: vec![],
decls: vec![Decl::Fn {
name: "main".into(),
visibility: Visibility::Private,
type_params: vec![],
params: vec![],
ret_ty,
body,
}],
};
let wasm = ephapax_wasm::compile_module(&module).expect("the module compiles");
wasmparser::validate(&wasm).expect("emitted wasm failed wasmparser validation");

let engine = wasmtime::Engine::default();
let module = wasmtime::Module::new(&engine, &wasm).expect("WASM module creation failed");
let mut store = wasmtime::Store::new(&engine, ());
let mut linker = wasmtime::Linker::new(&engine);
linker
.func_wrap("env", "print_i32", |_val: i32| {})
.expect("failed to define print_i32");
linker
.func_wrap("env", "print_string", |_ptr: i32, _len: i32| {})
.expect("failed to define print_string");
let instance = linker
.instantiate(&mut store, &module)
.expect("WASM instantiation failed");
instance
.get_typed_func::<(), R>(&mut store, "main")
.expect("main function not found")
.call(&mut store, ())
.expect("WASM execution trapped")
}

/// A tuple pattern reads the 8-byte slots of the tuple, each at its own
/// type. It used to follow pair pointers, which a tuple does not have.
#[test]
fn e2e_tuple_pattern_reads_the_tuple_slots() {
use ephapax_syntax::{BaseTy, Expr, ExprKind, Literal, MatchArm, Pattern, Ty};
let lit = |l| Expr::dummy(ExprKind::Lit(l));
// match (1, 5000000000, 3) of | (a, b, c) => b end
let body = Expr::dummy(ExprKind::Match {
scrutinee: Box::new(Expr::dummy(ExprKind::TupleLit(vec![
lit(Literal::I32(1)),
lit(Literal::I64(5_000_000_000)),
lit(Literal::I32(3)),
]))),
arms: vec![MatchArm {
pattern: Pattern::Tuple(vec![
Pattern::Var("a".into()),
Pattern::Var("b".into()),
Pattern::Var("c".into()),
]),
guard: None,
body: Expr::dummy(ExprKind::Var("b".into())),
}],
});
let result: i64 = run_main(Ty::Base(BaseTy::I64), body);
assert_eq!(result, 5_000_000_000);
}

/// A name that one `match` arm binds is not in scope in the next arm.
/// The next arm used to read the binder of the arm that did not match.
#[test]
fn e2e_match_arm_binder_ends_with_its_arm() {
use ephapax_syntax::{BaseTy, Expr, ExprKind, Literal, MatchArm, Pattern, Ty};
let lit = |n| Expr::dummy(ExprKind::Lit(Literal::I32(n)));
let var = |name: &str| Expr::dummy(ExprKind::Var(name.into()));
// let y = 5 in match (1, 2) of | (0, y) => y | _ => y end
let matched = Expr::dummy(ExprKind::Match {
scrutinee: Box::new(Expr::dummy(ExprKind::Pair {
left: Box::new(lit(1)),
right: Box::new(lit(2)),
})),
arms: vec![
MatchArm {
pattern: Pattern::Pair(
Box::new(Pattern::Literal(Literal::I32(0))),
Box::new(Pattern::Var("y".into())),
),
guard: None,
body: var("y"),
},
MatchArm {
pattern: Pattern::Wildcard,
guard: None,
body: var("y"),
},
],
});
let body = Expr::dummy(ExprKind::Let {
name: "y".into(),
ty: None,
value: Box::new(lit(5)),
body: Box::new(matched),
});
let result: i32 = run_main(Ty::Base(BaseTy::I32), body);
assert_eq!(result, 5);
}
62 changes: 35 additions & 27 deletions src/ephapax-driver/src/qualify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,33 +51,46 @@ fn module_key(name: &str) -> String {
/// its names (they are the entry points of the program); after it, the
/// first module that declares a name keeps it.
pub(crate) fn qualify_functions(modules: &mut [LoadedModule]) {
let root = modules.len().saturating_sub(1);
let qualified = qualified_names(modules);
if qualified.iter().all(Renames::is_empty) {
return;
}
let scopes = scopes(modules, &qualified);
for (m, scope) in modules.iter_mut().zip(&scopes) {
if !scope.is_empty() {
m.core.decls.iter_mut().for_each(|d| rename_decl(d, scope));
}
}
}

// The new name of each function that must change, by module.
/// For each module, the new name of each of its functions that must
/// change: `<module>::<function>`.
fn qualified_names(modules: &[LoadedModule]) -> Vec<Renames> {
let root = modules.len().saturating_sub(1);
let mut owner = HashMap::new();
let mut qualified = vec![Renames::new(); modules.len()];
for i in std::iter::once(root).chain(0..root) {
let Some(m) = modules.get(i) else { return };
let Some(m) = modules.get(i) else { break };
for (name, _, function) in callables(&m.core) {
if function && *owner.entry(name).or_insert(i) != i {
let new = format!("{}::{name}", m.logical_path);
qualified[i].insert(name.clone(), new.into());
}
}
}
if qualified.iter().all(Renames::is_empty) {
return;
}
qualified
}

// What each name means in each module, in the order the type checker
// brings names into scope: the imports in order (a later one shadows
// an earlier one), then the declarations of the module.
/// For each module, what each renamed name means there, in the order the
/// type checker brings names into scope: the imports in order (a later
/// one shadows an earlier one), then the declarations of the module.
fn scopes(modules: &[LoadedModule], qualified: &[Renames]) -> Vec<Renames> {
let index: HashMap<String, usize> = modules
.iter()
.enumerate()
.map(|(i, m)| (module_key(&m.core.name), i))
.collect();
let scopes: Vec<Renames> = modules
modules
.iter()
.enumerate()
.map(|(i, m)| {
Expand All @@ -103,25 +116,20 @@ pub(crate) fn qualify_functions(modules: &mut [LoadedModule]) {
}
scope
})
.collect();
.collect()
}

for (m, scope) in modules.iter_mut().zip(&scopes) {
if scope.is_empty() {
continue;
}
for decl in &mut m.core.decls {
match decl {
Decl::Fn {
name, params, body, ..
} => {
rename(name, scope);
params.iter_mut().for_each(|(p, _)| rename(p, scope));
rename_expr(body, scope);
}
Decl::Const { value, .. } => rename_expr(value, scope),
Decl::Type { .. } | Decl::Extern { .. } | Decl::Data { .. } => {}
}
fn rename_decl(decl: &mut Decl, scope: &Renames) {
match decl {
Decl::Fn {
name, params, body, ..
} => {
rename(name, scope);
params.iter_mut().for_each(|(p, _)| rename(p, scope));
rename_expr(body, scope);
}
Decl::Const { value, .. } => rename_expr(value, scope),
Decl::Type { .. } | Decl::Extern { .. } | Decl::Data { .. } => {}
}
}

Expand Down
Loading
Loading