prometeu-runtime/docs/specs/runtime/16-host-abi-and-syscalls.md
2026-05-23 19:15:14 +01:00

10 KiB

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:

("gfx", "present", 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.

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 <sysc_index>;
  3. the loader validates and resolves those identities;
  4. the loader rewrites HOSTCALL <sysc_index> into SYSCALL <id>;
  5. the executable image uses only numeric syscall ids at runtime.

Raw SYSCALL <id> is not valid in a PBX pre-load artifact and must be rejected there.

4 Syscall Instruction Semantics

Pre-load artifact form:

HOSTCALL <sysc_index>

Final executable form:

SYSCALL <id>

Where:

  • <sysc_index> indexes the program-declared syscall table;
  • <id> is the final numeric host syscall id.

Execution steps:

  1. the VM looks up syscall metadata by <id>;
  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:

  • 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:

  • 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.

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.

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

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.

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;
  • 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.

GFX surface (gfx, v1)

The public GFX ABI uses format-neutral operation names. API and syscall names MUST NOT encode RGB565 or any other pixel format suffix.

Canonical operations in v1 include:

  • gfx.clear(color) -> void
  • gfx.fill_rect(x, y, w, h, color) -> void
  • gfx.draw_line(x0, y0, x1, y1, color) -> void
  • gfx.draw_circle(x, y, radius, color) -> void
  • gfx.draw_disc(x, y, radius, color) -> void
  • gfx.draw_square(x, y, size, color) -> void
  • gfx.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.

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.

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.

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