diff --git a/DECISIONS.md b/DECISIONS.md index 2283a92..15a649e 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -11,3 +11,19 @@ against the existing examples. - **The Java package for generated code comes from `-package` in build.gradle only**; rejected: `@header` package declaration in the grammar (combined with `-package` it generates a duplicate `package` statement that does not compile). - **ANTLR warnings fail the build** (`-Werror` in build.gradle); rejected: warnings as advisory output (they hid the shadowed-token bugs). - **The choice-type rule is `⟨ expr ⟩`** with the interior `|` consumed by `condExpr`; rejected: explicit `⟨ expr | expr ⟩` (the interior expr already consumes the bar, making the explicit BAR unreachable). +- **Function calls are `f(a, b)` — a postfix argument list, nullary `f()` included**; rejected: Haskell juxtaposition `f x` (programmer convention, fails the Fatima test — children learn `f(x)` in school). +- **SEMICOLON has one role: sequence separator (trailing `;` permitted)**; the program, blocks, `(...)`, `⌈...⌉`, `〔...〕`, `⌜...⌝`, `⌞...⌟` and `⟳(...)` all contain one `seqExpr`; rejected: a separate statement-terminator rule duplicating the same token (two roles for one symbol). +- **Braces disambiguate structurally**: `IDENTIFIER :` → record, two-plus comma-separated exprs → set, everything else (incl. `{}` and `{x}`) → block; a singleton set literal cannot be written (deferred to M1); rejected: parser-order coin flips left undocumented. +- **`≜` defines, `←` assigns; both wired into the chain** with `≜` binding looser than `←`, both right-associative; rejected: `≜` tokenized but unreachable. +- **`⊕`/`⊖` are postfix resource operators** (`database ⊕`, `conn ⊖`), matching every example; rejected: prefix form `⊕open(path)` that appeared only in the whitepaper. +- **Channel operations are subscripted prefix operators** `⇀_ch expr` / `↽_ch expr`; rejected: leaving SEND/RECEIVE orphaned. +- **`‧` is qualified module access** (`Mathematics‧sin(angle)`), a postfix `‧IDENTIFIER`; rejected: leaving MIDDOT orphaned, or `.` (removed — one access syntax). +- **Handler clauses are `↯pattern ⟹ expr`, semicolon-separated**, where pattern is an identifier (binds the exception) or a string (matches a message); rejected: `↯e ⇒ expr` (⇒ is EXPORT; the clause arrow should mirror the guarded-alternative arrow ⟹). +- **IDENTIFIER may not start with `_`**, so subscripts (`⌉_db_lock`, `↽_socket`) lex as UNDERSCORE + IDENTIFIER; rejected: identifiers with a leading underscore (made every subscript lex as one identifier token). +- **`/` is an ASCII alias of ÷ (DIV)** so `π/4` parses; rejected: ÷-only division (unreachable on most keyboards). +- **Unary minus exists** (`-x`), sharing the MINUS token at prefix level; rejected: binary-only minus (cannot write negative numbers); unary plus was NOT added (`x ++ y` stays invalid). +- **`pathLiteral` accepts `🖫"…"` and `🖫identifier`**, as required by `readFile(🖫path)` in example 03. +- **Deleted tokens: `?` (QUERY), `∃` (EXISTS), `⇐` (IMPORT), `→` (ARROW), `.` (DOT)** — defined but used by no parser rule and no example; dead operators are debt; each returns in M1 only with a documented semantic. Rejected: keeping them tokenized-but-unreachable. +- **Exactly one ASCII escape per glyph** (`\lambda` not `\lam`, `\leftarrow` not `\gets`, `\neq` not `\ne`, `\nat` not `\N`, …); rejected: alias sets (two ways to write the same token). +- **`%` (modulo), `∑`, `√`, `²`, `|x|`, ranges `[a..b]`, indexing/slicing, `where`, and record field access are NOT in M0** — every document says so instead of using them; deferred to M1 with semantics, not smuggled in via prose. +- **Lambda parameters are a bare comma-separated pattern list** (`λa, b: body`); rejected: parenthesized parameter lists `λ(a, b):` (two ways to write parameters). diff --git a/src/main/antlr4/MPL.g4 b/src/main/antlr4/MPL.g4 index 3e56d53..a55ab30 100644 --- a/src/main/antlr4/MPL.g4 +++ b/src/main/antlr4/MPL.g4 @@ -5,56 +5,76 @@ grammar MPL; // ============================================================================ // PARSER RULES +// +// Structural decisions (see DECISIONS.md for rationale): +// +// * SEMICOLON has exactly one role: it separates expressions in a sequence +// (`seqExpr`). A trailing semicolon is permitted. There is no separate +// "statement terminator" concept; the program, blocks, parenthesized +// groups, atomic sections, RAII scopes and code quotations all contain a +// single seqExpr. +// +// * Braces are disambiguated structurally: +// - record: `{ name: expr, ... }` — at least one `IDENTIFIER : expr` +// - set: `{ a, b, ... }` — at least TWO comma-separated exprs +// - block: everything else, including `{}` and `{ x }` +// A singleton set cannot be written literally (use ∅ and set operations +// in M1); singleton braces always parse as a block. +// +// * Function calls are `f(a, b)` only — a postfix argument list. Haskell +// juxtaposition (`f x`) was removed. Nullary calls `f()` are supported. +// +// * ≜ defines (right-associative, binds looser than ←); ← assigns. +// +// * Guarded alternatives `(condition ⟹ result) | fallback` are the one +// conditional form, wired in as the condExpr precedence level. // ============================================================================ program - : statement* EOF - ; - -statement - : expr SEMICOLON - | expr // Allow last statement without semicolon - ; - -// Expression hierarchy following precedence table (lowest to highest) -expr - : seqExpr // Level -2: Statement sequencing + : seqExpr? EOF ; seqExpr - : parallelExpr (SEMICOLON parallelExpr)* + : expr (SEMICOLON expr)* SEMICOLON? + ; + +// Expression precedence chain, lowest binding first. +expr + : parallelExpr ; parallelExpr - : assignExpr (PARALLEL assignExpr)* // Level -1: Parallel composition + : defExpr (PARALLEL defExpr)* // ‖ parallel composition + ; + +defExpr + : assignExpr (DEFINITION defExpr)? // ≜ definition (right-assoc) ; assignExpr - : condExpr (LEFTARROW assignExpr)? // Level 0: Assignment (right-assoc) + : condExpr (LEFTARROW assignExpr)? // ← assignment (right-assoc) ; // Guarded alternatives: (condition ⟹ result) | fallback -// This is the canonical conditional form. It lives in the precedence chain -// (below assignment, above implication) instead of being a left-recursive -// standalone rule, which previously caused ANTLR error(119). +// The one conditional form. The BAR inside ⟨a|b⟩ is also consumed here. condExpr : impliesExpr (BAR impliesExpr)* ; impliesExpr - : orExpr (IMPLIES impliesExpr)? // Level 1: Implication (right-assoc) + : orExpr (IMPLIES impliesExpr)? // ⟹ guard/implication (right-assoc) ; orExpr - : andExpr (OR andExpr)* // Level 2: Logical OR (left-assoc) + : andExpr (OR andExpr)* // ∨ logical OR ; andExpr - : compareExpr (AND compareExpr)* // Level 3: Logical AND (left-assoc) + : cmpExpr (AND cmpExpr)* // ∧ logical AND ; -compareExpr - : addExpr (compareOp addExpr)? // Level 4: Comparisons (non-assoc) +cmpExpr + : addExpr (compareOp addExpr)? // comparisons (non-associative) ; compareOp @@ -62,48 +82,60 @@ compareOp ; addExpr - : mulExpr ((PLUS | MINUS) mulExpr)* // Level 5: Addition/subtraction + : mulExpr ((PLUS | MINUS) mulExpr)* ; mulExpr - : composeExpr ((TIMES | DIV | AST) composeExpr)* // Level 6: Multiplication + : composeExpr ((TIMES | DIV | AST) composeExpr)* ; composeExpr - : unaryExpr (COMPOSE unaryExpr)* // Level 7: Composition + : unaryExpr (COMPOSE unaryExpr)* // ∘ function composition ; unaryExpr - : prefixOp* postfixExpr // Level 8: Prefix operators + : (prefixOp | channelOp)* postfixExpr ; +// MINUS doubles as unary negation: -x prefixOp - : RAISE | TRACE | QUERY | BREAK | DELAY + : RAISE | TRACE | BREAK | DELAY | MINUS ; -// Exception handling is a postfix construct: expr ↴ { ↯name ⇒ handler } -// Formerly a standalone rule reachable from atomExpr, which cycled back into -// expr and caused ANTLR error(119) (mutual left recursion). +// Channel operations are subscripted prefix operators: ⇀_ch expr, ↽_ch expr +channelOp + : (SEND | RECEIVE) UNDERSCORE IDENTIFIER + ; + +// Postfix operators: call, qualified access, resource alloc/release, handler. postfixExpr - : appExpr handlerSuffix* + : atomExpr postfixOp* ; -handlerSuffix - : HANDLE LBRACE handlerClause+ RBRACE +postfixOp + : callArgs // f(a, b) — the one call syntax + | MIDDOT IDENTIFIER // Module‧member + | ALLOC // resource ⊕ + | RELEASE // resource ⊖ + | HANDLE handlerBlock // expr ↴ { ↯pat ⟹ expr; ... } ; +callArgs + : LPAREN (expr (COMMA expr)*)? RPAREN + ; + +handlerBlock + : LBRACE handlerClause (SEMICOLON handlerClause)* SEMICOLON? RBRACE + ; + +// ↯identifier binds the exception; ↯"literal" matches a specific message. +// The clause arrow is ⟹, mirroring guarded alternatives. handlerClause - : RAISE IDENTIFIER EXPORT expr - ; - -appExpr - : atomExpr atomExpr* // Level 9: Function application + : RAISE (IDENTIFIER | STRING) IMPLIES expr ; atomExpr - : primary - | LPAREN expr RPAREN - | block + : LPAREN seqExpr RPAREN | lambda | forall | choiceType @@ -114,6 +146,10 @@ atomExpr | periodicTask | moduleDecl | pathLiteral + | record + | set + | block + | primary ; primary @@ -128,13 +164,11 @@ primary | EMPTYSET | typeSymbol | list - | set - | record ; greekVar : ALPHA | BETA | GAMMA | DELTA | EPSILON | ZETA | ETA | THETA - | IOTA | KAPPA | LAMBDA_VAR | MU | NU | XI | OMICRON | PI + | IOTA | KAPPA | LAMBDA_VAR | MU | NU | XI | OMICRON | PI | RHO | SIGMA | TAU | UPSILON | PHI | CHI | PSI | OMEGA ; @@ -142,16 +176,13 @@ typeSymbol : NAT | INT | RAT | REAL | COMPLEX | BOOL ; -block - : LBRACE statement* RBRACE - ; - +// λx: body λx,y: body λx∈ℝ: body — parameters are a bare pattern list. lambda - : LAMBDA_VAR pattern (IN expr)? COLON expr + : LAMBDA_VAR pattern (IN condExpr)? COLON expr ; forall - : FORALL pattern IN expr COLON expr + : FORALL pattern IN condExpr COLON expr ; // The | inside ⟨a|b⟩ is consumed by condExpr, so the rule needs no explicit BAR. @@ -160,23 +191,23 @@ choiceType ; atomicSection - : LCEIL expr RCEIL (UNDERSCORE IDENTIFIER)? + : LCEIL seqExpr RCEIL (UNDERSCORE IDENTIFIER)? ; raiiScope - : LRAII statement* RRAII + : LRAII seqExpr? RRAII ; codeQuote - : ULCORNER expr URCORNER + : ULCORNER seqExpr URCORNER ; codeEval - : LLCORNER expr LRCORNER + : LLCORNER seqExpr LRCORNER ; periodicTask - : PERIODIC LPAREN expr COMMA NUMBER IDENTIFIER? RPAREN + : PERIODIC LPAREN seqExpr COMMA NUMBER IDENTIFIER? RPAREN ; moduleDecl @@ -184,7 +215,7 @@ moduleDecl ; pathLiteral - : PATH STRING + : PATH (STRING | IDENTIFIER) ; // Rewritten from the left-recursive, empty-tail form that caused error(148). @@ -202,8 +233,9 @@ list : LBRACK (expr (COMMA expr)*)? RBRACK ; +// At least two elements — singleton braces parse as a block (see header note). set - : LBRACE (expr (COMMA expr)*)? RBRACE + : LBRACE expr (COMMA expr)+ RBRACE ; record @@ -214,8 +246,15 @@ fieldAssignment : IDENTIFIER COLON expr ; +block + : LBRACE seqExpr? RBRACE + ; + // ============================================================================ // LEXER RULES +// +// Exactly one ASCII escape per glyph (One Right Answer). The authoritative +// escape table is glyph-escapes.md, which must match these rules exactly. // ============================================================================ // Keywords @@ -234,7 +273,7 @@ ETA : 'η' | '\\eta' ; THETA : 'θ' | '\\theta' ; IOTA : 'ι' | '\\iota' ; KAPPA : 'κ' | '\\kappa' ; -LAMBDA_VAR : 'λ' | '\\lambda' | '\\lam' ; +LAMBDA_VAR : 'λ' | '\\lambda' ; MU : 'μ' | '\\mu' ; NU : 'ν' | '\\nu' ; XI : 'ξ' | '\\xi' ; @@ -250,41 +289,40 @@ PSI : 'ψ' | '\\psi' ; OMEGA : 'ω' | '\\omega' ; // Type symbols -NAT : 'ℕ' | '\\nat' | '\\N' ; -INT : 'ℤ' | '\\int' | '\\Z' ; -RAT : 'ℚ' | '\\rat' | '\\Q' ; -REAL : 'ℝ' | '\\real' | '\\R' ; -COMPLEX : 'ℂ' | '\\complex' | '\\C' ; -BOOL : '𝔹' | '\\bool' | '\\B' ; +NAT : 'ℕ' | '\\nat' ; +INT : 'ℤ' | '\\int' ; +RAT : 'ℚ' | '\\rat' ; +REAL : 'ℝ' | '\\real' ; +COMPLEX : 'ℂ' | '\\complex' ; +BOOL : '𝔹' | '\\bool' ; // Operators by precedence SEMICOLON : ';' ; PARALLEL : '‖' | '\\parallel' ; -LEFTARROW : '←' | '\\leftarrow' | '\\gets' ; +LEFTARROW : '←' | '\\leftarrow' ; // '\Rightarrow' belongs to EXPORT (⇒); giving it to IMPLIES too fully // shadowed EXPORT's escape (ANTLR warning 184). IMPLIES : '⟹' | '\\implies' ; -OR : '∨' | '\\or' | '\\vee' ; -AND : '∧' | '\\and' | '\\wedge' ; +OR : '∨' | '\\or' ; +AND : '∧' | '\\and' ; EQ : '=' ; -NEQ : '≠' | '\\neq' | '\\ne' ; +NEQ : '≠' | '\\neq' ; LT : '<' ; GT : '>' ; -LEQ : '≤' | '\\leq' | '\\le' ; -GEQ : '≥' | '\\geq' | '\\ge' ; +LEQ : '≤' | '\\leq' ; +GEQ : '≥' | '\\geq' ; APPROX : '≈' | '\\approx' ; SIM : '∼' | '\\sim' ; PLUS : '+' ; MINUS : '-' ; TIMES : '×' | '\\times' ; -DIV : '÷' | '\\div' ; +DIV : '÷' | '\\div' | '/' ; AST : '∗' | '\\ast' ; COMPOSE : '∘' | '\\circ' ; // Unary operators RAISE : '↯' | '\\raise' ; TRACE : '✎' | '\\trace' ; -QUERY : '?' | '\\query' ; BREAK : '⧈' | '\\break' ; DELAY : '⏲' | '\\delay' ; @@ -292,13 +330,11 @@ DELAY : '⏲' | '\\delay' ; // (LAMBDA was fully shadowed by LAMBDA_VAR — warning 184; LAMBDA_VAR is the // single λ token and the lambda parser rule uses it.) FORALL : '∀' | '\\forall' ; -EXISTS : '∃' | '\\exists' ; DEFINITION : '≜' | '\\coloneq' ; HANDLE : '↴' | '\\handle' ; ALLOC : '⊕' | '\\oplus' ; RELEASE : '⊖' | '\\ominus' ; MODULE : '𝓜' | '\\module' ; -IMPORT : '⇐' | '\\Leftarrow' ; EXPORT : '⇒' | '\\Rightarrow' ; SEND : '⇀' | '\\send' ; RECEIVE : '↽' | '\\receive' ; @@ -328,15 +364,15 @@ LRCORNER : '⌟' | '\\lrcorner' ; // Other symbols COLON : ':' ; COMMA : ',' ; -DOT : '.' ; UNDERSCORE : '_' ; BAR : '|' ; -ARROW : '→' | '\\rightarrow' | '\\to' ; MIDDOT : '‧' ; -// Identifiers +// Identifiers. A leading underscore is NOT allowed: subscripts such as +// ⌉_db_lock and ↽_socket must lex as UNDERSCORE + IDENTIFIER, not as a +// single identifier "_db_lock". IDENTIFIER - : [a-zA-Z_][a-zA-Z0-9_]* + : [a-zA-Z][a-zA-Z0-9_]* ; // Numbers @@ -390,4 +426,4 @@ MULTILINE_COMMENT // Whitespace WS : [ \t\r\n]+ -> skip - ; \ No newline at end of file + ; diff --git a/src/test/java/com/mpl/test/LexerTest.java b/src/test/java/com/mpl/test/LexerTest.java index d7f4025..c45fff3 100644 --- a/src/test/java/com/mpl/test/LexerTest.java +++ b/src/test/java/com/mpl/test/LexerTest.java @@ -52,6 +52,7 @@ public class LexerTest extends MPLTestBase { assertTokenTypes("-", MPLLexer.MINUS); assertTokenTypes("×", MPLLexer.TIMES); assertTokenTypes("÷", MPLLexer.DIV); + assertTokenTypes("/", MPLLexer.DIV); // ASCII alias, e.g. π/4 assertTokenTypes("∗", MPLLexer.AST); assertTokenTypes("∘", MPLLexer.COMPOSE); @@ -74,6 +75,21 @@ public class LexerTest extends MPLTestBase { assertTokenTypes("←", MPLLexer.LEFTARROW); assertTokenTypes("≜", MPLLexer.DEFINITION); } + + @Test + public void testEscapeDisambiguation() throws IOException { + // \implies is ⟹ (IMPLIES); \Rightarrow is ⇒ (EXPORT). Formerly both + // mapped to IMPLIES, fully shadowing EXPORT's escape (warning 184). + assertTokenTypes("\\implies", MPLLexer.IMPLIES); + assertTokenTypes("\\Rightarrow", MPLLexer.EXPORT); + assertTokenTypes("⇒", MPLLexer.EXPORT); + } + + @Test + public void testModuleAccess() throws IOException { + assertTokenTypes("Mathematics‧sin", + MPLLexer.IDENTIFIER, MPLLexer.MIDDOT, MPLLexer.IDENTIFIER); + } @Test public void testEffectOperators() throws IOException { @@ -123,9 +139,23 @@ public class LexerTest extends MPLTestBase { @Test public void testIdentifiers() throws IOException { assertTokenTypes("foo", MPLLexer.IDENTIFIER); - assertTokenTypes("_bar", MPLLexer.IDENTIFIER); assertTokenTypes("baz123", MPLLexer.IDENTIFIER); assertTokenTypes("camelCase", MPLLexer.IDENTIFIER); + assertTokenTypes("db_lock", MPLLexer.IDENTIFIER); + } + + @Test + public void testSubscripts() throws IOException { + // Identifiers must not start with an underscore, so that subscripted + // constructs lex as UNDERSCORE + IDENTIFIER instead of one identifier. + // (Previously "⌉_db_lock" lexed as RCEIL + IDENTIFIER "_db_lock".) + assertTokenTypes("_bar", MPLLexer.UNDERSCORE, MPLLexer.IDENTIFIER); + assertTokenTypes("⌉_db_lock", + MPLLexer.RCEIL, MPLLexer.UNDERSCORE, MPLLexer.IDENTIFIER); + assertTokenTypes("↽_socket", + MPLLexer.RECEIVE, MPLLexer.UNDERSCORE, MPLLexer.IDENTIFIER); + assertTokenTypes("⇀_socket", + MPLLexer.SEND, MPLLexer.UNDERSCORE, MPLLexer.IDENTIFIER); } @Test @@ -140,6 +170,8 @@ public class LexerTest extends MPLTestBase { public void testPathLiterals() throws IOException { assertTokenTypes("🖫 \"path\"", MPLLexer.PATH, MPLLexer.STRING); assertTokenTypes("\\path \"path\"", MPLLexer.PATH, MPLLexer.STRING); + // Identifier form, e.g. readFile(🖫path) + assertTokenTypes("🖫path", MPLLexer.PATH, MPLLexer.IDENTIFIER); } @Test diff --git a/src/test/java/com/mpl/test/MPLTestBase.java b/src/test/java/com/mpl/test/MPLTestBase.java index d48e30c..1e13e9b 100644 --- a/src/test/java/com/mpl/test/MPLTestBase.java +++ b/src/test/java/com/mpl/test/MPLTestBase.java @@ -34,23 +34,29 @@ public class MPLTestBase { } private ParseTree parseWithErrors(String input, boolean collectErrors, List errorList) throws IOException { - ANTLRInputStream inputStream = new ANTLRInputStream(input); + // CharStreams works in Unicode code points; the deprecated + // ANTLRInputStream fed UTF-16 units and broke on supplementary-plane + // glyphs such as 𝔹, 𝓜 and 🖫. + CharStream inputStream = CharStreams.fromString(input); MPLLexer lexer = new MPLLexer(inputStream); CommonTokenStream tokens = new CommonTokenStream(lexer); MPLParser parser = new MPLParser(tokens); - + if (collectErrors && errorList != null) { - parser.removeErrorListeners(); - parser.addErrorListener(new BaseErrorListener() { + BaseErrorListener listener = new BaseErrorListener() { @Override public void syntaxError(Recognizer recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) { errorList.add(String.format("line %d:%d %s", line, charPositionInLine, msg)); } - }); + }; + lexer.removeErrorListeners(); + lexer.addErrorListener(listener); + parser.removeErrorListeners(); + parser.addErrorListener(listener); } - + return parser.program(); } @@ -84,7 +90,7 @@ public class MPLTestBase { * Get all tokens from input */ protected List tokenize(String input) throws IOException { - ANTLRInputStream inputStream = new ANTLRInputStream(input); + CharStream inputStream = CharStreams.fromString(input); MPLLexer lexer = new MPLLexer(inputStream); List tokens = new ArrayList<>(); diff --git a/src/test/java/com/mpl/test/ParseExamples.java b/src/test/java/com/mpl/test/ParseExamples.java index 9ef6e2d..6371ea2 100644 --- a/src/test/java/com/mpl/test/ParseExamples.java +++ b/src/test/java/com/mpl/test/ParseExamples.java @@ -59,15 +59,14 @@ public class ParseExamples { } private static void parseFile(Path file) throws IOException { - String content = Files.readString(file); - - ANTLRInputStream input = new ANTLRInputStream(content); + // CharStreams works in Unicode code points; the deprecated + // ANTLRInputStream broke on supplementary-plane glyphs (𝓜, 🖫). + CharStream input = CharStreams.fromPath(file); MPLLexer lexer = new MPLLexer(input); CommonTokenStream tokens = new CommonTokenStream(lexer); MPLParser parser = new MPLParser(tokens); - - // Collect errors - parser.removeErrorListeners(); + + // Fail on both lexer and parser errors var errorListener = new BaseErrorListener() { @Override public void syntaxError(Recognizer recognizer, Object offendingSymbol, @@ -76,8 +75,11 @@ public class ParseExamples { throw new RuntimeException(String.format("line %d:%d %s", line, charPositionInLine, msg)); } }; + lexer.removeErrorListeners(); + lexer.addErrorListener(errorListener); + parser.removeErrorListeners(); parser.addErrorListener(errorListener); - + // Parse parser.program(); } diff --git a/src/test/java/com/mpl/test/ParserTest.java b/src/test/java/com/mpl/test/ParserTest.java index 4646135..ff1e209 100644 --- a/src/test/java/com/mpl/test/ParserTest.java +++ b/src/test/java/com/mpl/test/ParserTest.java @@ -53,12 +53,15 @@ public class ParserTest extends MPLTestBase { assertParses("(x ← y);"); } + // Function calls are f(a, b) only — juxtaposition (f x) was removed. @Test - public void testFunctionApplication() throws IOException { - assertParses("f x;"); - assertParses("g a b c;"); - assertParses("sin π;"); - assertParses("(f x) y;"); + public void testFunctionCalls() throws IOException { + assertParses("f(x);"); + assertParses("g(a, b, c);"); + assertParses("sin(π);"); + assertParses("f(x)(y);"); + assertParses("f();"); // nullary call + assertParses("mergeResults();"); } @Test @@ -66,14 +69,15 @@ public class ParserTest extends MPLTestBase { assertParses("λx: x + 1;"); assertParses("λx∈ℕ: x × 2;"); assertParses("λa: λb: a + b;"); - assertParses("(λx: x × x) 5;"); + assertParses("λa, b: a + b;"); // multi-parameter pattern + assertParses("(λx: x × x)(5);"); // was (λx: x × x) 5 — juxtaposition removed } @Test public void testForall() throws IOException { - assertParses("∀x∈S: P x;"); + assertParses("∀x∈S: P(x);"); // was P x — juxtaposition removed assertParses("∀n∈ℕ: n ≥ 0;"); - assertParses("∀x∈A: ∀y∈B: f x y;"); + assertParses("∀x∈A: ∀y∈B: f(x, y);"); // was f x y — juxtaposition removed } @Test @@ -81,19 +85,31 @@ public class ParserTest extends MPLTestBase { assertParses("f ≜ λx: x + 1;"); assertParses("pi ≜ 3.14159;"); assertParses("id ≜ λx: x;"); + assertParses("π ≜ 3.14159;"); // greek letter on the left } @Test public void testBlocks() throws IOException { assertParses("{ x ← 1; y ← 2; x + y }"); - assertParses("{ a ← b; { c ← d; } e }"); + // Was "{ a ← b; { c ← d; } e }": the inner block juxtaposed with e + // relied on juxtaposition application, which was removed. + assertParses("{ a ← b; { c ← d; }; e }"); assertParses("{ }"); + assertParses("{ x }"); // singleton braces are a block } @Test public void testConditionals() throws IOException { assertParses("x > 0 ⟹ x | -x;"); - assertParses("(n = 0 ⟹ 1) | (n × fact (n-1));"); + assertParses("(n = 0 ⟹ 1) | (n × fact(n-1));"); + assertParses("(n≤1 ⟹ 1) | (n×factorial(n-1));"); + } + + @Test + public void testUnaryMinus() throws IOException { + assertParses("-x;"); + assertParses("a - -b;"); + assertParses("f(-1);"); } @Test @@ -129,26 +145,52 @@ public class ParserTest extends MPLTestBase { assertParses("↯\"error\";"); assertParses("✎\"log message\";"); assertParses("⏲ 100;"); - assertParses("x ↴ {↯e ⇒ handle e};"); + // Canonical handler clause: ↯pattern ⟹ expr (was ↯e ⇒ handle e) + assertParses("x ↴ {↯e ⟹ handle(e)};"); + assertParses("x ↴ {↯\"Invalid user\" ⟹ ⊥};"); + // Multiple clauses are semicolon-separated + assertParses("x ↴ {↯\"overflow\" ⟹ 0; ↯e ⟹ ↯e};"); + } + + @Test + public void testResources() throws IOException { + assertParses("conn ← database ⊕;"); + assertParses("conn ⊖;"); + assertParses("socket ← bind(port) ⊕;"); + } + + @Test + public void testChannels() throws IOException { + assertParses("data ← ↽_socket request;"); + assertParses("⇀_socket response;"); + } + + @Test + public void testModuleAccess() throws IOException { + assertParses("Mathematics‧sin(angle);"); + assertParses("A‧B‧f(x);"); } @Test public void testParallel() throws IOException { assertParses("a ‖ b;"); assertParses("task1 ‖ task2 ‖ task3;"); - assertParses("(f x) ‖ (g y);"); + assertParses("f(x) ‖ g(y);"); // was (f x) ‖ (g y) — juxtaposition removed } @Test public void testAtomic() throws IOException { assertParses("⌈x ← x + 1⌉;"); - assertParses("⌈critical section⌉_lock;"); + // Was "⌈critical section⌉_lock" — juxtaposition removed + assertParses("⌈criticalSection()⌉_lock;"); + assertParses("⌈a ← 1; b ← 2⌉_db_lock;"); } @Test public void testRAII() throws IOException { - assertParses("〔 r ← resource ⊕; use r 〕;"); - assertParses("〔 f ← open \"file\"; read f 〕;"); + // Was "use r" / "open \"file\"" / "read f" — juxtaposition removed + assertParses("〔 r ← resource ⊕; use(r) 〕;"); + assertParses("〔 f ← open(\"file\"); read(f) 〕;"); } @Test @@ -167,6 +209,7 @@ public class ParserTest extends MPLTestBase { public void testPaths() throws IOException { assertParses("🖫\"file.txt\";"); assertParses("\\path\"directory/file\";"); + assertParses("readFile(🖫path);"); // identifier form } @Test @@ -174,8 +217,8 @@ public class ParserTest extends MPLTestBase { // Factorial assertParses("factorial ≜ λn∈ℕ: (n≤1 ⟹ 1) | (n×factorial(n-1));"); - // File processing - assertParses("processFile ≜ λpath: { data ← readFile(🖫path); result ← transform(data); writeFile(result, 🖫\"output.txt\"); ⟨\"success\"|\"failed\"⟩ } ↴ {↯e ⇒ ⟨⊥|e⟩};"); + // File processing (canonical handler clause is ↯pattern ⟹ expr) + assertParses("processFile ≜ λpath: { data ← readFile(🖫path); result ← transform(data); writeFile(result, 🖫\"output.txt\"); ⟨\"success\"|\"failed\"⟩ } ↴ {↯e ⟹ ⟨⊥|e⟩};"); // Network server assertParses("server ≜ λport: 〔 socket ← bind(port) ⊕; ∀request∈acceptLoop(socket): ( data ← ↽_socket request; response ← processRequest(data); ⇀_socket response ) ‖ handleNext() 〕;"); @@ -197,6 +240,13 @@ public class ParserTest extends MPLTestBase { // Invalid lambda syntax assertDoesNotParse("λ: x;"); assertDoesNotParse("λx y: x + y;"); + + // Juxtaposition application was removed — calls need parentheses + assertDoesNotParse("f x;"); + assertDoesNotParse("g a b c;"); + + // The C-style ternary is not MPL — use (cond ⟹ a) | b + assertDoesNotParse("cond ? a : b;"); } @Test