ternary6502
Ternary model emitted as raw 6502 machine code, with Pascal opcode emitters
- Active · last change 10-20-2026
- ★ 1 on GitHub
README
ternary6502 — Ternary MLP on the 6502
A small ternary neural net (16→8→4 + argmax) for the NMOS 6502, with:
- Hand-written assembly (
asm/ternary_model.s) using a zero-page ternary dot-product kernel. - Pascal opcode-lowering (
pascal/lower.pas) that reads a text model and emits raw 6502 machine code in two modes:table— generic kernel + packed weight tables (semantics match the asm)unrolled— weights baked into straight-line ADC/SBC sequences
- Command front-end (
pascal/cmdlower.pas) that lowers a Sovereign-style command grammar (grammar/commands.gbnf) to 6502 opcodes. - Minimal 6502 emulator (
emu/emu6502.c) with cycle counting for offline verification.
Memory map
| Region | Address | Notes |
|---|---|---|
| Kernel ZP | $00–$06 |
InputPtr, WeightPtr, AccumLo/Hi, VectorLength |
| Extra ZP | $07–$08 |
ShiftAmt, OutPtr (MLP wrapper) |
| Bump allocator ZP | $10–$13 |
heap pointer / limit (cmdlower) |
| Code (model) | $0200… |
org for asm / Pascal-lowered model |
| Code (commands) | $0900… |
org for cmdlower programs |
| InputBuf | $1000 (16) |
unsigned 8-bit features |
| HiddenBuf | $1010 (8) |
|
| OutBuf | $1018 (4) |
|
| ArgmaxResult | $101C |
|
| Status / Result | $1020 / $1100 |
used by command programs |
| Heap base | $1200 |
bump allocator reset point |
Buffers sit at $1000+ so they never overlap code or weight payloads.
Weight encoding
| Value | Byte |
|---|---|
| +1 | $01 |
| 0 | $00 |
| −1 | $FF |
Inputs are unsigned 8-bit. The accumulator is signed 16-bit.
VectorLength must be ≤ 255 (Y-index loop).
Kernel contract (user-supplied semantics)
InputPtr=$00 WeightPtr=$02 AccumLo=$04 AccumHi=$05 VectorLength=$06
LDA (WeightPtr),Y / BEQ Skip / BMI Subtract
+1: CLC ADC (InputPtr),Y ; INC AccumHi on carry
???1: SEC SBC (InputPtr),Y ; DEC AccumHi on borrow
INY / JMP Loop / RTSChanges from the supplied description: none to the arithmetic semantics.
A CPY VectorLength / BEQ Done loop bound was added so the routine terminates
(implied by VectorLength but not spelled out in the one-line sketch).
Per neuron the MLP does: kernel → ReLU (clamp negative 16-bit to 0) →
logical right shift by layer ShiftAmt with saturate-to-$FF → store 8-bit.
Build (Debian/Ubuntu offline)
sudo apt-get install -y cc65 fp-compiler gcc python3 zip
cd /workspace/ternary6502 # or wherever you unpacked the zip
make all # emu + asm + pascal lower + cmdlower
make verify # bit-exact check vs tools/ref.pyIndividual steps:
# Assembler model (ca65)
ca65 -o build/ternary_model.o asm/ternary_model.s
ld65 -C asm/link.cfg -o build/ternary_model.bin build/ternary_model.o
# Pascal lowerer
fpc -O2 -Fupascal -FEbuild pascal/lower.pas
./build/lower pascal/model.txt table build/pas_table
./build/lower pascal/model.txt unrolled build/pas_unrolled
# → .bin (org $0200), .prg (C64-style 2-byte load addr), .lst
# Command lowerer
fpc -O2 -Fupascal -FEbuild pascal/cmdlower.pas
./build/cmdlower examples/demo.cmd build/demo_cmd InferEntry=0x0200
# Emulator
gcc -O2 -o build/emu6502 emu/emu6502.c
./build/emu6502 build/ternary_model.bin 0200 0200 1010:8 1018:4 101C:1Emulator usage
emu6502 image.bin LOAD ENTRY [ADDR:LEN ...]All numbers hex. Loads file at LOAD, seeds a fake return to $FFFF,
runs until the final RTS, prints cycle/instruction counts and memory dumps.
Model file format (pascal/model.txt)
layers 3
sizes 16 8 4
shifts 3 2
weights <16*8 ternary ints>
weights <8*4 ternary ints>
input <16 unsigned bytes>Generate another seed with python3 tools/ref.py gen SEED 16,8,4 3,2 > model.txt.
Command grammar
See grammar/commands.gbnf (synced from sovereign-cmd). Each line:
VERB TARGET [arg [arg [arg]]]| Verb | 6502 lowering |
|---|---|
SET name imm |
bind symbol (compile-time) |
RUN name |
JSR to symbol / 0xADDR |
READ addr |
LDA abs / STA Result++ |
WRITE addr imm |
LDA #imm / STA abs (rejected if addr < $1000) |
ALLOC n |
bump-allocate n bytes; return ptr into Result |
FREE |
reset bump pointer to $1200 |
OPEN FIX TEST |
rejected — host/tooling verbs, no bare-metal meaning |
filepath targets (/…) |
rejected — no filesystem on the 6502 target |
Protected-memory message (exact):
CONTRACT VIOLATION: Attempted write to protected memory space.On a 6502 this guards zero page, stack ($0100–$01FF), and low RAM used by
the runtime/kernel contract.
Measured results (seed=42 model, this box)
| Artifact | Code size | Cycles / inference | Match ref? |
|---|---|---|---|
asm/ternary_model.s → .bin |
400 bytes | 7140 | yes |
Pascal table |
403 bytes | 7146 | yes |
Pascal unrolled |
1871 bytes | 3051 | yes |
cmd demo.cmd + model |
52 (cmd) + 400 | 7213 (includes JSR/READ/WRITE/ALLOC/FREE) | yes |
Reference buffers: hidden 00 00 00 4D 00 18 00 00, out 13 0D 19 19, argmax 2.
Table vs asm cycle delta (~6) is from the extra JMP Main trampoline at $0200
in the Pascal table image; the kernel math is the same.
Limits
VectorLength≤ 255 (Y register)- Inputs unsigned 8-bit; weights ternary only
- Unrolled code size grows O(nonzero weights); keep buffers ≥
$1000 - Emulator is binary-mode ADC/SBC only (decimal mode traps)
- Forward
JMPplaceholders in.lstfiles may show$0000before patch; the.binis correct
Layout
asm/ ca65 sources + link.cfg + weights.inc
pascal/ lower.pas, cmdlower.pas, opcodes.pas, model.txt
emu/ emu6502.c
tools/ ref.py, verify.sh
grammar/ commands.gbnf
examples/ demo.cmd, violate.cmd, badverbs.cmd
build/ binaries, listings (created by make)License
Dual-licensed: GNU AGPL-3.0 (LICENSE) and the SnapKitty Opaque License (SNAPKITTY_OPAQUE_LICENSE.md).

