diff --git a/docs/general/specs/14. Name Resolution and Module Linking Specification.md b/docs/general/specs/14. Name Resolution and Module Linking Specification.md index 9809b87d..20df242f 100644 --- a/docs/general/specs/14. Name Resolution and Module Linking Specification.md +++ b/docs/general/specs/14. Name Resolution and Module Linking Specification.md @@ -71,7 +71,7 @@ The following inputs are already fixed elsewhere and must not be contradicted he - Imports target modules, not files. - Reserved stdlib project spaces are resolved only from the selected stdlib environment. - Only `pub` symbols may be imported from another module. -- Reserved stdlib/interface modules may expose compile-time-only shells for both host-backed and VM-owned surfaces. +- Reserved stdlib/interface modules may expose compile-time-only shells for both host-backed and VM-owned surfaces, and may expose service facades over non-public host owners. - Source-visible builtin names are resolved through imported builtin shell declarations rather than by hardcoded source spelling alone. - Canonical builtin identity and canonical intrinsic identity are governed by builtin metadata rather than by the imported PBS-visible declaration name alone. - Canonical host identity is governed by host metadata rather than by imported owner spelling alone. diff --git a/docs/general/specs/18. Standard Library Surface Specification.md b/docs/general/specs/18. Standard Library Surface Specification.md index 28e20e73..1f2c093d 100644 --- a/docs/general/specs/18. Standard Library Surface Specification.md +++ b/docs/general/specs/18. Standard Library Surface Specification.md @@ -53,6 +53,7 @@ The following inputs are already fixed elsewhere and must not be contradicted he - Reserved stdlib project spaces include `@sdk:*` and `@core:*`. - Stdlib interface modules are compile-time-only and loaded as PBS-like modules. +- Stdlib interface modules may include service facades whose bodies delegate to reserved host owners; such bodies are source-surface ergonomics and not standalone runtime modules. - User code must import stdlib modules explicitly. - Canonical host identity comes from reserved host-binding metadata, not from source owner spelling. - VM-owned builtin type, builtin constant, and intrinsic surfaces may be exposed through reserved stdlib shells without transferring semantic ownership away from the VM. @@ -149,7 +150,9 @@ shells. Rules: - host-backed stdlib surfaces use `declare host`, +- host-backed stdlib modules may also expose `declare service` facades that call internal non-public host owners, - host-backed method signatures carry `Host(...)`, +- internal low-level host owners can remain non-public via `mod` barrel visibility while higher-level services are exported with `pub`, - user code may import those host owners through ordinary stdlib module imports, - and lowering continues through canonical host identity, `SYSC`, `HOSTCALL`, loader resolution, and final `SYSCALL`. diff --git a/docs/general/specs/20. IRBackend to IRVM Lowering Specification.md b/docs/general/specs/20. IRBackend to IRVM Lowering Specification.md index 28a7b4d5..e60c50ee 100644 --- a/docs/general/specs/20. IRBackend to IRVM Lowering Specification.md +++ b/docs/general/specs/20. IRBackend to IRVM Lowering Specification.md @@ -56,7 +56,8 @@ Lowering from `IRBackend` to `IRVM` may start only when: 2. required callsite categories (`CALL_FUNC`/`CALL_HOST`/`CALL_INTRINSIC`) are available, 3. required canonical host/intrinsic metadata is available for admitted callsites, 4. dependency-scoped fail-fast exclusions have already been applied at the `IRBackend` boundary, -5. and target `vm_profile` is selected deterministically. +5. frontend-declared `IRBackend.entryPointCallableName` is present and non-blank, +6. and target `vm_profile` is selected deterministically. ## 6. IRVM Model Obligations @@ -91,9 +92,10 @@ Control-flow lowering must satisfy: Function indexing must be deterministic: -1. entrypoint function id is `0`, -2. remaining functions are ordered by `(module_key, callable_name, source_start)`, -3. and identical admitted input graphs must produce identical function-id assignments. +1. entrypoint callable is selected by exact name match against `IRBackend.entryPointCallableName`, +2. selected entrypoint function id is `0`, +3. remaining functions are ordered by `(module_key, callable_name, source_start)`, +4. and identical admitted input graphs must produce identical function-id assignments. ## 9. Callsite Lowering Obligations diff --git a/docs/pbs/pull-requests/INDEX.md b/docs/pbs/pull-requests/INDEX.md index 44f6a29e..9aca769d 100644 --- a/docs/pbs/pull-requests/INDEX.md +++ b/docs/pbs/pull-requests/INDEX.md @@ -132,3 +132,4 @@ Foco: eliminar os dois `partial` remanescentes na matriz backend antes de mover 1. `PR-07.1-optimize-irvm-span-source-hook-preservation.md` 2. `PR-07.2-pbs-callsite-category-no-textual-heuristics-regression.md` +3. `PR-07.3-irvm-entrypoint-export-emission-for-runtime-loader.md` diff --git a/docs/pbs/pull-requests/PR-07.3-irvm-entrypoint-export-emission-for-runtime-loader.md b/docs/pbs/pull-requests/PR-07.3-irvm-entrypoint-export-emission-for-runtime-loader.md new file mode 100644 index 00000000..32ee3e95 --- /dev/null +++ b/docs/pbs/pull-requests/PR-07.3-irvm-entrypoint-export-emission-for-runtime-loader.md @@ -0,0 +1,77 @@ +# PR-07.3 - IRVM Entrypoint Export Emission for Runtime Loader Compatibility + +## Briefing + +Hoje um cartridge pode compilar com `entrypoint: "frame"` no `manifest.json` e `pub fn frame()` no `mod.barrel`, mas ainda falhar em runtime com `EntrypointNotFound`. + +A causa observada e que o `program.pbx` pode ser emitido sem secao `exports` (section kind `4`), enquanto o loader resolve entrypoint nominal usando `program.exports`. + +## Motivation + +### Dor atual que esta PR resolve + +1. Build aparentemente valido, mas falha de bootstrap em runtime. +2. Divergencia entre contrato de manifesto (`entrypoint` nominal) e artefato PBX emitido. +3. Falta de evidencia automatizada para evitar regressao de exports no caminho `IRBackend -> IRVM -> Bytecode`. + +## Target + +Corrigir o pipeline backend para sempre emitir export nominal do entrypoint no PBX de preload, garantindo compatibilidade com o loader quando o `manifest.json` declara entrypoint por nome. + +## Dependencies + +Prerequisitos diretos: + +1. `PR-05.3` (politica deterministica de entrypoint no indice `0`). +2. `PR-05.4` (coerencia unica entre IRVM e EmissionPlan). + +## Scope + +1. `LowerToIRVMService` deve preencher `EmissionPlan.exports` com o simbolo do entrypoint apontando para `func_idx = 0`. +2. Adicionar regressao dedicada para garantir que `coherentEmissionPlan()` preserva export do entrypoint. +3. Validar serializacao PBX com secao `exports` presente quando houver entrypoint nominal. +4. Cobrir cenario de integracao onde `manifest.entrypoint = "frame"` inicializa VM sem `EntrypointNotFound`. + +## Non-Goals + +1. Nao exportar toda a superficie de funcoes executaveis neste passo. +2. Nao alterar formato binario PBX alem da presenca correta da secao `exports`. +3. Nao mudar semantica de resolucao por indice numerico de entrypoint. + +## Method + +### O que deve ser feito explicitamente + +1. No `LowerToIRVMService`, construir `BytecodeEmitter.EmissionPlan` com `exports` contendo, no minimo, `Export(entryPointCallableName, 0)`. +2. Garantir determinismo do export do entrypoint a partir da mesma politica que fixa `func_id = 0`. +3. Adicionar teste unitario em `LowerToIRVMServiceTest` para verificar export nominal do entrypoint no plano coerente. +4. Adicionar teste de emissao/link para verificar secao `exports` no modulo serializado. +5. Adicionar teste de integracao runtime-backed para falhar se regressar para `EntrypointNotFound` em cartridge com entrypoint nominal valido. + +## Acceptance Criteria + +1. Build de projeto PBS com entrypoint `frame` gera PBX com secao `exports`. +2. O simbolo `frame` aponta para `func_idx = 0` no artefato emitido. +3. Execucao de cartridge com `manifest.entrypoint = "frame"` nao falha por `EntrypointNotFound` quando o programa contem o entrypoint admitido. +4. Rebuilds equivalentes produzem mapping de export deterministico. + +## Tests + +1. Novos testes de backend: + - `:prometeu-compiler:prometeu-build-pipeline:test` +2. Reexecucao de compatibilidade runtime-backed: + - `:prometeu-compiler:prometeu-build-pipeline:test --tests *Runtime*` +3. Smoke local recomendado: + - `./runtime/prometeu build .` + - `./runtime/prometeu run cartridge` + +## Affected Documents + +1. `docs/general/specs/15. Bytecode and PBX Mapping Specification.md` +2. `docs/general/specs/20. IRBackend to IRVM Lowering Specification.md` +3. `docs/pbs/specs/7. Cartridge Manifest and Runtime Capabilities Specification.md` +4. `docs/general/specs/22. Backend Spec-to-Test Conformance Matrix.md` + +## Open Questions + +1. Em evolucao futura, devemos exportar apenas o entrypoint ou tambem funcoes explicitamente marcadas para linking externo? diff --git a/docs/pbs/specs/13. Lowering IRBackend Specification.md b/docs/pbs/specs/13. Lowering IRBackend Specification.md index a525289e..c7a6808e 100644 --- a/docs/pbs/specs/13. Lowering IRBackend Specification.md +++ b/docs/pbs/specs/13. Lowering IRBackend Specification.md @@ -78,6 +78,7 @@ For each admitted source unit and callable in the current lowering slice, `IRBac 4. source attribution anchor (`file + span`) for diagnostics and traceability, 5. source-observable parse intent for statement/expression structure (including precedence/associativity outcome already fixed by AST shape). 6. deterministic `requiredCapabilities` derived from admitted host-binding metadata for packer/runtime-manifest assistance. +7. frontend-declared executable entrypoint callable name from `FrontendSpec` (`entryPointCallableName`) for backend handoff. Lowering must not collapse source categories in a way that erases required declaration/callable identity needed by downstream diagnostics or conformance assertions. @@ -194,3 +195,13 @@ It does not replace: 1. `docs/general/specs/20. IRBackend to IRVM Lowering Specification.md`, 2. `docs/general/specs/21. IRVM Optimization Pipeline Specification.md`, 3. or `docs/general/specs/15. Bytecode and PBX Mapping Specification.md`. + +### 12.7 FrontendSpec Entry Point Obligation + +For executable language frontends, `FrontendSpec` must declare one canonical entrypoint callable name (`entryPointCallableName`). + +At `IRBackend` emission time: + +1. frontend must copy this declaration into `IRBackend.entryPointCallableName`, +2. value must be non-blank and deterministic for the same language/spec version, +3. and backend stages must treat this field as authoritative for entrypoint selection instead of hardcoded language-agnostic names. diff --git a/docs/pbs/specs/5. Manifest, Stdlib, and SDK Resolution Specification.md b/docs/pbs/specs/5. Manifest, Stdlib, and SDK Resolution Specification.md index 22aefb07..7b870b7f 100644 --- a/docs/pbs/specs/5. Manifest, Stdlib, and SDK Resolution Specification.md +++ b/docs/pbs/specs/5. Manifest, Stdlib, and SDK Resolution Specification.md @@ -19,7 +19,8 @@ This document is the authority for how the compiler discovers and loads: - ordinary project modules, - reserved stdlib modules such as `@sdk:gfx`, -- and compile-time-only host-binding metadata attached to those reserved modules. +- compile-time reserved metadata attached to those reserved modules, +- and interface-only service facades that wrap host surfaces for source ergonomics. This document does not define runtime load behavior. PBX host-binding emission and loader-side syscall resolution are defined by the Host ABI Binding and Loader Resolution Specification. @@ -445,7 +446,7 @@ The current PBS design direction is: 6. The root project selects the effective stdlib line for the whole build. 7. Dependency projects with a higher stdlib requirement than the root are rejected. 8. Stdlib modules are loaded as PBS-like interface modules, not as ordinary dependencies. -9. Interface modules are compile-time-only and do not emit executable bytecode. +9. Interface modules are compile-time-only in packaging terms: they may carry declarative service facades with method bodies for source-level API ergonomics, but they still do not emit executable bytecode as standalone modules. 10. `declare host` remains reserved to stdlib/toolchain interface modules. 11. Reserved attributes such as `[Host(...)]` are compile-time metadata, not runtime objects. 12. The compiler consumes `Host` metadata to produce runtime-facing host-binding artifacts defined elsewhere. diff --git a/docs/pbs/specs/7. Cartridge Manifest and Runtime Capabilities Specification.md b/docs/pbs/specs/7. Cartridge Manifest and Runtime Capabilities Specification.md index 50736d2b..28ae2274 100644 --- a/docs/pbs/specs/7. Cartridge Manifest and Runtime Capabilities Specification.md +++ b/docs/pbs/specs/7. Cartridge Manifest and Runtime Capabilities Specification.md @@ -63,7 +63,7 @@ The current manifest shape relevant to this document is: "title": "Example", "app_version": "1.0.0", "app_mode": "game", - "entrypoint": "main", + "entrypoint": "frame", "capabilities": ["gfx", "input"], "asset_table": [], "preload": [] @@ -75,6 +75,7 @@ Rules: - `magic` identifies a Prometeu cartridge manifest, - `cartridge_version` identifies the cartridge manifest format line, - `entrypoint` identifies the runtime entrypoint, +- for PBS v1, `entrypoint` should align with frontend-declared callable (`frame`), - `capabilities` declares the cartridge's requested runtime capabilities, - `asset_table` and `preload` remain separate concerns. diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSDefinitions.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSDefinitions.java index faaf199b..5e73a613 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSDefinitions.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSDefinitions.java @@ -9,5 +9,6 @@ public class PBSDefinitions { .languageId("pbs") .allowedExtensions(ReadOnlySet.from("pbs", "barrel")) .sourceRoots(ReadOnlySet.from("src")) + .entryPointCallableName("frame") .build(); } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsFrontendCompiler.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsFrontendCompiler.java index fe04b8ad..1a56c5d8 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsFrontendCompiler.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsFrontendCompiler.java @@ -120,10 +120,43 @@ public final class PbsFrontendCompiler { final String moduleKey, final HostAdmissionContext hostAdmissionContext, final NameTable nameTable) { + return compileParsedFile( + fileId, + ast, + diagnostics, + sourceKind, + moduleKey, + hostAdmissionContext, + nameTable, + ReadOnlyList.empty(), + ReadOnlyList.empty(), + IRReservedMetadata.empty()); + } + + public IRBackendFile compileParsedFile( + final FileId fileId, + final PbsAst.File ast, + final DiagnosticSink diagnostics, + final SourceKind sourceKind, + final String moduleKey, + final HostAdmissionContext hostAdmissionContext, + final NameTable nameTable, + final ReadOnlyList supplementalTopDecls, + final ReadOnlyList importedCallables, + final IRReservedMetadata importedReservedMetadata) { final var effectiveNameTable = nameTable == null ? new NameTable() : nameTable; + final var effectiveSupplementalTopDecls = supplementalTopDecls == null + ? ReadOnlyList.empty() + : supplementalTopDecls; + final var effectiveImportedCallables = importedCallables == null + ? ReadOnlyList.empty() + : importedCallables; + final var effectiveImportedReservedMetadata = importedReservedMetadata == null + ? IRReservedMetadata.empty() + : importedReservedMetadata; final var semanticsErrorBaseline = diagnostics.errorCount(); new PbsDeclarationSemanticsValidator(effectiveNameTable).validate(ast, sourceKind, diagnostics); - flowSemanticsValidator.validate(ast, diagnostics); + flowSemanticsValidator.validate(ast, effectiveSupplementalTopDecls, diagnostics); if (diagnostics.errorCount() > semanticsErrorBaseline) { return IRBackendFile.empty(fileId); } @@ -142,14 +175,22 @@ public final class PbsFrontendCompiler { extractedReservedMetadata.builtinTypeSurfaces(), extractedReservedMetadata.builtinConstSurfaces(), requiredCapabilities); + final var effectiveReservedMetadata = mergeReservedMetadata( + reservedMetadata, + effectiveImportedReservedMetadata); final ReadOnlyList functions = sourceKind == SourceKind.SDK_INTERFACE ? ReadOnlyList.empty() : lowerFunctions(fileId, ast); final var loweringErrorBaseline = diagnostics.errorCount(); - final var executableLowering = sourceKind == SourceKind.SDK_INTERFACE - ? new ExecutableLoweringResult(ReadOnlyList.empty(), ReadOnlyList.empty(), ReadOnlyList.empty()) - : lowerExecutableFunctions(fileId, ast, moduleKey, reservedMetadata, effectiveNameTable, diagnostics); + final var executableLowering = lowerExecutableFunctions( + fileId, + ast, + moduleKey, + effectiveReservedMetadata, + effectiveNameTable, + diagnostics, + effectiveImportedCallables); if (diagnostics.errorCount() > loweringErrorBaseline) { return IRBackendFile.empty(fileId); } @@ -181,7 +222,8 @@ public final class PbsFrontendCompiler { final String moduleKey, final IRReservedMetadata reservedMetadata, final NameTable nameTable, - final DiagnosticSink diagnostics) { + final DiagnosticSink diagnostics, + final ReadOnlyList importedCallables) { final var normalizedModuleKey = moduleKey == null ? "" : moduleKey; final var hostByMethodName = new HashMap>(); for (final var hostBinding : reservedMetadata.hostMethodBindings()) { @@ -199,31 +241,49 @@ public final class PbsFrontendCompiler { } final var callableIdTable = new CallableTable(); final var callableIdsByNameAndArity = new HashMap>(); - final var callableIdByDeclaration = new HashMap(); + final var callableIdByDeclaration = new HashMap(); final var returnSlotsByCallableId = new HashMap(); final var intrinsicIdTable = new IntrinsicTable(); final var typeSurfaceTable = new TypeSurfaceTable(); final var callableShapeTable = new CallableShapeTable(); - for (final var declaredFn : ast.functions()) { + final var localCallables = collectLocalLowerableCallables(ast); + for (final var declaredCallable : localCallables) { + final var declaredFn = declaredCallable.functionDecl(); final var callableShapeId = callableShapeId(declaredFn, typeSurfaceTable, callableShapeTable); final var callableId = callableIdTable.register( normalizedModuleKey, - declaredFn.name(), + declaredCallable.callableName(), declaredFn.parameters().size(), callableShapeSurface(callableShapeId, typeSurfaceTable, callableShapeTable)); - callableIdByDeclaration.put(declaredFn, callableId); + callableIdByDeclaration.put(declaredCallable, callableId); callableIdsByNameAndArity .computeIfAbsent( - new CallableResolutionKey(nameTable.register(declaredFn.name()), declaredFn.parameters().size()), + new CallableResolutionKey(nameTable.register(declaredCallable.callableName()), declaredFn.parameters().size()), ignored -> new ArrayList<>()) .add(callableId); final var retSlots = returnSlotsFor(declaredFn); returnSlotsByCallableId.put(callableId, retSlots); } + for (final var importedCallable : importedCallables) { + final var callableId = callableIdTable.register( + importedCallable.moduleKey(), + importedCallable.callableName(), + importedCallable.arity(), + importedCallable.shapeSurface()); + callableIdsByNameAndArity + .computeIfAbsent( + new CallableResolutionKey( + nameTable.register(importedCallable.callableName()), + importedCallable.arity()), + ignored -> new ArrayList<>()) + .add(callableId); + returnSlotsByCallableId.put(callableId, importedCallable.returnSlots()); + } - final var executableFunctions = new ArrayList(ast.functions().size()); - for (final var fn : ast.functions()) { - final var functionCallableId = callableIdByDeclaration.get(fn); + final var executableFunctions = new ArrayList(localCallables.size()); + for (final var callable : localCallables) { + final var fn = callable.functionDecl(); + final var functionCallableId = callableIdByDeclaration.get(callable); if (functionCallableId == null) { continue; } @@ -268,7 +328,7 @@ public final class PbsFrontendCompiler { executableFunctions.add(new IRBackendExecutableFunction( fileId, normalizedModuleKey, - fn.name(), + callable.callableName(), functionCallableId, start, end, @@ -300,6 +360,48 @@ public final class PbsFrontendCompiler { }; } + private ReadOnlyList collectLocalLowerableCallables(final PbsAst.File ast) { + final var callables = new ArrayList(); + for (final var functionDecl : ast.functions()) { + callables.add(new LowerableCallable(functionDecl.name(), functionDecl)); + } + for (final var topDecl : ast.topDecls()) { + if (!(topDecl instanceof PbsAst.ServiceDecl serviceDecl)) { + continue; + } + for (final var method : serviceDecl.methods()) { + callables.add(new LowerableCallable(serviceDecl.name() + "." + method.name(), method)); + } + } + return ReadOnlyList.wrap(callables); + } + + private IRReservedMetadata mergeReservedMetadata( + final IRReservedMetadata primary, + final IRReservedMetadata imported) { + final var hostBindings = new ArrayList(); + hostBindings.addAll(primary.hostMethodBindings().asList()); + hostBindings.addAll(imported.hostMethodBindings().asList()); + + final var builtinTypeSurfaces = new ArrayList(); + builtinTypeSurfaces.addAll(primary.builtinTypeSurfaces().asList()); + builtinTypeSurfaces.addAll(imported.builtinTypeSurfaces().asList()); + + final var builtinConstSurfaces = new ArrayList(); + builtinConstSurfaces.addAll(primary.builtinConstSurfaces().asList()); + builtinConstSurfaces.addAll(imported.builtinConstSurfaces().asList()); + + final var requiredCapabilities = new HashSet(); + requiredCapabilities.addAll(primary.requiredCapabilities().asList()); + requiredCapabilities.addAll(imported.requiredCapabilities().asList()); + + return new IRReservedMetadata( + ReadOnlyList.wrap(hostBindings), + ReadOnlyList.wrap(builtinTypeSurfaces), + ReadOnlyList.wrap(builtinConstSurfaces), + ReadOnlyList.wrap(requiredCapabilities.stream().toList())); + } + private CallableShapeId callableShapeId( final PbsAst.FunctionDecl functionDecl, final TypeSurfaceTable typeSurfaceTable, @@ -315,6 +417,13 @@ public final class PbsFrontendCompiler { return callableShapeTable.register(ReadOnlyList.wrap(parameterSurfaceIds), outputSurfaceId); } + public String callableShapeSurfaceOf(final PbsAst.FunctionDecl functionDecl) { + final var typeSurfaceTable = new TypeSurfaceTable(); + final var callableShapeTable = new CallableShapeTable(); + final var callableShapeId = callableShapeId(functionDecl, typeSurfaceTable, callableShapeTable); + return callableShapeSurface(callableShapeId, typeSurfaceTable, callableShapeTable); + } + private String callableShapeSurface( final CallableShapeId callableShapeId, final TypeSurfaceTable typeSurfaceTable, @@ -600,10 +709,26 @@ public final class PbsFrontendCompiler { reportUnsupportedLowering("executable lowering requires resolvable callee identity", callExpr.span(), context); return; } - final var callableCandidates = context.callableIdsByNameAndArity().get( - new CallableResolutionKey(calleeIdentity.nameId(), callExpr.arguments().size())); - final var hostCandidates = context.hostByMethodName().getOrDefault(calleeIdentity.nameId(), List.of()); - final var intrinsicCandidates = context.intrinsicByMethodName().getOrDefault(calleeIdentity.nameId(), List.of()); + final var callableCandidates = new ArrayList(); + final var primaryCallableCandidates = context.callableIdsByNameAndArity().get( + new CallableResolutionKey(calleeIdentity.primaryCallableNameId(), callExpr.arguments().size())); + if (primaryCallableCandidates != null) { + callableCandidates.addAll(primaryCallableCandidates); + } + if (calleeIdentity.secondaryCallableNameId() != null) { + final var secondaryCallableCandidates = context.callableIdsByNameAndArity().get( + new CallableResolutionKey(calleeIdentity.secondaryCallableNameId(), callExpr.arguments().size())); + if (secondaryCallableCandidates != null) { + for (final var candidate : secondaryCallableCandidates) { + if (!callableCandidates.contains(candidate)) { + callableCandidates.add(candidate); + } + } + } + } + + final var hostCandidates = context.hostByMethodName().getOrDefault(calleeIdentity.memberNameId(), List.of()); + final var intrinsicCandidates = context.intrinsicByMethodName().getOrDefault(calleeIdentity.memberNameId(), List.of()); var categoryCount = 0; if (callableCandidates != null && !callableCandidates.isEmpty()) { @@ -651,6 +776,7 @@ public final class PbsFrontendCompiler { return; } final var intrinsic = intrinsicCandidates.getFirst(); + final var effectiveArgSlots = intrinsic.argSlots() + implicitReceiverArgSlots(callExpr.callee()); context.instructions().add(new IRBackendExecutableFunction.Instruction( IRBackendExecutableFunction.InstructionKind.CALL_INTRINSIC, "", @@ -660,13 +786,13 @@ public final class PbsFrontendCompiler { intrinsic.canonicalName(), intrinsic.canonicalVersion(), context.intrinsicIdTable().register(intrinsic.canonicalName(), intrinsic.canonicalVersion())), - intrinsic.argSlots(), + effectiveArgSlots, intrinsic.retSlots(), callExpr.span())); return; } - if (callableCandidates == null || callableCandidates.isEmpty()) { + if (callableCandidates.isEmpty()) { reportUnsupportedLowering("executable lowering requires resolvable callable identity", callExpr.span(), context); return; } @@ -678,7 +804,7 @@ public final class PbsFrontendCompiler { context.instructions().add(new IRBackendExecutableFunction.Instruction( IRBackendExecutableFunction.InstructionKind.CALL_FUNC, context.moduleKey(), - calleeIdentity.displayName(), + calleeIdentity.primaryCallableDisplayName(), calleeCallableId, null, null, @@ -692,16 +818,71 @@ public final class PbsFrontendCompiler { final NameTable nameTable) { return switch (callee) { case PbsAst.IdentifierExpr identifierExpr -> - new CalleeIdentity(nameTable.register(identifierExpr.name()), identifierExpr.name()); - // Member access does not carry executable callsite identity in v1 lowering. - case PbsAst.MemberExpr ignored -> null; + new CalleeIdentity( + nameTable.register(identifierExpr.name()), + identifierExpr.name(), + null, + "", + nameTable.register(identifierExpr.name())); + case PbsAst.MemberExpr memberExpr -> { + final var memberName = memberExpr.memberName(); + final var rootName = memberRootName(memberExpr.receiver()); + final var qualified = rootName == null || rootName.isBlank() + ? memberName + : rootName + "." + memberName; + final NameId secondaryNameId; + final String secondaryDisplayName; + if (qualified.equals(memberName)) { + secondaryNameId = null; + secondaryDisplayName = ""; + } else { + secondaryNameId = nameTable.register(memberName); + secondaryDisplayName = memberName; + } + yield new CalleeIdentity( + nameTable.register(qualified), + qualified, + secondaryNameId, + secondaryDisplayName, + nameTable.register(memberName)); + } case PbsAst.BindExpr bindExpr -> - new CalleeIdentity(nameTable.register(bindExpr.functionName()), bindExpr.functionName()); + new CalleeIdentity( + nameTable.register(bindExpr.functionName()), + bindExpr.functionName(), + null, + "", + nameTable.register(bindExpr.functionName())); case PbsAst.GroupExpr groupExpr -> resolveCalleeIdentity(groupExpr.expression(), nameTable); default -> null; }; } + private String memberRootName(final PbsAst.Expression expression) { + return switch (expression) { + case PbsAst.IdentifierExpr identifierExpr -> identifierExpr.name(); + case PbsAst.MemberExpr memberExpr -> memberRootName(memberExpr.receiver()); + case PbsAst.CallExpr callExpr -> memberRootName(callExpr.callee()); + case PbsAst.GroupExpr groupExpr -> memberRootName(groupExpr.expression()); + default -> null; + }; + } + + private int implicitReceiverArgSlots(final PbsAst.Expression callee) { + if (!(callee instanceof PbsAst.MemberExpr memberExpr)) { + return 0; + } + return receiverProducesRuntimeValue(memberExpr.receiver()) ? 1 : 0; + } + + private boolean receiverProducesRuntimeValue(final PbsAst.Expression receiver) { + return switch (receiver) { + case PbsAst.CallExpr ignored -> true; + case PbsAst.GroupExpr groupExpr -> receiverProducesRuntimeValue(groupExpr.expression()); + default -> false; + }; + } + private void emitJump( final IRBackendExecutableFunction.InstructionKind jumpKind, final String targetLabel, @@ -785,7 +966,8 @@ public final class PbsFrontendCompiler { switch (instruction.kind()) { case CALL_FUNC -> outHeight += instruction.expectedRetSlots() == null ? 0 : instruction.expectedRetSlots(); case CALL_HOST -> outHeight += instruction.hostCall() == null ? 0 : instruction.hostCall().retSlots(); - case CALL_INTRINSIC, HALT, LABEL, JMP, RET -> { + case CALL_INTRINSIC -> outHeight += instruction.expectedRetSlots() == null ? 0 : instruction.expectedRetSlots(); + case HALT, LABEL, JMP, RET -> { } case JMP_IF_TRUE, JMP_IF_FALSE -> { if (outHeight <= 0) { @@ -947,14 +1129,30 @@ public final class PbsFrontendCompiler { ReadOnlyList intrinsicPool) { } + private record LowerableCallable( + String callableName, + PbsAst.FunctionDecl functionDecl) { + } + private record CallableResolutionKey( NameId callableNameId, int arity) { } private record CalleeIdentity( - NameId nameId, - String displayName) { + NameId primaryCallableNameId, + String primaryCallableDisplayName, + NameId secondaryCallableNameId, + String secondaryCallableDisplayName, + NameId memberNameId) { + } + + public record ImportedCallableSurface( + String moduleKey, + String callableName, + int arity, + int returnSlots, + String shapeSurface) { } private static final class ExecutableLoweringAnalysisException extends RuntimeException { diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/metadata/PbsReservedMetadataExtractor.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/metadata/PbsReservedMetadataExtractor.java index 45f52317..94214b39 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/metadata/PbsReservedMetadataExtractor.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/metadata/PbsReservedMetadataExtractor.java @@ -119,9 +119,10 @@ public final class PbsReservedMetadataExtractor { continue; } final var intrinsicMetadata = intrinsicAttribute.get(); + final var intrinsicName = stringArgument(intrinsicMetadata, "name").orElse(signature.name()); intrinsics.add(new IRReservedMetadata.IntrinsicSurface( signature.name(), - stringArgument(intrinsicMetadata, "name").orElse(signature.name()), + canonicalIntrinsicName(canonicalTypeName, intrinsicName), longArgument(intrinsicMetadata, "version").orElse(canonicalVersion), signature.parameters().size(), switch (signature.returnKind()) { @@ -215,6 +216,23 @@ public final class PbsReservedMetadataExtractor { return rawValue; } + private String canonicalIntrinsicName( + final String canonicalTypeName, + final String intrinsicName) { + final var normalizedTypeName = canonicalTypeName == null ? "" : canonicalTypeName.trim(); + final var normalizedIntrinsicName = intrinsicName == null ? "" : intrinsicName.trim(); + if (normalizedIntrinsicName.isBlank()) { + return normalizedTypeName; + } + if (normalizedIntrinsicName.contains(".")) { + return normalizedIntrinsicName; + } + if (normalizedTypeName.isBlank()) { + return normalizedIntrinsicName; + } + return normalizedTypeName + "." + normalizedIntrinsicName; + } + private String typeSurfaceKey(final PbsAst.TypeRef typeRef) { if (typeRef == null) { return "unit"; diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsExprParser.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsExprParser.java index 01ad48f8..6a11e2ea 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsExprParser.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsExprParser.java @@ -285,7 +285,7 @@ final class PbsExprParser { } if (cursor.match(PbsTokenKind.DOT)) { - final var member = consume(PbsTokenKind.IDENTIFIER, "Expected member name after '.'"); + final var member = consumeMemberName("Expected member name after '.'"); expression = new PbsAst.MemberExpr( expression, member.lexeme(), @@ -631,6 +631,18 @@ final class PbsExprParser { return token; } + private PbsToken consumeMemberName(final String message) { + if (cursor.check(PbsTokenKind.IDENTIFIER) || cursor.check(PbsTokenKind.ERROR)) { + return cursor.advance(); + } + final var token = cursor.peek(); + report(token, ParseErrors.E_PARSE_EXPECTED_TOKEN, message + ", found " + token.kind()); + if (!cursor.isAtEnd()) { + return cursor.advance(); + } + return token; + } + private Span span(final long start, final long end) { return new Span(fileId, start, end); } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsParser.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsParser.java index edecf881..802d8184 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsParser.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/parser/PbsParser.java @@ -730,7 +730,7 @@ public final class PbsParser { } private PbsAst.FunctionDecl parseFunctionLike(final PbsToken fnToken) { - final var name = consume(PbsTokenKind.IDENTIFIER, "Expected function name"); + final var name = consumeCallableName("Expected function name"); consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after function name"); final var parameters = parseParametersUntilRightParen(); final var rightParen = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after parameter list"); @@ -752,7 +752,7 @@ public final class PbsParser { final PbsToken fnToken, final boolean requireSemicolon, final ReadOnlyList attributes) { - final var name = consume(PbsTokenKind.IDENTIFIER, "Expected function name"); + final var name = consumeCallableName("Expected function name"); consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after function name"); final var parameters = parseParametersUntilRightParen(); final var rightParen = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after parameter list"); @@ -774,6 +774,18 @@ public final class PbsParser { span(fnToken.start(), end)); } + private PbsToken consumeCallableName(final String message) { + if (cursor.check(PbsTokenKind.IDENTIFIER) || cursor.check(PbsTokenKind.ERROR)) { + return cursor.advance(); + } + final var token = cursor.peek(); + report(token, ParseErrors.E_PARSE_EXPECTED_TOKEN, message + ", found " + token.kind()); + if (!cursor.isAtEnd()) { + return cursor.advance(); + } + return token; + } + /** * Parses `declare callback` declaration. */ diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsDeclarationSemanticsValidator.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsDeclarationSemanticsValidator.java index 1cd93e03..9fbac5fb 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsDeclarationSemanticsValidator.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsDeclarationSemanticsValidator.java @@ -88,12 +88,6 @@ public final class PbsDeclarationSemanticsValidator { if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) { binder.registerType(serviceDecl.name(), serviceDecl.span(), "service"); binder.registerValue(serviceDecl.name(), serviceDecl.span(), "service singleton"); - if (interfaceModule) { - reportInterfaceNonDeclarativeDecl( - serviceDecl.span(), - "Interface modules must not declare executable service bodies", - diagnostics); - } validateServiceDeclaration(serviceDecl, binder, rules); continue; } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowBodyAnalyzer.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowBodyAnalyzer.java index cef6a753..1b8b137c 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowBodyAnalyzer.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowBodyAnalyzer.java @@ -21,7 +21,14 @@ final class PbsFlowBodyAnalyzer { } public void validate(final PbsAst.File ast, final DiagnosticSink diagnostics) { - final var model = Model.from(ast); + validate(ast, ReadOnlyList.empty(), diagnostics); + } + + public void validate( + final PbsAst.File ast, + final ReadOnlyList supplementalTopDecls, + final DiagnosticSink diagnostics) { + final var model = Model.from(ast, supplementalTopDecls); for (final var topDecl : ast.topDecls()) { if (topDecl instanceof PbsAst.FunctionDecl functionDecl) { diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowSemanticSupport.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowSemanticSupport.java index a4b7cd3f..596fcd46 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowSemanticSupport.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowSemanticSupport.java @@ -169,108 +169,208 @@ final class PbsFlowSemanticSupport { final Map> errors = new HashMap<>(); final Map constTypes = new HashMap<>(); final Map serviceSingletons = new HashMap<>(); + final Set knownStructNames = new HashSet<>(); + final Set knownServiceNames = new HashSet<>(); + final Set knownContractNames = new HashSet<>(); + final Set knownEnumNames = new HashSet<>(); + final Set knownErrorNames = new HashSet<>(); + final Set knownCallbackNames = new HashSet<>(); static Model from(final PbsAst.File ast) { + return from(ast, ReadOnlyList.empty()); + } + + static Model from( + final PbsAst.File ast, + final ReadOnlyList supplementalTopDecls) { final var model = new Model(); for (final var topDecl : ast.topDecls()) { - if (topDecl instanceof PbsAst.StructDecl structDecl) { - final var fields = new HashMap(); - for (final var field : structDecl.fields()) { - fields.put( - field.name(), - new StructFieldInfo( - model.typeFrom(field.typeRef()), - field.isPublic(), - field.isMutable())); - } - final var methods = new HashMap>(); - for (final var method : structDecl.methods()) { - methods.computeIfAbsent(method.name(), ignored -> new ArrayList<>()) - .add(model.callableFrom( - method.name(), - method.parameters(), - method.returnKind(), - method.returnType(), - method.resultErrorType(), - method.span())); - } - model.structs.put(structDecl.name(), new StructInfo(fields, methods)); - continue; - } - if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) { - final var methods = new HashMap>(); - for (final var method : serviceDecl.methods()) { - methods.computeIfAbsent(method.name(), ignored -> new ArrayList<>()) - .add(model.callableFrom( - method.name(), - method.parameters(), - method.returnKind(), - method.returnType(), - method.resultErrorType(), - method.span())); - } - model.services.put(serviceDecl.name(), new ServiceInfo(methods)); - model.serviceSingletons.put(serviceDecl.name(), TypeView.service(serviceDecl.name())); - continue; - } - if (topDecl instanceof PbsAst.ContractDecl contractDecl) { - final var methods = new HashMap>(); - for (final var signature : contractDecl.signatures()) { - methods.computeIfAbsent(signature.name(), ignored -> new ArrayList<>()) - .add(model.callableFrom( - signature.name(), - signature.parameters(), - signature.returnKind(), - signature.returnType(), - signature.resultErrorType(), - signature.span())); - } - model.contracts.put(contractDecl.name(), new ContractInfo(methods)); - continue; - } - if (topDecl instanceof PbsAst.FunctionDecl functionDecl) { - model.topLevelCallables.computeIfAbsent(functionDecl.name(), ignored -> new ArrayList<>()) - .add(model.callableFrom( - functionDecl.name(), - functionDecl.parameters(), - functionDecl.returnKind(), - functionDecl.returnType(), - functionDecl.resultErrorType(), - functionDecl.span())); - continue; - } - if (topDecl instanceof PbsAst.CallbackDecl( - String name, ReadOnlyList parameters, PbsAst.ReturnKind returnKind, - PbsAst.TypeRef returnType, PbsAst.TypeRef resultErrorType, Span span - )) { - final var symbol = model.callableFrom( - name, - parameters, - returnKind, - returnType, - resultErrorType, - span); - model.callbacks.put(name, new CallbackSignature(symbol.inputTypes(), symbol.outputType())); - continue; - } - if (topDecl instanceof PbsAst.EnumDecl enumDecl) { - final var cases = new HashSet(); - for (final var enumCase : enumDecl.cases()) { - cases.add(enumCase.name()); - } - model.enums.put(enumDecl.name(), cases); - continue; - } - if (topDecl instanceof PbsAst.ErrorDecl errorDecl) { - model.errors.put(errorDecl.name(), new HashSet<>(errorDecl.cases().asList())); - continue; - } - if (topDecl instanceof PbsAst.ConstDecl constDecl && constDecl.explicitType() != null) { - model.constTypes.put(constDecl.name(), model.typeFrom(constDecl.explicitType())); - } + model.registerKnownTopDecl(topDecl); + } + for (final var topDecl : supplementalTopDecls) { + model.registerKnownTopDecl(topDecl); + } + for (final var topDecl : ast.topDecls()) { + model.ingestTopDecl(topDecl); + } + for (final var topDecl : supplementalTopDecls) { + model.ingestTopDecl(topDecl); } return model; } + + private void registerKnownTopDecl(final PbsAst.TopDecl topDecl) { + if (topDecl instanceof PbsAst.StructDecl structDecl) { + knownStructNames.add(structDecl.name()); + return; + } + if (topDecl instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) { + knownStructNames.add(builtinTypeDecl.name()); + return; + } + if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) { + knownServiceNames.add(serviceDecl.name()); + return; + } + if (topDecl instanceof PbsAst.HostDecl hostDecl) { + knownServiceNames.add(hostDecl.name()); + return; + } + if (topDecl instanceof PbsAst.ContractDecl contractDecl) { + knownContractNames.add(contractDecl.name()); + return; + } + if (topDecl instanceof PbsAst.EnumDecl enumDecl) { + knownEnumNames.add(enumDecl.name()); + return; + } + if (topDecl instanceof PbsAst.ErrorDecl errorDecl) { + knownErrorNames.add(errorDecl.name()); + return; + } + if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) { + knownCallbackNames.add(callbackDecl.name()); + } + } + + private void ingestTopDecl(final PbsAst.TopDecl topDecl) { + if (topDecl instanceof PbsAst.StructDecl structDecl) { + final var fields = new HashMap(); + for (final var field : structDecl.fields()) { + fields.put( + field.name(), + new StructFieldInfo( + typeFrom(field.typeRef()), + field.isPublic(), + field.isMutable())); + } + final var methods = new HashMap>(); + for (final var method : structDecl.methods()) { + methods.computeIfAbsent(method.name(), ignored -> new ArrayList<>()) + .add(callableFrom( + method.name(), + method.parameters(), + method.returnKind(), + method.returnType(), + method.resultErrorType(), + method.span())); + } + structs.put(structDecl.name(), new StructInfo(fields, methods)); + return; + } + if (topDecl instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) { + final var fields = new HashMap(); + for (final var field : builtinTypeDecl.fields()) { + fields.put( + field.name(), + new StructFieldInfo( + typeFrom(field.typeRef()), + true, + false)); + } + final var methods = new HashMap>(); + for (final var signature : builtinTypeDecl.signatures()) { + methods.computeIfAbsent(signature.name(), ignored -> new ArrayList<>()) + .add(callableFrom( + signature.name(), + signature.parameters(), + signature.returnKind(), + signature.returnType(), + signature.resultErrorType(), + signature.span())); + } + structs.put(builtinTypeDecl.name(), new StructInfo(fields, methods)); + return; + } + if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) { + final var methods = new HashMap>(); + for (final var method : serviceDecl.methods()) { + methods.computeIfAbsent(method.name(), ignored -> new ArrayList<>()) + .add(callableFrom( + method.name(), + method.parameters(), + method.returnKind(), + method.returnType(), + method.resultErrorType(), + method.span())); + } + services.put(serviceDecl.name(), new ServiceInfo(methods)); + serviceSingletons.put(serviceDecl.name(), TypeView.service(serviceDecl.name())); + return; + } + if (topDecl instanceof PbsAst.HostDecl hostDecl) { + final var methods = new HashMap>(); + for (final var signature : hostDecl.signatures()) { + methods.computeIfAbsent(signature.name(), ignored -> new ArrayList<>()) + .add(callableFrom( + signature.name(), + signature.parameters(), + signature.returnKind(), + signature.returnType(), + signature.resultErrorType(), + signature.span())); + } + // Host owners are value singletons with callable members, same access shape as services. + services.put(hostDecl.name(), new ServiceInfo(methods)); + serviceSingletons.put(hostDecl.name(), TypeView.service(hostDecl.name())); + return; + } + if (topDecl instanceof PbsAst.ContractDecl contractDecl) { + final var methods = new HashMap>(); + for (final var signature : contractDecl.signatures()) { + methods.computeIfAbsent(signature.name(), ignored -> new ArrayList<>()) + .add(callableFrom( + signature.name(), + signature.parameters(), + signature.returnKind(), + signature.returnType(), + signature.resultErrorType(), + signature.span())); + } + contracts.put(contractDecl.name(), new ContractInfo(methods)); + return; + } + if (topDecl instanceof PbsAst.FunctionDecl functionDecl) { + topLevelCallables.computeIfAbsent(functionDecl.name(), ignored -> new ArrayList<>()) + .add(callableFrom( + functionDecl.name(), + functionDecl.parameters(), + functionDecl.returnKind(), + functionDecl.returnType(), + functionDecl.resultErrorType(), + functionDecl.span())); + return; + } + if (topDecl instanceof PbsAst.CallbackDecl( + String name, ReadOnlyList parameters, PbsAst.ReturnKind returnKind, + PbsAst.TypeRef returnType, PbsAst.TypeRef resultErrorType, Span span + )) { + final var symbol = callableFrom( + name, + parameters, + returnKind, + returnType, + resultErrorType, + span); + callbacks.put(name, new CallbackSignature(symbol.inputTypes(), symbol.outputType())); + return; + } + if (topDecl instanceof PbsAst.EnumDecl enumDecl) { + final var cases = new HashSet(); + for (final var enumCase : enumDecl.cases()) { + cases.add(enumCase.name()); + } + enums.put(enumDecl.name(), cases); + return; + } + if (topDecl instanceof PbsAst.ErrorDecl errorDecl) { + errors.put(errorDecl.name(), new HashSet<>(errorDecl.cases().asList())); + return; + } + if (topDecl instanceof PbsAst.ConstDecl constDecl && constDecl.explicitType() != null) { + constTypes.put(constDecl.name(), typeFrom(constDecl.explicitType())); + } + } private CallableSymbol callableFrom( final String name, @@ -324,25 +424,28 @@ final class PbsFlowSemanticSupport { if ("str".equals(typeRef.name())) { yield TypeView.str(); } - if (structs.containsKey(typeRef.name())) { + if (structs.containsKey(typeRef.name()) || knownStructNames.contains(typeRef.name())) { yield TypeView.struct(typeRef.name()); } - if (services.containsKey(typeRef.name())) { + if (services.containsKey(typeRef.name()) || knownServiceNames.contains(typeRef.name())) { yield TypeView.service(typeRef.name()); } - if (contracts.containsKey(typeRef.name())) { + if (contracts.containsKey(typeRef.name()) || knownContractNames.contains(typeRef.name())) { yield TypeView.contract(typeRef.name()); } - if (enums.containsKey(typeRef.name())) { + if (enums.containsKey(typeRef.name()) || knownEnumNames.contains(typeRef.name())) { yield TypeView.enumType(typeRef.name()); } - if (errors.containsKey(typeRef.name())) { + if (errors.containsKey(typeRef.name()) || knownErrorNames.contains(typeRef.name())) { yield TypeView.error(typeRef.name()); } final var callbackSignature = callbacks.get(typeRef.name()); if (callbackSignature != null) { yield TypeView.callback(typeRef.name(), callbackSignature.inputTypes(), callbackSignature.outputType()); } + if (knownCallbackNames.contains(typeRef.name())) { + yield TypeView.callback(typeRef.name(), List.of(), TypeView.unknown()); + } yield TypeView.unknown(); } case OPTIONAL -> TypeView.optional(typeFrom(typeRef.inner())); diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowSemanticsValidator.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowSemanticsValidator.java index 987797d4..a9816dc7 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowSemanticsValidator.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowSemanticsValidator.java @@ -2,11 +2,19 @@ package p.studio.compiler.pbs.semantics; import p.studio.compiler.pbs.ast.PbsAst; import p.studio.compiler.source.diagnostics.DiagnosticSink; +import p.studio.utilities.structures.ReadOnlyList; public final class PbsFlowSemanticsValidator { private final PbsFlowBodyAnalyzer flowBodyAnalyzer = new PbsFlowBodyAnalyzer(); public void validate(final PbsAst.File ast, final DiagnosticSink diagnostics) { - flowBodyAnalyzer.validate(ast, diagnostics); + flowBodyAnalyzer.validate(ast, ReadOnlyList.empty(), diagnostics); + } + + public void validate( + final PbsAst.File ast, + final ReadOnlyList supplementalTopDecls, + final DiagnosticSink diagnostics) { + flowBodyAnalyzer.validate(ast, supplementalTopDecls, diagnostics); } } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java index 038c61b8..95b51a65 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java @@ -1,14 +1,17 @@ package p.studio.compiler.services; import lombok.extern.slf4j.Slf4j; +import p.studio.compiler.PBSDefinitions; import p.studio.compiler.messages.BuildingIssueSink; import p.studio.compiler.messages.FrontendPhaseContext; import p.studio.compiler.models.IRBackend; +import p.studio.compiler.models.IRReservedMetadata; import p.studio.compiler.models.ProjectDescriptor; import p.studio.compiler.models.SourceHandle; import p.studio.compiler.models.SourceKind; import p.studio.compiler.pbs.PbsFrontendCompiler; import p.studio.compiler.pbs.ast.PbsAst; +import p.studio.compiler.pbs.metadata.PbsReservedMetadataExtractor; import p.studio.compiler.pbs.lexer.PbsLexer; import p.studio.compiler.pbs.linking.PbsLinkErrors; import p.studio.compiler.pbs.linking.PbsModuleVisibilityValidator; @@ -42,6 +45,7 @@ import java.util.Set; @Slf4j public class PBSFrontendPhaseService implements FrontendPhaseService { private final PbsFrontendCompiler frontendCompiler = new PbsFrontendCompiler(); + private final PbsReservedMetadataExtractor reservedMetadataExtractor = new PbsReservedMetadataExtractor(); private final PbsModuleVisibilityValidator moduleVisibilityValidator = new PbsModuleVisibilityValidator(); private final StdlibEnvironmentResolver stdlibEnvironmentResolver; private final InterfaceModuleLoader interfaceModuleLoader; @@ -154,6 +158,7 @@ public class PBSFrontendPhaseService implements FrontendPhaseService { moduleVisibilityValidator.validate(ReadOnlyList.wrap(modules), nameTable, diagnostics); markModulesWithLinkingErrors(diagnostics, moduleIdByFile, failedModuleIds); final var moduleDependencyGraph = buildModuleDependencyGraph(parsedSourceFiles, moduleTable); + final var importedSemanticContexts = buildImportedSemanticContexts(parsedSourceFiles, moduleTable); final var compiledSourceFiles = new ArrayList(parsedSourceFiles.size()); for (final var parsedSource : parsedSourceFiles) { @@ -161,6 +166,9 @@ public class PBSFrontendPhaseService implements FrontendPhaseService { if (blockedModuleIds.contains(parsedSource.moduleId())) { continue; } + final var importedSemanticContext = importedSemanticContexts.getOrDefault( + parsedSource.fileId(), + ImportedSemanticContext.empty()); final var compileErrorBaseline = diagnostics.errorCount(); final var irBackendFile = frontendCompiler.compileParsedFile( parsedSource.fileId(), @@ -169,7 +177,10 @@ public class PBSFrontendPhaseService implements FrontendPhaseService { parsedSource.sourceKind(), renderModuleKey(moduleTable, parsedSource.moduleId()), ctx.hostAdmissionContext(), - nameTable); + nameTable, + importedSemanticContext.supplementalTopDecls(), + importedSemanticContext.importedCallables(), + importedSemanticContext.importedReservedMetadata()); if (diagnostics.errorCount() > compileErrorBaseline) { failedModuleIds.add(parsedSource.moduleId()); } @@ -183,6 +194,7 @@ public class PBSFrontendPhaseService implements FrontendPhaseService { } irBackendAggregator.merge(compiledSource.irBackendFile()); } + irBackendAggregator.entryPointCallableName(PBSDefinitions.PBS.getEntryPointCallableName()); final var irBackend = irBackendAggregator.emit(); logs.using(log).debug("PBS frontend lowered to IR BE:\n%s".formatted(irBackend)); @@ -358,6 +370,383 @@ public class PBSFrontendPhaseService implements FrontendPhaseService { return dependenciesByModule; } + private Map buildImportedSemanticContexts( + final ArrayList parsedSourceFiles, + final ModuleTable moduleTable) { + final Map> sourcesByModule = new HashMap<>(); + for (final var parsedSource : parsedSourceFiles) { + sourcesByModule + .computeIfAbsent(parsedSource.moduleId(), ignored -> new ArrayList<>()) + .add(parsedSource); + } + + final Map reservedMetadataByModule = new HashMap<>(); + for (final var entry : sourcesByModule.entrySet()) { + var merged = IRReservedMetadata.empty(); + for (final var source : entry.getValue()) { + final var extracted = reservedMetadataExtractor.extract(source.ast(), source.sourceKind()); + merged = mergeReservedMetadata(merged, extracted); + } + reservedMetadataByModule.put(entry.getKey(), merged); + } + final Map>> topDeclsByNameByModule = new HashMap<>(); + for (final var entry : sourcesByModule.entrySet()) { + topDeclsByNameByModule.put(entry.getKey(), indexTopDeclsByName(entry.getValue())); + } + + final Map contexts = new HashMap<>(); + for (final var parsedSource : parsedSourceFiles) { + final var supplementalTopDecls = new ArrayList(); + final var importedCallables = new ArrayList(); + final var importedCallableKeys = new HashSet(); + final var supplementalKeys = new HashSet(); + var importedReservedMetadata = IRReservedMetadata.empty(); + + for (final var importDecl : parsedSource.ast().imports()) { + final var moduleRef = importDecl.moduleRef(); + final var importedModuleId = moduleId(moduleTable, moduleRef.project(), moduleRef.pathSegments()); + final var importedSources = sourcesByModule.get(importedModuleId); + if (importedSources == null || importedSources.isEmpty()) { + continue; + } + final var importedTopDeclsByName = topDeclsByNameByModule.get(importedModuleId); + if (importedTopDeclsByName == null) { + continue; + } + final var importedModuleKey = renderModuleKey(moduleTable, importedModuleId); + importedReservedMetadata = mergeReservedMetadata( + importedReservedMetadata, + reservedMetadataByModule.getOrDefault(importedModuleId, IRReservedMetadata.empty())); + + for (final var importItem : importDecl.items()) { + final var importName = importItem.name(); + final var localName = importItemLocalName(importItem); + final var directDecls = importedTopDeclsByName.getOrDefault(importName, new ArrayList<>()); + for (final var topDecl : directDecls) { + appendSupplementalTopDecl(topDecl, supplementalTopDecls, supplementalKeys); + appendTypeDependencyDecls(topDecl, importedTopDeclsByName, supplementalTopDecls, supplementalKeys); + if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) { + for (final var method : serviceDecl.methods()) { + appendImportedCallable( + importedCallables, + importedCallableKeys, + new PbsFrontendCompiler.ImportedCallableSurface( + importedModuleKey, + localName + "." + method.name(), + method.parameters().size(), + returnSlotsFor(method), + frontendCompiler.callableShapeSurfaceOf(method))); + } + continue; + } + if (topDecl instanceof PbsAst.FunctionDecl functionDecl) { + appendImportedCallable( + importedCallables, + importedCallableKeys, + new PbsFrontendCompiler.ImportedCallableSurface( + importedModuleKey, + localName, + functionDecl.parameters().size(), + returnSlotsFor(functionDecl), + frontendCompiler.callableShapeSurfaceOf(functionDecl))); + } + } + } + } + + contexts.put(parsedSource.fileId(), new ImportedSemanticContext( + ReadOnlyList.wrap(supplementalTopDecls), + ReadOnlyList.wrap(importedCallables), + importedReservedMetadata)); + } + return contexts; + } + + private Map> indexTopDeclsByName( + final ArrayList moduleSources) { + final Map> index = new HashMap<>(); + for (final var source : moduleSources) { + for (final var topDecl : source.ast().topDecls()) { + final var declName = topDeclName(topDecl); + if (declName == null || declName.isBlank()) { + continue; + } + index.computeIfAbsent(declName, ignored -> new ArrayList<>()).add(topDecl); + } + } + return index; + } + + private void appendTypeDependencyDecls( + final PbsAst.TopDecl rootDecl, + final Map> topDeclsByName, + final ArrayList supplementalTopDecls, + final Set supplementalKeys) { + final var pendingTypeNames = new ArrayDeque(); + final var visitedTypeNames = new HashSet(); + collectReferencedTypeNames(rootDecl, pendingTypeNames); + while (!pendingTypeNames.isEmpty()) { + final var typeName = pendingTypeNames.removeFirst(); + if (!visitedTypeNames.add(typeName)) { + continue; + } + final var candidates = topDeclsByName.get(typeName); + if (candidates == null) { + continue; + } + for (final var candidate : candidates) { + if (!isTypeDecl(candidate)) { + continue; + } + appendSupplementalTopDecl(candidate, supplementalTopDecls, supplementalKeys); + collectReferencedTypeNames(candidate, pendingTypeNames); + } + } + } + + private void collectReferencedTypeNames( + final PbsAst.TopDecl topDecl, + final ArrayDeque sink) { + if (topDecl instanceof PbsAst.StructDecl structDecl) { + for (final var field : structDecl.fields()) { + collectReferencedTypeNames(field.typeRef(), sink); + } + for (final var method : structDecl.methods()) { + collectReferencedTypeNames(method, sink); + } + for (final var ctor : structDecl.ctors()) { + for (final var parameter : ctor.parameters()) { + collectReferencedTypeNames(parameter.typeRef(), sink); + } + } + return; + } + if (topDecl instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) { + for (final var field : builtinTypeDecl.fields()) { + collectReferencedTypeNames(field.typeRef(), sink); + } + for (final var signature : builtinTypeDecl.signatures()) { + collectReferencedTypeNames(signature, sink); + } + return; + } + if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) { + for (final var method : serviceDecl.methods()) { + collectReferencedTypeNames(method, sink); + } + return; + } + if (topDecl instanceof PbsAst.HostDecl hostDecl) { + for (final var signature : hostDecl.signatures()) { + collectReferencedTypeNames(signature, sink); + } + return; + } + if (topDecl instanceof PbsAst.ContractDecl contractDecl) { + for (final var signature : contractDecl.signatures()) { + collectReferencedTypeNames(signature, sink); + } + return; + } + if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) { + collectReferencedTypeNames(callbackDecl.parameters(), callbackDecl.returnType(), callbackDecl.resultErrorType(), sink); + return; + } + if (topDecl instanceof PbsAst.FunctionDecl functionDecl) { + collectReferencedTypeNames(functionDecl, sink); + return; + } + if (topDecl instanceof PbsAst.ConstDecl constDecl && constDecl.explicitType() != null) { + collectReferencedTypeNames(constDecl.explicitType(), sink); + } + } + + private void collectReferencedTypeNames( + final PbsAst.FunctionDecl functionDecl, + final ArrayDeque sink) { + collectReferencedTypeNames( + functionDecl.parameters(), + functionDecl.returnType(), + functionDecl.resultErrorType(), + sink); + } + + private void collectReferencedTypeNames( + final PbsAst.FunctionSignature functionSignature, + final ArrayDeque sink) { + collectReferencedTypeNames( + functionSignature.parameters(), + functionSignature.returnType(), + functionSignature.resultErrorType(), + sink); + } + + private void collectReferencedTypeNames( + final ReadOnlyList parameters, + final PbsAst.TypeRef returnType, + final PbsAst.TypeRef resultErrorType, + final ArrayDeque sink) { + for (final var parameter : parameters) { + collectReferencedTypeNames(parameter.typeRef(), sink); + } + collectReferencedTypeNames(returnType, sink); + collectReferencedTypeNames(resultErrorType, sink); + } + + private void collectReferencedTypeNames( + final PbsAst.TypeRef typeRef, + final ArrayDeque sink) { + if (typeRef == null) { + return; + } + switch (typeRef.kind()) { + case SIMPLE -> { + final var simpleName = typeRef.name(); + if (!isPrimitiveTypeName(simpleName)) { + sink.addLast(simpleName); + } + } + case OPTIONAL, GROUP -> collectReferencedTypeNames(typeRef.inner(), sink); + case NAMED_TUPLE -> { + for (final var field : typeRef.fields()) { + collectReferencedTypeNames(field.typeRef(), sink); + } + } + case UNIT, SELF, ERROR -> { + } + } + } + + private boolean isPrimitiveTypeName(final String typeName) { + return "int".equals(typeName) + || "float".equals(typeName) + || "bool".equals(typeName) + || "str".equals(typeName) + || "unit".equals(typeName); + } + + private boolean isTypeDecl(final PbsAst.TopDecl topDecl) { + return topDecl instanceof PbsAst.StructDecl + || topDecl instanceof PbsAst.BuiltinTypeDecl + || topDecl instanceof PbsAst.ServiceDecl + || topDecl instanceof PbsAst.HostDecl + || topDecl instanceof PbsAst.ContractDecl + || topDecl instanceof PbsAst.CallbackDecl + || topDecl instanceof PbsAst.EnumDecl + || topDecl instanceof PbsAst.ErrorDecl; + } + + private void appendSupplementalTopDecl( + final PbsAst.TopDecl topDecl, + final ArrayList supplementalTopDecls, + final Set supplementalKeys) { + final var key = topDeclKey(topDecl); + if (key == null || key.isBlank()) { + return; + } + if (supplementalKeys.add(key)) { + supplementalTopDecls.add(topDecl); + } + } + + private void appendImportedCallable( + final ArrayList importedCallables, + final Set importedCallableKeys, + final PbsFrontendCompiler.ImportedCallableSurface importedCallableSurface) { + final var callableKey = importedCallableSurface.moduleKey() + + "#" + + importedCallableSurface.callableName() + + "#" + + importedCallableSurface.arity() + + "#" + + importedCallableSurface.shapeSurface(); + if (importedCallableKeys.add(callableKey)) { + importedCallables.add(importedCallableSurface); + } + } + + private String topDeclKey(final PbsAst.TopDecl topDecl) { + final var declName = topDeclName(topDecl); + if (declName == null || declName.isBlank()) { + return null; + } + return topDecl.getClass().getSimpleName() + ":" + declName; + } + + private String topDeclName(final PbsAst.TopDecl topDecl) { + if (topDecl instanceof PbsAst.StructDecl structDecl) { + return structDecl.name(); + } + if (topDecl instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) { + return builtinTypeDecl.name(); + } + if (topDecl instanceof PbsAst.ConstDecl constDecl) { + return constDecl.name(); + } + if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) { + return serviceDecl.name(); + } + if (topDecl instanceof PbsAst.HostDecl hostDecl) { + return hostDecl.name(); + } + if (topDecl instanceof PbsAst.ContractDecl contractDecl) { + return contractDecl.name(); + } + if (topDecl instanceof PbsAst.FunctionDecl functionDecl) { + return functionDecl.name(); + } + if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) { + return callbackDecl.name(); + } + if (topDecl instanceof PbsAst.EnumDecl enumDecl) { + return enumDecl.name(); + } + if (topDecl instanceof PbsAst.ErrorDecl errorDecl) { + return errorDecl.name(); + } + return null; + } + + private IRReservedMetadata mergeReservedMetadata( + final IRReservedMetadata left, + final IRReservedMetadata right) { + final var hostBindings = new ArrayList(); + hostBindings.addAll(left.hostMethodBindings().asList()); + hostBindings.addAll(right.hostMethodBindings().asList()); + + final var builtinTypeSurfaces = new ArrayList(); + builtinTypeSurfaces.addAll(left.builtinTypeSurfaces().asList()); + builtinTypeSurfaces.addAll(right.builtinTypeSurfaces().asList()); + + final var builtinConstSurfaces = new ArrayList(); + builtinConstSurfaces.addAll(left.builtinConstSurfaces().asList()); + builtinConstSurfaces.addAll(right.builtinConstSurfaces().asList()); + + final var requiredCapabilities = new HashSet(); + requiredCapabilities.addAll(left.requiredCapabilities().asList()); + requiredCapabilities.addAll(right.requiredCapabilities().asList()); + + return new IRReservedMetadata( + ReadOnlyList.wrap(hostBindings), + ReadOnlyList.wrap(builtinTypeSurfaces), + ReadOnlyList.wrap(builtinConstSurfaces), + ReadOnlyList.wrap(requiredCapabilities.stream().toList())); + } + + private int returnSlotsFor(final PbsAst.FunctionDecl functionDecl) { + return switch (functionDecl.returnKind()) { + case INFERRED_UNIT, EXPLICIT_UNIT -> 0; + case PLAIN, RESULT -> 1; + }; + } + + private String importItemLocalName(final PbsAst.ImportItem importItem) { + if (importItem.alias() == null || importItem.alias().isBlank()) { + return importItem.name(); + } + return importItem.alias(); + } + private Set blockedModulesByDependency( final Set failedModuleIds, final Map> dependenciesByModule) { @@ -420,4 +809,13 @@ public class PBSFrontendPhaseService implements FrontendPhaseService { ModuleId moduleId, p.studio.compiler.models.IRBackendFile irBackendFile) { } + + private record ImportedSemanticContext( + ReadOnlyList supplementalTopDecls, + ReadOnlyList importedCallables, + IRReservedMetadata importedReservedMetadata) { + private static ImportedSemanticContext empty() { + return new ImportedSemanticContext(ReadOnlyList.empty(), ReadOnlyList.empty(), IRReservedMetadata.empty()); + } + } } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/resources/stdlib/1/sdk/log/main.pbs b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/resources/stdlib/1/sdk/log/main.pbs new file mode 100644 index 00000000..a25e4c69 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/resources/stdlib/1/sdk/log/main.pbs @@ -0,0 +1,62 @@ +declare host LowLog { + [Host(module = "log", name = "write", version = 1)] + [Capability(name = "log")] + fn write(level: int, message: str) -> void; + + [Host(module = "log", name = "write_tag", version = 1)] + [Capability(name = "log")] + fn write_tag(level: int, tag: int, message: str) -> void; +} + +declare service Log +{ + fn trace(msg: str) -> void + { + LowLog.write(0, msg); + } + + fn debug(msg: str) -> void + { + LowLog.write(1, msg); + } + + fn info(msg: str) -> void + { + LowLog.write(2, msg); + } + + fn warn(msg: str) -> void + { + LowLog.write(3, msg); + } + + fn error(msg: str) -> void + { + LowLog.write(4, msg); + } + + fn trace(tag: int, msg: str) -> void + { + LowLog.write_tag(0, tag, msg); + } + + fn debug(tag: int, msg: str) -> void + { + LowLog.write_tag(1, tag, msg); + } + + fn info(tag: int, msg: str) -> void + { + LowLog.write_tag(2, tag, msg); + } + + fn warn(tag: int, msg: str) -> void + { + LowLog.write_tag(3, tag, msg); + } + + fn error(tag: int, msg: str) -> void + { + LowLog.write_tag(4, tag, msg); + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/resources/stdlib/1/sdk/log/mod.barrel b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/resources/stdlib/1/sdk/log/mod.barrel new file mode 100644 index 00000000..cd494c6d --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/resources/stdlib/1/sdk/log/mod.barrel @@ -0,0 +1,2 @@ +mod host LowLog; +pub service Log; diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsGateUSdkInterfaceConformanceTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsGateUSdkInterfaceConformanceTest.java index daadbb06..42e4e53d 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsGateUSdkInterfaceConformanceTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsGateUSdkInterfaceConformanceTest.java @@ -95,9 +95,10 @@ class PbsGateUSdkInterfaceConformanceTest { import { Color } from @core:color; import { Gfx } from @sdk:gfx; import { Input, InputPad, InputButton } from @sdk:input; + import { Log } from @sdk:log; declare contract Renderer { - fn render(gfx: Gfx, color: Color, input: Input, pad: InputPad, button: InputButton) -> void; + fn render(gfx: Gfx, color: Color, input: Input, pad: InputPad, button: InputButton, log: Log) -> void; } """, "pub contract Renderer;", @@ -113,9 +114,19 @@ class PbsGateUSdkInterfaceConformanceTest { .anyMatch(t -> t.sourceTypeName().equals("Input") && t.canonicalTypeName().equals("input"))); assertTrue(positive.irBackend().getReservedMetadata().builtinTypeSurfaces().stream() .anyMatch(t -> t.sourceTypeName().equals("InputButton") - && t.intrinsics().stream().anyMatch(i -> i.canonicalName().equals("hold")))); + && t.intrinsics().stream().anyMatch(i -> i.canonicalName().equals("input.button.hold")))); assertTrue(positive.irBackend().getReservedMetadata().hostMethodBindings().stream() .anyMatch(h -> h.ownerName().equals("Gfx"))); + assertTrue(positive.irBackend().getReservedMetadata().hostMethodBindings().stream() + .anyMatch(h -> h.ownerName().equals("LowLog") + && h.abiModule().equals("log") + && h.abiMethod().equals("write") + && h.abiVersion() == 1)); + assertTrue(positive.irBackend().getReservedMetadata().hostMethodBindings().stream() + .anyMatch(h -> h.ownerName().equals("LowLog") + && h.abiModule().equals("log") + && h.abiMethod().equals("write_tag") + && h.abiVersion() == 1)); final var negative = compileWorkspaceModule( tempDir.resolve("gate-u-reserved-import-negative"), @@ -129,6 +140,19 @@ class PbsGateUSdkInterfaceConformanceTest { final var missingModule = firstDiagnostic(negative.diagnostics(), d -> d.getCode().equals(PbsLinkErrors.E_LINK_IMPORT_MODULE_NOT_FOUND.name())); assertStableDiagnosticIdentity(missingModule, PbsLinkErrors.E_LINK_IMPORT_MODULE_NOT_FOUND.name(), DiagnosticPhase.LINKING); + + final var lowLogNotPublic = compileWorkspaceModule( + tempDir.resolve("gate-u-reserved-import-lowlog-hidden"), + """ + import { LowLog } from @sdk:log; + fn run() -> int { return 1; } + """, + "pub fn run() -> int;", + 1, + d -> d.getCode().equals(PbsLinkErrors.E_LINK_IMPORT_SYMBOL_NOT_PUBLIC.name())); + final var notPublic = firstDiagnostic(lowLogNotPublic.diagnostics(), + d -> d.getCode().equals(PbsLinkErrors.E_LINK_IMPORT_SYMBOL_NOT_PUBLIC.name())); + assertStableDiagnosticIdentity(notPublic, PbsLinkErrors.E_LINK_IMPORT_SYMBOL_NOT_PUBLIC.name(), DiagnosticPhase.LINKING); } @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 a2119573..be01ad1c 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 @@ -142,6 +142,31 @@ class PbsParserTest { assertTrue(diagnostics.stream().anyMatch(d -> d.getCode().equals(ParseErrors.E_PARSE_ATTRIBUTES_NOT_ALLOWED.name()))); } + @Test + void shouldParseServiceAndMemberNamesUsingErrorKeyword() { + final var source = """ + declare service Log { + fn error(msg: str) -> void { return; } + fn error(tag: int, msg: str) -> void { return; } + } + + fn run() -> void { + Log.error("oops"); + Log.error(7, "oops"); + 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(), "Parser should accept 'error' as callable/member name"); + final var service = assertInstanceOf(PbsAst.ServiceDecl.class, ast.topDecls().getFirst()); + assertEquals(2, service.methods().size()); + assertEquals("error", service.methods().getFirst().name()); + } + @Test void shouldAcceptStructFieldTrailingComma() { final var source = "declare struct S(a: int,);"; diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsInterfaceModuleSemanticsTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsInterfaceModuleSemanticsTest.java index 889db1f4..45bedb48 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsInterfaceModuleSemanticsTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsInterfaceModuleSemanticsTest.java @@ -81,10 +81,6 @@ class PbsInterfaceModuleSemanticsTest { final var source = """ fn run() -> int { return 1; } - declare service Game { - fn tick() -> int { return 1; } - } - declare contract C { fn run() -> int; } declare struct S(v: int); implements C for S using s { @@ -98,7 +94,30 @@ class PbsInterfaceModuleSemanticsTest { final var nonDeclarativeCount = diagnostics.stream() .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_INTERFACE_NON_DECLARATIVE_DECLARATION.name())) .count(); - assertTrue(nonDeclarativeCount >= 3); + assertTrue(nonDeclarativeCount >= 2); + } + + @Test + void shouldAllowDeclarativeServiceFacadesInInterfaceModule() { + final var source = """ + declare host LowLog { + [Host(module = "log", name = "write", version = 1)] + [Capability(name = "log")] + fn write(level: int, message: str) -> void; + } + + declare service Log { + fn trace(message: str) -> void { + LowLog.write(0, message); + } + } + """; + final var diagnostics = DiagnosticSink.empty(); + + new PbsFrontendCompiler().compileFile(new FileId(3), source, diagnostics, SourceKind.SDK_INTERFACE); + + assertFalse(diagnostics.stream().anyMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_INTERFACE_NON_DECLARATIVE_DECLARATION.name()))); } @Test diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/services/PBSFrontendPhaseServiceTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/services/PBSFrontendPhaseServiceTest.java index 284d29e9..1e4d24ec 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/services/PBSFrontendPhaseServiceTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/services/PBSFrontendPhaseServiceTest.java @@ -181,6 +181,7 @@ class PBSFrontendPhaseServiceTest { BuildingIssueSink.empty()); assertTrue(diagnostics.isEmpty()); + assertEquals("frame", irBackend.getEntryPointCallableName()); assertEquals(2, irBackend.getFunctions().size()); } @@ -783,6 +784,89 @@ class PBSFrontendPhaseServiceTest { assertEquals("independent", irBackend.getFunctions().getFirst().name()); } + @Test + void shouldResolveImportedBuiltinTypeChainsInExpressionFlowSemantics() throws IOException { + final var projectRoot = tempDir.resolve("project-builtin-chain"); + final var sourceRoot = projectRoot.resolve("src"); + Files.createDirectories(sourceRoot); + + final var sourceFile = sourceRoot.resolve("main.pbs"); + final var modBarrel = sourceRoot.resolve("mod.barrel"); + Files.writeString(sourceFile, """ + import { Log } from @sdk:log; + import { Input } from @sdk:input; + + fn frame() -> void + { + if (Input.pad().a().pressed()) + { + Log.debug("Hello World!"); + } + } + """); + Files.writeString(modBarrel, "pub fn frame() -> void;"); + + final var projectTable = new ProjectTable(); + final var fileTable = new FileTable(1); + final var projectId = projectTable.register(ProjectDescriptor.builder() + .rootPath(projectRoot) + .name("main") + .version("1.0.0") + .sourceRoots(ReadOnlyList.wrap(List.of(sourceRoot))) + .build()); + + registerFile(projectId, projectRoot, sourceFile, fileTable); + registerFile(projectId, projectRoot, modBarrel, fileTable); + + final var ctx = new FrontendPhaseContext( + projectTable, + fileTable, + new BuildStack(ReadOnlyList.wrap(List.of(projectId))), + 1); + final var diagnostics = DiagnosticSink.empty(); + + final var irBackend = new PBSFrontendPhaseService().compile( + ctx, + diagnostics, + LogAggregator.empty(), + BuildingIssueSink.empty()); + + assertTrue(diagnostics.stream().noneMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_INVALID_MEMBER_ACCESS.name()))); + assertTrue(diagnostics.stream().noneMatch(d -> + d.getCode().equals(PbsSemanticsErrors.E_SEM_APPLY_NON_CALLABLE_TARGET.name()))); + final var frameExecutableOptional = irBackend.getExecutableFunctions().stream() + .filter(function -> "frame".equals(function.callableName())) + .findFirst(); + final var executableNames = irBackend.getExecutableFunctions().stream() + .map(function -> function.callableName() + "@" + function.moduleKey()) + .toList(); + assertTrue( + frameExecutableOptional.isPresent(), + "diagnostics=" + + diagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList() + + " executableNames=" + + executableNames); + final var frameExecutable = frameExecutableOptional.orElseThrow(); + final var intrinsicCalls = frameExecutable.instructions().stream() + .filter(instruction -> + instruction.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.CALL_INTRINSIC) + .toList(); + assertTrue(intrinsicCalls.stream().anyMatch(instruction -> Integer.valueOf(1).equals(instruction.expectedArgSlots()))); + assertTrue(intrinsicCalls.stream().anyMatch(instruction -> + instruction.intrinsicCall() != null && "input.pad".equals(instruction.intrinsicCall().canonicalName()))); + assertTrue(intrinsicCalls.stream().anyMatch(instruction -> + instruction.intrinsicCall() != null && "input.pad.a".equals(instruction.intrinsicCall().canonicalName()))); + assertTrue(intrinsicCalls.stream().anyMatch(instruction -> + instruction.intrinsicCall() != null && "input.button.pressed".equals(instruction.intrinsicCall().canonicalName()))); + assertTrue( + irBackend.getExecutableFunctions().stream().anyMatch(function -> "frame".equals(function.callableName())), + "diagnostics=" + + diagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList() + + " executableNames=" + + executableNames); + } + private void registerFile( final ProjectId projectId, final Path projectRoot, diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/IRVMIntrinsicRegistry.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/IRVMIntrinsicRegistry.java new file mode 100644 index 00000000..47337d0f --- /dev/null +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/IRVMIntrinsicRegistry.java @@ -0,0 +1,73 @@ +package p.studio.compiler.backend.irvm; + +import java.util.HashMap; +import java.util.Map; +import java.util.OptionalInt; + +final class IRVMIntrinsicRegistry { + private static final Map FINAL_ID_BY_INTRINSIC = createRegistry(); + + private IRVMIntrinsicRegistry() { + } + + static OptionalInt resolveFinalId( + final String canonicalName, + final long canonicalVersion) { + if (canonicalName == null || canonicalName.isBlank()) { + return OptionalInt.empty(); + } + final var resolved = FINAL_ID_BY_INTRINSIC.get(new IntrinsicKey(canonicalName, canonicalVersion)); + if (resolved == null) { + return OptionalInt.empty(); + } + return OptionalInt.of(resolved); + } + + private static Map createRegistry() { + final var registry = new HashMap(); + register(registry, "vec2.dot", 1, 0x1000); + register(registry, "vec2.length", 1, 0x1001); + + register(registry, "input.pad", 1, 0x2000); + register(registry, "input.touch", 1, 0x2001); + + register(registry, "input.pad.up", 1, 0x2010); + register(registry, "input.pad.down", 1, 0x2011); + register(registry, "input.pad.left", 1, 0x2012); + register(registry, "input.pad.right", 1, 0x2013); + register(registry, "input.pad.a", 1, 0x2014); + register(registry, "input.pad.b", 1, 0x2015); + register(registry, "input.pad.x", 1, 0x2016); + register(registry, "input.pad.y", 1, 0x2017); + register(registry, "input.pad.l", 1, 0x2018); + register(registry, "input.pad.r", 1, 0x2019); + register(registry, "input.pad.start", 1, 0x201A); + register(registry, "input.pad.select", 1, 0x201B); + + register(registry, "input.touch.button", 1, 0x2020); + register(registry, "input.touch.x", 1, 0x2021); + register(registry, "input.touch.y", 1, 0x2022); + + register(registry, "input.button.pressed", 1, 0x2030); + register(registry, "input.button.released", 1, 0x2031); + register(registry, "input.button.down", 1, 0x2032); + register(registry, "input.button.hold", 1, 0x2033); + return Map.copyOf(registry); + } + + private static void register( + final Map registry, + final String canonicalName, + final long canonicalVersion, + final int finalId) { + final var previous = registry.put(new IntrinsicKey(canonicalName, canonicalVersion), finalId); + if (previous != null && previous != finalId) { + throw new IllegalStateException("duplicate intrinsic registry identity: " + canonicalName + "@" + canonicalVersion); + } + } + + private record IntrinsicKey( + String canonicalName, + long canonicalVersion) { + } +} diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/IRVMLoweringErrorCode.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/IRVMLoweringErrorCode.java index b547a710..6bf3d11c 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/IRVMLoweringErrorCode.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/IRVMLoweringErrorCode.java @@ -2,6 +2,7 @@ package p.studio.compiler.backend.irvm; public enum IRVMLoweringErrorCode { LOWER_IRVM_EMPTY_EXECUTABLE_INPUT, + LOWER_IRVM_ENTRYPOINT_DECLARATION_MISSING, LOWER_IRVM_ENTRYPOINT_MISSING, LOWER_IRVM_ENTRYPOINT_AMBIGUOUS, LOWER_IRVM_MISSING_CALLEE, diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/IRVMValidator.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/IRVMValidator.java index 2191c201..75ff85aa 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/IRVMValidator.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/IRVMValidator.java @@ -173,7 +173,7 @@ public class IRVMValidator { if (inHeight < pops) { throw new IRVMValidationException( IRVMValidationErrorCode.MARSHAL_VERIFY_PRECHECK_STACK_UNDERFLOW, - "stack underflow", + "stack underflow. need=%d have=%d".formatted(pops, inHeight), functionIndex, pc); } diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/LowerToIRVMService.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/LowerToIRVMService.java index bf5a788a..ec067032 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/LowerToIRVMService.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/LowerToIRVMService.java @@ -42,7 +42,7 @@ public class LowerToIRVMService { "IRBackend has no executable functions"); } - final var ordered = orderFunctions(backend.getExecutableFunctions()); + final var ordered = orderFunctions(backend); final var funcIdByCallableId = new HashMap(); for (var i = 0; i < ordered.size(); i++) { final var fn = ordered.get(i); @@ -145,9 +145,31 @@ public class LowerToIRVMService { IRVMLoweringErrorCode.LOWER_IRVM_INVALID_INTRINSIC_ID, "invalid intrinsic id: " + intrinsic.intrinsicId()); } - instructions.add(new IRVMInstruction(IRVMOp.INTRINSIC, intrinsicIndex)); + final var canonicalIntrinsic = backend.getIntrinsicPool().get(intrinsicIndex); + if (!canonicalIntrinsic.canonicalName().equals(intrinsic.canonicalName()) + || canonicalIntrinsic.canonicalVersion() != intrinsic.canonicalVersion()) { + throw loweringError( + fn, + instr, + IRVMLoweringErrorCode.LOWER_IRVM_INVALID_INTRINSIC_ID, + "intrinsic identity mismatch between callsite and pool: call=%s@%d pool=%s@%d".formatted( + intrinsic.canonicalName(), + intrinsic.canonicalVersion(), + canonicalIntrinsic.canonicalName(), + canonicalIntrinsic.canonicalVersion())); + } + final var finalIntrinsicId = IRVMIntrinsicRegistry + .resolveFinalId(canonicalIntrinsic.canonicalName(), canonicalIntrinsic.canonicalVersion()) + .orElseThrow(() -> loweringError( + fn, + instr, + IRVMLoweringErrorCode.LOWER_IRVM_INVALID_INTRINSIC_ID, + "unknown VM-owned intrinsic identity: %s@%d".formatted( + canonicalIntrinsic.canonicalName(), + canonicalIntrinsic.canonicalVersion()))); + instructions.add(new IRVMInstruction(IRVMOp.INTRINSIC, finalIntrinsicId)); operations.add(BytecodeEmitter.Operation.intrinsic( - intrinsicIndex, + finalIntrinsicId, instr.expectedArgSlots(), instr.expectedRetSlots(), sourceSpan)); @@ -231,22 +253,29 @@ public class LowerToIRVMService { return program; } - private ReadOnlyList orderFunctions( - final ReadOnlyList functions) { - final var sorted = new ArrayList<>(functions.asList()); + private ReadOnlyList orderFunctions(final IRBackend backend) { + final var entryPointCallableName = backend.getEntryPointCallableName(); + if (entryPointCallableName == null || entryPointCallableName.isBlank()) { + throw new IRVMLoweringException( + IRVMLoweringErrorCode.LOWER_IRVM_ENTRYPOINT_DECLARATION_MISSING, + "frontend IRBackend entrypoint declaration is missing"); + } + final var sorted = new ArrayList<>(backend.getExecutableFunctions().asList()); sorted.sort(FUNCTION_ORDER); final var entrypoints = sorted.stream() - .filter(candidate -> "main".equals(candidate.callableName())) + .filter(candidate -> entryPointCallableName.equals(candidate.callableName())) .toList(); if (entrypoints.isEmpty()) { throw new IRVMLoweringException( IRVMLoweringErrorCode.LOWER_IRVM_ENTRYPOINT_MISSING, - "missing entrypoint callable 'main'"); + "missing entrypoint callable '%s'".formatted(entryPointCallableName)); } if (entrypoints.size() > 1) { throw new IRVMLoweringException( IRVMLoweringErrorCode.LOWER_IRVM_ENTRYPOINT_AMBIGUOUS, - "ambiguous entrypoint: found %d callables named 'main'".formatted(entrypoints.size())); + "ambiguous entrypoint: found %d callables named '%s'".formatted( + entrypoints.size(), + entryPointCallableName)); } final var entrypoint = entrypoints.getFirst(); final var ordered = new ArrayList(sorted.size()); diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/models/BuilderPipelineContext.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/models/BuilderPipelineContext.java index a8dcdfb3..3d9add82 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/models/BuilderPipelineContext.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/models/BuilderPipelineContext.java @@ -21,6 +21,7 @@ public class BuilderPipelineContext { public IRVMProgram optimizedIrvm; public BytecodeModule bytecodeModule; public byte[] bytecodeBytes; + public Path bytecodeArtifactPath; private BuilderPipelineContext( final BuilderPipelineConfig config, diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/BuilderPipelineService.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/BuilderPipelineService.java index 5f0266a2..738d09cc 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/BuilderPipelineService.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/BuilderPipelineService.java @@ -23,7 +23,8 @@ public class BuilderPipelineService { new OptimizeIRVMPipelineStage(), new EmitBytecodePipelineStage(), new LinkBytecodePipelineStage(), - new VerifyBytecodePipelineStage() + new VerifyBytecodePipelineStage(), + new WriteBytecodeArtifactPipelineStage() ); INSTANCE = new BuilderPipelineService(stages); } diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/WriteBytecodeArtifactPipelineStage.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/WriteBytecodeArtifactPipelineStage.java new file mode 100644 index 00000000..2b8813ce --- /dev/null +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/WriteBytecodeArtifactPipelineStage.java @@ -0,0 +1,72 @@ +package p.studio.compiler.workspaces.stages; + +import lombok.extern.slf4j.Slf4j; +import p.studio.compiler.messages.BuildingIssueSink; +import p.studio.compiler.models.BuilderPipelineContext; +import p.studio.compiler.workspaces.PipelineStage; +import p.studio.utilities.logs.LogAggregator; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; + +@Slf4j +public class WriteBytecodeArtifactPipelineStage implements PipelineStage { + private static final String OUTPUT_DIR = "build"; + private static final String OUTPUT_FILE = "program.pbx"; + + @Override + public BuildingIssueSink run(final BuilderPipelineContext ctx, final LogAggregator logs) { + if (ctx.rootProjectPathCanon == null) { + return BuildingIssueSink.empty() + .report(builder -> builder + .error(true) + .phase("BACKEND_WRITE_ARTIFACT") + .code("MARSHAL_FORMAT_STAGE_INPUT_MISSING") + .message("[BUILD]: root project path is missing before WriteBytecodeArtifact stage")); + } + + final var bytes = resolveBytes(ctx); + if (bytes == null || bytes.length == 0) { + return BuildingIssueSink.empty() + .report(builder -> builder + .error(true) + .phase("BACKEND_WRITE_ARTIFACT") + .code("MARSHAL_FORMAT_STAGE_INPUT_MISSING") + .message("[BUILD]: bytecode bytes are missing before WriteBytecodeArtifact stage")); + } + + final var outputDir = ctx.rootProjectPathCanon.resolve(OUTPUT_DIR); + final var outputPath = outputDir.resolve(OUTPUT_FILE); + try { + Files.createDirectories(outputDir); + Files.write( + outputPath, + bytes, + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.WRITE); + ctx.bytecodeArtifactPath = outputPath; + logs.using(log).info("[BUILD]: wrote bytecode artifact to " + outputPath); + return BuildingIssueSink.empty(); + } catch (IOException e) { + return BuildingIssueSink.empty() + .report(builder -> builder + .error(true) + .phase("BACKEND_WRITE_ARTIFACT") + .code("MARSHAL_ARTIFACT_WRITE_FAILED") + .message("[BUILD]: failed to write bytecode artifact: " + outputPath) + .exception(e)); + } + } + + private byte[] resolveBytes(final BuilderPipelineContext ctx) { + if (ctx.bytecodeBytes != null && ctx.bytecodeBytes.length > 0) { + return ctx.bytecodeBytes; + } + if (ctx.bytecodeModule != null) { + return ctx.bytecodeModule.serialize(); + } + return null; + } +} diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/GoldenArtifactsTest.java b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/GoldenArtifactsTest.java index 96200a5d..45292ae6 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/GoldenArtifactsTest.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/GoldenArtifactsTest.java @@ -68,7 +68,7 @@ class GoldenArtifactsTest { "", "", null, - new IRBackendExecutableFunction.IntrinsicCallMetadata("core.color.pack", 1, new IntrinsicId(0)), + new IRBackendExecutableFunction.IntrinsicCallMetadata("input.pad", 1, new IntrinsicId(0)), 0, 0, Span.none()), @@ -80,7 +80,7 @@ class GoldenArtifactsTest { null, Span.none())), Span.none()))) - .intrinsicPool(ReadOnlyList.from(new IntrinsicReference("core.color.pack", 1))) + .intrinsicPool(ReadOnlyList.from(new IntrinsicReference("input.pad", 1))) .build(); } diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/irvm/IRVMValidatorTest.java b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/irvm/IRVMValidatorTest.java index ef5d72f2..452575a5 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/irvm/IRVMValidatorTest.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/irvm/IRVMValidatorTest.java @@ -204,7 +204,7 @@ class IRVMValidatorTest { } @Test - void validateProgramMustApplyHostcallStackEffectsFromMetadata() { + void validateProgramMustRejectHostcallWhenDeclaredArgsAreMissingOnStack() { final var module = new IRVMModule( "core-v1", ReadOnlyList.from(new IRVMFunction( diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/irvm/LowerToIRVMServiceTest.java b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/irvm/LowerToIRVMServiceTest.java index 900be92b..a2e90568 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/irvm/LowerToIRVMServiceTest.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/irvm/LowerToIRVMServiceTest.java @@ -36,15 +36,34 @@ class LowerToIRVMServiceTest { assertEquals(0, lowered.module().functions().get(1).instructions().get(0).immediate()); } + @Test + void lowerMustUseFrontendDeclaredEntrypointCallable() { + final var backend = IRBackend.builder() + .entryPointCallableName("frame") + .executableFunctions(ReadOnlyList.from( + fn("aux", "app", 11, ReadOnlyList.from( + callFunc("app", "frame", 10), + ret())), + fn("frame", "app", 10, ReadOnlyList.from( + ret())))) + .build(); + + final var lowered = new LowerToIRVMService().lower(backend); + + assertEquals("frame", lowered.module().functions().get(0).name()); + assertEquals("aux", lowered.module().functions().get(1).name()); + assertEquals(0, lowered.module().functions().get(1).instructions().get(0).immediate()); + } + @Test void lowerMustMapHostAndIntrinsicCallsites() { final var backend = IRBackend.builder() .executableFunctions(ReadOnlyList.from( fn("main", "app", 10, ReadOnlyList.from( callHost("gfx", "draw_pixel", 1, 0, 0), - callIntrinsic("core.color.pack", 1, 0), + callIntrinsic("input.pad", 1, 0), ret())))) - .intrinsicPool(ReadOnlyList.from(new IntrinsicReference("core.color.pack", 1))) + .intrinsicPool(ReadOnlyList.from(new IntrinsicReference("input.pad", 1))) .build(); final var lowered = new LowerToIRVMService().lower(backend); @@ -52,7 +71,7 @@ class LowerToIRVMServiceTest { final var instructions = lowered.module().functions().getFirst().instructions(); assertEquals(IRVMOp.HOSTCALL, instructions.get(0).op()); assertEquals(IRVMOp.INTRINSIC, instructions.get(1).op()); - assertEquals(0, instructions.get(1).immediate()); + assertEquals(0x2000, instructions.get(1).immediate()); final var emissionOps = lowered.emissionPlan().functions().getFirst().operations(); assertEquals(p.studio.compiler.backend.bytecode.BytecodeEmitter.OperationKind.HOSTCALL, emissionOps.get(0).kind()); assertEquals(p.studio.compiler.backend.bytecode.BytecodeEmitter.OperationKind.INTRINSIC, emissionOps.get(1).kind()); @@ -84,6 +103,21 @@ class LowerToIRVMServiceTest { assertEquals(IRVMLoweringErrorCode.LOWER_IRVM_MISSING_CALLEE, thrown.code()); } + @Test + void lowerMustRejectUnknownCanonicalIntrinsicIdentity() { + final var backend = IRBackend.builder() + .executableFunctions(ReadOnlyList.from( + fn("main", "app", 10, ReadOnlyList.from( + callIntrinsic("core.color.pack", 1, 0), + ret())))) + .intrinsicPool(ReadOnlyList.from(new IntrinsicReference("core.color.pack", 1))) + .build(); + + final var thrown = assertThrows(IRVMLoweringException.class, () -> new LowerToIRVMService().lower(backend)); + assertEquals(IRVMLoweringErrorCode.LOWER_IRVM_INVALID_INTRINSIC_ID, thrown.code()); + assertTrue(thrown.getMessage().contains("unknown VM-owned intrinsic identity")); + } + @Test void lowerMustRejectCallArgSlotMismatch() { final var backend = IRBackend.builder() @@ -141,6 +175,18 @@ class LowerToIRVMServiceTest { assertEquals(IRVMLoweringErrorCode.LOWER_IRVM_ENTRYPOINT_MISSING, thrown.code()); } + @Test + void lowerMustRejectWhenEntrypointDeclarationIsMissing() { + final var backend = IRBackend.builder() + .entryPointCallableName(" ") + .executableFunctions(ReadOnlyList.from( + fn("main", "app", 10, ReadOnlyList.from(ret())))) + .build(); + + final var thrown = assertThrows(IRVMLoweringException.class, () -> new LowerToIRVMService().lower(backend)); + assertEquals(IRVMLoweringErrorCode.LOWER_IRVM_ENTRYPOINT_DECLARATION_MISSING, thrown.code()); + } + @Test void lowerMustRejectWhenEntrypointIsAmbiguous() { final var backend = IRBackend.builder() diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/irvm/OptimizeIRVMEquivalenceHarnessTest.java b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/irvm/OptimizeIRVMEquivalenceHarnessTest.java index f551fa7a..57e44223 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/irvm/OptimizeIRVMEquivalenceHarnessTest.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/backend/irvm/OptimizeIRVMEquivalenceHarnessTest.java @@ -31,9 +31,9 @@ class OptimizeIRVMEquivalenceHarnessTest { .executableFunctions(ReadOnlyList.from( fn("main", 1, ReadOnlyList.from( callHost("gfx", "draw_pixel", 1, 0, 0), - callIntrinsic("core.color.pack", 1, 0, 0, 0), + callIntrinsic("input.pad", 1, 0, 0, 0), ret())))) - .intrinsicPool(ReadOnlyList.from(new IntrinsicReference("core.color.pack", 1))) + .intrinsicPool(ReadOnlyList.from(new IntrinsicReference("input.pad", 1))) .build(); final var lowered = new LowerToIRVMService().lower(backend); diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/integration/BackendGateIIntegrationTest.java b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/integration/BackendGateIIntegrationTest.java index cee8ba61..6e1c2a23 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/integration/BackendGateIIntegrationTest.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/integration/BackendGateIIntegrationTest.java @@ -145,9 +145,9 @@ class BackendGateIIntegrationTest { void gateI_validIntrinsicPath() { final var module = emitFromBackend(backendWithSingleFunction( fn("main", ReadOnlyList.from( - callIntrinsic("core.color.pack", 1, 0), + callIntrinsic("input.pad", 1, 0), ret())), - ReadOnlyList.from(new IntrinsicReference("core.color.pack", 1)))); + ReadOnlyList.from(new IntrinsicReference("input.pad", 1)))); final var check = compatibilityAdapter.check(module); assertEquals(CompatibilityError.NONE, check.error()); diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/BuilderPipelineServiceOrderTest.java b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/BuilderPipelineServiceOrderTest.java index 91ecf0e1..9f48a28f 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/BuilderPipelineServiceOrderTest.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/BuilderPipelineServiceOrderTest.java @@ -9,6 +9,7 @@ import p.studio.compiler.workspaces.stages.LowerToIRVMPipelineStage; import p.studio.compiler.workspaces.stages.OptimizeIRVMPipelineStage; import p.studio.compiler.workspaces.stages.ResolveDepsPipelineStage; import p.studio.compiler.workspaces.stages.VerifyBytecodePipelineStage; +import p.studio.compiler.workspaces.stages.WriteBytecodeArtifactPipelineStage; import java.lang.reflect.Field; import java.util.List; @@ -35,7 +36,8 @@ class BuilderPipelineServiceOrderTest { OptimizeIRVMPipelineStage.class, EmitBytecodePipelineStage.class, LinkBytecodePipelineStage.class, - VerifyBytecodePipelineStage.class), + VerifyBytecodePipelineStage.class, + WriteBytecodeArtifactPipelineStage.class), stageTypes); } } diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/stages/LowerToIRVMPipelineStageTest.java b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/stages/LowerToIRVMPipelineStageTest.java index 720db4ca..13562bd3 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/stages/LowerToIRVMPipelineStageTest.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/stages/LowerToIRVMPipelineStageTest.java @@ -141,4 +141,67 @@ class LowerToIRVMPipelineStageTest { assertEquals(12, firstIssue.getStart()); assertEquals(24, firstIssue.getEnd()); } + + @Test + void runMustRejectCallWhenStackDoesNotProvideDeclaredArgs() { + final var ctx = BuilderPipelineContext.compilerContext(new BuilderPipelineConfig(false, ".")); + ctx.irBackend = IRBackend.builder() + .executableFunctions(ReadOnlyList.from( + new IRBackendExecutableFunction( + new FileId(0), + "app", + "callee", + new CallableId(2), + 0, + 10, + 1, + 0, + 0, + 1, + ReadOnlyList.from(new IRBackendExecutableFunction.Instruction( + IRBackendExecutableFunction.InstructionKind.RET, + "", + "", + null, + null, + Span.none())), + Span.none()), + new IRBackendExecutableFunction( + new FileId(0), + "app", + "main", + new CallableId(1), + 0, + 10, + 0, + 0, + 0, + 1, + ReadOnlyList.from( + new IRBackendExecutableFunction.Instruction( + IRBackendExecutableFunction.InstructionKind.CALL_FUNC, + "app", + "callee", + new CallableId(2), + null, + null, + 1, + 0, + Span.none()), + new IRBackendExecutableFunction.Instruction( + IRBackendExecutableFunction.InstructionKind.RET, + "", + "", + null, + null, + Span.none())), + Span.none()))) + .build(); + + final var issues = new LowerToIRVMPipelineStage().run(ctx, LogAggregator.empty()); + final var firstIssue = issues.asCollection().iterator().next(); + + assertTrue(issues.hasErrors()); + assertEquals("MARSHAL_VERIFY_PRECHECK_STACK_UNDERFLOW", firstIssue.getCode()); + } } diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/stages/WriteBytecodeArtifactPipelineStageTest.java b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/stages/WriteBytecodeArtifactPipelineStageTest.java new file mode 100644 index 00000000..56ae555a --- /dev/null +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/workspaces/stages/WriteBytecodeArtifactPipelineStageTest.java @@ -0,0 +1,86 @@ +package p.studio.compiler.workspaces.stages; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import p.studio.compiler.backend.bytecode.BytecodeModule; +import p.studio.compiler.messages.BuilderPipelineConfig; +import p.studio.compiler.models.BuilderPipelineContext; +import p.studio.utilities.logs.LogAggregator; +import p.studio.utilities.structures.ReadOnlyList; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class WriteBytecodeArtifactPipelineStageTest { + + @TempDir + Path tempDir; + + @Test + void runMustFailWhenRootProjectPathIsMissing() { + final var ctx = BuilderPipelineContext.compilerContext(new BuilderPipelineConfig(false, ".")); + ctx.bytecodeBytes = new byte[] { 0x01 }; + + final var issues = new WriteBytecodeArtifactPipelineStage().run(ctx, LogAggregator.empty()); + final var firstIssue = issues.asCollection().iterator().next(); + + assertTrue(issues.hasErrors()); + assertEquals("BACKEND_WRITE_ARTIFACT", firstIssue.getPhase()); + assertEquals("MARSHAL_FORMAT_STAGE_INPUT_MISSING", firstIssue.getCode()); + } + + @Test + void runMustFailWhenBytecodeInputIsMissing() { + final var ctx = BuilderPipelineContext.compilerContext(new BuilderPipelineConfig(false, ".")); + ctx.rootProjectPathCanon = tempDir; + + final var issues = new WriteBytecodeArtifactPipelineStage().run(ctx, LogAggregator.empty()); + final var firstIssue = issues.asCollection().iterator().next(); + + assertTrue(issues.hasErrors()); + assertEquals("BACKEND_WRITE_ARTIFACT", firstIssue.getPhase()); + assertEquals("MARSHAL_FORMAT_STAGE_INPUT_MISSING", firstIssue.getCode()); + } + + @Test + void runMustWriteBuildProgramPbxFromByteArray() throws Exception { + final var ctx = BuilderPipelineContext.compilerContext(new BuilderPipelineConfig(false, ".")); + ctx.rootProjectPathCanon = tempDir; + ctx.bytecodeBytes = new byte[] { 0x50, 0x42, 0x58, 0x00 }; + + final var issues = new WriteBytecodeArtifactPipelineStage().run(ctx, LogAggregator.empty()); + final var outputPath = tempDir.resolve("build").resolve("program.pbx"); + + assertFalse(issues.hasErrors()); + assertTrue(Files.exists(outputPath)); + assertArrayEquals(ctx.bytecodeBytes, Files.readAllBytes(outputPath)); + assertEquals(outputPath, ctx.bytecodeArtifactPath); + } + + @Test + void runMustWriteBuildProgramPbxFromModuleWhenByteArrayIsMissing() throws Exception { + final var ctx = BuilderPipelineContext.compilerContext(new BuilderPipelineConfig(false, ".")); + ctx.rootProjectPathCanon = tempDir; + ctx.bytecodeModule = new BytecodeModule( + 0, + ReadOnlyList.empty(), + ReadOnlyList.empty(), + new byte[] { (byte) 0x51, 0x00 }, + null, + ReadOnlyList.empty(), + ReadOnlyList.empty()); + + final var issues = new WriteBytecodeArtifactPipelineStage().run(ctx, LogAggregator.empty()); + final var outputPath = tempDir.resolve("build").resolve("program.pbx"); + + assertFalse(issues.hasErrors()); + assertTrue(Files.exists(outputPath)); + assertArrayEquals(ctx.bytecodeModule.serialize(), Files.readAllBytes(outputPath)); + assertEquals(outputPath, ctx.bytecodeArtifactPath); + } +} diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/resources/golden/bytecode-module.hex b/prometeu-compiler/prometeu-build-pipeline/src/test/resources/golden/bytecode-module.hex index 807e2be7..8a98f41b 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/test/resources/golden/bytecode-module.hex +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/resources/golden/bytecode-module.hex @@ -1 +1 @@ -504253000000000004000000000000000000000000000000000000000000000001000000500000001400000002000000640000000e00000003000000720000004400000005000000b60000001b00000001000000000000000e000000000000000000020071000000000072000000000051000300000000000000010000000000000000000000060000000100000000000000000000000c0000000100000000000000000000000100000000000000040000006d61696e0100000003006766780a00647261775f706978656c010000000000 +504253000000000004000000000000000000000000000000000000000000000001000000500000001400000002000000640000000e00000003000000720000004400000005000000b60000001b00000001000000000000000e000000000000000000020071000000000072000020000051000300000000000000010000000000000000000000060000000100000000000000000000000c0000000100000000000000000000000100000000000000040000006d61696e0100000003006766780a00647261775f706978656c010000000000 diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/resources/golden/irvm-program.txt b/prometeu-compiler/prometeu-build-pipeline/src/test/resources/golden/irvm-program.txt index 96b38446..22087570 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/test/resources/golden/irvm-program.txt +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/resources/golden/irvm-program.txt @@ -1,5 +1,5 @@ profile=core-v1 fn#0:main param=0 local=0 ret=0 max=2 HOSTCALL imm=0 - INTRINSIC imm=0 + INTRINSIC imm=8192 RET imm=- diff --git a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendSpec.java b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendSpec.java index ccca55ab..7a059464 100644 --- a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendSpec.java +++ b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendSpec.java @@ -11,8 +11,10 @@ public class FrontendSpec { private final ReadOnlySet allowedExtensions; private final ReadOnlySet sourceRoots; private final boolean caseSensitive; + @Builder.Default + private final String entryPointCallableName = "main"; public String toString() { - return String.format("FrontendSpec(language=%s)", languageId); + return String.format("FrontendSpec(language=%s, entryPoint=%s)", languageId, entryPointCallableName); } } diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRBackend.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRBackend.java index 692784de..0e7e6ef1 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRBackend.java +++ b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRBackend.java @@ -16,6 +16,8 @@ import java.util.LinkedHashSet; @Builder @Getter public class IRBackend { + @Builder.Default + private final String entryPointCallableName = "main"; @Builder.Default private final ReadOnlyList functions = ReadOnlyList.empty(); @Builder.Default @@ -32,6 +34,7 @@ public class IRBackend { } public static final class IRBackendAggregator { + private String entryPointCallableName; private final ArrayList functions = new ArrayList<>(); private final ArrayList executableFunctions = new ArrayList<>(); private final CallableTable callableTable = new CallableTable(); @@ -58,6 +61,13 @@ public class IRBackend { requiredCapabilities.addAll(metadata.requiredCapabilities().asList()); } + public IRBackendAggregator entryPointCallableName(final String callableName) { + if (callableName != null && !callableName.isBlank()) { + entryPointCallableName = callableName; + } + return this; + } + private CallableId[] reindexCallables(final ReadOnlyList localCallableSignatures) { if (localCallableSignatures == null || localCallableSignatures.isEmpty()) { return new CallableId[0]; @@ -170,6 +180,7 @@ public class IRBackend { public IRBackend emit() { return IRBackend .builder() + .entryPointCallableName(resolveEntryPointCallableName()) .functions(ReadOnlyList.wrap(functions)) .executableFunctions(ReadOnlyList.wrap(executableFunctions)) .callableSignatures(emitCallableSignatures()) @@ -181,12 +192,19 @@ public class IRBackend { ReadOnlyList.wrap(requiredCapabilities.stream().toList()))) .build(); } + + private String resolveEntryPointCallableName() { + return entryPointCallableName == null || entryPointCallableName.isBlank() + ? "main" + : entryPointCallableName; + } } @Override public String toString() { final var sb = new StringBuilder(); - sb.append("IRBackend{functions=").append(functions.size()) + sb.append("IRBackend{entrypoint=").append(entryPointCallableName) + .append(", functions=").append(functions.size()) .append(", executableFunctions=").append(executableFunctions.size()) .append(", callableSignatures=").append(callableSignatures.size()) .append(", intrinsicPool=").append(intrinsicPool.size()) diff --git a/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/models/IRBackendExecutableContractTest.java b/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/models/IRBackendExecutableContractTest.java index 348cdbbb..c2f56c4b 100644 --- a/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/models/IRBackendExecutableContractTest.java +++ b/prometeu-compiler/prometeu-frontend-api/src/test/java/p/studio/compiler/models/IRBackendExecutableContractTest.java @@ -177,11 +177,21 @@ class IRBackendExecutableContractTest { final var backend = aggregator.emit(); assertEquals(2, backend.getExecutableFunctions().size()); + assertEquals("main", backend.getEntryPointCallableName()); assertEquals("entry", backend.getExecutableFunctions().get(0).callableName()); assertEquals("aux", backend.getExecutableFunctions().get(1).callableName()); assertEquals(2, backend.getCallableSignatures().size()); } + @Test + void aggregatorMustEmitConfiguredEntrypointCallableName() { + final var aggregator = IRBackend.aggregator(); + aggregator.entryPointCallableName("frame"); + + final var backend = aggregator.emit(); + assertEquals("frame", backend.getEntryPointCallableName()); + } + @Test void aggregatorMustReindexIntrinsicsToBuildGlobalPool() { final var fileA = new IRBackendFile( diff --git a/test-projects/main/src/module-b/mod.barrel b/test-projects/main/cartridge/assets.pa similarity index 100% rename from test-projects/main/src/module-b/mod.barrel rename to test-projects/main/cartridge/assets.pa diff --git a/test-projects/main/cartridge/manifest.json b/test-projects/main/cartridge/manifest.json new file mode 100644 index 00000000..f9301734 --- /dev/null +++ b/test-projects/main/cartridge/manifest.json @@ -0,0 +1,10 @@ +{ + "magic": "PMTU", + "cartridge_version": 1, + "app_id": 1, + "title": "Main Test", + "app_version": "0.1.0", + "app_mode": "Game", + "entrypoint": "0", + "capabilities": ["log"] +} diff --git a/test-projects/main/cartridge/program.pbx b/test-projects/main/cartridge/program.pbx new file mode 100644 index 00000000..9788e6fd Binary files /dev/null and b/test-projects/main/cartridge/program.pbx differ diff --git a/test-projects/main/prometeu.json b/test-projects/main/prometeu.json index a7f8b342..0210be6e 100644 --- a/test-projects/main/prometeu.json +++ b/test-projects/main/prometeu.json @@ -4,9 +4,5 @@ "language": "pbs", "stdlib": "1", "dependencies": [ - { - "name": "dep1", - "version": "1.0.0" - } ] } diff --git a/test-projects/main/run.sh b/test-projects/main/run.sh new file mode 100755 index 00000000..f840678a --- /dev/null +++ b/test-projects/main/run.sh @@ -0,0 +1,6 @@ +#!/bin/zsh + +set -e + +cp build/program.pbx cartridge +./runtime/prometeu run cartridge \ No newline at end of file diff --git a/test-projects/main/runtime b/test-projects/main/runtime new file mode 120000 index 00000000..567e7714 --- /dev/null +++ b/test-projects/main/runtime @@ -0,0 +1 @@ +../../../runtime/dist-staging/stable/prometeu-cli-aarch64-apple-darwin/ \ No newline at end of file diff --git a/test-projects/main/src/main.pbs b/test-projects/main/src/main.pbs index e69de29b..5337be41 100644 --- a/test-projects/main/src/main.pbs +++ b/test-projects/main/src/main.pbs @@ -0,0 +1,10 @@ +import { Log } from @sdk:log; +import { Input } from @sdk:input; + +fn frame() -> void +{ + if (Input.pad().a().pressed()) + { + Log.debug("Hello World!"); + } +} \ No newline at end of file diff --git a/test-projects/main/src/mod.barrel b/test-projects/main/src/mod.barrel new file mode 100644 index 00000000..665da591 --- /dev/null +++ b/test-projects/main/src/mod.barrel @@ -0,0 +1 @@ +pub fn frame() -> void; \ No newline at end of file diff --git a/test-projects/main/src/module-a/mod.barrel b/test-projects/main/src/module-a/mod.barrel deleted file mode 100644 index eaed690d..00000000 --- a/test-projects/main/src/module-a/mod.barrel +++ /dev/null @@ -1 +0,0 @@ -module-a.barrel: content \ No newline at end of file diff --git a/test-projects/main/src/module-a/source-1.pbs b/test-projects/main/src/module-a/source-1.pbs deleted file mode 100644 index 63671dea..00000000 --- a/test-projects/main/src/module-a/source-1.pbs +++ /dev/null @@ -1,3 +0,0 @@ -fn a1() -{ -} \ No newline at end of file diff --git a/test-projects/main/src/module-a/source-2.pbs b/test-projects/main/src/module-a/source-2.pbs deleted file mode 100644 index abba8a5b..00000000 --- a/test-projects/main/src/module-a/source-2.pbs +++ /dev/null @@ -1,4 +0,0 @@ -fn a2(v1: int, v2: bounded): int -{ - return v1 + v2; -} \ No newline at end of file diff --git a/test-projects/main/src/module-a/source-3.pbs b/test-projects/main/src/module-a/source-3.pbs deleted file mode 100644 index 956ab5a4..00000000 --- a/test-projects/main/src/module-a/source-3.pbs +++ /dev/null @@ -1,4 +0,0 @@ -fn a3(): int -{ - return 1; -} \ No newline at end of file diff --git a/test-projects/main/src/module-b/source-1.pbs b/test-projects/main/src/module-b/source-1.pbs deleted file mode 100644 index e69de29b..00000000 diff --git a/test-projects/main/src/module-b/source-2.pbs b/test-projects/main/src/module-b/source-2.pbs deleted file mode 100644 index e69de29b..00000000 diff --git a/test-projects/main/src/module-b/source-3.pbs b/test-projects/main/src/module-b/source-3.pbs deleted file mode 100644 index e69de29b..00000000 diff --git a/test-projects/main/src/module-c/mod.barrel b/test-projects/main/src/module-c/mod.barrel deleted file mode 100644 index e69de29b..00000000 diff --git a/test-projects/main/src/module-c/source-1.pbs b/test-projects/main/src/module-c/source-1.pbs deleted file mode 100644 index e69de29b..00000000 diff --git a/test-projects/main/src/module-c/source-2.pbs b/test-projects/main/src/module-c/source-2.pbs deleted file mode 100644 index e69de29b..00000000 diff --git a/test-projects/main/src/module-c/source-3.pbs b/test-projects/main/src/module-c/source-3.pbs deleted file mode 100644 index e69de29b..00000000