Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/STRUCTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pocketjs/
├─ framework/ Guest: @pocketjs/framework
│ ├─ src/ the TS runtime (Solid + Vue Vapor renderers, components, input, osk…)
│ └─ compiler/ the interpreted-path build pipeline (jsx-plugin, tailwind, pak)
├─ vapor/ Pocket Vapor: the AOT compiler family (Vue Vapor subset → GBA/GB/NES)
├─ vapor/ Pocket Vapor: AOT Vue subset → consoles, ESP32, Playdate Simulator
├─ contracts/ single sources of truth binding the layers
│ ├─ spec/ op contract, platform contracts, manifest + package spec, gen-rust
│ └─ schema/ published JSON schemas (pocket-2.json)
Expand All @@ -46,7 +46,7 @@ New things go where the axis says — never invent a top-level directory:
- **A new Rust simulation core** → `engine/` (workspace member if it builds on
desktop; excluded standalone crate if it needs a console toolchain).
- **A new platform embedding** (ESP32, 3DS, …) → `hosts/<platform>/`.
- **A new AOT backend** (Vapor gains a console) → `vapor/runtime/<console>/`;
- **A new AOT backend** (Vapor gains a native target) → `vapor/runtime/<target>/`;
the vapor compiler grows a target entry, the top level does not change.
- **A new demo** → `apps/<name>/` with a `pocket.json`. Standalone products
keep the `pocket-<name>` separate-repo convention and do not move in.
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@
"vapor:esp32": "bun vapor/compiler/cli.ts vapor/examples/todo/todo.tsx --target esp32",
"vapor:esp32:flash": "bun vapor/scripts/esp32.ts flash",
"vapor:esp32:verify": "bun vapor/scripts/esp32.ts verify",
"vapor:playdate": "bun vapor/compiler/cli.ts vapor/examples/todo/todo.tsx --target playdate",
"vapor:playdate:play": "bun vapor/scripts/playdate.ts",
"vapor:dev": "bun vapor/scripts/dev.ts",
"vapor:check": "bun vapor/compiler/cli.ts check vapor/examples/todo/todo.tsx"
},
Expand Down
83 changes: 56 additions & 27 deletions vapor/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ Vapor** — real `ref`/`computed` reactivity, real JSX templates — and the
compiler emits native code for machines that could never host a JavaScript
engine. The original proof target is the Game Boy Advance: 16.8 MHz
ARM7TDMI, 256 KB of work RAM, no OS, no allocator, no GC. The same compiler
now also targets the Game Boy, NES, and an ESP32 MeowBit profile; the ESP32
build is still native C with a fixed memory plan, not an embedded JavaScript
runtime.
now also targets the Game Boy, NES, an ESP32 MeowBit profile, and the
official Playdate Simulator; every build is still native C with a fixed
memory plan, not an embedded JavaScript runtime. The Playdate backend
currently produces a host-native Simulator bundle only, not a physical
device build.

Vue Vapor's thesis is *compile the virtual DOM away*. Pocket Vapor extends
it one machine layer down: **compile the JavaScript engine away.** The
Expand All @@ -35,6 +37,11 @@ an operational definition:
> Vue build as the oracle, the compiled ROM in an emulator — drives the same
> input script into each, and asserts the same rendered screen.

The Playdate backend is currently an explicit simulator-only extension of
that model: it shares the compiler, generated C, and logical-grid contract,
but local official-Simulator acceptance is not yet part of the automated
ROM parity matrix.

That is the MicroPython positioning: MicroPython is not a Python-flavored
DSL, it is a subset of Python with an implementation sized for
microcontrollers. Pocket Vapor is that, for Vue.
Expand Down Expand Up @@ -171,6 +178,7 @@ MEANS is the target's style contract:
| gba | `rgb555`, <= 15 pairs | pair id = BG palette bank (BGR555 ink/paper) |
| gb, nes | `styles2` | pair -> glyph style by luminance polarity (dark-on-light / light-on-dark) |
| esp32 | `rgb565` | pair id -> RGB565 ink/paper values rasterized into the LCD cell |
| playdate | `styles2` | pair -> black-on-white or white-on-black 1-bit glyph style |

Diagnostics are compile-time and structured (`bun vapor/compiler/cli.ts
check <file>` prints the whole matrix in one run, no toolchains needed):
Expand All @@ -192,11 +200,12 @@ oxlint plugin would give red squiggles without booting the compiler. The
## 5. Host vocabulary and rendering

Each target presents a fixed logical cell screen: 30×20 on GBA, 20×18 on
GB and ESP32, and 22×18 on NES. The ESP32 MeowBit profile rasterizes its
20×18 grid as 8×7 cells into a 160×126 content area on the 160×128 ST7735
panel. The JSX vocabulary is deliberately one intrinsic with two
interpreters — the C cell grid on device, and a ~60-line tree walker over
the oracle's micro-DOM:
GB and ESP32, 22×18 on NES, and 50×30 in the Playdate Simulator. The ESP32
MeowBit profile rasterizes its 20×18 grid as 8×7 cells into a 160×126
content area on the 160×128 ST7735 panel. Playdate maps each 8×8 1-bit
glyph directly onto the Simulator's 400×240 framebuffer. The JSX vocabulary
is deliberately one intrinsic with two interpreters — the C cell grid on a
native target, and a ~60-line tree walker over the oracle's micro-DOM:

- `<row y={n} x={n} pal={p}>` — paints its text children at (x, y) in
palette `p`, padded with spaces to the right edge; later rows overwrite
Expand All @@ -216,6 +225,13 @@ KEYINPUT bit order). Under the oracle the module executes and the test tape
feeds it; under the compiler the import is recognized and the handler
compiles to a C function fed by the runtime's key-edge loop.

Playdate has D-pad plus A/B rather than Pocket's full ten-button vocabulary.
Its runtime sends D-pad edges directly, emits A/B for short presses on
release, maps a 500 ms A hold to START, and maps a 500 ms B hold to SELECT.
A completed hold suppresses the corresponding short press. The Todo app
already maps Right to the same filter action as R, so all of its actions
remain reachable without inventing a physical R button.

## 6. Pipeline

```
Expand All @@ -230,14 +246,17 @@ todo.tsx ─┬─ framework/compiler/jsx-plugin.ts + vue-jsx-vapor ──► re
6 memory plan slots, pools, budgets (printed with the graph)
7 emit C gen_app.c (state, computeds, effects, handler)
8 cc + link target toolchain + vapor/runtime/<target>/*
→ cartridge ROM or ESP32 firmware image
→ cartridge ROM, ESP32 firmware, or
host-native Playdate Simulator .pdx
```

The fixed C contract (`vapor/runtime/vapor.h` plus `vapor_core.c`) is shared
by all apps. Each target supplies its hardware half: the console runtimes
own startup, video commit, input edges and a fixed debug block; the ESP32
runtime owns the ESP-IDF frame loop, ST7735 RGB565 raster, MeowBit GPIO
input and the UART receipt protocol.
input and the UART receipt protocol; the Playdate runtime owns the official
Simulator update callback, 52-byte-stride 1-bit framebuffer commit, and
short/long-press translation.

## 7. E2E: the oracle is real Vue

Expand Down Expand Up @@ -268,29 +287,34 @@ Layers 3 and 4 state the claim of the whole project: same file, real Vue on
a JS engine, and native code on devices, compared for every step of the
interaction rather than only the final frame. The three console targets run
under automated emulators; ESP32 uses the same grid-receipt assertion when
a board is connected.
a board is connected. Playdate is narrower in scope: manual local acceptance
builds with the official SDK's `make simulator` target and opens the
resulting `.pdx` in the official Simulator. It does not currently claim
automated per-press parity, an ARM device build, or physical Playdate
verification.

## 7.5 Targets

| | GBA | GB (DMG) | NES | ESP32 MeowBit |
|---|---|---|---|---|
| CPU | ARM7TDMI 16.8MHz | SM83 4.19MHz | 6502 1.79MHz | Xtensa LX6, up to 240MHz |
| Toolchain | arm-none-eabi-gcc | sdcc + sdasgb + makebin + rgbfix | cc65/ca65/ld65 | ESP-IDF v6.0.2 |
| Grid | 30x20 | 20x18 | 22x18 (centered) | 20x18 on ST7735 160x128 |
| Palettes | 6 real BG banks | 2 glyph styles (BGP is global) | 2 glyph styles in CHR-ROM | RGB565 ink/paper pairs |
| Pool / str caps | 32 / 24 | 32 / 24 | 8 / 20 (2 KB CPU RAM) | 32 / 24 |
| Image | flat ROM + header patch | ROM-only 32 KB | NROM-256 + CHR-ROM | ESP-IDF flash image |
| Debug receipt | EWRAM 0x2000000 | WRAM 0xD800 (grid IS the block) | $0200 fixed segment | UART 115200 (`H/R/P/D`) |
| E2E transport | libmgba | libmgba | jsnes | physical USB serial (opt-in) |
| | GBA | GB (DMG) | NES | ESP32 MeowBit | Playdate Simulator |
|---|---|---|---|---|---|
| CPU | ARM7TDMI 16.8MHz | SM83 4.19MHz | 6502 1.79MHz | Xtensa LX6, up to 240MHz | macOS host |
| Toolchain | arm-none-eabi-gcc | sdcc + sdasgb + makebin + rgbfix | cc65/ca65/ld65 | ESP-IDF v6.0.2 | official Playdate C SDK + host clang |
| Grid | 30x20 | 20x18 | 22x18 (centered) | 20x18 on ST7735 160x128 | 50x30 on 400x240 |
| Palettes | 6 real BG banks | 2 glyph styles (BGP is global) | 2 glyph styles in CHR-ROM | RGB565 ink/paper pairs | 2 1-bit glyph styles |
| Pool / str caps | 32 / 24 | 32 / 24 | 8 / 20 (2 KB CPU RAM) | 32 / 24 | 32 / 24 |
| Image | flat ROM + header patch | ROM-only 32 KB | NROM-256 + CHR-ROM | ESP-IDF flash image | `.pdx` with host `pdex.dylib` |
| Debug receipt | EWRAM 0x2000000 | WRAM 0xD800 (grid IS the block) | $0200 fixed segment | UART 115200 (`H/R/P/D`) | startup console marker only |
| E2E transport | libmgba | libmgba | jsnes | physical USB serial (opt-in) | official Simulator (manual launch) |

The generated C is target-independent; geometry and budgets arrive as
`#define`s, `SCREEN.*` folds in the compiler, and each hardware runtime
implements the same `vapor.h` contract. 8-bit portability rules baked into
codegen: `s32`/`u32` are `long` (16-bit `int` consoles),
declarations hoist to function tops (cc65 is C89), variable-index record
access is u16 pointer arithmetic and bit masks come from a ROM table
(sdcc 4.6 SM83 miscompiles some u8-by-u8 multiplies — the __muluchar
lesson inherited from Pocket Static).
codegen: `s32`/`u32` are `long` on the 16-bit-`int` console toolchains, while
the Playdate Simulator uses `int32_t`/`uint32_t` because macOS `long` is
64-bit. Declarations hoist to function tops (cc65 is C89), variable-index
record access is u16 pointer arithmetic and bit masks come from a ROM table
(sdcc 4.6 SM83 miscompiles some u8-by-u8 multiplies — the __muluchar lesson
inherited from Pocket Static).

## 8. The demo — VAPOR TODO

Expand All @@ -308,6 +332,10 @@ The ESP32 MeowBit exposes the six direct directions/action inputs used by
the app. Release-latched pairs provide the remaining Pocket buttons:
A+B = START, Left+Right = SELECT, and Up+Down = R.

In the Playdate Simulator, D-pad input is direct. Short A/B perform the
normal A/B actions; hold A for 500 ms for START (new/save), and hold B for
500 ms for SELECT (clear/cancel). Right cycles the filter in list mode.

State: 6 refs, 4 computeds (two of them list views), 4 span-merged paint
effects, a 32-entry todo pool. The compiler's memory plan for the whole app
is ~940 bytes of RAM; the GBA cartridge is under 9 KB, while the ESP32
Expand All @@ -320,4 +348,5 @@ Components-with-props and slots (block machinery is shaped for them; not
demoed), `reactive()`/`watch()` sugar, floats/fixed-point, CJK text, sound,
saves, and unattended physical-device CI. Console parity remains
emulator-driven; ESP32 parity is available through the explicit
USB-connected verifier.
USB-connected verifier. Physical Playdate builds and hardware verification
are not claimed by the current Simulator-only target.
79 changes: 55 additions & 24 deletions vapor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
TypeScript subset of Vue Vapor — real `ref`/`computed`, real JSX — and the
Pocket Vapor compiler emits native code for devices that could never host
a JavaScript engine: **ARM7 on the Game Boy Advance, SM83 on the Game Boy,
6502 on the NES, and Xtensa LX6 on the ESP32**. No JS engine, no GC, no
allocator. Vue Vapor compiles the virtual DOM away; Pocket Vapor compiles
the JavaScript engine away.
6502 on the NES, and Xtensa LX6 on the ESP32**, plus a host-native C bundle
for the official Playdate Simulator. No JS engine, no GC, no allocator. Vue
Vapor compiles the virtual DOM away; Pocket Vapor compiles the JavaScript
engine away.

| GBA (arm-none-eabi-gcc) | GBA edit mode |
|---|---|
Expand All @@ -16,28 +17,32 @@ the JavaScript engine away.
|---|---|
| ![gb](docs/todo-gb.png) | ![nes](docs/todo-nes.png) |

One component file, five executions: the oracle on real vue 3.6, three
cartridges, and one ESP32 firmware image. Screen geometry is a compile-time
constant (`SCREEN.width`/`SCREEN.height` from the host module): layout math
and width ternaries fold per target, so the narrow help strings on
GB/NES/ESP32 cost zero bytes on GBA — compile-time responsive UI.
One component file spans the oracle on real vue 3.6, three cartridges, one
ESP32 firmware image, and a native `.pdx` running in the official Playdate
Simulator. Screen geometry is a compile-time constant
(`SCREEN.width`/`SCREEN.height` from the host module): layout math and width
ternaries fold per target, so target-specific help strings cost zero bytes
elsewhere — compile-time responsive UI. The Playdate target is currently
Simulator-only; this does not claim a physical-device build.

The proof is [`examples/todo/todo.tsx`](examples/todo/todo.tsx) — TodoMVC
with filters, a computed remaining-count, windowed scrolling and a glyph
editor. The **same file** runs two ways:

- **Oracle**: unmodified on `vue@3.6` `runtime-with-vapor` (through the
repo's vue-jsx-vapor pipeline) over a micro-DOM, in bun.
- **Device**: compiled to C by `vapor/compiler/compile.ts`, linked against a
target runtime, and run as native code on a console or ESP32 — still with
no JavaScript engine.
- **Native target**: compiled to C by `vapor/compiler/compile.ts`, linked
against a target runtime, and run as native code on a console, ESP32, or
the official Playdate Simulator — still with no JavaScript engine.

The parity suite drives one tape of button presses through the oracle and
each console emulator, then compares the rendered logical cell grid —
characters *and* palettes — cell-for-cell after every press. The ESP32
device verifier applies the same contract over UART to the physical board;
it is an opt-in hardware check, not a claim made by the emulator-only test
suite.
suite. Playdate currently has a separate interactive build-and-launch path;
it is not part of that per-press parity matrix and makes no physical
Playdate claim.

```
$ bun test vapor/tests/ # incl. 3-console per-press parity
Expand Down Expand Up @@ -107,7 +112,7 @@ it deliberately over-approximates Vue (static dependency analysis).

The look is declarative now — the same Tailwind names the big framework
compiles, lowered through each target's style contract (GBA: real palette
banks; ESP32: RGB565 ink/paper pairs; GB/NES: two glyph styles by
banks; ESP32: RGB565 ink/paper pairs; GB/NES/Playdate: two glyph styles by
luminance), with the whole diagnostics matrix one command away:

```tsx
Expand All @@ -123,6 +128,8 @@ gb OK 20x18, 6 style pairs
nes OK 22x18, 6 style pairs
warn VS104: 3 distinct color pairs render as the same glyph style ...
esp32 OK 20x18, 6 style pairs
playdate OK 50x30, 6 style pairs
warn VS104: 3 distinct color pairs render as the same glyph style ...
meowbit OK board (esp32)
warn VB103: "start" is only reachable as the a+b chord on meowbit ...
$ bun vapor/compiler/cli.ts check app.tsx --strict # lossy lowering = failure
Expand All @@ -138,7 +145,8 @@ ability to enumerate devices.
And the oracle is visible: `bun run vapor:dev` serves the app on real Vue
Vapor in your browser — inspectable DOM rows, keyboard as the pad,
`?target=gb` to see the DMG's two-style world before you burn a cart, or
`?target=esp32` to preview the MeowBit's 20×18 logical viewport.
`?target=esp32` to preview the MeowBit's 20×18 logical viewport. Use
`?target=playdate` for the Playdate target's 50×30 monochrome grid.

## Commands

Expand All @@ -155,6 +163,8 @@ bun vapor/compiler/cli.ts vapor/examples/todo/todo.tsx --target nes # → tod
bun run vapor:esp32 # → app-only todo.esp32.bin + gen-esp32/
bun run vapor:esp32:flash # build + flash the connected ESP32 MeowBit
bun run vapor:esp32:verify # build + flash + replay the Vue-oracle tape
bun run vapor:playdate # → dist/vapor/todo.pdx (Simulator bundle)
bun run vapor:playdate:play # build + open it in the official Simulator
bun vapor/scripts/play.ts # build + open in mGBA
bun vapor/scripts/dev.ts [app.tsx] # visible oracle in the browser
bun vapor/compiler/cli.ts check <app.tsx> [--strict] # cross-target diagnostics matrix
Expand All @@ -165,15 +175,35 @@ bun test vapor/tests/ # oracle + compiler +
Toolchains: `arm-none-eabi-gcc` + `mgba` (GBA/GB), `sdcc` + `rgbfix` (GB),
`cc65` (NES, emulated by the jsnes dev-dependency), and **ESP-IDF v6.0.2**
(ESP32; set `IDF_PATH` / `IDF_TOOLS_PATH` when auto-discovery does not find
the installation). Oracle tests run with bun alone. Notable per-target facts the
runtime absorbs: the console shadow grid IS the debug block (fixed
WRAM/CPU-RAM addresses), so the harness reads the logical screen even while
a 1 MHz SM83 trickles VRAM through vblank; DMG has one palette, so logical
palettes map to baked glyph styles; NES fits grid + pool + views into 2 KB
of CPU RAM with the font in CHR-ROM; ESP32 rasterizes the same logical
20×18 grid into RGB565 on a 160×128 ST7735; and sdcc 4.6's SM83 port
miscompiles some u8-by-u8 multiplies, so generated indexing is u16 pointer
arithmetic and bit masks come from a ROM table.
the installation). Playdate uses Panic's official macOS SDK and host clang.
Install the SDK from <https://play.date/dev/>; the build resolves it from
`PLAYDATE_SDK_PATH`, then `SDKRoot` in `~/.Playdate/config`, then the
installer default `~/Developer/PlaydateSDK`. For example:

```sh
export PLAYDATE_SDK_PATH="$HOME/Developer/PlaydateSDK"
bun run vapor:playdate
```

Both Playdate commands invoke the SDK's `make simulator` path and package a
host `pdex.dylib` with `pdc`. They intentionally do not run `make device` or
claim a device-compatible `.pdx`. Oracle tests run with bun alone. Notable
per-target facts the runtime absorbs: the console shadow grid IS the debug
block (fixed WRAM/CPU-RAM addresses), so the harness reads the logical
screen even while a 1 MHz SM83 trickles VRAM through vblank; DMG has one
palette, so logical palettes map to baked glyph styles; NES fits grid +
pool + views into 2 KB of CPU RAM with the font in CHR-ROM; ESP32
rasterizes the same logical 20×18 grid into RGB565 on a 160×128 ST7735;
Playdate maps a 50×30 grid of 8×8 glyphs directly onto its 400×240 1-bit
framebuffer; and sdcc 4.6's SM83 port miscompiles some u8-by-u8 multiplies,
so generated indexing is u16 pointer arithmetic and bit masks come from a
ROM table.

On Playdate, D-pad presses are direct. A short A/B press emits Pocket A/B
when released; holding A for 500 ms emits START (new/save), and holding B
for 500 ms emits SELECT (clear/cancel), without also emitting the short
press. Right also cycles the Todo filter, so the demo does not need a
physical R button.

## Layout

Expand All @@ -187,7 +217,8 @@ vapor/
runtime/ vapor.h contract + vapor_core.c (shared grid/strings/line)
runtime/gba|gb|nes/ per-console halves: crt0, video commit, input, debug block
runtime/esp32/ ESP-IDF loop, ST7735 RGB565 raster, buttons, UART receipt
scripts/ dev.ts (visible oracle), play.ts, shot.ts, esp32.ts (device protocol)
runtime/playdate/ official Simulator framebuffer, input and update loop
scripts/ dev.ts, play.ts, shot.ts, esp32.ts, playdate.ts
tests/ styles + compiler + oracle + 3-console parity + shared device tape
tests/harness/ headless libmgba runner (GBA+GB) + jsnes runner (NES)
```
Loading