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
2 changes: 2 additions & 0 deletions EXPLAINME.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,8 @@ the per-crate breakdown). Key test categories:
parser does not have (traits, effect declarations, comptime,
contracts), each with the mark `[ALIAS rule]` or `[UNIMPLEMENTED]`;
the parser refuses the fixture of each `[UNIMPLEMENTED]` production
with an error that names the production
(`ParseError::Unimplemented`)
* `docs/specs/DESIGN-DECISIONS.adoc` — 17 ADRs with rationale
* `docs/specs/LANGUAGE-COMPARISON.adoc` — comparison against 10 languages
* `docs/vision/EPHAPAX-VISION.adoc` — design rationale, why dyadic +
Expand Down
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: *614 tests* (`+cargo test --workspace --all-targets+`);
* Total: *618 tests* (`+cargo test --workspace --all-targets+`);
pass/fail enforced by `+rust-ci.yml+`
* Documented all-target tests: 614
* Documented all-target tests: 618

=== 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 — 614 pass, 0 fail
* [x] cargo test –workspace –all-targets — 618 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
242 changes: 169 additions & 73 deletions spec/ephapax-v2-grammar.ebnf

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions src/ephapax-cli/tests/generics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,15 +149,16 @@ fn a_call_can_give_its_type_arguments() {
}
}

// No argument fixes `a`: the brackets do. Wasm only: the interpreter
// cannot call a function with no parameter by its name.
// No argument fixes `a`: the brackets do.
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");
for args in [&["run", "zero.eph"][..], &["run", "--interp", "zero.eph"]] {
let out = ephapax(tmp.path(), args);
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "7", "{args:?}");
}

// A type argument of a function of another module, with and without
// the module qualifier.
Expand Down
175 changes: 172 additions & 3 deletions src/ephapax-cli/tests/run_wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,14 +201,121 @@ fn each_module_calls_its_own_function_of_a_shared_name() {

// `shadow` in `deep` has a parameter with the name of the function
// that the merge renames; the parameter still shadows the function.
// Wasm only: the interpreter reads a parameter that has the name of a
// function as that function, also in a program of one module.
write(
"param.eph",
"module param\n\nimport deep\n\nfn helper(n: I32): I32 = n\n\n\
fn main(): I32 = shadow(0)\n",
);
assert_eq!(stdout_of(tmp.path(), &["run", "param.eph"]), "1000");
for args in [&["run", "param.eph"][..], &["run", "--interp", "param.eph"]] {
assert_eq!(stdout_of(tmp.path(), args), "1000", "{args:?}");
}
}

/// `let (a, b, c) = e` binds the 3 parts of a tuple. The parser used to
/// make a chain of pair patterns for 3 or more names, which no tuple
/// value has as its type: a type error.
#[test]
fn a_binder_with_3_or_more_names_binds_a_tuple() {
let tmp = tempfile::tempdir().expect("tempdir");
// (declarations, type and body of `main`, output)
for (decls, ty, main, want) in [
("", "I32", "let (a, b, c) = (1, 2, 3) in\n a + b * c", "7"),
("", "I32", "let (a, b, c) = (5, 4, 3) in\n a - b - c", "-2"),
(
"fn three(): (I32, I32, I32) = (1, 2, 3)\n",
"I32",
"let (a, b, c) = three()\n a + b * c",
"7",
),
// 4 parts, each at its own type; `_` for a part that is not linear.
(
"fn four(): (I32, I64, F64, I32) = (1, 6000000000, 2.5, 4)\n",
"I64",
"let (a, b, _, d) = four()\n b",
"6000000000i64",
),
// A linear part is bound, and consumed one time.
(
"",
"I32",
"region r {\n let! s = String.new@r(\"hello\")\n \
let (a, t, c) = (1, s, 3)\n a + String.len(t) + c\n }",
"9",
),
// 2 names are a pair, as before.
("", "I32", "let (a, b) = (1, 2) in\n a + b", "3"),
] {
std::fs::write(
tmp.path().join("tuple.eph"),
format!("module tuple\n\n{decls}\nfn main(): {ty} =\n {main}\n"),
)
.expect("write program");
for args in [&["run", "tuple.eph"][..], &["run", "--interp", "tuple.eph"]] {
assert_eq!(stdout_of(tmp.path(), args), want, "{main} under {args:?}");
}
}

// `_` over a linear part is a discard of a linear value.
std::fs::write(
tmp.path().join("discard.eph"),
"module discard\n\nfn main(): I32 =\n region r {\n \
let! s = String.new@r(\"hello\")\n let (a, _, c) = (1, s, 3)\n a + c\n }\n",
)
.expect("write program");
let out = ephapax(tmp.path(), &["check", "discard.eph"]);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success() && stderr.contains("Type error"),
"exit {:?}, stderr:\n{stderr}",
out.status.code()
);
}

/// The interpreter calls a function with 0, 2 or 3 parameters by its
/// name, a parameter shadows a function of the same name, and a borrow of
/// a string is not a use. It used to stop on each of these programs
/// ("Not a function", "Unbound variable", "Linear value used after
/// consumption"); wasm ran them.
#[test]
fn the_interpreter_calls_and_borrows_as_wasm_does() {
let tmp = tempfile::tempdir().expect("tempdir");
// (declarations, `main` body, output)
for (decls, main, want) in [
("fn seven(): I32 = 7\n", "seven() + seven()", "14"),
("fn add(a: I32, b: I32): I32 = a + b\n", "add(3, 4)", "7"),
(
"fn add(a: I32, b: I32): I32 = a + b\n",
"add(add(1, 2), 30)",
"33",
),
(
"fn add3(a: I32, b: I32, c: I32): I32 = a + b * c\n",
"add3(1, 2, 3)",
"7",
),
// The order of the parameters: 10 - 3, not 3 - 10.
("fn sub(a: I32, b: I32): I32 = a - b\n", "sub(10, 3)", "7"),
(
"fn helper(n: I32): I32 = n + 100\n\nfn use_it(helper: I32): I32 = helper + 1\n",
"use_it(5) + helper(0)",
"106",
),
(
"",
"region r {\n let! s = String.new@r(\"hello\")\n \
let n = String.len(&s)\n n + String.len(s)\n }",
"10",
),
] {
std::fs::write(
tmp.path().join("calls.eph"),
format!("module calls\n\n{decls}\nfn main(): I32 =\n {main}\n"),
)
.expect("write program");
for args in [&["run", "calls.eph"][..], &["run", "--interp", "calls.eph"]] {
assert_eq!(stdout_of(tmp.path(), args), want, "{main} under {args:?}");
}
}
}

/// `M.Ctor(args)` builds a value of a data type of the imported module
Expand Down Expand Up @@ -556,6 +663,68 @@ fn a_false_guard_gives_the_value_to_the_next_arm() {
}
}

/// 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`.
#[test]
fn a_pattern_can_name_a_constructor_with_its_module() {
let tmp = tempfile::tempdir().expect("tempdir");
let write =
|name: &str, text: &str| std::fs::write(tmp.path().join(name), text).expect("write module");
write(
"shapes.eph",
"module shapes\n\npub data Shape = Dot | Seg(I32) | Rect(I32, I32)\n\n\
data Hidden = H(I32)\n",
);
// (function body, `main` body, output)
for (body, main, want) in [
(
"match s of | shapes.Dot => 1 | shapes.Seg(n) => n | shapes.Rect(w, h) => w * h end",
"area(shapes.Rect(3, 4)) * 100 + area(Seg(5)) * 10 + area(shapes.Dot)",
"1251",
),
// With a guard, and next to a pattern with no module.
(
"match s of | shapes.Seg(n) if n > 3 => 1 | Seg(n) => 2 | _ => 0 end",
"area(Seg(5)) * 100 + area(Seg(1)) * 10 + area(Dot)",
"120",
),
] {
write(
"app.eph",
&format!(
"module app\n\nimport shapes\n\nfn area(s: Shape): I32 =\n {body}\n\n\
fn main(): I32 = {main}\n"
),
);
for args in [&["run", "app.eph"][..], &["run", "--interp", "app.eph"]] {
assert_eq!(stdout_of(tmp.path(), args), want, "{body} under {args:?}");
}
}

// (pattern, text of the load error)
for (pattern, error) in [
("other.Dot", "`other` is not an imported module"),
("shapes.H(v)", "unknown constructor `H`"),
] {
write(
"bad.eph",
&format!(
"module bad\n\nimport shapes\n\n\
fn f(s: Shape): I32 = match s of | {pattern} => 1 | _ => 0 end\n\n\
fn main(): I32 = f(Dot)\n"
),
);
let out = ephapax(tmp.path(), &["compile", "bad.eph"]);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success() && stderr.contains(error),
"{pattern}: exit {:?}, stderr:\n{stderr}",
out.status.code()
);
}
}

/// A pair pattern binds each component at its own type: a destructuring
/// `let`, and a constructor with several fields (a right-nested pair).
/// Patterns used to read every component as an i32.
Expand Down
31 changes: 30 additions & 1 deletion src/ephapax-desugar/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -379,8 +379,21 @@ impl Desugarer {
}
}

/// The constructor `name` in the current scope.
/// The constructor `name` in the current scope. A pattern can name
/// it as `M.Ctor`: `M` must be an imported module, and the
/// constructor then resolves as `Ctor` (as `M.Ctor(x)` does in an
/// expression).
fn ctor(&self, name: &str) -> Result<&ConstructorInfo, DesugarError> {
let name = match name.split_once('.') {
Some((qualifier, member)) if self.module_qualifiers.contains(qualifier) => member,
Some((qualifier, member)) => {
return Err(DesugarError::QualifiedAccessUnknownModule {
qualifier: qualifier.to_string(),
member: member.to_string(),
})
}
None => name,
};
self.registry
.get_ctor(&self.scope.borrow(), name)
.ok_or_else(|| DesugarError::UnknownConstructor {
Expand Down Expand Up @@ -1661,6 +1674,22 @@ impl Desugarer {
let inner = self.bind_single_pattern(&snd, r, body, span)?;
self.bind_single_pattern(&fst, l, inner, span)
}
// A tuple pattern of variables, wildcards and pairs of them
// lowers to a one-arm core `match` on a tuple pattern, as a
// pair pattern does: the scrutinee is consumed one time.
Pattern::Tuple(elements) if elements.iter().all(Self::is_irrefutable_pair_pattern) => {
Ok(Expr::new(
ExprKind::Match {
scrutinee: Box::new(value.clone()),
arms: vec![ephapax_syntax::MatchArm {
pattern: Self::core_pattern(pattern)?,
guard: None,
body,
}],
},
span,
))
}
Pattern::Tuple(elements) => {
let as_fields: Vec<SurfaceTy> = vec![]; // placeholder
self.bind_pattern_vars(value, elements, &as_fields, body, span)
Expand Down
56 changes: 42 additions & 14 deletions src/ephapax-interp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -710,21 +710,39 @@ impl Interpreter {
})
}

/// A top-level function as a value: a closure for its first
/// parameter, with a lambda in its body for each next parameter. So
/// `f(a, b)`, which is `(f a) b`, gives each parameter its argument.
/// A function with no parameter takes the unit argument of `f()`.
fn function_value(&self, params: Vec<(Var, Ty)>, body: Expr) -> Value {
let mut params = params.into_iter();
let (param, param_ty) = params
.next()
.unwrap_or_else(|| ("_".into(), Ty::Base(BaseTy::Unit)));
let body = params.rev().fold(body, |body, (param, param_ty)| {
let span = body.span;
let lambda = ExprKind::Lambda {
param,
param_ty,
body: Box::new(body),
};
Expr::new(lambda, span)
});
Value::Closure {
param,
param_ty,
body,
env: self.env.clone(),
}
}

fn eval_var(&mut self, name: &Var) -> Result<Value, RuntimeError> {
// Check if it's a function
if let Some((params, _ret_ty, body)) = self.functions.get(name).cloned() {
if params.is_empty() {
// Nullary function - evaluate body
return self.eval(&body);
} else {
// Create closure for first parameter
let (first_param, first_ty) = params[0].clone();
return Ok(Value::Closure {
param: first_param,
param_ty: first_ty,
body: body.clone(),
env: self.env.clone(),
});
// A parameter or a `let` of this name shadows a top-level
// function, also after its value was consumed.
let local = self.env.get(name).is_some() || self.env.is_consumed(name);
if !local {
if let Some((params, _ret_ty, body)) = self.functions.get(name).cloned() {
return Ok(self.function_value(params, body));
Comment thread
hyperpolymath marked this conversation as resolved.
Resolved
}
}

Expand Down Expand Up @@ -1159,6 +1177,16 @@ impl Interpreter {
}

fn eval_borrow(&mut self, inner: &Expr) -> Result<Value, RuntimeError> {
// A borrow of a variable reads the value and leaves it in place:
// `&s` is not a use of `s`.
if let ExprKind::Var(name) = &inner.kind {
if self.env.is_consumed(name) {
return Err(RuntimeError::LinearValueConsumed);
}
if let Some(value) = self.env.get(name) {
return Ok(Value::Borrow(Box::new(value.clone())));
}
}
let val = self.eval(inner)?;
Ok(Value::Borrow(Box::new(val)))
}
Expand Down
Loading
Loading