# Host ABI and Syscalls Domain: host ABI structure Function: normative This chapter defines the structural ABI between the PVM and the host environment. It focuses on: - syscall identity; - load-time resolution; - instruction-level call semantics; - metadata shape; - argument and return-slot contract. Operational policies such as capabilities, fault classes, determinism, GC interaction, budgeting, and blocking are split into a companion chapter. Per `DEC-0009`, debug tooling and certification are host-owned concerns. This chapter therefore excludes general-purpose guest-visible debug syscalls from the canonical public ABI. ## 1 Design Principles The syscall ABI follows these rules: 1. **Stack-based ABI**: arguments and return values are passed through VM slots. 2. **Canonical identity**: host services are named by stable `(module, name, version)`. 3. **Load-time resolution**: symbolic host bindings are resolved before execution. 4. **Metadata-driven execution**: arity and result shape come from syscall metadata. 5. **Not first-class**: syscalls are callable but not ordinary function values. 6. **Status-first operational policy**: across syscall domains, operationally observable failure must surface through explicit `status` values, while only structural violations fault as `Trap` and only internal invariant breaks escalate as `Panic`. ## 2 Canonical Syscall Identity Syscalls are identified by a canonical triple: ``` (module, name, version) ``` Example: ``` ("gfx2d", "draw_line", 1) ("audio", "play", 2) ("composer", "emit_sprite", 1) ``` This identity is: - language-independent; - toolchain-stable; - used for linking and capability gating. Input queries are VM-owned intrinsic calls in v1 and are outside the syscall identity space. Cartridge boot is also outside the guest syscall identity space. A cartridge, Shell app, or other userland guest must not declare or call `system.run_cart`. Direct cartridge boot is a host/system boot-profile entrypoint, not an app-callable syscall. Syscall identity is transport, not the conceptual boundary for runtime profiles. `Game` and `System` may both use syscall transport internally or at their ABI edge, but that does not make their public authoring surfaces the same ABI. `System` APIs, when exposed to PBS or other guest languages, belong to a dedicated `System` profile ABI and stdlib/framework. ## 3 Syscall Resolution The host maintains a registry: ``` (module, name, version) -> syscall_id ``` At load time: 1. the cartridge declares required syscalls by canonical identity; 2. bytecode encodes host-backed call sites as `HOSTCALL `; 3. the loader validates and resolves those identities; 4. the loader rewrites `HOSTCALL ` into `SYSCALL `; 5. the executable image uses only numeric syscall ids at runtime. Raw `SYSCALL ` is not valid in a PBX pre-load artifact and must be rejected there. ## 4 Syscall Instruction Semantics Pre-load artifact form: ``` HOSTCALL ``` Final executable form: ``` SYSCALL ``` Where: - `` indexes the program-declared syscall table; - `` is the final numeric host syscall id. Execution steps: 1. the VM looks up syscall metadata by ``; 2. the VM ensures the argument contract is satisfiable; 3. the syscall executes in the host environment; 4. the syscall produces exactly the declared `ret_slots`. The execution result must respect the canonical runtime boundary defined with [`16a-syscall-policies.md`](16a-syscall-policies.md): - successful or operationally rejected execution returns values in the declared stack shape; - `Trap` is reserved for structural ABI misuse or invalid call shape; - `Panic` is reserved for runtime/host invariant failure. ## 5 Syscall Metadata Table Each syscall is defined by metadata. Conceptual structure: ``` SyscallMeta { id: u32 module: string name: string version: u16 arg_slots: u8 ret_slots: u8 capability: CapabilityId may_allocate: bool cost_hint: u32 } ``` Fields: | Field | Description | | -------------- | ------------------------------------------------ | | `id` | Unique numeric syscall identifier | | `module` | Canonical module name | | `name` | Canonical syscall name | | `version` | ABI version of the syscall | | `arg_slots` | Number of input stack slots | | `ret_slots` | Number of return stack slots | | `capability` | Required capability | | `may_allocate` | Whether the syscall may allocate VM heap objects | | `cost_hint` | Expected cycle cost | The loader uses this table to resolve identities and validate declared ABI shape. ## 6 Arguments and Return Values Syscalls use the same slot-oriented argument/return philosophy as ordinary calls. ### Argument passing Arguments are pushed before the syscall. Example: ``` push a push b SYSCALL X ``` ### Return values After execution, the syscall leaves exactly `ret_slots` values on the stack. Composite results use multiple stack slots rather than implicit hidden structures. Return shape must also follow the transversal status-first policy in [`16a-syscall-policies.md`](16a-syscall-policies.md): - when a syscall exposes operational failure, `status:int` is the canonical first return slot; - operations with observable operational failure paths must expose that explicit `status:int` return; - operations with no real operational error path may remain `void` (`ret_slots = 0`); - if extra payload is returned together with `status`, the payload must follow the leading status slot in the declared stack shape; - stack shape remains strict in both cases and must match syscall metadata exactly. ### MEMCARD game surface (`mem`, v1) The game memcard profile uses module `mem` with status-first return shapes. `mem` is a domain layer backed by runtime `fs` (it is not a separate storage backend). The `mem` module is a game profile surface. It is not the public storage model for `System` profile apps. Durable memcard data is keyed by `app_id` according to the save-memory contract. Runtime handles, open file tables, pending lifecycle delivery, and other mutable VM-facing syscall context are scoped to the owning VM session and MUST NOT be used as durable app identity. Canonical operations in v1 are: - `mem.slot_count() -> (status, count)` - `mem.slot_stat(slot) -> (status, state, used_bytes, generation, checksum)` - `mem.slot_read(slot, offset, max_bytes) -> (status, payload_hex, bytes_read)` - `mem.slot_write(slot, offset, payload_hex) -> (status, bytes_written)` - `mem.slot_commit(slot) -> status` - `mem.slot_clear(slot) -> status` Semantics and domain status catalog are defined by [`08-save-memory-and-memcard.md`](08-save-memory-and-memcard.md). ### Asset surface (`asset`, v1) The asset runtime surface also follows the status-first ABI shape. Canonical operations in v1 are: - `asset.load(asset_id, slot) -> (status, handle)` - `asset.status(handle) -> status` - `asset.commit(handle) -> status` - `asset.cancel(handle) -> status` - `asset.backlog_info() -> (status, pending_count, active_handle, active_asset_id, active_bank_type, active_slot, active_progress)` - `asset.backlog_position(handle) -> (status, state, position, progress)` - `asset.backlog_move(handle, new_position) -> status` - `asset.backlog_promote(handle) -> status` - `asset.backlog_demote(handle) -> status` - `asset.target_status(bank_type, slot) -> (status, asset_id, handle, state, position, progress)` For `asset.load`: - `asset_id` is a signed 32-bit runtime identity; - `slot` is the target slot index; - bank kind is resolved from `asset_table` by `asset_id`, not supplied by the caller. Asset handles represent stable bank slot targets. A handle can be queried even when its slot has no resident asset or active request. Internally the handle state separates resident `slot_state` from current `request_state`. The asset backlog is keyed by `bank_type/slot`. New requests for the same target supersede older requests for that target. `superseded` is an operational status, not a structural trap. `asset.backlog_promote(handle)` and `asset.backlog_demote(handle)` are convenience operations over backlog movement. They do not introduce a second ordering model. ### Bank diagnostics surface (`bank`, v1) `DEC-0009` narrows the public bank contract: - `bank.info(bank_type) -> (used_slots, total_slots)` is the only surviving public bank diagnostic shape in v1; - `bank.info` exists only as a cheap deterministic operational summary aligned with the slot-first bank telemetry contract from [`15-asset-management.md`](15-asset-management.md); - `bank.slot_info` is host/debug tooling surface and is not part of the canonical public guest ABI; - JSON-on-the-wire bank inspection payloads are not valid public ABI; - `bank.info` returns stack values, not textual structured payloads. ### Render surfaces (`composer`, `gfx2d`, `gfxui`, v1) `DEC-0030` splits logical render production from render implementation. Public render syscalls mutate app-mode-specific domain buffers. They do not directly publish a framebuffer and do not call host presentation APIs. The canonical flow is: ```text domain buffers during logical frame -> RenderManager closes buffers -> RenderSubmission snapshot -> render surface / implementation consumes submission -> RGBA8888 surface publication ``` `RenderSubmission` is a typed closed snapshot containing `frame_id`, `app_mode`, and exactly one packet: `Game2DFramePacket` for `AppMode::Game` or `ShellUiFramePacket` for `AppMode::Shell`. Producers may mutate domain buffers before closure only. Once `RenderManager` closes a submission, producers MUST NOT mutate it. Backpressure is latest-complete-submission-wins; the runtime MUST NOT expose an unbounded submission queue as part of the ABI. Render API and syscall names MUST NOT encode RGB565 or any other pixel format suffix. #### Game 2D primitive surface (`gfx2d`, v1) The `gfx2d` module is a Game profile primitive surface. It is limited to Game 2D primitive commands and does not own scene, camera, sprites, HUD, or frame orchestration. Canonical operations in v1 include: - `gfx2d.clear(color) -> void` - `gfx2d.fill_rect(x, y, w, h, color) -> void` - `gfx2d.draw_line(x0, y0, x1, y1, color) -> void` - `gfx2d.draw_circle(x, y, radius, color) -> void` - `gfx2d.draw_disc(x, y, radius, color) -> void` - `gfx2d.draw_square(x, y, size, color) -> void` - `gfx2d.draw_text(x, y, text, color) -> void` #### Shell UI primitive surface (`gfxui`, v1) The `gfxui` module is a Shell profile primitive surface. It contains Shell UI primitive commands only. Widget and layout policy belongs in Shell/UI code or stdlib code, not in the host/render-surface implementation. Canonical operations in v1 include primitive shapes equivalent to the subset needed by Shell UI: - `gfxui.clear(color) -> void` - `gfxui.fill_rect(x, y, w, h, color) -> void` - `gfxui.draw_line(x0, y0, x1, y1, color) -> void` - `gfxui.draw_circle(x, y, radius, color) -> void` - `gfxui.draw_disc(x, y, radius, color) -> void` - `gfxui.draw_square(x, y, size, color) -> void` - `gfxui.draw_text(x, y, text, color) -> void` Rules: - `color` arguments are packed RGBA8888 values using RGBA channel order. - RGB565 raw values are not a public ABI contract. - `Gfx*565` syscall names and `gfx*.*_565` canonical identities are not valid current ABI. - The runtime framebuffer exposed across the host boundary is RGBA8888, not `u16` RGB565. - The old primitive `gfx.*` ABI is not the canonical render domain. PBS stdlibs may expose ergonomic high-level names, but generated host bindings must map to `gfx2d.*` in Game mode or `gfxui.*` in Shell mode. - Render packets and public render syscalls MUST NOT expose fade fields or fade controls. ### Composition surface (`composer`, v1) The canonical frame-orchestration public ABI uses module `composer`. The `composer` module is a game profile surface. It is not inherited by `System` profile apps as their public UI/composition API. `composer` owns Game 2D high-level frame composition: scene binding, camera, sprites, HUD, and Game 2D frame orchestration. It closes into a `Game2DFramePacket` through `RenderManager`; it does not publish or present a framebuffer directly. Canonical operations in v1 are: - `composer.bind_scene(bank_id) -> (status)` - `composer.unbind_scene() -> (status)` - `composer.set_camera(x, y) -> void` - `composer.emit_sprite(glyph_id, palette_id, x, y, layer, bank_id, flip_x, flip_y, priority) -> (status)` For mutating composer operations: - `status` is a `ComposerOpStatus` value; - `bind_scene`, `unbind_scene`, and `emit_sprite` are status-returning; - `set_camera` remains `void` in v1; - no caller-provided sprite index or `active` flag is part of the canonical contract. - no fade level, fade color, or transition control is part of the canonical composer ABI. Fatal boundary clarification for `bind_scene`: - `bind_scene` remains status-returning for ordinary orchestration-domain rejection such as unresolved target scene slot; - missing glyph dependencies inside a resident scene are not accepted as passive `ComposerOpStatus` outcomes; - if scene activation or later composition detects an unresolved scene glyph dependency, runtime MUST escalate that condition as a fatal machine error with clear logging. ## 7 Syscalls as Callable Entities (Not First-Class) Syscalls behave like call sites, not like first-class guest values. This means: - syscalls can be invoked; - syscalls cannot be stored in variables; - syscalls cannot be passed as function values; - syscalls cannot be returned as closures. Only user-defined functions and closures are first-class. ## 8 Relationship to Other Specs - [`16a-syscall-policies.md`](16a-syscall-policies.md) defines operational policies layered on top of this ABI. - [`15-asset-management.md`](15-asset-management.md) defines asset-domain semantics behind some syscall families. - [`08-save-memory-and-memcard.md`](08-save-memory-and-memcard.md) defines persistent save-domain semantics. - [`02a-vm-values-and-calling-convention.md`](02a-vm-values-and-calling-convention.md) defines the slot/call philosophy reused by the syscall ABI.