diff --git a/docs/pbs/pull-requests/PR-004-pbs-parser-statements-and-control-flow.md b/docs/pbs/pull-requests/PR-004-pbs-parser-statements-and-control-flow.md deleted file mode 100644 index 630a2a07..00000000 --- a/docs/pbs/pull-requests/PR-004-pbs-parser-statements-and-control-flow.md +++ /dev/null @@ -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. diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/ast/PbsAst.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/ast/PbsAst.java index 429af7b2..6de2ab38 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/ast/PbsAst.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/ast/PbsAst.java @@ -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 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 { diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/ParseErrors.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/ParseErrors.java index 5fc957e2..6bf993a1 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/ParseErrors.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/ParseErrors.java @@ -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, } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsParser.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsParser.java index 72c8d120..e25e5314 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsParser.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsParser.java @@ -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 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(); + 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, diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserStatementsTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserStatementsTest.java new file mode 100644 index 00000000..ddec3d29 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserStatementsTest.java @@ -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"); + } +}