Compare commits

..

12 Commits

Author SHA1 Message Date
c9c7c23b6b
Tiled Parser and Scene Asset-Type Ownership in Assets Workspace
All checks were successful
JaCoCo Coverage #### Project Overview No changes detected, that affect the code coverage. * Line Coverage: 60.75% (15883/26144) * Branch Coverage: 53.46% (5976/11179) * Lines of Code: 26144 * Cyclomatic Complexity: 10272 #### Quality Gates Summary Output truncated.
Test / Build skipped: 11, passed: 559
Intrepid/Prometeu/Studio/pipeline/pr-master This commit looks good
2026-04-18 18:15:26 +01:00
9e128988e1
implements PLN-0055 2026-04-18 18:15:26 +01:00
33fd7c485e
implements PLN-0054 2026-04-18 18:15:26 +01:00
bcc5b413fa
implements PLN-0053 2026-04-18 18:15:26 +01:00
20a23dedb5
tiled parser discussion 2026-04-18 18:15:23 +01:00
81187212aa
initial studies over Tiled@ 2026-04-18 18:13:45 +01:00
cd82cb5cff Merge pull request 'dev/studio-frame-composer-syscall-and-sprite-alignment' (#5) from dev/studio-frame-composer-syscall-and-sprite-alignment into master
All checks were successful
JaCoCo Coverage #### Project Overview No changes detected, that affect the code coverage. * Line Coverage: 60.68% (15277/25176) * Branch Coverage: 53.63% (5781/10779) * Lines of Code: 25176 * Cyclomatic Complexity: 9960 #### Quality Gates Summary Output truncated.
Test / Build skipped: 11, passed: 547
Intrepid/Prometeu/Studio/pipeline/head This commit looks good
Reviewed-on: #5
2026-04-18 16:33:51 +00:00
908cb2b1fe
housekeep
All checks were successful
JaCoCo Coverage #### Project Overview No changes detected, that affect the code coverage. * Line Coverage: 60.68% (15277/25176) * Branch Coverage: 53.63% (5781/10779) * Lines of Code: 25176 * Cyclomatic Complexity: 9960 #### Quality Gates Summary Output truncated.
Test / Build skipped: 11, passed: 547
Intrepid/Prometeu/Studio/pipeline/head This commit looks good
Intrepid/Prometeu/Studio/pipeline/pr-master This commit looks good
2026-04-18 17:27:22 +01:00
1e99553d94
implements PLN-0055 2026-04-18 17:04:35 +01:00
a17dfde481
implements PLN-0054 2026-04-18 17:03:18 +01:00
46728a542f
implements PLN-0053 2026-04-18 17:01:31 +01:00
ef582b6687
Studio Alignment with Runtime FrameComposer Syscalls and Sprite Composition 2026-04-18 16:58:54 +01:00
14 changed files with 370 additions and 52 deletions

View File

@ -1,4 +1,5 @@
{"type":"meta","next_id":{"DSC":29,"AGD":31,"DEC":28,"PLN":56,"LSN":40,"CLSN":1}}
{"type":"meta","next_id":{"DSC":30,"AGD":32,"DEC":28,"PLN":56,"LSN":42,"CLSN":1}}
{"type":"discussion","id":"DSC-0029","status":"done","ticket":"studio-frame-composer-syscall-and-sprite-alignment","title":"Studio Alignment with Runtime FrameComposer Syscalls and Sprite Composition","created_at":"2026-04-18","updated_at":"2026-04-18","tags":["studio","compiler","pbs","stdlib","runtime-alignment","abi","syscall","frame-composer","sprites"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0041","file":"discussion/lessons/DSC-0029-studio-frame-composer-syscall-and-sprite-alignment/LSN-0041-composer-must-own-public-sprite-composition.md","status":"done","created_at":"2026-04-18","updated_at":"2026-04-18"}]}
{"type":"discussion","id":"DSC-0028","status":"open","ticket":"studio-tiled-parser-assets-scene-asset-type","title":"Tiled Parser and Scene Asset-Type Ownership in Assets Workspace","created_at":"2026-04-15","updated_at":"2026-04-17","tags":["studio","assets","scene","tiled","parser","asset-type"],"agendas":[{"id":"AGD-0030","file":"AGD-0030-tiled-parser-and-scene-asset-type.md","status":"accepted","created_at":"2026-04-15","updated_at":"2026-04-17"}],"decisions":[{"id":"DEC-0027","file":"DEC-0027-tiled-parser-and-scene-bank-asset-ownership.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_agenda":"AGD-0030"}],"plans":[{"id":"PLN-0053","file":"PLN-0053-scene-bank-asset-contract-and-studio-metadata-foundations.md","status":"open","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0027"]},{"id":"PLN-0054","file":"PLN-0054-tiled-xml-io-and-scene-bank-file-orchestration.md","status":"open","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0027"]},{"id":"PLN-0055","file":"PLN-0055-assets-workspace-scene-bank-validation-and-acceptance-flow.md","status":"open","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0027"]}],"lessons":[]}
{"type":"discussion","id":"DSC-0027","status":"abandoned","ticket":"studio-scene-workspace","title":"Scene Workspace for SCENE Authoring","created_at":"2026-04-14","updated_at":"2026-04-15","tags":["studio","workspace","scene","tilemap","asset","runtime-alignment"],"agendas":[{"id":"AGD-0029","file":"AGD-0029-studio-scene-workspace.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","_override_reason":"Explicit user request on 2026-04-15 to abandon the accepted agenda and its downstream work."}],"decisions":[{"id":"DEC-0026","file":"DEC-0026-studio-scene-workspace.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","ref_agenda":"AGD-0029","_override_reason":"Explicit user request on 2026-04-15 to abandon the accepted decision and stop using it as normative guidance."}],"plans":[{"id":"PLN-0049","file":"PLN-0049-scene-workspace-spec-and-boundary-propagation.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0026"],"_override_reason":"Explicit user request on 2026-04-15 to abandon all plans derived from DEC-0026."},{"id":"PLN-0050","file":"PLN-0050-scene-workspace-shell-and-project-state-foundations.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0026"],"_override_reason":"Explicit user request on 2026-04-15 to abandon all plans derived from DEC-0026."},{"id":"PLN-0051","file":"PLN-0051-scene-artifact-and-assets-handoff-contract.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0026"],"_override_reason":"Explicit user request on 2026-04-15 to abandon all plans derived from DEC-0026."},{"id":"PLN-0052","file":"PLN-0052-scene-workspace-wave-1-tilemap-editor.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0026"],"_override_reason":"Explicit user request on 2026-04-15 to abandon all plans derived from DEC-0026."}],"lessons":[]}
{"type":"discussion","id":"DSC-0026","status":"done","ticket":"glyph-bank-naming-alignment-with-runtime","title":"Glyph Bank Naming Alignment with Runtime DEC-0006","created_at":"2026-04-10","updated_at":"2026-04-10","tags":["packer","studio","naming","asset-contract","runtime-alignment","glyph-bank"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0040","file":"discussion/lessons/DSC-0026-glyph-bank-naming-alignment-with-runtime/LSN-0040-glyph-bank-artifact-naming-alignment.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}

View File

@ -0,0 +1,70 @@
---
id: LSN-0041
ticket: studio-frame-composer-syscall-and-sprite-alignment
title: Composer Must Own the Public Sprite-Composition Surface
created: 2026-04-18
tags: [studio, compiler, pbs, stdlib, runtime-alignment, abi, syscall, frame-composer, sprites]
---
## Context
The runtime had already moved canonical frame orchestration to `FrameComposer` and published the corresponding public ABI under `composer.*`, but Studio still exposed sprite composition through `@sdk:gfx` and `Gfx.set_sprite`.
That left the repository teaching the wrong owner even though runtime dispatch, architectural lessons, and the public syscall domain had already changed. The migration closed that drift for the sprite path without pulling scene binding, camera control, or scene-bank work into the same ticket.
## Key Decisions
### Public Source Ownership Must Match the Runtime ABI
**What:**
Studio now exposes sprite composition through `@sdk:composer`, with `LowComposer` as the low-level host owner and `Composer` as the public service facade. The old `Gfx.set_sprite` path was removed instead of being kept as compatibility sugar.
**Why:**
Once runtime ownership moved to `FrameComposer`, keeping sprite submission under `gfx.*` would preserve a false mental model and create a dual contract between what the runtime actually owns and what PBS/stdlib teaches.
**Trade-offs:**
The migration required coordinated changes across stdlib resources, specs, frontend conformance, examples, and LSP fixtures. That was more work up front, but it avoided a longer-lived compatibility layer that would have encoded the wrong architecture.
### A Partial Domain Rollout Is Valid If the Boundary Is Explicit
**What:**
This wave shipped only `composer.emit_sprite(...)` in Studio. `bind_scene`, `unbind_scene`, and `set_camera` were explicitly deferred.
**Why:**
The current pipeline only needed sprite composition. Pulling scene/camera rollout into the same thread would have mixed ABI convergence with unfinished scene-work concerns and made the migration harder to execute cleanly.
**Trade-offs:**
The docs had to state clearly that `@sdk:composer` exists now for sprite emission while other composer members remain future work. That is slightly less symmetrical in the short term, but it keeps the implemented contract honest.
### Raw Status Returns Can Be Preserved During Ownership Migration
**What:**
`Composer.emit_sprite(...)` kept the raw `int` status return for this wave instead of introducing a new editorial status shell.
**Why:**
The architectural problem in this discussion was ownership and ABI drift, not status typing. Keeping the return shape stable allowed the migration to focus on the service boundary and public surface without coupling it to a second API redesign.
**Trade-offs:**
The API remains less expressive than a nominal status shell, but the migration stayed narrow and easier to validate end to end.
## Patterns and Algorithms
- Treat runtime service ownership changes as public-source changes, not merely internal lowering rewrites.
- When the runtime removes a legacy public syscall path, prefer removing the matching source-level facade instead of silently retargeting it.
- Split migrations by boundary: first stdlib/spec surface, then compiler/conformance, then examples and fixture cleanup.
- Keep primitive-oriented `@sdk:gfx` coverage intact while moving only frame-composition behavior to `@sdk:composer`.
- Use a negative regression test for the removed API so compatibility leakage is caught explicitly.
## Pitfalls
- Retargeting `Gfx.set_sprite` internally to `composer.emit_sprite` would have looked convenient, but it would have preserved the wrong owner in the public teaching surface.
- A repository sweep that ignores examples and fixtures leaves stale guidance behind even when compiler tests are green.
- Replacing every `@sdk:gfx` example wholesale would be overcorrection; primitive and overlay operations still belong there.
- Updating docs to mention deferred composer members as if they already shipped would create a second kind of contract drift.
## Takeaways
- Public SDK ownership should mirror canonical runtime ownership, not historical implementation leftovers.
- Removing the old API is safer than keeping a compatibility facade that encodes the wrong subsystem boundary.
- A sprite-only `@sdk:composer` wave is acceptable when the deferred scene/camera scope is stated explicitly.
- Ownership migration and status-typing redesign do not need to happen in the same change.

View File

@ -18,7 +18,7 @@ This document defines the normative compile-time model for:
This document is the authority for how the compiler discovers and loads:
- ordinary project modules,
- reserved stdlib modules such as `@sdk:gfx`,
- reserved stdlib modules such as `@sdk:gfx` and `@sdk:composer`,
- compile-time reserved metadata attached to those reserved modules,
- interface-only service facades that wrap host surfaces for source ergonomics,
- and backend-provided compile surfaces consumed by the frontend for symbolic authoring.
@ -211,6 +211,20 @@ Rules:
- `Gfx` becomes visible only through `import`.
- The compiler accepts that import because the selected stdlib environment exposes `@sdk:gfx`.
For sprite composition in stdlib line `1`, the canonical reserved SDK surface is:
```pbs
import { Composer } from @sdk:composer;
let status: int = Composer.emit_sprite(7, 0, 12, 18, 0, 2, false, false, 1);
```
Rules:
- `@sdk:composer` is the canonical sprite-composition module in this wave.
- `@sdk:gfx` remains valid for primitive and overlay-oriented graphics operations.
- `Gfx.set_sprite` is not part of the active stdlib line `1` contract.
## 6. Effective Stdlib of the Build
The root project owns the effective stdlib line of the build.
@ -359,6 +373,26 @@ Rules:
- It contributes compile-time symbols and metadata.
- It does not produce executable bytecode by itself.
Another valid reserved stdlib interface module in stdlib line `1` is the sprite-composition surface:
```pbs
declare host LowComposer {
[Host(module = "composer", name = "emit_sprite", version = 1)]
[Capability(name = "gfx")]
fn emit_sprite(
glyph_id: int,
palette_id: int,
x: int,
y: int,
layer: int,
bank_id: int,
flip_x: bool,
flip_y: bool,
priority: int
) -> int;
}
```
## 10. Compiler Loading of the Stdlib Environment
The compiler loads the stdlib environment from the stdlib line selected by the root project.
@ -433,7 +467,7 @@ Rules:
- `declare host` remains reserved to SDK/toolchain-controlled modules.
- Ordinary user-authored project modules do not declare canonical host bindings directly.
- User code consumes SDK exports through normal imports.
- SDK-facing surfaces such as `Gfx.draw_pixel(...)` are resolved by the compiler against the selected stdlib environment.
- SDK-facing surfaces such as `Gfx.draw_pixel(...)` and `Composer.emit_sprite(...)` are resolved by the compiler against the selected stdlib environment.
- Canonical host identity comes from reserved host-binding metadata such as `[Host(...)]`, not from the source spelling of the imported owner.
## 13. Project Publication and Distribution

View File

@ -61,7 +61,7 @@ as the normative runtime-facing identity.
The host-binding pipeline is:
1. User code imports SDK surfaces such as `Gfx` from `@sdk:gfx`.
1. User code imports SDK surfaces such as `Gfx` from `@sdk:gfx` and `Composer` from `@sdk:composer`.
2. The compiler resolves those surfaces against the selected stdlib line.
3. The compiler maps each host-backed SDK member to a canonical identity `(module, name, version)`.
4. The compiler emits those required canonical identities into the PBX `SYSC` table.
@ -136,6 +136,33 @@ but the PBX-facing declaration is canonical, for example:
("gfx", "draw_pixel", 1)
```
The same rule applies to sprite composition through the composer domain.
For example, the PBS-facing declaration:
```pbs
declare host LowComposer {
[Host(module = "composer", name = "emit_sprite", version = 1)]
[Capability(name = "gfx")]
fn emit_sprite(
glyph_id: int,
palette_id: int,
x: int,
y: int,
layer: int,
bank_id: int,
flip_x: bool,
flip_y: bool,
priority: int
) -> int;
}
```
maps to the canonical runtime identity:
```text
("composer", "emit_sprite", 1)
```
The same rule applies to the low-level asset surface.
For example, the PBS-facing declaration:
@ -396,7 +423,7 @@ Diagnostics should identify the failing canonical identity whenever available.
Rules:
- the loader does not resolve source imports such as `@sdk:gfx`,
- the loader does not resolve source imports such as `@sdk:gfx` or `@sdk:composer`,
- the loader only consumes canonical host-binding metadata emitted into the PBX,
- `stdlib` affects the loader indirectly through what the compiler emitted,
- the loader must not attempt to reconstruct SDK/module semantics from the PBX.

View File

@ -56,6 +56,9 @@ prometeu-compiler/frontends/prometeu-frontend-pbs/
stdlib/
1/
sdk/
composer/
main.pbs
mod.barrel
gfx/
main.pbs
mod.barrel
@ -75,7 +78,7 @@ Interpretation:
- `stdlib/1` selects stdlib major line `1`,
- `sdk` and `core` are reserved project spaces,
- `gfx`, `audio`, `math` are module paths within those reserved spaces,
- `composer`, `gfx`, `audio`, `math` are module paths within those reserved spaces,
- `main.pbs` and `mod.barrel` form the interface module contents.
## 5. Logical Mapping
@ -85,6 +88,7 @@ The compiler must treat the physical layout as a logical stdlib environment.
Required mapping:
- `stdlib/<N>/sdk/gfx` -> `@sdk:gfx`
- `stdlib/<N>/sdk/composer` -> `@sdk:composer`
- `stdlib/<N>/sdk/asset` -> `@sdk:asset`
- `stdlib/<N>/sdk/audio` -> `@sdk:audio`
- `stdlib/<N>/core/math` -> `@core:math`
@ -94,7 +98,7 @@ Rules:
- physical storage is an implementation detail,
- logical module identity is authoritative,
- callers of the resolver should work with logical addresses such as `@sdk:gfx`, not resource paths.
- callers of the resolver should work with logical addresses such as `@sdk:gfx` and `@sdk:composer`, not resource paths.
## 6. Module File Convention
@ -145,6 +149,7 @@ Responsibility:
Minimum operations:
- `resolve(@sdk:gfx)`
- `resolve(@sdk:composer)`
- `resolve(@core:math/vector)`
### 7.3 `StdlibModuleSource`
@ -219,6 +224,26 @@ declare host LowAssets {
}
```
or:
```pbs
declare host LowComposer {
[Host(module = "composer", name = "emit_sprite", version = 1)]
[Capability(name = "gfx")]
fn emit_sprite(
glyph_id: int,
palette_id: int,
x: int,
y: int,
layer: int,
bank_id: int,
flip_x: bool,
flip_y: bool,
priority: int
) -> int;
}
```
Rules:
- the parser reads the attribute as part of the interface module source,
@ -226,6 +251,11 @@ Rules:
- the compiler stores the extracted metadata in its interface graph,
- the raw attribute surface is not treated as a runtime object,
- later lowering stages may consume the extracted metadata to produce PBX host-binding declarations.
- stdlib line `1` MUST expose the sprite-composition interface module at `@sdk:composer`,
- that module MUST declare `LowComposer` and public service `Composer`,
- that module MUST use runtime module `composer` and capability `gfx`,
- that module MUST expose `emit_sprite(glyph_id: int, palette_id: int, x: int, y: int, layer: int, bank_id: int, flip_x: bool, flip_y: bool, priority: int) -> int`,
- stdlib line `1` MUST NOT expose `Gfx.set_sprite`,
- stdlib line `1` MUST expose the low-level asset interface module at `@sdk:asset`,
- that module MUST declare `LowAssets`,
- that module MUST use runtime module `asset` and capability `asset`,

View File

@ -164,6 +164,20 @@ Rules:
- PBS symbolic asset references (`Addressable`) are not themselves required to be stdlib-imported modules;
- backend-owned frontend surface contracts MAY supply symbolic values that later lower into stdlib host-backed APIs.
### 12.1 Current line `1` sprite-composition rule
For stdlib line `1`, sprite composition is owned by `@sdk:composer`, not by `@sdk:gfx`.
Rules:
- `@sdk:composer` MUST expose a low-level host owner `LowComposer`.
- `@sdk:composer` MUST expose a public service facade `Composer`.
- `Composer.emit_sprite(...)` MUST lower through canonical host identity `("composer", "emit_sprite", 1)`.
- `Composer.emit_sprite(...)` MUST return a raw `int` in this wave.
- `@sdk:gfx` MAY continue to expose primitive and overlay-oriented operations.
- `@sdk:gfx` MUST NOT expose `set_sprite`.
- `bind_scene`, `unbind_scene`, and `set_camera` are deferred and are not required members of `@sdk:composer` in this wave.
## 13. Stdlib Contract Expectations for Builtin MVP
For the current builtin MVP, stdlib should be able to expose at least:

View File

@ -0,0 +1,33 @@
declare host LowComposer {
[Host(module = "composer", name = "emit_sprite", version = 1)]
[Capability(name = "gfx")]
fn emit_sprite(
glyph_id: int,
palette_id: int,
x: int,
y: int,
layer: int,
bank_id: int,
flip_x: bool,
flip_y: bool,
priority: int
) -> int;
}
declare service Composer
{
fn emit_sprite(
glyph_id: int,
palette_id: int,
x: int,
y: int,
layer: int,
bank_id: int,
flip_x: bool,
flip_y: bool,
priority: int
) -> int
{
return LowComposer.emit_sprite(glyph_id, palette_id, x, y, layer, bank_id, flip_x, flip_y, priority);
}
}

View File

@ -0,0 +1,2 @@
mod host LowComposer;
pub service Composer;

View File

@ -25,21 +25,6 @@ declare host LowGfx {
[Capability(name = "gfx")]
fn draw_square(x: int, y: int, w: int, h: int, border_color: Color, fill_color: Color) -> void;
[Host(module = "gfx", name = "set_sprite", version = 1)]
[Capability(name = "gfx")]
fn set_sprite(
bank_id: int,
index: int,
x: int,
y: int,
tile_id: int,
palette_id: int,
active: bool,
flip_x: bool,
flip_y: bool,
priority: int
) -> int;
[Host(module = "gfx", name = "draw_text", version = 1)]
[Capability(name = "gfx")]
fn draw_text(x: int, y: int, message: str, color: Color) -> void;
@ -81,22 +66,6 @@ declare service Gfx
LowGfx.draw_square(x, y, w, h, border_color, fill_color);
}
fn set_sprite(
bank_id: int,
index: int,
x: int,
y: int,
tile_id: int,
palette_id: int,
active: bool,
flip_x: bool,
flip_y: bool,
priority: int
) -> int
{
return LowGfx.set_sprite(bank_id, index, x, y, tile_id, palette_id, active, flip_x, flip_y, priority);
}
fn draw_text(x: int, y: int, message: str, color: Color) -> void
{
LowGfx.draw_text(x, y, message, color);

View File

@ -85,13 +85,14 @@ class PbsGateUSdkInterfaceConformanceTest {
tempDir.resolve("gate-u-reserved-import-positive"),
"""
import { Color } from @core:color;
import { Composer } from @sdk:composer;
import { Gfx } from @sdk:gfx;
import { Input, InputPad, InputButton } from @sdk:input;
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: Assets, log: Log) -> void;
fn render(composer: Composer, gfx: Gfx, color: Color, input: Input, pad: InputPad, button: InputButton, assets: Assets, log: Log) -> void;
}
""",
"pub contract Renderer;",
@ -115,6 +116,11 @@ class PbsGateUSdkInterfaceConformanceTest {
&& h.abiModule().equals("gfx")
&& h.abiMethod().equals("clear")
&& h.abiVersion() == 1));
assertTrue(positive.irBackend().getReservedMetadata().hostMethodBindings().stream()
.anyMatch(h -> h.ownerName().equals("LowComposer")
&& h.abiModule().equals("composer")
&& h.abiMethod().equals("emit_sprite")
&& h.abiVersion() == 1));
assertTrue(positive.irBackend().getReservedMetadata().hostMethodBindings().stream()
.anyMatch(h -> h.ownerName().equals("LowLog")
&& h.abiModule().equals("log")

View File

@ -84,4 +84,71 @@ class InterfaceModuleLoaderTest {
"pub host LowAssets;",
fileTable.get(module.barrelFiles().getFirst().fileId()).readUtf8().orElseThrow());
}
@Test
void shouldLoadComposerStdlibModuleWithPublicServiceBarrel() {
final var projectTable = new ProjectTable();
final var projectId = projectTable.register(ProjectDescriptor.builder()
.name("app")
.version("1.0.0")
.rootPath(Path.of("/tmp/app"))
.sourceRoots(ReadOnlyList.wrap(java.util.List.of(Path.of("/tmp/app/src"))))
.build());
final var fileTable = new FileTable(1);
final var diagnostics = DiagnosticSink.empty();
final var moduleSource = new StdlibModuleSource(
"sdk",
ReadOnlyList.wrap(java.util.List.of("composer")),
ReadOnlyList.wrap(java.util.List.of(new StdlibModuleSource.SourceFile(
"main.pbs",
"""
declare host LowComposer {
[Host(module = "composer", name = "emit_sprite", version = 1)]
[Capability(name = "gfx")]
fn emit_sprite(
glyph_id: int,
palette_id: int,
x: int,
y: int,
layer: int,
bank_id: int,
flip_x: bool,
flip_y: bool,
priority: int
) -> int;
}
declare service Composer {
fn emit_sprite(
glyph_id: int,
palette_id: int,
x: int,
y: int,
layer: int,
bank_id: int,
flip_x: bool,
flip_y: bool,
priority: int
) -> int {
return LowComposer.emit_sprite(glyph_id, palette_id, x, y, layer, bank_id, flip_x, flip_y, priority);
}
}
"""))),
"""
mod host LowComposer;
pub service Composer;
""");
final var module = new InterfaceModuleLoader().load(moduleSource, projectId, fileTable, diagnostics);
assertTrue(diagnostics.isEmpty(), diagnostics.stream().map(d -> d.getCode() + ":" + d.getMessage()).toList().toString());
assertEquals(1, module.sourceFiles().size());
assertEquals(1, module.barrelFiles().size());
assertEquals(
"""
mod host LowComposer;
pub service Composer;
""".strip(),
fileTable.get(module.barrelFiles().getFirst().fileId()).readUtf8().orElseThrow().strip());
}
}

View File

@ -1006,8 +1006,72 @@ class PBSFrontendPhaseServiceTest {
}
@Test
void shouldLowerSdkGfxSetSpriteFacadeUsingBankIdContract() throws IOException {
final var projectRoot = tempDir.resolve("project-bootstrap-sdk-gfx-set-sprite");
void shouldLowerSdkComposerEmitSpriteFacadeUsingCanonicalComposerContract() throws IOException {
final var projectRoot = tempDir.resolve("project-bootstrap-sdk-composer-emit-sprite");
final var sourceRoot = projectRoot.resolve("src");
final var modulePath = sourceRoot.resolve("app");
Files.createDirectories(modulePath);
final var sourceFile = modulePath.resolve("source.pbs");
final var modBarrel = modulePath.resolve("mod.barrel");
Files.writeString(sourceFile, """
import { Composer } from @sdk:composer;
fn render() -> int
{
return Composer.emit_sprite(7, 3, 12, 18, 0, 2, false, true, 1);
}
[Frame]
fn frame() -> void
{
render();
return;
}
""");
Files.writeString(modBarrel, """
pub fn render() -> int;
pub fn frame() -> void;
""");
final var projectTable = new ProjectTable();
final var fileTable = new FileTable(1);
final var projectId = projectTable.register(ProjectDescriptor.builder()
.rootPath(projectRoot)
.name("app")
.version("1.0.0")
.sourceRoots(ReadOnlyList.wrap(List.of(sourceRoot)))
.build());
registerFile(projectId, projectRoot, 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_EXEC_LOWERING_UNRESOLVED_CALLEE.name())));
assertTrue(irBackend.getExecutableFunctions().stream().anyMatch(function -> "frame".equals(function.callableName())));
assertTrue(irBackend.getReservedMetadata().hostMethodBindings().stream()
.anyMatch(h -> h.ownerName().equals("LowComposer")
&& h.sourceMethodName().equals("emit_sprite")
&& h.abiModule().equals("composer")
&& h.abiMethod().equals("emit_sprite")));
}
@Test
void shouldRejectLegacyGfxSetSpriteAfterComposerMigration() throws IOException {
final var projectRoot = tempDir.resolve("project-bootstrap-sdk-gfx-set-sprite-removed");
final var sourceRoot = projectRoot.resolve("src");
final var modulePath = sourceRoot.resolve("app");
Files.createDirectories(modulePath);
@ -1059,10 +1123,10 @@ class PBSFrontendPhaseServiceTest {
LogAggregator.empty(),
BuildingIssueSink.empty());
assertTrue(diagnostics.stream().noneMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_EXEC_LOWERING_UNRESOLVED_CALLEE.name())));
assertTrue(irBackend.getExecutableFunctions().stream().anyMatch(function -> "frame".equals(function.callableName())));
assertTrue(irBackend.getReservedMetadata().hostMethodBindings().stream()
assertTrue(diagnostics.stream().anyMatch(d ->
d.getCode().equals(PbsSemanticsErrors.E_SEM_INVALID_MEMBER_ACCESS.name())
|| d.getCode().equals(PbsSemanticsErrors.E_SEM_EXEC_LOWERING_UNRESOLVED_CALLEE.name())));
assertFalse(irBackend.getReservedMetadata().hostMethodBindings().stream()
.anyMatch(h -> h.ownerName().equals("LowGfx") && h.sourceMethodName().equals("set_sprite")));
}

View File

@ -55,10 +55,10 @@ final class LspServiceImplTest {
""";
private static final String SDK_IMPORT_SOURCE = """
import { Gfx } from @sdk:gfx;
import { Composer } from @sdk:composer;
fn main() -> void {
Gfx.clear();
Composer.emit_sprite(1, 0, 0, 0, 0, 0, false, false, 0);
}
""";
@ -180,7 +180,7 @@ final class LspServiceImplTest {
final var analysis = service.analyzeDocument(new LspAnalyzeDocumentRequest(mainFile));
assertEquals(List.of("pbs-service", "pbs-service"), semanticKeysForLexeme(analysis, SDK_IMPORT_SOURCE, "Gfx"));
assertEquals(List.of("pbs-service", "pbs-service"), semanticKeysForLexeme(analysis, SDK_IMPORT_SOURCE, "Composer"));
}
@Test

View File

@ -2,6 +2,7 @@ import { Color } from @core:color;
import { Log } from @sdk:log;
import { Input } from @sdk:input;
import { Composer } from @sdk:composer;
import { Gfx } from @sdk:gfx;
import { Assets } from @sdk:asset;
@ -41,9 +42,9 @@ fn frame() -> void
Log.failure("commit failed");
}
} else if (s == 3) {
let sprite_status : int = Gfx.set_sprite(3, 10, 150, 150, 0, 0, true, false, false, 1);
let sprite_status : int = Composer.emit_sprite(0, 0, 150, 150, 1, 3, false, false, 1);
if (sprite_status != 0) {
Log.failure("set_sprite failed");
Log.failure("emit_sprite failed");
}
} else {
Log.info("state: waiting");
@ -74,8 +75,8 @@ fn frame() -> void
}
}
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);
Composer.emit_sprite(tile_id, 0, touch.x() - 16, touch.y() + 8, 0, 0, true, false, 0);
Composer.emit_sprite(tile_id, 0, touch.x() + 16, touch.y() + 8, 0, 1, false, false, 0);
let a : int = 10;
let b : int = 15;