diff --git a/docs/pbs/pull-requests/PR-005-pbs-parser-expressions-optional-result-apply.md b/docs/pbs/pull-requests/PR-005-pbs-parser-expressions-optional-result-apply.md deleted file mode 100644 index c54c8751..00000000 --- a/docs/pbs/pull-requests/PR-005-pbs-parser-expressions-optional-result-apply.md +++ /dev/null @@ -1,36 +0,0 @@ -# PR-005 - PBS Parser Expressions, Optional/Result, and Apply - -## Briefing -A gramatica de expressoes da spec inclui formas que ainda nao existem no frontend (`apply`, `else`, `switch`, `handle`, `new`, `bind`, `some/none/ok/err`, member/postfix avancado). -Este PR fecha a cobertura sintatica dessas expressoes e preserva as regras de precedencia/rejeicao. - -## Target -- Specs: - - `docs/pbs/specs/3. Core Syntax Specification.md` (secao 10, 13, 14) - - `docs/pbs/specs/11. AST Specification.md` (secoes 9, 10) -- Codigo: - - `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsExprParser.java` - - `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/ast/PbsAst.java` - - `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/ParseErrors.java` - -## Method -1. Implementar cadeia de precedencia completa incluindo `apply` right-associative. -2. Implementar `else` extraction, `if` expression, `switch`, `handle` e `as`. -3. Implementar postfixes: member access, call sugar, propagate `!`. -4. Suportar primarias: `this`, `new`, `bind`, `some`, `none`, `ok`, `err`, unit e tuple expr. -5. Rejeitar cadeias nao associativas e formas proibidas (`?` propagation, single-slot tuple literal). - -## Acceptance Criteria -- Parser respeita precedencia/associatividade normativa (incluindo `apply` e `else`). -- `a < b < c` e `a == b == c` continuam rejeitados deterministicamente. -- `if` expression exige `else` e bloco nos ramos. -- `switch` e `handle` parseiam mapa de arms e wildcard conforme a gramatica. -- Superficies `optional/result` sao parseadas sem inferir semantica indevida. - -## Tests -- `PbsExprParserTest` ampliado com: - - precedencia completa e associatividade; - - expressoes `apply` encadeadas; - - `switch`/`handle` validos e invalidos; - - `some/none/ok/err/bind/new`; - - cenarios negativos obrigatorios da secao 12 de syntax. diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/ast/PbsAst.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/ast/PbsAst.java index 6de2ab38..e84f4039 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/ast/PbsAst.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/ast/PbsAst.java @@ -310,8 +310,26 @@ public final class PbsAst { BoolLiteralExpr, UnaryExpr, BinaryExpr, + ApplyExpr, + ElseExpr, + IfExpr, + SwitchExpr, + HandleExpr, + AsExpr, CallExpr, - GroupExpr { + MemberExpr, + PropagateExpr, + GroupExpr, + ThisExpr, + NewExpr, + BindExpr, + SomeExpr, + NoneExpr, + OkExpr, + ErrExpr, + UnitExpr, + TupleExpr, + BlockExpr { Span span(); } @@ -358,17 +376,172 @@ public final class PbsAst { Span span) implements Expression { } + public record ApplyExpr( + Expression callee, + Expression argument, + Span span) implements Expression { + } + + public record ElseExpr( + Expression optionalExpression, + Expression fallbackExpression, + Span span) implements Expression { + } + + public record IfExpr( + Expression condition, + Block thenBlock, + Expression elseExpression, + Span span) implements Expression { + } + + public sealed interface SwitchPattern permits WildcardSwitchPattern, + LiteralSwitchPattern, + EnumCaseSwitchPattern { + Span span(); + } + + public record WildcardSwitchPattern( + Span span) implements SwitchPattern { + } + + public record LiteralSwitchPattern( + Expression literal, + Span span) implements SwitchPattern { + } + + public record EnumCaseSwitchPattern( + ErrorPath path, + Span span) implements SwitchPattern { + } + + public record SwitchArm( + SwitchPattern pattern, + Block block, + Span span) { + } + + public record SwitchExpr( + Expression selector, + ReadOnlyList arms, + Span span) implements Expression { + } + + public sealed interface HandlePattern permits WildcardHandlePattern, ErrorPathHandlePattern { + Span span(); + } + + public record WildcardHandlePattern( + Span span) implements HandlePattern { + } + + public record ErrorPathHandlePattern( + ErrorPath path, + Span span) implements HandlePattern { + } + + public record HandleArm( + HandlePattern pattern, + ErrorPath remapTarget, + Block block, + Span span) { + } + + public record HandleExpr( + Expression value, + ReadOnlyList arms, + Span span) implements Expression { + } + + public record AsExpr( + Expression expression, + String contractName, + Span span) implements Expression { + } + public record CallExpr( Expression callee, ReadOnlyList arguments, Span span) implements Expression { } + public record MemberExpr( + Expression receiver, + String memberName, + Span span) implements Expression { + } + + public record PropagateExpr( + Expression expression, + Span span) implements Expression { + } + public record GroupExpr( Expression expression, Span span) implements Expression { } + public record ThisExpr( + Span span) implements Expression { + } + + public record NewExpr( + String typeName, + String ctorName, + ReadOnlyList arguments, + Span span) implements Expression { + } + + public record BindExpr( + Expression contextExpression, + String functionName, + Span span) implements Expression { + } + + public record SomeExpr( + Expression value, + Span span) implements Expression { + } + + public record NoneExpr( + Span span) implements Expression { + } + + public record OkExpr( + Expression value, + Span span) implements Expression { + } + + public record ErrExpr( + ErrorPath errorPath, + Span span) implements Expression { + } + + public record UnitExpr( + Span span) implements Expression { + } + + public record TupleItem( + String label, + Expression expression, + Span span) { + } + + public record TupleExpr( + ReadOnlyList items, + Span span) implements Expression { + } + + public record BlockExpr( + Block block, + Span span) implements Expression { + } + + public record ErrorPath( + ReadOnlyList segments, + Span span) { + } + // Barrel declarations are represented explicitly for linking-visible declaration families. public sealed interface BarrelItem permits BarrelFunctionItem, BarrelStructItem, diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/ParseErrors.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/ParseErrors.java index 6bf993a1..f36a5e0b 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/ParseErrors.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/ParseErrors.java @@ -12,4 +12,8 @@ public enum ParseErrors { E_PARSE_INVALID_ASSIGN_TARGET, E_PARSE_INVALID_FOR_FORM, E_PARSE_LOOP_CONTROL_OUTSIDE_LOOP, + E_PARSE_INVALID_SWITCH_FORM, + E_PARSE_INVALID_HANDLE_FORM, + E_PARSE_INVALID_TUPLE_LITERAL, + E_PARSE_INVALID_PROPAGATE_OPERATOR, } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsExprParser.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsExprParser.java index 66dce85e..f08575a7 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsExprParser.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsExprParser.java @@ -12,9 +12,6 @@ import java.util.ArrayList; /** * Dedicated expression parser for PBS. - * - *

This keeps precedence handling separate from declaration parsing and makes - * the top-level parser easier to read and extend. */ final class PbsExprParser { private final PbsTokenCursor cursor; @@ -34,15 +31,138 @@ final class PbsExprParser { * Entry point for expression parsing. */ PbsAst.Expression parseExpression() { - return parseOr(); + return parseHandle(); + } + + private PbsAst.Expression parseHandle() { + if (!cursor.match(PbsTokenKind.HANDLE)) { + return parseElse(); + } + + final var handleToken = cursor.previous(); + final var value = parseElse(); + consume(PbsTokenKind.LEFT_BRACE, "Expected '{' after handle expression"); + + final var arms = new ArrayList(); + while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) { + final var pattern = parseHandlePattern(); + consume(PbsTokenKind.ARROW, "Expected '->' in handle arm"); + + PbsAst.ErrorPath remapTarget = null; + PbsAst.Block block = null; + long armEnd; + if (cursor.check(PbsTokenKind.LEFT_BRACE)) { + block = parseSurfaceBlock("Expected handle arm block"); + armEnd = block.span().getEnd(); + } else { + remapTarget = parseErrorPath(); + armEnd = remapTarget.span().getEnd(); + } + + arms.add(new PbsAst.HandleArm( + pattern, + remapTarget, + block, + span(pattern.span().getStart(), armEnd))); + + if (!cursor.match(PbsTokenKind.COMMA)) { + break; + } + } + + final var close = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to close handle map"); + return new PbsAst.HandleExpr(value, ReadOnlyList.wrap(arms), span(handleToken.start(), close.end())); } /** - * Parses left-associative logical-or expressions such as {@code a || b || c}. + * Parses right-associative else extraction: {@code a else (b else c)}. */ + private PbsAst.Expression parseElse() { + var expression = parseIfExpression(); + if (cursor.match(PbsTokenKind.ELSE)) { + final var fallback = parseElse(); + expression = new PbsAst.ElseExpr( + expression, + fallback, + span(expression.span().getStart(), fallback.span().getEnd())); + } + return expression; + } + + private PbsAst.Expression parseIfExpression() { + if (!cursor.match(PbsTokenKind.IF)) { + return parseSwitchExpression(); + } + return parseIfExpressionFromToken(cursor.previous()); + } + + private PbsAst.IfExpr parseIfExpressionFromToken(final PbsToken ifToken) { + final var condition = parseExpression(); + final var thenBlock = parseSurfaceBlock("Expected '{' after if condition"); + + if (!cursor.match(PbsTokenKind.ELSE)) { + report(cursor.peek(), ParseErrors.E_PARSE_UNEXPECTED_TOKEN, + "If expression requires an else branch"); + final var unit = new PbsAst.UnitExpr(span(thenBlock.span().getEnd(), thenBlock.span().getEnd())); + return new PbsAst.IfExpr(condition, thenBlock, unit, span(ifToken.start(), unit.span().getEnd())); + } + + final PbsAst.Expression elseExpression; + if (cursor.check(PbsTokenKind.IF)) { + elseExpression = parseIfExpressionFromToken(cursor.advance()); + } else { + final var elseBlock = parseSurfaceBlock("Expected '{' after else"); + elseExpression = new PbsAst.BlockExpr(elseBlock, elseBlock.span()); + } + + return new PbsAst.IfExpr( + condition, + thenBlock, + elseExpression, + span(ifToken.start(), elseExpression.span().getEnd())); + } + + private PbsAst.Expression parseSwitchExpression() { + if (!cursor.match(PbsTokenKind.SWITCH)) { + return parseApply(); + } + + final var switchToken = cursor.previous(); + final var selector = parseExpression(); + consume(PbsTokenKind.LEFT_BRACE, "Expected '{' after switch selector"); + + final var arms = new ArrayList(); + while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) { + final var pattern = parseSwitchPattern(); + consume(PbsTokenKind.COLON, "Expected ':' after switch pattern"); + final var block = parseSurfaceBlock("Expected switch arm block"); + arms.add(new PbsAst.SwitchArm(pattern, block, span(pattern.span().getStart(), block.span().getEnd()))); + + if (!cursor.match(PbsTokenKind.COMMA)) { + break; + } + } + + final var close = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to close switch expression"); + return new PbsAst.SwitchExpr(selector, ReadOnlyList.wrap(arms), span(switchToken.start(), close.end())); + } + + /** + * Parses right-associative apply expressions. + */ + private PbsAst.Expression parseApply() { + final var left = parseOr(); + if (!cursor.match(PbsTokenKind.APPLY)) { + return left; + } + + final var right = parseApply(); + return new PbsAst.ApplyExpr(left, right, span(left.span().getStart(), right.span().getEnd())); + } + private PbsAst.Expression parseOr() { var expression = parseAnd(); - while (cursor.match(PbsTokenKind.OR_OR)) { + while (cursor.match(PbsTokenKind.OR_OR, PbsTokenKind.OR)) { final var operator = cursor.previous(); final var right = parseAnd(); expression = new PbsAst.BinaryExpr(operator.lexeme(), expression, right, @@ -51,12 +171,9 @@ final class PbsExprParser { return expression; } - /** - * Parses left-associative logical-and expressions such as {@code a && b && c}. - */ private PbsAst.Expression parseAnd() { var expression = parseEquality(); - while (cursor.match(PbsTokenKind.AND_AND)) { + while (cursor.match(PbsTokenKind.AND_AND, PbsTokenKind.AND)) { final var operator = cursor.previous(); final var right = parseEquality(); expression = new PbsAst.BinaryExpr(operator.lexeme(), expression, right, @@ -65,12 +182,6 @@ final class PbsExprParser { return expression; } - /** - * Parses equality expressions and rejects chained non-associative forms. - * - *

Accepted: {@code a == b} - *

Rejected: {@code a == b == c} - */ private PbsAst.Expression parseEquality() { var expression = parseComparison(); if (cursor.match(PbsTokenKind.EQUAL_EQUAL, PbsTokenKind.BANG_EQUAL)) { @@ -88,33 +199,33 @@ final class PbsExprParser { return expression; } - /** - * Parses comparison expressions and rejects chained non-associative forms. - * - *

Accepted: {@code a < b} - *

Rejected: {@code a < b < c} - */ private PbsAst.Expression parseComparison() { - var expression = parseTerm(); + var expression = parseAs(); if (cursor.match(PbsTokenKind.LESS, PbsTokenKind.LESS_EQUAL, PbsTokenKind.GREATER, PbsTokenKind.GREATER_EQUAL)) { final var operator = cursor.previous(); - final var right = parseTerm(); + final var right = parseAs(); expression = new PbsAst.BinaryExpr(operator.lexeme(), expression, right, span(expression.span().getStart(), right.span().getEnd())); if (cursor.check(PbsTokenKind.LESS) || cursor.check(PbsTokenKind.LESS_EQUAL) || cursor.check(PbsTokenKind.GREATER) || cursor.check(PbsTokenKind.GREATER_EQUAL)) { report(cursor.peek(), ParseErrors.E_PARSE_NON_ASSOC, "Chained comparison is not allowed"); while (cursor.match(PbsTokenKind.LESS, PbsTokenKind.LESS_EQUAL, PbsTokenKind.GREATER, PbsTokenKind.GREATER_EQUAL)) { - parseTerm(); + parseAs(); } } } return expression; } - /** - * Parses additive expressions such as {@code a + b - c}. - */ + private PbsAst.Expression parseAs() { + var expression = parseTerm(); + if (cursor.match(PbsTokenKind.AS)) { + final var contract = consume(PbsTokenKind.IDENTIFIER, "Expected contract identifier after 'as'"); + expression = new PbsAst.AsExpr(expression, contract.lexeme(), span(expression.span().getStart(), contract.end())); + } + return expression; + } + private PbsAst.Expression parseTerm() { var expression = parseFactor(); while (cursor.match(PbsTokenKind.PLUS, PbsTokenKind.MINUS)) { @@ -126,9 +237,6 @@ final class PbsExprParser { return expression; } - /** - * Parses multiplicative expressions such as {@code a * b / c % d}. - */ private PbsAst.Expression parseFactor() { var expression = parseUnary(); while (cursor.match(PbsTokenKind.STAR, PbsTokenKind.SLASH, PbsTokenKind.PERCENT)) { @@ -140,60 +248,58 @@ final class PbsExprParser { return expression; } - /** - * Parses unary prefix operators such as {@code -x} and {@code !ready}. - */ private PbsAst.Expression parseUnary() { - if (cursor.match(PbsTokenKind.BANG, PbsTokenKind.MINUS)) { + if (cursor.match(PbsTokenKind.BANG, PbsTokenKind.MINUS, PbsTokenKind.NOT)) { final var operator = cursor.previous(); final var right = parseUnary(); - return new PbsAst.UnaryExpr( - operator.lexeme(), - right, - span(operator.start(), right.span().getEnd())); + return new PbsAst.UnaryExpr(operator.lexeme(), right, span(operator.start(), right.span().getEnd())); } - return parseCall(); + return parsePostfix(); } - /** - * Parses call chains after a primary expression. - * - *

Examples: - *

{@code
-     * f()
-     * sum(a, b)
-     * factory()(1)
-     * }
- */ - private PbsAst.Expression parseCall() { + private PbsAst.Expression parsePostfix() { var expression = parsePrimary(); - while (cursor.match(PbsTokenKind.LEFT_PAREN)) { - final var open = cursor.previous(); - final var arguments = new ArrayList(); - if (!cursor.check(PbsTokenKind.RIGHT_PAREN)) { - do { - arguments.add(parseExpression()); - } while (cursor.match(PbsTokenKind.COMMA)); + while (true) { + if (cursor.match(PbsTokenKind.LEFT_PAREN)) { + final var arguments = new ArrayList(); + if (!cursor.check(PbsTokenKind.RIGHT_PAREN)) { + do { + arguments.add(parseExpression()); + } while (cursor.match(PbsTokenKind.COMMA)); + } + final var close = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after call arguments"); + expression = new PbsAst.CallExpr(expression, ReadOnlyList.wrap(arguments), + span(expression.span().getStart(), close.end())); + continue; } - final var close = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after arguments"); - expression = new PbsAst.CallExpr( - expression, - ReadOnlyList.wrap(arguments), - span(expression.span().getStart(), close.end())); - // Avoid endless loops on malformed "f((" forms. - if (open.start() == close.start()) { - break; + if (cursor.match(PbsTokenKind.DOT)) { + final var member = consume(PbsTokenKind.IDENTIFIER, "Expected member name after '.'"); + expression = new PbsAst.MemberExpr( + expression, + member.lexeme(), + span(expression.span().getStart(), member.end())); + continue; } + + if (cursor.match(PbsTokenKind.BANG)) { + final var bang = cursor.previous(); + expression = new PbsAst.PropagateExpr(expression, span(expression.span().getStart(), bang.end())); + continue; + } + + if (cursor.match(PbsTokenKind.QUESTION)) { + final var question = cursor.previous(); + report(question, ParseErrors.E_PARSE_INVALID_PROPAGATE_OPERATOR, + "Use '!' as propagation operator; '?' is not valid PBS syntax"); + continue; + } + + return expression; } - - return expression; } - /** - * Parses primary expressions: literals, identifiers, and grouped expressions. - */ private PbsAst.Expression parsePrimary() { if (cursor.match(PbsTokenKind.TRUE)) { final var token = cursor.previous(); @@ -220,15 +326,39 @@ final class PbsExprParser { final var token = cursor.previous(); return new PbsAst.StringLiteralExpr(unescapeString(token.lexeme()), span(token.start(), token.end())); } + if (cursor.match(PbsTokenKind.THIS)) { + final var token = cursor.previous(); + return new PbsAst.ThisExpr(span(token.start(), token.end())); + } + if (cursor.match(PbsTokenKind.NEW)) { + return parseNewExpression(cursor.previous()); + } + if (cursor.match(PbsTokenKind.BIND)) { + return parseBindExpression(cursor.previous()); + } + if (cursor.match(PbsTokenKind.SOME)) { + return parseSomeExpression(cursor.previous()); + } + if (cursor.match(PbsTokenKind.NONE)) { + final var token = cursor.previous(); + return new PbsAst.NoneExpr(span(token.start(), token.end())); + } + if (cursor.match(PbsTokenKind.OK)) { + return parseOkExpression(cursor.previous()); + } + if (cursor.match(PbsTokenKind.ERR)) { + return parseErrExpression(cursor.previous()); + } if (cursor.match(PbsTokenKind.IDENTIFIER)) { final var token = cursor.previous(); return new PbsAst.IdentifierExpr(token.lexeme(), span(token.start(), token.end())); } if (cursor.match(PbsTokenKind.LEFT_PAREN)) { - final var open = cursor.previous(); - final var expression = parseExpression(); - final var close = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after grouped expression"); - return new PbsAst.GroupExpr(expression, span(open.start(), close.end())); + return parseParenthesizedPrimary(cursor.previous()); + } + if (cursor.check(PbsTokenKind.LEFT_BRACE)) { + final var block = parseSurfaceBlock("Expected block expression"); + return new PbsAst.BlockExpr(block, block.span()); } final var token = cursor.peek(); @@ -237,11 +367,253 @@ final class PbsExprParser { return new PbsAst.IntLiteralExpr(0L, span(token.start(), token.end())); } - /** - * Consumes a required token and reports an error if it is missing. - * - *

The parser advances on failure when possible so recovery can continue. - */ + private PbsAst.Expression parseParenthesizedPrimary(final PbsToken open) { + if (cursor.match(PbsTokenKind.RIGHT_PAREN)) { + final var close = cursor.previous(); + return new PbsAst.UnitExpr(span(open.start(), close.end())); + } + + if (cursor.check(PbsTokenKind.IDENTIFIER) && cursor.checkNext(PbsTokenKind.COLON)) { + final var items = parseTupleItems(true); + final var close = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after tuple literal"); + if (items.size() < 2) { + report(close, ParseErrors.E_PARSE_INVALID_TUPLE_LITERAL, + "Single-slot tuple literal is not allowed in PBS core syntax"); + } + return new PbsAst.TupleExpr(ReadOnlyList.wrap(items), span(open.start(), close.end())); + } + + final var first = parseExpression(); + if (!cursor.match(PbsTokenKind.COMMA)) { + final var close = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after grouped expression"); + return new PbsAst.GroupExpr(first, span(open.start(), close.end())); + } + + final var items = new ArrayList(); + items.add(new PbsAst.TupleItem(null, first, first.span())); + var hasLabels = false; + while (!cursor.check(PbsTokenKind.RIGHT_PAREN) && !cursor.isAtEnd()) { + final var item = parseTupleItem(); + if (item.label() != null) { + hasLabels = true; + } + items.add(item); + if (!cursor.match(PbsTokenKind.COMMA)) { + break; + } + } + + if (items.size() < 2) { + report(cursor.peek(), ParseErrors.E_PARSE_INVALID_TUPLE_LITERAL, + "Tuple literals require at least two items"); + } + + final var close = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after tuple literal"); + if (hasLabels) { + for (final var item : items) { + if (item.label() == null) { + report(close, ParseErrors.E_PARSE_INVALID_TUPLE_LITERAL, + "Mixed labeled/unlabeled tuple items are not allowed"); + break; + } + } + } + + return new PbsAst.TupleExpr(ReadOnlyList.wrap(items), span(open.start(), close.end())); + } + + private ArrayList parseTupleItems(final boolean labeledOnly) { + final var items = new ArrayList(); + do { + final var item = parseTupleItem(); + items.add(item); + if (labeledOnly && item.label() == null) { + report(cursor.previous(), ParseErrors.E_PARSE_INVALID_TUPLE_LITERAL, + "Named tuple literal items must use labels"); + } + } while (cursor.match(PbsTokenKind.COMMA) && !cursor.check(PbsTokenKind.RIGHT_PAREN)); + return items; + } + + private PbsAst.TupleItem parseTupleItem() { + if (cursor.check(PbsTokenKind.IDENTIFIER) && cursor.checkNext(PbsTokenKind.COLON)) { + final var label = cursor.advance(); + consume(PbsTokenKind.COLON, "Expected ':' after tuple label"); + final var value = parseExpression(); + return new PbsAst.TupleItem(label.lexeme(), value, span(label.start(), value.span().getEnd())); + } + + final var value = parseExpression(); + return new PbsAst.TupleItem(null, value, value.span()); + } + + private PbsAst.Expression parseNewExpression(final PbsToken newToken) { + final var typeName = consume(PbsTokenKind.IDENTIFIER, "Expected type name after 'new'"); + String ctorName = null; + if (cursor.match(PbsTokenKind.DOT)) { + ctorName = consume(PbsTokenKind.IDENTIFIER, "Expected constructor name after '.' in new target").lexeme(); + } + + consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after new target"); + final var arguments = new ArrayList(); + if (!cursor.check(PbsTokenKind.RIGHT_PAREN)) { + do { + arguments.add(parseExpression()); + } while (cursor.match(PbsTokenKind.COMMA)); + } + final var close = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after new arguments"); + return new PbsAst.NewExpr( + typeName.lexeme(), + ctorName, + ReadOnlyList.wrap(arguments), + span(newToken.start(), close.end())); + } + + private PbsAst.Expression parseBindExpression(final PbsToken bindToken) { + consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after 'bind'"); + final var contextExpression = parseExpression(); + consume(PbsTokenKind.COMMA, "Expected ',' in bind(context, fn_name)"); + final var functionName = consume(PbsTokenKind.IDENTIFIER, "Expected function identifier in bind(context, fn_name)"); + final var close = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after bind arguments"); + return new PbsAst.BindExpr( + contextExpression, + functionName.lexeme(), + span(bindToken.start(), close.end())); + } + + private PbsAst.Expression parseSomeExpression(final PbsToken someToken) { + consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after 'some'"); + final var value = parseExpression(); + final var close = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after some(payload)"); + return new PbsAst.SomeExpr(value, span(someToken.start(), close.end())); + } + + private PbsAst.Expression parseOkExpression(final PbsToken okToken) { + consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after 'ok'"); + final var value = parseExpression(); + final var close = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after ok(payload)"); + return new PbsAst.OkExpr(value, span(okToken.start(), close.end())); + } + + private PbsAst.Expression parseErrExpression(final PbsToken errToken) { + consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after 'err'"); + final var errorPath = parseErrorPath(); + final var close = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after err(Error.case)"); + return new PbsAst.ErrExpr(errorPath, span(errToken.start(), close.end())); + } + + private PbsAst.SwitchPattern parseSwitchPattern() { + if (cursor.match(PbsTokenKind.DEFAULT)) { + return new PbsAst.WildcardSwitchPattern(span(cursor.previous().start(), cursor.previous().end())); + } + + if (cursor.check(PbsTokenKind.IDENTIFIER) && "_".equals(cursor.peek().lexeme())) { + final var wildcard = cursor.advance(); + return new PbsAst.WildcardSwitchPattern(span(wildcard.start(), wildcard.end())); + } + + if (cursor.check(PbsTokenKind.IDENTIFIER) + && cursor.peek(1).kind() == PbsTokenKind.DOT + && cursor.peek(2).kind() == PbsTokenKind.IDENTIFIER) { + final var path = parseErrorPath(); + return new PbsAst.EnumCaseSwitchPattern(path, path.span()); + } + + final var literal = parseLiteralPatternExpression(); + if (literal != null) { + return new PbsAst.LiteralSwitchPattern(literal, literal.span()); + } + + final var token = cursor.peek(); + report(token, ParseErrors.E_PARSE_INVALID_SWITCH_FORM, "Invalid switch pattern"); + cursor.advance(); + return new PbsAst.WildcardSwitchPattern(span(token.start(), token.end())); + } + + private PbsAst.Expression parseLiteralPatternExpression() { + if (cursor.match(PbsTokenKind.TRUE)) { + final var token = cursor.previous(); + return new PbsAst.BoolLiteralExpr(true, span(token.start(), token.end())); + } + if (cursor.match(PbsTokenKind.FALSE)) { + final var token = cursor.previous(); + return new PbsAst.BoolLiteralExpr(false, span(token.start(), token.end())); + } + if (cursor.match(PbsTokenKind.INT_LITERAL)) { + final var token = cursor.previous(); + return new PbsAst.IntLiteralExpr(parseLongOrDefault(token.lexeme()), span(token.start(), token.end())); + } + if (cursor.match(PbsTokenKind.FLOAT_LITERAL)) { + final var token = cursor.previous(); + return new PbsAst.FloatLiteralExpr(parseDoubleOrDefault(token.lexeme()), span(token.start(), token.end())); + } + if (cursor.match(PbsTokenKind.BOUNDED_LITERAL)) { + final var token = cursor.previous(); + final var raw = token.lexeme().substring(0, Math.max(token.lexeme().length() - 1, 0)); + return new PbsAst.BoundedLiteralExpr(parseIntOrDefault(raw), span(token.start(), token.end())); + } + if (cursor.match(PbsTokenKind.STRING_LITERAL)) { + final var token = cursor.previous(); + return new PbsAst.StringLiteralExpr(unescapeString(token.lexeme()), span(token.start(), token.end())); + } + return null; + } + + private PbsAst.HandlePattern parseHandlePattern() { + if (cursor.check(PbsTokenKind.IDENTIFIER) && "_".equals(cursor.peek().lexeme())) { + final var wildcard = cursor.advance(); + return new PbsAst.WildcardHandlePattern(span(wildcard.start(), wildcard.end())); + } + + if (cursor.check(PbsTokenKind.IDENTIFIER)) { + final var path = parseErrorPath(); + return new PbsAst.ErrorPathHandlePattern(path, path.span()); + } + + final var token = cursor.peek(); + report(token, ParseErrors.E_PARSE_INVALID_HANDLE_FORM, "Invalid handle pattern"); + if (!cursor.isAtEnd()) { + cursor.advance(); + } + return new PbsAst.WildcardHandlePattern(span(token.start(), token.end())); + } + + private PbsAst.ErrorPath parseErrorPath() { + final var first = consume(PbsTokenKind.IDENTIFIER, "Expected identifier in error path"); + final var segments = new ArrayList(); + segments.add(first.lexeme()); + var end = first.end(); + while (cursor.match(PbsTokenKind.DOT)) { + final var segment = consume(PbsTokenKind.IDENTIFIER, "Expected identifier after '.' in error path"); + segments.add(segment.lexeme()); + end = segment.end(); + } + return new PbsAst.ErrorPath(ReadOnlyList.wrap(segments), span(first.start(), end)); + } + + private PbsAst.Block parseSurfaceBlock(final String message) { + final var open = consume(PbsTokenKind.LEFT_BRACE, message); + return parseSurfaceBlockFromOpen(open); + } + + private PbsAst.Block parseSurfaceBlockFromOpen(final PbsToken open) { + var depth = 1; + var end = open.end(); + while (!cursor.isAtEnd() && depth > 0) { + final var token = cursor.advance(); + end = token.end(); + if (token.kind() == PbsTokenKind.LEFT_BRACE) { + depth++; + } else if (token.kind() == PbsTokenKind.RIGHT_BRACE) { + depth--; + } + } + if (depth > 0) { + report(cursor.peek(), ParseErrors.E_PARSE_EXPECTED_TOKEN, "Expected '}' to close block expression"); + } + return new PbsAst.Block(ReadOnlyList.empty(), span(open.start(), end)); + } + private PbsToken consume(final PbsTokenKind kind, final String message) { if (cursor.check(kind)) { return cursor.advance(); @@ -254,23 +626,14 @@ final class PbsExprParser { return token; } - /** - * Builds a source span for the current file. - */ private Span span(final long start, final long end) { return new Span(fileId, start, end); } - /** - * Reports an expression parser diagnostic at the given token span. - */ private void report(final PbsToken token, final ParseErrors parseErrors, final String message) { diagnostics.error(parseErrors.name(), message, new Span(fileId, token.start(), token.end())); } - /** - * Parses an integer literal for AST construction and falls back to zero on malformed input. - */ private long parseLongOrDefault(final String text) { try { return Long.parseLong(text); @@ -279,9 +642,6 @@ final class PbsExprParser { } } - /** - * Parses a bounded literal payload and falls back to zero on malformed input. - */ private int parseIntOrDefault(final String text) { try { return Integer.parseInt(text); @@ -290,9 +650,6 @@ final class PbsExprParser { } } - /** - * Parses a floating-point literal for AST construction and falls back to zero on malformed input. - */ private double parseDoubleOrDefault(final String text) { try { return Double.parseDouble(text); @@ -301,9 +658,6 @@ final class PbsExprParser { } } - /** - * Converts a quoted token lexeme such as {@code "\"hello\\n\""} into its unescaped runtime text. - */ private String unescapeString(final String lexeme) { if (lexeme.length() < 2) { return ""; diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsExprParserTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsExprParserTest.java new file mode 100644 index 00000000..2d9380d4 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsExprParserTest.java @@ -0,0 +1,162 @@ +package p.studio.compiler.pbs.parser; + +import org.junit.jupiter.api.Test; +import p.studio.compiler.pbs.ast.PbsAst; +import p.studio.compiler.pbs.lexer.PbsLexer; +import p.studio.compiler.source.diagnostics.DiagnosticSink; +import p.studio.compiler.source.identifiers.FileId; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PbsExprParserTest { + + @Test + void shouldParseApplyElseAndPostfixChains() { + final var source = """ + fn exprs(opt: int, fallback: int, x: int) -> int { + f apply g apply x; + opt else fallback else x; + obj.member().next!; + return x; + } + """; + + final var diagnostics = DiagnosticSink.empty(); + final var fileId = new FileId(0); + final var ast = PbsParser.parse(PbsLexer.lex(source, fileId, diagnostics), fileId, diagnostics); + + assertTrue(diagnostics.isEmpty(), "Valid expression forms should parse cleanly"); + + final var body = ast.functions().getFirst().body().statements(); + final var applyExpr = assertInstanceOf(PbsAst.ApplyExpr.class, + assertInstanceOf(PbsAst.ExpressionStatement.class, body.get(0)).expression()); + assertInstanceOf(PbsAst.ApplyExpr.class, applyExpr.argument()); + + final var elseExpr = assertInstanceOf(PbsAst.ElseExpr.class, + assertInstanceOf(PbsAst.ExpressionStatement.class, body.get(1)).expression()); + assertInstanceOf(PbsAst.ElseExpr.class, elseExpr.fallbackExpression()); + + assertInstanceOf(PbsAst.PropagateExpr.class, + assertInstanceOf(PbsAst.ExpressionStatement.class, body.get(2)).expression()); + } + + @Test + void shouldParseIfSwitchAndHandleExpressionsWithTailBlocks() { + final var source = """ + fn flow(cond: bool, state: int) -> int { + let a: int = if cond { 1 } else { 2 }; + let b: int = switch state { default: { 0 } }; + let c: int = handle run apply () { Err.fail -> Other.fail, _ -> { 1 } }; + return 0; + } + """; + + final var diagnostics = DiagnosticSink.empty(); + final var fileId = new FileId(0); + final var ast = PbsParser.parse(PbsLexer.lex(source, fileId, diagnostics), fileId, diagnostics); + + assertTrue(diagnostics.isEmpty(), "Expression control-flow forms should parse cleanly"); + + final var body = ast.functions().getFirst().body().statements(); + assertInstanceOf(PbsAst.IfExpr.class, + assertInstanceOf(PbsAst.LetStatement.class, body.get(0)).initializer()); + assertInstanceOf(PbsAst.SwitchExpr.class, + assertInstanceOf(PbsAst.LetStatement.class, body.get(1)).initializer()); + + final var handle = assertInstanceOf(PbsAst.HandleExpr.class, + assertInstanceOf(PbsAst.LetStatement.class, body.get(2)).initializer()); + assertEquals(2, handle.arms().size()); + } + + @Test + void shouldParseIfSwitchAndHandleExpressionsWithReturnBlocks() { + final var source = """ + fn flow(cond: bool, state: int) -> int { + let a: int = if cond { return 1; } else { return 2; }; + let b: int = switch state { default: { return 0; } }; + let c: int = handle run apply () { Err.fail -> Other.fail, _ -> { return 1; } }; + return 0; + } + """; + + final var diagnostics = DiagnosticSink.empty(); + final var fileId = new FileId(0); + final var ast = PbsParser.parse(PbsLexer.lex(source, fileId, diagnostics), fileId, diagnostics); + + assertTrue(diagnostics.isEmpty(), "Expression control-flow forms with return blocks should parse cleanly"); + + final var body = ast.functions().getFirst().body().statements(); + assertInstanceOf(PbsAst.IfExpr.class, + assertInstanceOf(PbsAst.LetStatement.class, body.get(0)).initializer()); + assertInstanceOf(PbsAst.SwitchExpr.class, + assertInstanceOf(PbsAst.LetStatement.class, body.get(1)).initializer()); + + final var handle = assertInstanceOf(PbsAst.HandleExpr.class, + assertInstanceOf(PbsAst.LetStatement.class, body.get(2)).initializer()); + assertEquals(2, handle.arms().size()); + } + + @Test + void shouldParseConstructionAndResultSurfaces() { + final var source = """ + fn build(ctx: int, v: int) -> int { + some(v); + none; + ok(v); + err(Game.Fail); + new Vec(v); + new Vec.zero(v); + bind(ctx, handler); + return v; + } + """; + + final var diagnostics = DiagnosticSink.empty(); + final var fileId = new FileId(0); + final var ast = PbsParser.parse(PbsLexer.lex(source, fileId, diagnostics), fileId, diagnostics); + + assertTrue(diagnostics.isEmpty(), "Constructor and optional/result expression surfaces should parse cleanly"); + + final var body = ast.functions().getFirst().body().statements(); + assertInstanceOf(PbsAst.SomeExpr.class, + assertInstanceOf(PbsAst.ExpressionStatement.class, body.get(0)).expression()); + assertInstanceOf(PbsAst.NoneExpr.class, + assertInstanceOf(PbsAst.ExpressionStatement.class, body.get(1)).expression()); + assertInstanceOf(PbsAst.OkExpr.class, + assertInstanceOf(PbsAst.ExpressionStatement.class, body.get(2)).expression()); + assertInstanceOf(PbsAst.ErrExpr.class, + assertInstanceOf(PbsAst.ExpressionStatement.class, body.get(3)).expression()); + assertInstanceOf(PbsAst.NewExpr.class, + assertInstanceOf(PbsAst.ExpressionStatement.class, body.get(4)).expression()); + assertInstanceOf(PbsAst.NewExpr.class, + assertInstanceOf(PbsAst.ExpressionStatement.class, body.get(5)).expression()); + assertInstanceOf(PbsAst.BindExpr.class, + assertInstanceOf(PbsAst.ExpressionStatement.class, body.get(6)).expression()); + } + + @Test + void shouldRejectInvalidExpressionFormsDeterministically() { + final var source = """ + fn bad(a: int, b: int, c: int) -> int { + a < b < c; + a == b == c; + value?; + (a: 1); + (a: 1, 2); + if a { 1; }; + return 0; + } + """; + + final var diagnostics = DiagnosticSink.empty(); + final var fileId = new FileId(0); + PbsParser.parse(PbsLexer.lex(source, fileId, diagnostics), fileId, diagnostics); + + assertTrue(diagnostics.hasErrors(), "Invalid expression forms should produce diagnostics"); + assertTrue(diagnostics.stream().anyMatch(d -> d.getCode().equals(ParseErrors.E_PARSE_NON_ASSOC.name()))); + assertTrue(diagnostics.stream().anyMatch(d -> d.getCode().equals(ParseErrors.E_PARSE_INVALID_PROPAGATE_OPERATOR.name()))); + assertTrue(diagnostics.stream().anyMatch(d -> d.getCode().equals(ParseErrors.E_PARSE_INVALID_TUPLE_LITERAL.name()))); + } +}