Shawn Quick dfcaae62dd Add qlink, the disk tools, and a bootable ProDOS 8 image.
Move the assembler to src/qasm. qlink links OMF 1.0 and 2.0 from a script. makeBin flattens a load file, qomf dumps OMF, and qpro builds ProDOS volumes. APW and MPW assemble through the ORCA front end. Relocatable modules emit OMF objects. Listings stay off unless -l is set.

make boot assembles the GS/OS 6.0.1 ProDOS 8 kernel and BASIC.SYSTEM into an 800K volume. BASIC.SYSTEM accepts ls and pfx, and its splash reports PRODOS BASIC 2.0, copyright 1983-2026. IIGS.601.SRC is the source and the BASIC.SYSTEM oracle that image is built from.
2026-10-16 14:41:55 -07:00
2026-08-31 11:20:44 -07:00
2026-08-31 11:20:44 -07:00

QuickAsm

QuickAsm (qasm) is a Linux cross-assembler for the 65816, and for the 6502 and 65C02 through XC. It reads source in more than one syntax, lowers every file to one common pcode stream, and assembles from that stream. The object is an absolute binary.

The original QuickAsm repository is now qasm-legacy. This tree is the current assembler.

The syntaxes are Merlin 16+, Merlin 32, ORCA/M, APW/MPW, LISA, and ca65. Merlin 16+ (Glen Bredon, Roger Wagner Publishing, 1988–89) is the language the encoder consumes. The others are rewritten into the same pcode, so one program can be written in any of them and assemble to the same bytes. APW is Apple's 65816 assembler, the ORCA/M language Apple shipped. mpw is the same dialect: the listing reports syntax=apw. Identification, pseudo-op rewriting, and the limits of that lowering are in doc/SYNTAX.md. The Merlin 16+ language contract is doc/DIALECT.md.

Edit source on Linux, assemble it with qasm, and run the object on a IIgs, an emulator, or the sibling m65816 FPGA core.

From source to pcode to object

source file
    → identify the syntax, or take --syntax
    → dialect front end writes one pcode stream
    → two passes over that pcode
    → absolute BIN, and a listing when -l is given

A file is identified by --syntax, a marker in the first 40 lines, native high-bit Merlin text, or a score of distinctive tokens. A tie stays Merlin 16+. One --syntax name applies to every file on the command line. Omitting it identifies each file on its own, so one qasm command can assemble a Merlin file and a ca65 file together.

The front end expands macros, and pulls in Merlin PUT and USE, ORCA/M COPY, MCOPY, and APPEND, and ca65 .include, while it writes the pcode. QuickAsm then walks that stream twice. Pass 1 assigns addresses and defines symbols. Pass 2 emits the object bytes and the listing. --dump-pcode writes the stream beside the source as <file>.pcode.

Several sources on one command line assemble independently, in parallel. Each file keeps its own syntax and its own object.

Startup matches Merlin 16+ on a IIgs: 65816 opcodes, MX %11 (M and X both 8-bit), case-insensitive labels, program counter $8000, and filetype $06 (BIN). Put explicit MX and XC at the top of a source so it matches a real 16+ assembly. TYP changes the type printed with the object. The bytes stay a raw image.

Native Merlin disk files are accepted as-is (bit 7 set, $A0 column tabs, $8D line endings). Linux text .s files with spaces, tabs, and LF work too.

Labels of 26 characters or fewer, lines of 255 characters or fewer, a macro nest of 15, and a condition nest of 8 stay inside the Merlin 16+ limits for a source that will also assemble on a real 16+.

Absolute BIN is the object qasm writes for a fixed origin. A relocatable module also gets an OMF 2.0 object. qlink links those into an OMF load file, and makeBin turns that load file into the raw image ProDOS can load. qomf prints an OMF file, including its header and relocation records. Under REL, an unknown label is 0 and the assembler still writes the raw image beside the object.

Build

QuickAsm builds and runs on Linux. The code is C++17. CMake is the build. The Makefile configures build/release and build/debug when CMakeLists.txt is newer than that cache, then builds the requested target. Release uses -O2 -DNDEBUG. Debug uses -O0 -g. Executables are copied under bin/, which is not version controlled. make produces release bin/qasm, bin/qlink, bin/makeBin, and bin/qomf, and the same four names under bin/debug/. make release and make debug build one configuration. make test uses the release build and also copies bin/test_qasm.

qasm, qlink, makeBin, and qomf each have a major, minor, and patch number in CMakeLists.txt. Debug and release each have a build number that increases by one every time that configuration is built. --version prints all of it, for example qasm 0.1.0 build 3 release.

make
make release
make debug
make test
make fixtures
./bin/qasm test/fixtures/hello.s
./bin/qasm a.s b.s c.s

make builds release and debug qasm, qlink, makeBin, and qomf. Several source files on one command line assemble in parallel. make test runs the unit tests, assembles test/fixtures/hello.s and test/test.s, assembles the six coverage fixtures, and requires those six objects to be byte-identical. make fixtures assembles and compares only the coverage fixtures. Run both from the repository root. PUT and USE paths are relative to that directory.

make clean removes build/ and the .bin, .lst, and .pcode files the test targets write under test/.

Usage

usage: qasm [options] file.s [file2.s ...]
  -o FILE            object output (single file only)
  -l FILE            write a listing to FILE (- or stdout for standard output)
  --no-lst           do not write a listing
  -j N, --jobs N     max parallel assemblies (default: HW threads)
  -I DIR              add PUT/USE search directory
  --case-sensitive
  --algebraic
  --syntax NAME      merlin, merlin32, orca, apw, mpw, lisa, ca65, or auto
  --dump-pcode       write <file>.pcode beside the source
  -V, --version      print version and exit

With no -o, the object path is the DSK or SAV name from the source, or <file>.bin when the source sets neither. A listing is written only when -l FILE is given. FILE may be - or stdout, which writes the listing to standard output and the assemble summary to standard error. One named file applies to a single source; - or stdout can be used with several sources. The listing shows the program counter, object bytes, and the original line. Its header begins ; qasm <name> syntax=<dialect>. --no-lst turns the listing off again.

--algebraic evaluates every expression with operator precedence. Merlin braces {...} do the same for one expression. Merlin expressions otherwise run left to right, so 1+2*3 is 9. ca65 and ORCA/M expressions are rewritten into fully parenthesized Merlin form, so the same text is 7. LISA stays left to right.

--syntax auto, and omitting --syntax, identifies each file. One name applies to every file on the command line. An unknown name is an error. --dump-pcode writes the lowered stream to <file>.pcode.

The assemble line reports the byte count, origin, dialect, object path, and filetype. A -l path is included when a listing is written. A clean QuickAsm run looks like:

test/fixtures/hello.s: assembled 4 bytes at $008000 syntax=merlin -> test/fixtures/hello.bin (typ $06)

makeBin

qlink writes an OMF load file. ProDOS 8 loads a raw image, which is what MPW MakeBinIIGS extracts. makeBin does that step.

usage: makeBin [options] loadfile [loadfile...]
  -o FILE            output image (one load file only; - or stdout)
  -org ADDR          address of the first output byte ($2000, 0x2000, or hex)
                     default: the lowest segment address
  -V, --version      print version and exit
  -h, --help         print this help and exit
  With no -o, each load file is replaced by its raw image.

Byte 0 of the image is the -org address, or the lowest segment address when -org is omitted. A later segment leaves zeros in the gap. A segment below that address, an object or library, or a segment that still has relocations is an error. The file type and auxiliary type are not stored in the image.

makeBin apple.omf -org '$2000' -o apple.bin

qomf

qomf prints an OMF 1.0 or 2.0 file: each segment header, every record, and the relocation entries.

usage: qomf [options] file [file...]
  -V, --version      print version and exit
  -h, --help         print this help and exit

The dump lists the byte or block count, reserved space, length, kind, origin, alignment, load name, and segment name. CONST and LCONST bytes are shown in hex. RELOC and INTERSEG lines give the width, shift, offset, and displacement. INTERSEG also gives the file and segment numbers. Object files show GLOBAL and LOCAL with the length, type, and private flag, and RELEXPR with its expression. Each segment ends with a relocation count and a fixup count. Bytes after END are the block padding on an OMF 1.0 load file. A SUPER record is printed as its type byte and the bytes that follow it. A record whose size is not known, including ENTRY, stops the dump and is reported on standard error.

qomf prog.omf

qpro

qpro builds a ProDOS volume and copies files in and out of it. The image is a series of 512-byte blocks. A name ending in .2mg or .2img is a 2MG wrapper around those blocks. Any other name is the raw blocks. Boot blocks are left empty, so the volume mounts and does not boot.

usage: qpro create VOLUME -name NAME (-blocks N | -size SPEC)
       qpro cat VOLUME [PATH]
       qpro add VOLUME PATH HOSTFILE [-t TYPE] [-a AUX]
       qpro get VOLUME PATH HOSTFILE
       qpro rm VOLUME PATH
       qpro mkdir VOLUME PATH
       qpro typ VOLUME PATH [-t TYPE] [-a AUX]
  -name NAME         volume name
  -blocks N          decimal block count (7 to 65535)
  -size SPEC         140k, 800k, or 32m
  -t TYPE            file type ($06, 0x06, or hex); add defaults to $06
  -a AUX             auxiliary type; add defaults to $0000
  -V, --version      print version and exit
  -h, --help         print this help and exit

-blocks is a decimal count. -size 140k is 280 blocks, 800k is 1600, and 32m is 65535. A path is DIR/FILE, or /VOLUME/DIR/FILE when the path starts with the volume name. Names are stored in upper case. add refuses a name that is already there. rm refuses a directory that still has files. mkdir does not create missing parents.

The file type and auxiliary type live in the directory entry. Quote them in the shell so $ is not expanded.

qpro create disk.po -name DISK -size 140k
qpro add disk.po BASIC.SYSTEM basic.bin -t '$FF' -a '$2000'
qpro get disk.po /DISK/BASIC.SYSTEM basic.bin
qpro typ disk.po BASIC.SYSTEM -t '$06' -a '$2000'

GS/OS extended files stay in the directory. qpro does not read or write their resource forks.

Syntaxes

Dialect --syntax How a file is recognized
Merlin 16+ merlin Default when the text is ambiguous. Native high-bit, $A0, or $8D files.
Merlin 32 merlin32 Same language. Assembler: Merlin 32, or GEQU / GEQ.
ORCA/M orca longa, anop, mcopy, dc i1'...'.
APW/MPW apw Apple's assembler. mpw selects it too. Assembler: APW, Assembler: MPW, or absaddr, msb, codechk, instime.
LISA lisa .OR, .EQ, .DA, .HS, .EP.
ca65 ca65 .org, .byte, .p816, .macro.

A marker in the first 40 lines wins: syntax: ca65, Assembler: Merlin 32, or Assembler: APW. Assembler: MPW selects APW.

Macros expand while the source is lowered, so Merlin ]1 text and LUP's @ letter still work when the assembler runs the pcode. PUT and USE are pulled in at that step, including when they sit in a false conditional. ca65 symbols are case-sensitive. ORCA/M CASE ON is too. Segments are flattened into the absolute image. A LISA EPZ name is forced to direct page on later instructions.

ORCA/M AIF, SETA, and LCLA, and ca65 anonymous : labels, are rejected. The full pseudo-op tables and the remaining limits are in doc/SYNTAX.md.

Tests

The coverage fixtures are one program, written once in each syntax:

  • test/fixtures/coverage-merlin.s
  • test/fixtures/coverage-merlin32.s
  • test/fixtures/coverage-orca.s
  • test/fixtures/coverage-apw.s
  • test/fixtures/coverage-lisa.s
  • test/fixtures/coverage-ca65.s

make test and make fixtures assemble them and compare each object with coverage-merlin.bin. A match is silent.

The relocatable fixtures are two sections that call and address each other. rel-code-*.s exports CODE, calls HOOK, and loads VALUE. rel-data-*.s exports VALUE and HOOK, calls CODE, and loads it. The six syntaxes of each section match. qlink will link the pair into an OMF. To compare the objects by hand after a run:

cmp test/fixtures/coverage-merlin.bin test/fixtures/coverage-merlin32.bin && \
cmp test/fixtures/coverage-merlin.bin test/fixtures/coverage-orca.bin && \
cmp test/fixtures/coverage-merlin.bin test/fixtures/coverage-apw.bin && \
cmp test/fixtures/coverage-merlin.bin test/fixtures/coverage-lisa.bin && \
cmp test/fixtures/coverage-merlin.bin test/fixtures/coverage-ca65.bin && \
echo match

testdata/ is the faddenSoft Merlin 32 corpus (Apache 2.0). It is a comparison suite and is not part of make test.

Documentation

  • doc/DIALECT.md — Merlin 16+ line format, expressions, locals, and directives
  • doc/SYNTAX.md — identification, pcode, per-dialect pseudo-ops, and limits
  • doc/manuals/Merlin_816_Macro_Assembler_Manual.pdf — Merlin 8/16 user’s manual
  • doc/manuals/Merlin_16_Supplementary_Manual.pdf — Merlin 16+ additions
  • doc/manuals/Merlin_16_Utilities_Manual.pdf — 16+ disk utilities, outside this assembler

License

MIT. See LICENSE.

S
Description
Merlin 8/16/16+ Compatible 6502/65816 assembler/linker suite in C++ for linux
Readme
41 MiB
0 Stars 1 Watchers 0 Forks
Languages
Assembly 40.4%
C++ 35.3%
C 23.6%
CMake 0.3%
Makefile 0.2%
Other 0.2%