diff --git a/TEST-NEEDS.adoc b/TEST-NEEDS.adoc index f182856..20cfd5d 100644 --- a/TEST-NEEDS.adoc +++ b/TEST-NEEDS.adoc @@ -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 @@ -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 diff --git a/spec/ephapax-v2-grammar.ebnf b/spec/ephapax-v2-grammar.ebnf index f70cd12..c9ac4c3 100644 --- a/spec/ephapax-v2-grammar.ebnf +++ b/spec/ephapax-v2-grammar.ebnf @@ -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) *) @@ -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 *) @@ -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: *) diff --git a/src/ephapax-cli/src/main.rs b/src/ephapax-cli/src/main.rs index 2cb55fa..23c5ea5 100644 --- a/src/ephapax-cli/src/main.rs +++ b/src/ephapax-cli/src/main.rs @@ -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], diff --git a/src/ephapax-cli/src/runner.rs b/src/ephapax-cli/src/runner.rs index 407a7de..a088424 100644 --- a/src/ephapax-cli/src/runner.rs +++ b/src/ephapax-cli/src/runner.rs @@ -40,6 +40,7 @@ pub fn run_main(wasm: &[u8], argv: Argv) -> Result { Ok(results[0]) } +/// The linear memory that the module exports as `memory`. fn memory(caller: &mut Caller<'_, Argv>) -> wasmtime::Result { caller .get_export("memory") @@ -47,6 +48,8 @@ fn memory(caller: &mut Caller<'_, Argv>) -> wasmtime::Result { .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) -> wasmtime::Result<()> { linker.func_wrap("env", "print_i32", |v: i32| println!("{v}"))?; linker.func_wrap( diff --git a/src/ephapax-cli/tests/driver_agreement.rs b/src/ephapax-cli/tests/driver_agreement.rs index 40ce21f..8d33e54 100644 --- a/src/ephapax-cli/tests/driver_agreement.rs +++ b/src/ephapax-cli/tests/driver_agreement.rs @@ -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("../..") @@ -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], diff --git a/src/ephapax-cli/tests/generics.rs b/src/ephapax-cli/tests/generics.rs index 90b2f40..1e84aa2 100644 --- a/src/ephapax-cli/tests/generics.rs +++ b/src/ephapax-cli/tests/generics.rs @@ -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) @@ -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"); } diff --git a/src/ephapax-cli/tests/run_wasm.rs b/src/ephapax-cli/tests/run_wasm.rs index edfa665..346f631 100644 --- a/src/ephapax-cli/tests/run_wasm.rs +++ b/src/ephapax-cli/tests/run_wasm.rs @@ -13,6 +13,7 @@ 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("../..") @@ -20,6 +21,7 @@ fn repo_root() -> PathBuf { .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) @@ -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"), @@ -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( @@ -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`. diff --git a/src/ephapax-cli/tests/typed_codegen_matrix.rs b/src/ephapax-cli/tests/typed_codegen_matrix.rs index c82dfd4..8328792 100644 --- a/src/ephapax-cli/tests/typed_codegen_matrix.rs +++ b/src/ephapax-cli/tests/typed_codegen_matrix.rs @@ -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) -> Case { Case { ty, @@ -82,9 +83,13 @@ fn samples(ty: &str) -> Vec { .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 { let mut out = Vec::new(); @@ -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()) @@ -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(); diff --git a/src/ephapax-desugar/src/lib.rs b/src/ephapax-desugar/src/lib.rs index d1bc1a3..e386e6d 100644 --- a/src/ephapax-desugar/src/lib.rs +++ b/src/ephapax-desugar/src/lib.rs @@ -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 }, @@ -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> = (0..ctors.len()) .map(|i| { - if guarded { + if ordered { return indexed .iter() .filter(|(index, _)| index.is_none_or(|n| n == i)) @@ -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 @@ -1451,7 +1469,9 @@ impl Desugarer { span: Span, ) -> Result { 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. @@ -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(), }) } diff --git a/src/ephapax-driver/src/lib.rs b/src/ephapax-driver/src/lib.rs index 9ff6e90..07cb674 100644 --- a/src/ephapax-driver/src/lib.rs +++ b/src/ephapax-driver/src/lib.rs @@ -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 { let mut names = Vec::new(); for decl in &module.decls { diff --git a/src/ephapax-driver/src/qualify.rs b/src/ephapax-driver/src/qualify.rs index 50423e9..6758563 100644 --- a/src/ephapax-driver/src/qualify.rs +++ b/src/ephapax-driver/src/qualify.rs @@ -119,6 +119,8 @@ fn scopes(modules: &[LoadedModule], qualified: &[Renames]) -> Vec { .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 { @@ -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(); @@ -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 => {} diff --git a/src/ephapax-driver/src/resolver.rs b/src/ephapax-driver/src/resolver.rs index 16561a9..3fc52d3 100644 --- a/src/ephapax-driver/src/resolver.rs +++ b/src/ephapax-driver/src/resolver.rs @@ -35,6 +35,7 @@ pub(crate) enum Parsed { } impl Parsed { + /// The imports of the module, from the parser that read it. fn imports(&self) -> &[Import] { match self { Parsed::Surface(m) => &m.imports, @@ -43,6 +44,7 @@ impl Parsed { } } +/// One module of a program, after the resolver read and parsed its file. pub(crate) struct ResolvedModule { /// Module path as written in the source (e.g. `hypatia/ui/gui`). pub logical_path: String, @@ -101,6 +103,8 @@ pub(crate) fn resolve( .collect()) } +/// The state of one resolution: where to look for a module, and the +/// modules found so far. struct Resolver<'a> { base_dir: Option<&'a Path>, stdlib_dir: Option<&'a Path>, @@ -269,10 +273,14 @@ fn first_module_declaration(source: &str) -> Option { ephapax_parser::declared_module_name(source).map(|name| normalise_path(&name)) } +/// A module path with `/` between its parts: `Foo.Bar` and `Foo/Bar` name +/// one module. fn normalise_path(raw: &str) -> String { raw.replace('.', "/") } +/// The file of the module `logical` below `base_dir`: `a/b` is +/// `/a/b.eph`. fn logical_to_file_path(logical: &str, base_dir: &Path) -> PathBuf { let mut p = base_dir.to_path_buf(); for seg in logical.split('/') { diff --git a/src/ephapax-interp/src/lib.rs b/src/ephapax-interp/src/lib.rs index b0bf48d..b562a21 100644 --- a/src/ephapax-interp/src/lib.rs +++ b/src/ephapax-interp/src/lib.rs @@ -611,6 +611,8 @@ impl Interpreter { } } + /// Apply a numeric operator to 2 values of one numeric type, with the + /// function for that type. fn numeric_binop( &self, left: Value, @@ -714,6 +716,9 @@ impl Interpreter { /// 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()`. + /// The closure starts from an empty environment: the body of a + /// top-level function sees its parameters and the top-level + /// functions, and no local name of the caller. fn function_value(&self, params: Vec<(Var, Ty)>, body: Expr) -> Value { let mut params = params.into_iter(); let (param, param_ty) = params @@ -732,7 +737,7 @@ impl Interpreter { param, param_ty, body, - env: self.env.clone(), + env: Environment::new(), } } diff --git a/src/ephapax-lsp/src/main.rs b/src/ephapax-lsp/src/main.rs index cacbe01..6976161 100644 --- a/src/ephapax-lsp/src/main.rs +++ b/src/ephapax-lsp/src/main.rs @@ -960,6 +960,7 @@ mod tests { use super::*; use std::path::{Path, PathBuf}; + /// Each `.eph` file below `dir`. fn eph_files(dir: &Path, out: &mut Vec) { for entry in std::fs::read_dir(dir).into_iter().flatten().flatten() { let path = entry.path(); diff --git a/src/ephapax-parser/src/ephapax.pest b/src/ephapax-parser/src/ephapax.pest index b9f9a2f..9400e68 100644 --- a/src/ephapax-parser/src/ephapax.pest +++ b/src/ephapax-parser/src/ephapax.pest @@ -74,7 +74,7 @@ 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) @@ -156,10 +156,7 @@ declaration = { // 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 @@ -515,7 +512,7 @@ construct_expr = { constructor_name ~ (("(" ~ expr_list? ~ ")") | ("{" ~ record_ // FFI call: __ffi("symbol_name", arg1, arg2, ...) // Calls a C ABI function from a linked shared library. ffi_expr = { - "__ffi" ~ "(" ~ string ~ ("," ~ expression)* ~ ")" + "__ffi" ~ unimplemented_capability_annotation? ~ "(" ~ string ~ ("," ~ expression)* ~ ")" } record_literal = { "{" ~ record_field_assign ~ ("," ~ record_field_assign)* ~ ","? ~ "}" } diff --git a/src/ephapax-parser/src/grammar_conformance.rs b/src/ephapax-parser/src/grammar_conformance.rs index fd69754..3cc189c 100644 --- a/src/ephapax-parser/src/grammar_conformance.rs +++ b/src/ephapax-parser/src/grammar_conformance.rs @@ -32,8 +32,11 @@ use pest_meta::ast::{Expr, RuleType}; use crate::{parse_module, parse_surface_module, EphapaxParser, ParseError, Rule}; +/// The text of the parser grammar. const PEST: &str = include_str!("ephapax.pest"); +/// The line before part 1 in the specification file. const BEGIN: &str = "(* BEGIN GENERATED: the parser grammar, from ephapax.pest. Do not edit. *)"; +/// The line after part 1 in the specification file. const END: &str = "(* END GENERATED *)"; /// Rules that are not silent, and still never in a parse tree, with the @@ -46,14 +49,18 @@ const NO_PAIR: &[(&str, &str)] = &[ ("keyword_boundary", "only in the lookahead of `identifier`"), ]; +/// The root of the repository. fn repo() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("../..") } +/// The specification grammar file. fn ebnf_path() -> PathBuf { repo().join("spec/ephapax-v2-grammar.ebnf") } +/// Each rule of `ephapax.pest`: its type (normal, silent, atomic) and +/// its expression, by name. fn pest_rules() -> BTreeMap { let pairs = pest_meta::parser::parse(pest_meta::parser::Rule::grammar_rules, PEST) .expect("ephapax.pest parses"); @@ -392,6 +399,30 @@ fn valid_syntax_near_an_unimplemented_form_still_parses() { } } +/// The 2 entry points for one expression refuse unimplemented syntax by +/// name too. The core parser used to read `1 ++ 2` as `1`: the code that +/// builds the expression does not know the rule for `++`. +#[test] +fn an_expression_with_unimplemented_syntax_is_refused_by_name() { + for (source, production) in [ + ("1 ++ 2", "concat_expr"), + ("f(*x)", "deref_expr"), + ("@type_desc_of(I32)", "type_desc_expr"), + ] { + assert_eq!( + refused_as(crate::parse(source)), + Some(production), + "core: {source}" + ); + assert_eq!( + refused_as(crate::parse_surface_expr(source)), + Some(production), + "surface: {source}" + ); + } + assert!(crate::parse("1 + 2").is_ok() && crate::parse_surface_expr("1 + 2").is_ok()); +} + /// The production that a parser names when it refuses a program for /// syntax that is not implemented. fn refused_as(result: Result>) -> Option<&'static str> { diff --git a/src/ephapax-parser/src/lib.rs b/src/ephapax-parser/src/lib.rs index cf7aa12..5434f09 100644 --- a/src/ephapax-parser/src/lib.rs +++ b/src/ephapax-parser/src/lib.rs @@ -45,6 +45,7 @@ pub fn parse(source: &str) -> Result> { span: Span::dummy(), }] })?; + refuse_unimplemented(pairs.clone())?; let pair = pairs .into_iter() @@ -190,8 +191,10 @@ fn unimplemented_production(rule: Rule) -> Option<&'static str> { /// Refuse a program that has syntax of the v2 design that is not /// implemented: the first such construct in the source gives -/// [`ParseError::Unimplemented`] with the name of its production. The 2 -/// parsers call this on the parse tree before they build a module. +/// [`ParseError::Unimplemented`] with the name of its production. Each +/// entry point of the 2 parsers (a module, an expression) calls this on +/// the parse tree before it builds from it: the code that builds does +/// not know these rules. pub(crate) fn refuse_unimplemented( pairs: pest::iterators::Pairs, ) -> Result<(), Vec> { diff --git a/src/ephapax-parser/src/surface.rs b/src/ephapax-parser/src/surface.rs index 8d69540..ea4ae3b 100644 --- a/src/ephapax-parser/src/surface.rs +++ b/src/ephapax-parser/src/surface.rs @@ -80,6 +80,7 @@ pub fn parse_surface_expr(source: &str) -> Result> span: Span::dummy(), }] })?; + crate::refuse_unimplemented(pairs.clone())?; let pair = pairs .into_iter() diff --git a/src/ephapax-repl/src/lib.rs b/src/ephapax-repl/src/lib.rs index d1580f2..d1d679d 100644 --- a/src/ephapax-repl/src/lib.rs +++ b/src/ephapax-repl/src/lib.rs @@ -365,6 +365,8 @@ impl Repl { Ok(()) } + /// Load a program that the driver read and checked. Gives the number of + /// declarations that it loaded. fn load( &mut self, program: Result, diff --git a/src/ephapax-syntax/src/lib.rs b/src/ephapax-syntax/src/lib.rs index b9bdf80..2afd914 100644 --- a/src/ephapax-syntax/src/lib.rs +++ b/src/ephapax-syntax/src/lib.rs @@ -444,6 +444,7 @@ pub struct HandlerClause { pub struct ExprId(u64); impl ExprId { + /// An id that no other expression of this process has. fn fresh() -> Self { static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); ExprId(NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)) @@ -662,6 +663,8 @@ impl Expr { } } + /// An expression with no source position: for a test, and for code that + /// the compiler makes. pub fn dummy(kind: ExprKind) -> Self { Self::new(kind, Span::dummy()) } diff --git a/src/ephapax-typing/src/lib.rs b/src/ephapax-typing/src/lib.rs index bdd6233..f22a180 100644 --- a/src/ephapax-typing/src/lib.rs +++ b/src/ephapax-typing/src/lib.rs @@ -1173,6 +1173,7 @@ impl TypeChecker { .collect() } + /// The type of `expr`, by the form of the expression. fn check_kind(&mut self, expr: &Expr) -> Result { let s = expr.span; match &expr.kind { @@ -1252,6 +1253,8 @@ impl TypeChecker { }) } + /// The type of the variable `name`, which this marks as used. A generic + /// function gets fresh type arguments, recorded for the node `id`. fn check_var(&mut self, s: Span, id: ExprId, name: &Var) -> Result { let ty = self .ctx diff --git a/src/ephapax-typing/src/mono.rs b/src/ephapax-typing/src/mono.rs index 50722d3..2737133 100644 --- a/src/ephapax-typing/src/mono.rs +++ b/src/ephapax-typing/src/mono.rs @@ -39,6 +39,8 @@ pub(crate) fn substitute_fn(decl: &mut Decl, subst: &Subst) { substitute_expr(body, subst); } +/// Replace type variables in the type annotations of `expr` and of each +/// expression in it. fn substitute_expr(expr: &mut Expr, subst: &Subst) { substitute_node(expr, subst); for child in expr.children_mut() { @@ -113,6 +115,8 @@ pub fn monomorphise(module: &Module) -> Result { }) } +/// The state of one monomorphisation: the generic functions, the type +/// arguments of each use, and the copies made so far. struct Mono<'a> { generic: HashMap<&'a Var, &'a Decl>, type_args: TypeArgs, diff --git a/src/ephapax-wasm/src/lib.rs b/src/ephapax-wasm/src/lib.rs index ff394a4..ce68cc5 100644 --- a/src/ephapax-wasm/src/lib.rs +++ b/src/ephapax-wasm/src/lib.rs @@ -211,6 +211,8 @@ pub fn first_unsupported_typed(expr: &Expr, types: &ExprTypes) -> Option) -> Option { fn unsupported(what: &str, span: ephapax_syntax::Span) -> CodegenError { CodegenError(format!( @@ -339,6 +341,7 @@ pub fn validate_supported_typed(module: &AstModule, types: &ExprTypes) -> Result validate_supported_in(module, Some(types)) } +/// Check each function body of `module` with [`first_unsupported_in`]. fn validate_supported_in( module: &AstModule, types: Option<&ExprTypes>, @@ -761,9 +764,11 @@ impl Codegen { fn fn_list_get(&self) -> u32 { self.import_count() + OFFSET_LIST_GET } + /// Index of the runtime helper that compares 2 strings. fn fn_string_eq(&self) -> u32 { self.import_count() + OFFSET_STRING_EQ } + /// Index of the runtime helper for the float remainder. fn fn_fmod(&self) -> u32 { self.import_count() + OFFSET_FMOD } @@ -2916,10 +2921,12 @@ impl Codegen { func.instruction(&Instruction::LocalGet(sum_local)); } + /// `inl(value)`: the injection with tag 0. fn compile_inl(&mut self, func: &mut Function, value: &Expr) { self.compile_injection(func, 0, value); } + /// `inr(value)`: the injection with tag 1. fn compile_inr(&mut self, func: &mut Function, value: &Expr) { self.compile_injection(func, 1, value); } @@ -3990,6 +3997,7 @@ fn pattern_not_lowered(pattern: &Pattern, ty: &Ty) -> bool { } } +/// If `pattern` is a tuple pattern or has one in it. fn has_tuple_pattern(pattern: &Pattern) -> bool { match pattern { Pattern::Tuple(_) => true, diff --git a/tests/v2-grammar/unimplemented/capability_annotation.eph b/tests/v2-grammar/unimplemented/capability_annotation.eph index 5c039a9..f2abd60 100644 --- a/tests/v2-grammar/unimplemented/capability_annotation.eph +++ b/tests/v2-grammar/unimplemented/capability_annotation.eph @@ -3,5 +3,4 @@ // [UNIMPLEMENTED] in spec/ephapax-v2-grammar.ebnf, part 2. module m -@capabilities(io) -fn main(): I32 = 0 +fn main(): I32 = __ffi@capabilities(io)("host_add", 1)