implements PR004
This commit is contained in:
parent
96c5505c04
commit
41f90930e5
@ -1,35 +0,0 @@
|
||||
# PR-004 - PBS Parser Statements and Control Flow
|
||||
|
||||
## Briefing
|
||||
O parser atual cobre apenas `let`, `return` e expression statement.
|
||||
Este PR implementa o bloco de statements exigido pela spec para viabilizar semantica de fluxo e validacoes posteriores.
|
||||
|
||||
## Target
|
||||
- Specs:
|
||||
- `docs/pbs/specs/3. Core Syntax Specification.md` (secao 9)
|
||||
- `docs/pbs/specs/11. AST Specification.md` (secao 9)
|
||||
- Codigo:
|
||||
- `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsParser.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. Adicionar parser para `assign`, `if`, `for`, `while`, `break`, `continue`.
|
||||
2. Introduzir `let const` e modelagem de `LValue` (sem call chain em alvo de atribuicao).
|
||||
3. Implementar validacao sintatica de forma de `for` (`from/until/step`).
|
||||
4. Marcar uso invalido de `break/continue` fora de loop como erro deterministico.
|
||||
5. Preservar recovery por fronteira de statement (`;` e fechamento de bloco).
|
||||
|
||||
## Acceptance Criteria
|
||||
- AST de bloco representa todas as formas de `Stmt` da secao 9.
|
||||
- Atribuicao e statement, nao expressao.
|
||||
- `break/continue` fora de loop emite diagnostico estavel.
|
||||
- `for` e `while` parseiam com estrutura e spans corretos.
|
||||
- Targets de atribuicao invalidos sao rejeitados no parser.
|
||||
|
||||
## Tests
|
||||
- `PbsParserStatementsTest` novo cobrindo:
|
||||
- `let`, `let const`, `assign` simples e composta;
|
||||
- `if/else`, `for` com e sem `step`, `while`;
|
||||
- `break/continue` valido e invalido;
|
||||
- recovery apos statements malformados.
|
||||
@ -213,7 +213,24 @@ public final class PbsAst {
|
||||
Span span) {
|
||||
}
|
||||
|
||||
public sealed interface Statement permits LetStatement, ReturnStatement, ExpressionStatement {
|
||||
public enum AssignOperator {
|
||||
ASSIGN,
|
||||
ADD_ASSIGN,
|
||||
SUB_ASSIGN,
|
||||
MUL_ASSIGN,
|
||||
DIV_ASSIGN,
|
||||
MOD_ASSIGN
|
||||
}
|
||||
|
||||
public sealed interface Statement permits LetStatement,
|
||||
AssignStatement,
|
||||
ReturnStatement,
|
||||
IfStatement,
|
||||
ForStatement,
|
||||
WhileStatement,
|
||||
BreakStatement,
|
||||
ContinueStatement,
|
||||
ExpressionStatement {
|
||||
Span span();
|
||||
}
|
||||
|
||||
@ -222,18 +239,64 @@ public final class PbsAst {
|
||||
Span span) {
|
||||
}
|
||||
|
||||
public record LValue(
|
||||
String rootName,
|
||||
ReadOnlyList<String> pathSegments,
|
||||
Span span) {
|
||||
}
|
||||
|
||||
public record LetStatement(
|
||||
boolean isConst,
|
||||
String name,
|
||||
TypeRef explicitType,
|
||||
Expression initializer,
|
||||
Span span) implements Statement {
|
||||
}
|
||||
|
||||
public record AssignStatement(
|
||||
LValue target,
|
||||
AssignOperator operator,
|
||||
Expression value,
|
||||
Span span) implements Statement {
|
||||
}
|
||||
|
||||
public record ReturnStatement(
|
||||
Expression value,
|
||||
Span span) implements Statement {
|
||||
}
|
||||
|
||||
public record IfStatement(
|
||||
Expression condition,
|
||||
Block thenBlock,
|
||||
IfStatement elseIf,
|
||||
Block elseBlock,
|
||||
Span span) implements Statement {
|
||||
}
|
||||
|
||||
public record ForStatement(
|
||||
String iteratorName,
|
||||
TypeRef iteratorType,
|
||||
Expression fromExpression,
|
||||
Expression untilExpression,
|
||||
Expression stepExpression,
|
||||
Block body,
|
||||
Span span) implements Statement {
|
||||
}
|
||||
|
||||
public record WhileStatement(
|
||||
Expression condition,
|
||||
Block body,
|
||||
Span span) implements Statement {
|
||||
}
|
||||
|
||||
public record BreakStatement(
|
||||
Span span) implements Statement {
|
||||
}
|
||||
|
||||
public record ContinueStatement(
|
||||
Span span) implements Statement {
|
||||
}
|
||||
|
||||
public record ExpressionStatement(
|
||||
Expression expression,
|
||||
Span span) implements Statement {
|
||||
|
||||
@ -9,4 +9,7 @@ public enum ParseErrors {
|
||||
E_PARSE_INVALID_RETURN_ANNOTATION,
|
||||
E_PARSE_INVALID_TYPE_SURFACE,
|
||||
E_PARSE_RESERVED_DECLARATION,
|
||||
E_PARSE_INVALID_ASSIGN_TARGET,
|
||||
E_PARSE_INVALID_FOR_FORM,
|
||||
E_PARSE_LOOP_CONTROL_OUTSIDE_LOOP,
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ public final class PbsParser {
|
||||
private final PbsExprParser exprParser;
|
||||
private final FileId fileId;
|
||||
private final DiagnosticSink diagnostics;
|
||||
private int loopDepth;
|
||||
|
||||
private PbsParser(
|
||||
final ReadOnlyList<PbsToken> tokens,
|
||||
@ -639,9 +640,27 @@ public final class PbsParser {
|
||||
if (cursor.match(PbsTokenKind.LET)) {
|
||||
return parseLetStatement(cursor.previous());
|
||||
}
|
||||
if (cursor.match(PbsTokenKind.IF)) {
|
||||
return parseIfStatement(cursor.previous());
|
||||
}
|
||||
if (cursor.match(PbsTokenKind.FOR)) {
|
||||
return parseForStatement(cursor.previous());
|
||||
}
|
||||
if (cursor.match(PbsTokenKind.WHILE)) {
|
||||
return parseWhileStatement(cursor.previous());
|
||||
}
|
||||
if (cursor.match(PbsTokenKind.BREAK)) {
|
||||
return parseBreakStatement(cursor.previous());
|
||||
}
|
||||
if (cursor.match(PbsTokenKind.CONTINUE)) {
|
||||
return parseContinueStatement(cursor.previous());
|
||||
}
|
||||
if (cursor.match(PbsTokenKind.RETURN)) {
|
||||
return parseReturnStatement(cursor.previous());
|
||||
}
|
||||
if (isAssignmentStatementStart()) {
|
||||
return parseAssignStatement();
|
||||
}
|
||||
return parseExpressionStatement();
|
||||
}
|
||||
|
||||
@ -649,7 +668,7 @@ public final class PbsParser {
|
||||
* Parses a local binding statement.
|
||||
*/
|
||||
private PbsAst.Statement parseLetStatement(final PbsToken letToken) {
|
||||
cursor.match(PbsTokenKind.CONST);
|
||||
final boolean isConst = cursor.match(PbsTokenKind.CONST);
|
||||
final var name = consume(PbsTokenKind.IDENTIFIER, "Expected variable name");
|
||||
|
||||
PbsAst.TypeRef explicitType = null;
|
||||
@ -662,12 +681,160 @@ public final class PbsParser {
|
||||
final var semicolon = consume(PbsTokenKind.SEMICOLON, "Expected ';' after let statement");
|
||||
|
||||
return new PbsAst.LetStatement(
|
||||
isConst,
|
||||
name.lexeme(),
|
||||
explicitType,
|
||||
initializer,
|
||||
span(letToken.start(), semicolon.end()));
|
||||
}
|
||||
|
||||
private PbsAst.Statement parseAssignStatement() {
|
||||
final var lValue = parseLValue();
|
||||
final var operatorToken = cursor.peek();
|
||||
final var operator = parseAssignOperator(consumeAssignOperator());
|
||||
final var value = exprParser.parseExpression();
|
||||
final var semicolon = consume(PbsTokenKind.SEMICOLON, "Expected ';' after assignment");
|
||||
return new PbsAst.AssignStatement(
|
||||
lValue,
|
||||
operator,
|
||||
value,
|
||||
span(lValue.span().getStart(), semicolon.end()));
|
||||
}
|
||||
|
||||
private PbsAst.LValue parseLValue() {
|
||||
final var root = consume(PbsTokenKind.IDENTIFIER, "Expected assignment target identifier");
|
||||
final var segments = new ArrayList<String>();
|
||||
var end = root.end();
|
||||
while (cursor.match(PbsTokenKind.DOT)) {
|
||||
final var segment = consume(PbsTokenKind.IDENTIFIER, "Expected member name after '.' in assignment target");
|
||||
segments.add(segment.lexeme());
|
||||
end = segment.end();
|
||||
}
|
||||
return new PbsAst.LValue(root.lexeme(), ReadOnlyList.wrap(segments), span(root.start(), end));
|
||||
}
|
||||
|
||||
private PbsToken consumeAssignOperator() {
|
||||
if (cursor.match(PbsTokenKind.EQUAL,
|
||||
PbsTokenKind.PLUS_EQUAL,
|
||||
PbsTokenKind.MINUS_EQUAL,
|
||||
PbsTokenKind.STAR_EQUAL,
|
||||
PbsTokenKind.SLASH_EQUAL,
|
||||
PbsTokenKind.PERCENT_EQUAL)) {
|
||||
return cursor.previous();
|
||||
}
|
||||
final var token = cursor.peek();
|
||||
report(token, ParseErrors.E_PARSE_INVALID_ASSIGN_TARGET,
|
||||
"Expected assignment operator after assignment target");
|
||||
if (!cursor.isAtEnd()) {
|
||||
return cursor.advance();
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
private PbsAst.AssignOperator parseAssignOperator(final PbsToken token) {
|
||||
return switch (token.kind()) {
|
||||
case EQUAL -> PbsAst.AssignOperator.ASSIGN;
|
||||
case PLUS_EQUAL -> PbsAst.AssignOperator.ADD_ASSIGN;
|
||||
case MINUS_EQUAL -> PbsAst.AssignOperator.SUB_ASSIGN;
|
||||
case STAR_EQUAL -> PbsAst.AssignOperator.MUL_ASSIGN;
|
||||
case SLASH_EQUAL -> PbsAst.AssignOperator.DIV_ASSIGN;
|
||||
case PERCENT_EQUAL -> PbsAst.AssignOperator.MOD_ASSIGN;
|
||||
default -> PbsAst.AssignOperator.ASSIGN;
|
||||
};
|
||||
}
|
||||
|
||||
private PbsAst.IfStatement parseIfStatement(final PbsToken ifToken) {
|
||||
final var condition = exprParser.parseExpression();
|
||||
final var thenBlock = parseBlock();
|
||||
|
||||
PbsAst.IfStatement elseIf = null;
|
||||
PbsAst.Block elseBlock = null;
|
||||
if (cursor.match(PbsTokenKind.ELSE)) {
|
||||
if (cursor.match(PbsTokenKind.IF)) {
|
||||
elseIf = parseIfStatement(cursor.previous());
|
||||
} else {
|
||||
elseBlock = parseBlock();
|
||||
}
|
||||
}
|
||||
|
||||
final var end = elseIf != null
|
||||
? elseIf.span().getEnd()
|
||||
: (elseBlock != null ? elseBlock.span().getEnd() : thenBlock.span().getEnd());
|
||||
|
||||
return new PbsAst.IfStatement(
|
||||
condition,
|
||||
thenBlock,
|
||||
elseIf,
|
||||
elseBlock,
|
||||
span(ifToken.start(), end));
|
||||
}
|
||||
|
||||
private PbsAst.Statement parseForStatement(final PbsToken forToken) {
|
||||
final var iterator = consume(PbsTokenKind.IDENTIFIER, "Expected loop iterator name in 'for'");
|
||||
consumeForToken(PbsTokenKind.COLON, "Expected ':' after iterator name in 'for'");
|
||||
final var iteratorType = parseTypeRef();
|
||||
consumeForToken(PbsTokenKind.FROM, "Expected 'from' in 'for' statement");
|
||||
final var fromExpression = exprParser.parseExpression();
|
||||
consumeForToken(PbsTokenKind.UNTIL, "Expected 'until' in 'for' statement");
|
||||
final var untilExpression = exprParser.parseExpression();
|
||||
|
||||
PbsAst.Expression stepExpression = null;
|
||||
if (cursor.match(PbsTokenKind.STEP)) {
|
||||
stepExpression = exprParser.parseExpression();
|
||||
}
|
||||
|
||||
loopDepth++;
|
||||
final var body = parseBlock();
|
||||
loopDepth--;
|
||||
|
||||
return new PbsAst.ForStatement(
|
||||
iterator.lexeme(),
|
||||
iteratorType,
|
||||
fromExpression,
|
||||
untilExpression,
|
||||
stepExpression,
|
||||
body,
|
||||
span(forToken.start(), body.span().getEnd()));
|
||||
}
|
||||
|
||||
private PbsToken consumeForToken(final PbsTokenKind kind, final String message) {
|
||||
if (cursor.check(kind)) {
|
||||
return cursor.advance();
|
||||
}
|
||||
final var token = cursor.peek();
|
||||
report(token, ParseErrors.E_PARSE_INVALID_FOR_FORM, message + ", found " + token.kind());
|
||||
if (!cursor.isAtEnd()) {
|
||||
return cursor.advance();
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
private PbsAst.Statement parseWhileStatement(final PbsToken whileToken) {
|
||||
final var condition = exprParser.parseExpression();
|
||||
loopDepth++;
|
||||
final var body = parseBlock();
|
||||
loopDepth--;
|
||||
return new PbsAst.WhileStatement(condition, body, span(whileToken.start(), body.span().getEnd()));
|
||||
}
|
||||
|
||||
private PbsAst.Statement parseBreakStatement(final PbsToken breakToken) {
|
||||
final var semicolon = consume(PbsTokenKind.SEMICOLON, "Expected ';' after 'break'");
|
||||
if (loopDepth <= 0) {
|
||||
report(breakToken, ParseErrors.E_PARSE_LOOP_CONTROL_OUTSIDE_LOOP,
|
||||
"'break' is only valid inside loop contexts");
|
||||
}
|
||||
return new PbsAst.BreakStatement(span(breakToken.start(), semicolon.end()));
|
||||
}
|
||||
|
||||
private PbsAst.Statement parseContinueStatement(final PbsToken continueToken) {
|
||||
final var semicolon = consume(PbsTokenKind.SEMICOLON, "Expected ';' after 'continue'");
|
||||
if (loopDepth <= 0) {
|
||||
report(continueToken, ParseErrors.E_PARSE_LOOP_CONTROL_OUTSIDE_LOOP,
|
||||
"'continue' is only valid inside loop contexts");
|
||||
}
|
||||
return new PbsAst.ContinueStatement(span(continueToken.start(), semicolon.end()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a return statement with an optional returned value.
|
||||
*/
|
||||
@ -689,6 +856,28 @@ public final class PbsParser {
|
||||
return new PbsAst.ExpressionStatement(expression, span(expression.span().getStart(), semicolon.end()));
|
||||
}
|
||||
|
||||
private boolean isAssignmentStatementStart() {
|
||||
if (!cursor.check(PbsTokenKind.IDENTIFIER)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int offset = 1;
|
||||
while (cursor.peek(offset).kind() == PbsTokenKind.DOT) {
|
||||
if (cursor.peek(offset + 1).kind() != PbsTokenKind.IDENTIFIER) {
|
||||
return false;
|
||||
}
|
||||
if (cursor.peek(offset + 2).kind() == PbsTokenKind.LEFT_PAREN) {
|
||||
return false;
|
||||
}
|
||||
offset += 2;
|
||||
}
|
||||
|
||||
return switch (cursor.peek(offset).kind()) {
|
||||
case EQUAL, PLUS_EQUAL, MINUS_EQUAL, STAR_EQUAL, SLASH_EQUAL, PERCENT_EQUAL -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
private void consumeBlockForMember(final String memberKind) {
|
||||
if (!cursor.match(PbsTokenKind.LEFT_BRACE)) {
|
||||
report(cursor.peek(), ParseErrors.E_PARSE_INVALID_DECL_SHAPE,
|
||||
|
||||
@ -0,0 +1,128 @@
|
||||
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 PbsParserStatementsTest {
|
||||
|
||||
@Test
|
||||
void shouldParseLetConstAssignIfForWhileAndLoopControl() {
|
||||
final var source = """
|
||||
fn flow(n: int) -> int {
|
||||
let const a: int = 1;
|
||||
let b: int = n;
|
||||
b += 2;
|
||||
data.value = b;
|
||||
|
||||
if b > 0 {
|
||||
b = b - 1;
|
||||
} else if b == 0 {
|
||||
b = 10;
|
||||
} else {
|
||||
b = 20;
|
||||
}
|
||||
|
||||
for i: int from 0 until 10 step 2 {
|
||||
if i == 4 {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
while b > 0 {
|
||||
b -= 1;
|
||||
}
|
||||
|
||||
return b;
|
||||
}
|
||||
""";
|
||||
|
||||
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 statement/control-flow forms should parse without diagnostics");
|
||||
|
||||
final var fn = ast.functions().getFirst();
|
||||
final var statements = fn.body().statements();
|
||||
|
||||
final var letConst = assertInstanceOf(PbsAst.LetStatement.class, statements.get(0));
|
||||
assertTrue(letConst.isConst());
|
||||
|
||||
final var assignCompound = assertInstanceOf(PbsAst.AssignStatement.class, statements.get(2));
|
||||
assertEquals(PbsAst.AssignOperator.ADD_ASSIGN, assignCompound.operator());
|
||||
|
||||
final var assignPath = assertInstanceOf(PbsAst.AssignStatement.class, statements.get(3));
|
||||
assertEquals("data", assignPath.target().rootName());
|
||||
assertEquals(1, assignPath.target().pathSegments().size());
|
||||
|
||||
assertInstanceOf(PbsAst.IfStatement.class, statements.get(4));
|
||||
assertInstanceOf(PbsAst.ForStatement.class, statements.get(5));
|
||||
assertInstanceOf(PbsAst.WhileStatement.class, statements.get(6));
|
||||
assertInstanceOf(PbsAst.ReturnStatement.class, statements.get(7));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReportBreakContinueOutsideLoop() {
|
||||
final var source = """
|
||||
fn invalid() -> int {
|
||||
break;
|
||||
continue;
|
||||
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.hasErrors(), "break/continue outside loops must report diagnostics");
|
||||
assertEquals(1, ast.functions().size());
|
||||
|
||||
final var fn = ast.functions().getFirst();
|
||||
assertInstanceOf(PbsAst.BreakStatement.class, fn.body().statements().get(0));
|
||||
assertInstanceOf(PbsAst.ContinueStatement.class, fn.body().statements().get(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReportInvalidForShape() {
|
||||
final var source = """
|
||||
fn badFor() -> int {
|
||||
for i: int 0 until 10 {
|
||||
return i;
|
||||
}
|
||||
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(), "Malformed for statement should produce deterministic diagnostics");
|
||||
assertTrue(diagnostics.stream().anyMatch(d -> d.getCode().equals(ParseErrors.E_PARSE_INVALID_FOR_FORM.name())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectCallInAssignmentTarget() {
|
||||
final var source = """
|
||||
fn badTarget() -> int {
|
||||
obj.method() = 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(), "Assignment target with call segment must be rejected");
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user