# Fanco16 v1 experimental core This is a provisional, dependency-free, browser-friendly TypeScript experiment, not a compatibility commitment or a finalized hardware design. It does not change the older root VM. Audio, interrupts, input, palette, display scanout timing, and additional devices are deliberately unspecified. The only current device operation is `out`, writing a separate 64 KiB framebuffer. There is no generic address-space operand on ordinary arithmetic or memory instructions. ## State and memory - Byte-addressed, writable, executable 65,536-byte RAM; no alignment requirements. - Eight unsigned 16-bit registers: `r0`–`r5`, `sp` (`r6`), `pc` (`r7`). `sp` is currently an ordinary register: no automatic stack operations. - Registers are **the RAM words at addresses 0, 2, …, 14**, not copies. Every byte load/store sees those aliases, and stores to addresses 14/15 change PC. - All words and immediate words are little-endian. Arithmetic wraps modulo 65,536. Every RAM address wraps modulo 65,536, including the second byte of a word at `0xffff`, which is byte 0 (the low byte of `r0`). Instruction fetch wraps too. - Separate framebuffer `screen[0..65535]`, one byte per element. The frontend may display it as 256×256 pixels (the current harness uses temporary RGB332); that does not define a hardware palette or reserve palette memory. - Flags are separate state, not RAM-mapped: `N=1`, `Z=2`, `C=4`, `V=8`. ### Atomic instruction / PC rule 1. Fetch and validate the **whole instruction**, including immediates and displacement, from the old PC. No architectural state changes during decode. 2. Set PC to the address immediately after that instruction, wrapping at 65,536. 3. Read operands and execute. Every PC operand and PC-relative effective address sees this next-instruction PC; accesses through RAM aliases also see it. 4. Explicit writes to PC, including aliased byte/word stores, override the preadvanced PC. Address and source values are read before destination writes. Thus self-modifying code cannot alter already-fetched bytes of its current instruction, but it can change a subsequent instruction without a cache flush. This also resolves the unusual case of an instruction overlapping bytes 14/15: its encoded operands are fetched before those PC bytes are updated. Each successfully executed instruction is atomic, increments `instructions` by one, and adds its **encoded byte count** to `cycles`. Branches cost two virtual cycles whether taken or not. Operand/data memory accesses have no additional virtual cost. This is an accounting model, **not a real-time or hardware bus timing guarantee**. HALT advances PC, counts one instruction/one cycle, and sets `halted`. Stepping a halted machine is a no-op. Invalid encodings throw an Error without changing PC, RAM, screen, flags, halt state, or counters; they do not silently halt. All RAM is mapped, so fetching beyond an image reads the remaining RAM, not an image-length fault (RAM initially contains zero, which encodes HALT). ## Assembly syntax `assemble(source: string)` returns: ```ts { bytes: Uint8Array, origin: number, labels: Record } ``` Default origin is `0x100`. An optional `.org integer` must precede all labels and emitted bytes and may occur only once. Origin must be in 16..65535; image plus origin must not exceed 65536. Reserving the first 16 bytes in the loader avoids silently overwriting register aliases while initializing PC. Programs can still access/execute those addresses at runtime. No wrapping or overlapping assembly sections are permitted. - One instruction or directive per line; operands are comma-separated. - `;` begins a comment. Blank lines are ignored. - Labels: `name:`, optionally preceding an instruction, with identifier syntax `[A-Za-z_][A-Za-z_0-9]*`. Multiple labels may share an address. Labels are case-sensitive; mnemonics and register names are case-insensitive. Register names (including invalid `r8`, etc.) cannot be labels. Duplicate and undefined labels are errors; a label beyond address 65535 is an error. - Integers: decimal, `0x` hex, or `$` hex, with optional leading `+`/`-`. No expressions, floats, strings, implicit numeric truncation, or macros. - Word immediates / `.word`: −32768..65535, emitted modulo 65536. `.byte`: −128..255, emitted modulo 256. Both directives accept comma-separated integers or labels. Labels used as bytes must fit the same byte range. - Memory operand: `[register]`, `[register + integer]`, or `[register - integer]`, with optional spaces and signed displacement −128..127. Displacement magnitude uses the integer formats above, without a second sign. Labels/expressions inside brackets are not supported. Explicit `+0` still uses the displacement encoding. - Branch operands are labels or **absolute unsigned 16-bit target addresses**, not literal relative offsets. The assembler calculates the signed displacement from next PC (allowing PC wrap) and rejects anything outside −128..127. - `jmp label-or-address` is exactly `mov pc, label-or-address`, not a new opcode; JMP targets must be 0..65535. For a register jump write `mov pc, register`. - Bad arity, invalid registers/operands, unknown mnemonics, invalid ranges, image overlap/overflow, and invalid origins produce errors with source line numbers. ## Operations and flags | Syntax | Effect | Flags | | --- | --- | --- | | `halt` | Halt execution | Preserve | | `inc d` / `dec d` | `d = d + 1` / `d = d - 1` | NZCV as ADD/SUB | | `mov d, s` / `mov d, imm16` | Copy word | Preserve | | `add d, s-or-imm16` | `d = d + operand` | NZCV | | `sub d, s-or-imm16` | `d = d - operand` | NZCV | | `cmp d, s-or-imm16` | Compute `d - operand`, discard result | NZCV | | `and/or/xor d, s-or-imm16` | Bitwise operation into d | NZ; clear CV | | `ldb d, [b±disp]` | Zero-extend byte from RAM into d | Preserve | | `ldw d, [b±disp]` | Load RAM word into d | Preserve | | `stb [b±disp], d` | Write low byte of d to RAM | Preserve | | `stw [b±disp], d` | Write word d to RAM | Preserve | | `out [b], d` | `screen[b] = d & 255` | Preserve | All register operands permit all eight registers, including SP and PC. ALU and CMP have a register destination/left operand and register or immediate right operand; memory is accessed only via explicit load/store. OUT intentionally has no displacement form. IN and division are not implemented. N is the result's bit 15; Z means the wrapped 16-bit result is zero. For addition, C means unsigned carry beyond 65535. For subtraction (including CMP and DEC), **C means borrow**, i.e. unsigned left operand is less than right operand. V means signed overflow outside −32768..32767. INC/DEC do not preserve carry. | Branch | Condition | Meaning after CMP | | --- | --- | --- | | `bra` | Always | Unconditional | | `bz` | Z | Equal | | `bnz` | !Z | Not equal | | `blt` | N != V | Signed less than | | `bge` | N == V | Signed greater/equal | | `blo` | C | Unsigned less than (borrow) | | `bhs` | !C | Unsigned greater/equal | | `ble` | Z or N != V | Signed less/equal | | `bgt` | !Z and N == V | Signed greater than | | `bls` | C or Z | Unsigned less/equal | | `bhi` | !C and !Z | Unsigned greater than | All branches preserve flags. On a taken branch, PC becomes next PC plus the sign-extended displacement, modulo 65536. ## Exact provisional byte encoding Hexadecimal byte values below. Register indices d/s/b are 0..7. A pair descriptor `P(d,s)` is `(d << 3) | s`: bits 7..6 **must be zero**. For memory and OUT, use `P(data-register, base-register)`, independent of assembly operand order. `lo hi` denotes a little-endian immediate word; `disp` is a signed two's-complement byte. `a` indexes `add, sub, and, or, xor, cmp` in that order (0..5). | Opcode byte(s) and operands | Instruction | Bytes / virtual cycles | | --- | --- | ---: | | `00` | HALT | 1 | | `10+d` | INC d | 1 | | `18+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)` | ALU a, register right operand | 2 | | `40+a d lo hi` | ALU a, immediate right operand | 4 | | `50 P(d,b)` | LDB d, [b] | 2 | | `51 P(d,b)` | LDW d, [b] | 2 | | `52 P(d,b)` | STB [b], d | 2 | | `53 P(d,b)` | STW [b], d | 2 | | `54 P(d,b) disp` | LDB d, [b+disp] | 3 | | `55 P(d,b) disp` | LDW d, [b+disp] | 3 | | `56 P(d,b) disp` | STB [b+disp], d | 3 | | `57 P(d,b) disp` | STW [b+disp], d | 3 | | `58 P(d,b)` | OUT [b], d | 2 | | `60 disp` | BRA | 2 | | `61 disp` | BZ | 2 | | `62 disp` | BNZ | 2 | | `63 disp` | BLT | 2 | | `64 disp` | BGE | 2 | | `65 disp` | BLO | 2 | | `66 disp` | BHS | 2 | | `67 disp` | BLE | 2 | | `68 disp` | BGT | 2 | | `69 disp` | BLS | 2 | | `6a disp` | BHI | 2 | Every unlisted opcode traps. Immediate ALU descriptor `d` must be exactly 0..7; nonzero unused descriptor bits trap rather than acting as undocumented aliases. **`0xe0`–`0xff` (`111xxxxx`) are reserved, invalid/trapping, for future frequent one-byte forms.** Reserving 32 rather than 64 high opcodes is a provisional choice; it may expand to `0xc0`–`0xff` in a later experiment. Do not populate this bank by guessing which operations deserve it: compare representative real programs and measure total encoded-byte savings first. Existing one-byte HALT/INC/DEC remain below this bank; current allocations all fit below `0xe0`. ## VM API ```ts import { assemble } from './assembler'; import { VM, N, Z, C, V } from './vm'; const image = assemble('mov r0,42\nhalt'); const vm = new VM(); vm.load(image.bytes, image.origin); const executed = vm.run(1000); ``` - `ram` and `screen` are public `Uint8Array(65536)` buffers. `readReg(index)` validates an integer index in 0..7; registers can be edited through RAM aliases. - `flags` is numeric; `halted` is boolean; `instructions` and `cycles` are numbers. - `load(bytes: Uint8Array, origin = 0x100)` validates the entire image before changing state, copies the bytes (including when passed a view of its own RAM), clears RAM/framebuffer/flags/counters/halted, loads the image, then sets PC. Loading into bytes 0..15, wrapping images, and noninteger origins are errors. - `step(): void` executes at most one instruction, with the atomic behavior above. - `run(budget: number): number` executes at most `budget` instructions, stopping early on HALT, and returns the count executed by this call. Budget must be a nonnegative safe integer (zero is allowed). Invalid budget throws even if halted; traps propagate to the caller. Use small UI time slices: this method is bounded by instructions, not wall-clock time, and is synchronous. ## Small examples Fill the complete framebuffer; loop control deliberately uses INC's zero flag: ```asm mov r0, 0 mov r1, 0 loop: out [r1], r0 inc r0 inc r1 bnz loop halt ``` This executes 262,147 instructions and 393,223 virtual cycles, storing `screen[address] = address & 255`. PC-relative self modification, leaving `r1 = 42`: ```asm mov r0, 42 stb [pc+1], r0 ; next PC points at MOV's opcode; +1 is its immediate low byte mov r1, 0 halt ``` Run core checks with `bun test v1/core.test.ts`.