implements PR005
This commit is contained in:
parent
41f90930e5
commit
931c5d96a3
@ -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.
|
|
||||||
@ -310,8 +310,26 @@ public final class PbsAst {
|
|||||||
BoolLiteralExpr,
|
BoolLiteralExpr,
|
||||||
UnaryExpr,
|
UnaryExpr,
|
||||||
BinaryExpr,
|
BinaryExpr,
|
||||||
|
ApplyExpr,
|
||||||
|
ElseExpr,
|
||||||
|
IfExpr,
|
||||||
|
SwitchExpr,
|
||||||
|
HandleExpr,
|
||||||
|
AsExpr,
|
||||||
CallExpr,
|
CallExpr,
|
||||||
GroupExpr {
|
MemberExpr,
|
||||||
|
PropagateExpr,
|
||||||
|
GroupExpr,
|
||||||
|
ThisExpr,
|
||||||
|
NewExpr,
|
||||||
|
BindExpr,
|
||||||
|
SomeExpr,
|
||||||
|
NoneExpr,
|
||||||
|
OkExpr,
|
||||||
|
ErrExpr,
|
||||||
|
UnitExpr,
|
||||||
|
TupleExpr,
|
||||||
|
BlockExpr {
|
||||||
Span span();
|
Span span();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -358,17 +376,172 @@ public final class PbsAst {
|
|||||||
Span span) implements Expression {
|
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(
|
public record CallExpr(
|
||||||
Expression callee,
|
Expression callee,
|
||||||
ReadOnlyList<Expression> arguments,
|
ReadOnlyList<Expression> arguments,
|
||||||
Span span) implements Expression {
|
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(
|
public record GroupExpr(
|
||||||
Expression expression,
|
Expression expression,
|
||||||
Span span) implements 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.
|
// Barrel declarations are represented explicitly for linking-visible declaration families.
|
||||||
public sealed interface BarrelItem permits BarrelFunctionItem,
|
public sealed interface BarrelItem permits BarrelFunctionItem,
|
||||||
BarrelStructItem,
|
BarrelStructItem,
|
||||||
|
|||||||
@ -12,4 +12,8 @@ public enum ParseErrors {
|
|||||||
E_PARSE_INVALID_ASSIGN_TARGET,
|
E_PARSE_INVALID_ASSIGN_TARGET,
|
||||||
E_PARSE_INVALID_FOR_FORM,
|
E_PARSE_INVALID_FOR_FORM,
|
||||||
E_PARSE_LOOP_CONTROL_OUTSIDE_LOOP,
|
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,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,9 +12,6 @@ import java.util.ArrayList;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Dedicated expression parser for PBS.
|
* 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 {
|
final class PbsExprParser {
|
||||||
private final PbsTokenCursor cursor;
|
private final PbsTokenCursor cursor;
|
||||||
@ -34,15 +31,138 @@ final class PbsExprParser {
|
|||||||
* Entry point for expression parsing.
|
* Entry point for expression parsing.
|
||||||
*/
|
*/
|
||||||
PbsAst.Expression parseExpression() {
|
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() {
|
private PbsAst.Expression parseOr() {
|
||||||
var expression = parseAnd();
|
var expression = parseAnd();
|
||||||
while (cursor.match(PbsTokenKind.OR_OR)) {
|
while (cursor.match(PbsTokenKind.OR_OR, PbsTokenKind.OR)) {
|
||||||
final var operator = cursor.previous();
|
final var operator = cursor.previous();
|
||||||
final var right = parseAnd();
|
final var right = parseAnd();
|
||||||
expression = new PbsAst.BinaryExpr(operator.lexeme(), expression, right,
|
expression = new PbsAst.BinaryExpr(operator.lexeme(), expression, right,
|
||||||
@ -51,12 +171,9 @@ final class PbsExprParser {
|
|||||||
return expression;
|
return expression;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses left-associative logical-and expressions such as {@code a && b && c}.
|
|
||||||
*/
|
|
||||||
private PbsAst.Expression parseAnd() {
|
private PbsAst.Expression parseAnd() {
|
||||||
var expression = parseEquality();
|
var expression = parseEquality();
|
||||||
while (cursor.match(PbsTokenKind.AND_AND)) {
|
while (cursor.match(PbsTokenKind.AND_AND, PbsTokenKind.AND)) {
|
||||||
final var operator = cursor.previous();
|
final var operator = cursor.previous();
|
||||||
final var right = parseEquality();
|
final var right = parseEquality();
|
||||||
expression = new PbsAst.BinaryExpr(operator.lexeme(), expression, right,
|
expression = new PbsAst.BinaryExpr(operator.lexeme(), expression, right,
|
||||||
@ -65,12 +182,6 @@ final class PbsExprParser {
|
|||||||
return expression;
|
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() {
|
private PbsAst.Expression parseEquality() {
|
||||||
var expression = parseComparison();
|
var expression = parseComparison();
|
||||||
if (cursor.match(PbsTokenKind.EQUAL_EQUAL, PbsTokenKind.BANG_EQUAL)) {
|
if (cursor.match(PbsTokenKind.EQUAL_EQUAL, PbsTokenKind.BANG_EQUAL)) {
|
||||||
@ -88,33 +199,33 @@ final class PbsExprParser {
|
|||||||
return expression;
|
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() {
|
private PbsAst.Expression parseComparison() {
|
||||||
var expression = parseTerm();
|
var expression = parseAs();
|
||||||
if (cursor.match(PbsTokenKind.LESS, PbsTokenKind.LESS_EQUAL, PbsTokenKind.GREATER, PbsTokenKind.GREATER_EQUAL)) {
|
if (cursor.match(PbsTokenKind.LESS, PbsTokenKind.LESS_EQUAL, PbsTokenKind.GREATER, PbsTokenKind.GREATER_EQUAL)) {
|
||||||
final var operator = cursor.previous();
|
final var operator = cursor.previous();
|
||||||
final var right = parseTerm();
|
final var right = parseAs();
|
||||||
expression = new PbsAst.BinaryExpr(operator.lexeme(), expression, right,
|
expression = new PbsAst.BinaryExpr(operator.lexeme(), expression, right,
|
||||||
span(expression.span().getStart(), right.span().getEnd()));
|
span(expression.span().getStart(), right.span().getEnd()));
|
||||||
if (cursor.check(PbsTokenKind.LESS) || cursor.check(PbsTokenKind.LESS_EQUAL)
|
if (cursor.check(PbsTokenKind.LESS) || cursor.check(PbsTokenKind.LESS_EQUAL)
|
||||||
|| cursor.check(PbsTokenKind.GREATER) || cursor.check(PbsTokenKind.GREATER_EQUAL)) {
|
|| cursor.check(PbsTokenKind.GREATER) || cursor.check(PbsTokenKind.GREATER_EQUAL)) {
|
||||||
report(cursor.peek(), ParseErrors.E_PARSE_NON_ASSOC, "Chained comparison is not allowed");
|
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)) {
|
while (cursor.match(PbsTokenKind.LESS, PbsTokenKind.LESS_EQUAL, PbsTokenKind.GREATER, PbsTokenKind.GREATER_EQUAL)) {
|
||||||
parseTerm();
|
parseAs();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return expression;
|
return expression;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private PbsAst.Expression parseAs() {
|
||||||
* Parses additive expressions such as {@code a + b - c}.
|
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() {
|
private PbsAst.Expression parseTerm() {
|
||||||
var expression = parseFactor();
|
var expression = parseFactor();
|
||||||
while (cursor.match(PbsTokenKind.PLUS, PbsTokenKind.MINUS)) {
|
while (cursor.match(PbsTokenKind.PLUS, PbsTokenKind.MINUS)) {
|
||||||
@ -126,9 +237,6 @@ final class PbsExprParser {
|
|||||||
return expression;
|
return expression;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses multiplicative expressions such as {@code a * b / c % d}.
|
|
||||||
*/
|
|
||||||
private PbsAst.Expression parseFactor() {
|
private PbsAst.Expression parseFactor() {
|
||||||
var expression = parseUnary();
|
var expression = parseUnary();
|
||||||
while (cursor.match(PbsTokenKind.STAR, PbsTokenKind.SLASH, PbsTokenKind.PERCENT)) {
|
while (cursor.match(PbsTokenKind.STAR, PbsTokenKind.SLASH, PbsTokenKind.PERCENT)) {
|
||||||
@ -140,60 +248,58 @@ final class PbsExprParser {
|
|||||||
return expression;
|
return expression;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses unary prefix operators such as {@code -x} and {@code !ready}.
|
|
||||||
*/
|
|
||||||
private PbsAst.Expression parseUnary() {
|
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 operator = cursor.previous();
|
||||||
final var right = parseUnary();
|
final var right = parseUnary();
|
||||||
return new PbsAst.UnaryExpr(
|
return new PbsAst.UnaryExpr(operator.lexeme(), right, span(operator.start(), right.span().getEnd()));
|
||||||
operator.lexeme(),
|
|
||||||
right,
|
|
||||||
span(operator.start(), right.span().getEnd()));
|
|
||||||
}
|
}
|
||||||
return parseCall();
|
return parsePostfix();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private PbsAst.Expression parsePostfix() {
|
||||||
* Parses call chains after a primary expression.
|
|
||||||
*
|
|
||||||
* <p>Examples:
|
|
||||||
* <pre>{@code
|
|
||||||
* f()
|
|
||||||
* sum(a, b)
|
|
||||||
* factory()(1)
|
|
||||||
* }</pre>
|
|
||||||
*/
|
|
||||||
private PbsAst.Expression parseCall() {
|
|
||||||
var expression = parsePrimary();
|
var expression = parsePrimary();
|
||||||
|
|
||||||
while (cursor.match(PbsTokenKind.LEFT_PAREN)) {
|
while (true) {
|
||||||
final var open = cursor.previous();
|
if (cursor.match(PbsTokenKind.LEFT_PAREN)) {
|
||||||
final var arguments = new ArrayList<PbsAst.Expression>();
|
final var arguments = new ArrayList<PbsAst.Expression>();
|
||||||
if (!cursor.check(PbsTokenKind.RIGHT_PAREN)) {
|
if (!cursor.check(PbsTokenKind.RIGHT_PAREN)) {
|
||||||
do {
|
do {
|
||||||
arguments.add(parseExpression());
|
arguments.add(parseExpression());
|
||||||
} while (cursor.match(PbsTokenKind.COMMA));
|
} 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 (cursor.match(PbsTokenKind.DOT)) {
|
||||||
if (open.start() == close.start()) {
|
final var member = consume(PbsTokenKind.IDENTIFIER, "Expected member name after '.'");
|
||||||
break;
|
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() {
|
private PbsAst.Expression parsePrimary() {
|
||||||
if (cursor.match(PbsTokenKind.TRUE)) {
|
if (cursor.match(PbsTokenKind.TRUE)) {
|
||||||
final var token = cursor.previous();
|
final var token = cursor.previous();
|
||||||
@ -220,15 +326,39 @@ final class PbsExprParser {
|
|||||||
final var token = cursor.previous();
|
final var token = cursor.previous();
|
||||||
return new PbsAst.StringLiteralExpr(unescapeString(token.lexeme()), span(token.start(), token.end()));
|
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)) {
|
if (cursor.match(PbsTokenKind.IDENTIFIER)) {
|
||||||
final var token = cursor.previous();
|
final var token = cursor.previous();
|
||||||
return new PbsAst.IdentifierExpr(token.lexeme(), span(token.start(), token.end()));
|
return new PbsAst.IdentifierExpr(token.lexeme(), span(token.start(), token.end()));
|
||||||
}
|
}
|
||||||
if (cursor.match(PbsTokenKind.LEFT_PAREN)) {
|
if (cursor.match(PbsTokenKind.LEFT_PAREN)) {
|
||||||
final var open = cursor.previous();
|
return parseParenthesizedPrimary(cursor.previous());
|
||||||
final var expression = parseExpression();
|
}
|
||||||
final var close = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after grouped expression");
|
if (cursor.check(PbsTokenKind.LEFT_BRACE)) {
|
||||||
return new PbsAst.GroupExpr(expression, span(open.start(), close.end()));
|
final var block = parseSurfaceBlock("Expected block expression");
|
||||||
|
return new PbsAst.BlockExpr(block, block.span());
|
||||||
}
|
}
|
||||||
|
|
||||||
final var token = cursor.peek();
|
final var token = cursor.peek();
|
||||||
@ -237,11 +367,253 @@ final class PbsExprParser {
|
|||||||
return new PbsAst.IntLiteralExpr(0L, span(token.start(), token.end()));
|
return new PbsAst.IntLiteralExpr(0L, span(token.start(), token.end()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private PbsAst.Expression parseParenthesizedPrimary(final PbsToken open) {
|
||||||
* Consumes a required token and reports an error if it is missing.
|
if (cursor.match(PbsTokenKind.RIGHT_PAREN)) {
|
||||||
*
|
final var close = cursor.previous();
|
||||||
* <p>The parser advances on failure when possible so recovery can continue.
|
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) {
|
private PbsToken consume(final PbsTokenKind kind, final String message) {
|
||||||
if (cursor.check(kind)) {
|
if (cursor.check(kind)) {
|
||||||
return cursor.advance();
|
return cursor.advance();
|
||||||
@ -254,23 +626,14 @@ final class PbsExprParser {
|
|||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds a source span for the current file.
|
|
||||||
*/
|
|
||||||
private Span span(final long start, final long end) {
|
private Span span(final long start, final long end) {
|
||||||
return new Span(fileId, start, 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) {
|
private void report(final PbsToken token, final ParseErrors parseErrors, final String message) {
|
||||||
diagnostics.error(parseErrors.name(), message, new Span(fileId, token.start(), token.end()));
|
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) {
|
private long parseLongOrDefault(final String text) {
|
||||||
try {
|
try {
|
||||||
return Long.parseLong(text);
|
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) {
|
private int parseIntOrDefault(final String text) {
|
||||||
try {
|
try {
|
||||||
return Integer.parseInt(text);
|
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) {
|
private double parseDoubleOrDefault(final String text) {
|
||||||
try {
|
try {
|
||||||
return Double.parseDouble(text);
|
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) {
|
private String unescapeString(final String lexeme) {
|
||||||
if (lexeme.length() < 2) {
|
if (lexeme.length() < 2) {
|
||||||
return "";
|
return "";
|
||||||
|
|||||||
@ -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())));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user