implements PR004

This commit is contained in:
bQUARKz 2026-03-05 10:32:39 +00:00
parent 96c5505c04
commit 41f90930e5
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
5 changed files with 385 additions and 37 deletions

View File

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

View File

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

View File

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

View File

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

View File

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