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 d8f13806..df69d499 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 @@ -4,6 +4,10 @@ import p.studio.compiler.source.Span; import p.studio.utilities.structures.ReadOnlyList; public final class PbsAst { + public sealed interface Statement permits LetStatement, ReturnStatement, ExpressionStatement { + Span span(); + } + private PbsAst() { } @@ -37,10 +41,6 @@ public final class PbsAst { Span span) { } - public sealed interface Statement permits LetStatement, ReturnStatement, ExpressionStatement { - Span span(); - } - public record LetStatement( String name, TypeRef explicitType, diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsExprParser.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsExprParser.java new file mode 100644 index 00000000..66dce85e --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsExprParser.java @@ -0,0 +1,331 @@ +package p.studio.compiler.pbs.parser; + +import p.studio.compiler.pbs.ast.PbsAst; +import p.studio.compiler.pbs.lexer.PbsToken; +import p.studio.compiler.pbs.lexer.PbsTokenKind; +import p.studio.compiler.source.Span; +import p.studio.compiler.source.diagnostics.DiagnosticSink; +import p.studio.compiler.source.identifiers.FileId; +import p.studio.utilities.structures.ReadOnlyList; + +import java.util.ArrayList; + +/** + * Dedicated expression parser for PBS. + * + *

This keeps precedence handling separate from declaration parsing and makes + * the top-level parser easier to read and extend. + */ +final class PbsExprParser { + private final PbsTokenCursor cursor; + private final FileId fileId; + private final DiagnosticSink diagnostics; + + PbsExprParser( + final PbsTokenCursor cursor, + final FileId fileId, + final DiagnosticSink diagnostics) { + this.cursor = cursor; + this.fileId = fileId; + this.diagnostics = diagnostics; + } + + /** + * Entry point for expression parsing. + */ + PbsAst.Expression parseExpression() { + return parseOr(); + } + + /** + * Parses left-associative logical-or expressions such as {@code a || b || c}. + */ + private PbsAst.Expression parseOr() { + var expression = parseAnd(); + while (cursor.match(PbsTokenKind.OR_OR)) { + final var operator = cursor.previous(); + final var right = parseAnd(); + expression = new PbsAst.BinaryExpr(operator.lexeme(), expression, right, + span(expression.span().getStart(), right.span().getEnd())); + } + return expression; + } + + /** + * Parses left-associative logical-and expressions such as {@code a && b && c}. + */ + private PbsAst.Expression parseAnd() { + var expression = parseEquality(); + while (cursor.match(PbsTokenKind.AND_AND)) { + final var operator = cursor.previous(); + final var right = parseEquality(); + expression = new PbsAst.BinaryExpr(operator.lexeme(), expression, right, + span(expression.span().getStart(), right.span().getEnd())); + } + return expression; + } + + /** + * Parses equality expressions and rejects chained non-associative forms. + * + *

Accepted: {@code a == b} + *

Rejected: {@code a == b == c} + */ + private PbsAst.Expression parseEquality() { + var expression = parseComparison(); + if (cursor.match(PbsTokenKind.EQUAL_EQUAL, PbsTokenKind.BANG_EQUAL)) { + final var operator = cursor.previous(); + final var right = parseComparison(); + expression = new PbsAst.BinaryExpr(operator.lexeme(), expression, right, + span(expression.span().getStart(), right.span().getEnd())); + if (cursor.check(PbsTokenKind.EQUAL_EQUAL) || cursor.check(PbsTokenKind.BANG_EQUAL)) { + report(cursor.peek(), ParseErrors.E_PARSE_NON_ASSOC, "Chained equality is not allowed"); + while (cursor.match(PbsTokenKind.EQUAL_EQUAL, PbsTokenKind.BANG_EQUAL)) { + parseComparison(); + } + } + } + return expression; + } + + /** + * Parses comparison expressions and rejects chained non-associative forms. + * + *

Accepted: {@code a < b} + *

Rejected: {@code a < b < c} + */ + private PbsAst.Expression parseComparison() { + var expression = parseTerm(); + if (cursor.match(PbsTokenKind.LESS, PbsTokenKind.LESS_EQUAL, PbsTokenKind.GREATER, PbsTokenKind.GREATER_EQUAL)) { + final var operator = cursor.previous(); + final var right = parseTerm(); + expression = new PbsAst.BinaryExpr(operator.lexeme(), expression, right, + span(expression.span().getStart(), right.span().getEnd())); + if (cursor.check(PbsTokenKind.LESS) || cursor.check(PbsTokenKind.LESS_EQUAL) + || cursor.check(PbsTokenKind.GREATER) || cursor.check(PbsTokenKind.GREATER_EQUAL)) { + report(cursor.peek(), ParseErrors.E_PARSE_NON_ASSOC, "Chained comparison is not allowed"); + while (cursor.match(PbsTokenKind.LESS, PbsTokenKind.LESS_EQUAL, PbsTokenKind.GREATER, PbsTokenKind.GREATER_EQUAL)) { + parseTerm(); + } + } + } + return expression; + } + + /** + * Parses additive expressions such as {@code a + b - c}. + */ + private PbsAst.Expression parseTerm() { + var expression = parseFactor(); + while (cursor.match(PbsTokenKind.PLUS, PbsTokenKind.MINUS)) { + final var operator = cursor.previous(); + final var right = parseFactor(); + expression = new PbsAst.BinaryExpr(operator.lexeme(), expression, right, + span(expression.span().getStart(), right.span().getEnd())); + } + return expression; + } + + /** + * Parses multiplicative expressions such as {@code a * b / c % d}. + */ + private PbsAst.Expression parseFactor() { + var expression = parseUnary(); + while (cursor.match(PbsTokenKind.STAR, PbsTokenKind.SLASH, PbsTokenKind.PERCENT)) { + final var operator = cursor.previous(); + final var right = parseUnary(); + expression = new PbsAst.BinaryExpr(operator.lexeme(), expression, right, + span(expression.span().getStart(), right.span().getEnd())); + } + return expression; + } + + /** + * Parses unary prefix operators such as {@code -x} and {@code !ready}. + */ + private PbsAst.Expression parseUnary() { + if (cursor.match(PbsTokenKind.BANG, PbsTokenKind.MINUS)) { + final var operator = cursor.previous(); + final var right = parseUnary(); + return new PbsAst.UnaryExpr( + operator.lexeme(), + right, + span(operator.start(), right.span().getEnd())); + } + return parseCall(); + } + + /** + * Parses call chains after a primary expression. + * + *

Examples: + *

{@code
+     * f()
+     * sum(a, b)
+     * factory()(1)
+     * }
+ */ + private PbsAst.Expression parseCall() { + var expression = parsePrimary(); + + while (cursor.match(PbsTokenKind.LEFT_PAREN)) { + final var open = cursor.previous(); + final var arguments = new ArrayList(); + if (!cursor.check(PbsTokenKind.RIGHT_PAREN)) { + do { + arguments.add(parseExpression()); + } while (cursor.match(PbsTokenKind.COMMA)); + } + final var close = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after arguments"); + expression = new PbsAst.CallExpr( + expression, + ReadOnlyList.wrap(arguments), + span(expression.span().getStart(), close.end())); + + // Avoid endless loops on malformed "f((" forms. + if (open.start() == close.start()) { + break; + } + } + + return expression; + } + + /** + * Parses primary expressions: literals, identifiers, and grouped expressions. + */ + private PbsAst.Expression parsePrimary() { + 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())); + } + if (cursor.match(PbsTokenKind.IDENTIFIER)) { + final var token = cursor.previous(); + return new PbsAst.IdentifierExpr(token.lexeme(), span(token.start(), token.end())); + } + if (cursor.match(PbsTokenKind.LEFT_PAREN)) { + final var open = cursor.previous(); + final var expression = parseExpression(); + final var close = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after grouped expression"); + return new PbsAst.GroupExpr(expression, span(open.start(), close.end())); + } + + final var token = cursor.peek(); + report(token, ParseErrors.E_PARSE_UNEXPECTED_TOKEN, "Unexpected token in expression: " + token.kind()); + cursor.advance(); + return new PbsAst.IntLiteralExpr(0L, span(token.start(), token.end())); + } + + /** + * Consumes a required token and reports an error if it is missing. + * + *

The parser advances on failure when possible so recovery can continue. + */ + private PbsToken consume(final PbsTokenKind kind, final String message) { + if (cursor.check(kind)) { + return cursor.advance(); + } + final var token = cursor.peek(); + report(token, ParseErrors.E_PARSE_EXPECTED_TOKEN, message + ", found " + token.kind()); + if (!cursor.isAtEnd()) { + return cursor.advance(); + } + return token; + } + + /** + * Builds a source span for the current file. + */ + private Span span(final long start, final long end) { + return new Span(fileId, start, end); + } + + /** + * Reports an expression parser diagnostic at the given token span. + */ + private void report(final PbsToken token, final ParseErrors parseErrors, final String message) { + diagnostics.error(parseErrors.name(), message, new Span(fileId, token.start(), token.end())); + } + + /** + * Parses an integer literal for AST construction and falls back to zero on malformed input. + */ + private long parseLongOrDefault(final String text) { + try { + return Long.parseLong(text); + } catch (NumberFormatException ignored) { + return 0L; + } + } + + /** + * Parses a bounded literal payload and falls back to zero on malformed input. + */ + private int parseIntOrDefault(final String text) { + try { + return Integer.parseInt(text); + } catch (NumberFormatException ignored) { + return 0; + } + } + + /** + * Parses a floating-point literal for AST construction and falls back to zero on malformed input. + */ + private double parseDoubleOrDefault(final String text) { + try { + return Double.parseDouble(text); + } catch (NumberFormatException ignored) { + return 0.0; + } + } + + /** + * Converts a quoted token lexeme such as {@code "\"hello\\n\""} into its unescaped runtime text. + */ + private String unescapeString(final String lexeme) { + if (lexeme.length() < 2) { + return ""; + } + final var raw = lexeme.substring(1, lexeme.length() - 1); + final var sb = new StringBuilder(raw.length()); + for (int i = 0; i < raw.length(); i++) { + final char c = raw.charAt(i); + if (c != '\\' || i + 1 >= raw.length()) { + sb.append(c); + continue; + } + final char next = raw.charAt(++i); + switch (next) { + case 'n' -> sb.append('\n'); + case 'r' -> sb.append('\r'); + case 't' -> sb.append('\t'); + case '"' -> sb.append('"'); + case '\\' -> sb.append('\\'); + default -> sb.append(next); + } + } + return sb.toString(); + } +} 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 b4badd2f..fd81ac2b 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 @@ -10,20 +10,25 @@ import p.studio.utilities.structures.ReadOnlyList; import java.util.ArrayList; +/** + * High-level manual parser for PBS source files. + * + *

This class focuses on declarations, blocks, statements, and error recovery. + * Expression precedence is delegated to {@link PbsExprParser}, while raw token + * navigation is delegated to {@link PbsTokenCursor}. + */ public final class PbsParser { - private final ArrayList tokens; + private final PbsTokenCursor cursor; + private final PbsExprParser exprParser; private final FileId fileId; private final DiagnosticSink diagnostics; - private int current; private PbsParser( final ReadOnlyList tokens, final FileId fileId, final DiagnosticSink diagnostics) { - this.tokens = new ArrayList<>(); - for (final var token : tokens) { - this.tokens.add(token); - } + this.cursor = new PbsTokenCursor(tokens); + this.exprParser = new PbsExprParser(cursor, fileId, diagnostics); this.fileId = fileId; this.diagnostics = diagnostics; } @@ -53,33 +58,34 @@ public final class PbsParser { private PbsAst.File parseFile() { final var functions = new ArrayList(); - while (!isAtEnd()) { - if (match(PbsTokenKind.IMPORT)) { + while (!cursor.isAtEnd()) { + if (cursor.match(PbsTokenKind.IMPORT)) { parseAndDiscardImport(); continue; } - if (match(PbsTokenKind.FN)) { - functions.add(parseFunction(previous())); + if (cursor.match(PbsTokenKind.FN)) { + functions.add(parseFunction(cursor.previous())); continue; } - if (match(PbsTokenKind.MOD, PbsTokenKind.PUB)) { - report(previous(), ParseErrors.E_PARSE_VISIBILITY_IN_SOURCE, + if (cursor.match(PbsTokenKind.MOD, PbsTokenKind.PUB)) { + report(cursor.previous(), ParseErrors.E_PARSE_VISIBILITY_IN_SOURCE, "Visibility modifiers are barrel-only and cannot appear in .pbs declarations"); synchronizeTopLevel(); continue; } - if (check(PbsTokenKind.EOF)) { + if (cursor.check(PbsTokenKind.EOF)) { break; } - report(peek(), ParseErrors.E_PARSE_UNEXPECTED_TOKEN, "Expected top-level declaration ('fn') or import"); + report(cursor.peek(), ParseErrors.E_PARSE_UNEXPECTED_TOKEN, + "Expected top-level declaration ('fn') or import"); synchronizeTopLevel(); } - final var eof = peek(); + final var eof = cursor.peek(); return new PbsAst.File(ReadOnlyList.wrap(functions), span(0, eof.end())); } @@ -93,21 +99,17 @@ public final class PbsParser { * } */ private void parseAndDiscardImport() { - // Supports both forms: - // import @core:math; - // import { A, B as C } from @core:math; - if (match(PbsTokenKind.LEFT_BRACE)) { - while (!check(PbsTokenKind.RIGHT_BRACE) && !isAtEnd()) { - if (match(PbsTokenKind.IDENTIFIER)) { - if (match(PbsTokenKind.AS)) { - consume(PbsTokenKind.IDENTIFIER, - "Expected alias identifier after 'as'"); + if (cursor.match(PbsTokenKind.LEFT_BRACE)) { + while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) { + if (cursor.match(PbsTokenKind.IDENTIFIER)) { + if (cursor.match(PbsTokenKind.AS)) { + consume(PbsTokenKind.IDENTIFIER, "Expected alias identifier after 'as'"); } - match(PbsTokenKind.COMMA); + cursor.match(PbsTokenKind.COMMA); continue; } - report(peek(), ParseErrors.E_PARSE_UNEXPECTED_TOKEN, "Invalid import item"); - advance(); + report(cursor.peek(), ParseErrors.E_PARSE_UNEXPECTED_TOKEN, "Invalid import item"); + cursor.advance(); } consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' in import list"); consume(PbsTokenKind.FROM, "Expected 'from' in named import"); @@ -125,7 +127,7 @@ public final class PbsParser { consume(PbsTokenKind.IDENTIFIER, "Expected project identifier in module reference"); consume(PbsTokenKind.COLON, "Expected ':' in module reference"); consume(PbsTokenKind.IDENTIFIER, "Expected module identifier"); - while (match(PbsTokenKind.SLASH)) { + while (cursor.match(PbsTokenKind.SLASH)) { consume(PbsTokenKind.IDENTIFIER, "Expected module path segment after '/'"); } } @@ -145,28 +147,28 @@ public final class PbsParser { consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after function name"); final var parameters = new ArrayList(); - if (!check(PbsTokenKind.RIGHT_PAREN)) { + if (!cursor.check(PbsTokenKind.RIGHT_PAREN)) { do { - final var pStart = peek(); - final var pName = consume(PbsTokenKind.IDENTIFIER, "Expected parameter name"); + final var parameterStart = cursor.peek(); + final var parameterName = consume(PbsTokenKind.IDENTIFIER, "Expected parameter name"); consume(PbsTokenKind.COLON, "Expected ':' after parameter name"); final var typeRef = parseTypeRef(); parameters.add(new PbsAst.Parameter( - pName.lexeme(), + parameterName.lexeme(), typeRef, - span(pStart.start(), typeRef.span().getEnd()))); - } while (match(PbsTokenKind.COMMA)); + span(parameterStart.start(), typeRef.span().getEnd()))); + } while (cursor.match(PbsTokenKind.COMMA)); } consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after parameter list"); PbsAst.TypeRef returnType = null; - if (match(PbsTokenKind.COLON)) { + if (cursor.match(PbsTokenKind.COLON)) { returnType = parseTypeRef(); } PbsAst.Expression elseFallback = null; - if (match(PbsTokenKind.ELSE)) { - elseFallback = parseExpression(); + if (cursor.match(PbsTokenKind.ELSE)) { + elseFallback = exprParser.parseExpression(); } final var body = parseBlock(); @@ -201,7 +203,7 @@ public final class PbsParser { private PbsAst.Block parseBlock() { final var leftBrace = consume(PbsTokenKind.LEFT_BRACE, "Expected '{' to start block"); final var statements = new ArrayList(); - while (!check(PbsTokenKind.RIGHT_BRACE) && !isAtEnd()) { + while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) { statements.add(parseStatement()); } final var rightBrace = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to end block"); @@ -211,14 +213,14 @@ public final class PbsParser { /** * Parses one statement inside a block. * - *

The current slice supports `let`, `return`, and expression statements. + *

The current slice supports {@code let}, {@code return}, and expression statements. */ private PbsAst.Statement parseStatement() { - if (match(PbsTokenKind.LET)) { - return parseLetStatement(previous()); + if (cursor.match(PbsTokenKind.LET)) { + return parseLetStatement(cursor.previous()); } - if (match(PbsTokenKind.RETURN)) { - return parseReturnStatement(previous()); + if (cursor.match(PbsTokenKind.RETURN)) { + return parseReturnStatement(cursor.previous()); } return parseExpressionStatement(); } @@ -236,12 +238,12 @@ public final class PbsParser { final var name = consume(PbsTokenKind.IDENTIFIER, "Expected variable name"); PbsAst.TypeRef explicitType = null; - if (match(PbsTokenKind.COLON)) { + if (cursor.match(PbsTokenKind.COLON)) { explicitType = parseTypeRef(); } consume(PbsTokenKind.EQUAL, "Expected '=' in let statement"); - final var initializer = parseExpression(); + final var initializer = exprParser.parseExpression(); final var semicolon = consume(PbsTokenKind.SEMICOLON, "Expected ';' after let statement"); return new PbsAst.LetStatement( @@ -256,8 +258,8 @@ public final class PbsParser { */ private PbsAst.Statement parseReturnStatement(final PbsToken returnToken) { PbsAst.Expression value = null; - if (!check(PbsTokenKind.SEMICOLON)) { - value = parseExpression(); + if (!cursor.check(PbsTokenKind.SEMICOLON)) { + value = exprParser.parseExpression(); } final var semicolon = consume(PbsTokenKind.SEMICOLON, "Expected ';' after return"); return new PbsAst.ReturnStatement(value, span(returnToken.start(), semicolon.end())); @@ -269,302 +271,45 @@ public final class PbsParser { *

Example: {@code log(value);} */ private PbsAst.Statement parseExpressionStatement() { - final var expression = parseExpression(); + final var expression = exprParser.parseExpression(); final var semicolon = consume(PbsTokenKind.SEMICOLON, "Expected ';' after expression"); return new PbsAst.ExpressionStatement(expression, span(expression.span().getStart(), semicolon.end())); } - /** - * Entry point for expression parsing. - */ - private PbsAst.Expression parseExpression() { - return parseOr(); - } - - /** - * Parses left-associative logical-or expressions such as {@code a || b || c}. - */ - private PbsAst.Expression parseOr() { - var expression = parseAnd(); - while (match(PbsTokenKind.OR_OR)) { - final var operator = previous(); - final var right = parseAnd(); - expression = new PbsAst.BinaryExpr(operator.lexeme(), expression, right, - span(expression.span().getStart(), right.span().getEnd())); - } - return expression; - } - - /** - * Parses left-associative logical-and expressions such as {@code a && b && c}. - */ - private PbsAst.Expression parseAnd() { - var expression = parseEquality(); - while (match(PbsTokenKind.AND_AND)) { - final var operator = previous(); - final var right = parseEquality(); - expression = new PbsAst.BinaryExpr(operator.lexeme(), expression, right, - span(expression.span().getStart(), right.span().getEnd())); - } - return expression; - } - - /** - * Parses equality expressions and rejects chained non-associative forms. - * - *

Accepted: {@code a == b} - *

Rejected: {@code a == b == c} - */ - private PbsAst.Expression parseEquality() { - var expression = parseComparison(); - if (match(PbsTokenKind.EQUAL_EQUAL, PbsTokenKind.BANG_EQUAL)) { - final var operator = previous(); - final var right = parseComparison(); - expression = new PbsAst.BinaryExpr(operator.lexeme(), expression, right, - span(expression.span().getStart(), right.span().getEnd())); - if (check(PbsTokenKind.EQUAL_EQUAL) || check(PbsTokenKind.BANG_EQUAL)) { - report(peek(), ParseErrors.E_PARSE_NON_ASSOC, "Chained equality is not allowed"); - while (match(PbsTokenKind.EQUAL_EQUAL, PbsTokenKind.BANG_EQUAL)) { - parseComparison(); - } - } - } - return expression; - } - - /** - * Parses comparison expressions and rejects chained non-associative forms. - * - *

Accepted: {@code a < b} - *

Rejected: {@code a < b < c} - */ - private PbsAst.Expression parseComparison() { - var expression = parseTerm(); - if (match(PbsTokenKind.LESS, PbsTokenKind.LESS_EQUAL, PbsTokenKind.GREATER, PbsTokenKind.GREATER_EQUAL)) { - final var operator = previous(); - final var right = parseTerm(); - expression = new PbsAst.BinaryExpr(operator.lexeme(), expression, right, - span(expression.span().getStart(), right.span().getEnd())); - if (check(PbsTokenKind.LESS) || check(PbsTokenKind.LESS_EQUAL) - || check(PbsTokenKind.GREATER) || check(PbsTokenKind.GREATER_EQUAL)) { - report(peek(), ParseErrors.E_PARSE_NON_ASSOC, "Chained comparison is not allowed"); - while (match(PbsTokenKind.LESS, PbsTokenKind.LESS_EQUAL, PbsTokenKind.GREATER, PbsTokenKind.GREATER_EQUAL)) { - parseTerm(); - } - } - } - return expression; - } - - /** - * Parses additive expressions such as {@code a + b - c}. - */ - private PbsAst.Expression parseTerm() { - var expression = parseFactor(); - while (match(PbsTokenKind.PLUS, PbsTokenKind.MINUS)) { - final var operator = previous(); - final var right = parseFactor(); - expression = new PbsAst.BinaryExpr(operator.lexeme(), expression, right, - span(expression.span().getStart(), right.span().getEnd())); - } - return expression; - } - - /** - * Parses multiplicative expressions such as {@code a * b / c % d}. - */ - private PbsAst.Expression parseFactor() { - var expression = parseUnary(); - while (match(PbsTokenKind.STAR, PbsTokenKind.SLASH, PbsTokenKind.PERCENT)) { - final var operator = previous(); - final var right = parseUnary(); - expression = new PbsAst.BinaryExpr(operator.lexeme(), expression, right, - span(expression.span().getStart(), right.span().getEnd())); - } - return expression; - } - - /** - * Parses unary prefix operators such as {@code -x} and {@code !ready}. - */ - private PbsAst.Expression parseUnary() { - if (match(PbsTokenKind.BANG, PbsTokenKind.MINUS)) { - final var operator = previous(); - final var right = parseUnary(); - return new PbsAst.UnaryExpr( - operator.lexeme(), - right, - span(operator.start(), right.span().getEnd())); - } - return parseCall(); - } - - /** - * Parses call chains after a primary expression. - * - *

Examples: - *

{@code
-     * f()
-     * sum(a, b)
-     * factory()(1)
-     * }
- */ - private PbsAst.Expression parseCall() { - var expression = parsePrimary(); - - while (match(PbsTokenKind.LEFT_PAREN)) { - final var open = previous(); - final var arguments = new ArrayList(); - if (!check(PbsTokenKind.RIGHT_PAREN)) { - do { - arguments.add(parseExpression()); - } while (match(PbsTokenKind.COMMA)); - } - final var close = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after arguments"); - expression = new PbsAst.CallExpr( - expression, - ReadOnlyList.wrap(arguments), - span(expression.span().getStart(), close.end())); - - // Avoid endless loops on malformed "f((" forms. - if (open.start() == close.start()) { - break; - } - } - - return expression; - } - - /** - * Parses primary expressions: literals, identifiers, and grouped expressions. - */ - private PbsAst.Expression parsePrimary() { - if (match(PbsTokenKind.TRUE)) { - final var token = previous(); - return new PbsAst.BoolLiteralExpr(true, span(token.start(), token.end())); - } - if (match(PbsTokenKind.FALSE)) { - final var token = previous(); - return new PbsAst.BoolLiteralExpr(false, span(token.start(), token.end())); - } - if (match(PbsTokenKind.INT_LITERAL)) { - final var token = previous(); - return new PbsAst.IntLiteralExpr(parseLongOrDefault(token.lexeme()), span(token.start(), token.end())); - } - if (match(PbsTokenKind.FLOAT_LITERAL)) { - final var token = previous(); - return new PbsAst.FloatLiteralExpr(parseDoubleOrDefault(token.lexeme()), span(token.start(), token.end())); - } - if (match(PbsTokenKind.BOUNDED_LITERAL)) { - final var token = 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 (match(PbsTokenKind.STRING_LITERAL)) { - final var token = previous(); - return new PbsAst.StringLiteralExpr(unescapeString(token.lexeme()), span(token.start(), token.end())); - } - if (match(PbsTokenKind.IDENTIFIER)) { - final var token = previous(); - return new PbsAst.IdentifierExpr(token.lexeme(), span(token.start(), token.end())); - } - if (match(PbsTokenKind.LEFT_PAREN)) { - final var open = previous(); - final var expression = parseExpression(); - final var close = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after grouped expression"); - return new PbsAst.GroupExpr(expression, span(open.start(), close.end())); - } - - final var token = peek(); - report(token, ParseErrors.E_PARSE_UNEXPECTED_TOKEN, "Unexpected token in expression: " + token.kind()); - advance(); - return new PbsAst.IntLiteralExpr(0L, span(token.start(), token.end())); - } - /** * Skips tokens until a safe top-level restart point is reached. * *

This allows the parser to continue reporting more than one diagnostic per file. */ private void synchronizeTopLevel() { - while (!isAtEnd()) { - if (check(PbsTokenKind.FN) || check(PbsTokenKind.IMPORT)) { + while (!cursor.isAtEnd()) { + if (cursor.check(PbsTokenKind.FN) || cursor.check(PbsTokenKind.IMPORT)) { return; } - if (match(PbsTokenKind.SEMICOLON)) { + if (cursor.match(PbsTokenKind.SEMICOLON)) { return; } - advance(); + cursor.advance(); } } - /** - * Consumes the next token if it matches any provided kind. - */ - private boolean match(final PbsTokenKind... kinds) { - for (final var kind : kinds) { - if (check(kind)) { - advance(); - return true; - } - } - return false; - } - /** * Consumes a required token and reports an error if it is missing. * *

The parser advances on failure when possible so recovery can continue. */ private PbsToken consume(final PbsTokenKind kind, final String message) { - if (check(kind)) { - return advance(); + if (cursor.check(kind)) { + return cursor.advance(); } - final var token = peek(); + final var token = cursor.peek(); report(token, ParseErrors.E_PARSE_EXPECTED_TOKEN, message + ", found " + token.kind()); - if (!isAtEnd()) { - return advance(); + if (!cursor.isAtEnd()) { + return cursor.advance(); } return token; } - /** - * Returns whether the current token matches the expected kind. - */ - private boolean check(final PbsTokenKind kind) { - if (isAtEnd()) return kind == PbsTokenKind.EOF; - return peek().kind() == kind; - } - - /** - * Advances to the next token and returns the previously current token. - */ - private PbsToken advance() { - if (!isAtEnd()) current++; - return previous(); - } - - /** - * Returns whether the parser reached the synthetic EOF token. - */ - private boolean isAtEnd() { - return peek().kind() == PbsTokenKind.EOF; - } - - /** - * Returns the current token without consuming it. - */ - private PbsToken peek() { - return tokens.get(current); - } - - /** - * Returns the most recently consumed token. - */ - private PbsToken previous() { - return tokens.get(Math.max(current - 1, 0)); - } - /** * Builds a source span for the current file. */ @@ -578,65 +323,4 @@ public final class PbsParser { private void report(final PbsToken token, final ParseErrors parseErrors, final String message) { diagnostics.error(parseErrors.name(), message, new Span(fileId, token.start(), token.end())); } - - /** - * Parses an integer literal for AST construction and falls back to zero on malformed input. - */ - private long parseLongOrDefault(final String text) { - try { - return Long.parseLong(text); - } catch (NumberFormatException ignored) { - return 0L; // fallback - } - } - - /** - * Parses a bounded literal payload and falls back to zero on malformed input. - */ - private int parseIntOrDefault(final String text) { - try { - return Integer.parseInt(text); - } catch (NumberFormatException ignored) { - return 0; // fallback - } - } - - /** - * Parses a floating-point literal for AST construction and falls back to zero on malformed input. - */ - private double parseDoubleOrDefault(final String text) { - try { - return Double.parseDouble(text); - } catch (NumberFormatException ignored) { - return 0.0; // fallback - } - } - - /** - * Converts a quoted token lexeme such as {@code "\"hello\\n\""} into its unescaped runtime text. - */ - private String unescapeString(final String lexeme) { - if (lexeme.length() < 2) { - return ""; - } - final var raw = lexeme.substring(1, lexeme.length() - 1); - final var sb = new StringBuilder(raw.length()); - for (int i = 0; i < raw.length(); i++) { - final char c = raw.charAt(i); - if (c != '\\' || i + 1 >= raw.length()) { - sb.append(c); - continue; - } - final char next = raw.charAt(++i); - switch (next) { - case 'n' -> sb.append('\n'); - case 'r' -> sb.append('\r'); - case 't' -> sb.append('\t'); - case '"' -> sb.append('"'); - case '\\' -> sb.append('\\'); - default -> sb.append(next); - } - } - return sb.toString(); - } } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsTokenCursor.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsTokenCursor.java new file mode 100644 index 00000000..df331330 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsTokenCursor.java @@ -0,0 +1,74 @@ +package p.studio.compiler.pbs.parser; + +import p.studio.compiler.pbs.lexer.PbsToken; +import p.studio.compiler.pbs.lexer.PbsTokenKind; +import p.studio.utilities.structures.ReadOnlyList; + +import java.util.ArrayList; + +/** + * Lightweight cursor used by the manual parser to navigate the token stream. + * + *

This type keeps token access logic out of the higher-level parser so the + * declaration parser can read more like grammar and less like array plumbing. + */ +final class PbsTokenCursor { + private final ArrayList tokens = new ArrayList<>(); + private int current; + + PbsTokenCursor(final ReadOnlyList tokens) { + for (final var token : tokens) { + this.tokens.add(token); + } + } + + /** + * Consumes the next token if it matches any provided kind. + */ + boolean match(final PbsTokenKind... kinds) { + for (final var kind : kinds) { + if (check(kind)) { + advance(); + return true; + } + } + return false; + } + + /** + * Returns whether the current token matches the expected kind. + */ + boolean check(final PbsTokenKind kind) { + if (isAtEnd()) return kind == PbsTokenKind.EOF; + return peek().kind() == kind; + } + + /** + * Advances to the next token and returns the previously current token. + */ + PbsToken advance() { + if (!isAtEnd()) current++; + return previous(); + } + + /** + * Returns whether the cursor reached the synthetic EOF token. + */ + boolean isAtEnd() { + return peek().kind() == PbsTokenKind.EOF; + } + + /** + * Returns the current token without consuming it. + */ + PbsToken peek() { + return tokens.get(current); + } + + /** + * Returns the most recently consumed token. + */ + PbsToken previous() { + return tokens.get(Math.max(current - 1, 0)); + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java index 68513fe1..27757181 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java @@ -8,10 +8,7 @@ import p.studio.compiler.models.IRFunction; import p.studio.compiler.pbs.PbsFrontendCompiler; import p.studio.compiler.source.diagnostics.DiagnosticSink; import p.studio.utilities.logs.LogAggregator; -import p.studio.utilities.structures.ReadOnlyList; - -import java.util.ArrayList; -import java.util.Comparator; +import p.studio.utilities.structures.MutableList; @Slf4j public class PBSFrontendPhaseService implements FrontendPhaseService { @@ -19,38 +16,40 @@ public class PBSFrontendPhaseService implements FrontendPhaseService { @Override public IRBackend compile(final FrontendPhaseContext ctx, final LogAggregator logs, final BuildingIssueSink issues) { - final var functions = new ArrayList(); + final MutableList functions = MutableList.create(); for (final var pId : ctx.stack.reverseTopologicalOrder) { - final var fileIds = ctx.fileTable - .getFiles(pId) - .stream() - .sorted(Comparator.comparing(fId -> ctx.fileTable.get(fId).getCanonPath().toString())) - .toList(); + final var fileIds = ctx.fileTable.getFiles(pId); for (final var fId : fileIds) { final var sourceHandle = ctx.fileTable.get(fId); - if (!sourceHandle.getFilename().endsWith(".pbs")) { - continue; + switch (sourceHandle.getExtension()) { + case "pbs": { + sourceHandle.readUtf8().ifPresentOrElse( + utf8Content -> { + final var diagnostics = DiagnosticSink.empty(); + functions.addAll(frontendCompiler + .compileFile(fId, utf8Content, diagnostics) + .stream() + .toList()); + adaptDiagnostics(sourceHandle.getCanonPath().toString(), diagnostics, issues); + }, + () -> issues.report(builder -> builder + .error(true) + .message("Failed to read file content: %s".formatted(sourceHandle.toString())))); + } break; + case "barrel": + break; + default: } - - sourceHandle.readUtf8().ifPresentOrElse( - utf8Content -> { - final var diagnostics = DiagnosticSink.empty(); - functions.addAll(frontendCompiler - .compileFile(fId, utf8Content, diagnostics) - .stream() - .toList()); - adaptDiagnostics(sourceHandle.getCanonPath().toString(), diagnostics, issues); - }, - () -> issues.report(builder -> builder - .error(true) - .message("Failed to read file content: %s".formatted(sourceHandle.toString())))); } } logs.using(log).debug("PBS frontend lowered %d function(s) to IR".formatted(functions.size())); - return new IRBackend(ReadOnlyList.wrap(functions)); + return IRBackend + .builder() + .functions(functions.toReadOnlyList()) + .build(); } private void adaptDiagnostics( diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/lexer/PbsLexerTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/lexer/PbsLexerTest.java index bd82e346..3aa2b625 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/lexer/PbsLexerTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/lexer/PbsLexerTest.java @@ -11,7 +11,12 @@ class PbsLexerTest { @Test void shouldLexFunctionTokens() { - final var source = "fn sum(a: int, b: int): int { return a + b; }"; + final var source = """ + fn sum(a: int, b: int): int + { + return a + b; + } + """; final var diagnostics = DiagnosticSink.empty(); final var tokens = PbsLexer.lex(source, new FileId(0), diagnostics); diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserTest.java index 5689f830..f88cbfab 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserTest.java @@ -15,10 +15,11 @@ class PbsParserTest { @Test void shouldParseSingleFunction() { final var source = """ - fn sum(a: int, b: int): int { - return a + b; - } - """; + fn sum(a: int, b: int): int + { + return a + b; + } + """; final var diagnostics = DiagnosticSink.empty(); final var fileId = new FileId(0); final var tokens = PbsLexer.lex(source, fileId, diagnostics); diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/FrontendPhasePipelineStage.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/FrontendPhasePipelineStage.java index 2a1d5303..24916aa0 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/FrontendPhasePipelineStage.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/FrontendPhasePipelineStage.java @@ -25,6 +25,8 @@ public class FrontendPhasePipelineStage implements PipelineStage { final var frontendPhaseContext = new FrontendPhaseContext(projectTable, fileTable, ctx.resolvedWorkspace.stack()); final var issues = BuildingIssueSink.empty(); ctx.irBackend = service.get().compile(frontendPhaseContext, logs, issues); + logs.using(log).debug("IR Backend: " + ctx.irBackend); + logs.using(log).debug("Frontend phase completed successfully!"); return issues; } } diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/LoadSourcesPipelineStage.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/LoadSourcesPipelineStage.java index b2c7e197..32a4af2c 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/LoadSourcesPipelineStage.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/LoadSourcesPipelineStage.java @@ -46,7 +46,7 @@ public class LoadSourcesPipelineStage implements PipelineStage { try { Files.walkFileTree(sourceRootPath, sourceCrawler); - paths.sort(Path::compareTo); // do we really need this for deterministic builds? + paths.sort(Path::compareTo); } catch (IOException e) { issues.report(builder -> builder .error(true) diff --git a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/messages/BuildingIssueSink.java b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/messages/BuildingIssueSink.java index 77e3b83c..4b1b6000 100644 --- a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/messages/BuildingIssueSink.java +++ b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/messages/BuildingIssueSink.java @@ -3,31 +3,37 @@ package p.studio.compiler.messages; import p.studio.utilities.structures.ReadOnlyCollection; import java.util.ArrayList; +import java.util.Collection; import java.util.function.Consumer; -public class BuildingIssueSink extends ReadOnlyCollection { +public class BuildingIssueSink implements ReadOnlyCollection { + private final ArrayList issues = new ArrayList<>(); private boolean hasErrors = false; protected BuildingIssueSink() { - super(new ArrayList<>()); } public static BuildingIssueSink empty() { return new BuildingIssueSink(); } + @Override + public Collection asCollection() { + return issues; + } + public BuildingIssueSink report(final Consumer consumer) { final var builder = BuildingIssue.builder(); consumer.accept(builder); final var issue = builder.build(); hasErrors |= issue.isError(); - collection.add(issue); + issues.add(issue); return this; } public BuildingIssueSink merge(final BuildingIssueSink issues) { hasErrors |= issues.hasErrors(); - collection.addAll(issues.collection); + this.issues.addAll(issues.issues); return this; } diff --git a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/SourceHandle.java b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/SourceHandle.java index c11b7796..ffde62b7 100644 --- a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/SourceHandle.java +++ b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/SourceHandle.java @@ -2,6 +2,7 @@ package p.studio.compiler.models; import lombok.EqualsAndHashCode; import lombok.Getter; +import org.apache.commons.io.FilenameUtils; import p.studio.compiler.source.identifiers.ProjectId; import p.studio.compiler.utilities.SourceProvider; import p.studio.compiler.utilities.SourceProviderFactory; @@ -26,6 +27,8 @@ public class SourceHandle { @Getter private final String filename; @Getter + private final String extension; + @Getter private final long size; @Getter private final long lastModified; @@ -42,6 +45,7 @@ public class SourceHandle { this.relativePath = relativePath; this.canonPath = canonPath; this.filename = canonPath.getFileName().toString(); + this.extension = FilenameUtils.getExtension(this.filename); this.size = size; this.lastModified = lastModified; this.provider = factory.create(canonPath); diff --git a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/source/diagnostics/DiagnosticSink.java b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/source/diagnostics/DiagnosticSink.java index 956cfdfd..f51929e2 100644 --- a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/source/diagnostics/DiagnosticSink.java +++ b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/source/diagnostics/DiagnosticSink.java @@ -4,21 +4,27 @@ import p.studio.compiler.source.Span; import p.studio.utilities.structures.ReadOnlyCollection; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Objects; -public class DiagnosticSink extends ReadOnlyCollection { +public class DiagnosticSink implements ReadOnlyCollection { + private final ArrayList diagnostics = new ArrayList<>(); private int errorCount = 0; private int warningCount = 0; protected DiagnosticSink() { - super(new ArrayList<>()); } public static DiagnosticSink empty() { return new DiagnosticSink(); } + @Override + public Collection asCollection() { + return diagnostics; + } + public DiagnosticSink report(final Diagnostic diagnostic) { Objects.requireNonNull(diagnostic); if (diagnostic.getSeverity().isError()) { @@ -26,7 +32,7 @@ public class DiagnosticSink extends ReadOnlyCollection { } else { warningCount++; } - collection.add(diagnostic); + diagnostics.add(diagnostic); return this; } diff --git a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/source/tables/FileTable.java b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/source/tables/FileTable.java index e0b35a29..b6949a9b 100644 --- a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/source/tables/FileTable.java +++ b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/source/tables/FileTable.java @@ -6,9 +6,9 @@ import p.studio.compiler.source.identifiers.FileId; import p.studio.compiler.source.identifiers.ProjectId; import p.studio.utilities.structures.ReadOnlyList; +import java.util.ArrayList; import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; +import java.util.List; public class FileTable extends InternTable implements FileTableReader { @@ -23,20 +23,20 @@ public class FileTable extends InternTable implements File @Override public FileId register(final SourceHandle value) { final var fileId = super.register(value); - projectFiles[value.getProjectId().getIndex()].set.add(fileId); + projectFiles[value.getProjectId().getIndex()].collection.add(fileId); return fileId; } @Override public ReadOnlyList getFiles(final ProjectId projectId) { final var fileIds = projectFiles[projectId.getIndex()]; - if (CollectionUtils.isEmpty(fileIds.set)) { + if (CollectionUtils.isEmpty(fileIds.collection)) { return ReadOnlyList.empty(); } - return ReadOnlyList.wrap(fileIds.set); + return ReadOnlyList.wrap(fileIds.collection); } private static class FileIds { - public final Set set = new HashSet<>(); + public final List collection = new ArrayList<>(); } } \ No newline at end of file diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRBackend.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRBackend.java index 846bfdcd..903758f5 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRBackend.java +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRBackend.java @@ -1,19 +1,47 @@ package p.studio.compiler.models; +import lombok.Builder; +import lombok.Getter; import p.studio.utilities.structures.ReadOnlyList; +@Builder +@Getter public class IRBackend { - private final ReadOnlyList functions; + @Builder.Default + private final ReadOnlyList functions = ReadOnlyList.empty(); - public IRBackend() { - this(ReadOnlyList.empty()); + /** + * Returns a stable human-readable representation useful for debug logs and tests. + */ + @Override + public String toString() { + final var sb = new StringBuilder(); + sb.append("IRBackend{functions=").append(functions.size()).append('}'); + + if (functions.isEmpty()) { + return sb.toString(); + } + + sb.append(System.lineSeparator()); + for (final var function : functions) { + sb.append(" - fn ") + .append(function.name()) + .append('(') + .append(function.parameterCount()) + .append(" params)") + .append(" returnType=") + .append(function.hasExplicitReturnType() ? "explicit" : "inferred") + .append(" fileId=") + .append(function.fileId()) + .append(" span=[") + .append(function.span().getStart()) + .append(',') + .append(function.span().getEnd()) + .append(']') + .append(System.lineSeparator()); + } + + return sb.toString().trim(); } - public IRBackend(final ReadOnlyList functions) { - this.functions = functions == null ? ReadOnlyList.empty() : functions; - } - - public ReadOnlyList getFunctions() { - return functions; - } } diff --git a/prometeu-infra/src/main/java/p/studio/utilities/structures/MutableList.java b/prometeu-infra/src/main/java/p/studio/utilities/structures/MutableList.java new file mode 100644 index 00000000..16a09d3f --- /dev/null +++ b/prometeu-infra/src/main/java/p/studio/utilities/structures/MutableList.java @@ -0,0 +1,240 @@ +package p.studio.utilities.structures; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.ListIterator; +import java.util.Objects; +import java.util.stream.Stream; + +public interface MutableList extends ReadOnlyCollection, List { + static MutableList create() { + return wrapInternal(new ArrayList<>()); + } + + static MutableList withCapacity(final int initialCapacity) { + return wrapInternal(new ArrayList<>(initialCapacity)); + } + + static MutableList wrap(final Collection c) { + if (c == null || c.isEmpty()) { + return MutableList.create(); + } + return wrapInternal(new ArrayList<>(c)); + } + + @SafeVarargs + static MutableList from(final T... values) { + if (values == null || values.length == 0) { + return MutableList.create(); + } + return MutableList.wrap(Stream.of(values).toList()); + } + + private static MutableList wrapInternal(final List list) { + return new MutableList<>() { + @Override + public List asList() { + return list; + } + + @Override + public boolean equals(final Object o) { + if (o == this) return true; + if (o instanceof MutableList other) { + return elementsEqual(other); + } + if (o instanceof ReadOnlyList otherReadOnly) { + return elementsEqual(otherReadOnly); + } + return false; + } + + @Override + public int hashCode() { + return list.hashCode(); + } + }; + } + + List asList(); + + @Override + default Collection asCollection() { + return asList(); + } + + @Override + default int size() { + return asList().size(); + } + + @Override + default boolean isEmpty() { + return asList().isEmpty(); + } + + @Override + default boolean contains(final Object o) { + return asList().contains(o); + } + + @Override + default java.util.Iterator iterator() { + return asList().iterator(); + } + + @Override + default java.util.Spliterator spliterator() { + return asList().spliterator(); + } + + @Override + default Object[] toArray() { + return asList().toArray(); + } + + @Override + default T1[] toArray(final T1[] a) { + return asList().toArray(a); + } + + @Override + default boolean add(final T t) { + return asList().add(t); + } + + @Override + default boolean remove(final Object o) { + return asList().remove(o); + } + + @Override + default boolean containsAll(final Collection c) { + return asList().containsAll(c); + } + + @Override + default boolean addAll(final Collection c) { + return asList().addAll(c); + } + + @Override + default boolean addAll(final int index, final Collection c) { + return asList().addAll(index, c); + } + + @Override + default boolean removeAll(final Collection c) { + return asList().removeAll(c); + } + + @Override + default boolean retainAll(final Collection c) { + return asList().retainAll(c); + } + + @Override + default void clear() { + asList().clear(); + } + + @Override + default T get(final int index) { + return asList().get(index); + } + + @Override + default T set(final int index, final T element) { + return asList().set(index, element); + } + + @Override + default void add(final int index, final T element) { + asList().add(index, element); + } + + @Override + default T remove(final int index) { + return asList().remove(index); + } + + @Override + default int indexOf(final Object o) { + return asList().indexOf(o); + } + + @Override + default int lastIndexOf(final Object o) { + return asList().lastIndexOf(o); + } + + @Override + default ListIterator listIterator() { + return asList().listIterator(); + } + + @Override + default ListIterator listIterator(final int index) { + return asList().listIterator(index); + } + + @Override + default List subList(final int fromIndex, final int toIndex) { + return asList().subList(fromIndex, toIndex); + } + + @Override + default Stream stream() { + return asList().stream(); + } + + default T getFirst() { + return asList().getFirst(); + } + + default T getLast() { + return asList().getLast(); + } + + default MutableList mutableSubList(final int fromIndex, final int toIndex) { + return MutableList.wrap(subList(fromIndex, toIndex)); + } + + default ReadOnlyList toReadOnlyList() { + return ReadOnlyList.wrap(asList()); + } + + default ReadOnlyList immutableSubList(final int fromIndex, final int toIndex) { + return ReadOnlyList.wrap(subList(fromIndex, toIndex)); + } + + default ReadOnlyList immutableSort(final java.util.Comparator c) { + if (c == null) { + return ReadOnlyList.wrap(asList().stream().sorted().toList()); + } + return ReadOnlyList.wrap(asList().stream().sorted(c).toList()); + } + + default ReadOnlyList invert() { + final var inverted = new ArrayList<>(asList()); + java.util.Collections.reverse(inverted); + return ReadOnlyList.wrap(inverted); + } + + default boolean elementsEqual(final ReadOnlyList other) { + if (other == null || other.size() != size()) { + return false; + } + for (int i = 0; i < size(); i++) { + if (!Objects.equals(get(i), other.get(i))) { + return false; + } + } + return true; + } + + default boolean elementsEqual(final MutableList other) { + return elementsEqual(other == null ? null : other.toReadOnlyList()); + } +} diff --git a/prometeu-infra/src/main/java/p/studio/utilities/structures/ReadOnlyCollection.java b/prometeu-infra/src/main/java/p/studio/utilities/structures/ReadOnlyCollection.java index 169a0758..b0699d23 100644 --- a/prometeu-infra/src/main/java/p/studio/utilities/structures/ReadOnlyCollection.java +++ b/prometeu-infra/src/main/java/p/studio/utilities/structures/ReadOnlyCollection.java @@ -1,94 +1,115 @@ package p.studio.utilities.structures; -import java.util.*; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Spliterator; import java.util.function.Function; import java.util.function.Supplier; -import java.util.stream.Collectors; import java.util.stream.Stream; -public class ReadOnlyCollection implements Iterable { - protected final Collection collection; - - protected ReadOnlyCollection(final Collection collection) { - this.collection = collection; +public interface ReadOnlyCollection extends Iterable { + static ReadOnlyCollection empty() { + return wrap(List.of()); } - public static ReadOnlyCollection empty() { - return ReadOnlyCollection.wrap(List.of()); - } - - public static ReadOnlyCollection wrap(final Collection collection) { - return new ReadOnlyCollection<>(collection); + static ReadOnlyCollection wrap(final Collection collection) { + if (collection == null || collection.isEmpty()) { + return wrapInternal(List.of()); + } + return wrapInternal(List.copyOf(collection)); } @SafeVarargs - public static ReadOnlyCollection with(final T... ts) { - if (ts == null) return ReadOnlyCollection.empty(); - if (ts.length == 0) return ReadOnlyCollection.empty(); - return new ReadOnlyCollection<>(Stream.of(ts).toList()); + static ReadOnlyCollection with(final T... values) { + if (values == null || values.length == 0) { + return ReadOnlyCollection.empty(); + } + return ReadOnlyCollection.wrap(Stream.of(values).toList()); } - public static boolean isEmpty(final ReadOnlyCollection c) { - if (c == null) return true; - return c.isEmpty(); + static boolean isEmpty(final ReadOnlyCollection c) { + return c == null || c.isEmpty(); } - public static boolean isNotEmpty(final ReadOnlyCollection c) { + static boolean isNotEmpty(final ReadOnlyCollection c) { return !isEmpty(c); } - public int size() { - return collection.size(); + private static ReadOnlyCollection wrapInternal(final Collection collection) { + return new ReadOnlyCollection<>() { + @Override + public Collection asCollection() { + return collection; + } + + @Override + public boolean equals(final Object o) { + if (o == this) return true; + if (o instanceof ReadOnlyCollection other) { + return asCollection().equals(other.asCollection()); + } + return false; + } + + @Override + public int hashCode() { + return collection.hashCode(); + } + }; } - public boolean isEmpty() { - return collection.isEmpty(); + Collection asCollection(); + + default int size() { + return asCollection().size(); } - public boolean contains(final T o) { - return collection.contains(o); + default boolean isEmpty() { + return asCollection().isEmpty(); } - public boolean containsAll(final ReadOnlyCollection c) { - return c.stream().allMatch(collection::contains); + default boolean contains(final Object o) { + return asCollection().contains(o); } - public boolean containsAny(final ReadOnlyCollection c) { - return c.stream().anyMatch(collection::contains); + default boolean containsAll(final ReadOnlyCollection c) { + if (ReadOnlyCollection.isEmpty(c)) { + return true; + } + return c.stream().allMatch(this::contains); } - public Iterator iterator() { - return collection.iterator(); + default boolean containsAny(final ReadOnlyCollection c) { + if (ReadOnlyCollection.isEmpty(c)) { + return false; + } + return c.stream().anyMatch(this::contains); } - public Stream stream() { - return collection.stream(); + @Override + default Iterator iterator() { + return asCollection().iterator(); } - public Stream map(final Function mapper) { + @Override + default Spliterator spliterator() { + return asCollection().spliterator(); + } + + default Stream stream() { + return asCollection().stream(); + } + + default Stream map(final Function mapper) { return stream().map(mapper); } - public T[] toArray(final Supplier factory) { - return collection.toArray(factory.get()); + default T[] toArray(final Supplier factory) { + return asCollection().toArray(factory.get()); } - @Override - public boolean equals(final Object o) { - if (o == this) return true; - if (o == null) return false; - if (o instanceof ReadOnlyCollection c) { - return c.collection.equals(collection); - } - return false; - } - - @Override - public int hashCode() { - return collection.hashCode(); - } - - public Spliterator spliterator() { - return collection.spliterator(); + default MutableList toMutableList() { + return MutableList.wrap(asCollection()); } } diff --git a/prometeu-infra/src/main/java/p/studio/utilities/structures/ReadOnlyList.java b/prometeu-infra/src/main/java/p/studio/utilities/structures/ReadOnlyList.java index 97a55eec..ecfa30e8 100644 --- a/prometeu-infra/src/main/java/p/studio/utilities/structures/ReadOnlyList.java +++ b/prometeu-infra/src/main/java/p/studio/utilities/structures/ReadOnlyList.java @@ -1,92 +1,126 @@ package p.studio.utilities.structures; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.ListIterator; +import java.util.Objects; import java.util.stream.Stream; -public class ReadOnlyList extends ReadOnlyCollection { - private ReadOnlyList(final List list) { - super(list); +public interface ReadOnlyList extends ReadOnlyCollection { + static ReadOnlyList empty() { + return wrapInternal(List.of()); } - public static ReadOnlyList empty() { - return ReadOnlyList.wrap(List.of()); - } - - public static ReadOnlyList wrap(final List list) { - return new ReadOnlyList<>(list); - } - - public static ReadOnlyList wrap(final ReadOnlyCollection c) { - if (ReadOnlyCollection.isEmpty(c)) return ReadOnlyList.empty(); - return ReadOnlyList.wrap(new ArrayList<>(c.collection)); - } - - public static ReadOnlyList wrap(final Set set) { - return new ReadOnlyList<>(List.copyOf(set)); + static ReadOnlyList wrap(final Collection c) { + if (c == null || c.isEmpty()) { + return ReadOnlyList.empty(); + } + return wrapInternal(List.copyOf(c)); } @SafeVarargs - public static ReadOnlyList from(final T... values) { - if (values == null) return ReadOnlyList.empty(); - if (values.length == 0) return ReadOnlyList.empty(); + static ReadOnlyList from(final T... values) { + if (values == null || values.length == 0) { + return ReadOnlyList.empty(); + } return ReadOnlyList.wrap(Stream.of(values).toList()); } + private static ReadOnlyList wrapInternal(final List list) { + return new ReadOnlyList<>() { + @Override + public List asList() { + return list; + } + + @Override + public boolean equals(final Object o) { + if (o == this) return true; + if (o instanceof ReadOnlyList other) { + return elementsEqual(other); + } + return false; + } + + @Override + public int hashCode() { + return list.hashCode(); + } + }; + } + + List asList(); + @Override - public boolean equals(final Object o) { - if (o == this) return true; - if (o == null) return false; - if (o instanceof ReadOnlyList c) { - return c.collection.equals(collection); + default Collection asCollection() { + return asList(); + } + + default T get(final int index) { + return asList().get(index); + } + + default int indexOf(final Object o) { + return asList().indexOf(o); + } + + default int lastIndexOf(final Object o) { + return asList().lastIndexOf(o); + } + + default ListIterator listIterator() { + return asList().listIterator(); + } + + default ListIterator listIterator(final int index) { + return asList().listIterator(index); + } + + default ReadOnlyList immutableSubList(final int fromIndex, final int toIndex) { + return ReadOnlyList.wrap(asList().subList(fromIndex, toIndex)); + } + + default T getFirst() { + return asList().getFirst(); + } + + default T getLast() { + return asList().getLast(); + } + + default ReadOnlyList immutableSort(final Comparator c) { + if (c == null) { + return ReadOnlyList.wrap(asList().stream().sorted().toList()); } - return false; + return ReadOnlyList.wrap(asList().stream().sorted(c).toList()); } - public T get(final int index) { - return ((List)collection).get(index); - } - - public int indexOf(final T o) { - return ((List)collection).indexOf(o); - } - - public int lastIndexOf(final T o) { - return ((List)collection).lastIndexOf(o); - } - - public ListIterator listIterator() { - return ((List)collection).listIterator(); - } - - public ListIterator listIterator(final int index) { - return ((List)collection).listIterator(index); - } - - public ReadOnlyList subList(final int fromIndex, final int toIndex) { - return ReadOnlyList.wrap(((List)collection).subList(fromIndex, toIndex)); - } - - public T getFirst() { - return ((List)collection).getFirst(); - } - - public T getLast() { - return ((List)collection).getLast(); - } - - public ReadOnlyList sort(final Comparator c) { - Stream s = collection.stream(); - if (Objects.isNull(c)) { - s = s.sorted(); - } else { - s = s.sorted(c); - } - return ReadOnlyList.wrap(s.toList()); - } - - public ReadOnlyList invert() { - final var inverted = new ArrayList<>(collection); + default ReadOnlyList invert() { + final var inverted = new ArrayList<>(asList()); java.util.Collections.reverse(inverted); return ReadOnlyList.wrap(inverted); } + + default Stream stream() { + return asList().stream(); + } + + @Override + default MutableList toMutableList() { + return MutableList.wrap(asList()); + } + + default boolean elementsEqual(final ReadOnlyList other) { + if (other == null || other.size() != size()) { + return false; + } + for (int i = 0; i < size(); i++) { + if (!Objects.equals(get(i), other.get(i))) { + return false; + } + } + return true; + } } diff --git a/prometeu-infra/src/main/java/p/studio/utilities/structures/ReadOnlySet.java b/prometeu-infra/src/main/java/p/studio/utilities/structures/ReadOnlySet.java index a4e2ccb5..7b8fdba6 100644 --- a/prometeu-infra/src/main/java/p/studio/utilities/structures/ReadOnlySet.java +++ b/prometeu-infra/src/main/java/p/studio/utilities/structures/ReadOnlySet.java @@ -1,36 +1,69 @@ package p.studio.utilities.structures; +import java.util.Collection; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; -public class ReadOnlySet extends ReadOnlyCollection { - protected ReadOnlySet(final Set set) { - super(set); +public interface ReadOnlySet extends ReadOnlyCollection { + static ReadOnlySet empty() { + return wrapInternal(Set.of()); } - public static ReadOnlySet empty() { - return ReadOnlySet.wrap(Set.of()); + static ReadOnlySet wrap(final Set set) { + if (set == null || set.isEmpty()) { + return ReadOnlySet.empty(); + } + return wrapInternal(Set.copyOf(set)); } - public static ReadOnlySet wrap(final Set set) { - return new ReadOnlySet<>(set); + static ReadOnlySet wrap(final Collection collection) { + if (collection == null || collection.isEmpty()) { + return ReadOnlySet.empty(); + } + return wrapInternal(Set.copyOf(collection)); } @SafeVarargs - public static ReadOnlySet from(final T... values) { - if (values == null) return ReadOnlySet.empty(); - if (values.length == 0) return ReadOnlySet.empty(); + static ReadOnlySet from(final T... values) { + if (values == null || values.length == 0) { + return ReadOnlySet.empty(); + } return ReadOnlySet.wrap(Stream.of(values).collect(Collectors.toSet())); } + private static ReadOnlySet wrapInternal(final Set set) { + return new ReadOnlySet<>() { + @Override + public Set asSet() { + return set; + } + + @Override + public boolean equals(final Object o) { + if (o == this) return true; + if (o instanceof ReadOnlySet other) { + return asSet().equals(other.asSet()); + } + return false; + } + + @Override + public int hashCode() { + return set.hashCode(); + } + }; + } + + Set asSet(); + @Override - public boolean equals(final Object o) { - if (o == this) return true; - if (o == null) return false; - if (o instanceof ReadOnlySet c) { - return c.collection.equals(collection); - } - return false; + default Collection asCollection() { + return asSet(); + } + + @Override + default Stream stream() { + return asSet().stream(); } } diff --git a/test-projects/main/src/module-a/source-1.pbs b/test-projects/main/src/module-a/source-1.pbs index fde78521..63671dea 100644 --- a/test-projects/main/src/module-a/source-1.pbs +++ b/test-projects/main/src/module-a/source-1.pbs @@ -1 +1,3 @@ -source-1.pbs content \ No newline at end of file +fn a1() +{ +} \ No newline at end of file diff --git a/test-projects/main/src/module-a/source-2.pbs b/test-projects/main/src/module-a/source-2.pbs index 0c9c3dc4..abba8a5b 100644 --- a/test-projects/main/src/module-a/source-2.pbs +++ b/test-projects/main/src/module-a/source-2.pbs @@ -1 +1,4 @@ -source-2.pbs content \ No newline at end of file +fn a2(v1: int, v2: bounded): int +{ + return v1 + v2; +} \ No newline at end of file diff --git a/test-projects/main/src/module-a/source-3.pbs b/test-projects/main/src/module-a/source-3.pbs index 324efdbc..956ab5a4 100644 --- a/test-projects/main/src/module-a/source-3.pbs +++ b/test-projects/main/src/module-a/source-3.pbs @@ -1 +1,4 @@ -source-3.pbs content \ No newline at end of file +fn a3(): int +{ + return 1; +} \ No newline at end of file