Merge pull request 'implements PLN-0125' (#19) from dev/multi-frontend-common-lifecycle into master
All checks were successful
JaCoCo Coverage #### Project Overview No changes detected, that affect the code coverage. * Line Coverage: 61.95% (17766/28676) * Branch Coverage: 52.69% (6860/13019) * Lines of Code: 28676 * Cyclomatic Complexity: 11490 #### Quality Gates Summary Output truncated.
Test / Build skipped: 15, passed: 643
Intrepid/Prometeu/Studio/pipeline/head This commit looks good

Reviewed-on: #19
This commit is contained in:
bquarkz 2026-09-18 23:40:50 +00:00
commit c248b91147
42 changed files with 2349 additions and 762 deletions

View File

@ -1,11 +1,11 @@
{"type":"meta","next_id":{"DSC":66,"AGD":69,"DEC":45,"PLN":124,"LSN":61,"CLSN":1}}
{"type":"meta","next_id":{"DSC":66,"AGD":69,"DEC":47,"PLN":126,"LSN":63,"CLSN":1}}
{"type":"discussion","id":"DSC-0065","status":"open","ticket":"multi-frontend-avoid-premature-abstractions","title":"Evitar abstracoes prematuras na preparacao multi-frontend","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","studio","frontend","architecture","multi-frontend","simplicity"],"agendas":[{"id":"AGD-0068","file":"AGD-0068-multi-frontend-avoid-premature-abstractions.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0064","status":"open","ticket":"multi-frontend-architectural-tests","title":"Testes arquiteturais para fronteiras multi-frontend","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","studio","frontend","architecture","tests","multi-frontend"],"agendas":[{"id":"AGD-0067","file":"AGD-0067-multi-frontend-architectural-tests.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0063","status":"open","ticket":"multi-frontend-synthetic-test-frontend","title":"Frontend sintetico de teste para provar neutralidade do pipeline","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","frontend","tests","backend","multi-frontend"],"agendas":[{"id":"AGD-0066","file":"AGD-0066-multi-frontend-synthetic-test-frontend.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0062","status":"open","ticket":"multi-frontend-pvm-neutrality","title":"Neutralidade da PVM e identificacao PBX independente de PBS","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["vm-arch","runtime","pvm","pbx","compiler","multi-frontend"],"agendas":[{"id":"AGD-0065","file":"AGD-0065-multi-frontend-pvm-neutrality.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0062","status":"done","ticket":"multi-frontend-pvm-neutrality","title":"Neutralidade da PVM e identificacao PBX independente de PBS","created_at":"2026-07-15","updated_at":"2026-09-18","tags":["vm-arch","runtime","pvm","pbx","compiler","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0061","file":"discussion/lessons/DSC-0062-multi-frontend-pvm-neutrality/LSN-0061-pbx-runtime-boundary.md","status":"done","created_at":"2026-09-18","updated_at":"2026-09-18"}]}
{"type":"discussion","id":"DSC-0061","status":"open","ticket":"multi-frontend-sdk-canonical-definition","title":"Auditoria e centralizacao gradual da definicao canonica do SDK","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","sdk","stdlib","hostcalls","intrinsics","multi-frontend"],"agendas":[{"id":"AGD-0064","file":"AGD-0064-multi-frontend-sdk-canonical-definition.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0060","status":"open","ticket":"multi-frontend-validation-boundaries","title":"Separar validacoes de linguagem e validacoes de plataforma","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","compiler-pbs","backend","validation","multi-frontend"],"agendas":[{"id":"AGD-0063","file":"AGD-0063-multi-frontend-validation-boundaries.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0059","status":"open","ticket":"multi-frontend-common-lifecycle","title":"Extrair lifecycle comum das responsabilidades do frontend PBS","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","compiler-pbs","lifecycle","backend","multi-frontend"],"agendas":[{"id":"AGD-0062","file":"AGD-0062-multi-frontend-common-lifecycle.md","status":"open","created_at":"2026-07-15","updated_at":"2026-07-15"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0059","status":"done","ticket":"multi-frontend-common-lifecycle","title":"Extrair lifecycle comum das responsabilidades do frontend PBS","created_at":"2026-07-15","updated_at":"2026-09-19","tags":["compiler","compiler-general","compiler-pbs","lifecycle","backend","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0062","file":"discussion/lessons/DSC-0059-multi-frontend-common-lifecycle/LSN-0062-common-lifecycle-assembly-stage.md","status":"done","created_at":"2026-09-19","updated_at":"2026-09-19"}]}
{"type":"discussion","id":"DSC-0058","status":"done","ticket":"multi-frontend-serializable-ir","title":"Manter a IR comum serializavel por design","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","ir","backend","serialization","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0060","file":"discussion/lessons/DSC-0058-multi-frontend-serializable-ir/LSN-0060-data-contract-first-for-serializable-irbackend.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15"}]}
{"type":"discussion","id":"DSC-0057","status":"done","ticket":"multi-frontend-frontend-backend-contract","title":"Estabilizar contrato entre frontend e backend comum","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","compiler-pbs","ir","backend","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0059","file":"discussion/lessons/DSC-0057-multi-frontend-frontend-backend-contract/LSN-0059-common-irbackend-handoff-and-backend-guardrails.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15"}]}
{"type":"discussion","id":"DSC-0056","status":"done","ticket":"multi-frontend-remove-pbs-branches","title":"Generalizar o contrato LSP/editorial para frontends","created_at":"2026-07-15","updated_at":"2026-07-15","tags":["compiler","compiler-general","compiler-pbs","studio","frontend","coupling","multi-frontend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0058","file":"discussion/lessons/DSC-0056-multi-frontend-remove-pbs-branches/LSN-0058-generic-frontend-editorial-contract-for-lsp.md","status":"done","created_at":"2026-07-15","updated_at":"2026-07-15"}]}

View File

@ -0,0 +1,143 @@
---
id: LSN-0062
ticket: multi-frontend-common-lifecycle
title: Common lifecycle assembly after frontend declaration
created: 2026-09-19
tags: [compiler, compiler-general, compiler-pbs, lifecycle, backend, multi-frontend]
---
# Common lifecycle assembly after frontend declaration
## Original Problem
`IRBackend` was already the common executable handoff, but the PBS frontend still
owned two different jobs. It admitted language markers `[Init]`, `[Frame]`, and
`InitAllowed`, and it also assembled the platform lifecycle: module init,
project-init wrapper, published frame wrapper, physical entrypoint, and boot
guard.
That second job discovered roles through `__pbs.*` names and lived inside
`PBSFrontendPhaseService`. Another frontend could not reuse the boot sequence
without copying PBS. Common backend code also risked treating PBS naming as the
integration contract.
The old path had a concrete defect. The `[Init]` colocated with `[Frame]` is
project init, but it was also included in the file-init fragment. The published
wrapper then called module init and project init separately, so the same
callable could run twice. Specs required project init exactly once, after all
module initializers and before the frame root.
## Consolidated Decision
Declaration and assembly are separate.
An executable frontend emits an immutable, ordered, typed
`IRLifecycleDeclaration` on `IRBackend`. The declaration carries:
- modules in explicit dependency order;
- each module's ordered file-init fragment references;
- an optional project-init callable;
- exactly one frame-root callable.
References use `ModuleId`, `FileId`, `CallableId`, a typed role, and source
attribution. Absence of a declaration means a deliberately non-executable
handoff. It must not synthesize a default `main`.
`FrontendPhasePipelineStage` stops at that declarative backend.
`AssembleLifecyclePipelineStage` runs immediately after it and is the terminal
stage of `analyze()`. `LifecycleAssemblerService` is the only owner of derived
lifecycle artifacts. `compile()` and `build()` reuse that single assembly pass.
`ResolveDepsPipelineStage` remains the ordering authority. Frontends propagate
that order; the assembler validates and applies it. Neither side may rebuild
order from synthetic names, `FileId`, textual module names, or incidental map
order.
Project init is excluded from file-init fragments and runs exactly once. Common
derived identities are neutral and deterministic. Common code must not import
PBS packages or inspect `__pbs.*` prefixes. Typed roles, ids, and origins are
the stable inspection surface; synthetic names are internal.
## Final Implementation
PBS now emits per-file lifecycle contributions and a project-level declaration.
A colocated `[Init]` is the project-init reference only. File-init fragments
remain frontend-lowered executables, consumed by id and role rather than by
name prefix. `synthesizePublishedLifecycle` was removed.
The common assembler validates the declaration and materializes:
1. module init callables in declared module/file order;
2. an optional project-init wrapper, invoked once;
3. the published frame wrapper, which is the physical entrypoint;
4. a hidden boot guard that prevents a second bootstrap after success.
The declaration remains on the assembled `IRBackend` as provenance. IRVM
lowering keeps only assembled-graph preconditions such as wrapper identity,
synthetic origin, and boot-guard presence. It does not reconstruct lifecycle
from names.
`analyze()` still returns a fully assembled executable backend, without IRVM,
bytecode, or file output. Neutral names use the `__prometeu.lifecycle.*` prefix.
Diagnostics stay split: PBS reports marker, signature, placement, cardinality,
and colocation errors; the common layer reports unresolved ids, incompatible
roles, missing frame root, invalid origins, and frontend-preassembled derived
artifacts.
## Examples
Good frontend output:
- a file with globals or a non-project `[Init]` contributes a
`FILE_INIT_FRAGMENT`;
- the `[Init]` next to `[Frame]` is `PROJECT_INIT` only;
- `[Frame]` is `FRAME_ROOT`;
- module order follows the resolved dependency graph even when names or file
ids would sort the other way.
Good assembled output:
- wrapper call order is module inits, then project init once, then frame root;
- boot guard is a hidden global, not a user global;
- `getLifecycleDeclaration()` still exposes the original typed references.
Bad output:
- PBS emitting `MODULE_INIT`, `PROJECT_INIT` wrappers, published wrappers, or
boot guards;
- common code matching `__pbs.frame_wrapper$m0` to discover the entrypoint;
- sorting modules by `FileId` or textual path because the declaration order was
inconvenient;
- leaving both the PBS assembler and the common stage active.
## Pitfalls
- Do not treat `analyze()` as "frontend only." It now includes common lifecycle
assembly so callers still receive an executable `IRBackend`.
- Do not keep a temporary dual path. Two assemblers duplicate wrappers or init.
- Do not infer missing frame roots or repair invalid declarations in the common
layer. Fail deterministically.
- Do not move PBS marker rules into the assembler. Language admission stays in
PBS; structural handoff checks stay common.
- Do not preserve `__pbs.*` names for common-derived artifacts as compatibility
behavior. They were never a public ABI.
- Do not confuse a frontend file-init fragment name with a common-derived
identity. PBS may still name its own fragments; common code must not parse
those names.
## References
- Decision: `DEC-0046`
- Plan: `PLN-0125`
- Related lessons: `LSN-0059`, `LSN-0060`
- `docs/specs/compiler/23. Compiler Pipeline Entry Points Specification.md`
- `docs/specs/compiler/20. IRBackend to IRVM Lowering Specification.md`
- `docs/specs/compiler/22. Backend Spec-to-Test Conformance Matrix.md`
- `docs/specs/compiler-languages/pbs/4. Static Semantics Specification.md`
- `docs/specs/compiler-languages/pbs/9. Dynamic Semantics Specification.md`
- `docs/specs/compiler-languages/pbs/12. Diagnostics Specification.md`
- `docs/specs/compiler-languages/pbs/13. Lowering IRBackend Specification.md`
- `prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/models/IRLifecycleDeclaration.java`
- `prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/lifecycle/LifecycleAssemblerService.java`
- `prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/AssembleLifecyclePipelineStage.java`

View File

@ -0,0 +1,136 @@
---
id: LSN-0061
ticket: multi-frontend-pvm-neutrality
title: PBX is the language-neutral runtime boundary
created: 2026-09-18
tags: [vm-arch, runtime, pvm, pbx, compiler, multi-frontend]
---
# PBX is the language-neutral runtime boundary
## Original Problem
PBS was the first implemented frontend, so its name still appeared in a few
common compiler specification titles. That editorial residue made it easier to
misread the architecture as if PBS owned bytecode, IRVM, or the PVM execution
model.
The implementation was already mostly neutral: the compiler emitted PBX, the
runtime loaded PBX, and the common backend did not import PBS frontend packages.
The real risk was future drift. In particular, IRVM could be mistaken for a
runtime input, `PBS\0` could be confused with the executable magic, and duplicated
VM-architecture documents could silently become competing authorities.
## Consolidated Decision
The executable pipeline is:
```text
language frontend -> IRBackend -> IRVM -> PBX -> loader/verifier -> PVM
```
`IRBackend`, `IRVM`, optimization, and PBX emission are compiler-owned.
`IRVM` is an internal compiler representation and never crosses the runtime
boundary. PBX is the only executable artifact consumed by the runtime.
The runtime repository owns PBX binary layout, Core ISA, loader behavior,
verification, and PVM execution. Studio owns the common compiler transformations
that produce conforming PBX. Studio copies of VM-architecture documents are
mirrors, not independent authorities.
`PBS` names a language, frontend, stdlib, and explicitly language-specific
integration. It does not name the executable format, magic, ISA, ABI, program
image, or runtime type. The canonical persisted artifact is `program.pbx`, and
its binary magic is `PBX\0`.
## Final Implementation
The common compiler specifications now identify bytecode/PBX and IRVM
optimization as language-neutral surfaces. They explicitly state that IRVM is
internal to the compiler and that PBX is the compiler/runtime boundary. Existing
PBS references were retained where they describe legitimate frontend-specific
publication or metadata behavior.
Studio's copies of `ARCHITECTURE.md`, `ISA_CORE.md`, and `INTRINSICS.csv` were
synchronized with the runtime repository. A mirror policy now identifies the
runtime repository as canonical and provides byte-for-byte comparison commands.
Existing Studio tests already proved two important invariants:
- `BytecodeModule` serializes the exact `PBX\0` header;
- common backend contracts neither import nor publicly expose PBS frontend
types.
Runtime conformance was strengthened in three focused places:
- the bytecode invalid-magic regression now explicitly proves that legacy
`PBS\0` is rejected;
- the PVM initialization regression now creates a `BytecodeModule` directly,
serializes it, loads and verifies it, prepares the boot call, and executes it
through `HALT` without any frontend;
- a quality check inspects the `prometeu-bytecode` and `prometeu-vm` manifests
and rejects dependencies whose identities belong to compiler or frontend
layers.
No production VM behavior, ISA, ABI, or binary format changed.
## Examples
Language-specific work stops before the common backend:
```text
PBS AST and semantic model
-> PBS lowering
-> neutral IRBackend data
-> common IRVM lowering and optimization
-> PBX\0
```
A future frontend follows the same boundary:
```text
FutureLanguage AST and semantic model
-> FutureLanguage lowering
-> neutral IRBackend data
-> the same common IRVM and PBX stages
```
The runtime sees the same PBX contract in both cases. It does not receive a
`languageId`, source extension, AST object, IRVM object, or frontend service.
## Pitfalls and Anti-Patterns
- Do not expose IRVM as a convenient runtime protocol. It is optimized for
compiler transformations, not binary compatibility or runtime loading.
- Do not rename PBX concepts after the first frontend. `PBS\0` is invalid even
when PBS produced the program.
- Do not globally ban the word `PBS` from runtime documentation. PBS is a valid
guest-language example; guards should inspect dependencies, public types, and
format identities instead of raw text.
- Do not duplicate tests when an existing guard proves the entire invariant.
Extend coverage only for the missing distinction, such as explicit `PBS\0`
rejection.
- Do not let a documentation mirror become a second authority. Synchronize it
against runtime or replace it with an explicit reference.
- Loading a PBX image is not the same as beginning execution. The PVM boot test
must prepare the published boot call before running the instruction budget.
## References
- `docs/specs/compiler/15. Bytecode and PBX Mapping Specification.md`
- `docs/specs/compiler/20. IRBackend to IRVM Lowering Specification.md`
- `docs/specs/compiler/21. IRVM Optimization Pipeline Specification.md`
- `docs/vm-arch/README.md`
- `prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/BytecodeModule.java`
- `prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/specs/CommonBackendArchitectureTest.java`
- Runtime: `crates/console/prometeu-bytecode/src/model.rs`
- Runtime: `crates/console/prometeu-vm/src/virtual_machine.rs`
- Runtime: `crates/dev/prometeu-quality-checks/tests/pvm_frontend_neutrality.rs`
## Takeaways
- PBX is the stable, language-neutral executable boundary.
- IRVM stays private to the compiler pipeline.
- Runtime authority and compiler authority are complementary, not competing.
- Small dependency and artifact tests are enough to protect this boundary
without introducing a plugin platform or a second language.

View File

@ -1,77 +0,0 @@
---
id: AGD-0062
ticket: multi-frontend-common-lifecycle
title: Extrair lifecycle comum das responsabilidades do frontend PBS
status: open
created: 2026-07-15
resolved:
decision:
tags: [compiler, compiler-general, compiler-pbs, lifecycle, backend, multi-frontend]
---
# Agenda - Extrair o lifecycle do frontend PBS
## Objetivo
Domain owner: `compiler/general`, com impacto em `compiler/pbs`.
Separar declaracao de lifecycle feita por uma linguagem da montagem comum de init, frame, module initializers e entrypoint publicado.
## Contexto atual
As lessons e specs antigas indicam que PBS ja trata lifecycle e wrapper/entrypoint. O alinhamento pede que `PBSFrontendPhaseService` deixe de conter regras universais da plataforma.
## Escopo
- Localizar onde PBS sintetiza ou valida lifecycle.
- Definir contrato comum de `LifecycleDeclaration` ou equivalente.
- Planejar servico comum para validar, ordenar e montar entrypoint.
## Fora de escopo
- Alterar sintaxe PBS.
- Mudar comportamento observavel de projetos existentes.
- Refatorar todo o backend junto da extracao.
## Arquivos e componentes a inspecionar
- `prometeu-compiler/frontends/prometeu-frontend-pbs/...PBSFrontendPhaseService...`
- `prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/irvm/LowerToIRVMService.java`
- `docs/specs/compiler/16. Runtime Execution and Initialization Specification.md`
- `docs/specs/compiler/20. IRBackend to IRVM Lowering Specification.md`
- `docs/specs/compiler-languages/pbs/13. Lowering IRBackend Specification.md`
## Alteracoes propostas
Opcao A: frontend PBS emite declaracoes neutras de lifecycle e backend/common assembler monta o entrypoint.
Opcao B: manter montagem no PBS por enquanto, mas isolar regras comuns em um servico compartilhado chamado pelo PBS.
Recomendacao inicial: extrair primeiro o servico comum com testes de caracterizacao, depois migrar o PBS para produzir apenas declaracoes.
## Estrategia de implementacao
Identificar fixture positiva atual, proteger com teste, criar contrato comum minimo, mover validacoes universais e manter mensagens PBS onde forem sintaxe/semantica da linguagem.
## Testes necessarios
- Lifecycle valido PBS continua gerando mesmo PBX/IRVM.
- Lifecycle ausente ou frame invalido falha no nivel comum quando aplicavel.
- Teste de IR comum com lifecycle sem parser PBS.
## Criterios de aceitacao
- PBS traduz a forma da linguagem para declaracoes neutras.
- Servico comum valida e monta lifecycle da plataforma.
## Riscos
- Mover validacoes PBS-especificas para camada comum por engano.
- Alterar ordem de inicializadores sem perceber.
## Decisoes que devem ser registradas
- Nome e ownership do servico comum.
- Forma canonica das declaracoes de lifecycle.
- Separacao entre erro de linguagem e erro de plataforma.

View File

@ -1,78 +0,0 @@
---
id: AGD-0065
ticket: multi-frontend-pvm-neutrality
title: Neutralidade da PVM e identificacao PBX independente de PBS
status: open
created: 2026-07-15
resolved:
decision:
tags: [vm-arch, runtime, pvm, pbx, compiler, multi-frontend]
---
# Agenda - Manter a PVM neutra
## Objetivo
Domain owner: `vm-arch`, com impacto em `compiler/general`.
Garantir que a PVM conheca apenas PBX/IRVM e conceitos runtime neutros, incluindo identificacao correta do formato como `PBX`/`PBX\0`, nao `PBS`.
## Contexto atual
O documento e focado no Studio, mas runtime neutrality e uma fronteira transversal. A PVM nao deve conhecer `PBS service`, `PBS struct`, `PBS optional` ou qualquer conceito de linguagem.
## Escopo
- Auditar nomenclatura e imports runtime/PVM disponiveis no repo.
- Verificar magic/header de PBX quando o codigo estiver no workspace.
- Confirmar que linguagem e convertida antes da execucao.
## Fora de escopo
- Modificar a PVM se o acoplamento estiver apenas no Studio ou compiler.
- Alterar ISA sem necessidade.
- Adicionar suporte a qualquer linguagem nova.
## Arquivos e componentes a inspecionar
- `docs/vm-arch/ARCHITECTURE.md`
- `docs/vm-arch/ISA_CORE.md`
- `docs/specs/compiler/15. Bytecode and PBX Mapping Specification.md`
- `prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend/bytecode/...`
- runtime/PVM em repositorio ou modulo relacionado, quando presente
## Alteracoes propostas
Opcao A: auditoria documental e de codigo para nomes PBS indevidos em runtime/bytecode.
Opcao B: corrigir imediatamente apenas identificacao `PBS` vs `PBX` se houver bug confirmado e isolado.
Recomendacao inicial: auditar primeiro; corrigir somente caso a fronteira esteja claramente violada e a mudanca seja pequena.
## Estrategia de implementacao
Buscar termos PBS no runtime, bytecode e specs VM; classificar ocorrencias validas de docs historicas vs erros de contrato.
## Testes necessarios
- Teste de magic/header PBX.
- Teste arquitetural impedindo imports PBS no runtime/PVM.
- Teste de carregamento/verificacao de programa sem dependencia de linguagem.
## Criterios de aceitacao
- PVM nao depende de conceitos PBS.
- Formato executavel e tratado como PBX.
- Qualquer correcao runtime necessaria fica separada de refactors Studio/compiler.
## Riscos
- Confundir nomenclatura documental antiga com acoplamento real.
- Expandir escopo para VM quando o problema pertence ao compiler.
## Decisoes que devem ser registradas
- Lista de conceitos permitidos na PVM.
- Politica de nomenclatura PBX/PBS.
- Onde ficam testes de neutralidade runtime.

View File

@ -194,16 +194,20 @@ At minimum, the PBS diagnostics baseline must cover:
- `project init must be colocated with frame`,
- `Init attribute target invalid`,
- `InitAllowed is valid only on SDK host methods`,
- `host call not allowed during init`,
- `host call not allowed during init`.
9. common lifecycle structural diagnostics owned by lifecycle assembly and backend preconditions, including:
- invalid or unresolved lifecycle declaration ids and roles,
- missing frame root on an executable declaration,
- frontend-preassembled derived lifecycle artifacts,
- `synthetic wrapper entrypoint missing`,
- `published entrypoint is not function index 0`,
- `hidden boot guard is missing`,
- `synthetic callable origin missing`.
9. symbolic asset-reference diagnostics required by backend-owned `Addressable` resolution, including:
10. symbolic asset-reference diagnostics required by backend-owned `Addressable` resolution, including:
- unresolved terminal asset reference,
- namespace-versus-terminal asset misuse,
- structurally invalid symbolic asset path admitted by source syntax but rejected by semantic surface rules.
10. ignored-value diagnostics for expression statements, including:
11. ignored-value diagnostics for expression statements, including:
- a stable warning when a materialized value is produced and then ignored,
- and no warning for unit-like expression statements.

View File

@ -86,10 +86,12 @@ For each admitted PBS source unit and callable in the current lowering slice, PB
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. compiler-selected published-wrapper entrypoint identity for backend handoff,
8. explicit global and synthetic lifecycle structure required by executable lowering.
7. a typed `IRLifecycleDeclaration` for executable programs, with file-init fragment, optional project-init, and required frame-root references,
8. explicit user globals and frontend-lowered file-init fragments required by later common lifecycle assembly,
9. backend-owned symbolic identities that must survive to later executable lowering, including symbolic asset references when admitted by the source slice.
PBS lowering MUST NOT emit module init, project-init wrapper, published frame wrapper, physical entrypoint, or boot guard. Those artifacts are produced exclusively by common lifecycle assembly.
Lowering must not collapse source categories in a way that erases required declaration/callable identity needed by downstream diagnostics or conformance assertions.
The normative contract is obligation-based, not tied to one mandatory in-memory class graph.
@ -221,20 +223,18 @@ It does not replace:
2. `docs/specs/compiler/21. IRVM Optimization Pipeline Specification.md`,
3. or `docs/specs/compiler/15. Bytecode and PBX Mapping Specification.md`.
### 12.7 Executable Lifecycle and Published Wrapper Obligation
### 12.7 Executable Lifecycle Declaration Obligation
For executable PBS frontends, PBS lowering must preserve the compiler-selected published wrapper rather than a frontend-declared nominal entrypoint.
For executable PBS frontends, PBS lowering MUST emit a typed `IRLifecycleDeclaration` rather than assembling the published lifecycle.
At `IRBackend` emission time:
At frontend `IRBackend` emission time:
1. the lowered graph must contain explicit lifecycle structure for:
- user-authored globals,
- file init fragments,
- module init,
- project init when present,
- and the published frame wrapper,
2. the published frame wrapper must be the effective entrypoint identity handed to common backend stages,
3. the userland callable marked with `[Frame]` must remain distinguishable as the logical frame root,
4. the wrapper must own final `FRAME_RET`,
5. common backend stages must not reintroduce manifest-owned or `FrontendSpec`-owned nominal entrypoint authority,
6. and hidden compiler-owned lifecycle state such as the boot guard must remain structurally distinguishable from user globals.
1. PBS MUST retain marker admission for `[Init]`, `[Frame]`, and `InitAllowed`,
2. PBS MUST emit typed references for:
- file-init fragments that include global materialization and non-project `[Init]`,
- the optional project-init callable, which is the `[Init]` colocated with `[Frame]`,
- and the required frame-root callable marked with `[Frame]`,
3. the colocated project init MUST be excluded from file-init fragments,
4. module order in the declaration MUST propagate the explicit dependency order already derived from resolved project/module data, without sorting by `FileId`, textual module name, or synthetic callable name,
5. PBS MUST NOT emit module init, project-init wrapper, published frame wrapper, physical entrypoint, or boot guard,
6. and common lifecycle assembly remains the owner of derived artifacts, including the published wrapper that becomes the physical entrypoint and owns the final frame return.

View File

@ -250,6 +250,8 @@ Rules:
- A file may declare at most one `[Init]`.
- The `[Init]` colocated with `[Frame]` is the project init.
- Project init must be colocated with `[Frame]`.
- PBS static semantics and lowering admit those markers and translate them into a typed `IRLifecycleDeclaration`.
- PBS MUST NOT materialize module init, project-init wrapper, published frame wrapper, physical entrypoint, or boot guard; those artifacts belong to common lifecycle assembly.
- `InitAllowed` is valid only on SDK host methods and is invalid on userland `fn`, `global`, `service`, `const`, `struct`, `contract`, and callback surfaces.
- A host method signature in a reserved stdlib/interface module MUST carry exactly one `Host` attribute.
- A host method signature in a reserved stdlib/interface module MUST carry exactly one `Capability` attribute.

View File

@ -102,17 +102,21 @@ Executable PBS programs have a logical bootstrap sequence before ordinary frame
Normative order:
1. materialize globals for each file according to dependency order,
2. execute that file's `[Init]`, if present,
2. execute that file's non-project `[Init]`, if present,
3. compose those steps into one synthetic module init per module,
4. execute project init, if present,
4. execute project init exactly once, if present, after all module initializers,
5. then invoke the userland callable marked with `[Frame]`.
The `[Init]` colocated with `[Frame]` is project init. It MUST NOT also run as a file-init fragment.
Lifecycle bootstrap is one-shot:
- no automatic retry occurs after boot failure,
- failure during module init aborts boot,
- failure during project init aborts boot.
Common lifecycle assembly materializes this order from the typed declaration. PBS remains the language source of marker meaning; it is not the owner of the published wrapper.
## 6.2 Logical frame root and physical wrapper
PBS distinguishes the logical frame root from the physical published entrypoint:

View File

@ -1,4 +1,4 @@
# PBS Bytecode and PBX Mapping Specification
# Bytecode and PBX Mapping Specification
Status: Draft v1 (Backend Baseline)
Applies to: mapping from lowered/optimized backend programs into PBX sections, bytecode-facing artifacts, and source-to-artifact invariants required by loader/verifier/runtime
@ -9,6 +9,10 @@ This document defines the normative mapping between backend-lowered semantics an
Its purpose is to keep artifact emission deterministic and compatible with runtime loader/verifier contracts.
PBX is the only executable boundary between the compiler and runtime. `IRVM`
is a compiler-internal representation consumed by bytecode emission; it MUST
NOT be exposed to or consumed by the runtime loader, verifier, or PVM.
## 2. Scope
This document defines:

View File

@ -11,6 +11,12 @@ Its purpose is to make backend lowering deterministic, reviewable, and compatibl
`IRBackend` is the common frontend-to-backend executable handoff. It is emitted by frontends and consumed by common backend stages. Backend lowering MUST NOT require PBS AST nodes, PBS tokens, PBS parser structures, PBS semantic objects, PBS editorial objects, or equivalent language-owned objects from future frontends.
`IRVM` is an internal compiler representation. It MUST be consumed by common
compiler optimization and PBX emission stages, and it MUST NOT cross the
compiler/runtime boundary or become an input, dependency, public type, or
operational concept of the runtime loader, verifier, or PVM. PBX is the only
executable artifact boundary between compiler and runtime.
## 2. Scope
This document defines:
@ -66,10 +72,32 @@ At this boundary, `IRBackend` may contain only language-neutral compiler concept
8. host-call metadata,
9. intrinsic metadata,
10. reserved metadata required for backend lowering,
11. and required runtime or platform capabilities.
11. required runtime or platform capabilities,
12. and an optional typed `IRLifecycleDeclaration` used as lifecycle provenance after common assembly.
The common backend contract MUST express these concepts as backend obligations, not as PBS syntax or PBS semantic rules. When a concept currently originates from PBS, the backend contract still owns only the language-neutral obligation carried by `IRBackend`.
### 4.3 IRLifecycleDeclaration
`IRLifecycleDeclaration` is the typed, immutable, serializable-by-design lifecycle provenance carried by `IRBackend`.
An executable handoff MUST contain exactly one declaration before common assembly. A deliberately non-executable handoff MAY omit it; absence MUST NOT synthesize a default `main` or published wrapper.
The declaration MUST represent, using ordered collections and typed ids:
1. modules in explicit dependency order,
2. each module's ordered file-init fragment references,
3. an optional project-init callable reference,
4. and exactly one frame-root callable reference.
References MUST use `ModuleId`, `FileId`, `CallableId`, a typed role, and source attribution. The public shape MUST NOT expose maps, sets, callbacks, compiler services, or frontend-owned types.
`AssembleLifecyclePipelineStage` MUST run after `FrontendPhasePipelineStage` and before `LowerToIRVMPipelineStage`. `LifecycleAssemblerService` is the only owner of derived lifecycle artifacts: module init, project-init wrapper, published frame wrapper, physical entrypoint, and hidden boot guard. Those artifacts MUST use deterministic neutral internal identities. Common code MUST NOT discover roles from `__pbs.*` prefixes or depend on PBS packages.
The declaration MUST remain available on the assembled `IRBackend` as typed provenance. Downstream inspection MUST use roles, ids, and origins rather than synthetic names.
Project init MUST be absent from file-init fragments and MUST execute exactly once, after all module initializers and before the frame root. Bootstrap failure MUST abort without retry. The boot guard MUST prevent a second bootstrap after successful completion. The published wrapper owns the final frame return.
The common backend contract MUST NOT expose or require:
1. `PbsExpression`, `PbsStatement`, `PbsToken`, or equivalent PBS-owned syntax objects,
@ -114,11 +142,14 @@ Initial enforcement for this contract SHOULD be audit and guardrail based: specs
Lowering from `IRBackend` to `IRVM` may start only when:
1. frontend admission to `IRBackend` is complete for the active slice,
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. compiler-selected published-wrapper entrypoint identity is present and unambiguous,
6. and target `vm_profile` is selected deterministically.
2. common lifecycle assembly has already materialized derived artifacts for an executable handoff,
3. required callsite categories (`CALL_FUNC`/`CALL_HOST`/`CALL_INTRINSIC`) are available,
4. required canonical host/intrinsic metadata is available for admitted callsites,
5. dependency-scoped fail-fast exclusions have already been applied at the `IRBackend` boundary,
6. compiler-selected published-wrapper entrypoint identity is present and unambiguous,
7. and target `vm_profile` is selected deterministically.
Declaration validation, role uniqueness, explicit order, and rejection of frontend-preassembled derived artifacts belong to `LifecycleAssemblerService`. `IRBackend -> IRVM` MUST keep only backend preconditions on the assembled graph, such as published-wrapper identity, synthetic origin presence, and hidden boot-guard presence. It MUST NOT reconstruct lifecycle order from callable names.
## 6. IRVM Model Obligations
@ -159,13 +190,14 @@ Function indexing must be deterministic:
4. manifest-owned or name-only entrypoint fallback is forbidden,
5. and identical admitted input graphs must produce identical function-id assignments.
For PBS executable lowering:
For executable lowering:
- the userland callable marked with `[Frame]` remains the logical frame root,
- the wrapper path must contain final `FRAME_RET`,
- the declared frame-root callable remains the logical frame root,
- the assembled published wrapper is the physical entrypoint,
- the wrapper path must contain the final frame return,
- and lowering must preserve that distinction through `IRVM`.
These PBS-facing names describe the current frontend source of the lifecycle structure. The backend-owned obligation is the neutral requirement that the compiler-selected published wrapper be the physical entrypoint and that lifecycle state remain explicit in `IRBackend`.
PBS `[Init]`/`[Frame]` markers are a frontend source of the declaration. The backend-owned obligation is the neutral requirement that the compiler-selected published wrapper be the physical entrypoint and that lifecycle provenance remain explicit in `IRBackend` through typed roles and ids. Neutral synthetic names are internal and are not a public ABI.
## 9. Callsite Lowering Obligations

View File

@ -1,4 +1,4 @@
# PBS IRVM Optimization Pipeline Specification
# IRVM Optimization Pipeline Specification
Status: Draft v1 (Pipeline Baseline)
Applies to: placement and invariants of the `OptimizeIRVM` stage between lowering and bytecode emission
@ -9,6 +9,10 @@ This document defines the normative placement and safety invariants of the `Opti
Its purpose is to allow optimization in the backend pipeline without transferring semantic optimization responsibility to runtime.
`IRVM` is internal to the compiler. Optimization output MUST flow to PBX
emission and MUST NOT be exposed as a runtime input or protocol. PBX remains the
only executable boundary consumed by the runtime loader, verifier, and PVM.
## 2. Scope
This document defines:

View File

@ -3,7 +3,7 @@
Status: Draft v1 (Traceability Baseline)
Applies to: compiler/backend conformance traceability for canonical stage order and entrypoint-specific contracts
Last Updated: 2026-07-15
Last Updated: 2026-09-18
## 1. Purpose
@ -46,6 +46,9 @@ to concrete positive/negative test evidence and current status.
| G20-4.2.1 | Public `IRBackend` contract types MUST remain modelable as an acyclic, deterministic data graph. | `IRBackendExecutableContractTest#publicIRBackendContractMustExposeOnlySerializableShapes`; `IRBackendExecutableContractTest#aggregatorMustEmitDeterministicOrderedPublicContractShape` | N/A | pass | Covers public shape guardrails, generic type arguments, ordered aggregate emission, table remapping, and capability de-duplication order. |
| G20-4.2.2 | Public `IRBackend` contract types MUST NOT expose callbacks, services, mutable/unordered collection contracts, frontend-owned objects, process-dependent lazy values, cyclic public references, or wire-format commitments. | `IRBackendExecutableContractTest#publicIRBackendContractMustExposeOnlySerializableShapes` | N/A | pass | Reflection guard covers public contract fields, constructors, methods, record components, and generic type arguments. |
| G20-4.2.3 | Public `IRBackend` cross-object references MUST use explicit ids or stable symbolic keys. | `IRBackendExecutableContractTest#publicIRBackendContractAuditMustClassifyAllowedSupportTypes`; `IRBackendExecutableContractTest#publicIRBackendContractMustUseExplicitIdsOrStableKeysForKnownReferences` | N/A | pass | Current audit found no concrete public serialization leak requiring model correction. Existing `FileId`, `ModuleId`, `CallableId`, and `IntrinsicId` remain explicit table-scoped identifiers. |
| G20-4.3.1 | Executable `IRBackend` MUST carry a typed, ordered, id-based `IRLifecycleDeclaration` as provenance. | `IRBackendExecutableContractTest#aggregatorMustRemapTypedLifecycleReferencesAndPreserveDeclaredModuleOrder`; `LifecycleAssemblerServiceTest#assembleMustMaterializeNeutralLifecycleArtifactsInDeclaredOrder` | `IRBackendExecutableContractTest#aggregatorMustOmitLifecycleDeclarationWhenNoFileContributesOne` | pass | Declaration uses `ReadOnlyList`, typed ids, roles, and source attribution; aggregator remaps ids with explicit module order. |
| G20-4.3.2 | Missing lifecycle declaration MUST mean a deliberately non-executable handoff and MUST NOT synthesize entrypoints. | `LifecycleAssemblerServiceTest#assembleMustNoOpWhenLifecycleDeclarationIsAbsent`; `AssembleLifecyclePipelineStageTest#runMustNoOpForDeliberatelyNonExecutableHandoff` | N/A | pass | Assembler and stage leave the backend unchanged when the declaration is absent. |
| G20-4.3.3 | Common lifecycle assembly MUST be the exclusive owner of derived artifacts and MUST use neutral identities. | `LifecycleAssemblerServiceTest#assembleMustMaterializeNeutralLifecycleArtifactsInDeclaredOrder`; `CommonBackendArchitectureTest#commonLifecycleAndBackendMustNotDiscoverRolesFromPbsNamePrefixes` | `LifecycleAssemblerServiceTest#assembleMustRejectPreMaterializedDerivedArtifacts` | pass | Derived wrappers and boot guard are common-owned; architecture guard rejects PBS imports and `__pbs.` prefix matching. |
| G20-6.2 | `IRVM_EXT` MUST declare structural metadata (`pops/pushes/is_branch/is_terminator`). | `IRVMValidatorTest#validateMustApplyStructuralMetadataForCustomInternalExtension`; `IRVMValidatorTest#validateMustRejectCustomInternalExtensionWhenStructuralMetadataUnderflowsStack`; `IRVMOp` record contract (`pops/pushes/branch/terminator/internal`) | N/A | pass | Dedicated extension fixtures now assert structural metadata is consumed by validation behavior. |
| G20-6.3 | `IRVM_EXT` MUST be eliminable before bytecode emission. | `OptimizeIRVMServiceTest#optimizeDefaultPassesMustEliminateUnreachableInternalExtensionBeforeEmission` | `EmitBytecodePipelineStageTest#runMustFailWhenInternalOpcodesRemain`; `EmitBytecodePipelineStageTest#runMustFailWhenInternalOpcodesRemainEvenWithNonEmptyEmissionPlan`; `IRVMValidatorTest#validateMustRejectInternalOpcodeWhenConfigured` | pass | Optimizer elimination path and emit-stage hard rejection path are both covered. |
| G20-6.4 | IRVM MUST preserve per-function slot and identity headers. | `IRVMProgramTest#constructorMustRejectModuleAndEmissionPlanMismatch` | `IRVMProgramTest#constructorMustRejectModuleAndEmissionPlanMismatch` | pass | Header mismatch is rejected deterministically. |
@ -73,14 +76,18 @@ to concrete positive/negative test evidence and current status.
| G21-9.1 | Validation MUST include optimized-vs-non-optimized equivalence fixtures. | `OptimizeIRVMEquivalenceHarnessTest#optimizeOnOffMustPreserveObservableTraceForLoweredHostIntrinsicFixture`; `OptimizeIRVMEquivalenceHarnessTest#optimizeOnOffMustPreserveObservableTraceForConditionalJoinFixture`; `OptimizeIRVMEquivalenceHarnessTest#optimizeOnOffMustPreserveObservableTraceForSimpleLoopFixture`; `OptimizeIRVMEquivalenceHarnessTest#optimizeOnOffMustPreserveObservableTraceForLinearCallFixture` | N/A | pass | Dedicated opt on/off harness with reusable interpreter and deterministic trace assertions is in place. |
| G21-9.2 | Validation MUST preserve known negative loader/verifier behavior. | `BackendSafetyGateSUTest#emitStageMustExposeMarshalingLinkageFailureDeterministically`; `BackendGateIIntegrationTest` rejection suite | N/A | pass | |
| G21-9.3 | Validation MUST preserve deterministic artifact-level invariants. | `BackendSafetyGateSUTest#fullPipelineMustProduceDeterministicBytecodeForSameInput`; `BytecodeEmitterTest#emitMustRemainDeterministicAfterInterning` | N/A | pass | |
| G23-5.1 | Canonical compiler pipeline MUST preserve the shared stage order through `WriteBytecodeArtifact`. | `MainProjectPipelineIntegrationTest#analyzeShouldNotWriteProgramBytecode`; `MainProjectPipelineIntegrationTest#compileShouldProduceInMemoryBytecodeWithoutWritingProgramBytecode`; `MainProjectPipelineIntegrationTest#buildShouldWriteProgramBytecode` | N/A | partial | Entrypoint execution now covers the full analyze -> compile -> build path, but a dedicated order-only assertion for every terminal boundary is still missing. |
| G23-6.1 | `analyze` MUST terminate after `FrontendPhase` and MUST NOT execute backend artifact stages. | `MainProjectPipelineIntegrationTest#analyzeShouldNotWriteProgramBytecode` | N/A | pass | The integration fixture asserts the analysis snapshot contract and proves `program.pbx` is not written on the analysis path. |
| G23-5.1 | Canonical compiler pipeline MUST preserve the shared stage order through `WriteBytecodeArtifact`. | `BuilderPipelineServiceOrderTest#canonicalOrderMustContainOptimizeBetweenLowerAndEmit`; `MainProjectPipelineIntegrationTest#analyzeShouldNotWriteProgramBytecode`; `MainProjectPipelineIntegrationTest#compileShouldProduceInMemoryBytecodeWithoutWritingProgramBytecode`; `MainProjectPipelineIntegrationTest#buildShouldWriteProgramBytecode` | N/A | pass | Dedicated order assertion now covers analyses, compile, and build stage lists, including `AssembleLifecyclePipelineStage`. |
| G23-5.2 | `AssembleLifecyclePipelineStage` MUST run immediately after `FrontendPhasePipelineStage` and before IRVM lowering. | `BuilderPipelineServiceOrderTest#canonicalOrderMustContainOptimizeBetweenLowerAndEmit`; `BuilderPipelineServiceOrderTest#analyzeMustTerminateAfterLifecycleAssemblyWithoutBackendArtifacts` | N/A | pass | Analyses terminate at lifecycle assembly; compile stages begin at `LowerToIRVMPipelineStage`. |
| G23-6.1 | `analyze` MUST terminate after `AssembleLifecycle` and MUST NOT execute backend artifact stages. | `BuilderPipelineServiceOrderTest#analyzeMustTerminateAfterLifecycleAssemblyWithoutBackendArtifacts`; `MainProjectPipelineIntegrationTest#analyzeShouldNotWriteProgramBytecode` | N/A | pass | Analyze returns an assembled executable `IRBackend` with no IRVM, bytecode, or file output. |
| G23-6.2 | `compile` MUST terminate after `VerifyBytecode` and MUST return a validated in-memory executable result without disk write. | `MainProjectPipelineIntegrationTest#compileShouldProduceInMemoryBytecodeWithoutWritingProgramBytecode`; `BuilderPipelinePublicSurfaceTest#compileResultMustExposeValidatedInMemoryArtifacts` | N/A | pass | Coverage asserts validated in-memory bytecode artifacts and the no-write boundary for `compile`. |
| G23-6.3 | `build` MUST extend `compile` only with terminal artifact persistence. | `MainProjectPipelineIntegrationTest#buildShouldWriteProgramBytecode`; `BuilderPipelinePublicSurfaceTest#buildResultMustExposeArtifactPathAndCompilePayload` | N/A | pass | Coverage asserts that `build` retains the compile payload and writes the filesystem artifact path expected by callers. |
| G23-7.1 | `AnalysisSnapshot` MUST expose the minimum shared analysis contract. | `MainProjectPipelineIntegrationTest#analyzeShouldNotWriteProgramBytecode`; `BuilderPipelinePublicSurfaceTest#analysisSnapshotMustExposeMinimumSharedContract` | N/A | pass | Both executable and structural contract checks now cover the shared minimum analysis payload. |
| G23-8.1 | Caller-specific configs/contexts MUST NOT redefine canonical stage semantics. | N/A | N/A | missing | Requires explicit multi-entrypoint composition tests once non-filesystem contexts are implemented. |
| G23-9.1 | Legacy public `run` MUST be removed as the normative entrypoint and filesystem-default behavior MUST be expressed through `build`. | `BuilderPipelinePublicSurfaceTest#publicServiceSurfaceMustExposeExplicitEntrypointsAndNoPublicRun`; `MainProjectPipelineIntegrationTest#buildShouldWriteProgramBytecode` | N/A | partial | Public service coverage now proves `run` is no longer public and `build` is the filesystem artifact path, but the CLI composition path is not yet exercised by a dedicated automated test. |
| PBS13-12.0 | PBS executable lowering MUST emit `IRBackend` satisfying frontend handoff obligations before common backend lowering. | `IRBackendExecutableContractTest` suite; `PBSFrontendPhaseServiceTest#shouldSynthesizePositiveTopic19FixtureAcrossFileInitProjectInitAndFrame` | `LowerToIRVMServiceTest#lowerMustRejectWhenSyntheticWrapperEntrypointIsMissing`; `LowerToIRVMServiceTest#lowerMustRejectWhenHiddenBootGuardIsMissing`; `LowerToIRVMServiceTest#lowerMustRejectWhenSyntheticCallableOriginIsMissing` | pass | Row group `PBS13-12.x` verifies PBS emission into the common handoff; common backend ownership is covered by G20 rows. |
| PBS13-12.7 | PBS executable lowering MUST emit a typed lifecycle declaration and MUST NOT assemble derived wrappers. | `PBSFrontendPhaseServiceTest#shouldSynthesizePositiveTopic19FixtureAcrossFileInitProjectInitAndFrame`; `PbsFrontendCompilerTest#shouldExposeGlobalsAndTypedLifecycleDeclarationInBackendFile` | N/A | pass | Frontend tests assert declaration roles and the absence of module init, published wrapper, and boot guard. |
| PBS13-12.8 | Project init MUST be excluded from file-init fragments and MUST execute exactly once after module initializers. | `PbsFrontendCompilerTest#shouldExposeGlobalsAndTypedLifecycleDeclarationInBackendFile`; `LifecycleAssemblerServiceTest#assembleMustInvokeProjectInitExactlyOnceAndExcludeItFromFileInit` | N/A | pass | Colocated `[Init]` is declared as project init and invoked once by the assembled wrapper. |
| PBS13-12.9 | Lifecycle module order MUST propagate resolved dependency order without name or `FileId` inference. | `PBSFrontendPhaseServiceTest#shouldPublishDependencyOrderedLifecycleModulesInsteadOfNameOrFileIdOrder`; `IRBackendExecutableContractTest#aggregatorMustRemapTypedLifecycleReferencesAndPreserveDeclaredModuleOrder` | N/A | pass | Adversarial module names and file registration order do not change declared dependency order. |
| PBS13-12.1.1 | Callable identity MUST be preserved at handoff. | `IRBackendExecutableContractTest#aggregatorMustPreserveExecutableFunctionOrderDeterministically` | N/A | pass | |
| PBS13-12.1.2 | Observable callable signature MUST be preserved at handoff. | `IRBackendExecutableContractTest#functionContractMustRejectInvalidSlotAndSpanBounds` | N/A | pass | |
| PBS13-12.1.3 | Callable category MUST be preserved at handoff. | `PbsFrontendCompilerTest#shouldLowerExecutableFunctionsWithCallsiteCategories` | N/A | pass | |

View File

@ -65,12 +65,13 @@ The canonical stage order is:
1. `ResolveDepsPipelineStage`
2. `LoadSourcesPipelineStage`
3. `FrontendPhasePipelineStage`
4. `LowerToIRVMPipelineStage`
5. `OptimizeIRVMPipelineStage`
6. `EmitBytecodePipelineStage`
7. `LinkBytecodePipelineStage`
8. `VerifyBytecodePipelineStage`
9. `WriteBytecodeArtifactPipelineStage`
4. `AssembleLifecyclePipelineStage`
5. `LowerToIRVMPipelineStage`
6. `OptimizeIRVMPipelineStage`
7. `EmitBytecodePipelineStage`
8. `LinkBytecodePipelineStage`
9. `VerifyBytecodePipelineStage`
10. `WriteBytecodeArtifactPipelineStage`
Public entrypoints MAY terminate early according to this document, but they MUST NOT:
@ -82,21 +83,23 @@ Public entrypoints MAY terminate early according to this document, but they MUST
### 6.1 `analyze`
`analyze` MUST terminate at `FrontendPhasePipelineStage`.
`analyze` MUST terminate at `AssembleLifecyclePipelineStage`.
`analyze` is defined as:
1. `ResolveDeps`
2. `LoadSources`
3. `FrontendPhase`
4. `AssembleLifecycle`
`analyze` MUST:
1. resolve workspace/dependency inputs needed by the frontend,
2. load source surfaces admitted by the selected compiler configuration,
3. run frontend semantic analysis,
4. return an `AnalysisSnapshot` or equivalent result contract,
5. and remain free of backend artifact side effects.
3. run frontend semantic analysis and emit a declarative `IRBackend`,
4. assemble the common lifecycle from `IRLifecycleDeclaration` when present,
5. return an `AnalysisSnapshot` or equivalent result contract whose `IRBackend` is fully assembled when the handoff is executable,
6. and remain free of backend artifact side effects.
`analyze` MUST NOT:

View File

@ -9,7 +9,7 @@ Scope boundary:
- PROMETEU itself is a fantasy handheld / fantasy console with a broader machine model, firmware model, cartridge model, and virtual hardware surface.
- This document does not define the whole PROMETEU machine.
- This document defines the VM/runtime subsystem that executes bytecode inside that machine.
- For broader machine-level framing, see [`../specs/README.md`](../specs/README.md).
- For broader machine-level framing, see [`../specs/README.md`](../specs/runtime/README.md).
Document roles:
@ -149,6 +149,7 @@ The verifier statically checks bytecode for structural safety and stackshape
- `SyscallMeta` defines expected arity and return slot counts. The loader resolves `HOSTCALL` against this metadata and rejects raw `SYSCALL` in PBX pre-load artifacts; the verifier checks final IDs/arity/returnslot counts against the same metadata.
- Arguments and returns
- Arguments are taken from the operand stack in the order defined by the ABI. Returns use multislot results via a hostside return buffer (`HostReturn`) which the VM copies back onto the stack, or zero slots for “void”. A mismatch in result counts is a fault/panic per current hardening logic.
- Example: the canonical asset runtime load surface is `asset.load(asset_id, slot) -> (status, handle)`. The caller does not supply `asset_name` or `asset_type`; bank kind is derived from `asset_table` using `asset_id`.
- Capabilities
- Cartridge capability flags are applied before load-time host resolution. Missing required capability aborts load; invoking a syscall without the required capability also traps defensively at runtime.

View File

@ -78,6 +78,17 @@ Authority rule:
- `INTRINSIC u32` — final numeric VM-owned intrinsic call.
- `FRAME_SYNC` — yield until the next frame boundary (e.g., vblank); explicit safepoint.
Host service arity is not encoded in the opcode itself. It is defined by resolved syscall metadata.
Example:
- `asset.load` currently resolves with `arg_slots = 2` and `ret_slots = 2`.
- The canonical stack contract is `asset_id, slot -> status, handle`.
- Callers do not provide an explicit asset kind; the runtime derives it from `asset_table`.
- `composer.bind_scene` resolves with `arg_slots = 1` and `ret_slots = 1`.
- The canonical stack contract is `bank_id -> status`.
- `composer.emit_sprite` resolves with `arg_slots = 9` and `ret_slots = 1`.
#### Canonical Intrinsic Registry Artifact
- Final intrinsic IDs and intrinsic stack metadata are published in [`INTRINSICS.csv`](INTRINSICS.csv).

32
docs/vm-arch/README.md Normal file
View File

@ -0,0 +1,32 @@
# VM Architecture Mirror Policy
The Prometeu runtime repository is the normative authority for PBX binary
layout, Core ISA, loader behavior, verification, and PVM execution semantics.
The files in this directory are convenience mirrors for Studio-side compiler
and integration work; they are not an independent architectural authority.
Canonical runtime sources:
- `../runtime/docs/vm-arch/ARCHITECTURE.md`
- `../runtime/docs/vm-arch/ISA_CORE.md`
- `../runtime/docs/vm-arch/INTRINSICS.csv`
When these mirrors are retained, an integration change that updates a canonical
runtime source MUST update the corresponding Studio mirror in the same logical
change. Verify synchronization from the Studio repository root with:
```bash
diff -u docs/vm-arch/ARCHITECTURE.md ../runtime/docs/vm-arch/ARCHITECTURE.md
diff -u docs/vm-arch/ISA_CORE.md ../runtime/docs/vm-arch/ISA_CORE.md
diff -u docs/vm-arch/INTRINSICS.csv ../runtime/docs/vm-arch/INTRINSICS.csv
```
All three commands MUST exit successfully with no output before a change claims
that the mirrors are synchronized. If the runtime repository is not available
in the same checkout, synchronization evidence must be produced against the
canonical runtime revision before merging.
Compiler-owned transformation contracts remain under `docs/specs/compiler`.
Those specifications define `IRBackend -> IRVM -> PBX` emission, but they MUST
conform to the runtime-owned PBX and ISA contracts and MUST NOT redefine runtime
execution semantics.

View File

@ -8,6 +8,9 @@ import p.studio.compiler.models.IRFunction;
import p.studio.compiler.models.IRGlobal;
import p.studio.compiler.models.IRGlobalOrigin;
import p.studio.compiler.models.IRGlobalVisibility;
import p.studio.compiler.models.IRLifecycleCallableReference;
import p.studio.compiler.models.IRLifecycleCallableRole;
import p.studio.compiler.models.IRLifecycleFileDeclaration;
import p.studio.compiler.models.IRReservedMetadata;
import p.studio.compiler.models.IRSyntheticCallableKind;
import p.studio.compiler.models.IRSyntheticFunction;
@ -215,6 +218,12 @@ public final class PbsFrontendCompiler {
if (fileInitExecutable != null) {
executableFunctions.add(fileInitExecutable);
}
final var lifecycleDeclaration = lowerLifecycleDeclaration(
fileId,
effectiveModuleId,
lifecycleArtifacts,
fileInitExecutable,
executableFunctions);
return new IRBackendFile(
fileId,
functions,
@ -224,7 +233,8 @@ public final class PbsFrontendCompiler {
reservedMetadata,
effectiveModulePool,
ReadOnlyList.wrap(callableSignatures),
executableLowering.intrinsicPool());
executableLowering.intrinsicPool(),
lifecycleDeclaration);
}
public String callableShapeSurfaceOf(final PbsAst.FunctionDecl functionDecl) {
@ -306,33 +316,19 @@ public final class PbsFrontendCompiler {
final var frameDecl = lifecycleArtifacts.frameDecl();
final var firstGlobalDecl = lifecycleArtifacts.firstGlobalDecl();
final var fileInitAnchorSpan = initDecl != null ? initDecl.span() : firstGlobalDecl == null ? null : firstGlobalDecl.span();
final var fileInitAnchorName = initDecl != null ? initDecl.name() : firstGlobalDecl == null ? "" : firstGlobalDecl.name();
final var ordinaryFileInit = frameDecl == null ? initDecl : null;
final var fileInitAnchorSpan = ordinaryFileInit != null
? ordinaryFileInit.span()
: firstGlobalDecl == null ? null : firstGlobalDecl.span();
final var fileInitAnchorName = ordinaryFileInit != null
? ordinaryFileInit.name()
: firstGlobalDecl == null ? "" : firstGlobalDecl.name();
if (fileInitAnchorSpan != null) {
syntheticFunctions.add(new IRSyntheticFunction(
moduleId,
fileInitCallableName(fileId),
IRSyntheticCallableKind.FILE_INIT_FRAGMENT,
new IRSyntheticOrigin(fileId, fileInitAnchorName, fileInitAnchorSpan)));
syntheticFunctions.add(new IRSyntheticFunction(
moduleId,
moduleInitCallableName(moduleId),
IRSyntheticCallableKind.MODULE_INIT,
new IRSyntheticOrigin(fileId, fileInitAnchorName, fileInitAnchorSpan)));
}
if (initDecl != null && frameDecl != null) {
syntheticFunctions.add(new IRSyntheticFunction(
moduleId,
projectInitCallableName(moduleId),
IRSyntheticCallableKind.PROJECT_INIT,
new IRSyntheticOrigin(fileId, initDecl.name(), initDecl.span())));
}
if (frameDecl != null) {
syntheticFunctions.add(new IRSyntheticFunction(
moduleId,
frameWrapperCallableName(moduleId),
IRSyntheticCallableKind.PUBLISHED_FRAME_WRAPPER,
new IRSyntheticOrigin(fileId, frameDecl.name(), frameDecl.span())));
}
return ReadOnlyList.wrap(syntheticFunctions);
}
@ -343,8 +339,10 @@ public final class PbsFrontendCompiler {
final PbsLifecycleArtifacts lifecycleArtifacts,
final ArrayList<IRBackendExecutableFunction> executableFunctions,
final ArrayList<CallableSignatureRef> callableSignatures) {
if (lifecycleArtifacts.firstGlobalDecl() == null
&& lifecycleArtifacts.initDecl() == null) {
final var ordinaryFileInit = lifecycleArtifacts.frameDecl() == null
? lifecycleArtifacts.initDecl()
: null;
if (lifecycleArtifacts.firstGlobalDecl() == null && ordinaryFileInit == null) {
return null;
}
final var syntheticCallableName = fileInitCallableName(fileId);
@ -352,8 +350,8 @@ public final class PbsFrontendCompiler {
callableSignatures.add(new CallableSignatureRef(moduleId, syntheticCallableName, 0, "() -> unit"));
final var instructions = new ArrayList<IRBackendExecutableFunction.Instruction>();
final var moduleInitSource = lifecycleArtifacts.initDecl();
if (moduleInitSource != null && moduleInitSource != lifecycleArtifacts.frameDecl()) {
final var moduleInitSource = ordinaryFileInit;
if (moduleInitSource != null) {
final var initExecutable = resolveLifecycleExecutable(executableFunctions, moduleId, moduleInitSource.name());
if (initExecutable != null) {
instructions.add(new IRBackendExecutableFunction.Instruction(
@ -390,6 +388,58 @@ public final class PbsFrontendCompiler {
span);
}
private IRLifecycleFileDeclaration lowerLifecycleDeclaration(
final FileId fileId,
final ModuleId moduleId,
final PbsLifecycleArtifacts lifecycleArtifacts,
final IRBackendExecutableFunction fileInitExecutable,
final ArrayList<IRBackendExecutableFunction> executableFunctions) {
final var fileInitReference = fileInitExecutable == null
? null
: new IRLifecycleCallableReference(
moduleId,
fileId,
fileInitExecutable.callableId(),
IRLifecycleCallableRole.FILE_INIT_FRAGMENT,
fileInitExecutable.span());
final var projectInitSource = lifecycleArtifacts.frameDecl() == null
? null
: lifecycleArtifacts.initDecl();
final var projectInitExecutable = projectInitSource == null
? null
: resolveLifecycleExecutable(executableFunctions, moduleId, projectInitSource.name());
final var projectInitReference = projectInitExecutable == null
? null
: new IRLifecycleCallableReference(
moduleId,
fileId,
projectInitExecutable.callableId(),
IRLifecycleCallableRole.PROJECT_INIT,
projectInitSource.span());
final var frameSource = lifecycleArtifacts.frameDecl();
final var frameExecutable = frameSource == null
? null
: resolveLifecycleExecutable(executableFunctions, moduleId, frameSource.name());
final var frameReference = frameExecutable == null
? null
: new IRLifecycleCallableReference(
moduleId,
fileId,
frameExecutable.callableId(),
IRLifecycleCallableRole.FRAME_ROOT,
frameSource.span());
final var declaration = new IRLifecycleFileDeclaration(
moduleId,
fileId,
fileInitReference,
projectInitReference,
frameReference);
return declaration.isEmpty() ? null : declaration;
}
private IRBackendExecutableFunction resolveLifecycleExecutable(
final ArrayList<IRBackendExecutableFunction> executableFunctions,
final ModuleId moduleId,
@ -443,27 +493,6 @@ public final class PbsFrontendCompiler {
return "__pbs.file_init$f" + fileId.getId();
}
public static String moduleInitCallableName(final ModuleId moduleId) {
return "__pbs.module_init$m" + normalizedModuleIndex(moduleId);
}
public static String projectInitCallableName(final ModuleId moduleId) {
return "__pbs.project_init$m" + normalizedModuleIndex(moduleId);
}
public static String frameWrapperCallableName(final ModuleId moduleId) {
return "__pbs.frame_wrapper$m" + normalizedModuleIndex(moduleId);
}
public static String bootGuardGlobalName(final ModuleId moduleId) {
return "__pbs.boot_guard$m" + normalizedModuleIndex(moduleId);
}
private static int normalizedModuleIndex(final ModuleId moduleId) {
final var normalized = moduleId == null ? ModuleId.none() : moduleId;
return normalized.isNone() ? -1 : normalized.getIndex();
}
private int safeToInt(final long value) {
if (value > Integer.MAX_VALUE) {
return Integer.MAX_VALUE;

View File

@ -5,24 +5,15 @@ import p.studio.compiler.messages.BuildingIssueSink;
import p.studio.compiler.messages.FESurfaceContext;
import p.studio.compiler.messages.FrontendPhaseContext;
import p.studio.compiler.models.IRBackend;
import p.studio.compiler.models.IRBackendExecutableFunction;
import p.studio.compiler.models.IRGlobal;
import p.studio.compiler.models.IRGlobalOrigin;
import p.studio.compiler.models.IRGlobalVisibility;
import p.studio.compiler.models.IRHiddenGlobalKind;
import p.studio.compiler.models.IRSyntheticCallableKind;
import p.studio.compiler.pbs.PbsFrontendCompiler;
import p.studio.compiler.pbs.PbsReservedMetadataExtractor;
import p.studio.compiler.pbs.semantics.PbsFlowSemanticsValidator;
import p.studio.compiler.pbs.stdlib.InterfaceModuleLoader;
import p.studio.compiler.pbs.stdlib.ResourceStdlibEnvironmentResolver;
import p.studio.compiler.pbs.stdlib.StdlibEnvironmentResolver;
import p.studio.compiler.source.Span;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.identifiers.CallableId;
import p.studio.compiler.source.identifiers.FileId;
import p.studio.compiler.source.identifiers.ModuleId;
import p.studio.compiler.source.tables.CallableSignatureRef;
import p.studio.utilities.logs.LogAggregator;
import p.studio.utilities.structures.ReadOnlyList;
@ -168,7 +159,7 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
private IRBackend mergeCompiledSources(
final ArrayList<CompiledSourceFile> compiledSourceFiles,
final Set<ModuleId> failedModuleIds,
final Map<ModuleId, Set<ModuleId>> moduleDependencyGraph) {
final Map<ModuleId, ReadOnlyList<ModuleId>> moduleDependencyGraph) {
final var irBackendAggregator = IRBackend.aggregator();
final var blockedModuleIds = blockedModulesByDependency(failedModuleIds, moduleDependencyGraph);
@ -178,438 +169,70 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
}
irBackendAggregator.merge(compiledSource.irBackendFile());
}
return synthesizePublishedLifecycle(irBackendAggregator.emit());
irBackendAggregator.lifecycleModuleOrder(lifecycleModuleOrder(
compiledSourceFiles,
blockedModuleIds,
moduleDependencyGraph));
return irBackendAggregator.emit();
}
private IRBackend synthesizePublishedLifecycle(final IRBackend baseBackend) {
final var callableSignatures = new ArrayList<>(baseBackend.getCallableSignatures().asList());
final var executableFunctions = new ArrayList<>(baseBackend.getExecutableFunctions().asList());
final var globals = new ArrayList<>(baseBackend.getGlobals().asList());
final var fileInitFunctionsByModule = new LinkedHashMap<ModuleId, List<IRBackendExecutableFunction>>();
final var projectInitAnchorsByModule = new LinkedHashMap<ModuleId, String>();
final var wrapperNamesByModule = new LinkedHashMap<ModuleId, String>();
final var frameAnchorsByModule = new LinkedHashMap<ModuleId, String>();
for (final var executable : executableFunctions) {
if (!executable.callableName().startsWith("__pbs.file_init$")) {
continue;
}
fileInitFunctionsByModule
.computeIfAbsent(normalizeModuleId(executable.moduleId()), ignored -> new ArrayList<>())
.add(executable);
}
for (final var syntheticFunction : baseBackend.getSyntheticFunctions()) {
final var moduleId = normalizeModuleId(syntheticFunction.moduleId());
switch (syntheticFunction.kind()) {
case PROJECT_INIT -> projectInitAnchorsByModule.put(moduleId, syntheticFunction.origin().anchorCallableName());
case PUBLISHED_FRAME_WRAPPER -> {
wrapperNamesByModule.put(moduleId, syntheticFunction.callableName());
frameAnchorsByModule.put(moduleId, syntheticFunction.origin().anchorCallableName());
}
default -> {
}
private ReadOnlyList<ModuleId> lifecycleModuleOrder(
final ArrayList<CompiledSourceFile> compiledSourceFiles,
final Set<ModuleId> blockedModuleIds,
final Map<ModuleId, ReadOnlyList<ModuleId>> dependenciesByModule) {
final var availableModules = new LinkedHashSet<ModuleId>();
for (final var compiledSource : compiledSourceFiles) {
if (!blockedModuleIds.contains(compiledSource.moduleId())) {
availableModules.add(compiledSource.moduleId());
}
}
final ModuleId entryPointModuleId;
final String entryPointCallableName;
if (wrapperNamesByModule.size() == 1) {
entryPointModuleId = wrapperNamesByModule.keySet().iterator().next();
entryPointCallableName = wrapperNamesByModule.get(entryPointModuleId);
} else {
entryPointModuleId = baseBackend.getEntryPointModuleId();
entryPointCallableName = baseBackend.getEntryPointCallableName();
}
final var sortedModuleIds = new ArrayList<ModuleId>();
sortedModuleIds.addAll(fileInitFunctionsByModule.keySet());
for (final var moduleId : wrapperNamesByModule.keySet()) {
if (!sortedModuleIds.contains(moduleId)) {
sortedModuleIds.add(moduleId);
}
}
sortedModuleIds.sort(Comparator.comparing(moduleId -> moduleSortKey(baseBackend, moduleId)));
final var moduleInitCallableIds = new LinkedHashMap<ModuleId, CallableId>();
for (final var moduleId : sortedModuleIds) {
final var moduleInitName = PbsFrontendCompiler.moduleInitCallableName(moduleId);
final var fileInitFunctions = new ArrayList<>(fileInitFunctionsByModule.getOrDefault(moduleId, List.of()));
if (fileInitFunctions.isEmpty()) {
continue;
}
fileInitFunctions.sort(Comparator.comparingInt(fn -> fn.fileId().getId()));
final var callableId = new CallableId(callableSignatures.size());
callableSignatures.add(new CallableSignatureRef(moduleId, moduleInitName, 0, "() -> unit"));
moduleInitCallableIds.put(moduleId, callableId);
final var instructions = new ArrayList<IRBackendExecutableFunction.Instruction>();
var anchorSpan = fileInitFunctions.getFirst().span();
var anchorFileId = fileInitFunctions.getFirst().fileId();
for (final var fileInitFunction : fileInitFunctions) {
instructions.add(callInstruction(fileInitFunction, fileInitFunction.span()));
anchorSpan = fileInitFunction.span();
anchorFileId = fileInitFunction.fileId();
}
instructions.add(retInstruction(anchorSpan));
executableFunctions.add(new IRBackendExecutableFunction(
anchorFileId,
final var ordered = new ArrayList<ModuleId>(availableModules.size());
final var visiting = new HashSet<ModuleId>();
final var visited = new HashSet<ModuleId>();
for (final var moduleId : availableModules) {
appendDependenciesFirst(
moduleId,
moduleInitName,
callableId,
safeToInt(anchorSpan.getStart()),
safeToInt(anchorSpan.getEnd()),
0,
0,
0,
1,
ReadOnlyList.wrap(instructions),
anchorSpan));
availableModules,
dependenciesByModule,
visiting,
visited,
ordered);
}
return ReadOnlyList.wrap(ordered);
}
CallableId projectInitCallableId = null;
if (!entryPointModuleId.isNone()) {
final var projectInitAnchor = projectInitAnchorsByModule.get(entryPointModuleId);
final var projectInitTarget = resolveExecutable(executableFunctions, entryPointModuleId, projectInitAnchor);
if (projectInitTarget != null) {
final var projectInitName = PbsFrontendCompiler.projectInitCallableName(entryPointModuleId);
projectInitCallableId = new CallableId(callableSignatures.size());
callableSignatures.add(new CallableSignatureRef(entryPointModuleId, projectInitName, 0, "() -> unit"));
executableFunctions.add(new IRBackendExecutableFunction(
projectInitTarget.fileId(),
entryPointModuleId,
projectInitName,
projectInitCallableId,
projectInitTarget.sourceStart(),
projectInitTarget.sourceEnd(),
0,
0,
0,
1,
ReadOnlyList.from(
callInstruction(projectInitTarget, projectInitTarget.span()),
retInstruction(projectInitTarget.span())),
projectInitTarget.span()));
}
}
final var frameExecutable = resolveExecutable(
executableFunctions,
entryPointModuleId,
frameAnchorsByModule.get(entryPointModuleId));
if (frameExecutable != null
&& entryPointCallableName != null
&& !entryPointCallableName.isBlank()
&& !entryPointModuleId.isNone()) {
final var existingBootGuard = globals.stream()
.filter(global -> moduleIdsMatch(entryPointModuleId, global.moduleId()))
.filter(global -> global.hiddenKind() == IRHiddenGlobalKind.BOOT_GUARD)
.findFirst()
.orElse(null);
final var bootGuardSlot = existingBootGuard != null
? existingBootGuard.slot()
: nextGlobalSlot(globals, entryPointModuleId);
final var wrapperCallableId = new CallableId(callableSignatures.size());
callableSignatures.add(new CallableSignatureRef(entryPointModuleId, entryPointCallableName, 0, "() -> unit"));
final var instructions = new ArrayList<IRBackendExecutableFunction.Instruction>();
instructions.add(getGlobalInstruction(bootGuardSlot, frameExecutable.span()));
instructions.add(pushBoolInstruction(true, frameExecutable.span()));
instructions.add(eqInstruction(frameExecutable.span()));
instructions.add(jumpIfTrueInstruction("bootstrap_done", frameExecutable.span()));
for (final var moduleId : sortedModuleIds) {
final var moduleInitCallableId = moduleInitCallableIds.get(moduleId);
if (moduleInitCallableId == null) {
continue;
}
instructions.add(new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.CALL_FUNC,
moduleId,
PbsFrontendCompiler.moduleInitCallableName(moduleId),
moduleInitCallableId,
null,
null,
0,
0,
frameExecutable.span()));
}
if (projectInitCallableId != null) {
instructions.add(new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.CALL_FUNC,
entryPointModuleId,
PbsFrontendCompiler.projectInitCallableName(entryPointModuleId),
projectInitCallableId,
null,
null,
0,
0,
frameExecutable.span()));
}
instructions.add(pushBoolInstruction(true, frameExecutable.span()));
instructions.add(setGlobalInstruction(bootGuardSlot, frameExecutable.span()));
instructions.add(labelInstruction("bootstrap_done", frameExecutable.span()));
instructions.add(callInstruction(frameExecutable, frameExecutable.span()));
instructions.add(retInstruction(frameExecutable.span()));
executableFunctions.add(new IRBackendExecutableFunction(
frameExecutable.fileId(),
entryPointModuleId,
entryPointCallableName,
wrapperCallableId,
frameExecutable.sourceStart(),
frameExecutable.sourceEnd(),
0,
0,
0,
2,
ReadOnlyList.wrap(instructions),
frameExecutable.span()));
if (existingBootGuard == null) {
globals.add(new IRGlobal(
frameExecutable.fileId(),
entryPointModuleId,
PbsFrontendCompiler.bootGuardGlobalName(entryPointModuleId),
"bool",
bootGuardSlot,
IRGlobalVisibility.MODULE,
true,
IRHiddenGlobalKind.BOOT_GUARD,
new IRGlobalOrigin(
frameExecutable.fileId(),
entryPointModuleId,
frameAnchorsByModule.getOrDefault(entryPointModuleId, frameExecutable.callableName()),
frameExecutable.span(),
"BOOT_GUARD"),
frameExecutable.span()));
}
}
final var normalizedGlobals = normalizeGlobalSlots(globals);
return IRBackend.builder()
.entryPointCallableName(entryPointCallableName)
.entryPointModuleId(entryPointModuleId)
.functions(baseBackend.getFunctions())
.syntheticFunctions(baseBackend.getSyntheticFunctions())
.globals(ReadOnlyList.wrap(normalizedGlobals))
.executableFunctions(ReadOnlyList.wrap(executableFunctions))
.modulePool(baseBackend.getModulePool())
.callableSignatures(ReadOnlyList.wrap(callableSignatures))
.intrinsicPool(baseBackend.getIntrinsicPool())
.reservedMetadata(baseBackend.getReservedMetadata())
.build();
}
private int nextGlobalSlot(
final ArrayList<IRGlobal> globals,
final ModuleId moduleId) {
var nextSlot = 0;
for (final var global : globals) {
if (!moduleIdsMatch(moduleId, global.moduleId())) {
continue;
}
nextSlot = Math.max(nextSlot, global.slot() + 1);
}
return nextSlot;
}
private ArrayList<IRGlobal> normalizeGlobalSlots(final ArrayList<IRGlobal> globals) {
final var nextSlotByModule = new LinkedHashMap<ModuleId, Integer>();
final var normalized = new ArrayList<IRGlobal>(globals.size());
for (final var global : globals) {
final var moduleId = normalizeModuleId(global.moduleId());
final var slot = nextSlotByModule.getOrDefault(moduleId, 0);
nextSlotByModule.put(moduleId, slot + 1);
normalized.add(new IRGlobal(
global.fileId(),
moduleId,
global.name(),
global.declaredTypeSurface(),
slot,
global.visibility(),
global.isHidden(),
global.hiddenKind(),
global.origin(),
global.span()));
}
return normalized;
}
private IRBackendExecutableFunction resolveExecutable(
final ArrayList<IRBackendExecutableFunction> executableFunctions,
private void appendDependenciesFirst(
final ModuleId moduleId,
final String callableName) {
if (callableName == null || callableName.isBlank()) {
return null;
final Set<ModuleId> availableModules,
final Map<ModuleId, ReadOnlyList<ModuleId>> dependenciesByModule,
final Set<ModuleId> visiting,
final Set<ModuleId> visited,
final ArrayList<ModuleId> ordered) {
if (visited.contains(moduleId) || !availableModules.contains(moduleId)) {
return;
}
for (final var executable : executableFunctions) {
if (!callableName.equals(executable.callableName())) {
continue;
if (!visiting.add(moduleId)) {
return;
}
if (!moduleIdsMatch(moduleId, executable.moduleId())) {
continue;
for (final var dependency : dependenciesByModule.getOrDefault(moduleId, ReadOnlyList.empty())) {
appendDependenciesFirst(
dependency,
availableModules,
dependenciesByModule,
visiting,
visited,
ordered);
}
return executable;
visiting.remove(moduleId);
if (visited.add(moduleId)) {
ordered.add(moduleId);
}
return null;
}
private IRBackendExecutableFunction.Instruction callInstruction(
final IRBackendExecutableFunction callee,
final Span span) {
return new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.CALL_FUNC,
callee.moduleId(),
callee.callableName(),
callee.callableId(),
null,
null,
callee.paramSlots(),
callee.returnSlots(),
span);
}
private IRBackendExecutableFunction.Instruction retInstruction(final Span span) {
return new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.RET,
"",
null,
null,
span);
}
private IRBackendExecutableFunction.Instruction getGlobalInstruction(
final int slot,
final Span span) {
return new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.GET_GLOBAL,
"",
null,
null,
null,
slot,
null,
span);
}
private IRBackendExecutableFunction.Instruction setGlobalInstruction(
final int slot,
final Span span) {
return new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.SET_GLOBAL,
"",
null,
null,
null,
slot,
null,
span);
}
private IRBackendExecutableFunction.Instruction pushBoolInstruction(
final boolean value,
final Span span) {
return new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.PUSH_BOOL,
"",
null,
null,
null,
value ? "true" : "false",
null,
null,
null,
span);
}
private IRBackendExecutableFunction.Instruction eqInstruction(final Span span) {
return new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.EQ,
"",
null,
null,
null,
span);
}
private IRBackendExecutableFunction.Instruction labelInstruction(
final String label,
final Span span) {
return new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.LABEL,
"",
null,
null,
null,
label,
null,
null,
null,
span);
}
private IRBackendExecutableFunction.Instruction jumpIfTrueInstruction(
final String targetLabel,
final Span span) {
return new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.JMP_IF_TRUE,
"",
null,
null,
null,
null,
targetLabel,
null,
null,
span);
}
private ModuleId normalizeModuleId(final ModuleId moduleId) {
return moduleId == null ? ModuleId.none() : moduleId;
}
private boolean moduleIdsMatch(
final ModuleId left,
final ModuleId right) {
final var normalizedLeft = normalizeModuleId(left);
final var normalizedRight = normalizeModuleId(right);
if (normalizedLeft.isNone() && normalizedRight.isNone()) {
return true;
}
return !normalizedLeft.isNone()
&& !normalizedRight.isNone()
&& normalizedLeft.getIndex() == normalizedRight.getIndex();
}
private String moduleSortKey(
final IRBackend backend,
final ModuleId moduleId) {
if (moduleId != null && !moduleId.isNone()) {
final var index = moduleId.getIndex();
if (index >= 0 && index < backend.getModulePool().size()) {
final var moduleReference = backend.getModulePool().get(index);
final var joiner = new StringJoiner("/", moduleReference.project() + ":", "");
for (final var segment : moduleReference.pathSegments()) {
joiner.add(segment);
}
return joiner.toString();
}
}
return "";
}
private int safeToInt(final long value) {
if (value > Integer.MAX_VALUE) {
return Integer.MAX_VALUE;
}
if (value < Integer.MIN_VALUE) {
return Integer.MIN_VALUE;
}
return (int) value;
}
private Set<ModuleId> blockedModulesByDependency(
final Set<ModuleId> failedModuleIds,
final Map<ModuleId, Set<ModuleId>> dependenciesByModule) {
final Map<ModuleId, ReadOnlyList<ModuleId>> dependenciesByModule) {
final Map<ModuleId, Set<ModuleId>> dependentsByModule = new HashMap<>();
for (final var entry : dependenciesByModule.entrySet()) {
final var importer = entry.getKey();

View File

@ -7,13 +7,14 @@ import p.studio.compiler.source.tables.ModuleTable;
import p.studio.utilities.structures.ReadOnlyList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
record PbsModuleAssembly(
ReadOnlyList<PbsParsedSourceFile> parsedSourceFiles,
Map<ModuleId, Set<ModuleId>> moduleDependencyGraph,
Map<ModuleId, ReadOnlyList<ModuleId>> moduleDependencyGraph,
ReadOnlyList<ModuleReference> canonicalModulePool,
Set<ModuleId> failedModuleIds,
ModuleTable moduleTable,
@ -26,11 +27,11 @@ record PbsModuleAssembly(
if (moduleDependencyGraph == null || moduleDependencyGraph.isEmpty()) {
moduleDependencyGraph = Map.of();
} else {
final var copiedGraph = new HashMap<ModuleId, Set<ModuleId>>();
final var copiedGraph = new LinkedHashMap<ModuleId, ReadOnlyList<ModuleId>>();
for (final var entry : moduleDependencyGraph.entrySet()) {
copiedGraph.put(entry.getKey(), Set.copyOf(entry.getValue() == null ? Set.of() : entry.getValue()));
copiedGraph.put(entry.getKey(), entry.getValue() == null ? ReadOnlyList.empty() : entry.getValue());
}
moduleDependencyGraph = Map.copyOf(copiedGraph);
moduleDependencyGraph = java.util.Collections.unmodifiableMap(copiedGraph);
}
if (moduleTable == null) {

View File

@ -353,20 +353,24 @@ final class PbsModuleAssemblyService {
}
}
private Map<ModuleId, Set<ModuleId>> buildModuleDependencyGraph(
private Map<ModuleId, ReadOnlyList<ModuleId>> buildModuleDependencyGraph(
final ArrayList<PbsParsedSourceFile> parsedSourceFiles,
final ModuleTable moduleTable) {
final Map<ModuleId, Set<ModuleId>> dependenciesByModule = new HashMap<>();
final Map<ModuleId, LinkedHashSet<ModuleId>> dependenciesByModule = new LinkedHashMap<>();
for (final var parsedSource : parsedSourceFiles) {
final var moduleDependencies = dependenciesByModule.computeIfAbsent(
parsedSource.moduleId(),
ignored -> new HashSet<>());
ignored -> new LinkedHashSet<>());
for (final var importDecl : parsedSource.ast().imports()) {
final var moduleRef = importDecl.moduleRef();
moduleDependencies.add(moduleId(moduleTable, moduleRef.project(), moduleRef.pathSegments()));
}
}
return dependenciesByModule;
final Map<ModuleId, ReadOnlyList<ModuleId>> ordered = new LinkedHashMap<>();
for (final var entry : dependenciesByModule.entrySet()) {
ordered.put(entry.getKey(), ReadOnlyList.wrap(new ArrayList<>(entry.getValue())));
}
return java.util.Collections.unmodifiableMap(ordered);
}
private ModuleId moduleId(

View File

@ -8,6 +8,7 @@ import p.studio.compiler.models.IRBackendExecutableFunction;
import p.studio.compiler.models.IRBackendFile;
import p.studio.compiler.models.IRHiddenGlobalKind;
import p.studio.compiler.models.IRGlobalVisibility;
import p.studio.compiler.models.IRLifecycleCallableRole;
import p.studio.compiler.models.IRSyntheticCallableKind;
import p.studio.compiler.models.SourceKind;
import p.studio.compiler.pbs.lexer.LexErrors;
@ -106,7 +107,7 @@ class PbsFrontendCompilerTest {
}
@Test
void shouldExposeGlobalsAndSyntheticLifecycleArtifactsInBackendFile() {
void shouldExposeGlobalsAndTypedLifecycleDeclarationInBackendFile() {
final var source = """
declare global SCORE: int = 0;
@ -129,12 +130,20 @@ class PbsFrontendCompilerTest {
assertFalse(fileBackend.globals().getFirst().isHidden());
assertEquals(IRHiddenGlobalKind.NONE, fileBackend.globals().getFirst().hiddenKind());
assertEquals("SCORE", fileBackend.globals().getFirst().origin().derivedFromUserSymbol());
assertEquals(4, fileBackend.syntheticFunctions().size());
assertEquals(1, fileBackend.syntheticFunctions().size());
assertTrue(fileBackend.syntheticFunctions().stream().anyMatch(fn -> fn.kind() == IRSyntheticCallableKind.FILE_INIT_FRAGMENT));
assertTrue(fileBackend.syntheticFunctions().stream().anyMatch(fn -> fn.kind() == IRSyntheticCallableKind.MODULE_INIT));
assertTrue(fileBackend.syntheticFunctions().stream().anyMatch(fn -> fn.kind() == IRSyntheticCallableKind.PROJECT_INIT));
assertTrue(fileBackend.syntheticFunctions().stream().anyMatch(fn -> fn.kind() == IRSyntheticCallableKind.PUBLISHED_FRAME_WRAPPER));
assertTrue(fileBackend.syntheticFunctions().stream().allMatch(fn -> !fn.origin().anchorCallableName().isBlank()));
assertEquals(IRLifecycleCallableRole.FILE_INIT_FRAGMENT,
fileBackend.lifecycleDeclaration().fileInitFragment().role());
assertEquals(IRLifecycleCallableRole.PROJECT_INIT,
fileBackend.lifecycleDeclaration().projectInit().role());
assertEquals(IRLifecycleCallableRole.FRAME_ROOT,
fileBackend.lifecycleDeclaration().frameRoot().role());
final var fileInit = fileBackend.executableFunctions().stream()
.filter(fn -> fn.callableId().equals(fileBackend.lifecycleDeclaration().fileInitFragment().callableId()))
.findFirst()
.orElseThrow();
assertTrue(fileInit.instructions().stream().noneMatch(instruction -> "boot".equals(instruction.calleeCallableName())));
}
@Test

View File

@ -386,8 +386,10 @@ class PBSFrontendPhaseServiceTest {
assertTrue(diagnostics.isEmpty());
assertEquals(2, irBackend.getFunctions().size());
assertTrue(irBackend.getExecutableFunctions().stream().anyMatch(function -> "frame".equals(function.callableName())));
assertTrue(irBackend.getExecutableFunctions().stream().anyMatch(function ->
function.callableName().startsWith("__pbs.frame_wrapper$m")));
assertNotNull(irBackend.getLifecycleDeclaration());
assertEquals(IRLifecycleCallableRole.FRAME_ROOT, irBackend.getLifecycleDeclaration().frameRoot().role());
assertTrue(irBackend.getSyntheticFunctions().stream().noneMatch(function ->
function.kind() == IRSyntheticCallableKind.PUBLISHED_FRAME_WRAPPER));
}
@Test
@ -483,36 +485,19 @@ class PBSFrontendPhaseServiceTest {
BuildingIssueSink.empty());
assertTrue(diagnostics.isEmpty(), diagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList().toString());
assertTrue(irBackend.getEntryPointCallableName().startsWith("__pbs.frame_wrapper$m"));
assertNotNull(irBackend.getLifecycleDeclaration());
assertEquals(IRLifecycleCallableRole.PROJECT_INIT, irBackend.getLifecycleDeclaration().projectInit().role());
assertEquals(IRLifecycleCallableRole.FRAME_ROOT, irBackend.getLifecycleDeclaration().frameRoot().role());
final var scoreGlobal = irBackend.getGlobals().stream()
.filter(global -> "SCORE".equals(global.name()))
.findFirst()
.orElseThrow();
assertEquals(IRGlobalVisibility.PUBLIC, scoreGlobal.visibility());
final var bootGuard = irBackend.getGlobals().stream()
.filter(global -> global.hiddenKind() == IRHiddenGlobalKind.BOOT_GUARD)
.findFirst()
.orElseThrow();
assertTrue(bootGuard.isHidden());
assertEquals(IRGlobalVisibility.MODULE, bootGuard.visibility());
assertTrue(bootGuard.name().startsWith("__pbs.boot_guard$m"));
assertEquals(1, bootGuard.slot());
final var wrapper = irBackend.getExecutableFunctions().stream()
.filter(function -> irBackend.getEntryPointCallableName().equals(function.callableName()))
.findFirst()
.orElseThrow();
assertEquals(11, wrapper.instructions().size());
assertEquals(p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.GET_GLOBAL, wrapper.instructions().get(0).kind());
assertEquals(p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.PUSH_BOOL, wrapper.instructions().get(1).kind());
assertEquals(p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.EQ, wrapper.instructions().get(2).kind());
assertEquals(p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.JMP_IF_TRUE, wrapper.instructions().get(3).kind());
assertTrue(wrapper.instructions().get(4).calleeCallableName().startsWith("__pbs.module_init$m"));
assertTrue(wrapper.instructions().get(5).calleeCallableName().startsWith("__pbs.project_init$m"));
assertEquals(p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.PUSH_BOOL, wrapper.instructions().get(6).kind());
assertEquals(p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.SET_GLOBAL, wrapper.instructions().get(7).kind());
assertEquals(p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.LABEL, wrapper.instructions().get(8).kind());
assertEquals("frame", wrapper.instructions().get(9).calleeCallableName());
assertEquals(p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.RET, wrapper.instructions().get(10).kind());
assertTrue(irBackend.getGlobals().stream().noneMatch(global -> global.hiddenKind() == IRHiddenGlobalKind.BOOT_GUARD));
assertTrue(irBackend.getSyntheticFunctions().stream().noneMatch(function ->
function.kind() == IRSyntheticCallableKind.MODULE_INIT
|| function.kind() == IRSyntheticCallableKind.PROJECT_INIT
|| function.kind() == IRSyntheticCallableKind.PUBLISHED_FRAME_WRAPPER));
}
@Test
@ -572,16 +557,100 @@ class PBSFrontendPhaseServiceTest {
BuildingIssueSink.empty());
assertTrue(diagnostics.isEmpty(), diagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList().toString());
assertTrue(irBackend.getEntryPointCallableName().startsWith("__pbs.frame_wrapper$m"));
assertNotNull(irBackend.getLifecycleDeclaration());
assertTrue(irBackend.getExecutableFunctions().stream().anyMatch(function ->
function.callableName().startsWith("__pbs.file_init$f")));
assertTrue(irBackend.getExecutableFunctions().stream().anyMatch(function ->
function.callableName().startsWith("__pbs.module_init$m")));
assertTrue(irBackend.getExecutableFunctions().stream().anyMatch(function ->
function.callableName().startsWith("__pbs.project_init$m")));
assertEquals(1, irBackend.getLifecycleDeclaration().modules().getFirst().fileInitFragments().size());
assertEquals(IRLifecycleCallableRole.PROJECT_INIT, irBackend.getLifecycleDeclaration().projectInit().role());
assertEquals(IRLifecycleCallableRole.FRAME_ROOT, irBackend.getLifecycleDeclaration().frameRoot().role());
assertTrue(irBackend.getGlobals().stream().anyMatch(global ->
"SCORE".equals(global.name()) && global.visibility() == IRGlobalVisibility.PUBLIC));
assertTrue(irBackend.getGlobals().stream().anyMatch(global -> global.hiddenKind() == IRHiddenGlobalKind.BOOT_GUARD));
assertTrue(irBackend.getGlobals().stream().noneMatch(global -> global.hiddenKind() == IRHiddenGlobalKind.BOOT_GUARD));
}
@Test
void shouldPublishDependencyOrderedLifecycleModulesInsteadOfNameOrFileIdOrder() throws IOException {
final var projectRoot = tempDir.resolve("project-lifecycle-dep-order");
final var sourceRoot = projectRoot.resolve("src");
final var zzzPath = sourceRoot.resolve("zzz");
final var aaaPath = sourceRoot.resolve("aaa");
Files.createDirectories(zzzPath);
Files.createDirectories(aaaPath);
final var zzzSource = zzzPath.resolve("source.pbs");
final var zzzBarrel = zzzPath.resolve("mod.barrel");
Files.writeString(zzzSource, """
declare global WARM: int = 1;
[Init]
fn warmup() -> void { return; }
""");
Files.writeString(zzzBarrel, """
pub fn warmup() -> void;
pub global WARM;
""");
final var aaaSource = aaaPath.resolve("source.pbs");
final var aaaBarrel = aaaPath.resolve("mod.barrel");
Files.writeString(aaaSource, """
import { warmup } from @app:zzz;
[Init]
fn boot() -> void {
warmup();
return;
}
[Frame]
fn frame() -> void { return; }
""");
Files.writeString(aaaBarrel, """
pub fn boot() -> void;
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("app")
.version("1.0.0")
.sourceRoots(ReadOnlyList.wrap(List.of(sourceRoot)))
.build());
registerFile(projectId, projectRoot, aaaSource, fileTable);
registerFile(projectId, projectRoot, aaaBarrel, fileTable);
registerFile(projectId, projectRoot, zzzSource, fileTable);
registerFile(projectId, projectRoot, zzzBarrel, fileTable);
final var ctx = new FrontendPhaseContext(
projectTable,
fileTable,
new BuildStack(ReadOnlyList.wrap(List.of(projectId))),
AppMode.Game);
final var diagnostics = DiagnosticSink.empty();
final var irBackend = new PBSFrontendPhaseService().compile(
ctx,
diagnostics,
LogAggregator.empty(),
BuildingIssueSink.empty());
assertEquals(0, diagnostics.errorCount(), diagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList().toString());
final var declaration = irBackend.getLifecycleDeclaration();
assertNotNull(declaration);
assertEquals(2, declaration.modules().size());
final var firstModule = irBackend.getModulePool().get(declaration.modules().get(0).moduleId().getIndex());
final var secondModule = irBackend.getModulePool().get(declaration.modules().get(1).moduleId().getIndex());
assertEquals("zzz", firstModule.pathSegments().getFirst());
assertEquals("aaa", secondModule.pathSegments().getFirst());
assertFalse(declaration.modules().get(0).fileInitFragments().isEmpty());
assertEquals(IRLifecycleCallableRole.PROJECT_INIT, declaration.projectInit().role());
assertEquals(IRLifecycleCallableRole.FRAME_ROOT, declaration.frameRoot().role());
assertTrue(irBackend.getSyntheticFunctions().stream().noneMatch(function ->
function.kind() == IRSyntheticCallableKind.MODULE_INIT
|| function.kind() == IRSyntheticCallableKind.PROJECT_INIT
|| function.kind() == IRSyntheticCallableKind.PUBLISHED_FRAME_WRAPPER));
}
@Test

View File

@ -500,6 +500,8 @@ public class LowerToIRVMService {
}
private void validateCommonLifecycleStructure(final IRBackend backend) {
// Backend precondition on an already-assembled handoff. Declaration
// validation and materialization belong to LifecycleAssemblerService.
if (backend.getSyntheticFunctions().isEmpty()) {
return;
}

View File

@ -0,0 +1,532 @@
package p.studio.compiler.lifecycle;
import p.studio.compiler.messages.BuildingIssueSink;
import p.studio.compiler.models.*;
import p.studio.compiler.source.Span;
import p.studio.compiler.source.identifiers.CallableId;
import p.studio.compiler.source.identifiers.ModuleId;
import p.studio.compiler.source.tables.CallableSignatureRef;
import p.studio.utilities.structures.ReadOnlyList;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Set;
public final class LifecycleAssemblerService {
public static final String PHASE = "lifecycle-assembly";
public static final String INVALID_DECLARATION = "COMMON_LIFECYCLE_INVALID_DECLARATION";
public IRBackend assemble(
final IRBackend backend,
final BuildingIssueSink issues) {
if (backend == null) {
report(issues, null, "IRBackend is required for lifecycle assembly");
return null;
}
final var declaration = backend.getLifecycleDeclaration();
if (declaration == null) {
return backend;
}
if (!validate(backend, declaration, issues)) {
return backend;
}
final var callableSignatures = new ArrayList<>(backend.getCallableSignatures().asList());
final var executableFunctions = new ArrayList<>(backend.getExecutableFunctions().asList());
final var syntheticFunctions = new ArrayList<>(backend.getSyntheticFunctions().asList());
final var globals = new ArrayList<>(backend.getGlobals().asList());
final var moduleInitCallableIds = new LinkedHashMap<ModuleId, CallableId>();
for (final var module : declaration.modules()) {
if (module.fileInitFragments().isEmpty()) {
continue;
}
final var moduleId = module.moduleId();
final var moduleInitName = moduleInitCallableName(moduleId);
final var moduleInitCallableId = new CallableId(callableSignatures.size());
callableSignatures.add(new CallableSignatureRef(moduleId, moduleInitName, 0, "() -> unit"));
moduleInitCallableIds.put(moduleId, moduleInitCallableId);
final var instructions = new ArrayList<IRBackendExecutableFunction.Instruction>();
IRBackendExecutableFunction anchor = null;
for (final var fragment : module.fileInitFragments()) {
final var executable = resolveExecutable(backend, fragment);
instructions.add(callInstruction(executable, fragment.origin()));
anchor = executable;
}
instructions.add(retInstruction(anchor.span()));
executableFunctions.add(new IRBackendExecutableFunction(
anchor.fileId(),
moduleId,
moduleInitName,
moduleInitCallableId,
anchor.sourceStart(),
anchor.sourceEnd(),
0,
0,
0,
1,
ReadOnlyList.wrap(instructions),
anchor.span()));
syntheticFunctions.add(new IRSyntheticFunction(
moduleId,
moduleInitName,
IRSyntheticCallableKind.MODULE_INIT,
new IRSyntheticOrigin(anchor.fileId(), anchor.callableName(), anchor.span())));
}
final var frameExecutable = resolveExecutable(backend, declaration.frameRoot());
final var entryPointModuleId = frameExecutable.moduleId();
CallableId projectInitCallableId = null;
if (declaration.projectInit() != null) {
final var projectInitTarget = resolveExecutable(backend, declaration.projectInit());
final var projectInitName = projectInitCallableName(entryPointModuleId);
projectInitCallableId = new CallableId(callableSignatures.size());
callableSignatures.add(new CallableSignatureRef(
entryPointModuleId,
projectInitName,
0,
"() -> unit"));
executableFunctions.add(new IRBackendExecutableFunction(
projectInitTarget.fileId(),
entryPointModuleId,
projectInitName,
projectInitCallableId,
projectInitTarget.sourceStart(),
projectInitTarget.sourceEnd(),
0,
0,
0,
1,
ReadOnlyList.from(
callInstruction(projectInitTarget, projectInitTarget.span()),
retInstruction(projectInitTarget.span())),
projectInitTarget.span()));
syntheticFunctions.add(new IRSyntheticFunction(
entryPointModuleId,
projectInitName,
IRSyntheticCallableKind.PROJECT_INIT,
new IRSyntheticOrigin(
projectInitTarget.fileId(),
projectInitTarget.callableName(),
projectInitTarget.span())));
}
final var entryPointCallableName = frameWrapperCallableName(entryPointModuleId);
final var bootGuardSlot = nextGlobalSlot(globals, entryPointModuleId);
final var wrapperCallableId = new CallableId(callableSignatures.size());
callableSignatures.add(new CallableSignatureRef(
entryPointModuleId,
entryPointCallableName,
0,
"() -> unit"));
final var wrapperInstructions = new ArrayList<IRBackendExecutableFunction.Instruction>();
wrapperInstructions.add(getGlobalInstruction(bootGuardSlot, frameExecutable.span()));
wrapperInstructions.add(pushBoolInstruction(true, frameExecutable.span()));
wrapperInstructions.add(eqInstruction(frameExecutable.span()));
wrapperInstructions.add(jumpIfTrueInstruction("bootstrap_done", frameExecutable.span()));
for (final var module : declaration.modules()) {
final var moduleInitCallableId = moduleInitCallableIds.get(module.moduleId());
if (moduleInitCallableId == null) {
continue;
}
wrapperInstructions.add(new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.CALL_FUNC,
module.moduleId(),
moduleInitCallableName(module.moduleId()),
moduleInitCallableId,
null,
null,
0,
0,
frameExecutable.span()));
}
if (projectInitCallableId != null) {
wrapperInstructions.add(new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.CALL_FUNC,
entryPointModuleId,
projectInitCallableName(entryPointModuleId),
projectInitCallableId,
null,
null,
0,
0,
frameExecutable.span()));
}
wrapperInstructions.add(pushBoolInstruction(true, frameExecutable.span()));
wrapperInstructions.add(setGlobalInstruction(bootGuardSlot, frameExecutable.span()));
wrapperInstructions.add(labelInstruction("bootstrap_done", frameExecutable.span()));
wrapperInstructions.add(callInstruction(frameExecutable, frameExecutable.span()));
wrapperInstructions.add(retInstruction(frameExecutable.span()));
executableFunctions.add(new IRBackendExecutableFunction(
frameExecutable.fileId(),
entryPointModuleId,
entryPointCallableName,
wrapperCallableId,
frameExecutable.sourceStart(),
frameExecutable.sourceEnd(),
0,
0,
0,
2,
ReadOnlyList.wrap(wrapperInstructions),
frameExecutable.span()));
syntheticFunctions.add(new IRSyntheticFunction(
entryPointModuleId,
entryPointCallableName,
IRSyntheticCallableKind.PUBLISHED_FRAME_WRAPPER,
new IRSyntheticOrigin(
frameExecutable.fileId(),
frameExecutable.callableName(),
frameExecutable.span())));
globals.add(new IRGlobal(
frameExecutable.fileId(),
entryPointModuleId,
bootGuardGlobalName(entryPointModuleId),
"bool",
bootGuardSlot,
IRGlobalVisibility.MODULE,
true,
IRHiddenGlobalKind.BOOT_GUARD,
new IRGlobalOrigin(
frameExecutable.fileId(),
entryPointModuleId,
frameExecutable.callableName(),
frameExecutable.span(),
"BOOT_GUARD"),
frameExecutable.span()));
return IRBackend.builder()
.entryPointCallableName(entryPointCallableName)
.entryPointModuleId(entryPointModuleId)
.functions(backend.getFunctions())
.syntheticFunctions(ReadOnlyList.wrap(syntheticFunctions))
.globals(ReadOnlyList.wrap(normalizeGlobalSlots(globals)))
.executableFunctions(ReadOnlyList.wrap(executableFunctions))
.modulePool(backend.getModulePool())
.callableSignatures(ReadOnlyList.wrap(callableSignatures))
.intrinsicPool(backend.getIntrinsicPool())
.reservedMetadata(backend.getReservedMetadata())
.lifecycleDeclaration(declaration)
.build();
}
private boolean validate(
final IRBackend backend,
final IRLifecycleDeclaration declaration,
final BuildingIssueSink issues) {
var valid = true;
for (final var synthetic : backend.getSyntheticFunctions()) {
if (synthetic.kind() != IRSyntheticCallableKind.FILE_INIT_FRAGMENT) {
report(issues, synthetic.origin().span(),
"frontend handoff contains pre-materialized lifecycle artifact: " + synthetic.kind());
valid = false;
}
}
for (final var global : backend.getGlobals()) {
if (global.hiddenKind() == IRHiddenGlobalKind.BOOT_GUARD) {
report(issues, global.span(), "frontend handoff contains pre-materialized boot guard");
valid = false;
}
}
final Set<Integer> moduleIds = new HashSet<>();
final Set<Integer> lifecycleCallableIds = new HashSet<>();
for (final var module : declaration.modules()) {
final var moduleIndex = module.moduleId().isNone() ? -1 : module.moduleId().getIndex();
if (!moduleIds.add(moduleIndex)) {
report(issues, null, "duplicate lifecycle module: " + moduleIndex);
valid = false;
}
for (final var fragment : module.fileInitFragments()) {
valid &= validateReference(
backend,
fragment,
IRLifecycleCallableRole.FILE_INIT_FRAGMENT,
module.moduleId(),
lifecycleCallableIds,
issues);
}
}
if (declaration.projectInit() != null) {
valid &= validateReference(
backend,
declaration.projectInit(),
IRLifecycleCallableRole.PROJECT_INIT,
declaration.projectInit().moduleId(),
lifecycleCallableIds,
issues);
}
if (declaration.frameRoot() == null) {
report(issues, null, "executable lifecycle declaration is missing frame root");
valid = false;
} else {
valid &= validateReference(
backend,
declaration.frameRoot(),
IRLifecycleCallableRole.FRAME_ROOT,
declaration.frameRoot().moduleId(),
lifecycleCallableIds,
issues);
}
return valid;
}
private boolean validateReference(
final IRBackend backend,
final IRLifecycleCallableReference reference,
final IRLifecycleCallableRole expectedRole,
final ModuleId expectedModuleId,
final Set<Integer> lifecycleCallableIds,
final BuildingIssueSink issues) {
if (reference == null) {
report(issues, null, "missing lifecycle reference for role " + expectedRole);
return false;
}
var valid = true;
if (reference.role() != expectedRole) {
report(issues, reference.origin(), "incompatible lifecycle role: expected "
+ expectedRole + " but found " + reference.role());
valid = false;
}
if (!sameModuleId(reference.moduleId(), expectedModuleId)) {
report(issues, reference.origin(), "lifecycle reference module does not match declaration module");
valid = false;
}
if (reference.origin() == null || reference.origin().isNone()) {
report(issues, reference.origin(), "lifecycle reference origin is missing for role " + expectedRole);
valid = false;
}
final var callableIndex = reference.callableId().getIndex();
if (!lifecycleCallableIds.add(callableIndex)) {
report(issues, reference.origin(), "callable is assigned to multiple lifecycle roles: " + callableIndex);
valid = false;
}
if (callableIndex < 0 || callableIndex >= backend.getCallableSignatures().size()) {
report(issues, reference.origin(), "unresolved lifecycle callable id: " + callableIndex);
return false;
}
final var signature = backend.getCallableSignatures().get(callableIndex);
if (!sameModuleId(signature.moduleId(), reference.moduleId())) {
report(issues, reference.origin(), "lifecycle callable signature module mismatch: " + callableIndex);
valid = false;
}
if (resolveExecutable(backend, reference) == null) {
report(issues, reference.origin(), "lifecycle callable has no executable body: " + callableIndex);
valid = false;
}
return valid;
}
private IRBackendExecutableFunction resolveExecutable(
final IRBackend backend,
final IRLifecycleCallableReference reference) {
if (reference == null) {
return null;
}
for (final var executable : backend.getExecutableFunctions()) {
if (executable.callableId().getIndex() == reference.callableId().getIndex()
&& sameModuleId(executable.moduleId(), reference.moduleId())) {
return executable;
}
}
return null;
}
private void report(
final BuildingIssueSink issues,
final Span span,
final String message) {
final var effectiveSpan = span == null ? Span.none() : span;
issues.report(builder -> builder
.error(true)
.phase(PHASE)
.code(INVALID_DECLARATION)
.fileId(effectiveSpan.getFileId().isNone() ? null : effectiveSpan.getFileId().getId())
.start(safeToInt(effectiveSpan.getStart()))
.end(safeToInt(effectiveSpan.getEnd()))
.message(message));
}
private int nextGlobalSlot(
final ArrayList<IRGlobal> globals,
final ModuleId moduleId) {
var nextSlot = 0;
for (final var global : globals) {
if (sameModuleId(moduleId, global.moduleId())) {
nextSlot = Math.max(nextSlot, global.slot() + 1);
}
}
return nextSlot;
}
private ArrayList<IRGlobal> normalizeGlobalSlots(final ArrayList<IRGlobal> globals) {
final var nextSlotByModule = new LinkedHashMap<ModuleId, Integer>();
final var normalized = new ArrayList<IRGlobal>(globals.size());
for (final var global : globals) {
final var moduleId = normalizeModuleId(global.moduleId());
final var slot = nextSlotByModule.getOrDefault(moduleId, 0);
nextSlotByModule.put(moduleId, slot + 1);
normalized.add(new IRGlobal(
global.fileId(),
moduleId,
global.name(),
global.declaredTypeSurface(),
slot,
global.visibility(),
global.isHidden(),
global.hiddenKind(),
global.origin(),
global.span()));
}
return normalized;
}
private IRBackendExecutableFunction.Instruction callInstruction(
final IRBackendExecutableFunction callee,
final Span span) {
return new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.CALL_FUNC,
callee.moduleId(),
callee.callableName(),
callee.callableId(),
null,
null,
callee.paramSlots(),
callee.returnSlots(),
span);
}
private IRBackendExecutableFunction.Instruction retInstruction(final Span span) {
return new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.RET,
"",
null,
null,
span);
}
private IRBackendExecutableFunction.Instruction getGlobalInstruction(final int slot, final Span span) {
return new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.GET_GLOBAL,
"",
null,
null,
null,
slot,
null,
span);
}
private IRBackendExecutableFunction.Instruction setGlobalInstruction(final int slot, final Span span) {
return new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.SET_GLOBAL,
"",
null,
null,
null,
slot,
null,
span);
}
private IRBackendExecutableFunction.Instruction pushBoolInstruction(final boolean value, final Span span) {
return new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.PUSH_BOOL,
"",
null,
null,
null,
value ? "true" : "false",
null,
null,
null,
span);
}
private IRBackendExecutableFunction.Instruction eqInstruction(final Span span) {
return new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.EQ,
"",
null,
null,
null,
span);
}
private IRBackendExecutableFunction.Instruction labelInstruction(final String label, final Span span) {
return new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.LABEL,
"",
null,
null,
null,
label,
null,
null,
null,
span);
}
private IRBackendExecutableFunction.Instruction jumpIfTrueInstruction(final String label, final Span span) {
return new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.JMP_IF_TRUE,
"",
null,
null,
null,
null,
label,
null,
null,
span);
}
private ModuleId normalizeModuleId(final ModuleId moduleId) {
return moduleId == null ? ModuleId.none() : moduleId;
}
private boolean sameModuleId(final ModuleId left, final ModuleId right) {
final var normalizedLeft = normalizeModuleId(left);
final var normalizedRight = normalizeModuleId(right);
if (normalizedLeft.isNone() || normalizedRight.isNone()) {
return normalizedLeft.isNone() && normalizedRight.isNone();
}
return normalizedLeft.getIndex() == normalizedRight.getIndex();
}
private String moduleInitCallableName(final ModuleId moduleId) {
return "__prometeu.lifecycle.module_init$m" + normalizedModuleIndex(moduleId);
}
private String projectInitCallableName(final ModuleId moduleId) {
return "__prometeu.lifecycle.project_init$m" + normalizedModuleIndex(moduleId);
}
private String frameWrapperCallableName(final ModuleId moduleId) {
return "__prometeu.lifecycle.frame_wrapper$m" + normalizedModuleIndex(moduleId);
}
private String bootGuardGlobalName(final ModuleId moduleId) {
return "__prometeu.lifecycle.boot_guard$m" + normalizedModuleIndex(moduleId);
}
private int normalizedModuleIndex(final ModuleId moduleId) {
final var normalized = normalizeModuleId(moduleId);
return normalized.isNone() ? -1 : normalized.getIndex();
}
private int safeToInt(final long value) {
if (value > Integer.MAX_VALUE) {
return Integer.MAX_VALUE;
}
if (value < Integer.MIN_VALUE) {
return Integer.MIN_VALUE;
}
return (int) value;
}
}

View File

@ -23,7 +23,8 @@ public class BuilderPipelineService {
final var analyses = List.<PipelineStage>of(
new ResolveDepsPipelineStage(),
new LoadSourcesPipelineStage(),
new FrontendPhasePipelineStage()
new FrontendPhasePipelineStage(),
new AssembleLifecyclePipelineStage()
);
final var compile = List.<PipelineStage>of(

View File

@ -0,0 +1,32 @@
package p.studio.compiler.workspaces.stages;
import lombok.extern.slf4j.Slf4j;
import p.studio.compiler.lifecycle.LifecycleAssemblerService;
import p.studio.compiler.messages.BuildingIssueSink;
import p.studio.compiler.models.BuilderPipelineContext;
import p.studio.compiler.workspaces.PipelineStage;
import p.studio.utilities.logs.LogAggregator;
@Slf4j
public final class AssembleLifecyclePipelineStage implements PipelineStage {
private final LifecycleAssemblerService assembler;
public AssembleLifecyclePipelineStage() {
this(new LifecycleAssemblerService());
}
AssembleLifecyclePipelineStage(final LifecycleAssemblerService assembler) {
this.assembler = assembler;
}
@Override
public BuildingIssueSink run(final BuilderPipelineContext ctx, final LogAggregator logs) {
final var issues = BuildingIssueSink.empty();
final var assembled = assembler.assemble(ctx.irBackend, issues);
if (!issues.hasErrors()) {
ctx.irBackend = assembled;
logs.using(log).debug("Common lifecycle assembly completed successfully!");
}
return issues;
}
}

View File

@ -67,31 +67,31 @@ class LowerToIRVMServiceTest {
@Test
void lowerMustPublishSyntheticWrapperAtFunctionZeroAndKeepFinalRetInWrapperPath() {
final var backend = IRBackend.builder()
.entryPointCallableName("__pbs.frame_wrapper$m0")
.entryPointCallableName("__prometeu.lifecycle.frame_wrapper$m0")
.entryPointModuleId(new ModuleId(0))
.globals(ReadOnlyList.from(hiddenBootGuard(0)))
.syntheticFunctions(ReadOnlyList.from(new IRSyntheticFunction(
new ModuleId(0),
"__pbs.frame_wrapper$m0",
"__prometeu.lifecycle.frame_wrapper$m0",
IRSyntheticCallableKind.PUBLISHED_FRAME_WRAPPER,
new IRSyntheticOrigin(new FileId(0), "frame", new Span(new FileId(0), 1, 10)))))
.executableFunctions(ReadOnlyList.from(
fn("__pbs.file_init$f1", "app", 30, ReadOnlyList.from(ret())),
fn("__pbs.module_init$m0", "app", 31, ReadOnlyList.from(
callFunc("app", "__pbs.file_init$f1", 30),
fn("__prometeu.lifecycle.file_init$f1", "app", 30, ReadOnlyList.from(ret())),
fn("__prometeu.lifecycle.module_init$m0", "app", 31, ReadOnlyList.from(
callFunc("app", "__prometeu.lifecycle.file_init$f1", 30),
ret())),
fn("__pbs.project_init$m0", "app", 32, ReadOnlyList.from(
fn("__prometeu.lifecycle.project_init$m0", "app", 32, ReadOnlyList.from(
callFunc("app", "boot", 10),
ret())),
fn("boot", "app", 10, ReadOnlyList.from(ret())),
fn("frame", "app", 20, ReadOnlyList.from(ret())),
fn("__pbs.frame_wrapper$m0", "app", 40, ReadOnlyList.from(
fn("__prometeu.lifecycle.frame_wrapper$m0", "app", 40, ReadOnlyList.from(
getGlobal(0),
pushBool(true),
eq(),
jmpIfTrue("bootstrap_done"),
callFunc("app", "__pbs.module_init$m0", 31),
callFunc("app", "__pbs.project_init$m0", 32),
callFunc("app", "__prometeu.lifecycle.module_init$m0", 31),
callFunc("app", "__prometeu.lifecycle.project_init$m0", 32),
pushBool(true),
setGlobal(0),
label("bootstrap_done"),
@ -101,7 +101,7 @@ class LowerToIRVMServiceTest {
final var lowered = new LowerToIRVMService().lower(backend);
assertEquals("__pbs.frame_wrapper$m0", lowered.module().functions().get(0).name());
assertEquals("__prometeu.lifecycle.frame_wrapper$m0", lowered.module().functions().get(0).name());
assertEquals(IRVMOp.GET_GLOBAL, lowered.module().functions().get(0).instructions().get(0).op());
assertEquals(IRVMOp.PUSH_BOOL, lowered.module().functions().get(0).instructions().get(1).op());
assertEquals(IRVMOp.EQ, lowered.module().functions().get(0).instructions().get(2).op());
@ -124,12 +124,12 @@ class LowerToIRVMServiceTest {
@Test
void lowerMustRejectWhenSyntheticWrapperEntrypointIsMissing() {
final var backend = IRBackend.builder()
.entryPointCallableName("__pbs.frame_wrapper$m0")
.entryPointCallableName("__prometeu.lifecycle.frame_wrapper$m0")
.entryPointModuleId(new ModuleId(0))
.globals(ReadOnlyList.from(hiddenBootGuard(0)))
.syntheticFunctions(ReadOnlyList.from(new IRSyntheticFunction(
new ModuleId(0),
"__pbs.frame_wrapper$m0",
"__prometeu.lifecycle.frame_wrapper$m0",
IRSyntheticCallableKind.PUBLISHED_FRAME_WRAPPER,
new IRSyntheticOrigin(new FileId(0), "frame", new Span(new FileId(0), 1, 10)))))
.executableFunctions(ReadOnlyList.from(
@ -148,11 +148,11 @@ class LowerToIRVMServiceTest {
.globals(ReadOnlyList.from(hiddenBootGuard(0)))
.syntheticFunctions(ReadOnlyList.from(new IRSyntheticFunction(
new ModuleId(0),
"__pbs.frame_wrapper$m0",
"__prometeu.lifecycle.frame_wrapper$m0",
IRSyntheticCallableKind.PUBLISHED_FRAME_WRAPPER,
new IRSyntheticOrigin(new FileId(0), "frame", new Span(new FileId(0), 1, 10)))))
.executableFunctions(ReadOnlyList.from(
fn("__pbs.frame_wrapper$m0", "app", 40, ReadOnlyList.from(
fn("__prometeu.lifecycle.frame_wrapper$m0", "app", 40, ReadOnlyList.from(
callFunc("app", "frame", 20),
ret())),
fn("frame", "app", 20, ReadOnlyList.from(ret()))))
@ -165,15 +165,15 @@ class LowerToIRVMServiceTest {
@Test
void lowerMustRejectWhenHiddenBootGuardIsMissing() {
final var backend = IRBackend.builder()
.entryPointCallableName("__pbs.frame_wrapper$m0")
.entryPointCallableName("__prometeu.lifecycle.frame_wrapper$m0")
.entryPointModuleId(new ModuleId(0))
.syntheticFunctions(ReadOnlyList.from(new IRSyntheticFunction(
new ModuleId(0),
"__pbs.frame_wrapper$m0",
"__prometeu.lifecycle.frame_wrapper$m0",
IRSyntheticCallableKind.PUBLISHED_FRAME_WRAPPER,
new IRSyntheticOrigin(new FileId(0), "frame", new Span(new FileId(0), 1, 10)))))
.executableFunctions(ReadOnlyList.from(
fn("__pbs.frame_wrapper$m0", "app", 40, ReadOnlyList.from(
fn("__prometeu.lifecycle.frame_wrapper$m0", "app", 40, ReadOnlyList.from(
callFunc("app", "frame", 20),
ret())),
fn("frame", "app", 20, ReadOnlyList.from(ret()))))
@ -186,16 +186,16 @@ class LowerToIRVMServiceTest {
@Test
void lowerMustRejectWhenSyntheticCallableOriginIsMissing() {
final var backend = IRBackend.builder()
.entryPointCallableName("__pbs.frame_wrapper$m0")
.entryPointCallableName("__prometeu.lifecycle.frame_wrapper$m0")
.entryPointModuleId(new ModuleId(0))
.globals(ReadOnlyList.from(userVisibleGlobal("__pbs.boot_guard$m0", 0)))
.globals(ReadOnlyList.from(userVisibleGlobal("__prometeu.lifecycle.boot_guard$m0", 0)))
.syntheticFunctions(ReadOnlyList.from(new IRSyntheticFunction(
new ModuleId(0),
"__pbs.frame_wrapper$m0",
"__prometeu.lifecycle.frame_wrapper$m0",
IRSyntheticCallableKind.PUBLISHED_FRAME_WRAPPER,
new IRSyntheticOrigin(new FileId(0), "", Span.none()))))
.executableFunctions(ReadOnlyList.from(
fn("__pbs.frame_wrapper$m0", "app", 40, ReadOnlyList.from(
fn("__prometeu.lifecycle.frame_wrapper$m0", "app", 40, ReadOnlyList.from(
callFunc("app", "frame", 20),
ret())),
fn("frame", "app", 20, ReadOnlyList.from(ret()))))
@ -450,7 +450,7 @@ class LowerToIRVMServiceTest {
return new IRGlobal(
fileId,
ownerModuleId,
"__pbs.boot_guard$m" + moduleId,
"__prometeu.lifecycle.boot_guard$m" + moduleId,
"bool",
0,
IRGlobalVisibility.MODULE,

View File

@ -0,0 +1,367 @@
package p.studio.compiler.lifecycle;
import org.junit.jupiter.api.Test;
import p.studio.compiler.messages.BuildingIssueSink;
import p.studio.compiler.models.IRBackend;
import p.studio.compiler.models.IRBackendExecutableFunction;
import p.studio.compiler.models.IRGlobal;
import p.studio.compiler.models.IRGlobalOrigin;
import p.studio.compiler.models.IRGlobalVisibility;
import p.studio.compiler.models.IRHiddenGlobalKind;
import p.studio.compiler.models.IRLifecycleCallableReference;
import p.studio.compiler.models.IRLifecycleCallableRole;
import p.studio.compiler.models.IRLifecycleDeclaration;
import p.studio.compiler.models.IRLifecycleModuleDeclaration;
import p.studio.compiler.models.IRSyntheticCallableKind;
import p.studio.compiler.models.IRSyntheticFunction;
import p.studio.compiler.models.IRSyntheticOrigin;
import p.studio.compiler.source.Span;
import p.studio.compiler.source.identifiers.CallableId;
import p.studio.compiler.source.identifiers.FileId;
import p.studio.compiler.source.identifiers.ModuleId;
import p.studio.compiler.source.tables.CallableSignatureRef;
import p.studio.compiler.source.tables.ModuleReference;
import p.studio.utilities.structures.ReadOnlyList;
import java.util.ArrayList;
import java.util.List;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.*;
class LifecycleAssemblerServiceTest {
@Test
void assembleMustMaterializeNeutralLifecycleArtifactsInDeclaredOrder() {
final var backend = declarativeBackend();
final var issues = BuildingIssueSink.empty();
final var assembled = new LifecycleAssemblerService().assemble(backend, issues);
assertFalse(issues.hasErrors(), issues.asCollection().toString());
assertSame(backend.getLifecycleDeclaration(), assembled.getLifecycleDeclaration());
assertEquals("__prometeu.lifecycle.frame_wrapper$m1", assembled.getEntryPointCallableName());
assertEquals(1, assembled.getEntryPointModuleId().getIndex());
assertTrue(assembled.getSyntheticFunctions().stream().anyMatch(fn ->
fn.kind() == IRSyntheticCallableKind.MODULE_INIT
&& "__prometeu.lifecycle.module_init$m0".equals(fn.callableName())));
assertTrue(assembled.getSyntheticFunctions().stream().anyMatch(fn ->
fn.kind() == IRSyntheticCallableKind.PROJECT_INIT
&& "__prometeu.lifecycle.project_init$m1".equals(fn.callableName())));
assertTrue(assembled.getSyntheticFunctions().stream().anyMatch(fn ->
fn.kind() == IRSyntheticCallableKind.PUBLISHED_FRAME_WRAPPER
&& "__prometeu.lifecycle.frame_wrapper$m1".equals(fn.callableName())));
assertTrue(assembled.getGlobals().stream().anyMatch(global ->
global.hiddenKind() == IRHiddenGlobalKind.BOOT_GUARD
&& global.isHidden()
&& "__prometeu.lifecycle.boot_guard$m1".equals(global.name())));
assertTrue(assembled.getSyntheticFunctions().stream().noneMatch(fn ->
fn.callableName().contains("__pbs.")));
assertTrue(assembled.getGlobals().stream().noneMatch(global ->
global.name().contains("__pbs.")));
final var wrapper = executable(assembled, "__prometeu.lifecycle.frame_wrapper$m1");
final var calledNames = wrapper.instructions().stream()
.filter(instruction -> instruction.kind() == IRBackendExecutableFunction.InstructionKind.CALL_FUNC)
.map(IRBackendExecutableFunction.Instruction::calleeCallableName)
.toList();
assertEquals(
List.of(
"__prometeu.lifecycle.module_init$m0",
"__prometeu.lifecycle.module_init$m1",
"__prometeu.lifecycle.project_init$m1",
"frame"),
calledNames);
assertEquals(
1,
calledNames.stream().filter("__prometeu.lifecycle.project_init$m1"::equals).count());
assertEquals(IRBackendExecutableFunction.InstructionKind.RET, wrapper.instructions().getLast().kind());
final var moduleInit = executable(assembled, "__prometeu.lifecycle.module_init$m0");
assertEquals(
List.of("file_init_dep"),
moduleInit.instructions().stream()
.filter(instruction -> instruction.kind() == IRBackendExecutableFunction.InstructionKind.CALL_FUNC)
.map(IRBackendExecutableFunction.Instruction::calleeCallableName)
.toList());
final var hostModuleInit = executable(assembled, "__prometeu.lifecycle.module_init$m1");
assertEquals(
List.of("file_init_host"),
hostModuleInit.instructions().stream()
.filter(instruction -> instruction.kind() == IRBackendExecutableFunction.InstructionKind.CALL_FUNC)
.map(IRBackendExecutableFunction.Instruction::calleeCallableName)
.toList());
}
@Test
void assembleMustInvokeProjectInitExactlyOnceAndExcludeItFromFileInit() {
final var assembled = new LifecycleAssemblerService().assemble(declarativeBackend(), BuildingIssueSink.empty());
final var wrapperCalls = executable(assembled, assembled.getEntryPointCallableName()).instructions().stream()
.filter(instruction -> instruction.kind() == IRBackendExecutableFunction.InstructionKind.CALL_FUNC)
.map(IRBackendExecutableFunction.Instruction::calleeCallableName)
.toList();
final var fileInitCalls = executable(assembled, "file_init_host").instructions().stream()
.filter(instruction -> instruction.kind() == IRBackendExecutableFunction.InstructionKind.CALL_FUNC)
.map(IRBackendExecutableFunction.Instruction::calleeCallableName)
.toList();
assertEquals(1, wrapperCalls.stream().filter("__prometeu.lifecycle.project_init$m1"::equals).count());
assertTrue(wrapperCalls.contains("frame"));
assertTrue(fileInitCalls.stream().noneMatch("boot"::equals));
final var projectInitWrapper = executable(assembled, "__prometeu.lifecycle.project_init$m1");
assertEquals(
List.of("boot"),
projectInitWrapper.instructions().stream()
.filter(instruction -> instruction.kind() == IRBackendExecutableFunction.InstructionKind.CALL_FUNC)
.map(IRBackendExecutableFunction.Instruction::calleeCallableName)
.toList());
}
@Test
void assembleMustNoOpWhenLifecycleDeclarationIsAbsent() {
final var backend = IRBackend.builder()
.entryPointCallableName("main")
.entryPointModuleId(new ModuleId(0))
.executableFunctions(ReadOnlyList.from(fn("main", 0, 0, 0, span(1))))
.callableSignatures(ReadOnlyList.from(new CallableSignatureRef(new ModuleId(0), "main", 0, "() -> unit")))
.build();
final var issues = BuildingIssueSink.empty();
final var assembled = new LifecycleAssemblerService().assemble(backend, issues);
assertFalse(issues.hasErrors());
assertSame(backend, assembled);
assertNull(assembled.getLifecycleDeclaration());
assertTrue(assembled.getSyntheticFunctions().isEmpty());
}
@Test
void assembleMustRejectMissingFrameRootWithoutRepair() {
final var backend = backendWithDeclaration(new IRLifecycleDeclaration(
ReadOnlyList.from(new IRLifecycleModuleDeclaration(new ModuleId(0), ReadOnlyList.empty())),
null,
null));
final var issues = BuildingIssueSink.empty();
final var assembled = new LifecycleAssemblerService().assemble(backend, issues);
assertTrue(issues.hasErrors());
assertSame(backend, assembled);
assertEquals(LifecycleAssemblerService.INVALID_DECLARATION, firstCode(issues));
assertTrue(firstMessage(issues).contains("frame root"));
}
@Test
void assembleMustRejectUnresolvedCallableIds() {
final var backend = backendWithDeclaration(new IRLifecycleDeclaration(
ReadOnlyList.empty(),
null,
reference(0, 1, 99, IRLifecycleCallableRole.FRAME_ROOT, span(3))));
final var issues = BuildingIssueSink.empty();
new LifecycleAssemblerService().assemble(backend, issues);
assertTrue(issues.hasErrors());
assertTrue(firstMessage(issues).contains("unresolved lifecycle callable id"));
}
@Test
void assembleMustRejectIncompatibleRoles() {
final var backend = backendWithDeclaration(new IRLifecycleDeclaration(
ReadOnlyList.empty(),
null,
reference(0, 1, 2, IRLifecycleCallableRole.PROJECT_INIT, span(3))));
final var issues = BuildingIssueSink.empty();
new LifecycleAssemblerService().assemble(backend, issues);
assertTrue(issues.hasErrors());
assertTrue(firstMessage(issues).contains("incompatible lifecycle role"));
}
@Test
void assembleMustRejectMissingOrigin() {
final var backend = backendWithDeclaration(new IRLifecycleDeclaration(
ReadOnlyList.empty(),
null,
reference(0, 1, 2, IRLifecycleCallableRole.FRAME_ROOT, Span.none())));
final var issues = BuildingIssueSink.empty();
new LifecycleAssemblerService().assemble(backend, issues);
assertTrue(issues.hasErrors());
assertTrue(firstMessage(issues).contains("origin is missing"));
}
@Test
void assembleMustRejectPreMaterializedDerivedArtifacts() {
final var backend = declarativeBackend(new IRSyntheticFunction(
new ModuleId(1),
"already_wrapped",
IRSyntheticCallableKind.PUBLISHED_FRAME_WRAPPER,
new IRSyntheticOrigin(new FileId(2), "frame", span(2))));
final var issues = BuildingIssueSink.empty();
final var assembled = new LifecycleAssemblerService().assemble(backend, issues);
assertTrue(issues.hasErrors());
assertSame(backend, assembled);
assertTrue(firstMessage(issues).contains("pre-materialized lifecycle artifact"));
}
@Test
void assembleMustRejectDuplicateModuleContributions() {
final var module = new IRLifecycleModuleDeclaration(
new ModuleId(0),
ReadOnlyList.from(reference(0, 1, 0, IRLifecycleCallableRole.FILE_INIT_FRAGMENT, span(1))));
final var backend = backendWithDeclaration(new IRLifecycleDeclaration(
ReadOnlyList.from(module, module),
reference(1, 2, 3, IRLifecycleCallableRole.PROJECT_INIT, span(2)),
reference(1, 2, 2, IRLifecycleCallableRole.FRAME_ROOT, span(2))));
final var issues = BuildingIssueSink.empty();
new LifecycleAssemblerService().assemble(backend, issues);
assertTrue(issues.hasErrors());
assertTrue(firstMessage(issues).contains("duplicate lifecycle module"));
}
@Test
void assembleMustRejectNullBackend() {
final var issues = BuildingIssueSink.empty();
final var assembled = new LifecycleAssemblerService().assemble(null, issues);
assertNull(assembled);
assertTrue(issues.hasErrors());
assertEquals(LifecycleAssemblerService.PHASE, issues.asCollection().iterator().next().getPhase());
}
private static IRBackend declarativeBackend(final IRSyntheticFunction... extraSynthetics) {
return declarativeBackend(null, extraSynthetics);
}
private static IRBackend backendWithDeclaration(final IRLifecycleDeclaration declaration) {
return declarativeBackend(declaration);
}
private static IRBackend declarativeBackend(
final IRLifecycleDeclaration declarationOverride,
final IRSyntheticFunction... extraSynthetics) {
final var depSpan = span(1);
final var hostSpan = span(2);
final var synthetics = new ArrayList<IRSyntheticFunction>();
synthetics.add(new IRSyntheticFunction(
new ModuleId(0),
"file_init_dep",
IRSyntheticCallableKind.FILE_INIT_FRAGMENT,
new IRSyntheticOrigin(new FileId(1), "warmup", depSpan)));
synthetics.add(new IRSyntheticFunction(
new ModuleId(1),
"file_init_host",
IRSyntheticCallableKind.FILE_INIT_FRAGMENT,
new IRSyntheticOrigin(new FileId(2), "boot", hostSpan)));
synthetics.addAll(Arrays.asList(extraSynthetics));
final var declaration = declarationOverride != null
? declarationOverride
: new IRLifecycleDeclaration(
ReadOnlyList.from(
new IRLifecycleModuleDeclaration(
new ModuleId(0),
ReadOnlyList.from(reference(0, 1, 0, IRLifecycleCallableRole.FILE_INIT_FRAGMENT, depSpan))),
new IRLifecycleModuleDeclaration(
new ModuleId(1),
ReadOnlyList.from(reference(1, 2, 1, IRLifecycleCallableRole.FILE_INIT_FRAGMENT, hostSpan)))),
reference(1, 2, 3, IRLifecycleCallableRole.PROJECT_INIT, hostSpan),
reference(1, 2, 2, IRLifecycleCallableRole.FRAME_ROOT, hostSpan));
return IRBackend.builder()
.entryPointCallableName("frame")
.entryPointModuleId(new ModuleId(1))
.modulePool(ReadOnlyList.from(
new ModuleReference("app", ReadOnlyList.from("zzz")),
new ModuleReference("app", ReadOnlyList.from("aaa"))))
.callableSignatures(ReadOnlyList.from(
new CallableSignatureRef(new ModuleId(0), "file_init_dep", 0, "() -> unit"),
new CallableSignatureRef(new ModuleId(1), "file_init_host", 0, "() -> unit"),
new CallableSignatureRef(new ModuleId(1), "frame", 0, "() -> unit"),
new CallableSignatureRef(new ModuleId(1), "boot", 0, "() -> unit")))
.executableFunctions(ReadOnlyList.from(
fn("file_init_dep", 1, 0, 0, depSpan),
fn("file_init_host", 2, 1, 1, hostSpan),
fn("frame", 2, 1, 2, hostSpan),
fn("boot", 2, 1, 3, hostSpan)))
.syntheticFunctions(ReadOnlyList.wrap(synthetics))
.globals(ReadOnlyList.from(new IRGlobal(
new FileId(1),
new ModuleId(0),
"SCORE",
"int",
0,
IRGlobalVisibility.MODULE,
false,
IRHiddenGlobalKind.NONE,
new IRGlobalOrigin(new FileId(1), new ModuleId(0), "SCORE", depSpan, "SCORE"),
depSpan)))
.lifecycleDeclaration(declaration)
.build();
}
private static IRLifecycleCallableReference reference(
final int moduleId,
final int fileId,
final int callableId,
final IRLifecycleCallableRole role,
final Span origin) {
return new IRLifecycleCallableReference(
new ModuleId(moduleId),
new FileId(fileId),
new CallableId(callableId),
role,
origin);
}
private static IRBackendExecutableFunction fn(
final String name,
final int fileId,
final int moduleId,
final int callableId,
final Span span) {
return new IRBackendExecutableFunction(
new FileId(fileId),
new ModuleId(moduleId),
name,
new CallableId(callableId),
(int) span.getStart(),
(int) span.getEnd(),
0,
0,
0,
1,
ReadOnlyList.from(new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.RET,
"",
null,
null,
span)),
span);
}
private static IRBackendExecutableFunction executable(final IRBackend backend, final String name) {
return backend.getExecutableFunctions().stream()
.filter(function -> name.equals(function.callableName()))
.findFirst()
.orElseThrow(() -> new AssertionError("missing executable " + name));
}
private static Span span(final int fileId) {
return new Span(new FileId(fileId), 4, 16);
}
private static String firstCode(final BuildingIssueSink issues) {
return issues.asCollection().iterator().next().getCode();
}
private static String firstMessage(final BuildingIssueSink issues) {
return issues.asCollection().iterator().next().getMessage();
}
}

View File

@ -42,6 +42,9 @@ class BackendConformanceMatrixSpecTest {
"G20-4.2.1",
"G20-4.2.2",
"G20-4.2.3",
"G20-4.3.1",
"G20-4.3.2",
"G20-4.3.3",
"G20-6.2",
"G20-6.3",
"G20-6.4",
@ -81,7 +84,11 @@ class BackendConformanceMatrixSpecTest {
"PBS13-12.3.2",
"PBS13-12.4.1",
"PBS13-12.4.2",
"PBS13-12.5");
"PBS13-12.5",
"PBS13-12.7",
"PBS13-12.8",
"PBS13-12.9",
"G23-5.2");
@Test
void matrixMustExistAndContainRequiredRequirementRows() throws IOException {

View File

@ -6,30 +6,53 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
class CommonBackendArchitectureTest {
private static final String BACKEND_SOURCE_ROOT =
"prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/backend";
private static final String COMPILER_SOURCE_ROOT =
"prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler";
private static final List<String> GUARDED_RELATIVE_ROOTS = List.of(
"backend",
"lifecycle",
"workspaces/stages/AssembleLifecyclePipelineStage.java");
private static final String FORBIDDEN_PBS_PACKAGE = "p.studio.compiler.pbs";
private static final String FORBIDDEN_PBS_PREFIX = "__pbs.";
@Test
void commonBackendMustNotImportPbsFrontendPackages() throws IOException {
final var backendRoot = locateRepoRoot().resolve(BACKEND_SOURCE_ROOT);
final var violations = new ArrayList<String>();
try (final var paths = Files.walk(backendRoot)) {
paths.filter(path -> path.toString().endsWith(".java"))
.forEach(path -> assertNoPbsImport(path, violations));
}
walkGuardedSources(path -> assertNoPbsImport(path, violations));
assertTrue(
violations.isEmpty(),
"common backend must not import PBS frontend packages: " + violations);
}
@Test
void commonLifecycleAndBackendMustNotDiscoverRolesFromPbsNamePrefixes() throws IOException {
final var violations = new ArrayList<String>();
walkGuardedSources(path -> assertNoPbsPrefixMatching(path, violations));
assertTrue(
violations.isEmpty(),
"common lifecycle/backend code must not inspect __pbs. prefixes: " + violations);
}
private static void walkGuardedSources(final java.util.function.Consumer<Path> consumer) throws IOException {
final var compilerRoot = locateRepoRoot().resolve(COMPILER_SOURCE_ROOT);
for (final var relativeRoot : GUARDED_RELATIVE_ROOTS) {
final var root = compilerRoot.resolve(relativeRoot);
if (Files.isRegularFile(root)) {
consumer.accept(root);
continue;
}
try (final var paths = Files.walk(root)) {
paths.filter(path -> path.toString().endsWith(".java")).forEach(consumer);
}
}
}
private static void assertNoPbsImport(
final Path path,
final ArrayList<String> violations) {
@ -44,6 +67,19 @@ class CommonBackendArchitectureTest {
}
}
private static void assertNoPbsPrefixMatching(
final Path path,
final ArrayList<String> violations) {
try {
final var content = Files.readString(path);
if (content.contains(FORBIDDEN_PBS_PREFIX)) {
violations.add(locateRepoRoot().relativize(path).toString());
}
} catch (IOException e) {
throw new IllegalStateException("failed to read source file: " + path, e);
}
}
private static Path locateRepoRoot() {
var cursor = Path.of(System.getProperty("user.dir")).toAbsolutePath().normalize();
while (cursor != null) {

View File

@ -0,0 +1,156 @@
package p.studio.compiler.workspaces;
import org.junit.jupiter.api.Test;
import p.studio.AppMode;
import p.studio.compiler.lifecycle.LifecycleAssemblerService;
import p.studio.compiler.messages.BuilderPipelineConfig;
import p.studio.compiler.models.BuilderPipelineContext;
import p.studio.compiler.models.IRBackend;
import p.studio.compiler.models.ResolvedWorkspace;
import p.studio.compiler.source.identifiers.ProjectId;
import p.studio.compiler.models.IRBackendExecutableFunction;
import p.studio.compiler.models.IRLifecycleCallableReference;
import p.studio.compiler.models.IRLifecycleCallableRole;
import p.studio.compiler.models.IRLifecycleDeclaration;
import p.studio.compiler.models.IRLifecycleModuleDeclaration;
import p.studio.compiler.models.IRSyntheticCallableKind;
import p.studio.compiler.source.Span;
import p.studio.compiler.source.identifiers.CallableId;
import p.studio.compiler.source.identifiers.FileId;
import p.studio.compiler.source.identifiers.ModuleId;
import p.studio.compiler.source.tables.CallableSignatureRef;
import p.studio.compiler.source.tables.ModuleReference;
import p.studio.compiler.workspaces.stages.AssembleLifecyclePipelineStage;
import p.studio.compiler.workspaces.stages.EmitBytecodePipelineStage;
import p.studio.compiler.workspaces.stages.FrontendPhasePipelineStage;
import p.studio.compiler.workspaces.stages.LinkBytecodePipelineStage;
import p.studio.compiler.workspaces.stages.LoadSourcesPipelineStage;
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 p.studio.utilities.logs.LogAggregator;
import p.studio.utilities.structures.ReadOnlyList;
import java.lang.reflect.Field;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class BuilderPipelineServiceOrderTest {
@Test
void canonicalOrderMustContainOptimizeBetweenLowerAndEmit() {
final var analyses = stageNames("analyses");
final var compile = stageNames("compile");
final var build = stageNames("build");
assertEquals(
List.of(
ResolveDepsPipelineStage.class.getSimpleName(),
LoadSourcesPipelineStage.class.getSimpleName(),
FrontendPhasePipelineStage.class.getSimpleName(),
AssembleLifecyclePipelineStage.class.getSimpleName()),
analyses);
assertEquals(
List.of(
LowerToIRVMPipelineStage.class.getSimpleName(),
OptimizeIRVMPipelineStage.class.getSimpleName(),
EmitBytecodePipelineStage.class.getSimpleName(),
LinkBytecodePipelineStage.class.getSimpleName(),
VerifyBytecodePipelineStage.class.getSimpleName()),
compile);
assertEquals(List.of(WriteBytecodeArtifactPipelineStage.class.getSimpleName()), build);
assertEquals(
FrontendPhasePipelineStage.class.getSimpleName(),
analyses.get(analyses.size() - 2));
assertEquals(
AssembleLifecyclePipelineStage.class.getSimpleName(),
analyses.getLast());
}
@Test
void analyzeMustTerminateAfterLifecycleAssemblyWithoutBackendArtifacts() {
final var ctx = BuilderPipelineContext.fromConfig(new BuilderPipelineConfig(false, "."));
ctx.irBackend = validDeclaration();
ctx.resolvedWorkspace = new ResolvedWorkspace(
new ProjectId(0),
null,
1,
AppMode.Game,
null,
null);
final var pipeline = new BuilderPipelineService(
List.of(new AssembleLifecyclePipelineStage()),
List.of(new LowerToIRVMPipelineStage()),
List.of());
final var snapshot = pipeline.analyze(ctx, LogAggregator.empty());
assertNotNull(snapshot.irBackend());
assertTrue(snapshot.irBackend().getSyntheticFunctions().stream().anyMatch(fn ->
fn.kind() == IRSyntheticCallableKind.PUBLISHED_FRAME_WRAPPER));
assertEquals(
"__prometeu.lifecycle.frame_wrapper$m0",
snapshot.irBackend().getEntryPointCallableName());
assertSame(ctx.irBackend.getLifecycleDeclaration(), snapshot.irBackend().getLifecycleDeclaration());
assertNull(ctx.irvm);
assertNull(ctx.optimizedIrvm);
assertNull(ctx.bytecodeModule);
assertNull(ctx.bytecodeBytes);
assertNull(ctx.bytecodeArtifactPath);
assertTrue(snapshot.diagnostics().stream().noneMatch(issue ->
LifecycleAssemblerService.INVALID_DECLARATION.equals(issue.getCode())));
}
@SuppressWarnings("unchecked")
private static List<String> stageNames(final String fieldName) {
try {
final Field field = BuilderPipelineService.class.getDeclaredField(fieldName);
field.setAccessible(true);
final var stages = (List<PipelineStage>) field.get(BuilderPipelineService.INSTANCE);
return stages.stream().map(stage -> stage.getClass().getSimpleName()).toList();
} catch (ReflectiveOperationException e) {
throw new AssertionError("failed to inspect pipeline field " + fieldName, e);
}
}
private static IRBackend validDeclaration() {
final var span = new Span(new FileId(1), 1, 8);
return IRBackend.builder()
.entryPointCallableName("frame")
.entryPointModuleId(new ModuleId(0))
.modulePool(ReadOnlyList.from(new ModuleReference("app", ReadOnlyList.from("main"))))
.callableSignatures(ReadOnlyList.from(
new CallableSignatureRef(new ModuleId(0), "frame", 0, "() -> unit")))
.executableFunctions(ReadOnlyList.from(new IRBackendExecutableFunction(
new FileId(1),
new ModuleId(0),
"frame",
new CallableId(0),
1,
8,
0,
0,
0,
1,
ReadOnlyList.from(new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.RET,
"",
null,
null,
span)),
span)))
.lifecycleDeclaration(new IRLifecycleDeclaration(
ReadOnlyList.from(new IRLifecycleModuleDeclaration(new ModuleId(0), ReadOnlyList.empty())),
null,
new IRLifecycleCallableReference(
new ModuleId(0),
new FileId(1),
new CallableId(0),
IRLifecycleCallableRole.FRAME_ROOT,
span)))
.build();
}
}

View File

@ -0,0 +1,131 @@
package p.studio.compiler.workspaces.stages;
import org.junit.jupiter.api.Test;
import p.studio.compiler.lifecycle.LifecycleAssemblerService;
import p.studio.compiler.messages.BuilderPipelineConfig;
import p.studio.compiler.messages.BuildingIssueSink;
import p.studio.compiler.models.BuilderPipelineContext;
import p.studio.compiler.models.IRBackend;
import p.studio.compiler.models.IRBackendExecutableFunction;
import p.studio.compiler.models.IRLifecycleCallableReference;
import p.studio.compiler.models.IRLifecycleCallableRole;
import p.studio.compiler.models.IRLifecycleDeclaration;
import p.studio.compiler.models.IRLifecycleModuleDeclaration;
import p.studio.compiler.models.IRSyntheticCallableKind;
import p.studio.compiler.source.Span;
import p.studio.compiler.source.identifiers.CallableId;
import p.studio.compiler.source.identifiers.FileId;
import p.studio.compiler.source.identifiers.ModuleId;
import p.studio.compiler.source.tables.CallableSignatureRef;
import p.studio.compiler.source.tables.ModuleReference;
import p.studio.utilities.logs.LogAggregator;
import p.studio.utilities.structures.ReadOnlyList;
import static org.junit.jupiter.api.Assertions.*;
class AssembleLifecyclePipelineStageTest {
@Test
void runMustAssignAssembledBackendOnSuccess() {
final var ctx = BuilderPipelineContext.fromConfig(new BuilderPipelineConfig(false, "."));
ctx.irBackend = validDeclaration();
final var original = ctx.irBackend;
final var issues = new AssembleLifecyclePipelineStage().run(ctx, LogAggregator.empty());
assertFalse(issues.hasErrors(), issues.asCollection().toString());
assertNotSame(original, ctx.irBackend);
assertTrue(ctx.irBackend.getSyntheticFunctions().stream().anyMatch(fn ->
fn.kind() == IRSyntheticCallableKind.PUBLISHED_FRAME_WRAPPER));
assertEquals(
original.getLifecycleDeclaration(),
ctx.irBackend.getLifecycleDeclaration());
assertNull(ctx.irvm);
assertNull(ctx.bytecodeModule);
assertNull(ctx.bytecodeBytes);
}
@Test
void runMustKeepOriginalBackendAndReportStructuralErrors() {
final var ctx = BuilderPipelineContext.fromConfig(new BuilderPipelineConfig(false, "."));
ctx.irBackend = IRBackend.builder()
.entryPointCallableName("frame")
.entryPointModuleId(new ModuleId(0))
.executableFunctions(ReadOnlyList.from(fn("frame", 0, 0)))
.callableSignatures(ReadOnlyList.from(new CallableSignatureRef(new ModuleId(0), "frame", 0, "() -> unit")))
.lifecycleDeclaration(new IRLifecycleDeclaration(ReadOnlyList.empty(), null, null))
.build();
final var original = ctx.irBackend;
final var issues = new AssembleLifecyclePipelineStage().run(ctx, LogAggregator.empty());
assertTrue(issues.hasErrors());
assertSame(original, ctx.irBackend);
assertEquals(LifecycleAssemblerService.INVALID_DECLARATION, issues.asCollection().iterator().next().getCode());
assertEquals(LifecycleAssemblerService.PHASE, issues.asCollection().iterator().next().getPhase());
assertNull(ctx.irvm);
}
@Test
void runMustNoOpForDeliberatelyNonExecutableHandoff() {
final var ctx = BuilderPipelineContext.fromConfig(new BuilderPipelineConfig(false, "."));
ctx.irBackend = IRBackend.builder()
.entryPointCallableName("main")
.entryPointModuleId(new ModuleId(0))
.executableFunctions(ReadOnlyList.from(fn("main", 0, 0)))
.build();
final var original = ctx.irBackend;
final var issues = new AssembleLifecyclePipelineStage().run(ctx, LogAggregator.empty());
assertFalse(issues.hasErrors());
assertSame(original, ctx.irBackend);
assertTrue(ctx.irBackend.getSyntheticFunctions().isEmpty());
}
private static IRBackend validDeclaration() {
final var span = new Span(new FileId(1), 1, 8);
return IRBackend.builder()
.entryPointCallableName("frame")
.entryPointModuleId(new ModuleId(0))
.modulePool(ReadOnlyList.from(new ModuleReference("app", ReadOnlyList.from("main"))))
.callableSignatures(ReadOnlyList.from(
new CallableSignatureRef(new ModuleId(0), "frame", 0, "() -> unit")))
.executableFunctions(ReadOnlyList.from(fn("frame", 0, 0)))
.lifecycleDeclaration(new IRLifecycleDeclaration(
ReadOnlyList.from(new IRLifecycleModuleDeclaration(new ModuleId(0), ReadOnlyList.empty())),
null,
new IRLifecycleCallableReference(
new ModuleId(0),
new FileId(1),
new CallableId(0),
IRLifecycleCallableRole.FRAME_ROOT,
span)))
.build();
}
private static IRBackendExecutableFunction fn(
final String name,
final int moduleId,
final int callableId) {
final var span = new Span(new FileId(1), 1, 8);
return new IRBackendExecutableFunction(
new FileId(1),
new ModuleId(moduleId),
name,
new CallableId(callableId),
1,
8,
0,
0,
0,
1,
ReadOnlyList.from(new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.RET,
"",
null,
null,
span)),
span);
}
}

View File

@ -34,6 +34,7 @@ public class IRBackend implements IRBackendReader {
private final ReadOnlyList<IntrinsicReference> intrinsicPool = ReadOnlyList.empty();
@Builder.Default
private final IRReservedMetadata reservedMetadata = IRReservedMetadata.empty();
private final IRLifecycleDeclaration lifecycleDeclaration;
public static IRBackendAggregator aggregator() {
return new IRBackendAggregator();
@ -53,6 +54,8 @@ public class IRBackend implements IRBackendReader {
private final ArrayList<IRReservedMetadata.BuiltinTypeSurface> builtinTypeSurfaces = new ArrayList<>();
private final ArrayList<IRReservedMetadata.BuiltinConstSurface> builtinConstSurfaces = new ArrayList<>();
private final LinkedHashSet<String> requiredCapabilities = new LinkedHashSet<>();
private final ArrayList<IRLifecycleFileDeclaration> lifecycleFiles = new ArrayList<>();
private ReadOnlyList<ModuleId> lifecycleModuleOrder = ReadOnlyList.empty();
public void merge(final IRBackendFile backendFile) {
if (backendFile == null) {
@ -90,6 +93,12 @@ public class IRBackend implements IRBackendReader {
for (final var function : backendFile.executableFunctions()) {
executableFunctions.add(remapExecutableFunction(function, moduleRemap, callableRemap, intrinsicRemap));
}
if (backendFile.lifecycleDeclaration() != null && !backendFile.lifecycleDeclaration().isEmpty()) {
lifecycleFiles.add(remapLifecycleFileDeclaration(
backendFile.lifecycleDeclaration(),
moduleRemap,
callableRemap));
}
final var metadata = backendFile.reservedMetadata();
hostMethodBindings.addAll(metadata.hostMethodBindings().asList());
builtinTypeSurfaces.addAll(metadata.builtinTypeSurfaces().asList());
@ -111,6 +120,11 @@ public class IRBackend implements IRBackendReader {
return this;
}
public IRBackendAggregator lifecycleModuleOrder(final ReadOnlyList<ModuleId> moduleOrder) {
lifecycleModuleOrder = moduleOrder == null ? ReadOnlyList.empty() : moduleOrder;
return this;
}
private ModuleId[] reindexModules(final ReadOnlyList<ModuleReference> localModulePool) {
if (localModulePool == null || localModulePool.isEmpty()) {
return new ModuleId[0];
@ -203,6 +217,37 @@ public class IRBackend implements IRBackendReader {
function.span());
}
private IRLifecycleFileDeclaration remapLifecycleFileDeclaration(
final IRLifecycleFileDeclaration declaration,
final ModuleId[] moduleRemap,
final CallableId[] callableRemap) {
final var remappedModuleId = remapModuleId(
declaration.moduleId(),
moduleRemap,
"lifecycle file");
return new IRLifecycleFileDeclaration(
remappedModuleId,
declaration.fileId(),
remapLifecycleCallable(declaration.fileInitFragment(), moduleRemap, callableRemap),
remapLifecycleCallable(declaration.projectInit(), moduleRemap, callableRemap),
remapLifecycleCallable(declaration.frameRoot(), moduleRemap, callableRemap));
}
private IRLifecycleCallableReference remapLifecycleCallable(
final IRLifecycleCallableReference reference,
final ModuleId[] moduleRemap,
final CallableId[] callableRemap) {
if (reference == null) {
return null;
}
return new IRLifecycleCallableReference(
remapModuleId(reference.moduleId(), moduleRemap, "lifecycle callable"),
reference.fileId(),
remapCallableId(reference.callableId(), callableRemap, "lifecycle callable"),
reference.role(),
reference.origin());
}
private ModuleId remapModuleId(
final ModuleId localModuleId,
final ModuleId[] moduleRemap,
@ -273,6 +318,52 @@ public class IRBackend implements IRBackendReader {
return ReadOnlyList.wrap(pool);
}
private IRLifecycleDeclaration emitLifecycleDeclaration() {
if (lifecycleFiles.isEmpty()) {
return null;
}
if (lifecycleModuleOrder.isEmpty()) {
throw new IllegalStateException("lifecycle module order is required for executable handoff");
}
final var modules = new ArrayList<IRLifecycleModuleDeclaration>();
IRLifecycleCallableReference projectInit = null;
IRLifecycleCallableReference frameRoot = null;
for (final var moduleId : lifecycleModuleOrder) {
final var fragments = new ArrayList<IRLifecycleCallableReference>();
var hasLifecycleContribution = false;
for (final var lifecycleFile : lifecycleFiles) {
if (!sameModuleId(moduleId, lifecycleFile.moduleId())) {
continue;
}
hasLifecycleContribution = true;
if (lifecycleFile.fileInitFragment() != null) {
fragments.add(lifecycleFile.fileInitFragment());
}
if (projectInit == null && lifecycleFile.projectInit() != null) {
projectInit = lifecycleFile.projectInit();
}
if (frameRoot == null && lifecycleFile.frameRoot() != null) {
frameRoot = lifecycleFile.frameRoot();
}
}
if (hasLifecycleContribution) {
modules.add(new IRLifecycleModuleDeclaration(moduleId, ReadOnlyList.wrap(fragments)));
}
}
return new IRLifecycleDeclaration(ReadOnlyList.wrap(modules), projectInit, frameRoot);
}
private boolean sameModuleId(final ModuleId left, final ModuleId right) {
if (left == null || right == null) {
return left == right;
}
if (left.isNone() || right.isNone()) {
return left.isNone() && right.isNone();
}
return left.getIndex() == right.getIndex();
}
public IRBackend emit() {
return IRBackend
.builder()
@ -290,6 +381,7 @@ public class IRBackend implements IRBackendReader {
ReadOnlyList.wrap(builtinTypeSurfaces),
ReadOnlyList.wrap(builtinConstSurfaces),
ReadOnlyList.wrap(requiredCapabilities.stream().toList())))
.lifecycleDeclaration(emitLifecycleDeclaration())
.build();
}
@ -321,6 +413,7 @@ public class IRBackend implements IRBackendReader {
.append(", builtinTypes=").append(reservedMetadata.builtinTypeSurfaces().size())
.append(", builtinConsts=").append(reservedMetadata.builtinConstSurfaces().size())
.append(", requiredCapabilities=").append(reservedMetadata.requiredCapabilities().size())
.append(", lifecycle=").append(lifecycleDeclaration == null ? "none" : "declared")
.append('}');
if (functions.isEmpty()) {

View File

@ -17,7 +17,8 @@ public record IRBackendFile(
IRReservedMetadata reservedMetadata,
ReadOnlyList<ModuleReference> modulePool,
ReadOnlyList<CallableSignatureRef> callableSignatures,
ReadOnlyList<IntrinsicReference> intrinsicPool) {
ReadOnlyList<IntrinsicReference> intrinsicPool,
IRLifecycleFileDeclaration lifecycleDeclaration) {
public IRBackendFile {
fileId = Objects.requireNonNull(fileId, "fileId");
functions = functions == null ? ReadOnlyList.empty() : functions;
@ -30,6 +31,29 @@ public record IRBackendFile(
intrinsicPool = intrinsicPool == null ? ReadOnlyList.empty() : intrinsicPool;
}
public IRBackendFile(
final FileId fileId,
final ReadOnlyList<IRFunction> functions,
final ReadOnlyList<IRSyntheticFunction> syntheticFunctions,
final ReadOnlyList<IRGlobal> globals,
final ReadOnlyList<IRBackendExecutableFunction> executableFunctions,
final IRReservedMetadata reservedMetadata,
final ReadOnlyList<ModuleReference> modulePool,
final ReadOnlyList<CallableSignatureRef> callableSignatures,
final ReadOnlyList<IntrinsicReference> intrinsicPool) {
this(
fileId,
functions,
syntheticFunctions,
globals,
executableFunctions,
reservedMetadata,
modulePool,
callableSignatures,
intrinsicPool,
null);
}
public IRBackendFile(
final FileId fileId,
final ReadOnlyList<IRFunction> functions) {
@ -42,7 +66,8 @@ public record IRBackendFile(
IRReservedMetadata.empty(),
ReadOnlyList.empty(),
ReadOnlyList.empty(),
ReadOnlyList.empty());
ReadOnlyList.empty(),
null);
}
public static IRBackendFile empty(final FileId fileId) {
@ -55,7 +80,8 @@ public record IRBackendFile(
IRReservedMetadata.empty(),
ReadOnlyList.empty(),
ReadOnlyList.empty(),
ReadOnlyList.empty());
ReadOnlyList.empty(),
null);
}
public IRBackendFile(
@ -71,7 +97,8 @@ public record IRBackendFile(
reservedMetadata,
ReadOnlyList.empty(),
ReadOnlyList.empty(),
ReadOnlyList.empty());
ReadOnlyList.empty(),
null);
}
public IRBackendFile(
@ -90,7 +117,8 @@ public record IRBackendFile(
reservedMetadata,
ReadOnlyList.empty(),
callableSignatures,
intrinsicPool);
intrinsicPool,
null);
}
public IRBackendFile(
@ -110,6 +138,7 @@ public record IRBackendFile(
reservedMetadata,
modulePool,
callableSignatures,
intrinsicPool);
intrinsicPool,
null);
}
}

View File

@ -17,4 +17,5 @@ public interface IRBackendReader {
ReadOnlyList<CallableSignatureRef> getCallableSignatures();
ReadOnlyList<IntrinsicReference> getIntrinsicPool();
IRReservedMetadata getReservedMetadata();
IRLifecycleDeclaration getLifecycleDeclaration();
}

View File

@ -0,0 +1,23 @@
package p.studio.compiler.models;
import p.studio.compiler.source.Span;
import p.studio.compiler.source.identifiers.CallableId;
import p.studio.compiler.source.identifiers.FileId;
import p.studio.compiler.source.identifiers.ModuleId;
import java.util.Objects;
public record IRLifecycleCallableReference(
ModuleId moduleId,
FileId fileId,
CallableId callableId,
IRLifecycleCallableRole role,
Span origin) {
public IRLifecycleCallableReference {
moduleId = Objects.requireNonNull(moduleId, "moduleId");
fileId = Objects.requireNonNull(fileId, "fileId");
callableId = Objects.requireNonNull(callableId, "callableId");
role = Objects.requireNonNull(role, "role");
origin = origin == null ? Span.none() : origin;
}
}

View File

@ -0,0 +1,7 @@
package p.studio.compiler.models;
public enum IRLifecycleCallableRole {
FILE_INIT_FRAGMENT,
PROJECT_INIT,
FRAME_ROOT
}

View File

@ -0,0 +1,12 @@
package p.studio.compiler.models;
import p.studio.utilities.structures.ReadOnlyList;
public record IRLifecycleDeclaration(
ReadOnlyList<IRLifecycleModuleDeclaration> modules,
IRLifecycleCallableReference projectInit,
IRLifecycleCallableReference frameRoot) {
public IRLifecycleDeclaration {
modules = modules == null ? ReadOnlyList.empty() : modules;
}
}

View File

@ -0,0 +1,22 @@
package p.studio.compiler.models;
import p.studio.compiler.source.identifiers.FileId;
import p.studio.compiler.source.identifiers.ModuleId;
import java.util.Objects;
public record IRLifecycleFileDeclaration(
ModuleId moduleId,
FileId fileId,
IRLifecycleCallableReference fileInitFragment,
IRLifecycleCallableReference projectInit,
IRLifecycleCallableReference frameRoot) {
public IRLifecycleFileDeclaration {
moduleId = Objects.requireNonNull(moduleId, "moduleId");
fileId = Objects.requireNonNull(fileId, "fileId");
}
public boolean isEmpty() {
return fileInitFragment == null && projectInit == null && frameRoot == null;
}
}

View File

@ -0,0 +1,15 @@
package p.studio.compiler.models;
import p.studio.compiler.source.identifiers.ModuleId;
import p.studio.utilities.structures.ReadOnlyList;
import java.util.Objects;
public record IRLifecycleModuleDeclaration(
ModuleId moduleId,
ReadOnlyList<IRLifecycleCallableReference> fileInitFragments) {
public IRLifecycleModuleDeclaration {
moduleId = Objects.requireNonNull(moduleId, "moduleId");
fileInitFragments = fileInitFragments == null ? ReadOnlyList.empty() : fileInitFragments;
}
}

View File

@ -58,6 +58,11 @@ class IRBackendExecutableContractTest {
IRGlobalOrigin.class,
IRGlobalVisibility.class,
IRHiddenGlobalKind.class,
IRLifecycleDeclaration.class,
IRLifecycleModuleDeclaration.class,
IRLifecycleFileDeclaration.class,
IRLifecycleCallableReference.class,
IRLifecycleCallableRole.class,
IRReservedMetadata.class,
IRReservedMetadata.HostMethodBinding.class,
IRReservedMetadata.BuiltinTypeSurface.class,
@ -80,7 +85,12 @@ class IRBackendExecutableContractTest {
IRReservedMetadata.class,
IRSyntheticCallableKind.class,
IRSyntheticFunction.class,
IRSyntheticOrigin.class);
IRSyntheticOrigin.class,
IRLifecycleDeclaration.class,
IRLifecycleModuleDeclaration.class,
IRLifecycleFileDeclaration.class,
IRLifecycleCallableReference.class,
IRLifecycleCallableRole.class);
assertTrue(
PUBLIC_CONTRACT_TYPES.containsAll(expectedTopLevelModelTypes),
@ -137,6 +147,14 @@ class IRBackendExecutableContractTest {
assertEquals(FileId.class, recordComponentType(IRSyntheticOrigin.class, "fileId"));
assertEquals(ModuleId.class, recordComponentType(IRSyntheticFunction.class, "moduleId"));
assertEquals(ModuleId.class, recordComponentType(IRLifecycleModuleDeclaration.class, "moduleId"));
assertEquals(ModuleId.class, recordComponentType(IRLifecycleFileDeclaration.class, "moduleId"));
assertEquals(FileId.class, recordComponentType(IRLifecycleFileDeclaration.class, "fileId"));
assertEquals(ModuleId.class, recordComponentType(IRLifecycleCallableReference.class, "moduleId"));
assertEquals(FileId.class, recordComponentType(IRLifecycleCallableReference.class, "fileId"));
assertEquals(CallableId.class, recordComponentType(IRLifecycleCallableReference.class, "callableId"));
assertEquals(Span.class, recordComponentType(IRLifecycleCallableReference.class, "origin"));
assertEquals(String.class, recordComponentType(ModuleReference.class, "project"));
assertEquals(String.class, recordComponentType(CallableSignatureRef.class, "callableName"));
assertEquals(String.class, recordComponentType(IntrinsicReference.class, "canonicalName"));
@ -640,6 +658,146 @@ class IRBackendExecutableContractTest {
second.getExecutableFunctions().get(1).callableId());
}
@Test
void aggregatorMustRemapTypedLifecycleReferencesAndPreserveDeclaredModuleOrder() {
final var fileASpan = new Span(new FileId(10), 1, 8);
final var fileBSpan = new Span(new FileId(20), 2, 9);
final var fileA = new IRBackendFile(
new FileId(10),
ReadOnlyList.empty(),
ReadOnlyList.from(new IRSyntheticFunction(
new ModuleId(0),
"file_init_zzz",
IRSyntheticCallableKind.FILE_INIT_FRAGMENT,
new IRSyntheticOrigin(new FileId(10), "warmup", fileASpan))),
ReadOnlyList.empty(),
ReadOnlyList.from(executable("file_init_zzz", 10, 0, 0, fileASpan)),
IRReservedMetadata.empty(),
ReadOnlyList.from(new ModuleReference("app", ReadOnlyList.from("zzz"))),
ReadOnlyList.from(new CallableSignatureRef(new ModuleId(0), "file_init_zzz", 0, "() -> unit")),
ReadOnlyList.empty(),
new IRLifecycleFileDeclaration(
new ModuleId(0),
new FileId(10),
new IRLifecycleCallableReference(
new ModuleId(0),
new FileId(10),
new CallableId(0),
IRLifecycleCallableRole.FILE_INIT_FRAGMENT,
fileASpan),
null,
null));
final var fileB = new IRBackendFile(
new FileId(20),
ReadOnlyList.empty(),
ReadOnlyList.empty(),
ReadOnlyList.empty(),
ReadOnlyList.from(
executable("boot", 20, 0, 0, fileBSpan),
executable("frame", 20, 0, 1, fileBSpan)),
IRReservedMetadata.empty(),
ReadOnlyList.from(new ModuleReference("app", ReadOnlyList.from("aaa"))),
ReadOnlyList.from(
new CallableSignatureRef(new ModuleId(0), "boot", 0, "() -> unit"),
new CallableSignatureRef(new ModuleId(0), "frame", 0, "() -> unit")),
ReadOnlyList.empty(),
new IRLifecycleFileDeclaration(
new ModuleId(0),
new FileId(20),
null,
new IRLifecycleCallableReference(
new ModuleId(0),
new FileId(20),
new CallableId(0),
IRLifecycleCallableRole.PROJECT_INIT,
fileBSpan),
new IRLifecycleCallableReference(
new ModuleId(0),
new FileId(20),
new CallableId(1),
IRLifecycleCallableRole.FRAME_ROOT,
fileBSpan)));
final var aggregator = IRBackend.aggregator();
aggregator.merge(fileA);
aggregator.merge(fileB);
aggregator.lifecycleModuleOrder(ReadOnlyList.from(new ModuleId(1), new ModuleId(0)));
final var backend = aggregator.emit();
final var declaration = backend.getLifecycleDeclaration();
assertNotNull(declaration);
assertEquals(2, declaration.modules().size());
assertEquals(1, declaration.modules().get(0).moduleId().getIndex());
assertEquals(0, declaration.modules().get(1).moduleId().getIndex());
assertEquals("aaa", backend.getModulePool().get(1).pathSegments().getFirst());
assertEquals("zzz", backend.getModulePool().get(0).pathSegments().getFirst());
assertEquals(IRLifecycleCallableRole.FILE_INIT_FRAGMENT, declaration.modules().get(1).fileInitFragments().getFirst().role());
assertEquals(0, declaration.modules().get(1).fileInitFragments().getFirst().callableId().getIndex());
assertEquals(1, declaration.projectInit().callableId().getIndex());
assertEquals(2, declaration.frameRoot().callableId().getIndex());
assertEquals(20, declaration.frameRoot().fileId().getId());
assertEquals(2, declaration.frameRoot().origin().getStart());
assertEquals("boot", backend.getExecutableFunctions().get(1).callableName());
assertEquals("frame", backend.getExecutableFunctions().get(2).callableName());
}
@Test
void aggregatorMustOmitLifecycleDeclarationWhenNoFileContributesOne() {
final var aggregator = IRBackend.aggregator();
aggregator.merge(new IRBackendFile(
new FileId(1),
ReadOnlyList.empty(),
ReadOnlyList.from(new IRBackendExecutableFunction(
new FileId(1),
"entry",
new CallableId(0),
0,
10,
0,
0,
0,
1,
ReadOnlyList.from(new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.RET,
"",
null,
null,
Span.none())),
Span.none())),
IRReservedMetadata.empty(),
ReadOnlyList.from(new CallableSignatureRef("entry", 0, "() -> unit")),
ReadOnlyList.empty()));
final var backend = aggregator.emit();
assertNull(backend.getLifecycleDeclaration());
}
private static IRBackendExecutableFunction executable(
final String name,
final int fileId,
final int moduleId,
final int callableId,
final Span span) {
return new IRBackendExecutableFunction(
new FileId(fileId),
new ModuleId(moduleId),
name,
new CallableId(callableId),
(int) span.getStart(),
(int) span.getEnd(),
0,
0,
0,
1,
ReadOnlyList.from(new IRBackendExecutableFunction.Instruction(
IRBackendExecutableFunction.InstructionKind.RET,
"",
null,
null,
span)),
span);
}
private static IRBackend emitDeterministicAggregateFixture() {
final var aggregator = IRBackend.aggregator();
aggregator.merge(new IRBackendFile(