diff --git a/EXPLAINME.adoc b/EXPLAINME.adoc index 8a5eaa5..f855e4e 100644 --- a/EXPLAINME.adoc +++ b/EXPLAINME.adoc @@ -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 + diff --git a/TEST-NEEDS.adoc b/TEST-NEEDS.adoc index 0a053e1..f182856 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: *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 @@ -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 diff --git a/spec/ephapax-v2-grammar.ebnf b/spec/ephapax-v2-grammar.ebnf index e66e79b..f70cd12 100644 --- a/spec/ephapax-v2-grammar.ebnf +++ b/spec/ephapax-v2-grammar.ebnf @@ -27,6 +27,11 @@ * Each rule is in the parse tree of a fixture in * tests/v2-grammar/productions. * + * A rule with the name `unimplemented_` is in part 1 for + * one purpose: the parser finds that syntax and refuses the program + * with an error that names ``. The parser accepts no + * program that has such a rule in its parse tree. + * * PART 2: THE V2 DESIGN, NOT IN THE PARSER BY THAT NAME. These are the * productions of the v2 design grammar that are not rules of the * parser: traits, effect and handler declarations, the comptime meta @@ -36,7 +41,11 @@ * [ALIAS rule] The parser has the construct as `rule` of part 1. * Part 1 is the current form. * [UNIMPLEMENTED] The parser does not have it. The parser refuses - * the fixture that the mark names. + * the fixture that the mark names, with an error + * that names the production after `as` + * (ParseError::Unimplemented). A production that is + * only inside another one has the name of the outer + * production there. * * A "DESIGN FORM" comment keeps the design form of a production that * part 1 has in a smaller form. @@ -83,12 +92,12 @@ (* Ephapax Grammar - PEG Parser *) (* Top-level rules *) -module = SOI , [ module_decl ] , { import_decl | declaration } , EOI ; +module = SOI , [ module_decl ] , { unimplemented_reexport | unimplemented_import | import_decl | declaration } , EOI ; expression_only = SOI , expression , EOI ; (* Module Declaration *) -module_decl = "module" , qualified_name ; +module_decl = "module" , qualified_name , [ unimplemented_capability_decl ] ; (* Only the header, for `declared_module_name`. *) module_header = SOI , module_decl ; @@ -104,9 +113,87 @@ qualified_name = ( ASCII_ALPHA | "_" ) , { ASCII_ALPHANUMERIC | "_" | "." | "/" import_decl = "import" , qualified_name , [ "(" , identifier , { "," , identifier } , ")" ] ; +(* Syntax of the v2 design that is not implemented *) +(* A rule with the name `unimplemented_` is in this grammar *) +(* for one purpose: the parser finds the syntax and refuses the program *) +(* with an error that names `` (`ParseError::Unimplemented`). *) +(* The production is in part 2 of spec/ephapax-v2-grammar.ebnf with the *) +(* mark [UNIMPLEMENTED]. Each rule stands only where no valid program can *) +(* have its first token, so it changes no program that parsed before. *) + +(* The body of `( ... )` when its contents have no grammar yet. *) +paren_body = { string | "(" , paren_body , ")" | !( "(" | ")" ) , ANY } ; (* silent: no node in the parse tree *) + +(* A word of the v2 design, as a full word (`where`, not the start of *) +(* `wherever`). Atomic: white space cannot stand before the check. *) +design_word = ( "where" | "requires" | "ensures" | "invariant" | "trait" | "impl" | "effect" | "handler" | "with" ) , !( ASCII_ALPHANUMERIC | "_" ) ; (* atomic: no white space between the parts *) + +(* import Foo from "foo" / import { a, b } from "foo" / import * as Foo from "foo" *) +unimplemented_import = "import" , ( identifier | "{" , identifier , { "," , identifier } , "}" | "*" , "as" , identifier ) , "from" , string ; + +(* pub import ... *) +unimplemented_reexport = visibility , ( unimplemented_import | import_decl ) ; + +(* module m capabilities: [io, network] *) +unimplemented_capability_decl = "capabilities" , ":" , "[" , identifier , { "," , identifier } , "]" ; + +(* @capabilities(io) *) +unimplemented_capability_annotation = "@capabilities" , "(" , paren_body , ")" ; + +(* @name(args) *) +unimplemented_annotation_args = "(" , paren_body , ")" ; + +(* @comptime { ... }, and the 4 declarations with an `end`: the rule takes *) +(* the rest of the file, because the bodies have no grammar yet. *) +unimplemented_comptime_decl = "@comptime" , "{" , { ANY } ; +unimplemented_trait_decl = [ visibility ] , &( "trait" ) , design_word , { ANY } ; +unimplemented_impl_decl = [ visibility ] , &( "impl" ) , design_word , { ANY } ; +unimplemented_effect_decl = [ visibility ] , &( "effect" ) , design_word , { ANY } ; +unimplemented_handler_decl = [ visibility ] , &( "handler" ) , design_word , { ANY } ; + +(* fn show(...) *) +unimplemented_type_param = ":" , constructor_name , { "+" , constructor_name } ; + +(* fn f(...): T where a: Show, b: Eq = ... *) +unimplemented_where_clause = &( "where" ) , design_word , identifier , unimplemented_type_param , { "," , identifier , unimplemented_type_param } ; + +(* fn f(...): T requires e ensures e invariant e = ... *) +unimplemented_contract_clause = &( "requires" | "ensures" | "invariant" ) , design_word , expression ; + +(* T with IO + State(I32) *) +unimplemented_effect_ty = &( "with" ) , design_word , ( constructor_name , [ "(" , ty_list , ")" ] | identifier ) , { "+" , ( constructor_name , [ "(" , ty_list , ")" ] | identifier ) } ; + +(* A * B *) +unimplemented_prod_ty = "*" ; + +(* (a, b, c) as a pattern, and &p *) +unimplemented_tuple_pattern = "(" , pattern , "," , pattern , { "," , pattern }- , ")" ; +unimplemented_borrow_pattern = "&" , pattern ; + +(* handle shallow e with ...: `shallow`, white space, then the start of a *) +(* name, a number or a string. `handle shallow(x) with` is a call. *) +unimplemented_handle_mode = "shallow" , { " " | ? TAB ? | ? LF ? | ? CR ? }- , &( ASCII_ALPHANUMERIC | "_" | '"' ) ; (* atomic: no white space between the parts *) + +(* handle e with h end *) +unimplemented_handler_ref = identifier , &( "end" ) ; + +(* | finally => e *) +unimplemented_finally_clause = "finally" , "=>" , expression ; + +(* | op(x), resume => e *) +unimplemented_inline_op_clause = "," , resume_param ; + +(* a ++ b, *e, @splice(...), @type_desc_of(T) *) +unimplemented_concat_expr = "++" ; +unimplemented_deref_expr = "*" , unary_expr ; +unimplemented_comptime_splice = "@splice" , "(" , paren_body , ")" ; +unimplemented_type_desc_expr = "@type_desc_of" , "(" , ty , ")" ; + (* Declarations *) -declaration = { annotation } , ( extern_block | data_decl | fn_decl | type_decl | const_decl ) ; +declaration = unimplemented_comptime_decl + | { annotation } , ( extern_block | data_decl | fn_decl | type_decl | const_decl | unimplemented_trait_decl | unimplemented_impl_decl | unimplemented_effect_decl | unimplemented_handler_decl ) + ; (* `@identifier` decorators attached to top-level declarations. Currently *) (* parser-only - annotations are parsed and stored on the surface AST so *) @@ -114,7 +201,9 @@ declaration = { annotation } , ( extern_block | data_decl | fn_decl | type_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 = "@" , identifier ; +annotation = unimplemented_capability_annotation + | "@" , identifier , [ unimplemented_annotation_args ] + ; (* Extern Blocks *) (* `extern "abi" { items... }` declares foreign types and function *) @@ -138,9 +227,9 @@ const_decl = "let" , identifier , [ ":" , ty ] , "=" , expression ; visibility = "pub" ; (* Optional generic type parameters: , *) -fn_type_params = "<" , identifier , { "," , identifier } , ">" ; +fn_type_params = "<" , identifier , [ unimplemented_type_param ] , { "," , identifier , [ unimplemented_type_param ] } , ">" ; -fn_decl = [ visibility ] , "fn" , identifier , [ fn_type_params ] , "(" , [ param_list ] , ")" , ( ":" | "->" ) , ty , "=" , expression ; +fn_decl = [ visibility ] , "fn" , identifier , [ fn_type_params ] , "(" , [ param_list ] , ")" , ( ":" | "->" ) , ty , { unimplemented_where_clause | unimplemented_contract_clause } , "=" , expression ; param_list = param , { "," , param } ; @@ -173,9 +262,9 @@ record_field = identifier , ":" , ty ; (* Types *) -ty = sum_ty , [ "->" , ty ] ; +ty = sum_ty , [ "->" , ty ] , [ unimplemented_effect_ty ] ; -sum_ty = type_atom , { "+" , type_atom } ; +sum_ty = type_atom , { ( "+" | unimplemented_prod_ty ) , type_atom } ; type_atom = base_ty | string_ty @@ -260,12 +349,17 @@ match_arm = pattern , [ "if" , expression ] , "=>" , expression ; pattern = constructor_pattern | pair_pattern + | unimplemented_tuple_pattern | wildcard_pattern | literal_pattern + | unimplemented_borrow_pattern | var_pattern ; -constructor_pattern = constructor_name , [ "(" , [ pattern_list ] , ")" ] ; +(* `Some(x)`, `None`, and with the module that declares the type: *) +(* `shapes.Rect(w, h)`. The module must be an import of this module. *) +constructor_pattern = [ pattern_qualifier , "." ] , constructor_name , [ "(" , [ pattern_list ] , ")" ] ; +pattern_qualifier = identifier ; pair_pattern = "(" , pattern , "," , pattern , ")" ; wildcard_pattern = "_" ; literal_pattern = literal ; @@ -295,16 +389,16 @@ region_expr = "region" , identifier , ( ":" | "{" ) , expression , [ "}" ] ; case_expr = "case" , expression , "of" , "inl" , "(" , identifier , ")" , "->" , expression , "inr" , "(" , identifier , ")" , "->" , expression , "end" ; (* Effect handling: handle expr with { return(x) => e | Op(args, resume) => e } end *) -handle_expr = "handle" , expression , "with" , [ "|" ] , handler_clause , { "|" , handler_clause } , "end" ; +handle_expr = "handle" , ( unimplemented_handle_mode , expression | expression ) , "with" , ( unimplemented_handler_ref | [ "|" ] , handler_clause , { "|" , handler_clause } ) , "end" ; -handler_clause = handler_return_clause | handler_op_clause ; +handler_clause = handler_return_clause | unimplemented_finally_clause | handler_op_clause ; (* return(x) => expr *) handler_return_clause = "return" , "(" , identifier , ")" , "=>" , expression ; (* Op(param1, param2, resume) => expr *) (* resume may optionally have a mode: resume(once) or resume(multi) *) -handler_op_clause = identifier , "(" , [ handler_param_list ] , ")" , "=>" , expression ; +handler_op_clause = identifier , "(" , [ handler_param_list ] , ")" , [ unimplemented_inline_op_clause ] , "=>" , expression ; handler_param_list = handler_param , { "," , handler_param } ; handler_param = resume_param | identifier ; @@ -324,7 +418,7 @@ eq_op = "==" | "!=" ; cmp_expr = add_expr , { cmp_op , add_expr } ; cmp_op = "<=" | ">=" | "<" | ">" ; -add_expr = mul_expr , { add_op , mul_expr } ; +add_expr = mul_expr , { ( unimplemented_concat_expr | add_op ) , mul_expr } ; add_op = "+" | "-" ; mul_expr = unary_expr , { mul_op , unary_expr } ; @@ -332,7 +426,7 @@ mul_op = "*" | "/" | "%" ; (* Unary and Postfix Expressions *) -unary_expr = "!" , unary_expr | "-" , unary_expr | postfix_expr ; +unary_expr = "!" , unary_expr | "-" , unary_expr | unimplemented_deref_expr | postfix_expr ; postfix_expr = atom_expr , { postfix_op } ; @@ -350,7 +444,9 @@ member_op = "." , ( integer | identifier | constructor_name ) ; (* Atomic Expressions *) -atom_expr = perform_expr +atom_expr = unimplemented_comptime_splice + | unimplemented_type_desc_expr + | perform_expr | ffi_expr | string_method | inl_expr @@ -513,17 +609,17 @@ module_path = IDENT , { "." , IDENT } ; (* 2. MODULES AND IMPORTS *) (* ================================================================ *) -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/import_from.eph (the parser has import_decl: `import a/b` and `import a/b(x, y)`) *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/import_from.eph as import (the parser has import_decl: `import a/b` and `import a/b(x, y)`) *) import = "import" , import_spec , "from" , STRING ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/import_from.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/import_from.eph as import *) import_spec = IDENT (* import Foo from "foo" *) | "{" , IDENT , { "," , IDENT } , "}" (* import { a, b } from "foo" *) | "*" , "as" , IDENT (* import * as Foo from "foo" *) ; (* Re-export: make imported names visible to importers of this module *) -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/reexport.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/reexport.eph as reexport *) reexport = "pub" , import ; @@ -537,10 +633,10 @@ reexport = "pub" , import ; * annotation = "@" , IDENT , [ "(" , annotation_args , ")" ] ; *) -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/annotation_args.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/annotation_args.eph as annotation_args *) annotation_args = annotation_arg , { "," , annotation_arg } ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/annotation_args.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/annotation_args.eph as annotation_args *) annotation_arg = IDENT | STRING | INTEGER | annotation ; @@ -579,49 +675,49 @@ decl_body = fn_decl * type_params = "[" , type_param , { "," , type_param } , "]" ; *) -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/type_param_bound.eph (the parser has type parameters with no bound: fn_type_params, type_params) *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/type_param_bound.eph as type_param (the parser has type parameters with no bound: fn_type_params, type_params) *) type_param = IDENT , [ ":" , trait_bound ] ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/type_param_bound.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/type_param_bound.eph as type_param *) trait_bound = CTOR_IDENT , { "+" , CTOR_IDENT } ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/where.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/where.eph as where_clause *) where_clause = "where" , constraint , { "," , constraint } ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/where.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/where.eph as where_clause *) constraint = IDENT , ":" , trait_bound ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/trait.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/trait.eph as trait_decl *) trait_decl = "trait" , CTOR_IDENT , "(" , type_param , ")" , [ ":" , trait_bound ] , (* supertrait bounds *) "=" , { trait_item } , "end" ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/trait.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/trait.eph as trait_decl *) trait_item = { annotation } , trait_fn_sig ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/trait.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/trait.eph as trait_decl *) trait_fn_sig = "fn" , IDENT , [ type_params ] , "(" , [ param_list ] , ")" , ":" , ty , [ "=" , expr ] (* default implementation *) ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/impl.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/impl.eph as impl_decl *) impl_decl = "impl" , CTOR_IDENT , "(" , ty , ")" , [ where_clause ] , "=" , { impl_item } , "end" ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/impl.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/impl.eph as impl_decl *) impl_item = { annotation } , fn_decl ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/effect.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/effect.eph as effect_decl *) effect_decl = "effect" , CTOR_IDENT , [ type_params ] , "=" , { effect_op } , "end" ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/effect.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/effect.eph as effect_decl *) effect_op = "fn" , IDENT , [ type_params ] , "(" , [ param_list ] , ")" , ":" , ty ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/handler_decl.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/handler_decl.eph as handler_decl *) handler_decl = "handler" , IDENT , [ type_params ] , "for" , CTOR_IDENT , [ type_params ] , [ "with" , "(" , param_list , ")" ] , (* handler-local state *) @@ -641,7 +737,7 @@ handler_decl = "handler" , IDENT , [ type_params ] , return_clause = "return" , "(" , IDENT , ")" , "=>" , expr ; (* Handler for an effect operation *) -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/handler_decl.eph (the parser has handler_op_clause, with `resume` in the parameter list) *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/handler_decl.eph as handler_decl (the parser has handler_op_clause, with `resume` in the parameter list) *) op_clause = "|" , IDENT , "(" , [ param_list ] , ")" , [ "," , resume_binding ] , "=>" , expr ; @@ -659,27 +755,27 @@ resume_binding = "resume" , [ "(" , resume_mode , ")" ] ; * CRITICAL for linear safety: if a handler doesn't call resume, * any linear values captured by the continuation must be consumed. * The finally clause handles this. *) -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/finally.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/finally.eph as finally_clause *) finally_clause = "finally" , "=>" , expr ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph as comptime_decl *) comptime_decl = "@comptime" , "{" , { comptime_item } , "}" ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph as comptime_decl *) comptime_item = comptime_fn | comptime_let ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph as comptime_decl *) comptime_fn = "fn" , IDENT , "(" , [ comptime_param_list ] , ")" , ":" , comptime_ty , "=" , comptime_expr ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph as comptime_decl *) comptime_param_list = comptime_param , { "," , comptime_param } ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph as comptime_decl *) comptime_param = IDENT , ":" , comptime_ty ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph as comptime_decl *) comptime_let = "let" , IDENT , "=" , comptime_expr ; @@ -687,10 +783,10 @@ comptime_let = "let" , IDENT , "=" , comptime_expr ; (* 6. CONTRACTS *) (* ================================================================ *) -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/contract.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/contract.eph as contract_clause *) contract_block = { contract_clause } ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/contract.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/contract.eph as contract_clause *) contract_clause = "requires" , expr (* precondition *) | "ensures" , result_expr (* postcondition *) | "invariant" , expr (* maintained through body *) @@ -701,10 +797,10 @@ contract_clause = "requires" , expr (* precondition *) * the result as an unrestricted snapshot. This is needed because * linear values are consumed by the body and cannot be referenced * in ensures directly. old() implicitly borrows. *) -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/contract.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/contract.eph as contract_clause *) result_expr = expr ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/contract.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/contract.eph as contract_clause *) old_expr = "old" , "(" , expr , ")" ; @@ -712,11 +808,11 @@ old_expr = "old" , "(" , expr , ")" ; (* 7. FFI CAPABILITIES *) (* ================================================================ *) -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/capability.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/capability.eph as capability_decl *) capability_decl = "capabilities" , ":" , "[" , capability , { "," , capability } , "]" ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/capability.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/capability.eph as capability_decl *) capability = "memory_read" (* can read from raw memory *) | "memory_write" (* can write to raw memory *) | "io" (* can perform I/O *) @@ -734,7 +830,7 @@ capability = "memory_read" (* can read from raw memory *) * "(" , STRING , { "," , expr } , ")" ; *) -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/capability_annotation.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/capability_annotation.eph as capability_annotation *) capability_annotation = "@capabilities" , "(" , capability , { "," , capability } , ")" ; @@ -750,7 +846,7 @@ capability_annotation = "@capabilities" , *) (* Effect annotation on function types: a -> b with IO + State(s) *) -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/effect_ty.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/effect_ty.eph as effect_ty *) effect_ty = fun_ty , [ "with" , effect_row ] ; (* Effect rows support both concrete effects and effect variables. @@ -758,13 +854,13 @@ effect_ty = fun_ty , [ "with" , effect_row ] ; * polymorphism over effects: * fn map[a, b, E: Effect](f: a -> b with E, xs: List(a)): List(b) with E * No `with` clause = pure (no effects). *) -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/effect_row.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/effect_row.eph as effect_ty *) effect_row = effect_ref , { "+" , effect_ref } | IDENT (* effect variable *) | effect_ref , { "+" , effect_ref } , "+" , IDENT (* concrete + variable *) ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/effect_row.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/effect_row.eph as effect_ty *) effect_ref = CTOR_IDENT , [ "(" , ty_list , ")" ] ; (* [ALIAS ty] the parser rule is the current form *) @@ -776,7 +872,7 @@ fun_ty = sum_ty , [ "->" , ty ] ; * sum_ty = prod_ty , { "+" , prod_ty } ; *) -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/prod_ty.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/prod_ty.eph as prod_ty *) prod_ty = type_atom , { "*" , type_atom } ; (* [ALIAS product_ty] the parser rule is the current form *) @@ -810,7 +906,7 @@ expr = let_expr * "->" , expr ; *) -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/effect_row.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/effect_row.eph as effect_ty *) effect_annotation = "with" , effect_row ; (* DESIGN FORM of handle_expr. The parser rule handle_expr in part 1 is the current form. @@ -820,10 +916,10 @@ effect_annotation = "with" , effect_row ; * handler_ref , "end" ; *) -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/handle_mode.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/handle_mode.eph as handle_mode *) handle_mode = "shallow" ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/handler_ref.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/handler_ref.eph as handler_ref *) handler_ref = IDENT (* named handler *) | [ "(" , param_list , ")" ] , (* handler-local state *) { inline_handler_clause } (* inline handler *) @@ -832,7 +928,7 @@ handler_ref = IDENT (* named handler *) (* [ALIAS handler_clause] the parser has no finally clause *) inline_handler_clause = return_clause | inline_op_clause | finally_clause ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/resume_after_params.eph (the parser has handler_op_clause, with `resume` in the parameter list) *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/resume_after_params.eph as inline_op_clause (the parser has handler_op_clause, with `resume` in the parameter list) *) inline_op_clause = "|" , IDENT , "(" , [ param_list ] , ")" , [ "," , resume_binding ] , "=>" , expr ; @@ -856,10 +952,10 @@ inline_op_clause = "|" , IDENT , "(" , [ param_list ] , ")" , * ; *) -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/tuple_pattern.eph (the parser has the pattern of 2 parts as pair_pattern) *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/tuple_pattern.eph as tuple_pattern (the parser has the pattern of 2 parts as pair_pattern) *) tuple_pattern = "(" , pattern , "," , pattern , { "," , pattern } , ")" ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/borrow_pattern.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/borrow_pattern.eph as borrow_pattern *) borrow_pattern = "&" , pattern ; @@ -873,7 +969,7 @@ borrow_pattern = "&" , pattern ; * mul_expr = concat_expr , { ( "*" | "/" | "%" ) , concat_expr } ; *) -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/concat.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/concat.eph as concat_expr *) concat_expr = unary_expr , { "++" , unary_expr } ; @@ -903,15 +999,15 @@ concat_expr = unary_expr , { "++" , unary_expr } ; * ; *) -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/deref.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/deref.eph as deref_expr *) deref_expr = "*" , unary_expr ; (* Splice comptime result into runtime code *) -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/splice.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/splice.eph as comptime_splice *) comptime_splice = "@splice" , "(" , comptime_expr , ")" ; (* Runtime type descriptor (unrestricted value, Harvard-safe) *) -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/type_desc.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/type_desc.eph as type_desc_expr *) type_desc_expr = "@type_desc_of" , "(" , ty , ")" ; @@ -919,7 +1015,7 @@ type_desc_expr = "@type_desc_of" , "(" , ty , ")" ; (* 13. COMPTIME EXPRESSIONS (META PLANE) *) (* ================================================================ *) -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph as comptime_decl *) comptime_ty = "Type" (* reified type *) | "TypeDesc" (* runtime type descriptor *) | "SurfaceDecl" (* surface AST declaration *) @@ -931,7 +1027,7 @@ comptime_ty = "Type" (* reified type *) | IDENT ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph as comptime_decl *) comptime_expr = comptime_intrinsic | comptime_match | comptime_if @@ -942,7 +1038,7 @@ comptime_expr = comptime_intrinsic | IDENT ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph as comptime_decl *) comptime_intrinsic = "@type_desc_of" , "(" , ty , ")" | "@fields_of" , "(" , ty , ")" | "@constructors_of" , "(" , ty , ")" @@ -958,35 +1054,35 @@ comptime_intrinsic = "@type_desc_of" , "(" , ty , ")" | "@warning" , "(" , STRING , ")" (* compile warning *) ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph as comptime_decl *) comptime_match = "match" , comptime_expr , "of" , { "|" , comptime_pattern , "=>" , comptime_expr } , "end" ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph as comptime_decl *) comptime_pattern = CTOR_IDENT , [ "(" , comptime_pattern_list , ")" ] | "_" | IDENT | comptime_literal ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph as comptime_decl *) comptime_pattern_list = comptime_pattern , { "," , comptime_pattern } ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph as comptime_decl *) comptime_if = "if" , comptime_expr , "then" , comptime_expr , "else" , comptime_expr ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph as comptime_decl *) comptime_let_in = "let" , IDENT , "=" , comptime_expr , "in" , comptime_expr ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph as comptime_decl *) comptime_call = IDENT , "(" , [ comptime_expr , { "," , comptime_expr } ] , ")" ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph as comptime_decl *) comptime_list = "[" , [ comptime_expr , { "," , comptime_expr } ] , "]" ; -(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph *) +(* [UNIMPLEMENTED] refused: tests/v2-grammar/unimplemented/comptime.eph as comptime_decl *) comptime_literal = INTEGER | STRING | BOOLEAN | "()" ; diff --git a/src/ephapax-cli/tests/generics.rs b/src/ephapax-cli/tests/generics.rs index 6f358f7..90b2f40 100644 --- a/src/ephapax-cli/tests/generics.rs +++ b/src/ephapax-cli/tests/generics.rs @@ -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(): 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. diff --git a/src/ephapax-cli/tests/run_wasm.rs b/src/ephapax-cli/tests/run_wasm.rs index cb44e3d..edfa665 100644 --- a/src/ephapax-cli/tests/run_wasm.rs +++ b/src/ephapax-cli/tests/run_wasm.rs @@ -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 @@ -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. diff --git a/src/ephapax-desugar/src/lib.rs b/src/ephapax-desugar/src/lib.rs index 854f26d..d1bc1a3 100644 --- a/src/ephapax-desugar/src/lib.rs +++ b/src/ephapax-desugar/src/lib.rs @@ -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 { @@ -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 = vec![]; // placeholder self.bind_pattern_vars(value, elements, &as_fields, body, span) diff --git a/src/ephapax-interp/src/lib.rs b/src/ephapax-interp/src/lib.rs index 4af42de..b0bf48d 100644 --- a/src/ephapax-interp/src/lib.rs +++ b/src/ephapax-interp/src/lib.rs @@ -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 { - // 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)); } } @@ -1159,6 +1177,16 @@ impl Interpreter { } fn eval_borrow(&mut self, inner: &Expr) -> Result { + // 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))) } diff --git a/src/ephapax-parser/src/ephapax.pest b/src/ephapax-parser/src/ephapax.pest index 171f5d8..b9f9a2f 100644 --- a/src/ephapax-parser/src/ephapax.pest +++ b/src/ephapax-parser/src/ephapax.pest @@ -7,14 +7,18 @@ // ============================================================================ // Top-level rules -module = { SOI ~ module_decl? ~ (import_decl | declaration)* ~ EOI } +module = { + SOI ~ module_decl? + ~ (unimplemented_reexport | unimplemented_import | import_decl | declaration)* + ~ EOI +} expression_only = { SOI ~ expression ~ EOI } // ============================================================================ // Module Declaration // ============================================================================ -module_decl = { "module" ~ qualified_name } +module_decl = { "module" ~ qualified_name ~ unimplemented_capability_decl? } // Only the header, for `declared_module_name`. module_header = { SOI ~ module_decl } @@ -34,11 +38,117 @@ import_decl = { "import" ~ qualified_name ~ ("(" ~ identifier ~ ("," ~ identifier)* ~ ")")? } +// ============================================================================ +// Syntax of the v2 design that is not implemented +// ============================================================================ +// +// A rule with the name `unimplemented_` is in this grammar +// for one purpose: the parser finds the syntax and refuses the program +// with an error that names `` (`ParseError::Unimplemented`). +// The production is in part 2 of spec/ephapax-v2-grammar.ebnf with the +// mark [UNIMPLEMENTED]. Each rule stands only where no valid program can +// have its first token, so it changes no program that parsed before. + +// The body of `( ... )` when its contents have no grammar yet. +paren_body = _{ (string | "(" ~ paren_body ~ ")" | !("(" | ")") ~ ANY)* } + +// A word of the v2 design, as a full word (`where`, not the start of +// `wherever`). Atomic: white space cannot stand before the check. +design_word = @{ + ("where" | "requires" | "ensures" | "invariant" | "trait" | "impl" | "effect" | "handler" | "with") + ~ !(ASCII_ALPHANUMERIC | "_") +} + +// import Foo from "foo" / import { a, b } from "foo" / import * as Foo from "foo" +unimplemented_import = { + "import" + ~ (identifier | "{" ~ identifier ~ ("," ~ identifier)* ~ "}" | "*" ~ "as" ~ identifier) + ~ "from" ~ string +} + +// pub import ... +unimplemented_reexport = { visibility ~ (unimplemented_import | import_decl) } + +// module m capabilities: [io, network] +unimplemented_capability_decl = { + "capabilities" ~ ":" ~ "[" ~ identifier ~ ("," ~ identifier)* ~ "]" +} + +// @capabilities(io) +unimplemented_capability_annotation = { "@capabilities" ~ "(" ~ paren_body ~ ")" } + +// @name(args) +unimplemented_annotation_args = { "(" ~ paren_body ~ ")" } + +// @comptime { ... }, and the 4 declarations with an `end`: the rule takes +// the rest of the file, because the bodies have no grammar yet. +unimplemented_comptime_decl = { "@comptime" ~ "{" ~ ANY* } +unimplemented_trait_decl = { visibility? ~ &"trait" ~ design_word ~ ANY* } +unimplemented_impl_decl = { visibility? ~ &"impl" ~ design_word ~ ANY* } +unimplemented_effect_decl = { visibility? ~ &"effect" ~ design_word ~ ANY* } +unimplemented_handler_decl = { visibility? ~ &"handler" ~ design_word ~ ANY* } + +// fn show(...) +unimplemented_type_param = { ":" ~ constructor_name ~ ("+" ~ constructor_name)* } + +// fn f(...): T where a: Show, b: Eq = ... +unimplemented_where_clause = { + &"where" ~ design_word + ~ identifier ~ unimplemented_type_param ~ ("," ~ identifier ~ unimplemented_type_param)* +} + +// fn f(...): T requires e ensures e invariant e = ... +unimplemented_contract_clause = { + &("requires" | "ensures" | "invariant") ~ design_word ~ expression +} + +// T with IO + State(I32) +unimplemented_effect_ty = { + &"with" ~ design_word + ~ (constructor_name ~ ("(" ~ ty_list ~ ")")? | identifier) + ~ ("+" ~ (constructor_name ~ ("(" ~ ty_list ~ ")")? | identifier))* +} + +// A * B +unimplemented_prod_ty = { "*" } + +// (a, b, c) as a pattern, and &p +unimplemented_tuple_pattern = { "(" ~ pattern ~ "," ~ pattern ~ ("," ~ pattern)+ ~ ")" } +unimplemented_borrow_pattern = { "&" ~ pattern } + +// handle shallow e with ...: `shallow`, white space, then the start of a +// name, a number or a string. `handle shallow(x) with` is a call. +unimplemented_handle_mode = @{ + "shallow" ~ (" " | "\t" | "\n" | "\r")+ ~ &(ASCII_ALPHANUMERIC | "_" | "\"") +} + +// handle e with h end +unimplemented_handler_ref = { identifier ~ &"end" } + +// | finally => e +unimplemented_finally_clause = { "finally" ~ "=>" ~ expression } + +// | op(x), resume => e +unimplemented_inline_op_clause = { "," ~ resume_param } + +// a ++ b, *e, @splice(...), @type_desc_of(T) +unimplemented_concat_expr = { "++" } +unimplemented_deref_expr = { "*" ~ unary_expr } +unimplemented_comptime_splice = { "@splice" ~ "(" ~ paren_body ~ ")" } +unimplemented_type_desc_expr = { "@type_desc_of" ~ "(" ~ ty ~ ")" } + // ============================================================================ // Declarations // ============================================================================ -declaration = { annotation* ~ (extern_block | data_decl | fn_decl | type_decl | const_decl) } +declaration = { + unimplemented_comptime_decl + | annotation* ~ ( + extern_block | data_decl | fn_decl | type_decl | const_decl + | unimplemented_trait_decl | unimplemented_impl_decl + | unimplemented_effect_decl | unimplemented_handler_decl + ) +} // `@identifier` decorators attached to top-level declarations. Currently // parser-only — annotations are parsed and stored on the surface AST so @@ -46,7 +156,10 @@ declaration = { annotation* ~ (extern_block | data_decl | fn_decl | type_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 = { "@" ~ identifier } +annotation = { + unimplemented_capability_annotation + | "@" ~ identifier ~ unimplemented_annotation_args? +} // ============================================================================ // Extern Blocks @@ -74,10 +187,15 @@ const_decl = { "let" ~ identifier ~ (":" ~ ty)? ~ "=" ~ expression } visibility = { "pub" } // Optional generic type parameters: , -fn_type_params = { "<" ~ identifier ~ ("," ~ identifier)* ~ ">" } +fn_type_params = { + "<" ~ identifier ~ unimplemented_type_param? + ~ ("," ~ identifier ~ unimplemented_type_param?)* ~ ">" +} fn_decl = { - visibility? ~ "fn" ~ identifier ~ fn_type_params? ~ "(" ~ param_list? ~ ")" ~ (":" | "->") ~ ty ~ "=" ~ expression + visibility? ~ "fn" ~ identifier ~ fn_type_params? ~ "(" ~ param_list? ~ ")" ~ (":" | "->") ~ ty + ~ (unimplemented_where_clause | unimplemented_contract_clause)* + ~ "=" ~ expression } param_list = { param ~ ("," ~ param)* } @@ -117,9 +235,9 @@ record_field = { identifier ~ ":" ~ ty } // Types // ============================================================================ -ty = { sum_ty ~ ("->" ~ ty)? } +ty = { sum_ty ~ ("->" ~ ty)? ~ unimplemented_effect_ty? } -sum_ty = { type_atom ~ ("+" ~ type_atom)* } +sum_ty = { type_atom ~ (("+" | unimplemented_prod_ty) ~ type_atom)* } type_atom = { base_ty @@ -224,12 +342,17 @@ match_arm = { pattern ~ ("if" ~ expression)? ~ "=>" ~ expression } pattern = { constructor_pattern | pair_pattern + | unimplemented_tuple_pattern | wildcard_pattern | literal_pattern + | unimplemented_borrow_pattern | var_pattern } -constructor_pattern = { constructor_name ~ ("(" ~ pattern_list? ~ ")")? } +// `Some(x)`, `None`, and with the module that declares the type: +// `shapes.Rect(w, h)`. The module must be an import of this module. +constructor_pattern = { (pattern_qualifier ~ ".")? ~ constructor_name ~ ("(" ~ pattern_list? ~ ")")? } +pattern_qualifier = { identifier } pair_pattern = { "(" ~ pattern ~ "," ~ pattern ~ ")" } wildcard_pattern = { "_" } literal_pattern = { literal } @@ -277,13 +400,14 @@ case_expr = { // Effect handling: handle expr with { return(x) => e | Op(args, resume) => e } end handle_expr = { - "handle" ~ expression ~ "with" - ~ "|"? ~ handler_clause ~ ("|" ~ handler_clause)* + "handle" ~ (unimplemented_handle_mode ~ expression | expression) ~ "with" + ~ (unimplemented_handler_ref | "|"? ~ handler_clause ~ ("|" ~ handler_clause)*) ~ "end" } handler_clause = { handler_return_clause + | unimplemented_finally_clause | handler_op_clause } @@ -295,7 +419,7 @@ handler_return_clause = { // Op(param1, param2, resume) => expr // resume may optionally have a mode: resume(once) or resume(multi) handler_op_clause = { - identifier ~ "(" ~ handler_param_list? ~ ")" ~ "=>" ~ expression + identifier ~ "(" ~ handler_param_list? ~ ")" ~ unimplemented_inline_op_clause? ~ "=>" ~ expression } handler_param_list = { handler_param ~ ("," ~ handler_param)* } @@ -321,7 +445,7 @@ eq_op = { "==" | "!=" } cmp_expr = { add_expr ~ (cmp_op ~ add_expr)* } cmp_op = { "<=" | ">=" | "<" | ">" } -add_expr = { mul_expr ~ (add_op ~ mul_expr)* } +add_expr = { mul_expr ~ ((unimplemented_concat_expr | add_op) ~ mul_expr)* } add_op = { "+" | "-" } mul_expr = { unary_expr ~ (mul_op ~ unary_expr)* } @@ -334,6 +458,7 @@ mul_op = { "*" | "/" | "%" } unary_expr = { "!" ~ unary_expr | "-" ~ unary_expr + | unimplemented_deref_expr | postfix_expr } @@ -361,7 +486,9 @@ member_op = { "." ~ (integer | identifier | constructor_name) } // ============================================================================ atom_expr = { - perform_expr + unimplemented_comptime_splice + | unimplemented_type_desc_expr + | perform_expr | ffi_expr | string_method | inl_expr diff --git a/src/ephapax-parser/src/error.rs b/src/ephapax-parser/src/error.rs index 9a0aaab..8154053 100644 --- a/src/ephapax-parser/src/error.rs +++ b/src/ephapax-parser/src/error.rs @@ -36,6 +36,15 @@ pub enum ParseError { #[error("Annotation `@{name}` is not enforced: the compiler does not check it, so it is rejected rather than silently ignored")] AnnotationNotEnforced { name: String, span: Span }, + /// Syntax of the v2 design that the parser does not have: `production` + /// is in part 2 of spec/ephapax-v2-grammar.ebnf with the mark + /// [UNIMPLEMENTED] (v2 plan rung E7). + #[error("`{production}` is syntax of the v2 design that is not implemented (see the mark [UNIMPLEMENTED] in spec/ephapax-v2-grammar.ebnf, part 2)")] + Unimplemented { + production: &'static str, + span: Span, + }, + /// An integer literal outside the I64 range. It used to become 0 /// without a diagnostic (v2 plan rung E3). #[error("Integer literal `{literal}` is out of range for I64 (-9223372036854775808 to 9223372036854775807)")] @@ -51,6 +60,7 @@ impl ParseError { ParseError::Expected { span, .. } => *span, ParseError::AnnotationNotEnforced { span, .. } => *span, ParseError::IntegerOutOfRange { span, .. } => *span, + ParseError::Unimplemented { span, .. } => *span, } } @@ -197,6 +207,19 @@ impl Report { .finish() } + ParseError::Unimplemented { production, .. } => { + AriadneReport::build(ReportKind::Error, self.source_name.clone(), range.start) + .with_message(format!("`{}` is not implemented", production)) + .with_label( + Label::new(location) + .with_message( + "syntax of the v2 design: spec/ephapax-v2-grammar.ebnf, part 2", + ) + .with_color(Color::Red), + ) + .finish() + } + ParseError::IntegerOutOfRange { literal, .. } => { AriadneReport::build(ReportKind::Error, self.source_name.clone(), range.start) .with_message(format!("Integer literal `{}` is out of range", literal)) diff --git a/src/ephapax-parser/src/grammar_conformance.rs b/src/ephapax-parser/src/grammar_conformance.rs index b2cf25f..fd69754 100644 --- a/src/ephapax-parser/src/grammar_conformance.rs +++ b/src/ephapax-parser/src/grammar_conformance.rs @@ -13,8 +13,9 @@ //! is in the parse tree of a fixture. //! 2. The productions of the v2 design that the parser does not have by //! that name. Each one has a mark: `[ALIAS rule]` (the parser has the -//! construct as `rule`) or `[UNIMPLEMENTED]` with a fixture that the -//! parser refuses. +//! construct as `rule`) or `[UNIMPLEMENTED]` with a fixture and the +//! production that the parser names when it refuses the fixture +//! (`ParseError::Unimplemented`). //! //! So no production is unaccounted for. To write part 1 again after a //! change of `ephapax.pest`: @@ -29,17 +30,16 @@ use std::path::{Path, PathBuf}; use pest::Parser as _; use pest_meta::ast::{Expr, RuleType}; -use crate::{parse_module, parse_surface_module, EphapaxParser, Rule}; +use crate::{parse_module, parse_surface_module, EphapaxParser, ParseError, Rule}; const PEST: &str = include_str!("ephapax.pest"); const BEGIN: &str = "(* BEGIN GENERATED: the parser grammar, from ephapax.pest. Do not edit. *)"; const END: &str = "(* END GENERATED *)"; -/// Rules that are in the grammar and never in a parse tree, with the -/// reason. Each other rule must be in the parse tree of a fixture. +/// Rules that are not silent, and still never in a parse tree, with the +/// reason. Each other rule that is not silent must be in the parse tree +/// of a fixture. const NO_PAIR: &[(&str, &str)] = &[ - ("WHITESPACE", "silent"), - ("COMMENT", "silent"), ("string_inner", "inside the atomic rule `string`"), ("escape", "inside the atomic rule `string`"), ("keyword", "only in the lookahead of `identifier`"), @@ -304,6 +304,10 @@ fn each_parser_rule_is_in_a_fixture() { !files.is_empty(), "no fixture in tests/v2-grammar/productions" ); + // The grammar has the rules `unimplemented_` to find + // syntax that the parser then refuses by name: the fixtures of that + // syntax have these rules in their parse trees. + eph_files(&fixtures.join("unimplemented"), &mut files); for file in &files { let source = std::fs::read_to_string(file).expect("read fixture"); let rules = rules_in(Rule::module, &source) @@ -316,7 +320,9 @@ fn each_parser_rule_is_in_a_fixture() { let no_pair: BTreeSet<&str> = NO_PAIR.iter().map(|(name, _)| *name).collect(); let wrong: Vec = pest_rules() - .into_keys() + .into_iter() + .filter(|(_, (ty, _))| *ty != RuleType::Silent) + .map(|(name, _)| name) .filter(|name| seen.contains(name) == no_pair.contains(name.as_str())) .collect(); assert!( @@ -326,9 +332,79 @@ fn each_parser_rule_is_in_a_fixture() { ); } +/// A rule `unimplemented_` stands only where no valid +/// program has its first token. These programs are valid, and each one +/// is near such a rule: the parsers must take them as before. The +/// surface parser has no `handle` expression; the core parser takes +/// those programs. +#[test] +fn valid_syntax_near_an_unimplemented_form_still_parses() { + // (program, the surface parser accepts it) + for (body, surface_accepts) in [ + // `shallow` as a function and as a variable, not as a handle mode. + ( + "fn f(): I32 = handle shallow(1) with | return(x) => x end", + false, + ), + ( + "fn f(shallow: I32): I32 = handle shallow with | return(x) => x end", + false, + ), + // An operation with the name `finally`, and `resume` as a parameter. + ( + "fn f(): I32 = handle g() with | finally(x, resume) => x | return(x) => x end", + false, + ), + // `*`, `+` and `&` as operators; a pair pattern; a sum type. + ("fn f(a: I32, b: I32): I32 = a * b + -a", true), + ( + "fn f(p: (I32, I32)): I32 = match p of | (a, b) => a end", + true, + ), + ( + "fn f(s: I32 + Bool, r: &I32): I32 -> I32 = fn(x: I32) -> x", + true, + ), + // Annotations with no argument, type parameters with no bound, + // and the import forms of the parser. + ( + "@inline\n@tail_recursive\nfn f(x: a, y: b): a = x", + true, + ), + ( + "import lib/math\nimport lib.shapes(area, Shape)\nfn f(): I32 = 0", + true, + ), + // Words of the design as names where a name can stand. + ( + "fn f(effect: I32, handler: I32, trait_id: I32): I32 = effect + handler + trait_id", + true, + ), + ] { + let source = format!("module m\n{body}\n"); + let surface = parse_surface_module(&source, "m").err(); + let core = parse_module(&source, "m").err(); + assert!( + surface.is_none() == surface_accepts && core.is_none(), + "this valid program does not parse as before:\n{source}\n\ + surface: {surface:?}\ncore: {core:?}" + ); + } +} + +/// 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> { + match result.err()?.first()? { + ParseError::Unimplemented { production, .. } => Some(production), + _ => None, + } +} + /// Part 2: each production that is not a rule of the parser has a mark. -/// An `[ALIAS rule]` names a rule of the parser. The parser refuses the -/// fixture of an `[UNIMPLEMENTED]` production. +/// An `[ALIAS rule]` names a rule of the parser. For an `[UNIMPLEMENTED]` +/// production, the 2 parsers refuse the fixture with an error that names +/// the production of the mark (`as `). #[test] fn each_design_production_is_an_alias_or_is_refused() { let (head, part, tail) = split(specification()); @@ -343,6 +419,8 @@ fn each_design_production_is_an_alias_or_is_refused() { let mut names: BTreeSet<&str> = defined(part).into_iter().collect(); let mut mark: Option<&str> = None; let (mut aliases, mut refused) = (0, 0); + // The productions that the parser refuses under their own name. + let mut own_name = BTreeSet::new(); for line in tail.lines() { if let Some(rest) = line.strip_prefix("(* [") { mark = Some(rest); @@ -363,21 +441,43 @@ fn each_design_production_is_an_alias_or_is_refused() { ); aliases += 1; } else { - let fixture = mark + // `UNIMPLEMENTED] refused: as ...` + let mut words = mark .strip_prefix("UNIMPLEMENTED] refused: ") - .and_then(|rest| rest.split(' ').next()) - .unwrap_or_else(|| panic!("`{name}`: the mark `{mark}` is not a known mark")); + .unwrap_or_else(|| panic!("`{name}`: the mark `{mark}` is not a known mark")) + .split(' '); + let (Some(fixture), Some("as"), Some(production)) = + (words.next(), words.next(), words.next()) + else { + panic!("`{name}`: the mark must say `refused: as `"); + }; let source = std::fs::read_to_string(repo().join(fixture)) .unwrap_or_else(|_| panic!("`{name}`: no fixture {fixture}")); + let surface = refused_as(parse_surface_module(&source, "fixture")); + let core = refused_as(parse_module(&source, "fixture")); assert!( - parse_surface_module(&source, "fixture").is_err() - && parse_module(&source, "fixture").is_err(), - "`{name}` has the mark UNIMPLEMENTED, but the parser accepts {fixture}" + surface == Some(production) && core == Some(production), + "`{name}`: the mark says that the parser refuses {fixture} as `{production}`; \ + the surface parser says {surface:?}, the core parser says {core:?}" ); + if production == name { + own_name.insert(name); + } refused += 1; } } assert!(mark.is_none(), "a mark with no production after it"); + // Each rule `unimplemented_` of the parser has its + // production here, refused under its own name. + for rule in rules.keys() { + if let Some(production) = rule.strip_prefix("unimplemented_") { + assert!( + own_name.contains(production), + "the rule `{rule}` has no production `{production}` in part 2 \ + with the mark `[UNIMPLEMENTED] refused: as {production}`" + ); + } + } // 0 productions unaccounted for: a rule of the parser, an alias, or // a production that the parser refuses. assert_eq!(names.len(), rules.len() + aliases + refused); diff --git a/src/ephapax-parser/src/lib.rs b/src/ephapax-parser/src/lib.rs index c00647b..cf7aa12 100644 --- a/src/ephapax-parser/src/lib.rs +++ b/src/ephapax-parser/src/lib.rs @@ -66,6 +66,7 @@ pub fn parse_module(source: &str, name: &str) -> Result> span: Span::dummy(), }] })?; + refuse_unimplemented(pairs.clone())?; let pair = pairs .into_iter() @@ -153,6 +154,58 @@ pub(crate) fn negated_integer_digits<'a>( (!text.is_empty() && text.bytes().all(|b| b.is_ascii_digit())).then_some(text) } +/// The production of the v2 design that `rule` stands for, when the +/// grammar has the rule only to refuse its syntax by name (the rules +/// `unimplemented_` of ephapax.pest). +fn unimplemented_production(rule: Rule) -> Option<&'static str> { + Some(match rule { + Rule::unimplemented_import => "import", + Rule::unimplemented_reexport => "reexport", + Rule::unimplemented_capability_decl => "capability_decl", + Rule::unimplemented_capability_annotation => "capability_annotation", + Rule::unimplemented_annotation_args => "annotation_args", + Rule::unimplemented_comptime_decl => "comptime_decl", + Rule::unimplemented_trait_decl => "trait_decl", + Rule::unimplemented_impl_decl => "impl_decl", + Rule::unimplemented_effect_decl => "effect_decl", + Rule::unimplemented_handler_decl => "handler_decl", + Rule::unimplemented_type_param => "type_param", + Rule::unimplemented_where_clause => "where_clause", + Rule::unimplemented_contract_clause => "contract_clause", + Rule::unimplemented_effect_ty => "effect_ty", + Rule::unimplemented_prod_ty => "prod_ty", + Rule::unimplemented_tuple_pattern => "tuple_pattern", + Rule::unimplemented_borrow_pattern => "borrow_pattern", + Rule::unimplemented_handle_mode => "handle_mode", + Rule::unimplemented_handler_ref => "handler_ref", + Rule::unimplemented_finally_clause => "finally_clause", + Rule::unimplemented_inline_op_clause => "inline_op_clause", + Rule::unimplemented_concat_expr => "concat_expr", + Rule::unimplemented_deref_expr => "deref_expr", + Rule::unimplemented_comptime_splice => "comptime_splice", + Rule::unimplemented_type_desc_expr => "type_desc_expr", + _ => return None, + }) +} + +/// 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. +pub(crate) fn refuse_unimplemented( + pairs: pest::iterators::Pairs, +) -> Result<(), Vec> { + for pair in pairs.flatten() { + if let Some(production) = unimplemented_production(pair.as_rule()) { + return Err(vec![ParseError::Unimplemented { + production, + span: span_from_pair(&pair), + }]); + } + } + Ok(()) +} + fn span_from_pair(pair: &pest::iterators::Pair) -> Span { let span = pair.as_span(); Span::new(span.start(), span.end()) @@ -987,7 +1040,12 @@ fn parse_pattern_core(pair: pest::iterators::Pair) -> Result, ) -> Result { - let mut inner = pair.into_inner(); + // snip: this parser has no list of imports, so it does not check the + // module of `M.Ctor`; the constructor resolves as `Ctor`, as `M.f` + // does. The surface parser path checks the module. + let mut inner = pair + .into_inner() + .filter(|p| p.as_rule() != Rule::pattern_qualifier); let ctor = SmolStr::new( inner diff --git a/src/ephapax-parser/src/surface.rs b/src/ephapax-parser/src/surface.rs index 89bd899..8d69540 100644 --- a/src/ephapax-parser/src/surface.rs +++ b/src/ephapax-parser/src/surface.rs @@ -37,6 +37,7 @@ pub fn parse_surface_module(source: &str, name: &str) -> Result) -> Result body end` from a -/// tuple-binder lowering. For N=2 the pattern is `Pattern::Pair`; for N>2 -/// it becomes a right-nested chain of `Pattern::Pair`s to match ephapax's -/// existing binary-product encoding. +/// tuple-binder lowering. For N=2 the pattern is `Pattern::Pair`, as the +/// value `(x, y)` is a pair. For N>2 it is `Pattern::Tuple`, as the value +/// `(x, y, z)` is a tuple (it was a chain of pairs, which no tuple value +/// has as its type). fn match_arm_from_tuple_binder( binders: Vec, scrutinee: SurfaceExpr, body: SurfaceExpr, span: Span, ) -> SurfaceExpr { - // Build the pair pattern from the right: (a, b, c) → Pair(a, Pair(b, c)). // A `_` binder is a wildcard, not a variable named `_`: it binds // nothing, so the checker can see (and reject) a discarded linear // component instead of reporting an unconsumed variable `_`. @@ -724,12 +725,14 @@ fn match_arm_from_tuple_binder( Pattern::Var(name) } }; - let mut iter = binders.into_iter().rev(); - let last = iter.next().expect("at least 2 binders"); - let mut pat = binder(last); - for name in iter { - pat = Pattern::Pair(Box::new(binder(name)), Box::new(pat)); - } + let mut parts: Vec = binders.into_iter().map(binder).collect(); + let pat = if parts.len() == 2 { + let right = parts.pop().expect("2 binders"); + let left = parts.pop().expect("2 binders"); + Pattern::Pair(Box::new(left), Box::new(right)) + } else { + Pattern::Tuple(parts) + }; SurfaceExpr::new( SurfaceExprKind::Match { scrutinee: Box::new(scrutinee), @@ -1038,13 +1041,22 @@ fn parse_pattern(pair: pest::iterators::Pair) -> Result) -> Result { - let mut inner = pair.into_inner(); + let mut inner = pair.into_inner().peekable(); + // `M.Ctor`: the name keeps the qualifier, as `M.Ctor`. The desugarer + // knows the imports: it checks `M` and finds `Ctor`. + let qualifier = inner + .next_if(|p| p.as_rule() == Rule::pattern_qualifier) + .map(|p| p.as_str().to_string()); let ctor = parse_constructor_name( inner .next() .ok_or_else(|| ParseError::missing("constructor name"))?, ); + let ctor = match qualifier { + Some(qualifier) => format!("{qualifier}.{ctor}").into(), + None => ctor, + }; let mut args = Vec::new(); for item in inner { diff --git a/tests/v2-grammar/productions/expressions.eph b/tests/v2-grammar/productions/expressions.eph index e37911f..fd6af07 100644 --- a/tests/v2-grammar/productions/expressions.eph +++ b/tests/v2-grammar/productions/expressions.eph @@ -33,6 +33,7 @@ fn control(o: Option(I32), s: I32 + I32): I32 = | None => 0 | Some(0) => 1 | Some(n) if n > 5 => n + | lib.Some(n) => n | _ => 2 end in let i = match (g, h) of | (a, b) => a + b end in