diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/ast/PbsAst.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/ast/PbsAst.java index 482b047a..2d4424cd 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/ast/PbsAst.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/ast/PbsAst.java @@ -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 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, diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/lexer/PbsLexer.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/lexer/PbsLexer.java index a9be86f7..5628e55c 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/lexer/PbsLexer.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/lexer/PbsLexer.java @@ -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); diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/lexer/PbsTokenKind.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/lexer/PbsTokenKind.java index 4f6790bd..4a13a3cf 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/lexer/PbsTokenKind.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/lexer/PbsTokenKind.java @@ -39,6 +39,7 @@ public enum PbsTokenKind { DECLARE, LET, CONST, + GLOBAL, STRUCT, CONTRACT, ERROR, diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/linking/PbsModuleVisibilityValidator.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/linking/PbsModuleVisibilityValidator.java index 5ab8f46b..b36c3ea2 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/linking/PbsModuleVisibilityValidator.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/linking/PbsModuleVisibilityValidator.java @@ -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 } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsBarrelParser.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsBarrelParser.java index 0b5167fd..b8d57f78 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsBarrelParser.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsBarrelParser.java @@ -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; diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsDeclarationParser.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsDeclarationParser.java index b4b2f9d4..aa246a27 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsDeclarationParser.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsDeclarationParser.java @@ -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 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(); 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 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 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(); diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsTopLevelParser.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsTopLevelParser.java index 82031d3e..0a784678 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsTopLevelParser.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsTopLevelParser.java @@ -30,12 +30,6 @@ final class PbsTopLevelParser { var pendingAttributes = ReadOnlyList.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; } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsBarrelParserTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsBarrelParserTest.java index 598c5c44..ef354e22 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsBarrelParserTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsBarrelParserTest.java @@ -16,6 +16,7 @@ class PbsBarrelParserTest { pub fn sum(a: int, b: int) -> int; mod fn run(state: optional int) -> result (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 diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserTest.java index d264ccb2..cacd1f16 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/parser/PbsParserTest.java @@ -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 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 = """