implements PLN-0006

This commit is contained in:
bQUARKz 2026-03-27 17:16:34 +00:00
parent e0c57814e1
commit 4a6cc35989
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
20 changed files with 430 additions and 14 deletions

View File

@ -4,6 +4,6 @@
{"type":"discussion","id":"DSC-0003","status":"done","ticket":"packer-docs-import","title":"Import docs/packer into discussion-framework artifacts","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["packer","migration","discussion-framework","docs-import"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0009","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0009-mental-model-packer-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0010","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0010-asset-identity-and-runtime-contract-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0011","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0011-foundations-workspace-runtime-and-build-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0012","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0012-runtime-ownership-and-studio-boundary-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0013","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0013-metadata-convergence-and-runtime-sink-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0014","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0014-pack-wizard-summary-validation-and-pack-execution-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0015","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0015-tile-bank-packing-contract-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0017","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0017-packer-docs-import-pattern.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"}]}
{"type":"discussion","id":"DSC-0004","status":"open","ticket":"tilemap-and-metatile-runtime-binary-layout","title":"Tilemap and Metatile Runtime Binary Layout","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["packer","legacy-import","tilemap","metatile","runtime-layout"],"agendas":[{"id":"AGD-0004","file":"AGD-0004-tilemap-and-metatile-runtime-binary-layout.md","status":"open","created_at":"2026-03-26","updated_at":"2026-03-26"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0005","status":"open","ticket":"variable-tile-bank-palette-serialization","title":"Variable Tile Bank Palette Serialization","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["packer","legacy-import","tile-bank","palette-serialization","versioning"],"agendas":[{"id":"AGD-0005","file":"AGD-0005-variable-tile-bank-palette-serialization.md","status":"open","created_at":"2026-03-26","updated_at":"2026-03-26"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0006","status":"open","ticket":"pbs-game-facing-asset-refs-and-call-result-discard","title":"PBS Game-Facing Asset References and Ignored Call Result Lowering","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["compiler","pbs","ergonomics","lowering","runtime","asset-identity","expression-statements"],"agendas":[{"id":"AGD-0006","file":"AGD-0006-pbs-game-facing-asset-refs-and-call-result-discard.md","status":"accepted","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[{"id":"DEC-0005","file":"DEC-0005-pbs-asset-address-surface-and-be-lowering.md","status":"accepted","created_at":"2026-03-27","updated_at":"2026-03-27","ref_agenda":"AGD-0006"},{"id":"DEC-0006","file":"DEC-0006-pbs-ignored-values-lowering-and-warning.md","status":"accepted","created_at":"2026-03-27","updated_at":"2026-03-27","ref_agenda":"AGD-0006"}],"plans":[{"id":"PLN-0005","file":"PLN-0005-pbs-asset-address-surface-spec-propagation.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27","ref_decisions":["DEC-0005"]},{"id":"PLN-0006","file":"PLN-0006-pbs-asset-address-surface-implementation.md","status":"review","created_at":"2026-03-27","updated_at":"2026-03-27","ref_decisions":["DEC-0005"]},{"id":"PLN-0007","file":"PLN-0007-pbs-ignored-values-warning-implementation.md","status":"review","created_at":"2026-03-27","updated_at":"2026-03-27","ref_decisions":["DEC-0006"]}],"lessons":[]}
{"type":"discussion","id":"DSC-0006","status":"open","ticket":"pbs-game-facing-asset-refs-and-call-result-discard","title":"PBS Game-Facing Asset References and Ignored Call Result Lowering","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["compiler","pbs","ergonomics","lowering","runtime","asset-identity","expression-statements"],"agendas":[{"id":"AGD-0006","file":"AGD-0006-pbs-game-facing-asset-refs-and-call-result-discard.md","status":"accepted","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[{"id":"DEC-0005","file":"DEC-0005-pbs-asset-address-surface-and-be-lowering.md","status":"accepted","created_at":"2026-03-27","updated_at":"2026-03-27","ref_agenda":"AGD-0006"},{"id":"DEC-0006","file":"DEC-0006-pbs-ignored-values-lowering-and-warning.md","status":"accepted","created_at":"2026-03-27","updated_at":"2026-03-27","ref_agenda":"AGD-0006"}],"plans":[{"id":"PLN-0005","file":"PLN-0005-pbs-asset-address-surface-spec-propagation.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27","ref_decisions":["DEC-0005"]},{"id":"PLN-0006","file":"PLN-0006-pbs-asset-address-surface-implementation.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27","ref_decisions":["DEC-0005"]},{"id":"PLN-0007","file":"PLN-0007-pbs-ignored-values-warning-implementation.md","status":"review","created_at":"2026-03-27","updated_at":"2026-03-27","ref_decisions":["DEC-0006"]}],"lessons":[]}
{"type":"discussion","id":"DSC-0007","status":"done","ticket":"pbs-learn-to-discussion-lessons-migration","title":"Migrate PBS Learn Documents into Discussion Lessons","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["compiler","pbs","migration","discussion-framework","lessons","learn-prune"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0018","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0018-pbs-ast-and-parser-contract-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0019","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0019-pbs-name-resolution-and-linking-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0020","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0020-pbs-runtime-values-identity-memory-boundaries-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0021","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0021-pbs-diagnostics-and-conformance-governance-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0022","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0022-pbs-globals-lifecycle-and-published-entrypoint-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]}
{"type":"discussion","id":"DSC-0008","status":"done","ticket":"pbs-low-level-asset-manager-surface","title":"PBS Low-Level Asset Manager Surface for Runtime AssetManager","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["compiler","pbs","runtime","asset-manager","host-abi","stdlib","asset"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0023","file":"discussion/lessons/DSC-0008-pbs-low-level-asset-manager-surface/LSN-0023-lowassets-runtime-aligned-sdk-surface.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]}

View File

@ -2,9 +2,9 @@
id: PLN-0006
ticket: pbs-game-facing-asset-refs-and-call-result-discard
title: Implement DEC-0005 Addressable Surface, FESurfaceContext, and Backend Asset Lowering
status: review
status: done
created: 2026-03-27
completed:
completed: 2026-03-27
tags: [compiler, pbs, implementation, addressable, lowering, be-fe-contract, asset-identity]
---

View File

@ -1,6 +1,7 @@
package p.studio.compiler.pbs;
import p.studio.compiler.messages.HostAdmissionContext;
import p.studio.compiler.messages.FESurfaceContext;
import p.studio.compiler.models.IRBackendExecutableFunction;
import p.studio.compiler.models.IRBackendFile;
import p.studio.compiler.models.IRFunction;
@ -61,6 +62,16 @@ public final class PbsFrontendCompiler {
final DiagnosticSink diagnostics,
final SourceKind sourceKind,
final HostAdmissionContext hostAdmissionContext) {
return compileFile(fileId, source, diagnostics, sourceKind, hostAdmissionContext, FESurfaceContext.empty());
}
public IRBackendFile compileFile(
final FileId fileId,
final String source,
final DiagnosticSink diagnostics,
final SourceKind sourceKind,
final HostAdmissionContext hostAdmissionContext,
final FESurfaceContext feSurfaceContext) {
final var nameTable = new NameTable();
final var admissionBaseline = diagnostics.errorCount();
final var tokens = PbsLexer.lex(source, fileId, diagnostics);
@ -76,6 +87,7 @@ public final class PbsFrontendCompiler {
ModuleId.none(),
ReadOnlyList.empty(),
hostAdmissionContext,
feSurfaceContext,
nameTable);
if (diagnostics.errorCount() > admissionBaseline) {
return IRBackendFile.empty(fileId);
@ -91,6 +103,7 @@ public final class PbsFrontendCompiler {
final ModuleId moduleId,
final ReadOnlyList<ModuleReference> modulePool,
final HostAdmissionContext hostAdmissionContext,
final FESurfaceContext feSurfaceContext,
final NameTable nameTable) {
return compileParsedFile(
fileId,
@ -100,6 +113,7 @@ public final class PbsFrontendCompiler {
moduleId,
modulePool,
hostAdmissionContext,
feSurfaceContext,
nameTable,
ReadOnlyList.empty(),
ReadOnlyList.empty(),
@ -116,6 +130,7 @@ public final class PbsFrontendCompiler {
final ModuleId moduleId,
final ReadOnlyList<ModuleReference> modulePool,
final HostAdmissionContext hostAdmissionContext,
final FESurfaceContext feSurfaceContext,
final NameTable nameTable,
final ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls,
final ReadOnlyList<ImportedCallableSurface> importedCallables,
@ -134,6 +149,9 @@ public final class PbsFrontendCompiler {
final var effectiveImportedGlobals = importedGlobals == null
? ReadOnlyList.<ImportedGlobalSurface>empty()
: importedGlobals;
final var effectiveFESurfaceContext = feSurfaceContext == null
? FESurfaceContext.empty()
: feSurfaceContext;
final var effectiveImportedReservedMetadata = importedReservedMetadata == null
? IRReservedMetadata.empty()
: importedReservedMetadata;
@ -147,7 +165,7 @@ public final class PbsFrontendCompiler {
effectiveModuleId,
effectiveImportedGlobals,
diagnostics);
flowSemanticsValidator.validate(ast, effectiveSupplementalTopDecls, diagnostics);
flowSemanticsValidator.validate(ast, effectiveSupplementalTopDecls, effectiveFESurfaceContext, diagnostics);
lifecycleSemanticsValidator.validate(ast, effectiveSupplementalTopDecls, diagnostics);
if (diagnostics.errorCount() > semanticsErrorBaseline) {
return IRBackendFile.empty(fileId);
@ -179,6 +197,7 @@ public final class PbsFrontendCompiler {
effectiveModuleId,
mergeReservedMetadata(reservedMetadata, effectiveImportedReservedMetadata),
effectiveNameTable,
effectiveFESurfaceContext,
diagnostics,
effectiveImportedCallables);
if (diagnostics.errorCount() > loweringErrorBaseline) {

View File

@ -378,7 +378,7 @@ final class PbsExecutableBodyLowerer {
handleExpr.span(),
context);
case PbsAst.AsExpr asExpr -> lowerExpression(asExpr.expression(), context);
case PbsAst.MemberExpr memberExpr -> lowerExpression(memberExpr.receiver(), context);
case PbsAst.MemberExpr memberExpr -> lowerMemberExpression(memberExpr, context);
case PbsAst.PropagateExpr propagateExpr -> lowerExpression(propagateExpr.expression(), context);
case PbsAst.GroupExpr groupExpr -> lowerExpression(groupExpr.expression(), context);
case PbsAst.NewExpr newExpr -> lowerExpressionList(newExpr.arguments(), context);
@ -426,6 +426,25 @@ final class PbsExecutableBodyLowerer {
}
}
private void lowerMemberExpression(
final PbsAst.MemberExpr memberExpr,
final PbsExecutableLoweringContext context) {
final var assetReference = flattenAssetReference(memberExpr);
if (assetReference != null) {
final var assetId = context.resolveAssetId(assetReference);
if (assetId == null) {
reportUnsupportedLowering(
"asset reference does not resolve in backend-owned surface: " + assetReference,
memberExpr.span(),
context);
return;
}
emitPushI32(assetId, memberExpr.span(), context);
return;
}
lowerExpression(memberExpr.receiver(), context);
}
private void lowerIfExpression(
final PbsAst.IfExpr ifExpr,
final PbsExecutableLoweringContext context) {
@ -456,6 +475,9 @@ final class PbsExecutableBodyLowerer {
private void lowerIdentifierExpression(
final PbsAst.IdentifierExpr identifierExpr,
final PbsExecutableLoweringContext context) {
if ("assets".equals(identifierExpr.name())) {
return;
}
final var slot = context.localSlotByNameId().get(context.nameTable().register(identifierExpr.name()));
if (slot != null) {
emitGetLocal(slot, identifierExpr.span(), context);
@ -472,6 +494,20 @@ final class PbsExecutableBodyLowerer {
}
}
private String flattenAssetReference(final PbsAst.Expression expression) {
if (expression instanceof PbsAst.IdentifierExpr identifierExpr) {
return "assets".equals(identifierExpr.name()) ? "assets" : null;
}
if (!(expression instanceof PbsAst.MemberExpr memberExpr)) {
return null;
}
final var receiverPath = flattenAssetReference(memberExpr.receiver());
if (receiverPath == null || receiverPath.isBlank()) {
return null;
}
return receiverPath + "." + memberExpr.memberName();
}
private void lowerConstExpression(
final PbsAst.Expression expression,
final PbsExecutableLoweringContext context) {

View File

@ -1,6 +1,7 @@
package p.studio.compiler.pbs.lowering;
import p.studio.compiler.models.IRBackendExecutableFunction;
import p.studio.compiler.messages.Addressable;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.identifiers.NameId;
import p.studio.compiler.source.tables.IntrinsicTable;
@ -66,6 +67,10 @@ final class PbsExecutableLoweringContext {
return metadataIndex.constDeclByNameId();
}
Map<String, Addressable> addressableByAddress() {
return metadataIndex.addressableByAddress();
}
Map<String, String> builtinCanonicalBySourceType() {
return metadataIndex.builtinCanonicalBySourceType();
}
@ -140,6 +145,14 @@ final class PbsExecutableLoweringContext {
return constDeclByNameId().get(nameTable.register(constName));
}
Integer resolveAssetId(final String address) {
if (address == null || address.isBlank()) {
return null;
}
final var addressable = addressableByAddress().get(address);
return addressable == null ? null : addressable.assetId();
}
int declareLocalSlot(final String localName) {
final var nameId = nameTable.register(localName);
final var existing = localSlotByNameId.get(nameId);

View File

@ -2,6 +2,7 @@ package p.studio.compiler.pbs.lowering;
import p.studio.compiler.models.IRBackendExecutableFunction;
import p.studio.compiler.models.IRReservedMetadata;
import p.studio.compiler.messages.Addressable;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.source.identifiers.CallableId;
import p.studio.compiler.source.identifiers.NameId;
@ -46,6 +47,7 @@ public class PbsExecutableLoweringModels {
Map<NameId, String> builtinConstOwnerByNameId,
Map<NameId, Integer> globalSlotByNameId,
Map<NameId, PbsAst.ConstDecl> constDeclByNameId,
Map<String, Addressable> addressableByAddress,
Map<PbsIntrinsicOwnerMethodKey, List<IRReservedMetadata.IntrinsicSurface>> intrinsicByOwnerAndMethod,
Map<PbsIntrinsicCanonicalKey, String> intrinsicReturnOwnerByCanonical) {
}

View File

@ -2,6 +2,7 @@ package p.studio.compiler.pbs.lowering;
import p.studio.compiler.models.IRBackendExecutableFunction;
import p.studio.compiler.models.IRReservedMetadata;
import p.studio.compiler.messages.FESurfaceContext;
import p.studio.compiler.pbs.PbsFrontendCompiler;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
@ -29,6 +30,7 @@ public final class PbsExecutableLoweringService {
final ModuleId moduleId,
final IRReservedMetadata reservedMetadata,
final NameTable nameTable,
final FESurfaceContext feSurfaceContext,
final DiagnosticSink diagnostics,
final ReadOnlyList<PbsFrontendCompiler.ImportedCallableSurface> importedCallables) {
final var normalizedModuleId = moduleId == null ? ModuleId.none() : moduleId;
@ -37,6 +39,7 @@ public final class PbsExecutableLoweringService {
supplementalTopDecls,
reservedMetadata,
nameTable,
feSurfaceContext,
diagnostics);
final var callableRegistry = callableRegistryFactory.create(
ast,

View File

@ -1,6 +1,8 @@
package p.studio.compiler.pbs.lowering;
import p.studio.compiler.models.IRReservedMetadata;
import p.studio.compiler.messages.Addressable;
import p.studio.compiler.messages.FESurfaceContext;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.pbs.semantics.PbsSemanticsErrors;
import p.studio.compiler.source.diagnostics.DiagnosticPhase;
@ -23,12 +25,14 @@ final class PbsExecutableMetadataIndexFactory {
final ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls,
final IRReservedMetadata reservedMetadata,
final NameTable nameTable,
final FESurfaceContext feSurfaceContext,
final DiagnosticSink diagnostics) {
final var hostByMethodName = indexHostBindingsByMethodName(reservedMetadata, nameTable);
final var builtinCanonicalBySourceType = indexBuiltinCanonicalBySourceType(reservedMetadata);
final var builtinConstOwnerByNameId = indexBuiltinConstOwners(reservedMetadata, nameTable);
final var globalSlotByNameId = indexGlobalSlots(ast, nameTable);
final var constDeclByNameId = indexConstDecls(ast, nameTable);
final var addressableByAddress = indexAddressables(feSurfaceContext);
final var builtinSignatureByOwnerAndMethod = indexBuiltinSignatures(
ast,
supplementalTopDecls,
@ -48,6 +52,7 @@ final class PbsExecutableMetadataIndexFactory {
builtinConstOwnerByNameId,
globalSlotByNameId,
constDeclByNameId,
addressableByAddress,
intrinsicByOwnerAndMethod,
intrinsicReturnOwnerByCanonical);
}
@ -112,6 +117,18 @@ final class PbsExecutableMetadataIndexFactory {
return constDeclByNameId;
}
private Map<String, Addressable> indexAddressables(final FESurfaceContext feSurfaceContext) {
final var addressableByAddress = new HashMap<String, Addressable>();
final var effectiveSurfaceContext = feSurfaceContext == null ? FESurfaceContext.empty() : feSurfaceContext;
for (final var addressable : effectiveSurfaceContext.assets()) {
if (addressable == null || addressable.address().isBlank()) {
continue;
}
addressableByAddress.putIfAbsent(addressable.address(), addressable);
}
return addressableByAddress;
}
private Map<PbsIntrinsicOwnerMethodKey, PbsAst.FunctionSignature> indexBuiltinSignatures(
final PbsAst.File ast,
final ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls,

View File

@ -1,5 +1,6 @@
package p.studio.compiler.pbs.semantics;
import p.studio.compiler.messages.FESurfaceContext;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.pbs.semantics.PbsFlowSemanticSupport.Model;
import p.studio.compiler.pbs.semantics.PbsFlowSemanticSupport.Scope;
@ -27,8 +28,9 @@ final class PbsFlowBodyAnalyzer {
public void validate(
final PbsAst.File ast,
final ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls,
final FESurfaceContext feSurfaceContext,
final DiagnosticSink diagnostics) {
final var model = Model.from(ast, supplementalTopDecls);
final var model = Model.from(ast, supplementalTopDecls, feSurfaceContext, diagnostics);
for (final var topDecl : ast.topDecls()) {
validateTopDecl(topDecl, model, diagnostics);

View File

@ -70,6 +70,11 @@ final class PbsFlowCallableResolutionAnalyzer {
ExprResult analyzeMemberExpression(
final PbsAst.MemberExpr memberExpr,
final PbsFlowExpressionContext context) {
final var assetReference = flattenAssetReference(memberExpr);
if (assetReference != null) {
return resolveAssetReference(assetReference, memberExpr, context);
}
final var receiver = analyzeValueExpression(memberExpr.receiver(), context, null).type();
final var model = context.model();
final var diagnostics = context.diagnostics();
@ -320,6 +325,47 @@ final class PbsFlowCallableResolutionAnalyzer {
&& receiver.name().equals(currentReceiverType.name());
}
private ExprResult resolveAssetReference(
final String assetReference,
final PbsAst.MemberExpr memberExpr,
final PbsFlowExpressionContext context) {
final var diagnostics = context.diagnostics();
final var model = context.model();
final var resolvedAssetId = model.resolveAssetId(assetReference);
if (resolvedAssetId != null) {
return ExprResult.type(TypeView.addressable(assetReference));
}
if (model.isAssetNamespace(assetReference)) {
if (context.use() == ExprUse.VALUE) {
Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_INVALID_MEMBER_ACCESS.name(),
"Asset namespace '%s' is not a terminal Addressable value".formatted(assetReference),
memberExpr.span());
return ExprResult.type(TypeView.unknown());
}
return ExprResult.type(TypeView.assetNamespace(assetReference));
}
Diagnostics.error(diagnostics,
PbsSemanticsErrors.E_SEM_INVALID_MEMBER_ACCESS.name(),
"Asset reference '%s' does not resolve in the backend-provided surface".formatted(assetReference),
memberExpr.span());
return ExprResult.type(TypeView.unknown());
}
private String flattenAssetReference(final PbsAst.Expression expression) {
if (expression instanceof PbsAst.IdentifierExpr identifierExpr) {
return "assets".equals(identifierExpr.name()) ? "assets" : null;
}
if (!(expression instanceof PbsAst.MemberExpr memberExpr)) {
return null;
}
final var receiverPath = flattenAssetReference(memberExpr.receiver());
if (receiverPath == null || receiverPath.isBlank()) {
return null;
}
return receiverPath + "." + memberExpr.memberName();
}
private ExprResult analyzeValueExpression(
final PbsAst.Expression expression,
final PbsFlowExpressionContext context,

View File

@ -1,8 +1,11 @@
package p.studio.compiler.pbs.semantics;
import lombok.experimental.UtilityClass;
import p.studio.compiler.messages.FESurfaceContext;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.source.Span;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.diagnostics.Diagnostics;
import p.studio.utilities.structures.ReadOnlyList;
import java.util.*;
@ -44,7 +47,9 @@ final class PbsFlowSemanticSupport {
OPTIONAL,
RESULT,
TUPLE,
TYPE_REF
TYPE_REF,
ADDRESSABLE,
ASSET_NAMESPACE
}
record TupleField(
@ -126,6 +131,14 @@ final class PbsFlowSemanticSupport {
static TypeView typeRef(final String name) {
return new TypeView(Kind.TYPE_REF, name, List.of(), null, null, List.of(), null);
}
static TypeView addressable(final String address) {
return new TypeView(Kind.ADDRESSABLE, address, List.of(), null, null, List.of(), null);
}
static TypeView assetNamespace(final String path) {
return new TypeView(Kind.ASSET_NAMESPACE, path, List.of(), null, null, List.of(), null);
}
}
record CallableSymbol(
@ -176,14 +189,18 @@ final class PbsFlowSemanticSupport {
final Set<String> knownEnumNames = new HashSet<>();
final Set<String> knownErrorNames = new HashSet<>();
final Set<String> knownCallbackNames = new HashSet<>();
final Map<String, Integer> assetIdByAddress = new HashMap<>();
final Set<String> assetNamespacePrefixes = new HashSet<>();
static Model from(final PbsAst.File ast) {
return from(ast, ReadOnlyList.empty());
return from(ast, ReadOnlyList.empty(), FESurfaceContext.empty(), DiagnosticSink.empty());
}
static Model from(
final PbsAst.File ast,
final ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls) {
final ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls,
final FESurfaceContext feSurfaceContext,
final DiagnosticSink diagnostics) {
final var model = new Model();
for (final var topDecl : ast.topDecls()) {
model.registerKnownTopDecl(topDecl);
@ -197,6 +214,7 @@ final class PbsFlowSemanticSupport {
for (final var topDecl : supplementalTopDecls) {
model.ingestTopDecl(topDecl);
}
model.ingestAssetSurface(feSurfaceContext, diagnostics);
return model;
}
@ -376,6 +394,72 @@ final class PbsFlowSemanticSupport {
globalTypes.put(globalDecl.name(), typeFrom(globalDecl.explicitType()));
}
}
private void ingestAssetSurface(
final FESurfaceContext feSurfaceContext,
final DiagnosticSink diagnostics) {
final var effectiveSurfaceContext = feSurfaceContext == null ? FESurfaceContext.empty() : feSurfaceContext;
for (final var addressable : effectiveSurfaceContext.assets()) {
if (addressable == null || addressable.address().isBlank()) {
continue;
}
assetIdByAddress.put(addressable.address(), addressable.assetId());
registerAssetNamespaces(addressable.address());
}
for (final var address : assetIdByAddress.keySet()) {
for (final var prefix : namespacePrefixesOf(address)) {
if (assetIdByAddress.containsKey(prefix)) {
Diagnostics.error(
diagnostics,
PbsSemanticsErrors.E_SEM_INVALID_MEMBER_ACCESS.name(),
"Asset surface collision: '%s' cannot be both terminal and namespace prefix".formatted(prefix),
astSyntheticSpan());
}
}
}
}
boolean hasAssetNamespaceRoot() {
return assetNamespacePrefixes.contains("assets") || assetIdByAddress.containsKey("assets");
}
boolean isAssetNamespace(final String path) {
return assetNamespacePrefixes.contains(path);
}
Integer resolveAssetId(final String address) {
return assetIdByAddress.get(address);
}
private void registerAssetNamespaces(final String address) {
for (final var prefix : namespacePrefixesOf(address)) {
assetNamespacePrefixes.add(prefix);
}
}
private List<String> namespacePrefixesOf(final String address) {
final var segments = address.split("\\.");
final var prefixes = new ArrayList<String>();
if (segments.length <= 1) {
return prefixes;
}
final var builder = new StringBuilder();
for (int i = 0; i < segments.length - 1; i++) {
if (segments[i] == null || segments[i].isBlank()) {
break;
}
if (builder.length() > 0) {
builder.append('.');
}
builder.append(segments[i]);
prefixes.add(builder.toString());
}
return prefixes;
}
private Span astSyntheticSpan() {
return Span.none();
}
private CallableSymbol callableFrom(
final String name,
@ -429,6 +513,9 @@ final class PbsFlowSemanticSupport {
if ("str".equals(typeRef.name())) {
yield TypeView.str();
}
if ("Addressable".equals(typeRef.name())) {
yield TypeView.addressable(typeRef.name());
}
if (structs.containsKey(typeRef.name()) || knownStructNames.contains(typeRef.name())) {
yield TypeView.struct(typeRef.name());
}

View File

@ -1,5 +1,6 @@
package p.studio.compiler.pbs.semantics;
import p.studio.compiler.messages.FESurfaceContext;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.utilities.structures.ReadOnlyList;
@ -8,13 +9,14 @@ public final class PbsFlowSemanticsValidator {
private final PbsFlowBodyAnalyzer flowBodyAnalyzer = new PbsFlowBodyAnalyzer();
public void validate(final PbsAst.File ast, final DiagnosticSink diagnostics) {
flowBodyAnalyzer.validate(ast, ReadOnlyList.empty(), diagnostics);
flowBodyAnalyzer.validate(ast, ReadOnlyList.empty(), FESurfaceContext.empty(), diagnostics);
}
public void validate(
final PbsAst.File ast,
final ReadOnlyList<PbsAst.TopDecl> supplementalTopDecls,
final FESurfaceContext feSurfaceContext,
final DiagnosticSink diagnostics) {
flowBodyAnalyzer.validate(ast, supplementalTopDecls, diagnostics);
flowBodyAnalyzer.validate(ast, supplementalTopDecls, feSurfaceContext, diagnostics);
}
}

View File

@ -70,6 +70,10 @@ final class PbsFlowStructuralExpressionAnalyzer {
return ExprResult.type(receiverType);
}
if (expression instanceof PbsAst.IdentifierExpr identifierExpr) {
if ("assets".equals(identifierExpr.name()) && model.hasAssetNamespaceRoot()) {
return ExprResult.type(TypeView.assetNamespace("assets"));
}
final var localType = context.scope().resolve(identifierExpr.name());
if (localType != null) {
return ExprResult.type(localType);

View File

@ -47,7 +47,8 @@ final class PbsFlowTypeOps {
}
return switch (actual.kind()) {
case UNIT, INT, FLOAT, BOOL, STR -> true;
case STRUCT, SERVICE, CONTRACT, CALLBACK, ENUM, ERROR, TYPE_REF -> actual.name().equals(expected.name());
case STRUCT, SERVICE, CONTRACT, CALLBACK, ENUM, ERROR, TYPE_REF, ASSET_NAMESPACE -> actual.name().equals(expected.name());
case ADDRESSABLE -> true;
case OPTIONAL -> compatible(actual.inner(), expected.inner());
case RESULT -> compatible(actual.errorType(), expected.errorType()) && compatible(actual.inner(), expected.inner());
case TUPLE -> tupleCompatible(actual, expected);
@ -173,6 +174,9 @@ final class PbsFlowTypeOps {
if ("str".equals(name)) {
return TypeView.str();
}
if ("Addressable".equals(name)) {
return TypeView.addressable(name);
}
if (model.structs.containsKey(name)) {
return TypeView.struct(name);
}

View File

@ -2,6 +2,7 @@ package p.studio.compiler.services;
import lombok.extern.slf4j.Slf4j;
import p.studio.compiler.messages.BuildingIssueSink;
import p.studio.compiler.messages.FESurfaceContext;
import p.studio.compiler.messages.FrontendPhaseContext;
import p.studio.compiler.models.IRBackend;
import p.studio.compiler.models.IRBackendExecutableFunction;
@ -59,6 +60,7 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
final LogAggregator logs,
final BuildingIssueSink issues) {
final var nameTable = ctx.nameTable();
final var feSurfaceContext = ctx.feSurfaceContext();
final var assembly = moduleAssemblyService.assemble(ctx, nameTable, diagnostics, issues);
final var parsedSourceFiles = assembly.parsedSourceFiles();
final var importedSemanticContexts = importedSemanticContextService.build(parsedSourceFiles, assembly.moduleTable());
@ -85,6 +87,7 @@ public class PBSFrontendPhaseService implements FrontendPhaseService {
parsedSource.moduleId(),
canonicalModulePool,
ctx.hostAdmissionContext(),
feSurfaceContext,
nameTable,
importedSemanticContext.supplementalTopDecls(),
importedSemanticContext.importedCallables(),

View File

@ -1,6 +1,8 @@
package p.studio.compiler.pbs;
import org.junit.jupiter.api.Test;
import p.studio.compiler.messages.Addressable;
import p.studio.compiler.messages.FESurfaceContext;
import p.studio.compiler.messages.HostAdmissionContext;
import p.studio.compiler.models.IRHiddenGlobalKind;
import p.studio.compiler.models.IRGlobalVisibility;
@ -458,6 +460,58 @@ class PbsFrontendCompilerTest {
&& h.abiVersion() == 1));
}
@Test
void shouldLowerBackendOwnedAssetReferenceToAssetId() {
final var source = """
fn boot() -> Addressable {
return assets.ui.panel;
}
""";
final var diagnostics = DiagnosticSink.empty();
final var compiler = new PbsFrontendCompiler();
final var fileBackend = compiler.compileFile(
new FileId(12),
source,
diagnostics,
SourceKind.PROJECT,
HostAdmissionContext.permissiveDefault(),
new FESurfaceContext(ReadOnlyList.from(new Addressable("assets.ui.panel", 37))));
assertTrue(diagnostics.isEmpty(), diagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList().toString());
final var executableBoot = fileBackend.executableFunctions().stream()
.filter(fn -> fn.callableName().equals("boot"))
.findFirst()
.orElseThrow();
assertTrue(executableBoot.instructions().stream().anyMatch(i ->
i.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.PUSH_I32
&& "37".equals(i.label())));
}
@Test
void shouldRejectUnresolvedBackendOwnedAssetReference() {
final var source = """
fn boot() -> Addressable {
return assets.ui.missing;
}
""";
final var diagnostics = DiagnosticSink.empty();
final var compiler = new PbsFrontendCompiler();
final var fileBackend = compiler.compileFile(
new FileId(13),
source,
diagnostics,
SourceKind.PROJECT,
HostAdmissionContext.permissiveDefault(),
new FESurfaceContext(ReadOnlyList.from(new Addressable("assets.ui.panel", 37))));
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_INVALID_MEMBER_ACCESS.name())
&& d.getMessage().contains("assets.ui.missing")));
assertEquals(0, fileBackend.executableFunctions().size());
}
@Test
void shouldRejectHostBindingWithoutCapabilityAtHostAdmission() {
final var source = """

View File

@ -2,7 +2,9 @@ package p.studio.compiler.services;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import p.studio.compiler.messages.Addressable;
import p.studio.compiler.messages.BuildingIssueSink;
import p.studio.compiler.messages.FESurfaceContext;
import p.studio.compiler.messages.FrontendPhaseContext;
import p.studio.compiler.models.IRGlobalVisibility;
import p.studio.compiler.models.BuildStack;
@ -40,6 +42,61 @@ class PBSFrontendPhaseServiceTest {
@TempDir
Path tempDir;
@Test
void shouldPropagateBackendOwnedAssetSurfaceIntoPbsFrontendCompilation() throws IOException {
final var projectRoot = tempDir.resolve("project-assets");
final var sourceRoot = projectRoot.resolve("src");
final var modulePath = sourceRoot.resolve("main");
Files.createDirectories(modulePath);
final var sourceFile = modulePath.resolve("source.pbs");
final var modBarrel = modulePath.resolve("mod.barrel");
Files.writeString(sourceFile, """
[Frame]
fn frame() -> void {
let asset: Addressable = assets.ui.panel;
return;
}
""");
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("core")
.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,
p.studio.compiler.messages.HostAdmissionContext.permissiveDefault(),
new FESurfaceContext(ReadOnlyList.from(new Addressable("assets.ui.panel", 91))));
final var diagnostics = DiagnosticSink.empty();
final var irBackend = new PBSFrontendPhaseService().compile(
ctx,
diagnostics,
LogAggregator.empty(),
BuildingIssueSink.empty());
assertTrue(diagnostics.isEmpty(), diagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList().toString());
final var frameFn = irBackend.getExecutableFunctions().stream()
.filter(function -> "frame".equals(function.callableName()))
.findFirst()
.orElseThrow();
assertTrue(frameFn.instructions().stream().anyMatch(instruction ->
instruction.kind() == p.studio.compiler.models.IRBackendExecutableFunction.InstructionKind.PUSH_I32
&& "91".equals(instruction.label())));
}
@Test
void shouldReportInvalidBarrelFilename() throws IOException {
final var projectRoot = tempDir.resolve("project");

View File

@ -0,0 +1,17 @@
package p.studio.compiler.messages;
public record Addressable(
String address,
int assetId) {
public Addressable {
address = normalize(address);
}
private static String normalize(final String address) {
if (address == null) {
return "";
}
return address.trim();
}
}

View File

@ -0,0 +1,32 @@
package p.studio.compiler.messages;
import p.studio.utilities.structures.ReadOnlyList;
import java.util.ArrayList;
import java.util.LinkedHashMap;
public record FESurfaceContext(
ReadOnlyList<Addressable> assets) {
public FESurfaceContext {
assets = normalizeAssets(assets);
}
public static FESurfaceContext empty() {
return new FESurfaceContext(ReadOnlyList.empty());
}
private static ReadOnlyList<Addressable> normalizeAssets(final ReadOnlyList<Addressable> assets) {
if (assets == null || assets.isEmpty()) {
return ReadOnlyList.empty();
}
final var dedup = new LinkedHashMap<String, Addressable>();
for (final var asset : assets) {
if (asset == null || asset.address().isBlank()) {
continue;
}
dedup.put(asset.address(), asset);
}
return ReadOnlyList.wrap(new ArrayList<>(dedup.values()));
}
}

View File

@ -14,12 +14,13 @@ public class FrontendPhaseContext {
private final NameTable nameTable;
private final int stdlibVersion;
private final HostAdmissionContext hostAdmissionContext;
private final FESurfaceContext feSurfaceContext;
public FrontendPhaseContext(
final ProjectTableReader projectTable,
final FileTableReader fileTable,
final BuildStack stack) {
this(projectTable, fileTable, stack, 1, HostAdmissionContext.permissiveDefault());
this(projectTable, fileTable, stack, 1, HostAdmissionContext.permissiveDefault(), FESurfaceContext.empty());
}
public FrontendPhaseContext(
@ -27,7 +28,7 @@ public class FrontendPhaseContext {
final FileTableReader fileTable,
final BuildStack stack,
final int stdlibVersion) {
this(projectTable, fileTable, stack, stdlibVersion, HostAdmissionContext.permissiveDefault());
this(projectTable, fileTable, stack, stdlibVersion, HostAdmissionContext.permissiveDefault(), FESurfaceContext.empty());
}
public FrontendPhaseContext(
@ -36,6 +37,16 @@ public class FrontendPhaseContext {
final BuildStack stack,
final int stdlibVersion,
final HostAdmissionContext hostAdmissionContext) {
this(projectTable, fileTable, stack, stdlibVersion, hostAdmissionContext, FESurfaceContext.empty());
}
public FrontendPhaseContext(
final ProjectTableReader projectTable,
final FileTableReader fileTable,
final BuildStack stack,
final int stdlibVersion,
final HostAdmissionContext hostAdmissionContext,
final FESurfaceContext feSurfaceContext) {
this.projectTable = projectTable;
this.fileTable = fileTable;
this.stack = stack;
@ -44,6 +55,9 @@ public class FrontendPhaseContext {
this.hostAdmissionContext = hostAdmissionContext == null
? HostAdmissionContext.permissiveDefault()
: hostAdmissionContext;
this.feSurfaceContext = feSurfaceContext == null
? FESurfaceContext.empty()
: feSurfaceContext;
}
public SourceKind sourceKind(final ProjectId projectId) {
@ -61,4 +75,8 @@ public class FrontendPhaseContext {
public NameTable nameTable() {
return nameTable;
}
public FESurfaceContext feSurfaceContext() {
return feSurfaceContext;
}
}