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.pbs.parser.PbsParser;
|
||||||
import p.studio.compiler.source.diagnostics.DiagnosticSink;
|
import p.studio.compiler.source.diagnostics.DiagnosticSink;
|
||||||
import p.studio.compiler.source.identifiers.FileId;
|
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 p.studio.utilities.structures.ReadOnlyList;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -27,9 +29,11 @@ public final class PbsFrontendCompiler {
|
|||||||
private void validateFunctionNames(
|
private void validateFunctionNames(
|
||||||
final PbsAst.File ast,
|
final PbsAst.File ast,
|
||||||
final DiagnosticSink diagnostics) {
|
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()) {
|
for (final var fn : ast.functions()) {
|
||||||
if (names.add(fn.name())) {
|
final var nameId = nameTable.register(fn.name());
|
||||||
|
if (nameIds.add(nameId)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
diagnostics.error("E_RESOLVE_DUPLICATE_SYMBOL",
|
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.compiler.source.Span;
|
||||||
import p.studio.utilities.structures.ReadOnlyList;
|
import p.studio.utilities.structures.ReadOnlyList;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
public final class PbsAst {
|
public final class PbsAst {
|
||||||
private PbsAst() {
|
private PbsAst() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public record File(
|
public record File(
|
||||||
ReadOnlyList<FunctionDecl> functions,
|
ReadOnlyList<ImportDecl> imports,
|
||||||
|
ReadOnlyList<TopDecl> topDecls,
|
||||||
Span span) {
|
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(
|
public record FunctionDecl(
|
||||||
@ -18,7 +62,58 @@ public final class PbsAst {
|
|||||||
TypeRef returnType,
|
TypeRef returnType,
|
||||||
Expression elseFallback,
|
Expression elseFallback,
|
||||||
Block body,
|
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(
|
public record Parameter(
|
||||||
@ -124,4 +219,78 @@ public final class PbsAst {
|
|||||||
Expression expression,
|
Expression expression,
|
||||||
Span span) implements 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.
|
* 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(
|
public static PbsAst.File parse(
|
||||||
final ReadOnlyList<PbsToken> tokens,
|
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.
|
* 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() {
|
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()) {
|
while (!cursor.isAtEnd()) {
|
||||||
if (cursor.match(PbsTokenKind.IMPORT)) {
|
if (cursor.match(PbsTokenKind.IMPORT)) {
|
||||||
parseAndDiscardImport();
|
imports.add(parseImport(cursor.previous()));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cursor.match(PbsTokenKind.FN)) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cursor.match(PbsTokenKind.MOD, PbsTokenKind.PUB)) {
|
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");
|
"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();
|
synchronizeTopLevel();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -80,31 +84,37 @@ public final class PbsParser {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
report(cursor.peek(), ParseErrors.E_PARSE_UNEXPECTED_TOKEN,
|
final var token = cursor.peek();
|
||||||
"Expected top-level declaration ('fn') or import");
|
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();
|
synchronizeTopLevel();
|
||||||
}
|
}
|
||||||
|
|
||||||
final var eof = cursor.peek();
|
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.
|
* Parses import syntax and stores it in the AST.
|
||||||
*
|
|
||||||
* <p>Supported forms:
|
|
||||||
* <pre>{@code
|
|
||||||
* import @core:math;
|
|
||||||
* import { Vector, Matrix as Mat } from @core:math;
|
|
||||||
* }</pre>
|
|
||||||
*/
|
*/
|
||||||
private void parseAndDiscardImport() {
|
private PbsAst.ImportDecl parseImport(final PbsToken importToken) {
|
||||||
|
final var items = new ArrayList<PbsAst.ImportItem>();
|
||||||
if (cursor.match(PbsTokenKind.LEFT_BRACE)) {
|
if (cursor.match(PbsTokenKind.LEFT_BRACE)) {
|
||||||
while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) {
|
while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) {
|
||||||
if (cursor.match(PbsTokenKind.IDENTIFIER)) {
|
if (cursor.match(PbsTokenKind.IDENTIFIER)) {
|
||||||
|
final var itemName = cursor.previous();
|
||||||
|
String alias = null;
|
||||||
if (cursor.match(PbsTokenKind.AS)) {
|
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);
|
cursor.match(PbsTokenKind.COMMA);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -115,54 +125,90 @@ public final class PbsParser {
|
|||||||
consume(PbsTokenKind.FROM, "Expected 'from' in named import");
|
consume(PbsTokenKind.FROM, "Expected 'from' in named import");
|
||||||
}
|
}
|
||||||
|
|
||||||
parseModuleRef();
|
final var moduleRef = parseModuleRef();
|
||||||
consume(PbsTokenKind.SEMICOLON, "Expected ';' after import");
|
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}.
|
* Parses a module reference such as {@code @core:math/tools}.
|
||||||
*/
|
*/
|
||||||
private void parseModuleRef() {
|
private PbsAst.ModuleRef parseModuleRef() {
|
||||||
consume(PbsTokenKind.AT, "Expected '@' in module reference");
|
final var at = consume(PbsTokenKind.AT, "Expected '@' in module reference");
|
||||||
consume(PbsTokenKind.IDENTIFIER, "Expected project identifier in module reference");
|
final var project = consume(PbsTokenKind.IDENTIFIER, "Expected project identifier in module reference");
|
||||||
consume(PbsTokenKind.COLON, "Expected ':' 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)) {
|
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.
|
* 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) {
|
private PbsAst.FunctionDecl parseFunction(final PbsToken fnToken) {
|
||||||
final var name = consume(PbsTokenKind.IDENTIFIER, "Expected function name");
|
final var name = consume(PbsTokenKind.IDENTIFIER, "Expected function name");
|
||||||
consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after function name");
|
consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after function name");
|
||||||
|
final var parameters = parseParametersUntilRightParen();
|
||||||
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));
|
|
||||||
}
|
|
||||||
consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after parameter list");
|
consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after parameter list");
|
||||||
|
|
||||||
PbsAst.TypeRef returnType = null;
|
PbsAst.TypeRef returnType = null;
|
||||||
if (cursor.match(PbsTokenKind.COLON)) {
|
if (cursor.match(PbsTokenKind.COLON, PbsTokenKind.ARROW)) {
|
||||||
returnType = parseTypeRef();
|
returnType = parseTypeRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,24 +227,123 @@ public final class PbsParser {
|
|||||||
span(fnToken.start(), body.span().getEnd()));
|
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}.
|
* Parses a simple identifier-based type reference such as {@code int} or {@code Vector}.
|
||||||
*/
|
*/
|
||||||
private PbsAst.TypeRef parseTypeRef() {
|
private PbsAst.TypeRef parseTypeRef() {
|
||||||
final var identifier = consume(PbsTokenKind.IDENTIFIER, "Expected type name");
|
if (cursor.match(PbsTokenKind.IDENTIFIER, PbsTokenKind.SELF)) {
|
||||||
|
final var identifier = cursor.previous();
|
||||||
return new PbsAst.TypeRef(identifier.lexeme(), span(identifier.start(), identifier.end()));
|
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.
|
* Parses a brace-delimited block.
|
||||||
*
|
|
||||||
* <p>Example:
|
|
||||||
* <pre>{@code
|
|
||||||
* {
|
|
||||||
* let x = 1;
|
|
||||||
* return x;
|
|
||||||
* }
|
|
||||||
* }</pre>
|
|
||||||
*/
|
*/
|
||||||
private PbsAst.Block parseBlock() {
|
private PbsAst.Block parseBlock() {
|
||||||
final var leftBrace = consume(PbsTokenKind.LEFT_BRACE, "Expected '{' to start block");
|
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.
|
* Parses one statement inside a block.
|
||||||
*
|
|
||||||
* <p>The current slice supports {@code let}, {@code return}, and expression statements.
|
|
||||||
*/
|
*/
|
||||||
private PbsAst.Statement parseStatement() {
|
private PbsAst.Statement parseStatement() {
|
||||||
if (cursor.match(PbsTokenKind.LET)) {
|
if (cursor.match(PbsTokenKind.LET)) {
|
||||||
@ -227,14 +370,9 @@ public final class PbsParser {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a local binding statement.
|
* 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) {
|
private PbsAst.Statement parseLetStatement(final PbsToken letToken) {
|
||||||
|
cursor.match(PbsTokenKind.CONST);
|
||||||
final var name = consume(PbsTokenKind.IDENTIFIER, "Expected variable name");
|
final var name = consume(PbsTokenKind.IDENTIFIER, "Expected variable name");
|
||||||
|
|
||||||
PbsAst.TypeRef explicitType = null;
|
PbsAst.TypeRef explicitType = null;
|
||||||
@ -267,8 +405,6 @@ public final class PbsParser {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses an expression statement terminated by a semicolon.
|
* Parses an expression statement terminated by a semicolon.
|
||||||
*
|
|
||||||
* <p>Example: {@code log(value);}
|
|
||||||
*/
|
*/
|
||||||
private PbsAst.Statement parseExpressionStatement() {
|
private PbsAst.Statement parseExpressionStatement() {
|
||||||
final var expression = exprParser.parseExpression();
|
final var expression = exprParser.parseExpression();
|
||||||
@ -276,14 +412,56 @@ public final class PbsParser {
|
|||||||
return new PbsAst.ExpressionStatement(expression, span(expression.span().getStart(), semicolon.end()));
|
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.
|
* 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() {
|
private void synchronizeTopLevel() {
|
||||||
while (!cursor.isAtEnd()) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (cursor.match(PbsTokenKind.SEMICOLON)) {
|
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.
|
* 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) {
|
private PbsToken consume(final PbsTokenKind kind, final String message) {
|
||||||
if (cursor.check(kind)) {
|
if (cursor.check(kind)) {
|
||||||
@ -323,4 +499,9 @@ public final class PbsParser {
|
|||||||
private void report(final PbsToken token, final ParseErrors parseErrors, final String message) {
|
private void report(final PbsToken token, final ParseErrors parseErrors, final String message) {
|
||||||
diagnostics.error(parseErrors.name(), message, new Span(fileId, token.start(), token.end()));
|
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);
|
final PbsAst.File ast = PbsParser.parse(tokens, fileId, diagnostics);
|
||||||
|
|
||||||
assertTrue(diagnostics.isEmpty(), "Parser should not report diagnostics for valid function");
|
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());
|
assertEquals(1, ast.functions().size());
|
||||||
|
|
||||||
final var fn = ast.functions().getFirst();
|
final var fn = ast.functions().getFirst();
|
||||||
@ -35,5 +37,85 @@ class PbsParserTest {
|
|||||||
assertEquals("int", fn.returnType().name());
|
assertEquals("int", fn.returnType().name());
|
||||||
assertEquals(1, fn.body().statements().size());
|
assertEquals(1, fn.body().statements().size());
|
||||||
assertInstanceOf(PbsAst.ReturnStatement.class, fn.body().statements().getFirst());
|
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