From 3019f96087554ceb231dbb345a6a5f2a9bdf5c9d Mon Sep 17 00:00:00 2001 From: Adrian Matoga Date: Wed, 23 Sep 2026 00:29:13 +0200 Subject: [PATCH] Enable key=value long options in OPT --- source/xasm/package.d | 248 ++++++++++++++++++++++++++++++++++++++++-- xasm.1.asciidoc | 46 +++++++- 2 files changed, 282 insertions(+), 12 deletions(-) diff --git a/source/xasm/package.d b/source/xasm/package.d index ee495ef..9af42cb 100644 --- a/source/xasm/package.d +++ b/source/xasm/package.d @@ -19,7 +19,7 @@ module xasm; import std.algorithm : canFind, find, sort; import std.array : Appender, replaceFirst; -import std.conv : hexString, to; +import std.conv : hexString, to, ConvException; import std.format : format; import std.math : PI, rint, sin; import std.range : empty, front, popFront; @@ -153,12 +153,17 @@ private: bool pass2 = false; + enum Hardware { ATARI800, ATARI5200, NONE } + enum Flag : bool { OFF = false, ON = true } + enum Output { OFF, RAW, ROM, ATARIDOS } + bool optionFill; // opt f - bool option5200; // opt g + Hardware optionHardware; // opt g, opt 'hardware=...' bool optionHeaders; // opt h bool optionListing; // opt l bool optionObject = true; // opt o bool optionUnusedLabels; // opt u + bool optionOutputSet; string currentFilename; int lineNo; @@ -350,7 +355,7 @@ private: } } - string readLabel() { + string readIdentifier() { int firstColumn = column; while (!eol()) { char c = line[column++]; @@ -362,7 +367,11 @@ private: column--; break; } - string label = line[firstColumn .. column].toUpper; + return line[firstColumn .. column].toUpper; + } + + string readLabel() { + string label = readIdentifier(); if (label.startsWith('?')) { if (lastGlobalLabel is null) throw new AssemblyError("Global label must be declared first"); @@ -371,6 +380,20 @@ private: return label >= "A" ? label : null; } + unittest { + auto a = testAssembler(); + with (a) { + line = "glob equ $1234"; + assemblyLine(); + line = "?loc equ 7"; + assemblyLine(); + assert(getLabel("GLOB").value == 0x1234); + assert(getLabel("GLOB?LOC").value == 7); + assert(testValue("?loc") == 7); + assert(testValue("glob") == 0x1234); + } + } + void readComma() { if (readChar() != ',') throw new AssemblyError("Bad or missing function parameter"); @@ -646,19 +669,22 @@ private: operand ^= 0x80; } break; - case '^': + case '^': { + if (optionHardware == Hardware.NONE) + throw new AssemblyError("Hardware registers are disabled"); + immutable bool a5200 = optionHardware == Hardware.ATARI5200; switch (readChar()) { case '0': - operand = option5200 ? 0xc000 : 0xd000; + operand = a5200 ? 0xc000 : 0xd000; break; case '1': - operand = option5200 ? 0xc010 : 0xd010; + operand = a5200 ? 0xc010 : 0xd010; break; case '2': - operand = option5200 ? 0xe800 : 0xd200; + operand = a5200 ? 0xe800 : 0xd200; break; case '3': - if (option5200) + if (a5200) throw new AssemblyError("There's no PIA chip in Atari 5200"); operand = 0xd300; break; @@ -673,6 +699,7 @@ private: illegalCharacter(); operand += d; break; + } case '{': if (inOpcode) throw new AssemblyError("Nested opcodes not supported"); @@ -2221,21 +2248,83 @@ private: assert(0); } + E readEnumeratedOption(E)(string name) if (is(E == enum)) { + try { + return readIdentifier().to!E; + } catch (ConvException) { + throw new AssemblyError("Invalid value of option " ~ name); + } + } + + void setNamedOption(string name) { + switch (name) { + case "HARDWARE": + optionHardware = readEnumeratedOption!Hardware(name); + break; + case "LISTING": + optionListing = readEnumeratedOption!Flag(name) && pass2; + break; + case "OUTPUT": { + if (optionOutputSet) + throw new AssemblyError("OUTPUT already set"); + if (objectBuffer.data.length) + throw new AssemblyError("OUTPUT must be set before object data is emitted"); + Output output = readEnumeratedOption!Output(name); + optionFill = output == Output.ROM; + optionHeaders = output == Output.ATARIDOS; + optionObject = output != Output.OFF; + optionOutputSet = true; + break; + } + case "WARN_UNUSED_LABELS": + optionUnusedLabels = readEnumeratedOption!Flag(name); + break; + default: + throw new AssemblyError("Unknown option: " ~ name); + } + } + + void assemblyOptSettings() { + char delimiter = readChar(); + for (;;) { + string name = readIdentifier(); + if (readChar() != '=' || name.length == 0) + illegalCharacter(); + setNamedOption(name); + char c = readChar(); + if (c == delimiter) + return; + if (c != ',') + illegalCharacter(); + } + } + + void checkNotAfterOptOutput(char option) { + if (optionOutputSet) + throw new AssemblyError("Can't switch " ~ option ~ " once OUTPUT is set"); + } + void assemblyOpt() { directive(); readSpaces(); + if (!eol() && (line[column] == '\'' || line[column] == '"')) { + assemblyOptSettings(); + return; + } while (!eol()) { switch (line[column++]) { case 'F': case 'f': + checkNotAfterOptOutput('F'); optionFill = readOption(); break; case 'G': case 'g': - option5200 = readOption(); + optionHardware = readOption() ? Hardware.ATARI5200 : Hardware.ATARI800; break; case 'H': case 'h': + checkNotAfterOptOutput('H'); optionHeaders = readOption(); break; case 'L': @@ -2244,6 +2333,7 @@ private: break; case 'O': case 'o': + checkNotAfterOptOutput('O'); optionObject = readOption(); break; case 'U': @@ -2261,6 +2351,132 @@ private: } } + unittest { + auto a = testAssembler(); + with (a) { + pass2 = true; + + testInstruction("opt f+g+h-l-o-u-"); + assert(optionFill); + assert(optionHardware == Hardware.ATARI5200); + assert(!optionHeaders && !optionListing && !optionObject && !optionUnusedLabels); + testInstruction("opt f-g-h+l+o+u+"); + assert(!optionFill); + assert(optionHardware == Hardware.ATARI800); + assert(optionHeaders && optionListing && optionObject && optionUnusedLabels); + testInstruction("opt ?+"); + assert(testInstructionError("opt ?-") == "OPT ?- not supported"); + + testInstruction("opt 'hardware=atari5200,listing=off,warn_unused_labels=off'"); + assert(optionHardware == Hardware.ATARI5200); + assert(!optionListing && !optionUnusedLabels); + + testInstruction(`opt "Hardware=None,Listing=on,WARN_UNUSED_LABELS=on"`); + assert(optionHardware == Hardware.NONE); + assert(optionListing && optionUnusedLabels); + + assert(testInstructionError("opt '' nothing to set") == "Illegal character"); + + testInstruction("opt 'hardware=none,hardware=atari800,hardware=atari5200'"); + assert(optionHardware == Hardware.ATARI5200); + + testInstruction("opt 'hardware=atari800'"); + assert(testValue("^27") == 0xd207); + testInstruction("opt 'hardware=atari5200'"); + assert(testValue("^27") == 0xe807); + assert(testInstructionError("dta ^31") == "There's no PIA chip in Atari 5200"); + testInstruction("opt g-"); + assert(testValue("^27") == 0xd207); + testInstruction("opt 'hardware=none'"); + assert(testInstructionError("dta ^27") == "Hardware registers are disabled"); + testInstruction("opt g+"); + assert(optionHardware == Hardware.ATARI5200); + + assert(testInstructionError("opt 'listin='") == "Unknown option: LISTIN"); + assert(testInstructionError("opt 'listing=maybe'") == "Invalid value of option LISTING"); + assert(testInstructionError("opt 'listing='") == "Invalid value of option LISTING"); + assert(testInstructionError("opt 'hardware'") == "Illegal character"); + assert(testInstructionError("opt 'hardware=AMIGA1200'") == "Invalid value of option HARDWARE"); + assert(testInstructionError("opt 'listing,'") == "Illegal character"); + assert(testInstructionError("opt 'listing hardware'") == "Illegal character"); + assert(testInstructionError("opt ' '") == "Illegal character"); + assert(testInstructionError("opt '=on'") == "Illegal character"); + assert(testInstructionError("opt 'listing=on,'") == "Illegal character"); + assert(testInstructionError("opt 'listing=on,,warn_unused_labels=on'") == "Illegal character"); + assert(testInstructionError("opt 'fill, object'") == "Illegal character"); + assert(testInstructionError("opt 'fill ,object'") == "Illegal character"); + assert(testInstructionError("opt 'hardware = atari800'") == "Illegal character"); + assert(testInstructionError("opt 'hardware=atari800 '") == "Illegal character"); + assert(testInstructionError("opt 'listing") == "Unexpected end of line"); + assert(testInstructionError("opt 'listing=") == "Invalid value of option LISTING"); + assert(testInstructionError("opt '") == "Unexpected end of line"); + } + } + + unittest { + import std.exception : collectExceptionMsg; + + static struct OutputCase { + string setting; + bool headers; + bool object; + bool fill; + } + + foreach (c; [ + OutputCase("opt 'output=off'", false, false, false), + OutputCase("opt 'output=raw'", false, true, false), + OutputCase("opt 'output=rom'", false, true, true), + OutputCase(`opt "Output=AtariDOS"`, true, true, false) + ]) { + foreach (bool initially; [false, true]) { + auto a = testAssembler(); + with (a) { + optionHeaders = optionObject = optionFill = initially; + testInstruction(c.setting); + assert(optionHeaders == c.headers); + assert(optionObject == c.object); + assert(optionFill == c.fill); + assert(testInstructionError(c.setting) == "OUTPUT already set"); + assert(testInstructionError("opt f+") == "Can't switch F once OUTPUT is set"); + assert(testInstructionError("opt h+") == "Can't switch H once OUTPUT is set"); + assert(testInstructionError("opt o-") == "Can't switch O once OUTPUT is set"); + assert(optionHeaders == c.headers); + assert(optionObject == c.object); + assert(optionFill == c.fill); + } + } + } + + auto a = testAssembler(); + with (a) { + assert(testInstructionError("opt 'output=xex'") == "Invalid value of option OUTPUT"); + assert(testInstructionError("opt 'output='") == "Invalid value of option OUTPUT"); + assert(testInstructionError("opt 'output") == "Unexpected end of line"); + assert(testInstructionError("opt 'output=off,output=raw'") == "OUTPUT already set"); + assert(!optionHeaders && !optionObject && !optionFill); + testInstruction("opt g+u-"); + assert(optionHardware == Hardware.ATARI5200 && !optionUnusedLabels); + } + + a = testAssembler(); + with (a) { + testInstruction("opt f+h-o+"); + assert(optionFill && !optionHeaders && optionObject); + testInstruction("opt 'output=ataridos'"); + assert(!optionFill && optionHeaders && optionObject); + } + + a = testAssembler(); + with (a) { + testInstruction("nop"); + line = "opt 'output=raw'"; + column = 0; + assert(collectExceptionMsg(assemblyInstruction(readInstruction())) + == "OUTPUT must be set before object data is emitted"); + } + } + void originWord(ushort value, char listingChar) { objectWord(value); listWord(value); @@ -2735,6 +2951,15 @@ private: return objectBuffer.data; } + version (unittest) string testInstructionError(string l) { + try { + testInstruction(l); + } catch (AssemblyError e) { + return e.msg; + } + return null; + } + unittest { auto a = testAssembler(); with (a) { @@ -2945,11 +3170,12 @@ private: blockIndex = -1; newBlock = false; optionFill = false; - option5200 = false; + optionHardware = Hardware.ATARI800; optionHeaders = true; optionListing = pass2; optionObject = true; optionUnusedLabels = true; + optionOutputSet = false; willSkip = false; skipping = false; repeatOffset = 0; diff --git a/xasm.1.asciidoc b/xasm.1.asciidoc index 67be8c0..62d0386 100644 --- a/xasm.1.asciidoc +++ b/xasm.1.asciidoc @@ -214,6 +214,10 @@ is the location of hardware registers. | ^4x |ANTIC|$D40x|`$D40x` |================================================ +Atari 5200 mode is enabled with `opt g+` or `opt 'hardware=atari5200'`. +The abbreviations can also be rejected altogether with `opt 'hardware=none'`, +which is useful if the code targets neither of these machines. + The opcode syntax represents the opcode byte of the instruction inside braces. The operand of the instruction is discarded and is needed only to recognize the addressing mode. The instruction should begin right after the left brace @@ -322,7 +326,9 @@ here equ * [[new_opt]]*OPT* - set assembler options:: -Six options are available: +The operand is either a sequence of single-letter switches +or <>. +Six switches are available: - `F` - fill the space between noncontiguous memory areas with `$FF` bytes - `G` - Atari 5200 mode for hardware register abbreviations (`^xx`) @@ -346,6 +352,41 @@ Examples: ------------------------------------------------------------------------------ -- ++ +-- +[[new_optnames]]Alternatively, the operand is a string in single or double quotes, +holding a comma-separated list of settings, each written as `name=value`: + +------------------------------------------------------------------------------ + opt 'listing=off' + opt 'output=rom,hardware=atari5200' + opt "WARN_UNUSED_LABELS=Off" +------------------------------------------------------------------------------ + +The following settings are available: + +- `hardware=atari800` - `^xx` are the hardware registers of Atari 8-bit computers + (the default, same as `opt g-`) +- `hardware=atari5200` - `^xx` are the hardware registers of the Atari 5200 + (same as `opt g+`) +- `hardware=none` - `^xx` is rejected +- `listing=on` / `listing=off` - same as `opt l+` / `opt l-` +- `warn_unused_labels=on` / `warn_unused_labels=off` - same as `opt u+` / `opt u-` +- `output=ataridos` - write an Atari DOS executable file + (the default, same as `opt f-h+o+`) +- `output=raw` - write the bytes with no headers (same as `opt f-h-o+`) +- `output=rom` - as above, but fill the space between noncontiguous memory areas + with `$FF` bytes, which is what cartridge images need (same as `opt f+h-o+`) +- `output=off` - write no object file (same as `opt f-h-o-`) + +Names and values are case-insensitive. +No spaces are allowed inside the string. + +`output` may be set only once in the source, and only before the first byte +of the object file is emitted. Once `output` is set, the `F`, `H` and `O` +switches are rejected. +-- + *ORG* - set the origin counter:: If Atari executable headers are enabled (`opt h+`), you can include an operand prefix: @@ -778,6 +819,9 @@ Version 3.3.0 (2026-10-??) with no emitted output (https://github.com/pfusik/xasm/issues/27[reported] by Sebastian Igielski) - fixed corrupted executable headers with `OPT O-` (reported by Tomasz Biela) +- <>, + for example `opt 'output=rom,hardware=atari5200'`, including the new + `output` format selection and `hardware=none` (by Adrian Matoga) - *xasm* can now be used as a link:libxasm.html[library] in any D program (by Adrian Matoga) - fixed writing the */t* label table: it no longer closes standard output