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