Releases
Every version, from the changelog the assembler ships. Downloads and install commands are on each GitHub release.
lua, while WebAssembly retains an explicit feature-unavailable diagnostic.IFN as the negated expression conditional, with the same
dotted, case-sensitive, lazy, and forward-reference behaviour as IF (#582).DEVICE/SLOT/PAGE mappings and
implement source-ordered SAVECPR cartridge artifacts (#563).SAVEBIN read
unwritten live memory, including the ROM-derived classic Spectrum state
(#318).ORG and negative DS/DEFS/BLOCK now move the logical address
without seeking in the append-only raw output. Negative reserves wrap at 16
bits and retain the reference’s Negative BLOCK? advisory; device-memory
writes land at the rewound address and remain last-write-wins.F and B suffixes from
JR, JP, DJNZ, and CALL, choosing the nearest definition in textual
order across includes and macro expansions. The contextual spelling leaves
binary literals elsewhere unchanged, and temporary definitions do not
re-anchor dotted locals. SpecNext Invaders now advances to its next genuine
boundary, logical && in an ASSERT expression.* works in !fill count expressions.
(#555)
The current program counter can now size padding, including the idiomatic
!fill target - *, value form used to extend a cartridge image to its
vector table. Its existing meanings in data, instruction operands, and
symbol assignments remain unchanged.END now stops the whole assembly.
(#554)
Pasmo and sjasmplus stop before every line after a live END, including
when it is reached through an include or macro expansion. Sjasmplus’s
dotted .end spelling behaves identically, while an END in an untaken
conditional remains inert. An optional entry-point expression is still
recorded for snapshot output.crates/asm198x-web wraps the library in wasm-bindgen glue:
assemble(dialect, source) returns the same JSON the CLI prints with
--message-format=json, listing() the --listing text, and
dialects() the dialect table. Every dialect is in one 1.4 MB module
(490 KB over the wire). The site’s playground at
asm198x.github.io/playground/
is built from this release, so what runs in the tab is what this entry
describes. One file per run for now; multi-file input, --cpu, and
vasm’s --exe are deferred (#573).IF, and a duplicate
name now error as SjASMPlus 1.21.0 errors, where they used to assemble.
Nine cross-file shapes probed against the reference, all matching.STRUCT instances take initialiser lists.
(#550,
#556)
label Name { v0, v1, … } — or the same list without braces — fills the
members in declaration order at each member’s width; an empty slot or a
short list keeps the default, a DS member takes no slot, and values may
reach forward. Lists span lines, one value per line with comments, and an
embedded structure takes a nested { … } group. The reader follows the
reference’s model, reconstructed from 34 probes against SjASMPlus 1.21.0:
a line end ends a value, a comma is only consumed on the same line, and
an unclosed brace at end of source is closing } missing. Too many
values is refused as the reference refuses it..end ld (x),a under top defines top.end and assembles
the load. pasmo keeps its behaviour — PasmoNext assembles a column-0
nop as the instruction, and was probed to confirm it.ADD/ADC/SBC is the accumulator form.
(#549)
add (hl) reads as add a,(hl), the mirror of the existing sub a,b →
sub b normalisation. Only the one-operand case: add hl,de and the
16-bit forms are untouched, and a lone hl is still an error. pasmo
rejects the spelling and continues to.DEVICE AMSTRADCPCPLUS.
(#562)
The fourteenth device in SjASMPlus 1.21.0: 32 pages of 16 KB in four
slots — the count follows the largest cartridge SAVECPR accepts, not
the 6128’s RAM. SAVECPR itself is still refused by name; it waits on a
memory image the engine does not yet keep
(#563).DS/DEFS/BLOCK counts resolve across the passes.
(#534)
A count that reaches forward — an EQU below it, a label the count
itself moves, a STRUCT member sized by a later constant — now settles
over the three passes the way conditions do, warning where the reference
warns. A never-defined symbol in a count or a condition is now an error
on the last pass, as in the reference, where an IF on one used to
assemble with an advisory. A negative count is refused naming the
reference’s behaviour: this engine cannot move the origin backwards.in expansion of macro … under an error read defined at line 1, invoked at line 3 even when the macro lived in an included file. It now
reads defined at bad.asm:1, invoked at p10.asm:3, in acme and
sjasmplus alike, matching what the JSON output already reported.STRUCT … ENDS.
(#529)
A structure binds its total size and every member’s offset — embedded
structures flatten to dotted paths (InvaderBullet.hitboxScreenSpace.x0)
— and each name resolves like any symbol, forward references included.
Member spellings BYTE/DB, WORD/DW, D24, DWORD, and DS, with
or without initialisers and labels; label Name lays the defaults down
and binds the instance and each member to its address. A structure
defined under a MODULE takes its prefix, and re-anchors the locals
that follow it. Every accepted shape is probe-pinned against SjASMPlus
1.21.0 and byte-identical in the recorded differential; the refusals —
a stray ENDS, an unclosed STRUCT, a statement inside a body, a
duplicate member — error as the reference errors. The pinned SpecNext
Invaders corpus advances past its structures to its next genuine
boundary, a forward DS length (#528)..define/.undefine work inside conditionals and repetitions — a
.repeat body may define, use, and undefine per iteration — a taken
branch’s definition flows to the rest of the source, and an untaken
branch touches nothing at all: no definitions, no constants, no
cheap-local rescoping, and its .include target is never opened.
Probing also matched three edges to the reference: redefining a live
text symbol errors, .undefine of an unknown name errors, and
.ifdef of a live text symbol substitutes its argument like any other
use. All probe-pinned against ca65 V2.18, byte-identical across the
curriculum and a six-behaviour differential.asm198x convert --from pasmo --to sjasmplus prog.asm reads pasmo source
and re-emits it as sjasmplus through the source-preserving AST — labels,
names, comments, and structure survive, so the output is source, not a
disassembly. Output is written only when assembling the input under
pasmo and the output under sjasmplus produce byte-identical images; a
conversion that cannot prove itself is a reported error naming the first
divergence. The structural rewrite worth knowing: pasmo closes REPT
with ENDM — the same word that closes a macro — and the converter
rewrites the repetition’s closer to ENDR from the block structure while
leaving a macro’s ENDM alone. Every eligible single-file source in the
Spectrum curriculum (226 of 226) converts verified.-C project.cfg hands the ca65 path a project’s own ld65 configuration
instead of the built-in curriculum layout. The reader is bounded to the
shapes real projects use — MEMORY and SEGMENTS with their placement
attributes — refusing anything outside the bound by name; segments
without a start place sequentially after their area-mates, exactly as
ld65 places them. The pinned bbbradsmith/NES-ca65-example builds
byte-identically to ca65 2.18 + ld65 with its own unmodified config.
Without -C, nothing changes.--map reports each memory area of the active layout — capacity, units
used, units free, and the largest contiguous free run, which a pinned
VECTORS can make smaller than the free total suggests — with the
segments placed in it; flat dialects report their one implicit area, the
64K address space. The same figures ride the JSON listing and the JSON
result. ; asm198x: free(PRG) >= $1000 joins cycles() as a
build-failing assertion: assembly fails when an area’s headroom drops
below the floor, naming the requirement and the actuals. Budgets are now
also checked on the ca65 path, where a cycles() assertion errors
honestly (that driver captures no cycle data yet) instead of being
silently ignored.AreaUsage and SegmentUsage are exported.
(#522)
Both types appear in public fields (Assembly.areas,
AssemblyResult.areas) but were not re-exported from the crate root, so
a consumer receiving .areas could not name the type it holds — and the
intra-doc link on those fields failed the Docs gate.--listing now annotates each instruction with its cost from that spec: one
number when fixed, 4/5 when a page-cross or branch-taken extra makes it a
range, never a collapsed figure. A tail totals each label’s straight-line
span, and --listing-json carries the same data for tools. On top of that,
; asm198x: cycles(irq) <= 224 asserts a routine’s worst case and fails
the assemble when the budget is exceeded, naming the routine, the budget,
and the actual cost — the raster programmer’s contract, checked like a test.
Reference assemblers read the line as the comment it is, so the source still
assembles byte-identically elsewhere. A CPU whose spec has no cycle data yet
says no cycle data (backfill pending) instead of inventing figures.!cpu selects the instruction set
lexically (#466), and the
targets it can name grew across the 6502 family: the 6510
(#467), the CMOS, Rockwell and
WDC 65C02 variants, the C64DTV2, and the 65CE02 and 4502
(#472).OPT --syntax=abfw, OPT --zxnext, and
OPT --zxnext=cspect now carry their lexical parser and target state into
following source and includes, including CSpect’s BREAK and EXIT fake
instructions (#474). This
clears SpecNext Invaders’ first corpus boundary.SLDOPT COMMENT accepts and validates
the source-level-debug keyword list
(#476). Native probes
establish that it changes only SjASMPlus’s optional .sld sidecar, so it is
explicitly inert for Asm198x’s machine output and Debug198x contract.~ with native two’s-complement semantics, including composition and
the reference assemblers’ existing truncation at byte/word output widths
(#478)..define/.undefine
state threads across ordinary source and nested includes
(#480); a definition inside a
deferred conditional branch is refused with an explicit diagnostic rather
than silently misassembled, until it is implemented at the projection layer.% parses as modulo (#459),
and a program that moves * backwards places its discontiguous regions the
way ACME does (#465).rgbfix’s
header finalisation (#451)
and banks pad completely
(#450); floating RAM sections
place (#448); and the
expression and data surface gains definedness queries
(#446), operandless storage
(#444), graphics literals
(#442), STARTOF
(#440), DS counts resolved
at the current location
(#438), RS offset counters
(#436), and numeric digit
separators (#432).--equ NAME=VALUE definitions
are available before conditional assembly, so one source tree can build
multiple Pasmo and PasmoNext targets without wrapper source files.ld ixl,a.:;
colons inside quoted literals remain literal data.end, including its optional ignored operand, and stops the whole source
stream when the directive is reached—even from inside an included file.move.b #';',(a0)+ assemble as vasm does.charset source,replacement; mappings apply to strings and
character literals, accumulate across included source, and reset to the
identity table with a bare charset, matching ASL 1.42.enum lists. Members start at zero, explicit assignments reset the running
value, later assignments may use earlier members, and each new directive
starts a fresh sequence; the resulting constants are included in symbol and
Debug198x output.radix 2 through radix 36. The selected base applies to unadorned numeric
tokens, constants, instruction operands, and included source; changes made
inside an include remain live when assembly returns to its caller.phase/dephase regions, so labels and PC-relative operands
use the claimed address while emitted bytes remain at their real location.
Edge cases match asl 1.42: an unmatched dephase is inert, and a phase may
remain open at end of source.align boundary[,fill], including non-power-of-two boundaries. The default
$FF holes match asl plus p2bin; custom fill values occupy one target
address unit, including full 16-bit decles on the word-addressed CP-1600.mod emits and checksums the four- or six-field
module header, establishes module-relative address zero, os9 emits the
system-call sequence, and emod closes the module with OS-9’s CRC-24. The
complete module is byte-identical to lwtools 4.25’s OS-9 output.lwasm string symbols and generated source. setstr now defines and
redefines general-string values, %(name) interpolates them where lwtools
does, includestr assembles the constructed source lazily, and all twelve
ifstr comparison forms are available. Generated source in an untaken
conditional is never parsed.
Pasmo projects can include source files. include "file" now uses the
same loader-backed, requester-relative project model as the other multi-file
dialects. Nested includes share constants and labels with their includer,
preserve per-file diagnostics, and are loaded only in taken conditional
branches.
asm198x crate to crates.io for the first time. The executable
ISA specs and shared disassemblers have graduated with their history to the
neutral Isa198x project as isa198x and isa198x-disasm 0.1.0. Asm198x now
consumes those registry releases instead of owning their source, and Emu198x
can version the same shared layer without raw commit pins.Two Spectrum tape targets, and one crate leaves the workspace. --tap and
--tzx complete the tier-one Spectrum outputs alongside .sna, and
debug198x — the debug-info format Asm198x writes and Emu198x reads — now
comes from crates.io rather than from this repository.
.tap and .tzx, with pasmo’s auto-run BASIC loader. --tap,
--tapbas, --tzx and --tzxbas produce output byte-identical to PasmoNext
v0.1.3. Two behaviours are worth knowing before scripting them: the tape
block name is the output path as given, clipped to ten characters and
space-padded — sub/o.tap names the block sub/o.tap, slash and extension
included — and the loader’s RANDOMIZE USR line is written only when the
source has an end directive, so a source without one loads and stops. The
.tzx files are version 1.13, which is pasmo’s.
(#369)debug198x is a published crate rather than a workspace member. The
debug-info format is the contract between Asm198x and Emu198x, and it now
lives at debug198x/debug198x and on
crates.io. Emu198x had been pinning
three crates out of this repository at two raw revisions; debug198x = "0.1"
replaces that arrangement for one of them. Its released versions restart at
0.1.0, because the old 0.0.x numbers were this workspace’s lockstep
version and said nothing about the format. Nothing about the emitted sidecar
changes — the full suite passes unchanged against the published crate — and
the spec moved with the code, so decisions/debug198x-format.md is now in
the new repository.
(#372)
The toolchain tracks Rust 1.98.0, up from a 1.95.0 pin that had been inherited by copying a sibling repository rather than chosen. One new lint had a single site in the workspace. Both compilers give the same 1223 passing tests. (#362)
The 6809 dialect said indexed addressing was unsupported. It works. Three
comments claimed it, one of them on a public entry point that ships to
docs.rs — so someone evaluating the crate for 6809 work read that it could
not assemble most real 6809 code, indexed being the 6809’s characteristic
mode. The same header called the register-list operations tfr, exg,
pshs and puls “the next increment”, and those work too. All of it is
byte-identical against lwasm --6809 --raw. What is genuinely outstanding on
lwasm is ten directives, and the replacement comments point at
cargo xtask surface for that rather than restating a list that can go stale
the way these three did.
(#364)
27 rustdoc warnings, four of them broken links on the first page a reader
lands on. asm198x’s crate-level architecture paragraph links to engine,
Dialect and dialects to explain the engine ↔ dialect ↔ spec seam; all
three are private, so docs.rs rendered them as plain text with no sign a link
was intended. sna_48k named a type that has never existed. CI now builds
the crate documentation with -D warnings, which is the part that stops the
count growing unseen again.
(#366)
The release where the references’ string features stopped being a gap. ca65 and
rgbasm both express whole idioms through text the assembler builds before it
parses — STRFMT, .sprintf, .concat, .ident, .match — and none of it
existed here. It does now, and as a source pre-pass rather than a second type in
the expression language, which is a decision with a record behind it. 364
outstanding reference words to 238: ca65 alone goes from 78 to 24.
Strings are a source pre-pass, not a second type in the expression
language. An expression evaluates to an i64, and continues to. The
references’ string features are resolved before the parse, in the shape macro
expansion already used: symbols are collected, functions are folded to the
text they produce, and the ordinary parse reads a string literal or a number
as it always would. The alternative — a string type in the expression tree —
would make every dialect’s evaluation two-typed and still could not answer
ca65’s token-list functions, which compare things that are neither number nor
string. The one case a pre-pass cannot reach is a string function applied to a
label’s address; ca65 refuses that itself, and rgbasm’s version of it is
refused here by name rather than answered wrongly.
(#350)
rgbasm’s string vocabulary — thirteen words. EQUS, STRCAT, STRUPR,
STRLWR, STRSUB, STRSLICE, STRLEN, STRCMP, STRFIND, STRIN,
STRRIN, STRRPL and STRFMT. The index conventions are the part worth
knowing: STRSUB is 1-based and takes a length, STRSLICE is 0-based and
takes an end, STRFIND answers a 0-based index or -1, and STRIN
answers a 1-based one or 0. STRFMT is printf’s shape and not printf’s
rules — %#x writes $ff, %#f appends q16 as a suffix, %f reads its
argument as a raw Q16.16 value, and the flags come in a fixed order, so %+#x
assembles where %#+x does not.
(#351,
#353)
ca65’s string vocabulary — .concat, .string, .ident, .sprintf.
.string stringifies its argument’s token rather than its value: with
N = 7, .string(N) is "N". .ident builds a name from text and resolves
it like any other, forward references included. .sprintf is C’s shape with
ca65’s own departures — %x is signed where %X is not, so the two disagree
about a negative value; %s and %c pad on the right by default and on the
left with -, the reverse of every other conversion; and # on %x shows
its prefix even for zero. All 135 measured cases are in the test.
(#354,
#357)
ca65’s token lists — .match, .xmatch, .tcount, .blank, .left,
.mid, .right. A token list is unevaluated source, so these answer over
what is written. .match asks what each token is and .xmatch asks
what it says: .match({1},{2}) is 1 and .xmatch({1},{2}) is 0, while
.match({a},{b}) is 0 because a is the accumulator and b is a name. The
register set follows the CPU, which is observable — .match({s},{q}) is 1 for
a 6502 and 0 for a 65816, where s is the stack register.
(#359)
ca65’s four remaining predicates — .const, .ismnem, .paramcount,
.definedmacro. .const answers 0 for a constant defined below the line,
which is what a pass walking in source order sees anyway, and errors for a
name defined nowhere, which is what ca65 does rather than answering 0.
.ismnem follows the CPU. .paramcount counts the call site and not the
declared parameters, so a macro with two of them called with one answers 1.
.definedmacro is answered above the line that asks — for a line inside a
body, the line the macro was invoked on.
(#360,
#361)
rgbasm’s fixed-point arithmetic. Q16.16 literals with an optional q
precision suffix, MUL, DIV, FMOD, FLOOR, CEIL, ROUND, TZCOUNT,
HIGH, LOW and dl. Only the operations whose answers are exactly defined;
the transcendental ones remain a named gap rather than an approximation.
(#343)
vasm goes from 87 outstanding words to 53. The listing controls, and the
two that say something — printt and printv. Seven more conditional heads
(ifb, ifnb, ifc, ifnc, ifmi, ifpl, elseif), where elseif reads
its argument and ignores it. The offset counters, which turn out to be two
rather than one: rs and so share a counter and fo counts down.
(#345,
#347,
#349)
sjasmplus’s sixteen data directives beyond the shared set. The dc/dz
string forms, the wide and graphic byte spellings, and the marked variants —
where dc marks the last character of each string, not of the whole list.
(#348)
ca65’s structure, reference and listing vocabulary. .proc and .scope,
with names inside them qualified as ca65 qualifies them. The record types
.struct, .union, .enum, .tag and .sizeof. .ref/.referenced and
the two conditionals over them, where a use in a branch the assembler never
took does not count. .org, .reloc and .end — .org moves the address
and not the bytes. The processor words, with the processors we do not assemble
named as the gap rather than silently accepted. And the eight words that
address the listing rather than the bytes.
(#334,
#338,
#339,
#340,
#341,
#342)
A fixed-point literal truncated where rgbasm rounds. 0.1 assembled to
$1999 against rgbasm’s $199A, and 0.3 to $4CCC against $4CCD. The
test that should have caught it pinned 3.7 as $3B333 and read that as
evidence of truncation — but 242483.2 rounds down, so both rules agree
there, and the one probe that would have told them apart was never asked.
(#352)
The formatter turned an else-if chain into source the assembler refused.
A leg is stored as a conditional nested in the else branch, and it came back
out flat only for .elseif; rgbasm spells it ELIF, so its chains were
re-emitted as ELSE followed by ELIF. The closer was the same bug’s other
half: the author writes one, the walk gives it to the innermost leg, and the
outer block then derived ENDIF — which rgbasm refuses outright. With that
honest, vasm’s elif was no longer blocked and landed alongside it.
(#355)
rgbasm’s formatter was never checked, and could not format the text
layer. The invariant that says fmt must read what asm reads walks a list
of dialects, and rgbasm was missing from it — so eight probes assembled and
would not format with nothing to say so. All eight now do, and the ledger of
known formatter gaps stays empty over one more dialect.
(#356)
The release where source compatibility stopped being a claim about a few dialects and started being measured against all six references. 476 words the installed reference assemblers accept and asm198x did not are now 364; ACME reaches zero. And an assembler can now write the files a source asks for besides the machine code — including a Spectrum tape image.
A source can name the files it wants written. ACME’s !to, vasm’s
output, sjasmplus’s SAVEBIN and SAVETAP all state an output file in the
source rather than on the command line, and each is now honoured. SAVETAP
writes a real .tap: the tape image the Spectrum’s ROM loader reads, built on
format198x-sinclair-zx-spectrum-tap, a crate graduated out of the emulator
into Format198x and published so anything can read one.
(#319,
#320,
#321)
ACME assembles what ACME assembles. Every word ACME 0.97 accepts is
accepted here: the conversion tables !ct/!convtab, the endian data family
!be16 through !le32, the condition loops !while/!do, the second
location counter !pseudopc, !xor and !scrxor, !addr, !skip,
!initmem, !as, !rs, !eof, !fi, !raw, !hex, !symbollist and
!cpu 6502. 31 outstanding words to none.
(#289 through
#303)
lwasm goes from 53 outstanding words to 10. The block and string data
forms, the listing controls, warning/msg, setdp (which emits the offset
within the page), phase/dephase, set and plain if on a redefinable
symbol, incl/lib/reorg, struct/endstruct/ends, and the
pragma/opt/ifpragma/ifopt family across 22 switches. The words it
still refuses, it refuses by name and says why.
(#304 through
#312)
ca65 goes from 113 to 78. Its operator vocabulary in both spellings —
&& and .and are one operator, and the keyword now lands on its symbol
twin’s token rather than getting a second, nearly-right precedence. The
plural byte extractors .lobytes/.hibytes/.bankbytes/.faraddr. Nine
conditional heads beyond .if: .ifblank, .ifconst and the .ifpNN CPU
tests, where .ifconst follows ca65’s own rule that a difference of two
labels in one segment is constant and a label alone is not.
(#323,
#324,
#326,
#330)
A data list split inside a function call. .word .max($100, $200), $3
was read as three items rather than two, because the operand splitter counted
commas without counting parentheses. Anything passing a two-argument function
in a data list assembled wrongly or was refused.
(#322)
A value’s range came from its width rather than its dialect. The engine
held one hardcoded answer — -128..=0xFF for a byte, 0..=0xFFFF for a word
— where the references disagree on two counts: ca65 refuses a negative value
a byte directive would take elsewhere, and lwasm, vasm, rgbasm, pasmo and
sjasmplus truncate a value out of range where acme and the asl-backed
dialects call it an error. Probed at the corners across all seven.
(#291)
ACME’s retired spellings read as our gap. Words ACME itself removed were declared unimplemented, which put them on the wrong side of the ledger: they are the reference’s refusal, not ours. (#288)
None of this changes what the assembler does. It changes what is proven, and what a wrong claim costs.
Every 68000 form is arbitrated against vasm. All 838 rows of the specification, encoded and compared against the reference’s own bytes. (#284)
Form coverage counted verdicts, not forms. A form arbitrated by two
versions of one tool counted twice, so a 68000 sweep under both installed
vasm builds published “Form coverage: 1676/838 (200.0%)”. It counts distinct
forms now, keyed on the form’s label rather than its source text — two forms
can share source, since an assembler that canonicalises move.w d1,a2 to
movea.w d1,a2 writes one line for both.
(#331)
A version claim in the source has to name a version that was recorded. Prose here cites reference-tool behaviour by version constantly, and nothing checked those citations against the corpus that observed them. (#317)
The ledger’s pinned curriculum date is checked against the commit it names. The revision itself is a CI checkout ref and cannot drift unseen; the date beside it was hand-maintained and checked by nothing. (#286)
The parity goal is written down, with a register of deferrals. Every word a reference accepts and asm198x does not is either outstanding or deferred with a stated reason, and a deferral with no record is treated as a backlog item wearing a decision’s clothes. (#313)
Three changes to what asm198x accepts or emits, and a body of work behind them proving the rest of it right. If you assemble Z8000 source, this release changes your output — see the first entry.
LDB Rbd, #data assembled to four bytes where the reference produces two.
The Z8000 has two encodings for a byte immediate loaded into a register, and
the CPU manual settles which to write: “although two formats exist for
LDB R, IM the assembler always uses the short format”. asm198x wrote the
long one, and could not read the short one back at all — a binary built by
asl disassembled to a word directive where the instruction should be. It
now writes the short form and reads both. Because this is a size change and
not only an encoding choice, anything position-dependent after an LDB #
moves.
(#254,
#252)
A replay mismatch on a sweep chunk printed the byte vectors and nothing else. The largest chunk is 4,096 instructions, so a single failure was around 147,000 characters that never said which instruction moved. It now names the first differing byte, the source line and the instruction there. (#269, #268)
reset, rhf and hcf. lwasm --6809 assembles all
three, so source using them is no longer refused; the bytes match, 3E 14 14.
The disassembler never writes them, and that asymmetry is deliberate: $14
is SEXW on the Hitachi 6309, so fcb $14 is the reading that holds whichever
part produced the byte, while hcf would assert both the part and that the
byte was meant as code. Neither opcode has a defined result, so no working
program contains one on purpose.
(#257,
#233)xref, import and nref are accepted when the name is defined.
vasm 2.0b refused them in binary output from both directions at once, so no
program satisfied them and asm198x refused them too. 2.0f accepts them, which
is the same rule as the seven visibility words beside them. Matching the
reference means this changes with the reference.
(#282)None of this changes what the assembler does. It changes how much of it is proven against a real reference assembler rather than asserted.
The Z8000 and Z8001 go from nothing to every row. Both now arbitrate all
271 rows of their specification against asl — encoding a representative
instance of each row, disassembling it, and comparing the reference’s bytes.
The previous attempt placed 42 and was abandoned. The 6809, PDP-11, TMS9900
and CP-1610 gained the same audit; eighteen of twenty CPUs now arbitrate every
row they declare, and the two that do not say in the repository why.
(#242 through
#259)
A conformance ledger is published with the documentation, generated from the corpus and held to it on every pull request, so a release cannot carry one that has stopped being true. It names the release, the corpus hash, the pinned curriculum revision, and per CPU the arbiter, its version and what it proved. (#266)
Arbitration coverage is governed rather than reported. A shortfall states its size and its reason, a CPU that arbitrates nothing cannot merge, and a release will not tag while any shortfall is still owed. (#261, #262, #263)
Two correctness fixes, both bigger than the issues that reported them. If you assemble CP-1600 source, this release changes your output — see below.
The CP-1600 byte directive emitted the wrong bytes, and said nothing.
asl’s BYTE takes a 16-bit operand and emits its two bytes low-first, one
byte per decle: byte x'1234' is the decle pair 0034 0012, and byte 1 is
0001 0000. asm198x read the operand as 8 bits and packed one raw byte per
item, so byte 1,2,3 produced three bytes where asl writes six decles. Any
CP-1600 program with a byte table was building a different image from the
one asl builds. It now matches, byte for byte.
The issue behind this suspected byte and binclude of disagreeing with each
other. They never did — asl’s listing shows one rule, one byte per decle, and
only byte was misread.
(#235,
#227)
Nine 6809 instructions were missing, and lwasm-syntax source using any of
them was refused outright: adca, adcb, sbca, sbcb, bita, bitb,
cmpd, cmpy and cwai. Add and subtract with carry, bit test, the two
16-bit compares and wait-for-interrupt — bita and cmpd appear in ordinary
6809 source. All four addressing modes each, 33 forms, every encoding read
from lwasm and cross-checked against Motorola’s 1981 programming manual.
(#232,
#225)
Five CP-1600 spellings are no longer accepted: db, dc.b, data, dw
and dc.w. asl has none of them on this chip — each is an unknown
instruction there — so source using them was never valid CP-1600 asl source,
and asm198x accepting them meant a program that built here failed against the
tool it claims compatibility with. If your source uses them, replace them with
byte and word.
(#235)
A CP-1600 byte statement with a string or character operand now warns.
asl drops the whole statement — byte 1,"AB" emits nothing, the 1
included — and reports neither an error nor a warning. The bytes are
unchanged and still match asl exactly; asm198x now tells you the statement
produced nothing, and that the numeric operands beside the text went with it.
(#237)
The release that closes four directive families across every reference at once — diagnostics, assertions, comparisons and symbol visibility — and rebuilds placement so the dialects with sections stop hand-rolling their own. Source that uses any of these assembles now where it was refused before.
Comparison operators in expressions, in all seven dialects that have them.
Two facts here are per-dialect data, not a shared rule: true is $FF in
vasm, sjasmplus and pasmo and 1 in ca65, acme, rgbasm and lwasm — dc.b 2=2 is $FF and .byte 2=2 is $01 from the same source shape — and the
accepted spellings differ, with sjasmplus refusing <>, pasmo refusing both
== and <>, and lwasm refusing =, <= and >=. Each was probed against
the tool rather than read from a manual.
(#231,
#229,
#230)
Assertions: sjasmplus ASSERT, vasm assert, rgbasm ASSERT and
STATIC_ASSERT, ca65 .assert. They fold after the symbols resolve, because
an assertion reaches forward — ASSERT fin-beg above both labels is the
point of having one. ca65’s takes an action operand, so warning assembles
anyway and error does not.
(#231)
Directives that say something: vasm echo, rgbasm PRINT/PRINTLN,
sjasmplus DISPLAY, ca65 .out for text; acme !error/!serious/!warn,
lwasm error, rgbasm FAIL/WARN, ca65 .warning/.error/.fatal for
diagnostics. Print-style output arrives as a new WarningKind::Note rather
than as a warning — it is not a complaint — and each reference’s own radix is
used, so a value reads 5 under vasm, $5 under rgbasm and 0x0005 under
sjasmplus.
(#231)
Symbol visibility across every reference. In a fused assemble-and-link
these are checks rather than no-ops, and accepting them silently would take
source the reference refuses: ca65 .export/.exportzp and vasm
xdef/public/global/export/entry/weak/extrn/comm require the
name be defined; ca65 .import/.importzp require that it is not; ca65
.global/.globalzp/.autoimport, vasm local/idnt and rgbasm EXPORT
ask nothing. .export name := expr defines the name it exports, and the zp
spellings warn for a label outside the zero page but never for a constant.
(#231)
sjasmplus’s DEVICE, PAGE and SLOT. Thirteen devices with their real
page and slot bounds, and the write check that comes with them — which is on
the 64K address space rather than on total memory, so a program that overruns
$FFFF is refused even on a device with 8MB of pages. Two pages written at
one address concatenate rather than colliding.
(#231)
ca65 expression functions: .lobyte, .hibyte, .bankbyte, .loword,
.hiword, .max, .min, .strlen, .strat, and .defined/.def.
.defined is answered in source order — 0 above the definition and 1
below — while rgbasm’s BANK() is answered after the whole program is read,
because it reaches forward. Opposite treatments, and a careless test passes
either way.
(#231)
Alignment and padding: ca65 .align, sjasmplus ALIGN, vasm
align/cnop, and ACME’s pad-to-a-stated-boundary, which is not the
alignment ACME already had. vasm’s operand is an exponent — align 2 is a
four-byte boundary — and its cnop pads with whole NOP words where one
fits.
(#231)
Segment shorthands for the two dialects that have segments: ca65’s
.code/.zeropage/.bss place as their spelled-out segments, and
.pushseg/.popseg restore the segment a reservation interrupted.
(#231)
The directives each reference has that asm198x does not are now declared rather than absent, so they are refused as real directives with a diagnostic saying the gap is ours — not as unknown words, which sends a reader with valid source looking for a typo. Ninety-seven for ca65, thirty-three for rgbasm, and the remaining four references swept the same way. (#222, #223, #224)
sjasmplus takes the optional leading dot on every directive, so .db and
db are one word to it and to us.
(#221)
equ was capped at 24 bits in every dialect, and three references allow
more. ca65, vasm and rgbasm all take a 32-bit constant, and pasmo turns out
to be the only reference that bounds one at all. Each dialect now states its
own range.
(#228)
Nine 6809 instructions were being reported as directives by lwasm’s declared surface. An instruction declared a directive points the reader at the wrong layer. (#226)
xtask surface counted a wider target as a gap, and two concurrent runs
corrupted each other’s probe files — one run reported 386 words where the true
figure was 538. Each run now gets its own scratch directory.
(#219,
#231)
A word the reference itself refuses now says so. lwasm export is
Only supported for object target; vasm’s xref, import and nref cannot
be satisfied in binary output at all; ca65’s .forceimport is unresolvable
either way. These used to be refused with “the source is valid and the gap is
ours”, which is wrong in both halves and sends a reader off to wait for a
feature that is never coming. They are no longer counted as outstanding work.
(#231)
Placement is one implementation. The NES ROM, a Game Boy bank, a sjasmplus
page and a flat program’s single section are laid out by the same code, where
ca65 and vasm previously each hand-rolled a pass. Warning gains a kind
field, additively.
(#231)
Nineteen asl directives that used to be refused as unknown mnemonics are
now answered properly. Five change nothing and are accepted and dropped —
outradix, message, warning, prtinit, shared. Fourteen change what
the source means, and are refused with a diagnostic that says the source is
valid and the gap is ours rather than that the word does not exist:
relaxed, radix, phase/dephase, align, enum, charset, segment,
save/restore, expect/endexpect, assume, function.
The distinction is the point. relaxed on makes db 012 emit ten instead of
twelve, so sweeping it into an ignore list would assemble a different program
and report success. Each of the nineteen was probed against asl on five chips
before being classified.
(#215)
supmode is now accepted on the TMS9900, which asl takes it on and we did
not. (#215)
CP1610 listings speak strict asl. asm198x disasm emitted Intel 0FFFFH
hex for the chip and opened each listing with relaxed on to make asl accept
it. Strict cpu CP-1600 takes its own x'FFFF' form and nothing else, so
that is what listings use now — and the assembler reads it, wherever it comes
from. (#218)
cargo xtask supersede --cpu <CPU> --suite <suite> retires verdicts by
scope rather than by divergence tag. Changing a generated listing strands
every recorded fact keyed on the text it used to emit — those facts stay true
and stop being about source the project produces — and there was no way to
retire them. Maintainers only; the corpus stays append-only either way.
(#218)Source that eight reference assemblers accept and this one refused now assembles. Six of those refusals are closed in this release, and the differential suite’s ledger of known gaps is empty for the first time — every snippet in it now matches its reference byte for byte.
: between statements, as hand-written Spectrum source
does — ld a,1 : ld b,2. A label’s own colon, ::, a colon in a string or
a comment all stay put.
(#210)name MACRO spelling as well as
MACRO name. (#207)<, > and <> in !if, and a one-character string
wherever it wants a number — !byte "a", lda #"a".
(#207)acme sized a backward zero-page label as absolute — lda lbl after
lbl at $00 emitted AD 00 00 where acme emits A5 00. Wrong size and
wrong byte count, on source with nothing unusual in it. The only one of these
that changed bytes rather than refusing to assemble.
(#207)
vasm accepted a name defined twice, where vasm itself refuses the program. That is the worst direction to differ in: an accidental collision got a working binary here and a build failure there. (#209)
Three advisories the references raise are no longer silent — sjasmplus on
a module left open at end of file, acme on an instruction that came out wider
than it needed to be, and vasm answering unknown instruction for a mnemonic
it knows. Matching a reference’s bytes without its warnings was only half of
matching it. (#213)
cargo xtask surface reports how much of each reference’s own vocabulary
this assembler takes. It asks the tool: harvest the words in its binary,
offer each one back to it, and whatever it does not call unknown is
vocabulary. The existing coverage metric measures against our spec, so a
form we never wrote down is invisible to it — this one cannot be, and it
found the first thing it fixed. Needs the reference tools installed.
(#212)sjasmplus source using MODULE now assembles. A module prefixes the names
defined inside it, @ escapes to the global scope, nesting concatenates, and
EQU is scoped while macros and DEFINEs are not — which is how a Spectrum
project keeps two libraries from colliding. Resolution matches the reference
exactly, including the part that is easy to get wrong: a name inside a module
has two candidates, the fully-qualified one and the bare global one, and
nothing in between. An inner module does not see an outer one’s unqualified
names. Twelve snippets are arbitrated byte-for-byte against SjASMPlus 1.21.0.
(#206)
One difference from the reference, which changes no bytes: it warns on a module left open at end of file and assembles anyway. asm198x accepts it silently.
This was the last of the three items in the macro stage. Macros, repetition and modules are now all present in every dialect whose reference has them.
An error raised inside a macro expansion now says which macro it came from. Two things were dropping the defined-at/invoked-at chain: parse-time errors lost it in every dialect, because the frames were only attached to an error that already carried a source span, and vasm lost it everywhere, because its errors are raised after the tree has been projected to its own layout form. Both now print what the other dialects already printed:
asm198x: v.asm:4: error: unknown instruction `FROBNICATE`
in expansion of macro `bad` invoked at line 4
Nested expansions list the whole chain, outermost last. (#203)
Every dialect now assembles the macros, conditionals and repetition its own reference has. This release finishes that: lwasm, vasm and rgbasm were the last three, and the table is complete.
| dialect | macros | conditionals | repetition |
|---|---|---|---|
| acme | yes | yes | yes |
| ca65 | yes | yes | yes |
| lwasm | yes | new | the reference has none |
| pasmo | yes | yes | yes |
| rgbasm | new | new | new |
| sjasmplus | yes | yes | yes |
| vasm | yes | new | new |
If you have been keeping a project on the real assembler because asm198x
refused a !if, an ifne, a REPT or a MACRO, that reason is gone.
(#196,
#198,
#201,
#202)
Each dialect keeps its own spellings, including the ones that look like
mistakes. vasm takes ifd/ifnd and rejects ifdef; rgbasm takes ELIF
and ENDC and rejects ELSEIF and ENDIF; lwasm takes endc and endif
and compares each of ifne/ifeq/ifgt/ifge/iflt/ifle against zero.
Every one of those was measured against the tool rather than read from a
manual, and a spelling a reference refuses is refused here too.
Loop variables behave as each reference defines them, which is four
different things: acme’s !for counts from 1 and survives the block, ca65’s
.repeat counts from 0 and stops existing at .endrepeat, vasm’s REPTN is
implicit and reads -1 outside any rept, and rgbasm’s REPT has none.
fmt rewrote a conditional’s closing keyword. It rendered every
keyword-style closer as ENDIF, so lwasm’s endc came back as endif —
harmless there, since lwasm takes both — and rgbasm’s ENDC came back as
ENDIF, which rgbasm does not accept. Formatting an rgbasm file with a
conditional in it produced source that would not assemble. The closer is now
the word you wrote. (#202).if / .ifdef / .ifndef
/ .elseif / .else / .endif, and .repeat n[, var] / .endrepeat, in
any case. Real ca65 has had all of them since forever and asm198x answered
unsupported directive, so a NES project using any of them did not build.
The loop variable counts from 0 and stops existing at .endrepeat, both
matching ca65 — and a condition inside the body can read it.
(#193)!for. Both spellings: !for i, n counts 1 to n, and
!for i, a, b runs inclusive from a to b — counting down when b is
below a, which is what acme does and is simple to get wrong.
(#190)A ca65 label may be indented. start: dex is legal ca65 and
asm198x refused it with unknown instruction \START:``, because two of the
three ca65 front-ends required a label at column 0. In ca65 the colon makes
a label and the column is irrelevant.
It also made fmt unsafe, not just strict, which is how it was found:
indenting a macro body is correct ca65 layout, so the formatter moved a
body’s label off column 0 and produced a file the same parser then refused.
If you have formatted ca65 source containing a macro whose body defines a
label, it is worth rebuilding.
(#188)
fmt deleted the body of every repetition block. On sjasmplus source, a
DUP or REPT block came back as its head alone — the body and the closer
were dropped, silently, exit 0. Three lines in, one out. The guide describes
fmt as safe to run over a project you have not read and shows you how to
move the result over your original, so this is worth checking for if you have
formatted sjasmplus source with a loop in it. Now rendered in full, with the
closer kept exactly as you spelled it — EDUP stays EDUP, endr stays
lowercase. (#187)DEFINE, which is sjasmplus’s;
an unclosed pasmo repetition was reported as a missing EDUP, which is also
sjasmplus’s. Each message now comes from the dialect you are assembling.
(#187)include in pasmo said your source was wrong when it was not. Real pasmo
assembles it and we do not implement it, and refusing it as an unknown
mnemonic sent readers to check their own file. It now says the spelling is
recognised and unimplemented, which is a different thing and the one you can
act on. The same pass gave every dialect one wording for a word it does not
know. (#181)IF / ELSE / ENDIF and
REPT n … ENDM, in any case, with the count folding against the constants
above it. Both were in real pasmo and answered unknown instruction here.
Only the spellings pasmo actually has: IFDEF, IFNDEF, ELSEIF, ENDC,
the dotted forms, DUP and ENDR are all sjasmplus’s, and all still
refused. (#187)if/else/endif and rept spellings. That page is the
list the parser reads, so a spelling on it is one the assembler accepts and a
spelling missing from it is one it refuses.
(#187,
#184)SBC reaches the 6502, HuC6280, 65C816, SM83 and
Z80 sections directly. Headings rather than full text, which is also what
answers a prose question: “Where a relative include is looked for” beats
every page that happens to use the word.
(#179)equ is part of the label grammar,
not a directive. (#178)sha2 0.10 → 0.11. Test and tooling only; the shipped binary does not link
it and its output is unchanged.
(#151)include resolves against a different directory depending on the
dialect — the including file’s own, that plus each enclosing includer’s, or
the root input’s — each pinned against its own reference assembler. That was
true before and stated nowhere. The table is generated from
asm198x::includes::resolution(), a new public accessor that most dialects
answer straight off the semantics their multi-file walk runs on.
(#168)fmt handled seven CPU families and not the 6502, and that disasm read
6502 and Z80. Both cover every dialect. A test now runs each operation for
every name --dialect accepts, so the claim fails the build if it stops being
true. (#171)include as the one gap.
(#165)fmt and disasm have worked examples on the command-line page — both were
described in prose and never shown. Formatting turns out to move a label onto
its own line. (#167)--message-format=json example on the command-line page reported column
13 where the assembler reports 15, and had no source sample it was the output
of. Every output block in the book — diagnostics, JSON, formatted listings and
disassembly — is now compared against what the binary prints.
(#167)--dialect and the
output flag change between them. All three are assembled by CI with the real
binary. (#154)The command line keeps a one-line Homebrew command and points at the new
install page for platforms, archives and the crates.io note./docs/. mdBook is withdrawn; the pages
themselves are unchanged and still live in this repository, beside the code
they describe. (#148)version (#97)fmt sent its outputbank! as bitwise-OR and relocate dc.l <label> data@ current-PC symbol (#8 follow-up)gapisa path dependency an explicit version requirement so
cargo package succeeds. release-plz runs cargo package to compute the
release diff; a path dependency without a version requirement fails it, which
blocked release automation. Local builds still resolve isa via the path.git_only so release-plz reads the previous version from the git tag
rather than the (unused, publish = false) crates.io registry.asm198x CLI — built on the
isa instruction-set spec. This first slice covers the common addressing
modes, labels, the </> byte-select operators, and the .org / .byte /
.word directives. The 6502 dialect is an early subset; ca65 compatibility
(arithmetic expressions, the full directive set, segments, macros) is still to
come — see decisions/syntax-stance.md.