added lexer and parser basics

This commit is contained in:
bQUARKz 2026-02-27 09:40:31 +00:00
parent 833c6ec049
commit d55ec48707
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
21 changed files with 1057 additions and 581 deletions

View File

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

View File

@ -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.
*
* <p>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.
*
* <p>Accepted: {@code a == b}
* <p>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.
*
* <p>Accepted: {@code a < b}
* <p>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.
*
* <p>Examples:
* <pre>{@code
* f()
* sum(a, b)
* factory()(1)
* }</pre>
*/
private PbsAst.Expression parseCall() {
var expression = parsePrimary();
while (cursor.match(PbsTokenKind.LEFT_PAREN)) {
final var open = cursor.previous();
final var arguments = new ArrayList<PbsAst.Expression>();
if (!cursor.check(PbsTokenKind.RIGHT_PAREN)) {
do {
arguments.add(parseExpression());
} while (cursor.match(PbsTokenKind.COMMA));
}
final var close = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after 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.
*
* <p>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();
}
}

View File

@ -10,20 +10,25 @@ import p.studio.utilities.structures.ReadOnlyList;
import java.util.ArrayList;
/**
* High-level manual parser for PBS source files.
*
* <p>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<PbsToken> tokens;
private final PbsTokenCursor cursor;
private final PbsExprParser exprParser;
private final FileId fileId;
private final DiagnosticSink diagnostics;
private int current;
private PbsParser(
final ReadOnlyList<PbsToken> 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<PbsAst.FunctionDecl>();
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 {
* }</pre>
*/
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<PbsAst.Parameter>();
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<PbsAst.Statement>();
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.
*
* <p>The current slice supports `let`, `return`, and expression statements.
* <p>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 {
* <p>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.
*
* <p>Accepted: {@code a == b}
* <p>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.
*
* <p>Accepted: {@code a < b}
* <p>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.
*
* <p>Examples:
* <pre>{@code
* f()
* sum(a, b)
* factory()(1)
* }</pre>
*/
private PbsAst.Expression parseCall() {
var expression = parsePrimary();
while (match(PbsTokenKind.LEFT_PAREN)) {
final var open = previous();
final var arguments = new ArrayList<PbsAst.Expression>();
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.
*
* <p>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.
*
* <p>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();
}
}

View File

@ -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.
*
* <p>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<PbsToken> tokens = new ArrayList<>();
private int current;
PbsTokenCursor(final ReadOnlyList<PbsToken> 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));
}
}

View File

@ -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<IRFunction>();
final MutableList<IRFunction> 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(

View File

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

View File

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

View File

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

View File

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

View File

@ -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<BuildingIssue> {
public class BuildingIssueSink implements ReadOnlyCollection<BuildingIssue> {
private final ArrayList<BuildingIssue> issues = new ArrayList<>();
private boolean hasErrors = false;
protected BuildingIssueSink() {
super(new ArrayList<>());
}
public static BuildingIssueSink empty() {
return new BuildingIssueSink();
}
@Override
public Collection<BuildingIssue> asCollection() {
return issues;
}
public BuildingIssueSink report(final Consumer<BuildingIssue.BuildingIssueBuilder> 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;
}

View File

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

View File

@ -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<Diagnostic> {
public class DiagnosticSink implements ReadOnlyCollection<Diagnostic> {
private final ArrayList<Diagnostic> 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<Diagnostic> 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<Diagnostic> {
} else {
warningCount++;
}
collection.add(diagnostic);
diagnostics.add(diagnostic);
return this;
}

View File

@ -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<FileId, SourceHandle> implements FileTableReader {
@ -23,20 +23,20 @@ public class FileTable extends InternTable<FileId, SourceHandle> 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<FileId> 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<FileId> set = new HashSet<>();
public final List<FileId> collection = new ArrayList<>();
}
}

View File

@ -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<IRFunction> functions;
@Builder.Default
private final ReadOnlyList<IRFunction> 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<IRFunction> functions) {
this.functions = functions == null ? ReadOnlyList.empty() : functions;
}
public ReadOnlyList<IRFunction> getFunctions() {
return functions;
}
}

View File

@ -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<T> extends ReadOnlyCollection<T>, List<T> {
static <T> MutableList<T> create() {
return wrapInternal(new ArrayList<>());
}
static <T> MutableList<T> withCapacity(final int initialCapacity) {
return wrapInternal(new ArrayList<>(initialCapacity));
}
static <T> MutableList<T> wrap(final Collection<T> c) {
if (c == null || c.isEmpty()) {
return MutableList.create();
}
return wrapInternal(new ArrayList<>(c));
}
@SafeVarargs
static <T> MutableList<T> from(final T... values) {
if (values == null || values.length == 0) {
return MutableList.create();
}
return MutableList.wrap(Stream.of(values).toList());
}
private static <T> MutableList<T> wrapInternal(final List<T> list) {
return new MutableList<>() {
@Override
public List<T> 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<T> asList();
@Override
default Collection<T> 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<T> iterator() {
return asList().iterator();
}
@Override
default java.util.Spliterator<T> spliterator() {
return asList().spliterator();
}
@Override
default Object[] toArray() {
return asList().toArray();
}
@Override
default <T1> 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<? extends T> c) {
return asList().addAll(c);
}
@Override
default boolean addAll(final int index, final Collection<? extends T> 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<T> listIterator() {
return asList().listIterator();
}
@Override
default ListIterator<T> listIterator(final int index) {
return asList().listIterator(index);
}
@Override
default List<T> subList(final int fromIndex, final int toIndex) {
return asList().subList(fromIndex, toIndex);
}
@Override
default Stream<T> stream() {
return asList().stream();
}
default T getFirst() {
return asList().getFirst();
}
default T getLast() {
return asList().getLast();
}
default MutableList<T> mutableSubList(final int fromIndex, final int toIndex) {
return MutableList.wrap(subList(fromIndex, toIndex));
}
default ReadOnlyList<T> toReadOnlyList() {
return ReadOnlyList.wrap(asList());
}
default ReadOnlyList<T> immutableSubList(final int fromIndex, final int toIndex) {
return ReadOnlyList.wrap(subList(fromIndex, toIndex));
}
default ReadOnlyList<T> immutableSort(final java.util.Comparator<? super T> c) {
if (c == null) {
return ReadOnlyList.wrap(asList().stream().sorted().toList());
}
return ReadOnlyList.wrap(asList().stream().sorted(c).toList());
}
default ReadOnlyList<T> 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());
}
}

View File

@ -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<T> implements Iterable<T> {
protected final Collection<T> collection;
protected ReadOnlyCollection(final Collection<T> collection) {
this.collection = collection;
public interface ReadOnlyCollection<T> extends Iterable<T> {
static <T> ReadOnlyCollection<T> empty() {
return wrap(List.of());
}
public static <T> ReadOnlyCollection<T> empty() {
return ReadOnlyCollection.wrap(List.of());
}
public static <T> ReadOnlyCollection<T> wrap(final Collection<T> collection) {
return new ReadOnlyCollection<>(collection);
static <T> ReadOnlyCollection<T> wrap(final Collection<T> collection) {
if (collection == null || collection.isEmpty()) {
return wrapInternal(List.of());
}
return wrapInternal(List.copyOf(collection));
}
@SafeVarargs
public static <T> ReadOnlyCollection<T> 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 <T> ReadOnlyCollection<T> with(final T... values) {
if (values == null || values.length == 0) {
return ReadOnlyCollection.empty();
}
return ReadOnlyCollection.wrap(Stream.of(values).toList());
}
public static <T> boolean isEmpty(final ReadOnlyCollection<T> c) {
if (c == null) return true;
return c.isEmpty();
static <T> boolean isEmpty(final ReadOnlyCollection<T> c) {
return c == null || c.isEmpty();
}
public static <T> boolean isNotEmpty(final ReadOnlyCollection<T> c) {
static <T> boolean isNotEmpty(final ReadOnlyCollection<T> c) {
return !isEmpty(c);
}
public int size() {
return collection.size();
private static <T> ReadOnlyCollection<T> wrapInternal(final Collection<T> collection) {
return new ReadOnlyCollection<>() {
@Override
public Collection<T> 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<T> 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<T> c) {
return c.stream().allMatch(collection::contains);
default boolean contains(final Object o) {
return asCollection().contains(o);
}
public boolean containsAny(final ReadOnlyCollection<T> c) {
return c.stream().anyMatch(collection::contains);
default boolean containsAll(final ReadOnlyCollection<T> c) {
if (ReadOnlyCollection.isEmpty(c)) {
return true;
}
return c.stream().allMatch(this::contains);
}
public Iterator<T> iterator() {
return collection.iterator();
default boolean containsAny(final ReadOnlyCollection<T> c) {
if (ReadOnlyCollection.isEmpty(c)) {
return false;
}
return c.stream().anyMatch(this::contains);
}
public Stream<T> stream() {
return collection.stream();
@Override
default Iterator<T> iterator() {
return asCollection().iterator();
}
public <R> Stream<R> map(final Function<T, R> mapper) {
@Override
default Spliterator<T> spliterator() {
return asCollection().spliterator();
}
default Stream<T> stream() {
return asCollection().stream();
}
default <R> Stream<R> map(final Function<T, R> mapper) {
return stream().map(mapper);
}
public T[] toArray(final Supplier<T[]> factory) {
return collection.toArray(factory.get());
default T[] toArray(final Supplier<T[]> 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<T> spliterator() {
return collection.spliterator();
default MutableList<T> toMutableList() {
return MutableList.wrap(asCollection());
}
}

View File

@ -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<T> extends ReadOnlyCollection<T> {
private ReadOnlyList(final List<T> list) {
super(list);
public interface ReadOnlyList<T> extends ReadOnlyCollection<T> {
static <T> ReadOnlyList<T> empty() {
return wrapInternal(List.of());
}
public static <T> ReadOnlyList<T> empty() {
return ReadOnlyList.wrap(List.of());
}
public static <T> ReadOnlyList<T> wrap(final List<T> list) {
return new ReadOnlyList<>(list);
}
public static <T> ReadOnlyList<T> wrap(final ReadOnlyCollection<T> c) {
if (ReadOnlyCollection.isEmpty(c)) return ReadOnlyList.empty();
return ReadOnlyList.wrap(new ArrayList<>(c.collection));
}
public static <T> ReadOnlyList<T> wrap(final Set<T> set) {
return new ReadOnlyList<>(List.copyOf(set));
static <T> ReadOnlyList<T> wrap(final Collection<T> c) {
if (c == null || c.isEmpty()) {
return ReadOnlyList.empty();
}
return wrapInternal(List.copyOf(c));
}
@SafeVarargs
public static <T> ReadOnlyList<T> from(final T... values) {
if (values == null) return ReadOnlyList.empty();
if (values.length == 0) return ReadOnlyList.empty();
static <T> ReadOnlyList<T> from(final T... values) {
if (values == null || values.length == 0) {
return ReadOnlyList.empty();
}
return ReadOnlyList.wrap(Stream.of(values).toList());
}
private static <T> ReadOnlyList<T> wrapInternal(final List<T> list) {
return new ReadOnlyList<>() {
@Override
public List<T> 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<T> 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<T> 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<T> listIterator() {
return asList().listIterator();
}
default ListIterator<T> listIterator(final int index) {
return asList().listIterator(index);
}
default ReadOnlyList<T> 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<T> immutableSort(final Comparator<? super T> 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<T>)collection).get(index);
}
public int indexOf(final T o) {
return ((List<T>)collection).indexOf(o);
}
public int lastIndexOf(final T o) {
return ((List<T>)collection).lastIndexOf(o);
}
public ListIterator<T> listIterator() {
return ((List<T>)collection).listIterator();
}
public ListIterator<T> listIterator(final int index) {
return ((List<T>)collection).listIterator(index);
}
public ReadOnlyList<T> subList(final int fromIndex, final int toIndex) {
return ReadOnlyList.wrap(((List<T>)collection).subList(fromIndex, toIndex));
}
public T getFirst() {
return ((List<T>)collection).getFirst();
}
public T getLast() {
return ((List<T>)collection).getLast();
}
public ReadOnlyList<T> sort(final Comparator<? super T> c) {
Stream<T> s = collection.stream();
if (Objects.isNull(c)) {
s = s.sorted();
} else {
s = s.sorted(c);
}
return ReadOnlyList.wrap(s.toList());
}
public ReadOnlyList<T> invert() {
final var inverted = new ArrayList<>(collection);
default ReadOnlyList<T> invert() {
final var inverted = new ArrayList<>(asList());
java.util.Collections.reverse(inverted);
return ReadOnlyList.wrap(inverted);
}
default Stream<T> stream() {
return asList().stream();
}
@Override
default MutableList<T> 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;
}
}

View File

@ -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<T> extends ReadOnlyCollection<T> {
protected ReadOnlySet(final Set<T> set) {
super(set);
public interface ReadOnlySet<T> extends ReadOnlyCollection<T> {
static <T> ReadOnlySet<T> empty() {
return wrapInternal(Set.of());
}
public static <T> ReadOnlySet<T> empty() {
return ReadOnlySet.wrap(Set.of());
static <T> ReadOnlySet<T> wrap(final Set<T> set) {
if (set == null || set.isEmpty()) {
return ReadOnlySet.empty();
}
return wrapInternal(Set.copyOf(set));
}
public static <T> ReadOnlySet<T> wrap(final Set<T> set) {
return new ReadOnlySet<>(set);
static <T> ReadOnlySet<T> wrap(final Collection<T> collection) {
if (collection == null || collection.isEmpty()) {
return ReadOnlySet.empty();
}
return wrapInternal(Set.copyOf(collection));
}
@SafeVarargs
public static <T> ReadOnlySet<T> from(final T... values) {
if (values == null) return ReadOnlySet.empty();
if (values.length == 0) return ReadOnlySet.empty();
static <T> ReadOnlySet<T> from(final T... values) {
if (values == null || values.length == 0) {
return ReadOnlySet.empty();
}
return ReadOnlySet.wrap(Stream.of(values).collect(Collectors.toSet()));
}
private static <T> ReadOnlySet<T> wrapInternal(final Set<T> set) {
return new ReadOnlySet<>() {
@Override
public Set<T> 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<T> 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<T> asCollection() {
return asSet();
}
@Override
default Stream<T> stream() {
return asSet().stream();
}
}

View File

@ -1 +1,3 @@
source-1.pbs content
fn a1()
{
}

View File

@ -1 +1,4 @@
source-2.pbs content
fn a2(v1: int, v2: bounded): int
{
return v1 + v2;
}

View File

@ -1 +1,4 @@
source-3.pbs content
fn a3(): int
{
return 1;
}