Changelog

[2.0.0] - 2026-09-04

Breaking — the machine is a device, not a target. -d/--device, !device, [assembler] device and {_device} replace the target spellings; [[target]] is unchanged. Each old spelling still works and says so (W1103), and is withdrawn in 2.1.

Breaking — INCLUDE no longer searches the working directory first. A basename now finds the file beside the including source, so a build is the same wherever fantasm is run from. A build relying on the old order must name the directory, or add it with -I.

Breaking — init --force has no short form. -f is --format everywhere else. init -f still works and says so (W1103); it is withdrawn in 2.1.

Breaking — the unused-label sweep is --find-unused, not --enable-warnings. -u/--find-unused, --no-find-unused and [assembler] find_unused replace the warnings spellings, which never turned any other warning on or off. Each old spelling still works and says so (W1103), and is withdrawn in 2.1.

Breaking — a bin whose code is not contiguous is now refused (E1085). It used to assemble to a file where a label pointed at the wrong byte. Assemble each region separately and combine them with a loader, a [[target]] per region, or an incbin. No other format is affected.

Breaking — a label must start at column 0 (E1093). An indented name that is no instruction, directive or macro is refused. Move it to column 0; local labels, constants and labels inside a macro body are unaffected.

Breaking — a macro’s name at column 0 defines a label instead of expanding (W1094). Indent the call, or write the colon where a label really was meant.

Breaking — rep is a directive, so it may not name a macro (E1060). Rename the macro. Labels, constants, STRUCT members and the rest still take the word, so only a MACRO of that name is affected.

Breaking — assert and debug are directives, so neither may name a macro (E1060). Rename the macro. Labels, constants, STRUCT members and the rest still take both words, so only a MACRO of that name is affected.

Breaking — two targets may not share an artifact path, inherited or not. A project-wide [artifacts] applies to every [[target]], so two targets and one labels key left a file holding only the last one’s symbols. Move the key into each [target.artifacts] under its own name. A project with one target is unaffected.

Breaking — an index displacement outside -128 to 127 is refused (E1123). ld a,(ix+200) used to assemble as ld a,(ix-56) in silence, addressing a byte 256 away from the one written. Add the offset to the register first: ld de,200 then add ix,de.

Breaking — a !test command outside a !test block is refused (E1122). init_reg, init_mem, init_cycle_limit, assert_reg, assert_mem, assert_cycles and assert_checksum were accepted anywhere and silently discarded, so an assertion past its !endtest passed without running. Move it inside the block.

Breaking — !nex screen is refused for anything but 0 (E1117). No screen was ever written, so the flag sent a loader into the program. Remove the directive.

Breaking — a .nex is tagged V1.2 unless it needs V1.3. It was always V1.3, which a core older than 3.x refuses to load. A checksum, copper block, CLI buffer, extended screen or tilemap still raises it; a loading bar does not.

Breaking — -vvv is refused. There are two levels of -v and it silently meant -vv, so a build asking for a third was told there was more to see when there was not. Use -vv.

Breaking — an undefined name in an ENUM member’s value is refused (E1003). The member used to take the next counter value silently. Define the name above the ENUM, as its <start> has always required.

Added

  • A [lint] table in fantasm.toml sets what each warning is filed as, by code: w1049 = "deny" fails the build on an unused label, w1025 = "allow" reports nothing. default covers the rest. An entry naming a code that does not exist (W1131), one that may not be reconfigured (W1132) or a level that is not allow, warn or deny (W1133) is refused and the rest of the table still applies. The editor draws what the build reports. See Configurable Diagnostics for the codes a table may name.
  • REP <count>, <instruction> assembles the instruction that many times — REP 32, ldi is an unrolled copy without ldir’s loop overhead. Only an instruction may be repeated (E1125); a run of identical bytes is still DS <count>, <fill>. A repetition runs to 65536 statements (E1126), as DS runs to 65536 bytes.
  • REP <count> with nothing after it opens a block, closed by ENDR or END. A local label inside is named apart per repetition, so a djnz reaches its own copy. A block the source never closed is reported (E1074) rather than swallowing the rest of the file.
  • !assert and !debug record an annotation into the --sld file at the address they are written at, for a debugger to act on. Nothing reaches the image, and the condition crosses as written — FantASM does not read it. The ! is optional as on every directive, and the record names the canonical spelling either way.
  • A build carrying an annotation with no --sld file says so once (W1120), rather than dropping them in silence.
  • An annotation inside a MACRO is recorded once per expansion, at each address it reached, naming the invocation.
  • A reserved word may name a STRUCT member, an ENUM member or type, a MODULE or a !librarydevice, ld and a are all names now. A macro’s name, a macro’s parameter and a STRUCT’s own name still refuse one.
  • E1062 is withdrawn. It refused a module or library named after a reserved word.
  • A STRUCT member may be another STRUCT: tl Pt gives Rect.tl.x and Rect.tl.y, and SIZEOF counts them. Initialisers stay flat — one value per byte, in declaration order.
  • A STRUCT holding itself is refused (E1118), and a name where a member’s size belongs that is not a type says so (E1119).
  • !nex version V1.2 or V1.3 overrides the version chosen for a .nex. V1.2 is refused where the file uses a V1.3 feature, naming it (E1115); any other spelling is E1114.
  • init_mem and assert_mem take a run of memory as well as a single byte: assert_mem addr=$9000, length=256, fill=$00. A failure says how many bytes differ and where the first one is.
  • A rhai.name() call that failed inside its function was reported as that function not existing, hinting at exactly what was written. It now names what is missing, under E1200.
  • --check-updates on a build compiled without update checking is refused. It printed nothing and exited 0, so a script could not tell no update from this build cannot answer.
  • --lsp on a build without the language server reports on stderr rather than stdout — the channel a client parses, and the one that mode exists to keep clean.
  • A failing build shows the warnings it collected, which it used to discard. They are often upstream of the error and sometimes name its cause.
  • --report-discards <file>, and report_discards under [artifacts], write down what --gc-modules and --gc-libraries threw away, grouped by module or library with sizes and a total. It is written even where no sweep ran, and says which did.
  • assert_checksum folds a run of memory into a CRC-32C and checks it against a file’s own or a figure written in the source, for a result pinned with no reference file at all.
  • A span takes its bytes from a file — assert_mem addr=$4000, file="screen.scr" — read when the test runs and never entering the image. file_offset= slices one reference across several spans, and a run past its end is refused (E1112).
  • A span may start at phys=, indexing the machine’s memory, so it reaches a bank that is not paged in and can run past 64K. Both starts on one span is E1110, and a physical one outside memory is E1111.
  • A directive with optional arguments spells them name=value, separated by commas; the positional forms are unchanged. Mixing the two is E1107, an unknown option E1105, one given twice E1106, and an incomplete span E1109.
  • init_cycle_limit <limit> in a !test block gives the body that long to run without asserting anything about it. It takes an expression, and there is no project-wide setting.
  • A renamed switch or fantasm.toml key keeps its old spelling working and says what replaced it (W1103). The aliases come out in 2.1.
  • --reloc <file>, and reloc under [artifacts], write what a loader must patch to run the image at another address. An address the table cannot describe is E1092, and output that is not one image moved by one delta is E1102.
  • !library Name makes the rest of a file a library, guarded so a second INCLUDE is free (E1095, E1096).
  • An INCLUDE above a !library line stays global; below it, its names are the library’s.
  • INTERFACE / ENDINTERFACE lists what a library publishes, and the library must define every name in it (E1097, E1098, E1100).
  • A name a library’s interface omits is refused from outside it (E1101); @NAME goes round it.
  • --gc-libraries, and gc_libraries in fantasm.toml, discard unreached !library code.
  • A MODULE used only to place bytes survives --gc-libraries without a KEEP.
  • A STRUCT type and a MACRO belong to the library or module declaring them, so two may each define a Pt.
  • @NAME reaches the program’s macro or type past a library’s own of the same name.
  • A type name may be qualified where an instance is written: pos Gfx.Pt 1, 2.
  • include "helpers.rhai" loads a Rhai script, alongside !rhai "helpers.rhai".
  • A !rhai block can call functions defined by any script above it, block or file.
  • A Rhai function can be called as rhai.double(3), alongside rhai("double", 3).
  • rhai.gfx.pack(1) calls the pack in gfx.rhai, so two scripts defining one name are both reachable.
  • Two loaded scripts defining the same function is reported (W1090), naming both and which will be called.
  • A missing Rhai function has its own code (E1088) and says which mistake it was.
  • A !rhai block can be written inside a !test body, and what it defines belongs to that test alone.
  • A failing !test assertion has a code of its own (E1091), reported against the line it is written on.
  • !opt maxcodesize takes an expression — !opt maxcodesize PAGES * 8192. It may not name a label (E1089).
  • The language server reads the document’s fantasm.toml, so the editor shows what the build does. A target from the editor still wins.
  • A [[target]] answers for the file its main names, so two sources in one directory get their own settings.
  • The language server’s output pane names the fantasm.toml that answered, the target chosen and the search path.
  • Every build carries the release notes as RELEASE-NOTES.md — beside the licence in the .deb and the Arch package, and in the macOS tarball and the Windows zip. They are the release’s description on the download page too.

Changed

  • E1034 is withdrawn. It documented a rule the assembler never had — only local labels are permitted inside macros. A plain label in a macro body is legal, and collides only when the macro expands twice, which is E1013.
  • E1013 raised inside a macro expansion names the macro the line came from, points at the local-label form that is renamed per expansion, and lands on the invocation.
  • I1051 is withdrawn. A build’s progress lines are no longer filed as diagnostics. -v prints the same lines as before.
  • E1062 and E1063 name whichever of MODULE and !library was written.
  • W1084 names the flag that discarded the routine, --gc-modules or --gc-libraries.
  • W1086 and E1087 no longer say MODULE, which a library body does too.
  • E1024 described a file included twice, where what is refused is a circular include.
  • Eight entries in the diagnostics reference carried an example that no longer produced the code it documents.
  • --help lists every option there is, init’s included, and -h and -V are read wherever they appear rather than only as the first argument.
  • A refused argument is named, and the nearest spelling suggested.
  • -O and -M take a number written any way the source may write one — $9000, 9000h, 0b1001, _ between digits. Decimal was all they accepted.
  • --help groups the switches under Build, Machine, Assembly, Dead Code, Artifacts and Reporting.
  • -vv says how full the image is against whichever ceiling applied, and where each MODULE and !library body was placed. -v and verbose = true are unchanged.
  • -v says where INCLUDE and INCBIN look, in the order they are searched. That order is documented for the first time.
  • --help described --test, -e and -n wrongly: !test blocks are assembly run on the simulator, -e writes the GLOBAL labels and constants, and -n hides the update notice too.
  • Test coverage expanded from 742 to 1,432 tests.

Fixed

Crashes:

  • ds -1, and any count that works out negative or past 64K, aborted the assembler outright. A count outside 0 to 65536 is refused where it is written (E1121).
  • db (1<<63)/-1 aborted the assembler. Division wraps, as every other operator in an expression already did.

Wrong output, silently:

  • A macro invoked on a line that already carried a label was not expanded, and the body was dropped. start: Bump now defines start at the body’s first byte, as every other statement on a labelled line does. W1094 no longer fires on this shape, having pointed at the wrong column.
  • An ORG that moved forward put the code after it at the wrong address in a .nex, packed against the code before it instead of at the address written.
  • A line assembled into a bank was also written to whichever other bank its position in the file happened to fall in, so a .nex carried the code twice.
  • Seven of the block-graphics characters emitted the code below the right one — db "▘" gave the blank cell, and nothing could emit 135 at all. They are 128 plus a quadrant mask now: top-left 1, top-right 2, bottom-left 4, bottom-right 8.
  • add hl,ix, adc hl,ix, sbc hl,iy, add ix,hl and add ix,iy assembled as add hl,hl and add ix,ix. None is a Z80 instruction, and each is now refused (E1007).
  • ld ixh,iyl and ld iyh,ixl assembled as ld ixh,ixl and ld iyh,iyl. An index half cannot meet the other index register, nor plain h, l or (hl), and now says so (E1008).
  • Two comparisons on one line lost the operand between them: db 1<2<3 assembled as 1<<3 and gave 8. Spacing hid it, 1 < 2 < 3 always being right.
  • ^ was not an operator: discarded from a constant, so V equ 255 ^ MASK gave 255, and refused in an operand. It is bitwise exclusive-or, binding tighter than | and looser than &.
  • -i/--case-insensitive turned the INTERFACE privacy check off entirely, so every private name in every library was reachable and nothing was said (E1101).
  • Under -i, a STRUCT type whose name was not already upper case could not be used: here Point 3, 4 was E1005 with no message and SIZEOF(Point) was E1037, on source that assembles without the flag.
  • Under -i, a local label inside a MODULE was named with the module prefix twice — Beta.thing.local_here reported and exported as BETA.BETA.THING.LOCAL_HERE. The program assembled; the name a diagnostic, --export-labels and the .sld carried was wrong.
  • An !assert or !debug in a macro body reached the SLD with everything before it on the line folded into the record, so a label name leaked into the annotation and the text was malformed for anything reading it.
  • A 128K .sna restored with the 128K editor ROM paged in rather than the 48K BASIC ROM, so a restored program calling RST 16 — or any other ROM address — ran something arbitrary.
  • A macro whose body invoked another macro lost every line after that invocation, with no error and no warning.
  • A macro that invoked itself aborted the assembler with a stack overflow. Nesting is limited to 64 and names the macro that reached it (E1116).
  • A !test could not read an MMU register back — $50$57 returned zero — so a routine that borrows a slot could not put it back. No other Next register is modelled.
  • --gc-modules and --gc-libraries discarded a STRUCT instance reached only through a member name — ld (Text.view.colour),a — so every access read and wrote the wrong memory, and !test passed.
  • A constant assigned from a MODULE label — payload_offset = Payload.body - $2000 — held the address the body was read at, so a program computing an offset to its own payload was short (W1086).
  • A constant defined inside a MODULE was never corrected after the body moved, and every read of it emitted the wrong address in silence. Where W1086 did fire it named a value the image did not hold, and the E1087 for an if inside one never fired at all.
  • A label named after a directive — target, message, page — was not recognised as an address in an expression, so an operand reading one kept the address the line was gathered at.
  • if chose its branch the same way, assembling one arm on a condition that was false by the time addresses settled (E1087).
  • Where two loaded scripts defined the same function, an expression call and a !rhai block picked different ones.
  • A !test was cut off at 1,000,000 T-states and reported as failed to halt, so assert_cycles above that figure could never report. A block’s assert_cycles now buys it that long, and reaching the ceiling names the figure (E1104).
  • An ENUM member’s value was read only as far as a plain number, so Big = 5 * 2 was 5. It is an expression now, as the enum’s start and step always were.

Source that should have assembled, or should not have:

  • An IFDEF or IFNDEF nested inside a branch that was not being assembled was refused, blaming a balanced ENDIF or ELSE (E1027, E1028).
  • An IF inside a branch that was not being assembled had its condition evaluated, so a name existing only in the configuration that branch guarded failed the build (E1003).
  • A second ELSE for one IF emitted both arms. It is refused where it is written (E1113).
  • An index displacement could not name a constant defined further down the file: ld a,(ix+DISP) above DISP equ 4 was E1021. It is the last operand to be settled at emission like every other.
  • ld (ix+0),300 truncated its value in silence, where ld a,300 says so (W1011).
  • -D DEBUG=1, and a [defines] DEBUG = true in a project file, defined something the program could never name (E1003). A define whose name is a directive’s keyword is now stored the way the source stores one.
  • An 8-bit load refused a constant named after a directive: MESSAGE equ 7 then ld a,MESSAGE was E1008, while ld hl,MESSAGE assembled.
  • IFDEF and IFNDEF refused a constant named after a directive (E1006), so IFDEF DEBUG could not ask about a build flag of that name.
  • !opt device, !opt target and !opt format were refused outright (E1020) — three of the six options documented.
  • !opt format hex was refused as a format called DH.
  • !opt case_insensitive takes an underscore. The documented case-insensitive never worked.
  • An INCLUDE inside a MODULE reported E1064, so an included file could not sit inside one at all.
  • A STRUCT instance’s members and an ENUM’s members reported E1003 when reached by their short names from inside the module that declares them.
  • A !test body written inside a MODULE or a !library could not use that scope’s short names, so call scratch was E1003 where every line around it used exactly that form.
  • The assertions beside it still could not: call fill resolved and assert_mem buffer on the next line was E1003, the two halves of one block disagreeing about what a bare name meant.
  • A !test operand that would not resolve was reported against the !test line, with no source quoted, rather than against the line it was written on.
  • A Rhai function could not be called from a !test body, while a constant the same script defined crossed fine.
  • Calling a Rhai function from an expression re-ran the statements of the block that defined it.
  • A local label defined in a macro could not be referenced from that macro’s body.
  • A macro parameter inside brackets was not substituted, so db (n + 1) in a macro body reported n as undefined.
  • SIZEOF of a type declared in a module reported E1037.
  • SIZEOF(@Name) was refused (E1021), so a library declaring a type named as the program’s could build one — here @Pt 1, 2 has always worked — and not ask how big it is.
  • E1037 said only SizeOf cannot be determined, naming nothing to go and look at.
  • SIZEOF of a STRUCT type was refused in =, EQU and IF, and the E1003 it gave named no symbol at all.

Output and tooling:

  • Output carried colour into a pipe or a redirect, so fantasm src.asm out.bin > build.log wrote escape sequences into the log. Colour is now used only where the stream is a terminal; --color auto|always|never overrides it, and NO_COLOR and TERM=dumb are honoured.
  • default-features = false did not compile, so the documented way to depend on FantASM for assembling alone was the one that failed. Every combination of nex, sna, lsp and update-check builds, and its tests pass.
  • A failing build printed one error however many it found — three bad instructions reported one, and each rebuild met the next. Every fault is reported, in source order.
  • Only the first undefined name was found, so a rename that missed three call sites took three builds to learn about. All of them are reported now.
  • Completion offered an ALIGN directive that does not exist, and omitted most of the ones that do — MODULE, INTERFACE, KEEP, GLOBAL, IFDEF, the !test commands and the rest. The list is now the assembler’s own.
  • Hover said nothing for any ctx. member in a !rhai block; only the one free function ever answered.
  • A line merely mentioning !rhai in a comment put every line below it into Rhai mode, so completion offered no instructions, directives or labels for the rest of the file.
  • Goto-definition, find-references and hover found nothing for a local label or a name inside a MODULE — the two forms FantASM uses most. Hover was worse: it answered, about the same local label in whichever routine the file happened to end in.
  • A warning reached the editor identified as E1049 while its message quoted W1049, so a client filtering or linking by code followed it nowhere.
  • The language server assembled without the middle phase of the pipeline, so any file with a MODULE or !library got different diagnostics in the editor from the build’s — silently. A file it had no earlier state for was never emitted at all.
  • Asking for signature help on a line holding an accented character or an emoji took the language server down, so a comment with an accent in it broke completion for the file. Symbol lookup and the extent a diagnostic underlines were measured the same wrong way.
  • --lsp wrote a project-file warning to stdout, which is the JSON-RPC channel, so a strict editor failed to start the server. A fantasm.toml that would not parse stopped it starting at all — no diagnostics on the file that would have said what was wrong.
  • --version failed in a directory holding a fantasm.toml it could not read.
  • A second assemble() on one Assembler inherited the first source’s said-once flags, so W1068, W1069 and W1120 were suppressed for the second. Only an embedder reusing one is affected; every shipped path builds a fresh assembler.
  • -e, --sld, --reloc and --report-discards were discarded when the selected target named the matching key, so the file asked for on the command line was never written and nothing said so.
  • report_discards in a project file resolved against the working directory rather than beside fantasm.toml, unlike the other three artifact paths.
  • An ignored shebang line (I1057) was documented and shown to nobody: nothing displayed the Info level.
  • E1024 said Source file previously included where it refuses a circular one. It now names the cycle, and a file including itself is caught at once.
  • An image under 8 KiB was written after every artifact describing it, not before, so an SLD could not be told stale by its timestamp on a small program.
  • No two --sld files matched: the records were written in hash-map order, so a debug artifact could not be diffed between builds.
  • No two --export-labels files matched either, for the same reason. The symbols follow their GLOBAL declarations now.
  • Every label defined in an included file was recorded in the SLD against the top-level source, so a debugger asked where one lives opened that line of the wrong file.
  • Hover named the 8K page as a bank, so a label at $C000 in bank 3 read as Bank 6. It says Bank 3 on a 128K now, and Page 6 on a Next, which pages in 8K.
  • The editor showed a diagnostic without the hint under it, so Did you mean known? reached the command line and nobody editing. The hint now follows the message.
  • With two files open in different targets, editing one cleared the other’s marks, so only the file last typed in showed its faults. Findings now belong to the target that found them, and a file two targets both report on shows both — an identical finding once, differing ones together.
  • Closing a file left its text held for the life of the server, and its marks in the problem list. Both go when it closes.
  • Typing Gfx. in the editor offers what Gfx holds, rather than every instruction, directive, label, constant and macro in the program. Completion narrows on what has been typed, workspace/symbol answers its query instead of shipping the whole table on each keystroke, and a name inside a MODULE, !library, STRUCT or ENUM reaches the client with its owner, so a client can group them.
  • An INCLUDE path is clickable in the editor, opening the file it actually resolved to. One that did not resolve is left unlinked — E1004 is what says why.
  • A MACRO, STRUCT, ENUM, MODULE, INTERFACE, REP or !test body folds. Read from the text, so it works on a file that does not assemble; a block the file never closes folds nowhere.
  • A label’s settled address and a constant’s value are shown in the margin of the line that defines them.
  • The editor can walk what reaches a routine and what it reaches, through its call-hierarchy view. It is a reference hierarchy: ld hl, table and call routine are one edge, and each entry says so — the graph is the one --gc-modules walks, which records that a name was reached rather than how.
  • A label can be renamed across the project from the editor, and its other uses in a file are highlighted with the cursor on it. A comment or a string mentioning the name is left alone, and a line writing two names that end the same way — Gfx.draw beside Snd.draw — refuses the rename rather than half-applying it.
  • A diagnostic whose hint names a replacement offers it as a quick fix in the editor — Did you mean known? is an edit rather than something to retype. Several in one file can be applied together. A hint that describes rather than names offers nothing.
  • The editor can list a file’s own symbols — go-to-symbol-in-file, and the outline pane. Nested, so a library’s routines are not buried by their local labels: lib/ansi.asm’s 76 names collapse to one.
  • A shared library beside the programs that use it — no fantasm.toml above it — is now assembled in the editor with the configuration of a project whose include_dirs name its directory, so its own includes resolve. A file no project declares is still assembled on its own, and now says so as a warning rather than in passing.
  • Opening a second file of one program filled the editor with hundreds of faults in correct source — a missing include beside the file that asks for it, a syntax error on an INCLUDE line (E1004, E1005, E1003). One file open was clean; the build always was.
  • db __VERSION__ was underlined in the editor as an undefined name in a source that assembled: __NAME__ and __VERSION__ reached the build and not the language server. __NAME__ follows the [[target]] that builds the file, as it does in a build.

Diagnostics:

  • A KEEP, a GLOBAL or an INTERFACE entry naming nothing prints the line it was written on and underlines the name. Each reported file:line and left the reader to find the directive. E1100 points at the INTERFACE entry rather than at the !library line.
  • A diagnostic points at the name it is about and underlines the whole of it. The caret was placed at the end of the statement — db "v", NOPE, $0D, $0D, 0 marked the final 0 while complaining about NOPE — and the editor drew a ten-character mark from a character further left still.
  • E1101, a name outside a library’s INTERFACE, is refused where it is written rather than audited after the build, so it carries the line, the column and a caret like every other diagnostic. @ still reaches a private name silently. A reach inside code a sweep discards is no longer reported — the name is never evaluated, so nothing shipped and nothing broke a contract.
  • A machine nobody makes is W1129 and page on a machine that pages in 16K is W1130. Both were reported as W1020 invalid assembler option, which neither is.
  • A MACRO whose name or parameter was refused reported up to three faults for one mistake: the body assembled as ordinary code, so a parameter it named was undefined and its ENDM was dangling. The body is read to its ENDM and dropped, and the refusal is all that is said.
  • A misspelt key in fantasm.toml was accepted, ignored and never mentioned: divice = "zxnext" built for the default machine, outpt = "a.bin" left the name inferred, and a [tasks] pre never ran. Each is named with its section now, with the nearest real key where there is one (W1124).
  • W1049 came out in hash-map order, so two builds of one source listed the same labels differently. Where only the first fault of a kind is shown — an unknown KEEP or GLOBAL target, an unkept INTERFACE promise — which one you were told about changed between builds too. All four follow the source now.
  • W1049 unused label fired on the untouched members of a STRUCT instance, and on the instance when only a member was read.
  • W1049 unused label fired on a label named anywhere but first in an expression — ld hl, first + second reported second as dead.
  • A diagnostic raised inside a !test body lost its code, position and hint.
  • E1026 ended with a stray space.
  • E1020 Invalid assembler option named nothing — a bare colon where the value belonged — and stood for three different faults. It is now an option that is no option; a value an option will not take is E1134, and an output format this build does not write is E1135. All three name what is wrong and underline it.
  • E1005 ran its detail into the word before it: eight messages read as Syntax errorExpected register, Syntax error!endtest without !test, Syntax errorempty parenthesised operand and the like.
  • bank and page on a machine that has neither were invalid instruction, which they are not, and the machine was left unnamed (E1136). A slot on a machine that pages in 16K was a syntax error, which it is not (E1137).
  • E1058 General error stood for five unrelated faults. A --test run’s failure count is E1138 and reads 1 test failed rather than 1 tests; a test with no body is E1139 and one with nowhere to be placed is E1140, both naming the block; a !rhai missing its brace is a syntax error; and fantasm.toml’s two target faults are E1141 and E1142.
  • A mistyped machine — -d zx81, or device = "zx81" — built as a flat 64K one without a word, though !device zx81 in a source file warned. Every route says so now.
  • !message’s {_file}, {_device}, {_format}, {_date} and {_time} were documented nowhere, so the only way to find them was to read the source.
  • A byte at $FFFF warned that the address space had overflowed (W1001). It fits, and the hint now names the address a byte went to rather than the one after it.
  • A second assemble() on one Assembler ran the first source’s !test blocks again. Only a program embedding the crate is affected; every shipped path builds a fresh assembler.

The language server:

  • A discarded run of characters (W1025) and a deprecated spelling (W1103) are now tagged, so an editor fades the first and strikes through the second instead of underlining both as ordinary warnings.
  • A file that is INCLUDEd rather than being a target’s main was analysed on its own, so the editor reported faults the build did not — a name its parent supplies was E1003 on screen while fantasm build said nothing. It is now analysed as part of the target that includes it, and the output pane says which one. A file no target reaches is unchanged.
  • With a multi-project tree open, every file was assembled against a sibling project’s include path, so a same-named header there was included in place of the file’s own.
  • An error on the last line of an included file was reported twice, against that file and against the INCLUDE line.
  • Every include line carried a mark on source that assembles cleanly (I1051).

Documentation:

  • The directive reference documented !message with the comma-separated argument list that was removed, and never documented the "{NAME}" form that works.
  • !message and !rhai print underlined the line they were written on; they go to the language server’s output pane instead.

Nothing here changes what fantasm does. These affect a program embedding the crate, or a debug build of FantASM itself.

  • Breaking — a format writer is given values, not a live assembler. FormatContext carries sp and pc already evaluated, the paging state, and a &SymbolTable for the SLD writer; context and expr are gone, and resolve_addr is HeaderAddress::within. A NEX or SNA writer can now be exercised without assembling a program.
  • Breaking — four shared items moved to the area that owns them. NumberParser and ZXAscii are under tokens, HighLow under emitter, and a directive’s spellings are tokens::directive::DIRECTIVE_NAMESdirective::registry keeps the handlers. Eleven pairs of areas depending on each other both ways became five.
  • SymbolTable and SessionState answer for themselves. 104 methods moved off AssemblerContext onto whichever of the two owns their data, and neither now reaches into the other. What a caller types is unchanged — the context still delegates.
  • Breaking — one image’s settings are a TargetOptions. Options keeps what the invocation asked for; opts.device is opts.build.device, and BuildTarget::options is a TargetOptions, which cannot hold the target list.
  • Breaking — Placed’s twelve fields are grouped. p.pc, p.asm_pc and p.slot_page are p.at; the three flags the sweep reads are p.about, which is the LineState the encoder already built.
  • Breaking — what a run learns lives in Assembler::run. sld_records, tests and the rest moved into a RunState that prepare rebuilds; asm.sld_records is asm.run.sld_records. warnings and errors stay where they are, being what the caller reads.
  • Breaking — six Diagnostic constructors are gone. warning, fatal, info and message were called only by tests, error_with_hint and fatal_with_code by nothing. Use Diagnostic::at(level, code, location, message) with with_hint, with_line_code and with_related.
  • Breaking — a bank, an 8K page and a slot are Bank, Page8k and Slot, not bare integers. MemoryImage::banks and written are keyed by Bank; bank_data_mut, current_bank, slot_page and the simulator’s set_slots and load_bank take and give the new types. Wrap the number — Bank(5), Page8k(11).
  • Breaking — an origin is a Z80Addr. MemoryImage::get_base_address gives one and set_base_address takes one; Z80Addr::offset_from is the crossing to a FileOffset.
  • Breaking — the !test simulator’s store is addressed by PhysAddr. Simulator::peek_physical and poke_physical take one, which is what phys= names.
  • Breaking — SLDRecord::record_type is an SLDType, not a String. SLDType names only the records FantASM writes — Trace, Label, Device — and its Display is the letter. The unused Function and EndOfFile are gone.
  • Breaking — assemble() writes no files. save() writes the image and then every artifact — labels, --reloc, the discard report, and the SLD last. Call save(), or save_labels() and save_sld() for one of them alone.
  • Breaking — DiagnosticsEngine::check_file returns a third value. (diagnostics, messages, assembler), where messages is !message and !rhai print output for the client’s output pane.
  • Breaking — Assembler::device() and Options::device replace Assembler::target() and Options::target.
  • default-features = false builds again: assert_checksum needed a CRC that only the nex output format pulled in.
  • asm.test_failures carries a Diagnostic per failing test.
  • RUST_LOG could raise the tracing level and never lower it, so a debug build’s stderr could not be quietened — RUST_LOG=off still printed.

[1.9.0] - 2026-08-22

Breaking — Invalid address or bank is now E1082, not E1049. 1049 named both that error and the unused label warning, so looking it up found whichever entry came first. The warning keeps the number.

Breaking — NEX entry point … has no code is now W1083, not W1060. 1060 named both that warning and reserved word used as a macro name. The error keeps the number, its family sitting at 10601062.

Breaking — Assembler::parsed_lines() and the ParsedLine type are removed. Only a Rust program embedding FantASM as a library is affected; the command line is unchanged.

Added

  • --gc-modules with --test now says when a routine only a !test calls has been swept (W1084), instead of leaving the test to fail with Execution timed out or failed to halt. KEEP or GLOBAL on the routine makes the test run.
  • A print in a !rhai block files a diagnostic (1081) as well as reaching stderr, so it shows in an editor rather than only in the language server’s output pane. Twenty lines a block, then one saying how many more there were; stderr still carries all of them.
  • The language server takes a target from the editor’s initializationOptions{"target": "zxnext"} — so a Next project no longer shows E1016 on every Z80N instruction. A client that sends nothing is unaffected.
  • rhai("fn") may answer with a string: BANNER equ rhai("banner") then dz BANNER.
  • --sld now carries EQU constants (+equ), MODULE/ENDMODULE boundaries (+module/+endmod), and label names split into module/main/local with a +used trait — previously it emitted labels only, with the whole dotted name in one field.
  • _ may be used as a digit separator between digits of a number: 65_536, 0b1010_1010, $FF_FF. An apostrophe separator, which sjasmplus also accepts, is named rather than reported as a bad label (E1076).
  • EQU joins text with +: BANNER equ __NAME__ + " v" + __VERSION__.
  • SIZEOF answers the length of a constant holding text, in the bytes db would emit.
  • A GLOBAL constant holding text is exported as NAME = "text" in the labels file.
  • !rhai: ctx.get_constant returns text for a constant holding it, and ctx.define_constant accepts a string.
  • A --no- form for every valueless flag — --no-z80n, --no-cspect, --no-case-insensitive, --no-warnings, --no-verbose, --no-gc-modules — to turn off a setting [assembler] turned on. The last flag given wins.

Several images from one project file:

  • [[target]] declares an image the project builds: name, main, output, depends, default, intermediate, its own [target.artifacts], and any [assembler] key it overrides.
  • One fantasm run builds every target, in dependency order. A file with no [[target]] is unaffected.
  • --only NAME builds one target and what it depends on, and is repeatable; --all adds targets marked default = false. Spelt --only because --target names the machine.
  • [target.tasks] gives a target its own pre_build/post_build, for a generated source only one image needs. Project-wide [tasks] still runs once around the whole build.
  • A target that fails skips its dependents and nothing else; each skipped target is named with its reason, and the exit status is non-zero.
  • --test counts are reported against the target the tests came from, and a target with no !test blocks says so rather than being left out.
  • A depends cycle is refused, naming every target in it (E1077).
  • An incbin of a target’s output left out of depends names that target (E1078).
  • Two targets writing the same file are refused before anything is assembled.

Changed

  • ifdef/ifndef on a name defined by #define now says so (W1079) instead of quietly deciding false. #define makes a macro and the conditionals test constants; the decision is unchanged, but the block no longer vanishes in silence.
  • #if now exists, completing the #-prefixed conditional family the reference already described. It was the only one of the five missing, and using it gave E1002 Invalid character in label.
  • IF takes an expression and is true when it is not zero, where it used to accept only IF <constant> = <value>. IF FOO = 1 decides exactly as before; IF FOO > 1, IF 1 and IF (FLAGS & 4) now work, and a label may be tested as well as a constant.
  • Breaking — [assembler] format now takes effect. It was parsed and ignored, so a project file saying format = "nex" produced whatever the output extension implied. Check any project file whose format and output extension disagree; -f still wins.
  • What a !rhai script says through ctx.warn or ctx.error is now 1080, in the script’s own words. Both were 1058 General error, so a script’s warning read General error: …; ctx.error came back wrapped four deep as Rhai script error: Runtime error: Error: t.asm:2:5 - General error: stop. 1200 still covers a script that will not compile, or a fault in the engine.
  • A failing --test now names the test and what it expected against what it got, without -v, and prints the counts whether or not anything failed. A single-target build says what a multi-target one already did.
  • Test coverage expanded from 575 to 742 tests.

Fixed

  • A constant could not be named after a directive: hex equ 9 reported E1022 Invalid Hexadecimal string and word equ 5 reported E1021, each describing what the directive wanted rather than the name. A label of that name always worked, and now both do.
  • A label named after a directive — message, end, block, page — worked in some operand positions and not others: call message assembled while ld hl,message was E1005. It now resolves everywhere, including when defined further down the file. An aliased spelling (hex, also dh) that is never defined still reports a syntax error rather than guessing which was written.
  • A label called page or bank was silently replaced by the builtin of that name, so call page assembled a call to the current page number instead of to the label. Use _page and _bank for the values.
  • E1058 from a failing test pointed at <none>:0 with the caret under endtest; it now names the !test block that failed.
  • !message silently discarded anything after its template, so the old comma-separated form — !message "T States: ", _tstates — printed the string and dropped the value. It now warns (W1025), as every other directive given more than it wants does.
  • --sld recorded a MODULE body at the address it was written at rather than where it was placed, so a breakpoint set inside a module went to ROM and never hit, and stepping into one highlighted the caller’s line. Module labels reported the wrong page for the same reason.
  • !nex bank <n> was silently ignored for every bank but 10, 5, 2 and the one currently selected, so the file loaded without a bank the program pages in at runtime.
  • The NEX reference documented neither !nex ram nor !nex cfg.
  • The reference listed the paging values in expressions as bank and page, which are directive names and give E1005. They are _page, _bank and _slot, the last of which went undocumented; asmpc, codesize and tstates still take no underscore.
  • A parenthesised sub-expression was rejected with E1021 when it followed an operator inside another parenthesis — (0 - (PAGE_SIZE >> 8)) & $FF. The leading position, as in ((1 + 2) + (3 + 4)), always worked.
  • The Rhai reference documented a run "script.rhai" directive that does not exist and never did — using it is a syntax error. The form is !rhai "script.rhai", and the .rhai extension is required.
  • fantasm --help listed neither --ram nor --check-updates, and none of init’s options.
  • Two diagnostic messages were misspelt: 1023 read “out of range will and will be truncated”, 1025 read “extra characters at and of line”.
  • The diagnostics reference documented no 1001, 1058, 1059 or 1200 at all. E1049/W1049 and E1060/W1060 are each two unrelated diagnostics sharing a number, which it now says.
  • A build that failed on its only target printed the diagnostic twice.
  • A setting in fantasm.toml could only ever be turned on: gc_modules = true could not be overridden from the command line, because false and unset were the same value. Use a --no- flag.
  • -O 0 and -M 0 read as unset, so a fantasm.toml origin or max_code_size won instead. Both are now explicit values.
  • A computed port operand was a syntax error: out (0x80-0x10),a and in a,(0x80-0x10) now assemble.
  • 12EFh was a syntax error: a trailing-radix hex literal needed a leading zero rather than any leading decimal digit. 1010B was refused for its uppercase suffix, which hex had always accepted.
  • A define holding text — __NAME__, __VERSION__, a string in [defines] — emitted a zero byte instead of the text, silently. db and dz now assemble it, and EQU can hold one; text where a number is required is an error (E1075).
  • --gc-modules discarded a routine that the one above it falls into, so the caller ran on into whatever survived. KEEP is no longer needed for it.
  • Assembly time and memory grew steeply with the number of INCLUDEd files: seven libraries took 37 seconds and 56 GB for 3 KB of output, now under a tenth of a second.

[1.8.1] - 2026-08-20

Fixed

  • The THIRD-PARTY-LICENSES.html bundled into every 1.8.0 artifact listed no entry for hg80, the CPU core !test runs on. Anyone redistributing a 1.8.0 binary should replace the file.

[1.8.0] - 2026-08-19

Added

  • @name reaches global scope from inside a MODULEcall @helper, ld a,@WIDTH. It means global, not one scope out.
  • Module dead-code elimination (--gc-modules, off by default): a routine inside a MODULE that nothing reaches is left out of the output, so a project can include a shared library and pay only for what it calls. KEEP <label> pins one reached by a computed address.
  • LDIRSCALE (ED B6) — LDIRX under another opcode; the scaling the name promises is not in the Next’s design.
  • A generated Z80N conformance table, run through the simulator, pinning flags, registers, timing and memory for every Z80N instruction across nine register seeds.
  • MODULE / ENDMODULE documentationdocs/index.md gains a Modules section covering qualified names, nesting, resolution, local labels and GLOBAL.
  • Reserved-word diagnostics (E1060, E1061): a macro or macro parameter named after a word the tokeniser claims is now named and refused, rather than only reported as invalid. docs/diagnostics.md lists the set.

Fixed

Wrong output, silently:

  • A STRUCT instance emitted no bytes at all.
  • Index prefixes, incbin data and !rhai output leaked past a module body discarded by --gc-modules, shifting everything after them.
  • A forward reference in a byte-wide operand (out (PORT),a) overwrote the following instruction.
  • nextreg’s second operand overwrote its register number.
  • A MODULE body was pinned by the program’s ORG, and a re-placed first line could land in ROM.
  • $ in a relocated MODULE body gave the address the line had before it moved. A ds size cannot be corrected this way, so it is reported instead (W1073).
  • An unclosed MACRO, STRUCT, ENUM, !test or !rhai swallowed the rest of the source, assembling to an empty binary and exiting 0 (E1074).
  • ADD HL,A, ADD DE,A and ADD BC,A left the carry they clear, in both the simulator and docs/instructions.md.
  • ENDMODULE without MODULE was accepted silently, closing a module early and storing everything after it unqualified. A module left open at end of file is reported too (E1064, E1065).

Source that should have assembled, or should not have:

  • A quote of one kind ended a literal of the other, so db "it's" was refused.
  • Backslashes were stripped from strings, and '\' never closed. They are escapes now, with an unknown one refused (E1072).
  • db (5) was rejected where db (1+2) worked; the same for dw, ds, ORG and both constant forms.
  • A reserved-word label in brackets failed inside a !test body.
  • A brace inside a Rhai string or comment ended the block early.
  • @name was rejected in the IN/OUT port operand and as a jr/djnz target.
  • GLOBAL defined the name it was meant to declare, so an export wrote the wrong address and a GLOBAL naming nothing assembled (E1070, E1071).
  • Choosing a target silently discarded a requested -M code-size limit.
  • SIZEOF on an included binary reported E1037.
  • Constants and ENUM members inside a MODULE were not namespaced, so two modules could not each define a WIDTH — and the collision named the innocent one.
  • A reserved word as a MODULE name was accepted silently, so every reference into it failed later at the call site (E1062, E1063).

!test:

  • A block can now call the routines in your program, rather than only instructions retyped inside it.
  • init_* and assert_* read addresses before relocation settled, so an assertion could pass by coincidence.
  • The Z80N flag columns in docs/instructions.md were guessed rather than measured; every row was re-derived and JP (C) added.

!rhai:

  • ctx.warn built a diagnostic and dropped it.
  • ctx.get_label and friends handed Rhai a value it had no operators for, so a build-time guard read as a check that had passed.

Diagnostics:

  • E1003 names the undefined symbol and suggests a fix; a typo inside a MODULE now suggests that module’s own symbols.
  • E1036 says which limit was reached and by how much.
  • Misplaced dw, ds, dh and incbin are reported, and misplaced code warns (W1068, W1069).
  • Diagnostics after a MACRO named the wrong line; a header address error named the wrong line; a forward reference had no location in find_references.
  • bit/res/set named the wrong operand for an undefined symbol.
  • An out-of-range value was reported twice.
  • A KEEP-ed label is no longer reported unused.
  • A !nex pc or !sna pc entry label failed if any module carried a label operand.
  • Local labels referenced by their short name were reported unused, so -W produced a bogus warning for the ordinary local-loop idiom.
  • NEX and SNA entry labels were reported unused, so -W was never clean on those targets.
  • Macro parameter list diagnostics reported each other’s error.

Changed

  • Breaking — the !test CPU core is now hg80, and the vendored iz80 fork is gone. It corrects four Z80N behaviours — the block-copy flags, LDWS’s carry, PIXELDN’s bottom-row wrap, and JP (C) executing at all — so a test asserting any of the old ones will now fail. No cycle assertion in the suite moved.
  • Breaking — -M/max_code_size limits how much code there is, not the address it reaches. A source relying on the old behaviour will build where it used to fail.
  • Breaking — constants no longer fall back to global scope from inside a MODULE; use @NAME. Cross-module Gfx.WIDTH is unaffected.
  • Breaking — a stray backslash in a literal is now an error rather than being silently stripped.
  • Z80N is decoded only for a Z80N target, where the old core decoded it whatever the target.
  • MODULE is unconditional — the modules build feature is gone, and allow_dot_in_labels with it.
  • Addresses are settled before anything is written, by a decide phase between gathering and emitting. Forward-reference patching is gone with it, so a value that genuinely cannot wait now fails rather than patching over the opcode it helped choose.
  • Module elimination is decided once rather than by assembling the source twice.
  • A green release also publishes a source-only mirror to Codeberg, with CI, packaging and release tooling stripped out.
  • Test coverage expanded from 341 to 575 tests.

[1.7.5] - 2026-07-17

Added

  • Data-driven memory model per target: the Next pages in 8 KiB pages and slots, the 128K in 16 KiB banks. page N, slot S maps any 8 KiB page into a slot, including odd pages 16 KiB banking cannot express; bank N, slot S now targets any window, not only $C000.
  • Paging-aware !test simulator: paged code runs with a live MMU, so NEXTREG, the NEXTREG I/O ports ($243B/$253B) and the 128K paging port ($7FFD) remap slots during execution.
  • Device-variant selection: --ram 2mb/1mb/512k, or ram in fantasm.toml, range-checks bank and page numbers against the real RAM.
  • Paging builtins _page, _bank and _slot — the current instruction’s 8 KiB page, its 16 KiB bank, and its slot index.
  • NEX generation checks: a warning when the output has no code, or the entry point lands in an empty bank.
  • Online update check: --check-updates, plus a throttled notice on the banner. Disable with FANTASM_NO_UPDATE_CHECK or the update-check build feature.

Fixed

  • An empty parenthesised operand (ld a,()) is a syntax error rather than assembling as (0).
  • SLD output conforms to sjasmplus SLD v1: L records replace the deprecated F, the Z device record carries the source file, spurious T records and the non-standard E record are gone, and <page> numbers are banking-correct.

Changed

  • Test coverage expanded from 304 to 341 tests.

[1.7.4] - 2026-07-17

Fixed

  • --lsp wrote the version banner to stdout, corrupting the JSON-RPC stream.
  • _asmpc and _codesize now resolve consistently — _asmpc is the current instruction’s PC, _codesize the live PC.
  • An out-of-range jr/djnz errors rather than silently truncating the offset.
  • !test init_reg/assert_reg rejects operands that name no register ((hl), af') rather than mapping them to A/HL.
  • Block-graphics glyphs (codes 128–143) were falsely flagged as non-ASCII.

Changed

  • Internal maintainability refactor, with no behavioural change.
  • Test coverage expanded from ~206 to 304 tests, including a self-contained byte-encoding oracle for the base Z80 instruction set.

[1.7.3] - 2026-07-15

Added

  • Rhai host API: define_label, symbol introspection (symbol_exists, constant_names, label_names), and data emission — emit_byte, emit_word, emit_bytes, emit_string, emit_string_zero. These advance the PC, so labels placed after a block stay correct.
  • Rhai language: the array, map and blob packages and range iteration (for i in 0..n), which is what makes build-time table generation practical.
  • docs/rhai.md: the entry points, the ctx host API, sandbox limits and worked examples.
  • LSP: completion and hover for the Rhai host API inside !rhai blocks.

Fixed

  • A !rhai block wiped every byte emitted before it, and peek/poke read stale data, because the script ran against an empty copy of the emitter.
  • Compile and runtime errors from a block are reported at the offending source line rather than the !rhai directive line.

Changed

  • Rhai expressions are read-only: mutating assembler state from a function invoked via rhai(...) in an expression now errors rather than being silently discarded. Do such work in a !rhai block.
  • The Z80 CPU core is vendored in-tree at crates/iz80, previously a sibling checkout, so building from source needs no separate repository.

[1.7.2] - 2026-03-30

Changed

  • Log filtering is applied at compile time via release_max_level_warn, and the default tracing level in release builds is WARN for both the CLI and the LSP server.
  • README.md covers LSP support, unit testing and instruction timing.

[1.7.1] - 2026-03-27

Fixed

  • E1018 Unclosed parentheses when a label name coincides with a directive mnemonic alias — word, for instance. The token reader preserves the source spelling and the expression parser recovers it, so such a token is a label reference rather than a directive.

[1.7.0] - 2026-03-25

Added

  • Rhai scripting: !rhai { ... } blocks, external scripts (!rhai "file.rhai") and expression calls (rhai("func", args...)), with symbols, constants, memory and diagnostics exposed to the script and AST caching between passes.
  • Sandboxing: operation limits, expression depth caps and a whitelist of packages.
  • Diagnostics: fuzzy symbol suggestions, hints, and banking and address context on errors and warnings.
  • Namespacing: hierarchical MODULE scoping, behind a build feature, and deterministic local-label renaming in macro expansion.
  • Incremental assembly for the LSP, memory overlap detection, and a plugin-style output format registry.
  • Structured tracing events and multi-span diagnostics in both the CLI and the LSP.

Fixed

  • Next bank mapping and ordering in NEX output, and a SNA 48K regression that left the PC on the stack.
  • -O origin initialisation, script execution across passes, and symbol reference tracking in read-only evaluations.

Changed

  • Core assembler logic is decoupled from file IO and split into per-pass modules.
  • Heap allocations reduced across emission, instruction encoding and macro handling.
  • LSP logic is decoupled from tower_lsp behind domain types, with signature-help metadata added.

[1.6.3] - 2026-02-28

Added

  • LSP hover carries instruction timing (T-states) for every Z80 and Z80N instruction, including conditional timing (taken and not taken) and block-instruction timing (repeated and final).
  • Hover metadata for LDI, LDIR, LDD and LDDR, which had none.

Fixed

  • A crash on a duplicate label or constant defined via -D.
  • A global define via -D raised “Attempt to redefine label or constant” when used with include.
  • LSP diagnostic mapping and logging.

[1.6.2] - 2026-02-28

Added

  • LSP: go-to-definition, find-references and hover for every label and constant; workspace symbol search; completion for instructions, directives and symbols; signature help for operands and directive parameters.
  • The instruction set reference is embedded in the binary, so the LSP needs nothing alongside it.

Changed

  • LSP analysis is incremental, using in-memory assembly and ProjectSnapshot state restoration.
  • An LSPQuery interface separates IDE features from the core assembler.
  • Project root detection and multi-file diagnostic reporting across multi-directory projects.

[1.6.1] - 2026-02-27

Changed

  • Directive processing is a trait-based DirectiveRegistry, and core directive logic returns Result rather than signalling failure by other means, so a directive’s error propagates with its own diagnostic.

[1.6.0] - 2026-02-26

Added

  • !test blocks: unit tests written in assembly and run on an integrated Z80 simulator (iz80), with init_reg and init_mem for setup and assert_reg, assert_mem and assert_cycles for verification.
  • --test runs every !test block and reports the results.

[1.5.0] - 2026-02-26

Added

  • Instruction timing: T-state calculation for every Z80 and Z80N instruction, including conditional jumps (JR, CALL, RET), block instructions (LDIR, CPIR and the rest) and NEXTREG — 20 T-states for NEXTREG n, n', 17 for NEXTREG n, A.
  • $tstates (or tstates) in an expression gives the accumulated timing at that point.

[1.4.3] - 2026-02-26

Added

  • An explicit build subcommand — fantasm build main.asm. fantasm main.asm still works, and remains the default.

[1.4.2] - 2026-02-26

Changed

  • Usage and help output describe every option and subcommand, and init’s help names its parameters and metavars.

[1.4.1] - 2026-02-26

Added

  • fantasm with no arguments prints help rather than attempting a build or reporting an error.

[1.4.0] - 2026-02-26

Added

  • fantasm init scaffolds a new Z80/ZX Next project, interactively via cursor-key prompts for name, target and format. Overwrite protection, with -f/--force to bypass it.

[1.3.6] - 2026-02-26

Changed

  • Documentation reorganised and cross-referenced: docs/index.md rewritten as the entry point, docs/ASSEMBLER_OUTPUT.md renamed to docs/diagnostics.md, docs/z80n.md and docs/Z80_INSTRUCTIONS.md consolidated into docs/instructions.md, and README.md cut back to a quick start plus links.

[1.3.5] - 2026-02-26

Changed

  • Output format handling (NEX, SNA, SLD, Intel HEX, binary) is consolidated into the formats module, and ForwardReference, IfBlock and TokenReader moved to modules of their own.

[1.3.4] - 2026-02-25

Added

  • A shell shebang at the start of a source file is detected and ignored.

Fixed

  • Line numbering was off in a file carrying a shebang.
  • Diagnostic error codes across the parsing stages.

Changed

  • Diagnostics are colour-coded by level: progress messages use Info, and !message uses a dedicated Message level.
  • docs/ASSEMBLER_OUTPUT.md renamed to docs/diagnostics.md; docs/instructions.md added with the full instruction set reference.

[1.3.3] - 2026-02-25

Added

  • !target and !format set the machine target and output format from the source.
  • NEX v1.3: CRC-32C checksums, automatic RAM detection and entry bank configuration.
  • SLD: a 10-field format with position tracking and macro support.

Fixed

  • !format and !target failed when given a reserved keyword.
  • The output extension now follows the selected format.
  • Second-pass errors carried no source line.

Changed

  • Output handlers modularised into the formats module.
  • A NEX file includes only banks holding data or explicitly requested.
  • Character mapping extended to block graphics and special symbols.

[1.3.2] - 2026-02-22

Added

  • NEX v1.3: Next-specific header fields and the !nex sub-commands core, entry, preserve, crc and bar.

Fixed

  • NEX header offsets and bank bitmap mapping.
  • The default NEX stack pointer is $FFFE.

[1.3.1] - 2026-02-15

Added

  • LSP server with real-time diagnostics.

[1.3.0] - 2026-02-06

Added

  • fantasm.toml for project settings, defines and build tasks (pre_build/post_build).
  • LSP server and a Zed editor extension.
  • Rewritten documentation, with diagnostic tables.

Changed

  • tree-sitter-fantasm moved to a Git submodule.
  • !message takes multiple arguments and system variables (codesize, asmpc and the rest).

[1.2.0] - 2026-01-17

Added

  • BANK for the ZX Spectrum Next and 128K targets.
  • 128K SNA snapshots, with NEX and SLD bank tracking refined to match.
  • A recursive-descent expression evaluator, replacing the external dependency.
  • --target/-t and --format/-f, with the format inferred where it is not given.
  • ZX Spectrum character set translation, and memory limit enforcement per target.

Fixed

  • The CSpect break opcode was encoded wrongly.
  • Errors in token reading and label exports.

Changed

  • Rust 2021 edition, and dependencies refreshed.