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

=== 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 — 618 pass, 0 fail
* [x] cargo test –workspace –all-targets — 620 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
8 changes: 3 additions & 5 deletions spec/ephapax-v2-grammar.ebnf
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ unimplemented_reexport = visibility , ( unimplemented_import | import_decl ) ;
(* module m capabilities: [io, network] *)
unimplemented_capability_decl = "capabilities" , ":" , "[" , identifier , { "," , identifier } , "]" ;

(* @capabilities(io) *)
(* __ffi@capabilities(io)("symbol", ...): the capabilities of one FFI call *)
unimplemented_capability_annotation = "@capabilities" , "(" , paren_body , ")" ;

(* @name(args) *)
Expand Down Expand Up @@ -201,9 +201,7 @@ declaration = unimplemented_comptime_decl
(* them. They exist so that downstream tools (hypatia's `@tail_recursive`, *)
(* future `@inline`, `@no_mangle`, etc.) can hang metadata off a decl *)
(* without breaking the parser today. *)
annotation = unimplemented_capability_annotation
| "@" , identifier , [ unimplemented_annotation_args ]
;
annotation = "@" , identifier , [ unimplemented_annotation_args ] ;

(* Extern Blocks *)
(* `extern "abi" { items... }` declares foreign types and function *)
Expand Down Expand Up @@ -472,7 +470,7 @@ construct_expr = constructor_name , [ "(" , [ expr_list ] , ")" | "{" , record_f

(* FFI call: __ffi("symbol_name", arg1, arg2, ...) *)
(* Calls a C ABI function from a linked shared library. *)
ffi_expr = "__ffi" , "(" , string , { "," , expression } , ")" ;
ffi_expr = "__ffi" , [ unimplemented_capability_annotation ] , "(" , string , { "," , expression } , ")" ;

record_literal = "{" , record_field_assign , { "," , record_field_assign } , [ "," ] , "}" ;
(* Three surface forms, all producing the same AST node: *)
Expand Down
2 changes: 2 additions & 0 deletions src/ephapax-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,8 @@ fn run_file(
Ok(())
}

/// `run --interp`: run `main` of `module` in the interpreter and print
/// its value.
fn run_interp(
module: &ephapax_syntax::Module,
ffi_libs: &[String],
Expand Down
3 changes: 3 additions & 0 deletions src/ephapax-cli/src/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,16 @@ pub fn run_main(wasm: &[u8], argv: Argv) -> Result<Val, String> {
Ok(results[0])
}

/// The linear memory that the module exports as `memory`.
fn memory(caller: &mut Caller<'_, Argv>) -> wasmtime::Result<Memory> {
caller
.get_export("memory")
.and_then(|e| e.into_memory())
.ok_or_else(|| Error::msg("module exports no `memory`"))
}

/// Define the host functions that a compiled program can import from the
/// module `env`.
fn define_host(linker: &mut Linker<Argv>) -> wasmtime::Result<()> {
linker.func_wrap("env", "print_i32", |v: i32| println!("{v}"))?;
linker.func_wrap(
Expand Down
2 changes: 2 additions & 0 deletions src/ephapax-cli/tests/driver_agreement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use std::process::Command;

use ephapax_driver::DriverError;

/// The root of the repository.
fn repo_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../..")
Expand Down Expand Up @@ -61,6 +62,7 @@ const LOAD_ERRORS: [&str; 6] = [
"Codegen error",
];

/// The text that the CLI prints for each kind of load error.
fn load_error_text(e: &DriverError) -> &'static str {
match e {
DriverError::Io { .. } => LOAD_ERRORS[0],
Expand Down
2 changes: 2 additions & 0 deletions src/ephapax-cli/tests/generics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use std::process::{Command, Output};

use ephapax_syntax::Decl;

/// 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)
Expand All @@ -22,6 +23,7 @@ fn ephapax(dir: &Path, args: &[&str]) -> Output {
.expect("spawn ephapax")
}

/// Write the program `text` to the file `name` in `dir`.
fn write(dir: &Path, name: &str, text: &str) {
std::fs::write(dir.join(name), text).expect("write program");
}
Expand Down
77 changes: 76 additions & 1 deletion src/ephapax-cli/tests/run_wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};

/// The root of the repository.
fn repo_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.canonicalize()
.expect("repo root")
}

/// 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)
Expand Down Expand Up @@ -306,6 +308,13 @@ fn the_interpreter_calls_and_borrows_as_wasm_does() {
let n = String.len(&s)\n n + String.len(s)\n }",
"10",
),
// A local name of the caller is not in scope in the function it
// calls: `g` in `f` is the function, not the `g` of `main`.
(
"fn g(): I32 = 7\n\nfn f(x: I32): I32 = g() + x\n",
"let g = 0 in f(1) + g",
"8",
),
] {
std::fs::write(
tmp.path().join("calls.eph"),
Expand Down Expand Up @@ -643,7 +652,7 @@ fn a_false_guard_gives_the_value_to_the_next_arm() {
),
(
"match W(o) of | W(Some(x)) if x > 5 => 1 | _ => 0 end",
"cannot have the constructor pattern `Some` inside another pattern",
"the constructor pattern `Some` inside another pattern is not supported",
),
] {
write(
Expand All @@ -663,6 +672,72 @@ fn a_false_guard_gives_the_value_to_the_next_arm() {
}
}

/// A match with no guard honours a literal inside a constructor pattern.
/// The desugarer used to take the pattern inside as a wildcard, and to
/// keep only the last arm of a constructor: `Some(0)` matched each
/// `Some`, or was lost. A constructor inside a constructor pattern has
/// no lowering yet: it is refused by name, not taken as a wildcard.
#[test]
fn a_pattern_inside_a_constructor_pattern_is_not_a_wildcard() {
let tmp = tempfile::tempdir().expect("tempdir");
let opt = "data Opt = None | Some(I32)\n\n";
// (declarations, `main` body, output)
for (decls, main, want) in [
(
format!(
"{opt}fn f(o: Opt): I32 = match o of | Some(0) => 1 | Some(n) => 2 | None => 3 end\n"
),
"f(Some(0)) * 100 + f(Some(5)) * 10 + f(None)",
"123",
),
(
"data P = MkP(I32, I32)\n\n\
fn f(p: P): I32 = match p of | MkP(0, y) => y | MkP(x, 0) => x | MkP(x, y) => x + y end\n"
.to_string(),
"f(MkP(0, 7)) * 100 + f(MkP(4, 0)) * 10 + f(MkP(2, 3))",
"745",
),
] {
std::fs::write(
tmp.path().join("inside.eph"),
format!("module inside\n\n{decls}\nfn main(): I32 = {main}\n"),
)
.expect("write program");
for args in [&["run", "inside.eph"][..], &["run", "--interp", "inside.eph"]] {
assert_eq!(stdout_of(tmp.path(), args), want, "{main} under {args:?}");
}
}

// (declarations and match, text of the error)
for (program, error) in [
// `Some(5)` has no arm.
(
format!("{opt}fn f(o: Opt): I32 = match o of | Some(0) => 1 | None => 3 end\n"),
"NonExhaustiveMatch",
),
(
format!(
"{opt}data Wrap = W(Opt)\n\n\
fn f(w: Wrap): I32 = match w of | W(Some(x)) => 1 | W(None) => 0 end\n"
),
"the constructor pattern `Some` inside another pattern is not supported",
),
] {
std::fs::write(
tmp.path().join("bad.eph"),
format!("module bad\n\n{program}\nfn main(): I32 = 0\n"),
)
.expect("write program");
let out = ephapax(tmp.path(), &["compile", "bad.eph"]);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success() && stderr.contains(error),
"exit {:?}, stderr:\n{stderr}",
out.status.code()
);
}
}

/// A pattern can name a constructor with its module: `shapes.Rect(w, h)`,
/// as an expression can since `a_qualified_constructor_takes_its_arguments`.
/// The module must be an import, and the type must be `pub`.
Expand Down
7 changes: 7 additions & 0 deletions src/ephapax-cli/tests/typed_codegen_matrix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ struct Case {
expr: String,
}

/// One case from its result type and its expression.
fn case(ty: &'static str, expr: impl Into<String>) -> Case {
Case {
ty,
Expand Down Expand Up @@ -82,9 +83,13 @@ fn samples(ty: &str) -> Vec<String> {
.collect()
}

/// The numeric types.
const NUMERIC: [&str; 4] = ["I32", "I64", "F32", "F64"];
/// The types that a wasm value can have.
const VALUE_TYPES: [&str; 5] = ["I32", "I64", "F32", "F64", "Bool"];

/// Each case of the matrix: the operators, the conversions and the
/// value forms, at each type.
fn cases_matrix() -> Vec<Case> {
let mut out = Vec::new();

Expand Down Expand Up @@ -270,6 +275,7 @@ fn show(ty: &str, val: Val) -> String {
}
}

/// Load a program through the driver, as one merged module.
fn load(src: &str) -> ephapax_syntax::Module {
let stdlib = ephapax_driver::stdlib_dir(None);
ephapax_driver::load_source(src, "matrix", None, stdlib.as_deref())
Expand All @@ -278,6 +284,7 @@ fn load(src: &str) -> ephapax_syntax::Module {
.expect("the matrix module merges")
}

/// A linker with the print functions of `env`, which do nothing here.
fn linker(engine: &Engine) -> Linker<()> {
let mut linker = Linker::new(engine);
linker.func_wrap("env", "print_i32", |_: i32| {}).unwrap();
Expand Down
42 changes: 31 additions & 11 deletions src/ephapax-desugar/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,11 @@ pub enum DesugarError {
#[error("type arguments need the name of a generic function before them, as in `id[I32](3)`")]
TypeArgumentsOnExpression,

/// A `match` with a guard goes to the core `match`, which has no
/// form for a constructor inside another pattern.
#[error(
"a match with a guard cannot have the constructor pattern `{ctor}` inside another pattern"
)]
GuardWithNestedConstructor { ctor: String },
/// A constructor pattern inside another pattern (`W(Some(x))`). The
/// lowering has no code for it yet, and it does not take such a
/// pattern as a wildcard.
#[error("the constructor pattern `{ctor}` inside another pattern is not supported")]
NestedConstructorPattern { ctor: String },

#[error("qualified access `{qualifier}.{member}`: `{qualifier}` is not an imported module (record field access is not yet supported)")]
QualifiedAccessUnknownModule { qualifier: String, member: String },
Expand Down Expand Up @@ -1343,13 +1342,18 @@ impl Desugarer {
indexed.push((index, arm));
}

// An arm can refuse a value of its constructor: with a guard, or
// with a literal or a constructor inside its pattern. The arms
// are then tried in source order.
let ordered = guarded || arms.iter().any(|a| Self::can_refuse_payload(&a.pattern));

// The arms that can take constructor `i`, in the order to try.
// With a guard in the match: each arm for `i`, in source order.
// With no guard, one arm: the last arm for `i`, or else the last
// When an arm can refuse a value: each arm for `i`, in source
// order. If not, one arm: the last arm for `i`, or else the last
// arm for every constructor.
let chains: Vec<Vec<&MatchArm>> = (0..ctors.len())
.map(|i| {
if guarded {
if ordered {
return indexed
.iter()
.filter(|(index, _)| index.is_none_or(|n| n == i))
Expand Down Expand Up @@ -1439,6 +1443,20 @@ impl Desugarer {
}
}

/// If a constructor pattern has a literal or a constructor inside it:
/// the arm can then refuse a value of its own constructor.
fn can_refuse_payload(pattern: &Pattern) -> bool {
fn can_refuse(pattern: &Pattern) -> bool {
match pattern {
Pattern::Literal(_) | Pattern::Constructor { .. } => true,
Pattern::Pair(left, right) => can_refuse(left) || can_refuse(right),
Pattern::Tuple(parts) => parts.iter().any(can_refuse),
Pattern::Var(_) | Pattern::Wildcard | Pattern::Unit => false,
}
}
matches!(pattern, Pattern::Constructor { args, .. } if args.iter().any(can_refuse))
}

/// The code for one constructor: `chain` has the arms to try on its
/// payload, in order. A chain where each arm has a guard does not
/// cover the constructor. For a longer chain, the type checker finds
Expand All @@ -1451,7 +1469,9 @@ impl Desugarer {
span: Span,
) -> Result<Expr, DesugarError> {
match chain {
[arm] if arm.guard.is_none() => self.build_arm_body_inner(payload, ctor, arm, span),
[arm] if arm.guard.is_none() && !Self::can_refuse_payload(&arm.pattern) => {
self.build_arm_body_inner(payload, ctor, arm, span)
}
_ if chain.iter().any(|arm| arm.guard.is_none()) => {
// A pattern on the constructor is a pattern on its
// payload: `()`, the one field, or a right-nested pair.
Expand Down Expand Up @@ -1523,7 +1543,7 @@ impl Desugarer {
),
Pattern::Tuple(elements) => CorePattern::Tuple(all(elements)?),
Pattern::Constructor { ctor, .. } => {
return Err(DesugarError::GuardWithNestedConstructor {
return Err(DesugarError::NestedConstructorPattern {
ctor: ctor.to_string(),
})
}
Expand Down
1 change: 1 addition & 0 deletions src/ephapax-driver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@ pub fn load_source(
mod tests {
use super::*;

/// The names of the extern functions of `module`, in order.
fn extern_fn_names(module: &Module) -> Vec<String> {
let mut names = Vec::new();
for decl in &module.decls {
Expand Down
4 changes: 4 additions & 0 deletions src/ephapax-driver/src/qualify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ fn scopes(modules: &[LoadedModule], qualified: &[Renames]) -> Vec<Renames> {
.collect()
}

/// Rename the names of `scope` in one declaration: a function with its
/// parameters and its body, or the value of a constant.
fn rename_decl(decl: &mut Decl, scope: &Renames) {
match decl {
Decl::Fn {
Expand All @@ -133,6 +135,7 @@ fn rename_decl(decl: &mut Decl, scope: &Renames) {
}
}

/// Give `name` its new name, if `scope` has one for it.
fn rename(name: &mut Var, scope: &Renames) {
if let Some(new) = scope.get(name) {
*name = new.clone();
Expand Down Expand Up @@ -200,6 +203,7 @@ fn rename_expr(expr: &mut Expr, scope: &Renames) {
}
}

/// Rename the variables that `pattern` binds.
fn rename_pattern(pattern: &mut Pattern, scope: &Renames) {
match pattern {
Pattern::Wildcard | Pattern::Literal(_) | Pattern::Unit => {}
Expand Down
Loading
Loading