Tantal: Designing a 6502-Native Language with Claude Code

How Action!, Claude Code, and the constraints of the MOS 6502 led me to build Tantal—a small compiled language for the PAL-2.

A physical PAL-2 computer running a Tantal program on its six-digit LED display.
Tantal running on the real PAL-2.

What started as an idea inspired by Action! became a small compiled language for the MOS 6502, built around predictable machine code, memory-mapped I/O, and the practical constraints of my PAL-2 computer.

I came across Action!, the compiled programming language for Atari 8-bit computers, almost by accident through the Programming for 8-bit Atari series on Root.cz. The series is in Czech, but its articles about Action! introduced me to a language that offered a productive middle ground: more structured and convenient than assembly language, yet still close enough to the machine to feel appropriate for an 8-bit system. Without that chance discovery, Tantal would not exist.

That inspiration became the starting point for Tantal, a small compiled language for the MOS 6502. I developed the compiler with the help of Claude Code, using it not only to implement the compiler, assembler, and emulator, but also to explore language-design decisions and test how those decisions behave on a machine with very limited resources.

The source code is available on GitHub:

github.com/jrydval/tantal-6502-compiled-language

Why another language for the 6502?

Tantal is not intended to hide the 6502 behind abstractions borrowed from modern desktop programming. I wanted a language whose constructs translate simply and predictably into 6502 instructions—a language that remains conceptually close to the processor.

The 6502 presents one especially important constraint: its hardware stack is only 256 bytes and shares page $0100 with return addresses and saved processor state. A conventional stack-based calling convention can consume that space quickly and adds overhead to parameter and local-variable access.

Tantal therefore does not use the hardware stack for parameters or local variables. The stack is reserved mainly for return addresses and interrupt state. Procedure parameters and local variables have fixed addresses, while frames belonging to procedures that cannot be active simultaneously can share memory. The compiler calculates this overlay from the call graph.

The first three parameter bytes are passed in the A, X, and Y registers. Additional parameters use fixed frame slots, and return values use A or A/X. A consequence of this model is that recursion is not supported; the compiler reports it as an error. For the small, hardware-oriented programs Tantal targets, I consider that a useful and understandable trade-off.

Other design choices follow the same principle:

  • top-level statements are the program—there is no mandatory main function or module header;
  • memory-mapped registers can be declared and used like variables;
  • individual bits and bit fields can have names;
  • pointers are accessed through indexing that maps naturally to the 6502 (zp),Y addressing mode;
  • generated listings show the instructions, bytes, and cycle counts associated with source lines;
  • the compiler includes its own assembler rather than depending on an external toolchain.

PAL-2 as the primary target

My main target is the PAL-2, a modern implementation of the KIM-1 concept. It has an NMOS 6502 running at 1 MHz, a hexadecimal keypad, a six-digit LED display, and RIOT-based memory-mapped I/O.

The application I originally had in mind was a simple logic analyzer for relatively slow signals. The PAL-2 user port can observe several digital lines, record their changes, and show basic information on the built-in display. This goal influenced the language more strongly than general-purpose benchmark programs would have done.

Tantal therefore has direct language support for waiting on pins, bits, and port values:

cur = wait change PAD from PAD_OLD timeout 2 s
wait rising PAD.7
wait falling PAD.6 timeout 500 us

A wait expression returns the complete port byte sampled when the condition becomes true. The from form compares the port with a variable holding the previously observed state and updates that variable after a change. This is useful inside acquisition loops because a transition that occurs while the previous sample is being processed is seen immediately by the next wait.

An optional timeout uses the RIOT timer and sets the built-in timedout value. A program can therefore stop cleanly when a signal goes quiet instead of remaining in an endless polling loop.

Tantal also provides power-of-two ring buffers for captured samples. The logic-analyzer example can record the latest 256 port values with a compact statement:

ring byte[256] log noinit
byte PAD_OLD, cur
card changes

PADD = $00
PAD_OLD = PAD
clear(log)

do
  cur = wait change PAD from PAD_OLD timeout 2 s
  if timedout then exit fi
  log <- cur
  changes += 1
od

The noinit qualifier allows captured data to survive a return to the monitor, which is useful on a small development system where memory is often inspected directly.

A display example

The following code comes from examples/display.tantal. It refreshes the KIM-1-compatible six-digit LED display through the system RIOT. The example also demonstrates fixed-address data, constant tables, procedures, simple types, loops, array indexing, and direct writes to hardware registers.

byte[6] text @ $10

proc seg7(byte c): byte
  if c >= 'a' and c <= 'z' then
    c -= $20
  fi
  if c < ' ' or c > 'Z' then
    return 0
  fi
  return FONT[c - ' ']
endproc

proc refresh()
  byte i, d, w
  SYS_PADD = $7F
  SYS_PBDD = $1F
  d = 9
  for i = 0 until 6 do
    SYS_PA = 0
    SYS_PB = d
    SYS_PA = seg7(text[i])
    for w = 0 until 40 do
    od
    d += 2
  od
  SYS_PA = 0
endproc

text[0] = 'H'
text[1] = 'E'
text[2] = 'L'
text[3] = 'L'
text[4] = 'O'
text[5] = ' '

do
  refresh()
od

The font table used by the complete example contains seven-segment patterns for digits, spaces, and uppercase letters. Lowercase input is converted to uppercase, while unsupported characters appear as blank digits.

A Tantal program displaying HELLO on the six-digit LED display in the KIM-1 Simulator, with 6502 registers, RIOT ports, and memory visible beside the simulated computer.
The display.tantal example running in the KIM-1 Simulator. The program refreshes all six LED positions directly through the system RIOT.

The example follows the documented KIM-1 display wiring, and I have now verified it on the physical PAL-2 as well. The same display.tantal program that produces “HELLO” in the simulator successfully drives the real machine’s six-digit LED display. This is an important milestone: it confirms that Tantal can move from source code, through its own compiler and assembler, to working code on the target hardware.

A physical PAL-2 computer running the Tantal display.tantal example and showing HELLO on its six-digit red LED display.
The same Tantal display example running on a real PAL-2. Successful hardware testing confirms that the KIM-1-compatible display wiring and generated 6502 code work on the physical machine.

Compiler, assembler, and emulator in one tool

The compiler is written in Go and has no external dependencies. It builds as a single executable for macOS, Linux, or Windows. A normal build produces several useful formats:

  • a raw binary image;
  • Intel HEX;
  • KIM-1 paper-tape format for loading through the monitor;
  • an assembly listing with generated instructions and cycle counts;
  • a memory map and symbol table.

The same executable also contains a 6502 emulator and a model of the PAL-2 memory map. Programs can therefore be compiled and exercised on a modern computer before being transferred to the real hardware:

tantal build examples/display.tantal
tantal run examples/analyzer.tantal \
  --input PAD=@0:00,@500:01,@800:03,@1500:00,@1600:80 \
  --max 5000000

Timed input changes are particularly useful for testing signal-waiting code. They make it possible to check whether a loop notices a transition at a specific emulated CPU cycle.

The project is still at version 0.1. Some planned features are intentionally absent, including signed integer types, 65C02 instructions, automated serial uploading, and enforcement of source-level cycle-limit directives. The current version is nevertheless sufficient to compile the included examples, produce PAL-2-compatible output formats, and run programs in the integrated emulator.

Developing a language with Claude Code

Claude Code made it practical to move repeatedly between language design, implementation, documentation, and tests. For a project like this, generating code was only part of the job. Many of the important questions were architectural:

  • What should map directly to a 6502 addressing mode?
  • How can procedure calls avoid consuming the small hardware stack?
  • Which restrictions make generated code easier to understand?
  • How should an interrupt handler interact with fixed procedure frames?
  • How can waiting for a pin transition be expressed without hiding important timing behavior?

Working through those questions interactively helped turn an initial idea into a coherent compiler, rather than a collection of unrelated syntax features. It was still necessary to define the hardware constraints, inspect generated code, and decide which trade-offs were appropriate. Claude Code accelerated that process, but the PAL-2 and the 6502 remained the real specification.

What comes next

The display example is now running on the physical PAL-2, confirming direct access to the system RIOT and successful multiplexed refresh of the six-digit LED display. The next step is testing more of the generated code on real hardware, especially the RIOT timer behavior and the signal-capture path needed by the logic analyzer. I also want to continue developing that use case, because it exposes exactly the problems Tantal is meant to solve: predictable loops, direct bit access, compact sample storage, and code that remains visibly connected to the machine executing it.

Tantal is deliberately small. It is not intended to turn a 6502 into a modern workstation. Its goal is simpler: make short 6502 programs faster to write without losing the character—and the constraints—of the processor itself.


PAL-2 and KIM-1 project series

Open the complete project log