implements PR002
This commit is contained in:
parent
04f65f46b3
commit
9ddfa19bf2
@ -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.
|
||||
@ -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",
|
||||
|
||||
@ -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 {
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
package p.studio.compiler.source.tables;
|
||||
|
||||
import p.studio.compiler.source.identifiers.NameId;
|
||||
|
||||
public interface NameTableReader extends InternTableReader<NameId, String> {
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user