Release notes for the 6502 assembler of the Breaknes project.
What's new in 1.4
Multi-pass assembling
The assembler now compiles the source in several passes until all identifiers, labels and defines are resolved and the label values stop changing between passes. The intermediate passes run silently; only the final pass reports errors.
This also resolves the long-standing Zero Page / Absolute ambiguity: a forward-referenced label whose final address is below $100 now automatically uses the short zero page opcode (saving one byte per instruction), instead of unconditionally emitting a 3-byte absolute form. Boundary cases (labels landing exactly at $100) converge correctly.
The ABS directive
A new hint directive that forces the absolute opcode for the next CPU instruction where a Zero Page / Absolute ambiguity exists (address below $100):
ABS
LDA $10 ; AD 10 00 (absolute), not A5 10 (zero page)
LDA $20 ; the hint is consumed by the previous instruction: A5 20The hint survives labels and non-emitting directives and works for plain, ,X and ,Y addressing.
The expression engine everywhere
The syntax-expression parser (asmexpr) is now the main expression engine for all operand positions. Labels, defines and complex expressions work everywhere:
LDA MyData + 32 * entry_size + 12
STA (Base << 4) | 1
BYTE Table + 2- Fixed evaluation of mixed-precedence expressions (
a*b+c,a+b*c, ...). - All operators are now implemented:
* / % + - << >> <<< >>> > >= < <= == != & | ^ ! ~(previously several of them were recognized but silently ignored). - Immediate expressions with the
#prefix now work correctly:
LDX #(MONSTER_TAB & #$FF) ; A2 08
LDY #(MONSTER_TAB >> 8) ; A0 D7
LDA #-1 ; A9 FF- Parentheses inside an expression are grouping; only a whole operand wrapped in parentheses means indirect addressing (
JMP (addr)). - Forward-referenced immediates (
LDX #labelwhere the label is defined later) are patched on the final pass.
Unified instruction handling
The ~1000 lines of copy-pasted per-instruction handlers in asmops.cpp were replaced by a single table-driven handler with one opcode table covering every addressing mode of every instruction. All original opcodes are preserved (verified byte-for-byte against the old output).
Assembly listing
New -l <file.lst> option writes an assembly listing (address, emitted bytes including patched values, and the source line):
Breakasm [-l <file.lst>] <source.asm> <output.prg>
Python port
A faithful Python port of the assembler with the same behaviour and command line:
python breakasm.py [-l <file.lst>] <source.asm> <output.prg>
It reproduces the C++ version byte-for-byte on the whole test suite, including error messages, exit codes and the listing format, and can also be used as a module (breakasm.Assembler, eval_expr).
Bug fixes
- Label-name trimming: the trailing-whitespace trim in
add_labelno longer removes interior spaces, so composite expressions containing spaces (e.g.lda base + 1,#(TAB >> 8)) are no longer silently truncated. - Stale PRG bytes between passes: the PRG buffer is zeroed at the start of each pass, so bytes left over from an earlier (larger) pass can no longer corrupt the output.
- Unknown commands are now counted as errors (previously the error was printed but the exit code was 0).
- Register misuse (
lda x, x) now reports a proper error instead of emitting garbage. - Immediate forward references emit a patched operand instead of a silent
0.
Tests
testall.asmextended with new cases for theABSdirective and for immediate expressions (#(label & #$FF),#(label >> 8)).- The whole suite (testall, test, INCLUDE, expressions, addressing boundaries, error cases, BreaksDebug sources) assembles byte-identically between the C++ and Python implementations.
Notes
- The PRG output is always 64 Kbytes (the size of the 6502 address space);
ORGcan be set anywhere in it. - Directives:
ORG,INCLUDE,DEFINE,BYTE,WORD,END,PROCESSOR,ABS.