Sign in Join

Live chat for Free Pascal, Lazarus, Delphi and every Pascal programmer.

Catalog · How the catalog works
Anything · Lazarus or Delphi
Random projectRandom project

ternary6502

Ternary model emitted as raw 6502 machine code, with Pascal opcode emitters

View on GitHub

Member rating

No ratings yet.

Sign in to rate this project.

README

ternary6502 — Ternary MLP on the 6502

A small ternary neural net (16→8→4 + argmax) for the NMOS 6502, with:

  1. Hand-written assembly (asm/ternary_model.s) using a zero-page ternary dot-product kernel.
  2. 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
  3. Command front-end (pascal/cmdlower.pas) that lowers a Sovereign-style command grammar (grammar/commands.gbnf) to 6502 opcodes.
  4. 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)

pascal
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 / RTS

Changes 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)

bash
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.py

Individual steps:

bash
# 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:1

Emulator usage

pascal
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)

pascal
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:

pascal
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):

pascal
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 JMP placeholders in .lst files may show $0000 before patch; the .bin is correct

Layout

pascal
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).