implements PR-19.3 parser and ast globals lifecycle markers

This commit is contained in:
bQUARKz 2026-03-26 19:08:29 +00:00
parent b190227b53
commit 738eea71ee
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
9 changed files with 154 additions and 25 deletions

View File

@ -113,18 +113,26 @@ public final class PbsAst {
ErrorDecl,
EnumDecl,
CallbackDecl,
GlobalDecl,
ConstDecl,
ImplementsDecl,
InvalidDecl {
Span span();
}
public enum LifecycleMarker {
NONE,
INIT,
FRAME
}
public record FunctionDecl(
String name,
ReadOnlyList<Parameter> parameters,
ReturnKind returnKind,
TypeRef returnType,
TypeRef resultErrorType,
LifecycleMarker lifecycleMarker,
Block body,
Span span) implements TopDecl {
}
@ -185,6 +193,13 @@ public final class PbsAst {
Span span) implements TopDecl {
}
public record GlobalDecl(
String name,
TypeRef explicitType,
Expression initializer,
Span span) implements TopDecl {
}
public record ConstDecl(
String name,
TypeRef explicitType,
@ -637,6 +652,7 @@ public final class PbsAst {
BarrelErrorItem,
BarrelEnumItem,
BarrelServiceItem,
BarrelGlobalItem,
BarrelConstItem,
BarrelCallbackItem {
Span span();
@ -693,6 +709,12 @@ public final class PbsAst {
Span span) implements BarrelItem {
}
public record BarrelGlobalItem(
Visibility visibility,
String name,
Span span) implements BarrelItem {
}
public record BarrelConstItem(
Visibility visibility,
String name,

View File

@ -316,6 +316,7 @@ public final class PbsLexer {
map.put("ctor", PbsTokenKind.CTOR);
map.put("let", PbsTokenKind.LET);
map.put("const", PbsTokenKind.CONST);
map.put("global", PbsTokenKind.GLOBAL);
map.put("declare", PbsTokenKind.DECLARE);
map.put("struct", PbsTokenKind.STRUCT);
map.put("contract", PbsTokenKind.CONTRACT);

View File

@ -39,6 +39,7 @@ public enum PbsTokenKind {
DECLARE,
LET,
CONST,
GLOBAL,
STRUCT,
CONTRACT,
ERROR,

View File

@ -274,6 +274,8 @@ public final class PbsModuleVisibilityValidator {
registerNonFunctionDeclaration(declarations, NonFunctionKind.ENUM, enumDecl.name(), enumDecl.span(), nameTable);
} else if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) {
registerNonFunctionDeclaration(declarations, NonFunctionKind.SERVICE, serviceDecl.name(), serviceDecl.span(), nameTable);
} else if (topDecl instanceof PbsAst.GlobalDecl globalDecl) {
registerNonFunctionDeclaration(declarations, NonFunctionKind.GLOBAL, globalDecl.name(), globalDecl.span(), nameTable);
} else if (topDecl instanceof PbsAst.ConstDecl constDecl) {
registerNonFunctionDeclaration(declarations, NonFunctionKind.CONST, constDecl.name(), constDecl.span(), nameTable);
} else if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) {
@ -356,6 +358,9 @@ public final class PbsModuleVisibilityValidator {
if (item instanceof PbsAst.BarrelServiceItem) {
return NonFunctionKind.SERVICE;
}
if (item instanceof PbsAst.BarrelGlobalItem) {
return NonFunctionKind.GLOBAL;
}
if (item instanceof PbsAst.BarrelConstItem) {
return NonFunctionKind.CONST;
}
@ -384,6 +389,9 @@ public final class PbsModuleVisibilityValidator {
if (item instanceof PbsAst.BarrelServiceItem serviceItem) {
return serviceItem.name();
}
if (item instanceof PbsAst.BarrelGlobalItem globalItem) {
return globalItem.name();
}
if (item instanceof PbsAst.BarrelConstItem constItem) {
return constItem.name();
}
@ -412,6 +420,9 @@ public final class PbsModuleVisibilityValidator {
if (item instanceof PbsAst.BarrelServiceItem serviceItem) {
return serviceItem.visibility();
}
if (item instanceof PbsAst.BarrelGlobalItem globalItem) {
return globalItem.visibility();
}
if (item instanceof PbsAst.BarrelConstItem constItem) {
return constItem.visibility();
}
@ -469,6 +480,7 @@ public final class PbsModuleVisibilityValidator {
ERROR,
ENUM,
SERVICE,
GLOBAL,
CONST,
CALLBACK
}

View File

@ -98,6 +98,9 @@ public final class PbsBarrelParser {
if (cursor.match(PbsTokenKind.SERVICE)) {
return parseSimpleItem(visibility, visibilityToken, PbsTokenKind.SERVICE);
}
if (cursor.match(PbsTokenKind.GLOBAL)) {
return parseSimpleItem(visibility, visibilityToken, PbsTokenKind.GLOBAL);
}
if (cursor.match(PbsTokenKind.CONST)) {
return parseSimpleItem(visibility, visibilityToken, PbsTokenKind.CONST);
}
@ -126,6 +129,7 @@ public final class PbsBarrelParser {
case ERROR -> new PbsAst.BarrelErrorItem(visibility, name.lexeme(), itemSpan);
case ENUM -> new PbsAst.BarrelEnumItem(visibility, name.lexeme(), itemSpan);
case SERVICE -> new PbsAst.BarrelServiceItem(visibility, name.lexeme(), itemSpan);
case GLOBAL -> new PbsAst.BarrelGlobalItem(visibility, name.lexeme(), itemSpan);
case CONST -> new PbsAst.BarrelConstItem(visibility, name.lexeme(), itemSpan);
case CALLBACK -> new PbsAst.BarrelCallbackItem(visibility, name.lexeme(), itemSpan);
default -> null;

View File

@ -131,6 +131,10 @@ final class PbsDeclarationParser {
rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "callback declarations");
return parseCallbackDeclaration(declareToken);
}
if (cursor.match(PbsTokenKind.GLOBAL)) {
rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "global declarations");
return parseGlobalDeclaration(declareToken);
}
if (cursor.match(PbsTokenKind.CONST)) {
return parseConstDeclaration(declareToken, pendingAttributes);
}
@ -166,8 +170,10 @@ final class PbsDeclarationParser {
return new PbsAst.InvalidDecl("invalid declare form", span(declareToken.start(), token.end()));
}
PbsAst.FunctionDecl parseFunction(final PbsToken fnToken) {
return parseFunctionLike(fnToken);
PbsAst.FunctionDecl parseFunction(
final PbsToken fnToken,
final ReadOnlyList<PbsAst.Attribute> pendingAttributes) {
return parseFunctionLike(fnToken, pendingAttributes);
}
PbsAst.ImplementsDecl parseImplementsDeclaration(final PbsToken implementsToken) {
@ -188,7 +194,7 @@ final class PbsDeclarationParser {
cursor.advance();
continue;
}
methods.add(parseFunctionLike(cursor.previous()));
methods.add(parseFunctionLike(cursor.previous(), ReadOnlyList.empty()));
}
end = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to end implements body").end();
} else {
@ -377,7 +383,7 @@ final class PbsDeclarationParser {
final var ctors = new ArrayList<PbsAst.CtorDecl>();
while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) {
if (cursor.match(PbsTokenKind.FN)) {
methods.add(parseFunctionLike(cursor.previous()));
methods.add(parseFunctionLike(cursor.previous(), ReadOnlyList.empty()));
continue;
}
if (cursor.match(PbsTokenKind.CTOR)) {
@ -441,7 +447,7 @@ final class PbsDeclarationParser {
cursor.advance();
continue;
}
methods.add(parseFunctionLike(cursor.previous()));
methods.add(parseFunctionLike(cursor.previous(), ReadOnlyList.empty()));
}
final var rightBrace = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to end service body");
return new PbsAst.ServiceDecl(name.lexeme(), ReadOnlyList.wrap(methods), span(declareToken.start(), rightBrace.end()));
@ -519,7 +525,10 @@ final class PbsDeclarationParser {
return new PbsAst.EnumDecl(name.lexeme(), ReadOnlyList.wrap(cases), span(declareToken.start(), Math.max(rightParen.end(), semicolon.end())));
}
private PbsAst.FunctionDecl parseFunctionLike(final PbsToken fnToken) {
private PbsAst.FunctionDecl parseFunctionLike(
final PbsToken fnToken,
final ReadOnlyList<PbsAst.Attribute> pendingAttributes) {
final var lifecycleMarker = validateAndResolveLifecycleMarker(pendingAttributes);
final var name = consumeCallableName();
consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after function name");
final var parameters = typeParser.parseParametersUntilRightParen();
@ -534,6 +543,7 @@ final class PbsDeclarationParser {
returnSpec.kind(),
returnSpec.returnType(),
returnSpec.resultErrorType(),
lifecycleMarker,
body,
span(fnToken.start(), body.span().getEnd()));
}
@ -614,6 +624,20 @@ final class PbsDeclarationParser {
span(declareToken.start(), semicolon.end()));
}
private PbsAst.GlobalDecl parseGlobalDeclaration(final PbsToken declareToken) {
final var name = consume(PbsTokenKind.IDENTIFIER, "Expected global name");
consume(PbsTokenKind.COLON, "Expected ':' after global name");
final var explicitType = typeParser.parseTypeRef();
consume(PbsTokenKind.EQUAL, "Expected '=' after global type annotation");
final var initializer = exprParser.parseExpression();
final var semicolon = consume(PbsTokenKind.SEMICOLON, "Expected ';' after global declaration");
return new PbsAst.GlobalDecl(
name.lexeme(),
explicitType,
initializer,
span(declareToken.start(), semicolon.end()));
}
private PbsAst.Block parseBlock() {
return blockParserDelegate.parse();
}
@ -646,6 +670,43 @@ final class PbsDeclarationParser {
return context.parseMode() == PbsParser.ParseMode.ORDINARY;
}
private PbsAst.LifecycleMarker validateAndResolveLifecycleMarker(
final ReadOnlyList<PbsAst.Attribute> attributes) {
if (attributes.isEmpty()) {
return PbsAst.LifecycleMarker.NONE;
}
if (!isOrdinaryMode()) {
reportAttributesNotAllowed(attributes, "Attributes are not allowed before top-level functions");
return PbsAst.LifecycleMarker.NONE;
}
PbsAst.LifecycleMarker marker = PbsAst.LifecycleMarker.NONE;
for (final var attribute : attributes) {
final PbsAst.LifecycleMarker nextMarker;
if ("Init".equals(attribute.name())) {
nextMarker = PbsAst.LifecycleMarker.INIT;
} else if ("Frame".equals(attribute.name())) {
nextMarker = PbsAst.LifecycleMarker.FRAME;
} else {
reportAttributesNotAllowed(attributes, "Only marker attributes [Init] and [Frame] are allowed before top-level functions");
return PbsAst.LifecycleMarker.NONE;
}
if (!attribute.arguments().isEmpty()) {
reportAttributesNotAllowed(attributes, "Lifecycle markers do not accept arguments");
return PbsAst.LifecycleMarker.NONE;
}
if (marker != PbsAst.LifecycleMarker.NONE && marker != nextMarker) {
reportAttributesNotAllowed(attributes, "Top-level functions cannot combine [Init] and [Frame]");
return PbsAst.LifecycleMarker.NONE;
}
marker = nextMarker;
}
return marker;
}
private PbsToken consume(final PbsTokenKind kind, final String message) {
if (cursor.check(kind)) {
return cursor.advance();

View File

@ -30,12 +30,6 @@ final class PbsTopLevelParser {
var pendingAttributes = ReadOnlyList.<PbsAst.Attribute>empty();
if (cursor.check(PbsTokenKind.LEFT_BRACKET)) {
pendingAttributes = attributeParser.parseAttributeList();
if (isOrdinaryMode()) {
reportAttributesNotAllowed(
pendingAttributes,
"Attributes are not allowed in ordinary .pbs source modules");
pendingAttributes = ReadOnlyList.empty();
}
}
if (cursor.match(PbsTokenKind.IMPORT)) {
@ -45,8 +39,7 @@ final class PbsTopLevelParser {
}
if (cursor.match(PbsTokenKind.FN)) {
rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "top-level functions");
topDecls.add(declarationParser.parseFunction(cursor.previous()));
topDecls.add(declarationParser.parseFunction(cursor.previous(), pendingAttributes));
continue;
}

View File

@ -16,6 +16,7 @@ class PbsBarrelParserTest {
pub fn sum(a: int, b: int) -> int;
mod fn run(state: optional int) -> result<Err> (value: int, meta: float);
pub struct Vec2;
mod global Palette;
pub callback Tick;
""";
@ -25,7 +26,7 @@ class PbsBarrelParserTest {
final var barrel = PbsBarrelParser.parse(tokens, fileId, diagnostics);
assertTrue(diagnostics.isEmpty(), "Valid barrel should parse without diagnostics");
assertEquals(4, barrel.items().size());
assertEquals(5, barrel.items().size());
final var sum = assertInstanceOf(PbsAst.BarrelFunctionItem.class, barrel.items().get(0));
assertEquals("sum", sum.name());
@ -39,7 +40,8 @@ class PbsBarrelParserTest {
assertEquals(PbsAst.TypeRefKind.NAMED_TUPLE, run.returnType().kind());
assertInstanceOf(PbsAst.BarrelStructItem.class, barrel.items().get(2));
assertInstanceOf(PbsAst.BarrelCallbackItem.class, barrel.items().get(3));
assertInstanceOf(PbsAst.BarrelGlobalItem.class, barrel.items().get(3));
assertInstanceOf(PbsAst.BarrelCallbackItem.class, barrel.items().get(4));
}
@Test

View File

@ -75,12 +75,17 @@ class PbsParserTest {
void shouldParseDeclarationFamiliesWithShape() {
final var source = """
fn f() -> int { return 1; }
[Init]
fn init() -> void { return; }
[Frame]
fn frame() -> void { return; }
declare struct S(pub mut x: int, y: optional int) { fn run() -> void { return; } ctor make(x: int) { return; } }
declare contract C { fn run(x: int) -> int; }
declare service Game { fn tick(x: int) -> int { return x; } }
declare error Err { Fail; Crash; }
declare enum Mode(Idle = 0, Run = 1);
declare callback TickCb(x: int) -> result<Err> int;
declare global STATE: int = 10;
declare const LIMIT: int = 10;
implements C for S using s { fn run(x: int) -> int { return x; } }
""";
@ -91,32 +96,40 @@ class PbsParserTest {
final PbsAst.File ast = PbsParser.parse(tokens, fileId, diagnostics);
assertTrue(diagnostics.isEmpty(), "Parser should represent declaration families with expected shapes");
assertEquals(9, ast.topDecls().size());
assertEquals(12, ast.topDecls().size());
final var structDecl = assertInstanceOf(PbsAst.StructDecl.class, ast.topDecls().get(1));
final var initDecl = assertInstanceOf(PbsAst.FunctionDecl.class, ast.topDecls().get(1));
assertEquals(PbsAst.LifecycleMarker.INIT, initDecl.lifecycleMarker());
final var frameDecl = assertInstanceOf(PbsAst.FunctionDecl.class, ast.topDecls().get(2));
assertEquals(PbsAst.LifecycleMarker.FRAME, frameDecl.lifecycleMarker());
final var structDecl = assertInstanceOf(PbsAst.StructDecl.class, ast.topDecls().get(3));
assertEquals(2, structDecl.fields().size());
assertTrue(structDecl.hasBody());
assertEquals(1, structDecl.methods().size());
assertEquals(1, structDecl.ctors().size());
final var contractDecl = assertInstanceOf(PbsAst.ContractDecl.class, ast.topDecls().get(2));
final var contractDecl = assertInstanceOf(PbsAst.ContractDecl.class, ast.topDecls().get(4));
assertEquals(1, contractDecl.signatures().size());
final var serviceDecl = assertInstanceOf(PbsAst.ServiceDecl.class, ast.topDecls().get(3));
final var serviceDecl = assertInstanceOf(PbsAst.ServiceDecl.class, ast.topDecls().get(5));
assertEquals(1, serviceDecl.methods().size());
final var errorDecl = assertInstanceOf(PbsAst.ErrorDecl.class, ast.topDecls().get(4));
final var errorDecl = assertInstanceOf(PbsAst.ErrorDecl.class, ast.topDecls().get(6));
assertEquals(2, errorDecl.cases().size());
final var enumDecl = assertInstanceOf(PbsAst.EnumDecl.class, ast.topDecls().get(5));
final var enumDecl = assertInstanceOf(PbsAst.EnumDecl.class, ast.topDecls().get(7));
assertEquals(2, enumDecl.cases().size());
final var callbackDecl = assertInstanceOf(PbsAst.CallbackDecl.class, ast.topDecls().get(6));
final var callbackDecl = assertInstanceOf(PbsAst.CallbackDecl.class, ast.topDecls().get(8));
assertEquals(PbsAst.ReturnKind.RESULT, callbackDecl.returnKind());
assertEquals("Err", callbackDecl.resultErrorType().name());
assertInstanceOf(PbsAst.ConstDecl.class, ast.topDecls().get(7));
final var implementsDecl = assertInstanceOf(PbsAst.ImplementsDecl.class, ast.topDecls().get(8));
final var globalDecl = assertInstanceOf(PbsAst.GlobalDecl.class, ast.topDecls().get(9));
assertEquals("STATE", globalDecl.name());
assertInstanceOf(PbsAst.ConstDecl.class, ast.topDecls().get(10));
final var implementsDecl = assertInstanceOf(PbsAst.ImplementsDecl.class, ast.topDecls().get(11));
assertEquals(1, implementsDecl.methods().size());
}
@ -139,6 +152,26 @@ class PbsParserTest {
assertTrue(diagnostics.stream().anyMatch(d -> d.getCode().equals(ParseErrors.E_PARSE_ATTRIBUTES_NOT_ALLOWED.name())));
}
@Test
void shouldParseLifecycleMarkersOnlyOnTopLevelFunctions() {
final var source = """
[Init]
fn init() -> void { return; }
[Frame]
fn frame() -> void { return; }
""";
final var diagnostics = DiagnosticSink.empty();
final var fileId = new FileId(0);
final PbsAst.File ast = PbsParser.parse(PbsLexer.lex(source, fileId, diagnostics), fileId, diagnostics);
assertTrue(diagnostics.isEmpty(), "Lifecycle markers should be accepted on top-level functions");
final var initDecl = assertInstanceOf(PbsAst.FunctionDecl.class, ast.topDecls().get(0));
final var frameDecl = assertInstanceOf(PbsAst.FunctionDecl.class, ast.topDecls().get(1));
assertEquals(PbsAst.LifecycleMarker.INIT, initDecl.lifecycleMarker());
assertEquals(PbsAst.LifecycleMarker.FRAME, frameDecl.lifecycleMarker());
}
@Test
void shouldParseServiceAndMemberNamesUsingErrorKeyword() {
final var source = """