implements PR005

This commit is contained in:
bQUARKz 2026-03-05 10:55:13 +00:00
parent 41f90930e5
commit 931c5d96a3
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
5 changed files with 792 additions and 135 deletions

View File

@ -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.

View File

@ -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<SwitchArm> 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<HandleArm> arms,
Span span) implements Expression {
}
public record AsExpr(
Expression expression,
String contractName,
Span span) implements Expression {
}
public record CallExpr(
Expression callee,
ReadOnlyList<Expression> 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<Expression> 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<TupleItem> items,
Span span) implements Expression {
}
public record BlockExpr(
Block block,
Span span) implements Expression {
}
public record ErrorPath(
ReadOnlyList<String> segments,
Span span) {
}
// Barrel declarations are represented explicitly for linking-visible declaration families.
public sealed interface BarrelItem permits BarrelFunctionItem,
BarrelStructItem,

View File

@ -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,
}

View File

@ -12,9 +12,6 @@ import java.util.ArrayList;
/**
* Dedicated expression parser for PBS.
*
* <p>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<PbsAst.HandleArm>();
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<PbsAst.SwitchArm>();
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.
*
* <p>Accepted: {@code a == b}
* <p>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.
*
* <p>Accepted: {@code a < b}
* <p>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.
*
* <p>Examples:
* <pre>{@code
* f()
* sum(a, b)
* factory()(1)
* }</pre>
*/
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<PbsAst.Expression>();
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<PbsAst.Expression>();
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.
*
* <p>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<PbsAst.TupleItem>();
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<PbsAst.TupleItem> parseTupleItems(final boolean labeledOnly) {
final var items = new ArrayList<PbsAst.TupleItem>();
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<PbsAst.Expression>();
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<String>();
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 "";

View File

@ -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())));
}
}