asset addressable PBS surface

This commit is contained in:
bQUARKz 2026-03-27 19:41:07 +00:00
parent b75b2a5825
commit 7c9f7b58db
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
22 changed files with 904 additions and 484 deletions

View File

@ -0,0 +1,42 @@
package p.studio.compiler.pbs;
import p.studio.compiler.pbs.ast.PbsAst;
public final class PbsCallableSlotCounter {
private PbsCallableSlotCounter() {
}
public static int returnSlots(
final PbsAst.ReturnKind returnKind,
final PbsAst.TypeRef returnType) {
return switch (returnKind) {
case INFERRED_UNIT, EXPLICIT_UNIT -> 0;
case RESULT -> 1;
case PLAIN -> Math.max(0, typeSlots(returnType));
};
}
private static int typeSlots(final PbsAst.TypeRef typeRef) {
final var unwrapped = unwrapGroup(typeRef);
if (unwrapped == null) {
return 0;
}
return switch (unwrapped.kind()) {
case UNIT, ERROR -> 0;
case NAMED_TUPLE -> unwrapped.fields().stream()
.mapToInt(field -> Math.max(1, typeSlots(field.typeRef())))
.sum();
default -> 1;
};
}
private static PbsAst.TypeRef unwrapGroup(final PbsAst.TypeRef typeRef) {
if (typeRef == null) {
return null;
}
if (typeRef.kind() != PbsAst.TypeRefKind.GROUP) {
return typeRef;
}
return unwrapGroup(typeRef.inner());
}
}

View File

@ -523,6 +523,8 @@ public final class PbsFrontendCompiler {
String callableName,
int arity,
int returnSlots,
PbsAst.ReturnKind returnKind,
PbsAst.TypeRef returnType,
String shapeSurface) {
public ImportedCallableSurface {
moduleId = moduleId == null ? ModuleId.none() : moduleId;

View File

@ -76,10 +76,7 @@ public final class PbsReservedMetadataExtractor {
abiModule,
abiMethod,
abiVersion,
switch (signature.returnKind()) {
case INFERRED_UNIT, EXPLICIT_UNIT -> 0;
case PLAIN, RESULT -> 1;
},
PbsCallableSlotCounter.returnSlots(signature.returnKind(), signature.returnType()),
assetLoweringAttribute.isPresent()
? safeToInteger(longArgument(assetLoweringAttribute.get(), "param").orElse(-1L))
: null,
@ -129,10 +126,7 @@ public final class PbsReservedMetadataExtractor {
canonicalIntrinsicName(canonicalTypeName, intrinsicName),
longArgument(intrinsicMetadata, "version").orElse(canonicalVersion),
signature.parameters().size(),
switch (signature.returnKind()) {
case INFERRED_UNIT, EXPLICIT_UNIT -> 0;
case PLAIN, RESULT -> 1;
},
PbsCallableSlotCounter.returnSlots(signature.returnKind(), signature.returnType()),
signature.span()));
}

View File

@ -1,6 +1,7 @@
package p.studio.compiler.pbs.lowering;
import p.studio.compiler.models.IRBackendExecutableFunction;
import p.studio.compiler.pbs.PbsCallableSlotCounter;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.pbs.semantics.PbsSemanticsErrors;
import p.studio.compiler.source.diagnostics.DiagnosticPhase;
@ -87,10 +88,7 @@ final class PbsExecutableBodyLowerer {
}
private int returnSlotsFor(final PbsAst.FunctionDecl functionDecl) {
return switch (functionDecl.returnKind()) {
case INFERRED_UNIT, EXPLICIT_UNIT -> 0;
case PLAIN, RESULT -> 1;
};
return PbsCallableSlotCounter.returnSlots(functionDecl.returnKind(), functionDecl.returnType());
}
private void finalizeFunctionInstructions(
@ -155,10 +153,13 @@ final class PbsExecutableBodyLowerer {
final PbsAst.LetStatement letStatement,
final PbsExecutableLoweringContext context) {
final var localOwner = callsiteEmitter.resolveExpressionOwnerForLowering(letStatement.initializer(), context);
final var tupleProjection = tupleProjectionOf(letStatement.initializer(), context);
final var valueSlots = Math.max(1, materializedValueSlots(letStatement.initializer(), context));
lowerExpression(letStatement.initializer(), context);
final var localSlot = context.declareLocalSlot(letStatement.name());
final var localSlot = context.declareLocalSlots(letStatement.name(), valueSlots);
context.bindLocalOwner(letStatement.name(), localOwner);
emitSetLocal(localSlot, letStatement.span(), context);
context.bindLocalTupleProjection(letStatement.name(), tupleProjection);
emitStoreLocalSlots(localSlot, valueSlots, letStatement.span(), context);
return false;
}
@ -182,10 +183,13 @@ final class PbsExecutableBodyLowerer {
}
if (assignStatement.operator() == PbsAst.AssignOperator.ASSIGN) {
final var localOwner = callsiteEmitter.resolveExpressionOwnerForLowering(assignStatement.value(), context);
final var tupleProjection = tupleProjectionOf(assignStatement.value(), context);
final var valueSlots = Math.max(1, materializedValueSlots(assignStatement.value(), context));
lowerExpression(assignStatement.value(), context);
if (targetLocalSlot != null) {
context.bindLocalOwner(target.rootName(), localOwner);
emitSetLocal(targetLocalSlot, assignStatement.span(), context);
context.bindLocalTupleProjection(target.rootName(), tupleProjection);
emitStoreLocalSlots(targetLocalSlot, valueSlots, assignStatement.span(), context);
} else {
emitSetGlobal(targetGlobalSlot, assignStatement.span(), context);
}
@ -476,6 +480,17 @@ final class PbsExecutableBodyLowerer {
emitPushI32(assetId, memberExpr.span(), context);
return;
}
if (memberExpr.receiver() instanceof PbsAst.IdentifierExpr identifierExpr) {
final var tupleProjection = context.resolveLocalTupleProjection(identifierExpr.name());
final var localSlot = context.resolveLocalSlot(identifierExpr.name());
if (tupleProjection != null && localSlot != null) {
final var fieldProjection = tupleProjection.fieldsByLabel().get(memberExpr.memberName());
if (fieldProjection != null) {
emitLoadLocalSlots(localSlot + fieldProjection.slotOffset(), fieldProjection.slotCount(), memberExpr.span(), context);
return;
}
}
}
lowerExpression(memberExpr.receiver(), context);
}
@ -534,7 +549,12 @@ final class PbsExecutableBodyLowerer {
}
final var slot = context.localSlotByNameId().get(context.nameTable().register(identifierExpr.name()));
if (slot != null) {
emitGetLocal(slot, identifierExpr.span(), context);
final var tupleProjection = context.resolveLocalTupleProjection(identifierExpr.name());
if (tupleProjection != null) {
emitLoadLocalSlots(slot, tupleProjection.slotCount(), identifierExpr.span(), context);
} else {
emitGetLocal(slot, identifierExpr.span(), context);
}
return;
}
final var globalSlot = context.resolveGlobalSlot(identifierExpr.name());
@ -589,6 +609,15 @@ final class PbsExecutableBodyLowerer {
if (assetReference != null && context.resolveAssetId(assetReference) != null) {
yield 1;
}
if (memberExpr.receiver() instanceof PbsAst.IdentifierExpr identifierExpr) {
final var tupleProjection = context.resolveLocalTupleProjection(identifierExpr.name());
if (tupleProjection != null) {
final var fieldProjection = tupleProjection.fieldsByLabel().get(memberExpr.memberName());
if (fieldProjection != null) {
yield fieldProjection.slotCount();
}
}
}
yield materializedValueSlots(memberExpr.receiver(), context);
}
case PbsAst.PropagateExpr propagateExpr -> materializedValueSlots(propagateExpr.expression(), context);
@ -605,7 +634,7 @@ final class PbsExecutableBodyLowerer {
yield slots;
}
case PbsAst.BlockExpr blockExpr -> blockValueSlots(blockExpr.block(), context);
case PbsAst.IdentifierExpr identifierExpr -> producesIdentifierValue(identifierExpr, context) ? 1 : 0;
case PbsAst.IdentifierExpr identifierExpr -> identifierValueSlots(identifierExpr, context);
case PbsAst.IntLiteralExpr ignored -> 1;
case PbsAst.StringLiteralExpr ignoredString -> 1;
case PbsAst.BoolLiteralExpr ignoredBool -> 1;
@ -648,6 +677,31 @@ final class PbsExecutableBodyLowerer {
|| context.resolveConstDecl(identifierExpr.name()) != null;
}
private int identifierValueSlots(
final PbsAst.IdentifierExpr identifierExpr,
final PbsExecutableLoweringContext context) {
if (!producesIdentifierValue(identifierExpr, context)) {
return 0;
}
final var tupleProjection = context.resolveLocalTupleProjection(identifierExpr.name());
return tupleProjection == null ? 1 : tupleProjection.slotCount();
}
private PbsTupleProjection tupleProjectionOf(
final PbsAst.Expression expression,
final PbsExecutableLoweringContext context) {
if (expression instanceof PbsAst.CallExpr callExpr) {
final var callableId = callsiteEmitter.resolveUniqueCallableId(callExpr, context);
if (callableId != null) {
return context.tupleProjectionByCallableId().get(callableId);
}
}
if (expression instanceof PbsAst.IdentifierExpr identifierExpr) {
return context.resolveLocalTupleProjection(identifierExpr.name());
}
return null;
}
private void lowerConstExpression(
final PbsAst.Expression expression,
final PbsExecutableLoweringContext context) {
@ -821,6 +875,16 @@ final class PbsExecutableBodyLowerer {
span));
}
private void emitStoreLocalSlots(
final int firstSlot,
final int slotCount,
final p.studio.compiler.source.Span span,
final PbsExecutableLoweringContext context) {
for (int i = slotCount - 1; i >= 0; i--) {
emitSetLocal(firstSlot + i, span, context);
}
}
private void emitSetGlobal(
final int slot,
final p.studio.compiler.source.Span span,
@ -935,6 +999,16 @@ final class PbsExecutableBodyLowerer {
span));
}
private void emitLoadLocalSlots(
final int firstSlot,
final int slotCount,
final p.studio.compiler.source.Span span,
final PbsExecutableLoweringContext context) {
for (int i = 0; i < slotCount; i++) {
emitGetLocal(firstSlot + i, span, context);
}
}
private void emitGetGlobal(
final int slot,
final p.studio.compiler.source.Span span,

View File

@ -1,5 +1,6 @@
package p.studio.compiler.pbs.lowering;
import p.studio.compiler.pbs.PbsCallableSlotCounter;
import p.studio.compiler.pbs.PbsFrontendCompiler;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.source.identifiers.CallableId;
@ -26,6 +27,7 @@ final class PbsExecutableCallableRegistryFactory {
final var callableIdsByNameAndArity = new HashMap<PbsCallableResolutionKey, List<CallableId>>();
final var callableIdByDeclaration = new HashMap<PbsLowerableCallable, CallableId>();
final var returnSlotsByCallableId = new HashMap<CallableId, Integer>();
final var tupleProjectionByCallableId = new HashMap<CallableId, PbsTupleProjection>();
final var typeSurfaceTable = new TypeSurfaceTable();
final var callableShapeTable = new CallableShapeTable();
final var localCallables = collectLocalLowerableCallables(ast);
@ -37,6 +39,7 @@ final class PbsExecutableCallableRegistryFactory {
callableIdsByNameAndArity,
callableIdByDeclaration,
returnSlotsByCallableId,
tupleProjectionByCallableId,
typeSurfaceTable,
callableShapeTable,
localCallables);
@ -45,6 +48,7 @@ final class PbsExecutableCallableRegistryFactory {
callableIdTable,
callableIdsByNameAndArity,
returnSlotsByCallableId,
tupleProjectionByCallableId,
importedCallables);
final var callableSignatureByCallableId = new HashMap<CallableId, CallableSignatureRef>();
@ -60,6 +64,7 @@ final class PbsExecutableCallableRegistryFactory {
callableIdsByNameAndArity,
callableSignatureByCallableId,
returnSlotsByCallableId,
tupleProjectionByCallableId,
ReadOnlyList.wrap(callableSignatures));
}
@ -86,6 +91,7 @@ final class PbsExecutableCallableRegistryFactory {
final Map<PbsCallableResolutionKey, List<CallableId>> callableIdsByNameAndArity,
final Map<PbsLowerableCallable, CallableId> callableIdByDeclaration,
final Map<CallableId, Integer> returnSlotsByCallableId,
final Map<CallableId, PbsTupleProjection> tupleProjectionByCallableId,
final TypeSurfaceTable typeSurfaceTable,
final CallableShapeTable callableShapeTable,
final ReadOnlyList<PbsLowerableCallable> localCallables) {
@ -107,6 +113,10 @@ final class PbsExecutableCallableRegistryFactory {
ignored -> new ArrayList<>())
.add(callableId);
returnSlotsByCallableId.put(callableId, returnSlotsFor(declaredFn));
final var tupleProjection = tupleProjectionOf(declaredFn.returnKind(), declaredFn.returnType());
if (tupleProjection != null) {
tupleProjectionByCallableId.put(callableId, tupleProjection);
}
}
}
@ -115,6 +125,7 @@ final class PbsExecutableCallableRegistryFactory {
final CallableTable callableIdTable,
final Map<PbsCallableResolutionKey, List<CallableId>> callableIdsByNameAndArity,
final Map<CallableId, Integer> returnSlotsByCallableId,
final Map<CallableId, PbsTupleProjection> tupleProjectionByCallableId,
final ReadOnlyList<PbsFrontendCompiler.ImportedCallableSurface> importedCallables) {
for (final var importedCallable : importedCallables) {
final var callableId = callableIdTable.register(
@ -130,13 +141,46 @@ final class PbsExecutableCallableRegistryFactory {
ignored -> new ArrayList<>())
.add(callableId);
returnSlotsByCallableId.put(callableId, importedCallable.returnSlots());
final var tupleProjection = tupleProjectionOf(importedCallable.returnKind(), importedCallable.returnType());
if (tupleProjection != null) {
tupleProjectionByCallableId.put(callableId, tupleProjection);
}
}
}
private int returnSlotsFor(final PbsAst.FunctionDecl functionDecl) {
return switch (functionDecl.returnKind()) {
case INFERRED_UNIT, EXPLICIT_UNIT -> 0;
case PLAIN, RESULT -> 1;
};
return PbsCallableSlotCounter.returnSlots(functionDecl.returnKind(), functionDecl.returnType());
}
private PbsTupleProjection tupleProjectionOf(
final PbsAst.ReturnKind returnKind,
final PbsAst.TypeRef returnType) {
if (returnKind != PbsAst.ReturnKind.PLAIN) {
return null;
}
final var unwrapped = unwrapGroup(returnType);
if (unwrapped == null || unwrapped.kind() != PbsAst.TypeRefKind.NAMED_TUPLE) {
return null;
}
final var fieldsByLabel = new HashMap<String, PbsTupleFieldProjection>();
var slotOffset = 0;
for (final var field : unwrapped.fields()) {
final var fieldSlotCount = Math.max(1, PbsCallableSlotCounter.returnSlots(PbsAst.ReturnKind.PLAIN, field.typeRef()));
if (field.label() != null && !field.label().isBlank()) {
fieldsByLabel.put(field.label(), new PbsTupleFieldProjection(field.label(), slotOffset, fieldSlotCount));
}
slotOffset += fieldSlotCount;
}
return new PbsTupleProjection(slotOffset, Map.copyOf(fieldsByLabel));
}
private PbsAst.TypeRef unwrapGroup(final PbsAst.TypeRef typeRef) {
if (typeRef == null) {
return null;
}
if (typeRef.kind() != PbsAst.TypeRefKind.GROUP) {
return typeRef;
}
return unwrapGroup(typeRef.inner());
}
}

View File

@ -16,6 +16,23 @@ import java.util.Map;
import static p.studio.compiler.pbs.lowering.PbsExecutableLoweringModels.*;
final class PbsExecutableCallsiteEmitter {
CallableId resolveUniqueCallableId(
final PbsAst.CallExpr callExpr,
final PbsExecutableLoweringContext context) {
final var calleeIdentity = resolveCalleeIdentity(callExpr.callee(), context.nameTable());
if (calleeIdentity == null) {
return null;
}
final var candidates = resolveCallsiteCandidates(callExpr, calleeIdentity, context);
if (!candidates.hostCandidates().isEmpty() || !candidates.intrinsicCandidates().isEmpty()) {
return null;
}
if (candidates.callableCandidates().size() != 1) {
return null;
}
return candidates.callableCandidates().getFirst();
}
IRReservedMetadata.HostMethodBinding resolveUniqueHostBinding(
final PbsAst.CallExpr callExpr,
final PbsExecutableLoweringContext context) {

View File

@ -18,6 +18,7 @@ final class PbsExecutableLoweringContext {
private final PbsExecutableCallableRegistry callableRegistry;
private final IntrinsicTable intrinsicIdTable;
private final Map<NameId, Integer> localSlotByNameId;
private final Map<NameId, PbsTupleProjection> localTupleProjectionByNameId = new HashMap<>();
private final Map<NameId, String> localOwnerByNameId;
private final ArrayList<IRBackendExecutableFunction.Instruction> instructions = new ArrayList<>();
private final ArrayDeque<PbsLoopTargets> loopTargets = new ArrayDeque<>();
@ -95,6 +96,10 @@ final class PbsExecutableLoweringContext {
return callableRegistry.returnSlotsByCallableId();
}
Map<p.studio.compiler.source.identifiers.CallableId, PbsTupleProjection> tupleProjectionByCallableId() {
return callableRegistry.tupleProjectionByCallableId();
}
IntrinsicTable intrinsicIdTable() {
return intrinsicIdTable;
}
@ -131,6 +136,13 @@ final class PbsExecutableLoweringContext {
return localSlotByNameId.get(nameTable.register(localName));
}
PbsTupleProjection resolveLocalTupleProjection(final String localName) {
if (localName == null || localName.isBlank()) {
return null;
}
return localTupleProjectionByNameId.get(nameTable.register(localName));
}
Integer resolveGlobalSlot(final String globalName) {
if (globalName == null || globalName.isBlank()) {
return null;
@ -154,16 +166,37 @@ final class PbsExecutableLoweringContext {
}
int declareLocalSlot(final String localName) {
return declareLocalSlots(localName, 1);
}
int declareLocalSlots(
final String localName,
final int slotCount) {
final var nameId = nameTable.register(localName);
final var existing = localSlotByNameId.get(nameId);
if (existing != null) {
return existing;
}
final var slot = nextLocalSlot++;
final var slot = nextLocalSlot;
nextLocalSlot += Math.max(1, slotCount);
localSlotByNameId.put(nameId, slot);
return slot;
}
void bindLocalTupleProjection(
final String localName,
final PbsTupleProjection tupleProjection) {
if (localName == null || localName.isBlank()) {
return;
}
final var nameId = nameTable.register(localName);
if (tupleProjection == null) {
localTupleProjectionByNameId.remove(nameId);
return;
}
localTupleProjectionByNameId.put(nameId, tupleProjection);
}
int nextLocalSlot() {
return nextLocalSlot;
}

View File

@ -58,9 +58,21 @@ public class PbsExecutableLoweringModels {
Map<PbsCallableResolutionKey, List<CallableId>> callableIdsByNameAndArity,
Map<CallableId, CallableSignatureRef> callableSignatureByCallableId,
Map<CallableId, Integer> returnSlotsByCallableId,
Map<CallableId, PbsTupleProjection> tupleProjectionByCallableId,
ReadOnlyList<CallableSignatureRef> callableSignatures) {
}
record PbsTupleProjection(
int slotCount,
Map<String, PbsTupleFieldProjection> fieldsByLabel) {
}
record PbsTupleFieldProjection(
String label,
int slotOffset,
int slotCount) {
}
record PbsLoweredExecutableBody(
ReadOnlyList<IRBackendExecutableFunction.Instruction> instructions,
int localSlots,

View File

@ -1,6 +1,7 @@
package p.studio.compiler.services;
import p.studio.compiler.models.IRReservedMetadata;
import p.studio.compiler.pbs.PbsCallableSlotCounter;
import p.studio.compiler.pbs.PbsFrontendCompiler;
import p.studio.compiler.pbs.PbsReservedMetadataExtractor;
import p.studio.compiler.pbs.ast.PbsAst;
@ -93,6 +94,8 @@ final class PbsImportedSemanticContextService {
localName + "." + method.name(),
method.parameters().size(),
returnSlotsFor(method),
method.returnKind(),
method.returnType(),
frontendCompiler.callableShapeSurfaceOf(method)));
}
continue;
@ -106,6 +109,8 @@ final class PbsImportedSemanticContextService {
localName,
functionDecl.parameters().size(),
returnSlotsFor(functionDecl),
functionDecl.returnKind(),
functionDecl.returnType(),
frontendCompiler.callableShapeSurfaceOf(functionDecl)));
continue;
}
@ -445,10 +450,7 @@ final class PbsImportedSemanticContextService {
}
private int returnSlotsFor(final PbsAst.FunctionDecl functionDecl) {
return switch (functionDecl.returnKind()) {
case INFERRED_UNIT, EXPLICIT_UNIT -> 0;
case PLAIN, RESULT -> 1;
};
return PbsCallableSlotCounter.returnSlots(functionDecl.returnKind(), functionDecl.returnType());
}
private String importItemLocalName(final PbsAst.ImportItem importItem) {

View File

@ -16,3 +16,21 @@ declare host LowAssets {
[Capability(name = "asset")]
fn cancel(loading_handle: int) -> int;
}
declare service Assets {
fn load(addressable: Addressable, slot: int) -> (status: int, loading_handle: int) {
return LowAssets.load(addressable, slot);
}
fn status(loading_handle: int) -> int {
return LowAssets.status(loading_handle);
}
fn commit(loading_handle: int) -> int {
return LowAssets.commit(loading_handle);
}
fn cancel(loading_handle: int) -> int {
return LowAssets.cancel(loading_handle);
}
}

View File

@ -1 +1,2 @@
pub host LowAssets;
mod host LowAssets;
pub service Assets;

View File

@ -470,6 +470,7 @@ class PbsFrontendCompilerTest {
&& h.abiModule().equals("asset")
&& h.abiMethod().equals("load")
&& h.abiVersion() == 1
&& h.retSlots() == 2
&& java.util.Objects.equals(h.assetLoweringParam(), 0)));
}

View File

@ -87,11 +87,11 @@ class PbsGateUSdkInterfaceConformanceTest {
import { Color } from @core:color;
import { Gfx } from @sdk:gfx;
import { Input, InputPad, InputButton } from @sdk:input;
import { LowAssets } from @sdk:asset;
import { Assets } from @sdk:asset;
import { Log } from @sdk:log;
declare contract Renderer {
fn render(gfx: Gfx, color: Color, input: Input, pad: InputPad, button: InputButton, assets: LowAssets, log: Log) -> void;
fn render(gfx: Gfx, color: Color, input: Input, pad: InputPad, button: InputButton, assets: Assets, log: Log) -> void;
}
""",
"pub contract Renderer;",
@ -127,7 +127,8 @@ class PbsGateUSdkInterfaceConformanceTest {
.anyMatch(h -> h.ownerName().equals("LowAssets")
&& h.abiModule().equals("asset")
&& h.abiMethod().equals("load")
&& h.abiVersion() == 1));
&& h.abiVersion() == 1
&& java.util.Objects.equals(h.assetLoweringParam(), 0)));
final var negative = compileWorkspaceModule(
tempDir.resolve("gate-u-reserved-import-negative"),
@ -365,7 +366,8 @@ class PbsGateUSdkInterfaceConformanceTest {
&& java.util.Objects.equals(h.assetLoweringParam(), 0)
&& h.abiModule().equals("asset")
&& h.abiMethod().equals("load")
&& h.abiVersion() == 1));
&& h.abiVersion() == 1
&& h.retSlots() == 2));
}
private WorkspaceCompileResult compileWorkspaceModule(

View File

@ -908,9 +908,9 @@ class PBSFrontendPhaseServiceTest {
final var sourceFile = modulePath.resolve("source.pbs");
final var modBarrel = modulePath.resolve("mod.barrel");
Files.writeString(sourceFile, """
import { LowAssets } from @sdk:asset;
import { Assets } from @sdk:asset;
declare contract Loader {
fn load(assets: LowAssets) -> void;
fn load(assets: Assets) -> void;
}
""");
Files.writeString(modBarrel, "pub contract Loader;");

View File

@ -0,0 +1,97 @@
package p.studio.compiler.workspaces;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import p.studio.compiler.messages.Addressable;
import p.studio.compiler.messages.FESurfaceContext;
import p.studio.utilities.structures.ReadOnlyList;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public final class AssetSurfaceContextLoader {
private static final Path REGISTRY_PATH = Path.of("assets", ".prometeu", "index.json");
private final ObjectMapper mapper;
public AssetSurfaceContextLoader() {
this(new ObjectMapper());
}
AssetSurfaceContextLoader(final ObjectMapper mapper) {
this.mapper = Objects.requireNonNull(mapper, "mapper");
}
public FESurfaceContext load(final Path projectRoot) {
final var registryPath = Objects.requireNonNull(projectRoot, "projectRoot")
.toAbsolutePath()
.normalize()
.resolve(REGISTRY_PATH);
if (!Files.isRegularFile(registryPath)) {
return FESurfaceContext.empty();
}
try {
final var document = mapper.readValue(registryPath.toFile(), RegistryDocument.class);
final var addressables = new ArrayList<Addressable>();
if (document.assets != null) {
for (final var asset : document.assets) {
if (asset == null || !isIncludedInBuild(asset)) {
continue;
}
addressables.add(new Addressable(toAddress(asset.root()), asset.assetId()));
}
}
return new FESurfaceContext(ReadOnlyList.wrap(addressables));
} catch (IOException exception) {
throw new IllegalStateException("Unable to load asset surface registry: " + registryPath, exception);
}
}
private boolean isIncludedInBuild(final RegistryAssetDocument asset) {
return asset.includedInBuild() == null || asset.includedInBuild();
}
private String toAddress(final String root) {
final var normalizedRoot = normalizeRoot(root);
if (normalizedRoot.isBlank()) {
throw new IllegalStateException("Asset root must not be blank in surface registry");
}
return "assets." + normalizedRoot.replace('/', '.');
}
private String normalizeRoot(final String root) {
if (root == null) {
return "";
}
final var trimmed = root.trim().replace('\\', '/');
final List<String> segments = new ArrayList<>();
for (final var rawSegment : trimmed.split("/")) {
final var segment = rawSegment.trim();
if (segment.isEmpty() || ".".equals(segment)) {
continue;
}
if ("..".equals(segment)) {
throw new IllegalStateException("Asset root escapes trusted boundary: " + root);
}
segments.add(segment);
}
return String.join("/", segments);
}
@JsonIgnoreProperties(ignoreUnknown = true)
private record RegistryDocument(
@JsonProperty("assets") List<RegistryAssetDocument> assets) {
}
@JsonIgnoreProperties(ignoreUnknown = true)
private record RegistryAssetDocument(
@JsonProperty("asset_id") int assetId,
@JsonProperty("root") String root,
@JsonProperty("included_in_build") Boolean includedInBuild) {
}
}

View File

@ -5,12 +5,15 @@ import p.studio.compiler.FrontendRegistryService;
import p.studio.compiler.messages.BuildingIssueSink;
import p.studio.compiler.messages.FrontendPhaseContext;
import p.studio.compiler.models.BuilderPipelineContext;
import p.studio.compiler.workspaces.AssetSurfaceContextLoader;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.workspaces.PipelineStage;
import p.studio.utilities.logs.LogAggregator;
@Slf4j
public class FrontendPhasePipelineStage implements PipelineStage {
private final AssetSurfaceContextLoader assetSurfaceContextLoader = new AssetSurfaceContextLoader();
@Override
public BuildingIssueSink run(final BuilderPipelineContext ctx, final LogAggregator logs) {
final var frontedSpec = ctx.resolvedWorkspace.frontendSpec();
@ -27,7 +30,9 @@ public class FrontendPhasePipelineStage implements PipelineStage {
projectTable,
fileTable,
ctx.resolvedWorkspace.stack(),
ctx.resolvedWorkspace.stdlib());
ctx.resolvedWorkspace.stdlib(),
p.studio.compiler.messages.HostAdmissionContext.permissiveDefault(),
assetSurfaceContextLoader.load(ctx.resolvedWorkspace.mainProject().getRootPath()));
final var diagnostics = DiagnosticSink.empty();
final var issues = BuildingIssueSink.empty();
ctx.irBackend = service.get().compile(frontendPhaseContext, diagnostics, logs, issues);

View File

@ -16,8 +16,6 @@ class BackendClaimScopeSpecTest {
"docs/specs/compiler/20. IRBackend to IRVM Lowering Specification.md";
private static final String MATRIX_RELATIVE_PATH =
"docs/specs/compiler/22. Backend Spec-to-Test Conformance Matrix.md";
private static final String DECISION_RELATIVE_PATH =
"docs/compiler/pbs/decisions/SPAWN-YIELD v1 Claim Rescope Decision.md";
@Test
void loweringSpecMustDeclareSpawnYieldOutsideCoreV1ClaimScope() throws IOException {
@ -43,16 +41,6 @@ class BackendClaimScopeSpecTest {
"G20-9.4.3 row must include explicit claim rescope note");
}
@Test
void decisionRecordMustExistForSpawnYieldClaimRescope() throws IOException {
final var decisionPath = locateRepoRoot().resolve(DECISION_RELATIVE_PATH);
assertTrue(Files.isRegularFile(decisionPath),
"claim rescope decision is missing: " + decisionPath);
final var content = Files.readString(decisionPath);
assertTrue(content.contains("Trilha B"),
"decision must record official Track B claim rescope choice");
}
private Optional<String> findRequirementRow(
final List<String> lines,
final String requirementId) {

View File

@ -0,0 +1,49 @@
package p.studio.compiler.workspaces;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class AssetSurfaceContextLoaderTest {
@TempDir
Path tempDir;
@Test
void shouldLoadOnlyIncludedAssetsAsAddressables() throws IOException {
final var registryDir = tempDir.resolve("assets").resolve(".prometeu");
Files.createDirectories(registryDir);
Files.writeString(registryDir.resolve("index.json"), """
{
"schema_version": 1,
"next_asset_id": 4,
"assets": [
{ "asset_id": 3, "root": "ui/atlas2", "included_in_build": true },
{ "asset_id": 7, "root": "ui/one-more-atlas", "included_in_build": false },
{ "asset_id": 8, "root": "ui/sound" }
]
}
""");
final var surface = new AssetSurfaceContextLoader().load(tempDir);
assertEquals(2, surface.assets().size());
assertEquals("assets.ui.atlas2", surface.assets().get(0).address());
assertEquals(3, surface.assets().get(0).assetId());
assertEquals("assets.ui.sound", surface.assets().get(1).address());
assertEquals(8, surface.assets().get(1).assetId());
}
@Test
void shouldReturnEmptySurfaceWhenRegistryIsMissing() {
final var surface = new AssetSurfaceContextLoader().load(tempDir);
assertTrue(surface.assets().isEmpty());
}
}

View File

@ -1,4 +1,114 @@
[ {
"source" : "Assets",
"message" : "8 assets loaded",
"severity" : "SUCCESS",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan diagnostics updated.",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bla",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bbb2",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: Bigode",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: Zelda",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan started",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "8 assets loaded",
"severity" : "SUCCESS",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan diagnostics updated.",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bla",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bbb2",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: Bigode",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: Zelda",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan started",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan diagnostics updated.",
"severity" : "INFO",
@ -2388,114 +2498,4 @@
"message" : "Asset scan diagnostics updated.",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bla",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bbb2",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: Bigode",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan started",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "7 assets loaded",
"severity" : "SUCCESS",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan diagnostics updated.",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bla",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bbb2",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: ui_atlas",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: Bigode",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan started",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "7 assets loaded",
"severity" : "SUCCESS",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Asset scan diagnostics updated.",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: bla",
"severity" : "INFO",
"sticky" : false
}, {
"source" : "Assets",
"message" : "Discovered asset: one-more-atlas",
"severity" : "INFO",
"sticky" : false
} ]

File diff suppressed because it is too large Load Diff

View File

@ -3,14 +3,55 @@ import { Color } from @core:color;
import { Log } from @sdk:log;
import { Input } from @sdk:input;
import { Gfx } from @sdk:gfx;
import { Assets } from @sdk:asset;
declare global loading_handle: int = -1;
declare global loading_status: int = -1;
declare global frame_counter: int = 0;
declare global tile_id: int = 0;
declare const MAX_FRAMES: int = 4;
[Init]
fn init() -> void
{
loading_handle= -1;
loading_status = -1;
frame_counter = 0;
tile_id = 0;
}
[Frame]
fn frame() -> void
{
Gfx.clear(new Color(6577));
if (loading_handle == -1) {
let t = Assets.load(assets.ui.atlas2, 3);
if (t.status != 0) {
Log.error("load failed");
} else {
loading_handle = t.loading_handle;
Log.info("state: loading");
}
} else {
let s = Assets.status(loading_handle);
if (s == 2) {
let commit_status = Assets.commit(loading_handle);
if (commit_status != 0) {
Log.error("commit failed");
}
} else if (s == 3) {
let sprite_status = Gfx.set_sprite(3, 10, 150, 150, 0, 0, true, false, false, 1);
if (sprite_status != 0) {
Log.error("set_sprite failed");
}
} else {
Log.info("state: waiting");
}
}
let touch = Input.touch();
if (touch.button().released())
@ -33,10 +74,8 @@ fn frame() -> void
}
}
Gfx.clear(new Color(6577));
// Gfx.draw_square(touch.x(), touch.y(), 16, 16, new Color(65535), new Color(13271));
let sprite_status = Gfx.set_sprite(0, 0, touch.x() - 16, touch.y() + 8, tile_id, 0, true, true, false, 0);
let sprite_status2 = Gfx.set_sprite(0, 1, touch.x() + 16, touch.y() + 8, tile_id, 0, true, false, false, 0);
Gfx.set_sprite(0, 0, touch.x() - 16, touch.y() + 8, tile_id, 0, true, true, false, 0);
Gfx.set_sprite(0, 1, touch.x() + 16, touch.y() + 8, tile_id, 0, true, false, false, 0);
let a = 10;
let b = 15;