implements PR002

This commit is contained in:
bQUARKz 2026-03-05 10:08:39 +00:00
parent 04f65f46b3
commit 9ddfa19bf2
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
8 changed files with 557 additions and 121 deletions

View File

@ -1,38 +0,0 @@
# PR-001 - PBS Lexer Core Syntax Alignment
## Briefing
O lexer atual cobre apenas uma parte pequena da superficie definida em `3. Core Syntax Specification.md`.
Este PR fecha o contrato lexico minimo de v1 para que parser, semantica e diagnosticos trabalhem sobre um conjunto estavel de tokens.
## Target
- Specs:
- `docs/pbs/specs/3. Core Syntax Specification.md` (secoes 4.1, 4.2, 4.4, 4.5, 10)
- `docs/pbs/specs/11. AST Specification.md` (secao 7: atribuicao estavel)
- Codigo:
- `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/lexer/PbsTokenKind.java`
- `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/lexer/PbsLexer.java`
- `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/lexer/LexErrors.java`
## Method
1. Expandir `PbsTokenKind` para todos os keywords ativos de v1 e operadores/pontuacao usados na gramatica.
2. Adicionar tokens para `COMMENT` e para operadores compostos ausentes (`->`, `+=`, `-=`, `*=`, `/=`, `%=`).
3. Suportar aliases lexicos (`and/or/not`) sem quebrar `&&/||/!`.
4. Preservar spans estaveis em todos os tokens emitidos.
5. Manter rejeicao deterministica para caracteres invalidos e strings nao terminadas.
## Acceptance Criteria
- Lexer emite classes obrigatorias: `identifiers`, `keywords`, `literals`, `punctuation`, `operators`, `comments`, `EOF`.
- Keywords reservados nao entram como `IDENTIFIER`.
- `and/or/not` e `&&/||/!` sao tokenizados de forma consistente.
- `->` nao e decomposto em dois tokens (`-` e `>`).
- Comentarios de linha sao representados como token e nao perdem a continuidade do stream.
- Todos os tokens carregam `start/end` corretos no arquivo.
## Tests
- `PbsLexerTest`:
- caso feliz com arquivo contendo imports, declaracoes, controle de fluxo e operadores compostos;
- caso de comentarios em multiplas linhas;
- caso de palavras reservadas vs identificadores parecidos;
- caso de string nao terminada (`E_LEX_UNTERMINATED_STRING`);
- caso de caractere invalido (`E_LEX_INVALID_CHAR`).
- Fixtures de regressao para `->`, `and/or/not` e atribuicoes compostas.

View File

@ -7,6 +7,8 @@ import p.studio.compiler.pbs.lexer.PbsLexer;
import p.studio.compiler.pbs.parser.PbsParser;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.identifiers.FileId;
import p.studio.compiler.source.identifiers.NameId;
import p.studio.compiler.source.tables.NameTable;
import p.studio.utilities.structures.ReadOnlyList;
import java.util.ArrayList;
@ -27,9 +29,11 @@ public final class PbsFrontendCompiler {
private void validateFunctionNames(
final PbsAst.File ast,
final DiagnosticSink diagnostics) {
final Set<String> names = new HashSet<>();
final var nameTable = new NameTable();
final Set<NameId> nameIds = new HashSet<>();
for (final var fn : ast.functions()) {
if (names.add(fn.name())) {
final var nameId = nameTable.register(fn.name());
if (nameIds.add(nameId)) {
continue;
}
diagnostics.error("E_RESOLVE_DUPLICATE_SYMBOL",

View File

@ -3,13 +3,57 @@ package p.studio.compiler.pbs.ast;
import p.studio.compiler.source.Span;
import p.studio.utilities.structures.ReadOnlyList;
import java.util.ArrayList;
public final class PbsAst {
private PbsAst() {
}
public record File(
ReadOnlyList<FunctionDecl> functions,
ReadOnlyList<ImportDecl> imports,
ReadOnlyList<TopDecl> topDecls,
Span span) {
public ReadOnlyList<FunctionDecl> functions() {
final var functions = new ArrayList<FunctionDecl>();
for (final var topDecl : topDecls) {
if (topDecl instanceof FunctionDecl functionDecl) {
functions.add(functionDecl);
}
}
return ReadOnlyList.wrap(functions);
}
}
public record ImportDecl(
ReadOnlyList<ImportItem> items,
ModuleRef moduleRef,
Span span) {
}
public record ImportItem(
String name,
String alias,
Span span) {
}
public record ModuleRef(
String project,
ReadOnlyList<String> pathSegments,
Span span) {
}
public sealed interface TopDecl permits FunctionDecl,
StructDecl,
ContractDecl,
ServiceDecl,
ErrorDecl,
EnumDecl,
CallbackDecl,
ConstDecl,
ImplementsDecl,
InvalidDecl {
Span span();
}
public record FunctionDecl(
@ -18,7 +62,58 @@ public final class PbsAst {
TypeRef returnType,
Expression elseFallback,
Block body,
Span span) {
Span span) implements TopDecl {
}
public record StructDecl(
String name,
Span span) implements TopDecl {
}
public record ContractDecl(
String name,
Span span) implements TopDecl {
}
public record ServiceDecl(
String name,
Span span) implements TopDecl {
}
public record ErrorDecl(
String name,
Span span) implements TopDecl {
}
public record EnumDecl(
String name,
Span span) implements TopDecl {
}
public record CallbackDecl(
String name,
ReadOnlyList<Parameter> parameters,
TypeRef returnType,
Span span) implements TopDecl {
}
public record ConstDecl(
String name,
TypeRef explicitType,
Expression initializer,
Span span) implements TopDecl {
}
public record ImplementsDecl(
String contractName,
String ownerName,
String binderName,
Span span) implements TopDecl {
}
public record InvalidDecl(
String reason,
Span span) implements TopDecl {
}
public record Parameter(
@ -124,4 +219,78 @@ public final class PbsAst {
Expression expression,
Span span) implements Expression {
}
// Barrel declarations are represented explicitly for linking-visible declaration families.
public sealed interface BarrelItem permits BarrelFunctionItem,
BarrelStructItem,
BarrelContractItem,
BarrelHostItem,
BarrelErrorItem,
BarrelEnumItem,
BarrelServiceItem,
BarrelConstItem,
BarrelCallbackItem {
Span span();
}
public enum Visibility {
MOD,
PUB
}
public record BarrelFunctionItem(
Visibility visibility,
String name,
ReadOnlyList<TypeRef> parameterTypes,
TypeRef returnType,
Span span) implements BarrelItem {
}
public record BarrelStructItem(
Visibility visibility,
String name,
Span span) implements BarrelItem {
}
public record BarrelContractItem(
Visibility visibility,
String name,
Span span) implements BarrelItem {
}
public record BarrelHostItem(
Visibility visibility,
String name,
Span span) implements BarrelItem {
}
public record BarrelErrorItem(
Visibility visibility,
String name,
Span span) implements BarrelItem {
}
public record BarrelEnumItem(
Visibility visibility,
String name,
Span span) implements BarrelItem {
}
public record BarrelServiceItem(
Visibility visibility,
String name,
Span span) implements BarrelItem {
}
public record BarrelConstItem(
Visibility visibility,
String name,
Span span) implements BarrelItem {
}
public record BarrelCallbackItem(
Visibility visibility,
String name,
Span span) implements BarrelItem {
}
}

View File

@ -35,13 +35,6 @@ public final class PbsParser {
/**
* Parses a token stream into a PBS file AST.
*
* <p>Example:
* <pre>{@code
* fn sum(a: int, b: int): int {
* return a + b;
* }
* }</pre>
*/
public static PbsAst.File parse(
final ReadOnlyList<PbsToken> tokens,
@ -52,26 +45,37 @@ public final class PbsParser {
/**
* Parses a full file as a sequence of imports and top-level declarations.
*
* <p>The current slice only stores top-level functions in the AST.
*/
private PbsAst.File parseFile() {
final var functions = new ArrayList<PbsAst.FunctionDecl>();
final var imports = new ArrayList<PbsAst.ImportDecl>();
final var topDecls = new ArrayList<PbsAst.TopDecl>();
while (!cursor.isAtEnd()) {
if (cursor.match(PbsTokenKind.IMPORT)) {
parseAndDiscardImport();
imports.add(parseImport(cursor.previous()));
continue;
}
if (cursor.match(PbsTokenKind.FN)) {
functions.add(parseFunction(cursor.previous()));
topDecls.add(parseFunction(cursor.previous()));
continue;
}
if (cursor.match(PbsTokenKind.DECLARE)) {
topDecls.add(parseDeclareTopDecl(cursor.previous()));
continue;
}
if (cursor.match(PbsTokenKind.IMPLEMENTS)) {
topDecls.add(parseImplementsDeclaration(cursor.previous()));
continue;
}
if (cursor.match(PbsTokenKind.MOD, PbsTokenKind.PUB)) {
report(cursor.previous(), ParseErrors.E_PARSE_VISIBILITY_IN_SOURCE,
final var token = cursor.previous();
report(token, ParseErrors.E_PARSE_VISIBILITY_IN_SOURCE,
"Visibility modifiers are barrel-only and cannot appear in .pbs declarations");
topDecls.add(new PbsAst.InvalidDecl("visibility modifier in source", span(token.start(), token.end())));
synchronizeTopLevel();
continue;
}
@ -80,31 +84,37 @@ public final class PbsParser {
break;
}
report(cursor.peek(), ParseErrors.E_PARSE_UNEXPECTED_TOKEN,
"Expected top-level declaration ('fn') or import");
final var token = cursor.peek();
report(token, ParseErrors.E_PARSE_UNEXPECTED_TOKEN,
"Expected top-level declaration ('fn', 'declare', 'implements') or import");
topDecls.add(new PbsAst.InvalidDecl("unexpected top-level token", span(token.start(), token.end())));
synchronizeTopLevel();
}
final var eof = cursor.peek();
return new PbsAst.File(ReadOnlyList.wrap(functions), span(0, eof.end()));
return new PbsAst.File(
ReadOnlyList.wrap(imports),
ReadOnlyList.wrap(topDecls),
span(0, eof.end()));
}
/**
* Parses import syntax for validation and recovery, but does not store imports yet.
*
* <p>Supported forms:
* <pre>{@code
* import @core:math;
* import { Vector, Matrix as Mat } from @core:math;
* }</pre>
* Parses import syntax and stores it in the AST.
*/
private void parseAndDiscardImport() {
private PbsAst.ImportDecl parseImport(final PbsToken importToken) {
final var items = new ArrayList<PbsAst.ImportItem>();
if (cursor.match(PbsTokenKind.LEFT_BRACE)) {
while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) {
if (cursor.match(PbsTokenKind.IDENTIFIER)) {
final var itemName = cursor.previous();
String alias = null;
if (cursor.match(PbsTokenKind.AS)) {
consume(PbsTokenKind.IDENTIFIER, "Expected alias identifier after 'as'");
alias = consume(PbsTokenKind.IDENTIFIER, "Expected alias identifier after 'as'").lexeme();
}
items.add(new PbsAst.ImportItem(
itemName.lexeme(),
alias,
span(itemName.start(), cursor.previous().end())));
cursor.match(PbsTokenKind.COMMA);
continue;
}
@ -115,54 +125,90 @@ public final class PbsParser {
consume(PbsTokenKind.FROM, "Expected 'from' in named import");
}
parseModuleRef();
consume(PbsTokenKind.SEMICOLON, "Expected ';' after import");
final var moduleRef = parseModuleRef();
final var semicolon = consume(PbsTokenKind.SEMICOLON, "Expected ';' after import");
return new PbsAst.ImportDecl(
ReadOnlyList.wrap(items),
moduleRef,
span(importToken.start(), semicolon.end()));
}
/**
* Parses a module reference such as {@code @core:math/tools}.
*/
private void parseModuleRef() {
consume(PbsTokenKind.AT, "Expected '@' in module reference");
consume(PbsTokenKind.IDENTIFIER, "Expected project identifier in module reference");
private PbsAst.ModuleRef parseModuleRef() {
final var at = consume(PbsTokenKind.AT, "Expected '@' in module reference");
final var project = consume(PbsTokenKind.IDENTIFIER, "Expected project identifier in module reference");
consume(PbsTokenKind.COLON, "Expected ':' in module reference");
consume(PbsTokenKind.IDENTIFIER, "Expected module identifier");
final var segments = new ArrayList<String>();
final var firstSegment = consume(PbsTokenKind.IDENTIFIER, "Expected module identifier");
segments.add(firstSegment.lexeme());
var end = firstSegment.end();
while (cursor.match(PbsTokenKind.SLASH)) {
consume(PbsTokenKind.IDENTIFIER, "Expected module path segment after '/'");
final var segment = consume(PbsTokenKind.IDENTIFIER, "Expected module path segment after '/'");
segments.add(segment.lexeme());
end = segment.end();
}
return new PbsAst.ModuleRef(project.lexeme(), ReadOnlyList.wrap(segments), span(at.start(), end));
}
/**
* Parses declarations introduced by 'declare'.
*/
private PbsAst.TopDecl parseDeclareTopDecl(final PbsToken declareToken) {
if (cursor.match(PbsTokenKind.STRUCT)) {
return parseSimpleNamedDecl(declareToken, "struct", PbsAst.StructDecl::new);
}
if (cursor.match(PbsTokenKind.CONTRACT)) {
return parseSimpleNamedDecl(declareToken, "contract", PbsAst.ContractDecl::new);
}
if (cursor.match(PbsTokenKind.SERVICE)) {
return parseSimpleNamedDecl(declareToken, "service", PbsAst.ServiceDecl::new);
}
if (cursor.match(PbsTokenKind.ERROR)) {
return parseSimpleNamedDecl(declareToken, "error", PbsAst.ErrorDecl::new);
}
if (cursor.match(PbsTokenKind.ENUM)) {
return parseSimpleNamedDecl(declareToken, "enum", PbsAst.EnumDecl::new);
}
if (cursor.match(PbsTokenKind.CALLBACK)) {
return parseCallbackDeclaration(declareToken);
}
if (cursor.match(PbsTokenKind.CONST)) {
return parseConstDeclaration(declareToken);
}
if (cursor.match(PbsTokenKind.HOST)) {
final var end = consumeDeclarationTerminator();
report(cursor.previous(), ParseErrors.E_PARSE_UNEXPECTED_TOKEN,
"'declare host' is reserved and not supported in ordinary source modules");
return new PbsAst.InvalidDecl("reserved declare host", span(declareToken.start(), end));
}
if (cursor.match(PbsTokenKind.BUILTIN)) {
cursor.match(PbsTokenKind.TYPE);
final var end = consumeDeclarationTerminator();
report(cursor.previous(), ParseErrors.E_PARSE_UNEXPECTED_TOKEN,
"'declare builtin type' is reserved and not supported in ordinary source modules");
return new PbsAst.InvalidDecl("reserved declare builtin type", span(declareToken.start(), end));
}
report(cursor.peek(), ParseErrors.E_PARSE_UNEXPECTED_TOKEN,
"Expected declaration kind after 'declare'");
synchronizeTopLevel();
final var token = cursor.previous();
return new PbsAst.InvalidDecl("invalid declare form", span(declareToken.start(), token.end()));
}
/**
* Parses a top-level function declaration.
*
* <p>Example:
* <pre>{@code
* fn sum(a: int, b: int): int {
* return a + b;
* }
* }</pre>
*/
private PbsAst.FunctionDecl parseFunction(final PbsToken fnToken) {
final var name = consume(PbsTokenKind.IDENTIFIER, "Expected function name");
consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after function name");
final var parameters = new ArrayList<PbsAst.Parameter>();
if (!cursor.check(PbsTokenKind.RIGHT_PAREN)) {
do {
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(
parameterName.lexeme(),
typeRef,
span(parameterStart.start(), typeRef.span().getEnd())));
} while (cursor.match(PbsTokenKind.COMMA));
}
final var parameters = parseParametersUntilRightParen();
consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after parameter list");
PbsAst.TypeRef returnType = null;
if (cursor.match(PbsTokenKind.COLON)) {
if (cursor.match(PbsTokenKind.COLON, PbsTokenKind.ARROW)) {
returnType = parseTypeRef();
}
@ -181,24 +227,123 @@ public final class PbsParser {
span(fnToken.start(), body.span().getEnd()));
}
/**
* Parses `declare callback` declaration.
*/
private PbsAst.CallbackDecl parseCallbackDeclaration(final PbsToken declareToken) {
final var name = consume(PbsTokenKind.IDENTIFIER, "Expected callback name");
consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after callback name");
final var parameters = parseParametersUntilRightParen();
consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after callback parameter list");
PbsAst.TypeRef returnType = null;
if (cursor.match(PbsTokenKind.COLON, PbsTokenKind.ARROW)) {
returnType = parseTypeRef();
}
final var semicolon = consume(PbsTokenKind.SEMICOLON, "Expected ';' after callback declaration");
return new PbsAst.CallbackDecl(
name.lexeme(),
ReadOnlyList.wrap(parameters),
returnType,
span(declareToken.start(), semicolon.end()));
}
/**
* Parses `declare const` declaration.
*/
private PbsAst.ConstDecl parseConstDeclaration(final PbsToken declareToken) {
final var name = consume(PbsTokenKind.IDENTIFIER, "Expected constant name");
consume(PbsTokenKind.COLON, "Expected ':' after constant name");
final var typeRef = parseTypeRef();
PbsAst.Expression initializer = null;
if (cursor.match(PbsTokenKind.EQUAL)) {
initializer = exprParser.parseExpression();
}
final var semicolon = consume(PbsTokenKind.SEMICOLON, "Expected ';' after constant declaration");
return new PbsAst.ConstDecl(
name.lexeme(),
typeRef,
initializer,
span(declareToken.start(), semicolon.end()));
}
/**
* Parses `implements Contract for Owner using binder { ... }`.
*/
private PbsAst.ImplementsDecl parseImplementsDeclaration(final PbsToken implementsToken) {
final var contractName = consume(PbsTokenKind.IDENTIFIER, "Expected contract name in 'implements'");
consume(PbsTokenKind.FOR, "Expected 'for' in 'implements' declaration");
final var ownerName = consume(PbsTokenKind.IDENTIFIER, "Expected owner name after 'for'");
consume(PbsTokenKind.USING, "Expected 'using' in 'implements' declaration");
final var binderName = consume(PbsTokenKind.IDENTIFIER, "Expected binder name after 'using'");
long end;
if (cursor.check(PbsTokenKind.LEFT_BRACE)) {
end = consumeBalancedBraces(cursor.advance());
} else {
report(cursor.peek(), ParseErrors.E_PARSE_EXPECTED_TOKEN,
"Expected '{' to start 'implements' body");
end = consumeDeclarationTerminator();
}
return new PbsAst.ImplementsDecl(
contractName.lexeme(),
ownerName.lexeme(),
binderName.lexeme(),
span(implementsToken.start(), end));
}
private PbsAst.TopDecl parseSimpleNamedDecl(
final PbsToken declareToken,
final String kind,
final NamedDeclFactory factory) {
final var name = consume(PbsTokenKind.IDENTIFIER, "Expected " + kind + " name");
final var end = consumeDeclarationTerminator();
return factory.build(name.lexeme(), span(declareToken.start(), end));
}
private ArrayList<PbsAst.Parameter> parseParametersUntilRightParen() {
final var parameters = new ArrayList<PbsAst.Parameter>();
if (cursor.check(PbsTokenKind.RIGHT_PAREN)) {
return parameters;
}
do {
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(
parameterName.lexeme(),
typeRef,
span(parameterStart.start(), typeRef.span().getEnd())));
} while (cursor.match(PbsTokenKind.COMMA));
return parameters;
}
/**
* Parses a simple identifier-based type reference such as {@code int} or {@code Vector}.
*/
private PbsAst.TypeRef parseTypeRef() {
final var identifier = consume(PbsTokenKind.IDENTIFIER, "Expected type name");
return new PbsAst.TypeRef(identifier.lexeme(), span(identifier.start(), identifier.end()));
if (cursor.match(PbsTokenKind.IDENTIFIER, PbsTokenKind.SELF)) {
final var identifier = cursor.previous();
return new PbsAst.TypeRef(identifier.lexeme(), span(identifier.start(), identifier.end()));
}
final var token = cursor.peek();
report(token, ParseErrors.E_PARSE_EXPECTED_TOKEN, "Expected type name");
if (!cursor.isAtEnd()) {
cursor.advance();
}
return new PbsAst.TypeRef("<error>", span(token.start(), token.end()));
}
/**
* Parses a brace-delimited block.
*
* <p>Example:
* <pre>{@code
* {
* let x = 1;
* return x;
* }
* }</pre>
*/
private PbsAst.Block parseBlock() {
final var leftBrace = consume(PbsTokenKind.LEFT_BRACE, "Expected '{' to start block");
@ -212,8 +357,6 @@ public final class PbsParser {
/**
* Parses one statement inside a block.
*
* <p>The current slice supports {@code let}, {@code return}, and expression statements.
*/
private PbsAst.Statement parseStatement() {
if (cursor.match(PbsTokenKind.LET)) {
@ -227,14 +370,9 @@ public final class PbsParser {
/**
* Parses a local binding statement.
*
* <p>Examples:
* <pre>{@code
* let x = 1;
* let y: int = x + 1;
* }</pre>
*/
private PbsAst.Statement parseLetStatement(final PbsToken letToken) {
cursor.match(PbsTokenKind.CONST);
final var name = consume(PbsTokenKind.IDENTIFIER, "Expected variable name");
PbsAst.TypeRef explicitType = null;
@ -267,8 +405,6 @@ public final class PbsParser {
/**
* Parses an expression statement terminated by a semicolon.
*
* <p>Example: {@code log(value);}
*/
private PbsAst.Statement parseExpressionStatement() {
final var expression = exprParser.parseExpression();
@ -276,14 +412,56 @@ public final class PbsParser {
return new PbsAst.ExpressionStatement(expression, span(expression.span().getStart(), semicolon.end()));
}
private long consumeDeclarationTerminator() {
var end = cursor.previous().end();
var parenDepth = 0;
while (!cursor.isAtEnd()) {
final var token = cursor.peek();
if (parenDepth == 0 && token.kind() == PbsTokenKind.SEMICOLON) {
return cursor.advance().end();
}
if (parenDepth == 0 && token.kind() == PbsTokenKind.LEFT_BRACE) {
return consumeBalancedBraces(cursor.advance());
}
final var consumed = cursor.advance();
end = consumed.end();
if (consumed.kind() == PbsTokenKind.LEFT_PAREN) {
parenDepth++;
} else if (consumed.kind() == PbsTokenKind.RIGHT_PAREN) {
parenDepth = Math.max(0, parenDepth - 1);
}
}
return end;
}
private long consumeBalancedBraces(final PbsToken leftBrace) {
var depth = 1;
var end = leftBrace.end();
while (!cursor.isAtEnd() && depth > 0) {
final var token = cursor.advance();
end = token.end();
if (token.kind() == PbsTokenKind.LEFT_BRACE) {
depth++;
} else if (token.kind() == PbsTokenKind.RIGHT_BRACE) {
depth--;
}
}
if (depth > 0) {
report(cursor.peek(), ParseErrors.E_PARSE_EXPECTED_TOKEN, "Expected '}' to close declaration body");
}
return 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 (!cursor.isAtEnd()) {
if (cursor.check(PbsTokenKind.FN) || cursor.check(PbsTokenKind.IMPORT)) {
if (cursor.check(PbsTokenKind.FN)
|| cursor.check(PbsTokenKind.IMPORT)
|| cursor.check(PbsTokenKind.DECLARE)
|| cursor.check(PbsTokenKind.IMPLEMENTS)) {
return;
}
if (cursor.match(PbsTokenKind.SEMICOLON)) {
@ -295,8 +473,6 @@ public final class PbsParser {
/**
* 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)) {
@ -323,4 +499,9 @@ 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()));
}
@FunctionalInterface
private interface NamedDeclFactory {
PbsAst.TopDecl build(String name, Span declarationSpan);
}
}

View File

@ -27,6 +27,8 @@ class PbsParserTest {
final PbsAst.File ast = PbsParser.parse(tokens, fileId, diagnostics);
assertTrue(diagnostics.isEmpty(), "Parser should not report diagnostics for valid function");
assertEquals(0, ast.imports().size());
assertEquals(1, ast.topDecls().size());
assertEquals(1, ast.functions().size());
final var fn = ast.functions().getFirst();
@ -35,5 +37,85 @@ class PbsParserTest {
assertEquals("int", fn.returnType().name());
assertEquals(1, fn.body().statements().size());
assertInstanceOf(PbsAst.ReturnStatement.class, fn.body().statements().getFirst());
assertEquals(fileId, fn.span().getFileId());
assertTrue(fn.span().getEnd() > fn.span().getStart());
}
@Test
void shouldPreserveImportsAndTopDeclOrdering() {
final var source = """
import { Vec2 as V2 } from @core:math;
fn first(): int { return 1; }
declare struct Point(x: int, y: int);
declare callback Tick(dt: int): int;
""";
final var diagnostics = DiagnosticSink.empty();
final var fileId = new FileId(0);
final var tokens = PbsLexer.lex(source, fileId, diagnostics);
final PbsAst.File ast = PbsParser.parse(tokens, fileId, diagnostics);
assertTrue(diagnostics.isEmpty(), "Parser should accept valid declarations in this slice");
assertEquals(1, ast.imports().size());
assertEquals(3, ast.topDecls().size());
assertInstanceOf(PbsAst.FunctionDecl.class, ast.topDecls().get(0));
assertInstanceOf(PbsAst.StructDecl.class, ast.topDecls().get(1));
assertInstanceOf(PbsAst.CallbackDecl.class, ast.topDecls().get(2));
final var importDecl = ast.imports().getFirst();
assertEquals("core", importDecl.moduleRef().project());
assertEquals("math", importDecl.moduleRef().pathSegments().getFirst());
}
@Test
void shouldRepresentMandatoryDeclarationFamilies() {
final var source = """
fn f(): int { return 1; }
declare struct S(x: int);
declare contract C { fn run(x: int): int; }
declare service Game { fn tick(x: int): int { return x; } }
declare error Err { Fail; }
declare enum Mode(Idle, Run);
declare callback TickCb(x: int): int;
declare const LIMIT: int = 10;
implements C for S using s { fn run(x: int): int { return x; } }
""";
final var diagnostics = DiagnosticSink.empty();
final var fileId = new FileId(0);
final var tokens = PbsLexer.lex(source, fileId, diagnostics);
final PbsAst.File ast = PbsParser.parse(tokens, fileId, diagnostics);
assertTrue(diagnostics.isEmpty(), "Parser should represent all mandatory declaration families");
assertEquals(9, ast.topDecls().size());
assertInstanceOf(PbsAst.FunctionDecl.class, ast.topDecls().get(0));
assertInstanceOf(PbsAst.StructDecl.class, ast.topDecls().get(1));
assertInstanceOf(PbsAst.ContractDecl.class, ast.topDecls().get(2));
assertInstanceOf(PbsAst.ServiceDecl.class, ast.topDecls().get(3));
assertInstanceOf(PbsAst.ErrorDecl.class, ast.topDecls().get(4));
assertInstanceOf(PbsAst.EnumDecl.class, ast.topDecls().get(5));
assertInstanceOf(PbsAst.CallbackDecl.class, ast.topDecls().get(6));
assertInstanceOf(PbsAst.ConstDecl.class, ast.topDecls().get(7));
assertInstanceOf(PbsAst.ImplementsDecl.class, ast.topDecls().get(8));
}
@Test
void shouldRecoverWithInvalidDeclarationNode() {
final var source = """
declare ;
fn ok(): int { return 1; }
""";
final var diagnostics = DiagnosticSink.empty();
final var fileId = new FileId(0);
final var tokens = PbsLexer.lex(source, fileId, diagnostics);
final PbsAst.File ast = PbsParser.parse(tokens, fileId, diagnostics);
assertTrue(diagnostics.hasErrors(), "Parser should report malformed declaration");
assertEquals(2, ast.topDecls().size());
assertInstanceOf(PbsAst.InvalidDecl.class, ast.topDecls().get(0));
assertInstanceOf(PbsAst.FunctionDecl.class, ast.topDecls().get(1));
}
}

View File

@ -0,0 +1,9 @@
package p.studio.compiler.source.tables;
import p.studio.compiler.source.identifiers.NameId;
public class NameTable extends InternTable<NameId, String> implements NameTableReader {
public NameTable() {
super(NameId::new);
}
}

View File

@ -0,0 +1,6 @@
package p.studio.compiler.source.tables;
import p.studio.compiler.source.identifiers.NameId;
public interface NameTableReader extends InternTableReader<NameId, String> {
}

View File

@ -0,0 +1,23 @@
package p.studio.compiler.source.tables;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class NameTableTest {
@Test
void shouldInternEqualNamesToSameIdentifier() {
final var table = new NameTable();
final var first = table.register("sum");
final var second = table.register("sum");
final var third = table.register("tick");
assertEquals(first, second);
assertEquals(2, table.size());
assertTrue(table.containsKey("sum"));
assertEquals("tick", table.get(third));
}
}