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 infantasm.tomlsets what each warning is filed as, by code:w1049 = "deny"fails the build on an unused label,w1025 = "allow"reports nothing.defaultcovers the rest. An entry naming a code that does not exist (W1131), one that may not be reconfigured (W1132) or a level that is notallow,warnordeny(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, ldiis an unrolled copy withoutldir’s loop overhead. Only an instruction may be repeated (E1125); a run of identical bytes is stillDS <count>, <fill>. A repetition runs to 65536 statements (E1126), asDSruns to 65536 bytes.REP <count>with nothing after it opens a block, closed byENDRorEND. A local label inside is named apart per repetition, so adjnzreaches its own copy. A block the source never closed is reported (E1074) rather than swallowing the rest of the file.!assertand!debugrecord an annotation into the--sldfile 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
--sldfile says so once (W1120), rather than dropping them in silence. - An annotation inside a
MACROis recorded once per expansion, at each address it reached, naming the invocation. - A reserved word may name a
STRUCTmember, anENUMmember or type, aMODULEor a!library—device,ldandaare all names now. A macro’s name, a macro’s parameter and aSTRUCT’s own name still refuse one. E1062is withdrawn. It refused a module or library named after a reserved word.- A
STRUCTmember may be anotherSTRUCT:tl PtgivesRect.tl.xandRect.tl.y, andSIZEOFcounts them. Initialisers stay flat — one value per byte, in declaration order. - A
STRUCTholding itself is refused (E1118), and a name where a member’s size belongs that is not a type says so (E1119). !nex version V1.2orV1.3overrides the version chosen for a.nex.V1.2is refused where the file uses a V1.3 feature, naming it (E1115); any other spelling isE1114.init_memandassert_memtake 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, underE1200. --check-updateson 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.--lspon 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>, andreport_discardsunder[artifacts], write down what--gc-modulesand--gc-librariesthrew away, grouped by module or library with sizes and a total. It is written even where no sweep ran, and says which did.assert_checksumfolds 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 isE1110, and a physical one outside memory isE1111. - A directive with optional arguments spells them
name=value, separated by commas; the positional forms are unchanged. Mixing the two isE1107, an unknown optionE1105, one given twiceE1106, and an incomplete spanE1109. init_cycle_limit <limit>in a!testblock 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.tomlkey keeps its old spelling working and says what replaced it (W1103). The aliases come out in 2.1. --reloc <file>, andrelocunder[artifacts], write what a loader must patch to run the image at another address. An address the table cannot describe isE1092, and output that is not one image moved by one delta isE1102.!library Namemakes the rest of a file a library, guarded so a secondINCLUDEis free (E1095,E1096).- An
INCLUDEabove a!libraryline stays global; below it, its names are the library’s. INTERFACE/ENDINTERFACElists 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);@NAMEgoes round it. --gc-libraries, andgc_librariesinfantasm.toml, discard unreached!librarycode.- A
MODULEused only to place bytes survives--gc-librarieswithout aKEEP. - A
STRUCTtype and aMACRObelong to the library or module declaring them, so two may each define aPt. @NAMEreaches 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
!rhaiblock can call functions defined by any script above it, block or file. - A Rhai function can be called as
rhai.double(3), alongsiderhai("double", 3). rhai.gfx.pack(1)calls thepackingfx.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
!rhaiblock can be written inside a!testbody, and what it defines belongs to that test alone. - A failing
!testassertion has a code of its own (E1091), reported against the line it is written on. !opt maxcodesizetakes 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. Atargetfrom the editor still wins. - A
[[target]]answers for the file itsmainnames, so two sources in one directory get their own settings. - The language server’s output pane names the
fantasm.tomlthat answered, the target chosen and the search path. - Every build carries the release notes as
RELEASE-NOTES.md— beside the licence in the.deband the Arch package, and in the macOS tarball and the Windows zip. They are the release’s description on the download page too.
Changed
E1034is 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 isE1013.E1013raised 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.I1051is withdrawn. A build’s progress lines are no longer filed as diagnostics.-vprints the same lines as before.E1062andE1063name whichever ofMODULEand!librarywas written.W1084names the flag that discarded the routine,--gc-modulesor--gc-libraries.W1086andE1087no longer sayMODULE, which a library body does too.E1024described 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.
--helplists every option there is,init’s included, and-hand-Vare read wherever they appear rather than only as the first argument.- A refused argument is named, and the nearest spelling suggested.
-Oand-Mtake a number written any way the source may write one —$9000,9000h,0b1001,_between digits. Decimal was all they accepted.--helpgroups the switches under Build, Machine, Assembly, Dead Code, Artifacts and Reporting.-vvsays how full the image is against whichever ceiling applied, and where eachMODULEand!librarybody was placed.-vandverbose = trueare unchanged.-vsays whereINCLUDEandINCBINlook, in the order they are searched. That order is documented for the first time.--helpdescribed--test,-eand-nwrongly:!testblocks are assembly run on the simulator,-ewrites theGLOBALlabels and constants, and-nhides 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)/-1aborted 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: Bumpnow definesstartat the body’s first byte, as every other statement on a labelled line does.W1094no longer fires on this shape, having pointed at the wrong column. - An
ORGthat 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
bankwas also written to whichever other bank its position in the file happened to fall in, so a.nexcarried the code twice. - Seven of the block-graphics characters emitted the code below the right one —
db "▘"gave the blank cell, and nothing could emit135at all. They are128plus 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,hlandadd ix,iyassembled asadd hl,hlandadd ix,ix. None is a Z80 instruction, and each is now refused (E1007).ld ixh,iylandld iyh,ixlassembled asld ixh,ixlandld iyh,iyl. An index half cannot meet the other index register, nor plainh,lor(hl), and now says so (E1008).- Two comparisons on one line lost the operand between them:
db 1<2<3assembled as1<<3and gave 8. Spacing hid it,1 < 2 < 3always being right. ^was not an operator: discarded from a constant, soV equ 255 ^ MASKgave 255, and refused in an operand. It is bitwise exclusive-or, binding tighter than|and looser than&.-i/--case-insensitiveturned theINTERFACEprivacy check off entirely, so every private name in every library was reachable and nothing was said (E1101).- Under
-i, aSTRUCTtype whose name was not already upper case could not be used:here Point 3, 4wasE1005with no message andSIZEOF(Point)wasE1037, on source that assembles without the flag. - Under
-i, a local label inside aMODULEwas named with the module prefix twice —Beta.thing.local_herereported and exported asBETA.BETA.THING.LOCAL_HERE. The program assembled; the name a diagnostic,--export-labelsand the.sldcarried was wrong. - An
!assertor!debugin 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
.snarestored with the 128K editor ROM paged in rather than the 48K BASIC ROM, so a restored program callingRST 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
!testcould not read an MMU register back —$50–$57returned zero — so a routine that borrows a slot could not put it back. No other Next register is modelled. --gc-modulesand--gc-librariesdiscarded aSTRUCTinstance reached only through a member name —ld (Text.view.colour),a— so every access read and wrote the wrong memory, and!testpassed.- A constant assigned from a
MODULElabel —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
MODULEwas never corrected after the body moved, and every read of it emitted the wrong address in silence. WhereW1086did fire it named a value the image did not hold, and theE1087for anifinside 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. ifchose 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
!rhaiblock picked different ones. - A
!testwas cut off at 1,000,000 T-states and reported as failed to halt, soassert_cyclesabove that figure could never report. A block’sassert_cyclesnow buys it that long, and reaching the ceiling names the figure (E1104). - An
ENUMmember’s value was read only as far as a plain number, soBig = 5 * 2was 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
IFDEForIFNDEFnested inside a branch that was not being assembled was refused, blaming a balancedENDIForELSE(E1027,E1028). - An
IFinside 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
ELSEfor oneIFemitted 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)aboveDISP equ 4wasE1021. It is the last operand to be settled at emission like every other. ld (ix+0),300truncated its value in silence, whereld a,300says so (W1011).-D DEBUG=1, and a[defines] DEBUG = truein 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 7thenld a,MESSAGEwasE1008, whileld hl,MESSAGEassembled. IFDEFandIFNDEFrefused a constant named after a directive (E1006), soIFDEF DEBUGcould not ask about a build flag of that name.!opt device,!opt targetand!opt formatwere refused outright (E1020) — three of the six options documented.!opt format hexwas refused as a format calledDH.!opt case_insensitivetakes an underscore. The documentedcase-insensitivenever worked.- An
INCLUDEinside aMODULEreportedE1064, so an included file could not sit inside one at all. - A
STRUCTinstance’s members and anENUM’s members reportedE1003when reached by their short names from inside the module that declares them. - A
!testbody written inside aMODULEor a!librarycould not use that scope’s short names, socall scratchwasE1003where every line around it used exactly that form. - The assertions beside it still could not:
call fillresolved andassert_mem bufferon the next line wasE1003, the two halves of one block disagreeing about what a bare name meant. - A
!testoperand that would not resolve was reported against the!testline, with no source quoted, rather than against the line it was written on. - A Rhai function could not be called from a
!testbody, 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 reportednas undefined. SIZEOFof a type declared in a module reportedE1037.SIZEOF(@Name)was refused (E1021), so a library declaring a type named as the program’s could build one —here @Pt 1, 2has always worked — and not ask how big it is.E1037said only SizeOf cannot be determined, naming nothing to go and look at.SIZEOFof aSTRUCTtype was refused in=,EQUandIF, and theE1003it 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.logwrote escape sequences into the log. Colour is now used only where the stream is a terminal;--color auto|always|neveroverrides it, andNO_COLORandTERM=dumbare honoured. default-features = falsedid not compile, so the documented way to depend on FantASM for assembling alone was the one that failed. Every combination ofnex,sna,lspandupdate-checkbuilds, 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
ALIGNdirective that does not exist, and omitted most of the ones that do —MODULE,INTERFACE,KEEP,GLOBAL,IFDEF, the!testcommands and the rest. The list is now the assembler’s own. - Hover said nothing for any
ctx.member in a!rhaiblock; only the one free function ever answered. - A line merely mentioning
!rhaiin 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
E1049while its message quotedW1049, 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
MODULEor!librarygot 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.
--lspwrote a project-file warning to stdout, which is the JSON-RPC channel, so a strict editor failed to start the server. Afantasm.tomlthat would not parse stopped it starting at all — no diagnostics on the file that would have said what was wrong.--versionfailed in a directory holding afantasm.tomlit could not read.- A second
assemble()on oneAssemblerinherited the first source’s said-once flags, soW1068,W1069andW1120were suppressed for the second. Only an embedder reusing one is affected; every shipped path builds a fresh assembler. -e,--sld,--relocand--report-discardswere 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_discardsin a project file resolved against the working directory rather than besidefantasm.toml, unlike the other three artifact paths.- An ignored shebang line (
I1057) was documented and shown to nobody: nothing displayed theInfolevel. E1024said 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
--sldfiles matched: the records were written in hash-map order, so a debug artifact could not be diffed between builds. - No two
--export-labelsfiles matched either, for the same reason. The symbols follow theirGLOBALdeclarations 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
$C000inbank 3read as Bank 6. It saysBank 3on a 128K now, andPage 6on 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 whatGfxholds, rather than every instruction, directive, label, constant and macro in the program. Completion narrows on what has been typed,workspace/symbolanswers its query instead of shipping the whole table on each keystroke, and a name inside aMODULE,!library,STRUCTorENUMreaches the client with its owner, so a client can group them. - An
INCLUDEpath is clickable in the editor, opening the file it actually resolved to. One that did not resolve is left unlinked —E1004is what says why. - A
MACRO,STRUCT,ENUM,MODULE,INTERFACE,REPor!testbody 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, tableandcall routineare one edge, and each entry says so — the graph is the one--gc-moduleswalks, 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.drawbesideSnd.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.tomlabove it — is now assembled in the editor with the configuration of a project whoseinclude_dirsname 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
INCLUDEline (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, aGLOBALor anINTERFACEentry naming nothing prints the line it was written on and underlines the name. Each reportedfile:lineand left the reader to find the directive.E1100points at theINTERFACEentry rather than at the!libraryline. - 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, 0marked the final0while complaining aboutNOPE— and the editor drew a ten-character mark from a character further left still. E1101, a name outside a library’sINTERFACE, 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
W1129andpageon a machine that pages in 16K isW1130. Both were reported asW1020 invalid assembler option, which neither is. - A
MACROwhose 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 itsENDMwas dangling. The body is read to itsENDMand dropped, and the refusal is all that is said. - A misspelt key in
fantasm.tomlwas accepted, ignored and never mentioned:divice = "zxnext"built for the default machine,outpt = "a.bin"left the name inferred, and a[tasks] prenever ran. Each is named with its section now, with the nearest real key where there is one (W1124). W1049came 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 unknownKEEPorGLOBALtarget, an unkeptINTERFACEpromise — which one you were told about changed between builds too. All four follow the source now.W1049 unused labelfired on the untouched members of aSTRUCTinstance, and on the instance when only a member was read.W1049 unused labelfired on a label named anywhere but first in an expression —ld hl, first + secondreportedsecondas dead.- A diagnostic raised inside a
!testbody lost its code, position and hint. E1026ended with a stray space.E1020 Invalid assembler optionnamed 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 isE1134, and an output format this build does not write isE1135. All three name what is wrong and underline it.E1005ran 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.bankandpageon 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 errorstood for five unrelated faults. A--testrun’s failure count isE1138and reads 1 test failed rather than 1 tests; a test with no body isE1139and one with nowhere to be placed isE1140, both naming the block; a!rhaimissing its brace is a syntax error; andfantasm.toml’s two target faults areE1141andE1142.- A mistyped machine —
-d zx81, ordevice = "zx81"— built as a flat 64K one without a word, though!device zx81in 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
$FFFFwarned 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 oneAssemblerran the first source’s!testblocks 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’smainwas analysed on its own, so the editor reported faults the build did not — a name its parent supplies wasE1003on screen whilefantasm buildsaid 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 theINCLUDEline. - Every
includeline carried a mark on source that assembles cleanly (I1051).
Documentation:
- The directive reference documented
!messagewith the comma-separated argument list that was removed, and never documented the"{NAME}"form that works. !messageand!rhai printunderlined 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.
FormatContextcarriesspandpcalready evaluated, the paging state, and a&SymbolTablefor the SLD writer;contextandexprare gone, andresolve_addrisHeaderAddress::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.
NumberParserandZXAsciiare undertokens,HighLowunderemitter, and a directive’s spellings aretokens::directive::DIRECTIVE_NAMES—directive::registrykeeps the handlers. Eleven pairs of areas depending on each other both ways became five. SymbolTableandSessionStateanswer for themselves. 104 methods moved offAssemblerContextonto 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.Optionskeeps what the invocation asked for;opts.deviceisopts.build.device, andBuildTarget::optionsis aTargetOptions, which cannot hold the target list. - Breaking —
Placed’s twelve fields are grouped.p.pc,p.asm_pcandp.slot_pagearep.at; the three flags the sweep reads arep.about, which is theLineStatethe encoder already built. - Breaking — what a run learns lives in
Assembler::run.sld_records,testsand the rest moved into aRunStatethatpreparerebuilds;asm.sld_recordsisasm.run.sld_records.warningsanderrorsstay where they are, being what the caller reads. - Breaking — six
Diagnosticconstructors are gone.warning,fatal,infoandmessagewere called only by tests,error_with_hintandfatal_with_codeby nothing. UseDiagnostic::at(level, code, location, message)withwith_hint,with_line_codeandwith_related. - Breaking — a bank, an 8K page and a slot are
Bank,Page8kandSlot, not bare integers.MemoryImage::banksandwrittenare keyed byBank;bank_data_mut,current_bank,slot_pageand the simulator’sset_slotsandload_banktake and give the new types. Wrap the number —Bank(5),Page8k(11). - Breaking — an origin is a
Z80Addr.MemoryImage::get_base_addressgives one andset_base_addresstakes one;Z80Addr::offset_fromis the crossing to aFileOffset. - Breaking — the
!testsimulator’s store is addressed byPhysAddr.Simulator::peek_physicalandpoke_physicaltake one, which is whatphys=names. - Breaking —
SLDRecord::record_typeis anSLDType, not aString.SLDTypenames only the records FantASM writes —Trace,Label,Device— and itsDisplayis the letter. The unusedFunctionandEndOfFileare gone. - Breaking —
assemble()writes no files.save()writes the image and then every artifact — labels,--reloc, the discard report, and the SLD last. Callsave(), orsave_labels()andsave_sld()for one of them alone. - Breaking —
DiagnosticsEngine::check_filereturns a third value.(diagnostics, messages, assembler), wheremessagesis!messageand!rhai printoutput for the client’s output pane. - Breaking —
Assembler::device()andOptions::devicereplaceAssembler::target()andOptions::target. default-features = falsebuilds again:assert_checksumneeded a CRC that only thenexoutput format pulled in.asm.test_failurescarries aDiagnosticper failing test.RUST_LOGcould raise the tracing level and never lower it, so a debug build’s stderr could not be quietened —RUST_LOG=offstill 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 1060–1062.
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-moduleswith--testnow says when a routine only a!testcalls has been swept (W1084), instead of leaving the test to fail with Execution timed out or failed to halt.KEEPorGLOBALon the routine makes the test run.- A
printin a!rhaiblock 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 showsE1016on every Z80N instruction. A client that sends nothing is unaffected. rhai("fn")may answer with a string:BANNER equ rhai("banner")thendz BANNER.--sldnow carriesEQUconstants (+equ),MODULE/ENDMODULEboundaries (+module/+endmod), and label names split into module/main/local with a+usedtrait — 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).EQUjoins text with+:BANNER equ __NAME__ + " v" + __VERSION__.SIZEOFanswers the length of a constant holding text, in the bytesdbwould emit.- A
GLOBALconstant holding text is exported asNAME = "text"in the labels file. !rhai:ctx.get_constantreturns text for a constant holding it, andctx.define_constantaccepts 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
fantasmrun builds every target, in dependency order. A file with no[[target]]is unaffected. --only NAMEbuilds one target and what it depends on, and is repeatable;--alladds targets markeddefault = false. Spelt--onlybecause--targetnames the machine.[target.tasks]gives a target its ownpre_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.
--testcounts are reported against the target the tests came from, and a target with no!testblocks says so rather than being left out.- A
dependscycle is refused, naming every target in it (E1077). - An
incbinof a target’s output left out ofdependsnames that target (E1078). - Two targets writing the same file are refused before anything is assembled.
Changed
ifdef/ifndefon a name defined by#definenow says so (W1079) instead of quietly deciding false.#definemakes a macro and the conditionals test constants; the decision is unchanged, but the block no longer vanishes in silence.#ifnow exists, completing the#-prefixed conditional family the reference already described. It was the only one of the five missing, and using it gaveE1002 Invalid character in label.IFtakes an expression and is true when it is not zero, where it used to accept onlyIF <constant> = <value>.IF FOO = 1decides exactly as before;IF FOO > 1,IF 1andIF (FLAGS & 4)now work, and a label may be tested as well as a constant.- Breaking —
[assembler] formatnow takes effect. It was parsed and ignored, so a project file sayingformat = "nex"produced whatever the output extension implied. Check any project file whoseformatand output extension disagree;-fstill wins. - What a
!rhaiscript says throughctx.warnorctx.erroris now1080, in the script’s own words. Both were1058 General error, so a script’s warning readGeneral error: …;ctx.errorcame back wrapped four deep asRhai script error: Runtime error: Error: t.asm:2:5 - General error: stop.1200still covers a script that will not compile, or a fault in the engine. - A failing
--testnow 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 9reportedE1022 Invalid Hexadecimal stringandword equ 5reportedE1021, 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 messageassembled whileld hl,messagewasE1005. It now resolves everywhere, including when defined further down the file. An aliased spelling (hex, alsodh) that is never defined still reports a syntax error rather than guessing which was written. - A label called
pageorbankwas silently replaced by the builtin of that name, socall pageassembled a call to the current page number instead of to the label. Use_pageand_bankfor the values. E1058from a failing test pointed at<none>:0with the caret underendtest; it now names the!testblock that failed.!messagesilently 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.--sldrecorded aMODULEbody 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 but10,5,2and the one currently selected, so the file loaded without a bank the program pages in at runtime.- The NEX reference documented neither
!nex ramnor!nex cfg. - The reference listed the paging values in expressions as
bankandpage, which are directive names and giveE1005. They are_page,_bankand_slot, the last of which went undocumented;asmpc,codesizeandtstatesstill take no underscore. - A parenthesised sub-expression was rejected with
E1021when 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.rhaiextension is required. fantasm --helplisted neither--ramnor--check-updates, and none ofinit’s options.- Two diagnostic messages were misspelt:
1023read “out of range will and will be truncated”,1025read “extra characters at and of line”. - The diagnostics reference documented no
1001,1058,1059or1200at all.E1049/W1049andE1060/W1060are 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.tomlcould only ever be turned on:gc_modules = truecould not be overridden from the command line, because false and unset were the same value. Use a--no-flag. -O 0and-M 0read as unset, so afantasm.tomloriginormax_code_sizewon instead. Both are now explicit values.- A computed port operand was a syntax error:
out (0x80-0x10),aandin a,(0x80-0x10)now assemble. 12EFhwas a syntax error: a trailing-radix hex literal needed a leading zero rather than any leading decimal digit.1010Bwas 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.dbanddznow assemble it, andEQUcan hold one; text where a number is required is an error (E1075). --gc-modulesdiscarded a routine that the one above it falls into, so the caller ran on into whatever survived.KEEPis 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.htmlbundled into every 1.8.0 artifact listed no entry for hg80, the CPU core!testruns on. Anyone redistributing a 1.8.0 binary should replace the file.
[1.8.0] - 2026-08-19
Added
@namereaches global scope from inside aMODULE—call @helper,ld a,@WIDTH. It means global, not one scope out.- Module dead-code elimination (
--gc-modules, off by default): a routine inside aMODULEthat 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) —LDIRXunder 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/ENDMODULEdocumentation —docs/index.mdgains a Modules section covering qualified names, nesting, resolution, local labels andGLOBAL.- 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.mdlists the set.
Fixed
Wrong output, silently:
- A
STRUCTinstance emitted no bytes at all. - Index prefixes,
incbindata and!rhaioutput 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
MODULEbody was pinned by the program’sORG, and a re-placed first line could land in ROM. $in a relocatedMODULEbody gave the address the line had before it moved. Adssize cannot be corrected this way, so it is reported instead (W1073).- An unclosed
MACRO,STRUCT,ENUM,!testor!rhaiswallowed the rest of the source, assembling to an empty binary and exiting 0 (E1074). ADD HL,A,ADD DE,AandADD BC,Aleft the carry they clear, in both the simulator anddocs/instructions.md.ENDMODULEwithoutMODULEwas 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 wheredb (1+2)worked; the same fordw,ds,ORGand both constant forms.- A reserved-word label in brackets failed inside a
!testbody. - A brace inside a Rhai string or comment ended the block early.
@namewas rejected in theIN/OUTport operand and as ajr/djnztarget.GLOBALdefined the name it was meant to declare, so an export wrote the wrong address and aGLOBALnaming nothing assembled (E1070,E1071).- Choosing a target silently discarded a requested
-Mcode-size limit. SIZEOFon an included binary reportedE1037.- Constants and
ENUMmembers inside aMODULEwere not namespaced, so two modules could not each define aWIDTH— and the collision named the innocent one. - A reserved word as a
MODULEname 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_*andassert_*read addresses before relocation settled, so an assertion could pass by coincidence.- The Z80N flag columns in
docs/instructions.mdwere guessed rather than measured; every row was re-derived andJP (C)added.
!rhai:
ctx.warnbuilt a diagnostic and dropped it.ctx.get_labeland friends handed Rhai a value it had no operators for, so a build-time guard read as a check that had passed.
Diagnostics:
E1003names the undefined symbol and suggests a fix; a typo inside aMODULEnow suggests that module’s own symbols.E1036says which limit was reached and by how much.- Misplaced
dw,ds,dhandincbinare reported, and misplaced code warns (W1068,W1069). - Diagnostics after a
MACROnamed the wrong line; a header address error named the wrong line; a forward reference had no location infind_references. bit/res/setnamed 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 pcor!sna pcentry label failed if any module carried a label operand. - Local labels referenced by their short name were reported unused, so
-Wproduced a bogus warning for the ordinary local-loop idiom. - NEX and SNA entry labels were reported unused, so
-Wwas never clean on those targets. - Macro parameter list diagnostics reported each other’s error.
Changed
- Breaking — the
!testCPU core is now hg80, and the vendorediz80fork is gone. It corrects four Z80N behaviours — the block-copy flags,LDWS’s carry,PIXELDN’s bottom-row wrap, andJP (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_sizelimits 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-moduleGfx.WIDTHis 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.
MODULEis unconditional — themodulesbuild feature is gone, andallow_dot_in_labelswith 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 Smaps any 8 KiB page into a slot, including odd pages 16 KiB banking cannot express;bank N, slot Snow targets any window, not only$C000. - Paging-aware
!testsimulator: paged code runs with a live MMU, soNEXTREG, the NEXTREG I/O ports ($243B/$253B) and the 128K paging port ($7FFD) remap slots during execution. - Device-variant selection:
--ram 2mb/1mb/512k, orraminfantasm.toml, range-checksbankandpagenumbers against the real RAM. - Paging builtins
_page,_bankand_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 withFANTASM_NO_UPDATE_CHECKor theupdate-checkbuild feature.
Fixed
- An empty parenthesised operand (
ld a,()) is a syntax error rather than assembling as(0). - SLD output conforms to sjasmplus SLD v1:
Lrecords replace the deprecatedF, theZdevice record carries the source file, spuriousTrecords and the non-standardErecord are gone, and<page>numbers are banking-correct.
Changed
- Test coverage expanded from 304 to 341 tests.
[1.7.4] - 2026-07-17
Fixed
--lspwrote the version banner to stdout, corrupting the JSON-RPC stream._asmpcand_codesizenow resolve consistently —_asmpcis the current instruction’s PC,_codesizethe live PC.- An out-of-range
jr/djnzerrors rather than silently truncating the offset. !test init_reg/assert_regrejects operands that name no register ((hl),af') rather than mapping them toA/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, thectxhost API, sandbox limits and worked examples.- LSP: completion and hover for the Rhai host API inside
!rhaiblocks.
Fixed
- A
!rhaiblock wiped every byte emitted before it, andpeek/pokeread 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
!rhaidirective 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!rhaiblock. - 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 isWARNfor both the CLI and the LSP server. README.mdcovers LSP support, unit testing and instruction timing.
[1.7.1] - 2026-03-27
Fixed
E1018 Unclosed parentheseswhen 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
MODULEscoping, 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
tracingevents 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.
-Oorigin 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_lspbehind 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,LDDandLDDR, which had none.
Fixed
- A crash on a duplicate label or constant defined via
-D. - A global define via
-Draised “Attempt to redefine label or constant” when used withinclude. - 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
ProjectSnapshotstate restoration. - An
LSPQueryinterface 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 returnsResultrather than signalling failure by other means, so a directive’s error propagates with its own diagnostic.
[1.6.0] - 2026-02-26
Added
!testblocks: unit tests written in assembly and run on an integrated Z80 simulator (iz80), withinit_regandinit_memfor setup andassert_reg,assert_memandassert_cyclesfor verification.--testruns every!testblock 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,CPIRand the rest) andNEXTREG— 20 T-states forNEXTREG n, n', 17 forNEXTREG n, A. $tstates(ortstates) in an expression gives the accumulated timing at that point.
[1.4.3] - 2026-02-26
Added
- An explicit
buildsubcommand —fantasm build main.asm.fantasm main.asmstill 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
fantasmwith no arguments prints help rather than attempting a build or reporting an error.
[1.4.0] - 2026-02-26
Added
fantasm initscaffolds a new Z80/ZX Next project, interactively via cursor-key prompts for name, target and format. Overwrite protection, with-f/--forceto bypass it.
[1.3.6] - 2026-02-26
Changed
- Documentation reorganised and cross-referenced:
docs/index.mdrewritten as the entry point,docs/ASSEMBLER_OUTPUT.mdrenamed todocs/diagnostics.md,docs/z80n.mdanddocs/Z80_INSTRUCTIONS.mdconsolidated intodocs/instructions.md, andREADME.mdcut 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
formatsmodule, andForwardReference,IfBlockandTokenReadermoved 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!messageuses a dedicatedMessagelevel. docs/ASSEMBLER_OUTPUT.mdrenamed todocs/diagnostics.md;docs/instructions.mdadded with the full instruction set reference.
[1.3.3] - 2026-02-25
Added
!targetand!formatset 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
!formatand!targetfailed 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
formatsmodule. - 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
!nexsub-commandscore,entry,preserve,crcandbar.
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.tomlfor 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-fantasmmoved to a Git submodule.!messagetakes multiple arguments and system variables (codesize,asmpcand the rest).
[1.2.0] - 2026-01-17
Added
BANKfor 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/-tand--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
breakopcode was encoded wrongly. - Errors in token reading and label exports.
Changed
- Rust 2021 edition, and dependencies refreshed.