This file pins the interface between the two tracks so they move independently:
- Compiler track (in the tish repo
/Users/a_/Projects/tish/tish): theGbaemit mode, the portable runtime facadetishlang_runtime_gba, typed Rust-scalar lowering, typed externs, theasset:import scheme, and async emission. - Framework track (this repo):
tish-agbbindings,tish-gba-game-engine, macros, the host asset importer, and the example games.
Any change to the items below is a breaking change and must be updated in lockstep
on both sides. Committed .d.tish files + a CI drift check enforce this.
Status legend: β locked & proven in P0 Β· π· locked by design Β· π lands in a later phase.
The Gba emit mode generates a no_std binary crate:
#![no_std]
#![no_main]
extern crate alloc;
use tishlang_runtime::Arc; // = alloc::rc::Rc<T> (facade alias) β absorbs Arc::from/clone
// ...alloc imports, prelude `use tishlang_runtime::{...}`...
fn run() -> Result<(), Box<dyn core::error::Error>> { /* program body */ } // π P5: async fn
#[agb::entry]
fn agb_main(gba: agb::Gba) -> ! {
tishlang_runtime::gba::init(gba); // stash peripherals for binding crates
let _ = run(); // π P5: tishlang_runtime::gba::block_on(run())
tishlang_runtime::gba::halt()
}- Runtime crate name: the Cargo dependency is
tishlang_runtime = { package = "tishlang_runtime_gba", path = ... }. The rename makes every emitted absolutetishlang_runtime::β¦path resolve to the GBA facade with no codegen edits. - Panic/halt:
gba::halt() -> !loops onagb::halt().
Name-for-name, everything codegen.rs emits in its use tishlang_runtime::{...}
list: Value, VmRef, ObjectMap, ObjectData, PropMap, TishError, the
math/json/uri/string/array/object/number/symbol/collection/typedarray helpers, and
console_log & co (β agb::println!). Plus:
pub type Arc<T> = alloc::rc::Rc<T>;pub type Fixed = agb_fixnum::Num<i32, 8>;(typed-numeric target type, Β§5)pub mod gba(Β§4).- Capped-out capabilities (
fetch/serve/fs/http/timers/ws/tty/pty) arecompile_error!stubs so missing features read as "not available on GBA".
Portable Value core (proven P0a):
Number(f64) | Str(Rc<str>) | Bool | Null | Array(Rc<RefCell<Vec<Value>>>) | Object(Rc<RefCell<ObjectMap>>) | Function(Rc<dyn Fn(&[Value]) -> Value>) | Promise(Rc<dyn TishPromise>) | Opaque(Rc<dyn TishOpaque>). Single-threaded Rc
(NOT Arc; send-values OFF). ObjectMap hashes with
rustc_hash::FxBuildHasher β NOT foldhash/ahash (both need atomics; see
docs/findings/P0-findings.md).
Current implementation: exports are hand-written in Rust and declared in
crates/*/tish.d.tish. There is no #[tish_export] macro or compiler bindgen
scan in this tree today β the .d.tish files are the typed ABI contract the
tish compiler reads at build time.
- Binding crates (
tish-agb,tish-gba-game-engine) expose idiomatic Rust fns plus matchingdeclare fnentries intish.d.tish. - Classifiable param/return types at the ABI boundary:
i8 u8 i16 u16 i32 u32 bool,Fixed,&str/String,(),Option<T>of those, andVector2D<Fixed>(destructured to a(x: Fixed, y: Fixed)scalar pair). Anything else falls back to the explicitfn(args: &[Value]) -> Valueconvention, which crates MAY still hand-write where dynamic data is the point (e.g.spawn(componentConfigs),defineComponent(def)). - Future direction: a
#[tish_export]marker + compiler bindgen that auto-generates the.d.tishdeclaration and boxed shim remains compatible with this contract but is not implemented yet.
init(gba: agb::Gba)β stash peripherals (single-core,critical-sectionRefCells). β shape proven.halt() -> !. β- Peripheral accessors for binding crates (graphics/input/mixer/timers/save/rng).
- Hooks:
hooks::register_pre_commit(fn)βtish-agb's frame driver andtish-gba-game-engine's pipeline install here; run once per frame beforecommit(). - Executor π P5:
block_on(fut), tish-levelspawn/cancel, andWakeCondition { Ready, NextFrame, AtFrame(u32), Buttons(u16), ChannelDone, Flag(FlagId) }. All wakes at frame boundaries in spawn order β deterministic.
- Annotations may name concrete Rust scalars:
i8 u8 i16 u16 i32 u32 boolandfixed(βFixed=agb_fixnum::Num<i32,8>). Explicit: number=f64(soft-float; exact JS semantics). - Unannotated
numberin Gba mode: integral-inferred βi32, fractional βfixed. Value::Numberstaysf64on the dynamic path (losslessfixedβf64boxing; JSON/formatting unchanged).
Phase 1 (P2βP4): synchronous frame loop; await in Gba mode is a compile error
(not a silent sync fallback). Phase 2 (P5): tish async fn β Rust async fn,
await β .await, Value::Promise(Rc<dyn TishPromise>) bridges a
Pin<Box<dyn Future<Output=Value>>>. Framework awaitables: video.frame(),
time.waitFrames(n), input.buttonPress(mask), engine tween/dialogue/walkTo.
import { player } from "asset:gfx/sprites.aseprite" β compile-time file check;
Gba codegen emits include_aseprite! / include_background_gfx! / include_wav!
into the generated crate and registers each imported name as an i32 handle
with the binding crate's asset table before run(). Aseprite tags are animation
handles. The build step copies the project assets/ tree into the build dir
(mtime-diffed) so proc-macro paths resolve.
tish-gba-game-engine uses a custom SoA/archetype store, not hecs (hecs's spin +
foldhash need atomics unavailable on thumbv4t). Component set is closed (Β§ engine).
.cargo/config.toml sets thumbv4t-none-eabi as the default target, so plain
workspace members build for GBA. Host-only tooling must NOT be a plain member:
tish-agb-macros/tish-gba-game-engine-macrosareproc-macrocrates β cargo builds them for the host automatically. β safe as members.- The host LDtk importer (
tish-gba-assets) is consumed only as a[build-dependencies]entry of example games β cargo builds it for the host. It must live outside the default GBA members (its owntools/workspace, or[patch]/path build-dep) socargo buildnever tries to cross-compile it.
Until those crates exist, the workspace member list stays minimal.