# Fanco16 v2 experimental core V2 is a separate, provisional machine profile. It leaves v1's encoding, assembler, interpreter and loader unchanged. All previously valid v1 instructions retain their semantics and bytes; v1 images with origins at least `0x100` can be loaded unchanged. V2's assembler defaults to `0x1000`, and loaders reserve the entire zero page rather than only register bytes. See [v1's specification](BASELINE.md) for the unchanged baseline encoding, arithmetic flags, and branch condition table. The extension allocation below is experimental, not a long-term compatibility promise. ## Memory and atomic execution RAM is 65,536 writable executable bytes. Eight 16-bit registers `r0`–`r7` (alias `sp=r6`, `pc=r7`) are the little-endian RAM words at `0,2,...,14`, not copies. Every instruction permits PC and SP where it permits a register. All addresses, PC fetches, word second-byte accesses and arithmetic results wrap modulo 65,536. A word store at `0xffff` writes its high byte to RAM byte 0. The framebuffer is a separate `screen: Uint8Array(65536)`; `out [base],data` writes its low byte without changing RAM. CPU code can access all of zero page. SP is an ordinary register; no automatic stack or calling convention exists. Each instruction fetches and validates its complete encoding **before any visible state changes**. PC then advances to next instruction, operands are sampled, and execution occurs. PC operands and aliased RAM reads see this preadvanced PC. An explicit PC write, including a RAM store to bytes 14/15, overrides that advance. Self-modification is visible on the next fetch without cache invalidation. Invalid encodings throw without changing RAM, flags, PC, framebuffer, counters or halt state for the offending instruction. Prior instructions in a bulk run remain committed. HALT advances PC, costs one cycle, counts one instruction, and sets `halted`; stepping it afterwards is a no-op. Ordinary instructions cost their full **encoded byte length**, including taken and untaken branches. Operand RAM accesses cost no additional cycles. SLEEP is the single exception described below. Instructions execute atomically and cannot be interrupted partway through. The profile device clock is **120,000,000 virtual byte cycles/second**, with **2,500 cycles per 48 kHz audio sample**. This is profile timing, not a claim of 120 MIPS or guaranteed wall-clock performance. See [SOUND.md](SOUND.md) for the synthesizer profile, separate from CPU semantics. ## Unchanged baseline ISA | Encoding (hex) | Operation | Length/cycles | | --- | --- | ---: | | `00` | HALT | 1 | | `10+d`, `18+d` | INC d, DEC d | 1 | | `20+d lo hi` | MOV d, imm16 | 3 | | `30 P(d,s)` | MOV d, s | 2 | | `31+a P(d,s)` | ADD/SUB/AND/OR/XOR/CMP d, s | 2 | | `40+a d lo hi` | Same ALU operations, immediate operand | 4 | | `50/51 P(d,b)` | LDB/LDW d, [b] | 2 | | `52/53 P(d,b)` | STB/STW [b], d | 2 | | `54/55 P(d,b) disp8` | LDB/LDW d, [b+disp8] | 3 | | `56/57 P(d,b) disp8` | STB/STW [b+disp8], d | 3 | | `58 P(d,b)` | OUT [b], d | 2 | | `60..6A disp8` | BRA/BZ/BNZ/BLT/BGE/BLO/BHS/BLE/BGT/BLS/BHI | 2 | `P(d,s)=(d<<3)|s`, both registers 0..7; descriptor bits 7..6 must be zero. Immediate ALU register byte must be 0..7. `a` indexes ADD, SUB, AND, OR, XOR, CMP in order. Words are low byte first. Signed displacement is -128..127. All unlisted encodings trap, including `0xE0..0xFF`: that bank remains unused. Flags are separate numeric state: N=1, Z=2, C=4, V=8. N is result bit 15, Z indicates a zero 16-bit result. ADD/INC set carry on unsigned overflow; SUB/DEC/CMP set **C on borrow** (left operand below right operand). V is signed 16-bit overflow. AND/OR/XOR set NZ and clear CV. MOV, loads, stores, OUT, branches, HALT and SLEEP preserve flags. CMP discards its result. Branches test, in table order: always, Z, !Z, N!=V, N==V, C, !C, Z||(N!=V), !Z&&(N==V), C||Z, !C&&!Z. Flags are preserved. Short branches use next-PC-relative signed byte displacement. ## V2 extensions | Encoding (hex) | Assembly | Length | Cycles | | --- | --- | ---: | ---: | | `70 P(d,s)` / `78 d lo hi` | `shl d, s-or-imm16` | 2 / 4 | 2 / 4 | | `71 P(d,s)` / `79 d lo hi` | `shr d, s-or-imm16` | 2 / 4 | 2 / 4 | | `72 P(d,s)` / `7A d lo hi` | `sar d, s-or-imm16` | 2 / 4 | 2 / 4 | | `73 P(d,s)` / `7B d lo hi` | `mul d, s-or-imm16` | 2 / 4 | 2 / 4 | | `80+d` | `neg d` | 1 | 1 | | `90..9A lo hi` | `bra.w` through `bhi.w`, absolute target | 3 | 3 | | `A0 lo hi` | `sleep imm16` | 3 | 3 + imm16 | Extended descriptors have the same strict validation as baseline descriptors. - **SHL/SHR/SAR:** count is the right operand masked with 15. SHL shifts left, SHR shifts right filling zeros, SAR shifts right copying the original sign bit. Result is the low 16 bits. For nonzero counts, NZ reflect the result, C is the **last bit shifted out**, and V is cleared. A masked count of zero preserves **all flags** and the destination value (PC still has its ordinary preadvance). - **MUL:** result is low 16 bits of the unsigned product; NZ reflect the result. C is set iff the unsigned product's upper 16 bits are nonzero. V independently tests whether the product of the two signed 16-bit interpretations lies outside -32768..32767. All four flags are replaced, not accumulated. No high-word result is exposed. Example: `0xffff * 0xffff` produces 1, C=1, V=0. - **NEG:** exactly the flags/result of `0 - destination`, including borrow and signed overflow. Zero gives Z; `0x8000` remains `0x8000` and gives N|C|V. - **Long branches:** explicit `.w` forms use the same conditions as short forms but carry an **absolute little-endian 16-bit target**, not rel16 displacement. No automatic branch relaxation occurs; the programmer chooses `.w`. - **SLEEP:** unsigned immediate 0..65535. One instruction preserves RAM/flags, advances PC by three, and adds `3+n` to cycles exactly once. It is not an emulated busy loop, interrupt wait, host delay, or special halt state. Even `sleep 0` counts one instruction and three cycles. ## Assembly `assemble(source)` returns `{bytes: Uint8Array, origin, labels}`. Instructions, registers and directives are case-insensitive; symbol names are case-sensitive. Operands are comma-separated, `;` starts a comment. Labels use `[A-Za-z_][A-Za-z_0-9]*`; register names cannot be symbols. Default origin is `0x1000`. `.org integer` may appear once, before labels and emitted bytes; `.equ` declarations may precede it. Origins must be 256..65535; an image may end at 65536, but cannot wrap or overwrite an earlier section. Words/immediates accept -32768..65535 (emitted modulo 65536); `.byte` accepts -128..255. Branch/jump addresses and SLEEP durations accept 0..65535. - Integer syntax: decimal, `0x` hex or `$` hex, optional leading sign. - `.equ NAME, expression` defines a non-emitting constant; forward references to constants and labels work. Duplicate, undefined and circular symbols fail, including unused invalid `.equ` declarations. Constants are not entries in the returned `labels` map. - Expressions are an integer or symbol, optionally followed by `+ integer` or `- integer`, e.g. `table+2`, `SCREEN-1`, `0x40+2`. Constants can refer to another expression-defined constant. Unary plus/minus on symbols is also accepted. There is no multiplication, label subtraction, parentheses, precedence parser, arbitrary chained arithmetic, macro facility, or string literal syntax. - Expressions work in immediate/data/branch operands and signed memory displacement, e.g. `.equ OFF,2` then `ldw r0,[r1+OFF-1]`. The resulting displacement must still fit -128..127. `[register]` has no displacement byte; explicit `+0` does have one. OUT has no displacement form. - Short branch assembly operands are absolute targets; the assembler computes the wrapped next-PC difference and rejects out-of-range signed byte offsets. `.w` forms work anywhere in the address space without expanding another opcode. - `jmp target` remains the transparent baseline pseudo-instruction `mov pc,target` (3 bytes). Register jumps use `mov pc,register` (2 bytes). **CALL/RET are deferred:** no hidden expansion, scratch register, stack layout or implicit flags contract has been introduced. Malformed syntax, invalid arity/ranges/descriptors and unknown mnemonics fail instead of truncating or accepting undocumented aliases. ## Passive device RAM and sound events Input occupies RAM `0x20..0x2F`. The host refreshes that passive snapshot before scheduled chunks. CPU reads do not poll a device, and CPU stores are ordinary RAM stores; the next host refresh can overwrite them. Input layout is defined by the frontend's input profile rather than by additional CPU opcodes. Sound registers occupy RAM `0x40..0x5F`. Only an ordinary **STB or STW** whose written byte address is in this inclusive range enqueues a sound event: ```ts interface SoundEvent { cycle: number; address: number; value: number } ``` Addresses are absolute RAM addresses; values are low bytes 0..255. The timestamp is the instruction's **retirement cycle**, old cycles plus encoded length, before any subsequent instruction. An STW emits low byte then high byte, both at the same cycle, and emits only the bytes actually in the sound range. The high-byte address wraps normally. Sound RAM is updated immediately as ordinary RAM. Same-value stores still generate events. Loads, register writes, host RAM edits, image loading and reset do not generate events. Sound events do not pollute input. `drainSoundEvents()` returns all queued events in execution/byte order and clears that queue. With normal monotonically increasing VM cycles, event cycles are monotonic; manually editing cycles can deliberately break that property. The scheduler should run no farther than one audio chunk before draining; the profile is intended to tolerate approximately 400,000 sound byte writes/second. TS uses an array. WASM uses a fixed native 4,096-event queue and bulk adapter transfers with **no JS callback per instruction**. Before a sound-writing instruction, native execution checks exact required byte slots and stops before changing visible state if capacity is insufficient; the adapter drains in bulk and resumes. Both backends cap undrained host events at 1,000,000. Reaching this cap throws `Sound event queue full; drainSoundEvents() before continuing` before the next store that cannot fit. Previously executed instructions and every prior event are retained; draining lets execution resume at that store. No sound write is silently dropped. Non-sound stores do not need event capacity. ## Public core API `VM` and `WasmVM` expose the same RAM, framebuffer, register reads, numeric `flags/cycles/instructions`, boolean `halted`, and methods: - `load(bytes: Uint8Array, origin=0x1000)`: validates before mutating, copies aliased input views, clears RAM/framebuffer/flags/counters/halt/event queues, loads bytes and sets PC. Reserved zero page applies to images, not execution. - `step()`: execute at most one instruction. - `run(budget)`: execute up to budget instructions, stopping at HALT or error; returns the instruction count, as in v1. - `runUntil(deadlineCycles, maxInstructions=1_000_000)`: execute while `cycles < deadlineCycles`, below instruction cap and not halted. Returns the instruction count. Instructions are atomic and may cross the deadline; **SLEEP may jump far beyond it**. Equality executes nothing. Arguments must be nonnegative safe integers, even when halted; deadlines are absolute, not deltas. - `drainSoundEvents()`: consume all pending sound events. `createWasmVM(bytesOrModule?)` accepts binary bytes or a WebAssembly.Module; without an argument it fetches `./vm.wasm` relative to the bundled adapter. The bundled native module has fixed memory and no host imports. Build with `CLANG=/path/to/wasi-sdk/bin/clang sh v2/wasm/build.sh`. Run `bun test v2/core.test.ts v2/wasm.test.ts` for baseline, full decoder TS/WASM differential, extensions, timing, queue pressure and recovery tests. ## Explicitly open Interrupts, division, stack/calling conventions, palette hardware and durable cross-version compatibility are not settled here. Profile clock timing does not imply measured emulator throughput; synthesis and browser audio latency belong to the device/runtime layers, not the ISA. High-opcode one-byte allocation remains reserved pending evidence from representative programs.