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

=== 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 — 605 pass, 0 fail
* [x] cargo test –workspace –all-targets — 607 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
37 changes: 37 additions & 0 deletions src/ephapax-cli/tests/generics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,43 @@ fn generic_programs_the_checker_refuses() {
}
}

/// Only the `main` of the root module is the entry point. A generic
/// `main` of an imported module is a function like any other; the same
/// module as the root of a program is refused.
#[test]
fn a_generic_main_of_an_imported_module_is_not_an_entry_point() {
let tmp = tempfile::tempdir().expect("tempdir");
write(
tmp.path(),
"lib.eph",
"module lib\n\nfn main<a>(x: a): a = x\n\npub fn keep(n: I32): I32 = main(n)\n",
);
for input in ["7", "11"] {
write(
tmp.path(),
"app.eph",
&format!("module app\n\nimport lib\n\nfn main(): I32 = keep({input})\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(),
input,
"{args:?}:\n{}",
String::from_utf8_lossy(&out.stderr)
);
}
}

let out = ephapax(tmp.path(), &["check", "lib.eph"]);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success() && stderr.contains("cannot have type parameters"),
"exit {:?}, 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]
Expand Down
53 changes: 53 additions & 0 deletions src/ephapax-cli/tests/run_wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,59 @@ fn each_module_calls_its_own_function_of_a_shared_name() {
assert_eq!(stdout_of(tmp.path(), &["run", "param.eph"]), "1000");
}

/// `M.Ctor(args)` builds a value of a data type of the imported module
/// `M`, with 0, 1 or 2 fields. The desugarer used to read `M.Ctor` as a
/// constructor with no arguments, then refuse the arguments.
#[test]
fn a_qualified_constructor_takes_its_arguments() {
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\
pub fn area(s: Shape): I32 =\n \
match s of | Dot => 1 | Seg(n) => n | Rect(w, h) => w * h end\n",
);
for (shape, want) in [
("shapes.Dot", "1"),
("shapes.Dot()", "1"),
("shapes.Seg(5)", "5"),
("shapes.Seg(7)", "7"),
("shapes.Rect(3, 4)", "12"),
("shapes.Rect(4, 5)", "20"),
] {
write(
"app.eph",
&format!("module app\n\nimport shapes\n\nfn main(): I32 = shapes.area({shape})\n"),
);
for args in [&["run", "app.eph"][..], &["run", "--interp", "app.eph"]] {
assert_eq!(stdout_of(tmp.path(), args), want, "{shape} under {args:?}");
}
}

// (call, text of the load error)
for (shape, error) in [
(
"shapes.Seg(5, 6)",
"constructor `Seg` expects 1 arguments, got 2",
),
("other.Seg(5)", "`other` is not an imported module"),
] {
write(
"bad.eph",
&format!("module bad\n\nimport shapes\n\nfn main(): I32 = shapes.area({shape})\n"),
);
let out = ephapax(tmp.path(), &["compile", "bad.eph"]);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success() && stderr.contains(error),
"{shape}: 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
53 changes: 49 additions & 4 deletions src/ephapax-desugar/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -470,10 +470,15 @@ impl Desugarer {
body: Box::new(self.desugar_expr(body)?),
},

SurfaceExprKind::App { func, arg } => ExprKind::App {
func: Box::new(self.desugar_expr(func)?),
arg: Box::new(self.desugar_expr(arg)?),
},
SurfaceExprKind::App { func, arg } => {
if let Some((ctor, args)) = self.qualified_ctor_call(expr) {
return self.desugar_construct(ctor, &args, span);
}
ExprKind::App {
func: Box::new(self.desugar_expr(func)?),
arg: Box::new(self.desugar_expr(arg)?),
}
}

SurfaceExprKind::Pair { left, right } => ExprKind::Pair {
left: Box::new(self.desugar_expr(left)?),
Expand Down Expand Up @@ -597,6 +602,46 @@ impl Desugarer {
Ok(Expr::new(kind, span))
}

/// The constructor and the arguments of `M.Ctor(a, b)`, if `expr` is
/// such a call on an imported module. The parser gives the call as
/// `App(App(M.Ctor, a), b)`, and `M.Ctor()` as `App(M.Ctor, ())`.
fn qualified_ctor_call<'e>(
&self,
expr: &'e SurfaceExpr,
) -> Option<(&'e SmolStr, Vec<SurfaceExpr>)> {
let mut args = Vec::new();
let mut head = expr;
while let SurfaceExprKind::App { func, arg } = &head.kind {
args.push(arg.as_ref().clone());
head = func;
}
let SurfaceExprKind::FieldAccess { base, field } = &head.kind else {
return None;
};
let imported =
module_qualifier_of(base).is_some_and(|q| self.module_qualifiers.contains(&q));
if !imported || !field.starts_with(|c: char| c.is_ascii_uppercase()) {
return None;
}
args.reverse();
let nullary = self
.registry
.get_ctor(field.as_str())
.is_some_and(|info| info.fields.is_empty());
if nullary
&& matches!(
args[..],
[SurfaceExpr {
kind: SurfaceExprKind::Lit(Literal::Unit),
Comment on lines +631 to +635

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve empty calls separately from unit arguments

This heuristic cannot distinguish M.Dot() from M.Dot(()), because parse_postfix_expr represents both as App(M.Dot, Lit(Unit)). Consequently, for data T = Dot, the explicitly supplied argument in M.Dot(()) is silently discarded instead of producing an arity error; conversely, for data T = Wrap(Unit), M.Wrap() is accepted as though one argument were supplied, unlike the equivalent unqualified Wrap(). The parser or surface AST needs to preserve whether the call's argument list was actually empty before constructor arity is checked.

Useful? React with 👍 / 👎.

..
}]
)
{
args.clear();
}
Some((field, args))
}

/// Desugar qualified module-member access `M.member`.
///
/// `M` must be an imported module (matched by the last segment of its
Expand Down
27 changes: 25 additions & 2 deletions src/ephapax-driver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use std::path::{Path, PathBuf};
use ephapax_desugar::{DataRegistry, DesugarError, Desugarer};
use ephapax_parser::ParseError;
use ephapax_syntax::{Decl, ExternItem, Module};
use ephapax_typing::{module_expr_types, ModuleRegistry, SpannedTypeError};
use ephapax_typing::{module_expr_types, ModuleRegistry, SpannedTypeError, TypeError};
use ephapax_wasm::CodegenError;

/// One module of a loaded program, desugared to core.
Expand Down Expand Up @@ -265,6 +265,30 @@ pub fn check_program(program: &Program) -> Result<(), DriverError> {
}
})?);
}
// Only the `main` of the root module is the entry point of the
// program, and no call gives an entry point its type arguments. A
// generic `main` of an imported module is a function like any other.
let root = program.root();
let generic_main = root.core.decls.iter().find_map(|decl| match decl {
Decl::Fn {
name,
type_params,
body,
..
} if name == "main" && !type_params.is_empty() => Some(body.span),
_ => None,
});
if let Some(span) = generic_main {
return Err(DriverError::Type {
module: root.core.name.to_string(),
path: root.file_path.clone(),
source_text: root.source.clone(),
error: Box::new(SpannedTypeError {
error: TypeError::GenericMain,
span,
}),
});
}
// A program is one wasm module, and codegen finds a callee by its
// bare name. Two functions of the same name get module-qualified names
// in the merge (`qualify`). An extern function cannot: its name is the
Expand Down Expand Up @@ -326,7 +350,6 @@ pub fn check_program(program: &Program) -> Result<(), DriverError> {
.iter()
.any(|m| m.core.decls.iter().any(generic))
{
let root = program.root();
let merged = program.clone().into_merged()?;
let types = module_expr_types(&merged, &mut ModuleRegistry::new()).map_err(|error| {
DriverError::Type {
Expand Down
6 changes: 0 additions & 6 deletions src/ephapax-typing/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2758,12 +2758,6 @@ fn type_check_module_inner(tc: &mut TypeChecker, module: &Module) -> Result<(),
if type_params.is_empty() {
continue;
}
if name == "main" {
return Err(SpannedTypeError {
error: TypeError::GenericMain,
span: body.span,
});
}
// A type parameter can stand for a linear type. The check above
// took each type parameter as an opaque type, so the body uses
// such a value only in ways that every type allows. Check the
Expand Down
8 changes: 4 additions & 4 deletions tests/probes/expectations.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ gen_angle check ok ok E7 generics: id<a> instantiated at I32
gen_angle compile ok ok E7
gen_angle run ok:3 ok:3 E7
gen_angle wasm-run ok:3 ok:3 E7 (baseline recorded from `ephapax run`, same engine; no wasmtime CLI in that session)
qa/app2 check error:DesugarError ok E7: qualified M.Ctor(x) across modules
qa/app2 compile error:DesugarError ok E7
qa/app2 run error:DesugarError ok:9 E7 + E3 (run goes through ephapax-driver::load_program)
qa/app2 wasm-run no-module ok:9 E7
qa/app2 check ok ok E7: qualified M.Ctor(x) across modules
qa/app2 compile ok ok E7
qa/app2 run ok:9 ok:9 E7 + E3 (run goes through ephapax-driver::load_program)
qa/app2 wasm-run ok:9 ok:9 E7 (baseline recorded from `ephapax run`, same engine; no wasmtime CLI in that session)
p1 check ok ok already accepted
p1 compile ok ok already compiles (959 B in the survey)
p1 run ok:162 ok:162 E3/E4: ephapax run executes the wasm and prints 162
Expand Down
Loading