diff --git a/source/xasm/package.d b/source/xasm/package.d index 329eaa0..e4d508f 100644 --- a/source/xasm/package.d +++ b/source/xasm/package.d @@ -140,10 +140,12 @@ class Assembler { private: version (unittest) static Assembler testAssembler() { - return new Assembler( + auto a = new Assembler( (string path) => (immutable(ubyte)[]).init, (string path, int offset, int length) => (immutable(ubyte)[]).init, null); + a.optionObject = true; + return a; } string sourceFilename = null; @@ -153,12 +155,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; // opt o bool optionUnusedLabels; // opt u + bool optionOutputSet; string currentFilename; int lineNo; @@ -349,7 +356,7 @@ private: } } - string readLabel() { + string readIdentifier(bool nonEmpty = false) { int firstColumn = column; while (!eol()) { char c = line[column++]; @@ -361,7 +368,14 @@ private: column--; break; } - string label = line[firstColumn .. column].toUpper; + auto result = line[firstColumn .. column].toUpper; + if (nonEmpty && result.length == 0) + throw new AssemblyError(eol() ? "Unexpected end of line" : "Illegal character"); + return result; + } + + string readLabel() { + string label = readIdentifier(); if (label.startsWith('?')) { if (lastGlobalLabel is null) throw new AssemblyError("Global label must be declared first"); @@ -370,6 +384,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"); @@ -645,19 +673,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; @@ -672,6 +703,7 @@ private: illegalCharacter(); operand += d; break; + } case '{': if (inOpcode) throw new AssemblyError("Nested opcodes not supported"); @@ -1233,8 +1265,8 @@ private: void objectByte(ubyte b) { version (unittest) {} else { assert(pass2); - if (!optionObject) return; } + if (!optionObject) return; objectBuffer.put(b); } @@ -2205,21 +2237,90 @@ private: assert(0); } + E readEnumeratedOption(E)(string name) if (is(E == enum)) { + switch (readIdentifier(true)) { + static foreach (string v; __traits(allMembers, E)) { + case v: + return __traits(getMember, E, v); + } + default: + 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(); + if (!eol() && line[column] == delimiter) { + column++; + return; + } + for (;;) { + string name = readIdentifier(true); + if (readChar() != '=') + 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': @@ -2228,6 +2329,7 @@ private: break; case 'O': case 'o': + checkNotAfterOptOutput('O'); optionObject = readOption(); break; case 'U': @@ -2245,6 +2347,127 @@ 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); + + testInstruction("opt '' nothing to set"); + checkNoExtraCharacters(); + assert(optionHardware == Hardware.NONE); + + 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='") == "Illegal character"); + 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,,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=") == "Unexpected end of line"); + assert(testInstructionError("opt '") == "Unexpected end of line"); + } + } + + unittest { + static struct OutputCase { + string setting; + bool headers; + bool object; + bool fill; + + void checkFlags(Assembler a) { + assert(a.optionHeaders == headers); + assert(a.optionObject == object); + assert(a.optionFill == 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); + c.checkFlags(a); + 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"); + c.checkFlags(a); + } + } + } + + auto a = testAssembler(); + with (a) { + assert(testInstructionError("opt 'output=xex'") == "Invalid value of option OUTPUT"); + assert(testInstructionError("opt 'output='") == "Illegal character"); + 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); + } + + assert(testError(" opt h-\n nop\n opt 'output=raw'") == "OUTPUT must be set before object data is emitted"); + } + void originWord(ushort value, char listingChar) { objectWord(value); listWord(value); @@ -2715,16 +2938,25 @@ 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) { - assert(testInstruction("nop") == representation(hexString!"ea")); - assert(testInstruction("add (5,0)") == representation(hexString!"18a2006105")); - assert(testInstruction("mwa #$abcd $1234") == representation(hexString!"a9cd8d3412a9ab8d3512")); - assert(testInstruction("mwx #-256 $80") == representation(hexString!"a2008680ca8681")); - assert(testInstruction("dta 5,d'Foo'*,a($4589),e($123456),f($12345678)") == representation(hexString!"05a6efef894556341278563412")); + assert(testInstruction("nop") == hexData!"ea"); + assert(testInstruction("add (5,0)") == hexData!"18a2006105"); + assert(testInstruction("mwa #$abcd $1234") == hexData!"a9cd8d3412a9ab8d3512"); + assert(testInstruction("mwx #-256 $80") == hexData!"a2008680ca8681"); + assert(testInstruction("dta 5,d'Foo'*,a($4589),e($123456),f($12345678)") == hexData!"05a6efef894556341278563412"); assert(testInstruction("dta r(1,12,123,1234567890,12345678900000,.5,.03,000.1664534589,1e97)") - == representation(hexString!"400100000000 401200000000 410123000000 441234567890 461234567890 3f5000000000 3f0300000000 3f1664534589 701000000000")); + == hexData!"400100000000 401200000000 410123000000 441234567890 461234567890 3f5000000000 3f0300000000 3f1664534589 701000000000"); } } @@ -2906,16 +3138,7 @@ private: } unittest { - auto a = testAssembler(); - with (a) { - sourceFiles[""] = " lda:sne:ldy:inx $1234".representation; - assemblyFile(""); - pass2 = true; - objectBuffer.clear(); - assemblyFile(""); - writefln!"%(%02x%)"(objectBuffer.data); - assert(objectBuffer.data == [0xad, 0x34, 0x12, 0xd0, 0x03, 0xac, 0x34, 0x12, 0xe8]); - } + assert(testObject(" opt h-\n lda:sne:ldy:inx $1234") == hexData!"ad3412d003ac3412e8"); } void assemblyPass() { @@ -2924,11 +3147,12 @@ private: loadingOrigin = -1; blockIndex = -1; optionFill = false; - option5200 = false; + optionHardware = Hardware.ATARI800; optionHeaders = true; optionListing = pass2; optionObject = true; optionUnusedLabels = true; + optionOutputSet = false; willSkip = false; skipping = false; repeatOffset = 0; @@ -2972,26 +3196,58 @@ unittest { assert(!assembler.getLabel("foo")); } +version (unittest) private { + +enum hexData(string hex) = representation(hexString!hex); + +struct TestAssembly { + const(ubyte)[] object; + string[] listing; + string error; +} + +TestAssembly testAssemble(string[string] sources, string main, string[] commandLineDefinitions = null, bool listIncludedFiles = true) { + TestAssembly r; + auto assembler = new Assembler( + (string path) => sources[path].representation, + null, + (in Diagnostic diag) { if (diag.severity == Severity.error) r.error = diag.message; }); + assembler.commandLineDefinitions = commandLineDefinitions; + assembler.listIncludedFiles = listIncludedFiles; + assembler.listingSink = (const(char)[] line) { r.listing ~= line.idup; }; + assembler.assemble(main); + r.object = assembler.object; + return r; +} + +TestAssembly testAssemble(string source) { + return testAssemble(["": source], ""); +} + +const(ubyte)[] testObject(string source) { + auto r = testAssemble(source); + assert(r.error is null, r.error); + return r.object; +} + +string testError(string source) { + return testAssemble(source).error; +} + +} + // listIncludedFiles unittest { - import std.functional : toDelegate; - string[string] sources = [ "main.asx": " org $600\n icl 'inc.asx'\n dta $42\n", "inc.asx": " dta $37\n" ]; string[] listing(bool listIncludedFiles) { - auto assembler = new Assembler( - (string path) => sources[path].representation, - null, - toDelegate((in Diagnostic diag) => stderr.writeln(diag))); - string[] lines; - assembler.listIncludedFiles = listIncludedFiles; - assembler.listingSink = (const(char)[] line) { lines ~= line.idup; }; - assembler.assemble("main.asx"); - assert(assembler.object == [0xff, 0xff, 0x00, 0x06, 0x01, 0x06, 0x37, 0x42]); - return lines; + auto r = testAssemble(sources, "main.asx", null, listIncludedFiles); + assert(r.error is null, r.error); + assert(r.object == hexData!"ffff00060106 3742"); + return r.listing; } auto listed = listing(true); @@ -3006,4 +3262,71 @@ unittest { assert(!notListed.canFind("Source: inc.asx")); assert(!notListed.canFind!(l => l.canFind("dta $37"))); assert(notListed.canFind!(l => l.canFind("dta $42"))); +} + +// object file layout: headers, filling, run/ini, opt o +unittest { + // opt h, opt f switched mid-file + assert(testObject(" org $600\n dta 1\n opt h-\n org $700\n dta 2\n opt h+\n org $800\n dta 3") + == hexData!"ffff00060006 01 02 00080008 03"); + assert(testObject(" opt f+\n org $600\n dta 1\n org $602\n dta 2") + == hexData!"ffff00060206 01ff02"); + assert(testObject(" opt h-f+\n org $600\n dta 1\n org $602\n dta 2\n opt f-\n org $606\n dta 3\n opt f+\n org $608\n dta 4") + == hexData!"01ff02 03ff04"); + + // a:, f: + assert(testObject(" org $600\n dta 1\n org a:$601\n dta 2\n org f:$602\n dta 3") + == hexData!"ffff00060006 01 01060106 02 ffff02060206 03"); + assert(testObject(" org $600\n org $700\n org $800\n dta 1") == hexData!"ffff00080008 01"); + assert(testError(" org $600\n org a:$700\n org $800\n dta 1") == "Cannot generate an empty block"); + foreach (directive; ["org a:", "org f:", "run ", "ini "]) + assert(testError(" opt h-\n " ~ directive ~ "$600") == "Illegal when Atari file headers disabled"); + + // run, ini + assert(testObject(" org $600\n dta 1\n run $600\n ini $601\n ini f:$602") + == hexData!"ffff00060006 01 e002e102 0006 e202e302 0106 ffffe202e302 0206"); + assert(testObject(" opt f+\n org $600\n dta 1\n run $600\n dta 2") + == hexData!"ffff00060006 01 e002e202 0006 02"); + + // aln + assert(testObject(" org $601\n dta 1\n aln 4\n dta 2") + == hexData!"ffff01060106 01 04060406 02"); + assert(testObject(" opt f+\n org $601\n dta 1\n aln 4\n dta 2") + == hexData!"ffff01060406 01ffff02"); + assert(testObject(" opt h-f+\n org $601\n dta 1\n aln 4\n dta 2") == hexData!"01ffff02"); + + // errors + assert(testError(" opt f+\n org $600\n dta 1\n org $5ff\n dta 2") == "Can't fill from higher to lower memory location"); + assert(testError(" dta 1") == "No ORG specified"); + assert(testObject(" opt h-\n dta 1") == hexData!"01"); + + // header words in the listing + auto r = testAssemble(" org $600\n dta 1\n org a:$601\n dta 2\n run $600\n ini f:$602"); + assert(r.listing[0] == " 1 FFFF> 0600-0600> org $600"); + assert(r.listing[2] == " 3 0601 0601-0601> org a:$601"); + assert(r.listing[4] == " 5 0602 02E0-02E1> 00 06 run $600"); + assert(r.listing[5] == " 6 02E2 FFFF> 02E2-02E3> 0+ ini f:$602"); + + // opt o in the middle of a block + r = testAssemble(" org $600\n dta 1\n opt o-\n dta 2\n opt o+\n dta 3"); + assert(r.object == hexData!"ffff00060206 01 03"); + assert(r.listing[3] == " 4 0601 02 dta 2"); + // between blocks + r = testAssemble(" org $600\n dta 1\n opt o-\n org $700\n dta 2\n opt o+\n org $800\n dta 3"); + assert(r.object == hexData!"ffff00060006 01 00080008 03"); + assert(r.listing[3] == " 4 0601 0700-0700> org $700"); + // contiguous continuation after a dropped block + assert(testObject(" org $600\n dta 1\n opt o-\n org $700\n dta 2\n opt o+\n dta 3") + == hexData!"ffff00060006 01 03"); + // the first block dropped + assert(testObject(" opt o-\n org $600\n dta 1\n opt o+\n org $700\n dta 2") + == hexData!"ffff00070007 02"); + // in the middle of a block with fill + assert(testObject(" opt h-f+\n org $600\n dta 1\n opt o-\n dta 2\n opt o+\n dta 3") == hexData!"0103"); + assert(testObject(" opt h-f+\n org $600\n dta 1\n opt o-\n org $602\n dta 2\n opt o+\n dta 3") == hexData!"0103"); + assert(testObject(" opt h-f+\n org $600\n dta 1\n opt o-\n org $602\n opt o+\n dta 3") == hexData!"01ff03"); + assert(testObject(" opt f+\n org $600\n dta 1\n opt o-\n org $602\n dta 2\n opt o+\n dta 3") + == hexData!"ffff00060306 01 03"); + // accepted, as no object byte has been written + assert(testObject(" opt o-\n org $600\n dta 1\n opt 'output=raw'") == []); } \ No newline at end of file diff --git a/xasm.1.asciidoc b/xasm.1.asciidoc index 9387c14..10f319c 100644 --- a/xasm.1.asciidoc +++ b/xasm.1.asciidoc @@ -213,6 +213,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 @@ -294,7 +298,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`) @@ -318,6 +324,43 @@ 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 "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-` +- `unused_labels=on` / `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. +The empty string (`opt ''`) sets nothing. +An unknown name or an invalid value is an error. + +`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: @@ -742,6 +785,9 @@ Version 3.3.0 (2026-09-??) - avoid misinterpreting certain non-ASCII characters as Atari $9b EOL, by detecting EOL in the first line of each source file (https://github.com/pfusik/xasm/issues/21[reported] by Sebastian Igielski) +- <>, + 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