diff --git a/examples/count.asm b/examples/count.asm
new file mode 100644
index 0000000..b6823a5
--- /dev/null
+++ b/examples/count.asm
@@ -0,0 +1,18 @@
+; count.asm - prints "0123456789" then a newline and halts.
+; Exercises ADC, CMP, branches and a loop.
+
+ .export start
+
+start:
+ lda #'0'
+loop:
+ sta $F001
+ clc
+ adc #$01
+ cmp #$3A ; '9' + 1
+ bne loop
+ lda #$0A ; newline
+ sta $F001
+ lda #$00
+ sta $F003
+ rts
diff --git a/examples/hello.asm b/examples/hello.asm
new file mode 100644
index 0000000..94a3a1f
--- /dev/null
+++ b/examples/hello.asm
@@ -0,0 +1,24 @@
+; hello.asm - single-object demo.
+; Prints "Hello, World!" through the memory-mapped terminal at $F001,
+; then writes to the halt register at $F003.
+
+ .export start
+ .org $0600
+
+ jmp start
+
+msg:
+ .asciiz "Hello, World!"
+
+start:
+ ldx #0
+loop:
+ lda msg,x
+ beq done
+ sta $F001
+ inx
+ jmp loop
+done:
+ lda #$00
+ sta $F003
+ rts
diff --git a/examples/main.asm b/examples/main.asm
new file mode 100644
index 0000000..10e8fbd
--- /dev/null
+++ b/examples/main.asm
@@ -0,0 +1,23 @@
+; main.asm - cross-object demo, part 2.
+; Imports `putc` from putc.wo and prints "Hello, 6502!".
+
+ .export start
+ .import putc
+
+ jmp start
+
+msg:
+ .asciiz "Hello, 6502!"
+
+start:
+ ldx #0
+loop:
+ lda msg,x
+ beq done
+ jsr putc
+ inx
+ jmp loop
+done:
+ lda #$00
+ sta $F003
+ rts
diff --git a/examples/putc.asm b/examples/putc.asm
new file mode 100644
index 0000000..40261f4
--- /dev/null
+++ b/examples/putc.asm
@@ -0,0 +1,9 @@
+; putc.asm - cross-object demo, part 1.
+; Exports `putc`: write the byte in A to the terminal and return.
+; This object is relocatable; the linker places it at the load address.
+
+ .export putc
+
+putc:
+ sta $F001
+ rts
diff --git a/src/assembler/assembler.d b/src/assembler/assembler.d
new file mode 100644
index 0000000..3f1b610
--- /dev/null
+++ b/src/assembler/assembler.d
@@ -0,0 +1,1616 @@
+module assembler.assembler;
+
+import std.array : split;
+import std.ascii : isHexDigit;
+import std.conv : to;
+import std.file : readText;
+import std.format : format;
+import std.string : strip, toLower;
+import wcore.objfmt;
+alias Object = wcore.objfmt.Object;
+
+// ---------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------
+
+/// Assemble `source` into a wcore.objfmt.Object. `filename` is used only for
+/// error messages. Errors are thrown as Exception with "file:line: message".
+Object assembleSource(string source, string filename = "")
+{
+ auto a = Asm(filename);
+ return a.assemble(source);
+}
+
+/// Read and assemble a source file.
+Object assembleFile(string path)
+{
+ string src;
+ try
+ src = readText(path);
+ catch (Exception e)
+ throw new Exception(format("%s: %s", path, e.msg));
+ return assembleSource(src, path);
+}
+
+// ---------------------------------------------------------------------------
+// Internal types
+// ---------------------------------------------------------------------------
+
+private enum AddrMode : ubyte
+{
+ imp,
+ acc,
+ imm,
+ zp,
+ zpx,
+ zpy,
+ abs,
+ absx,
+ absy,
+ ind,
+ izx,
+ izy,
+ rel,
+}
+
+private enum OpForm : ubyte
+{
+ imp,
+ acc,
+ imm,
+ direct,
+ izx,
+ izy,
+ ind,
+}
+
+private enum ubyte SEG_CODE = 0x07; // alloc|exec|write
+private enum ubyte SEG_DATA = 0x05; // alloc|write
+private enum ubyte SEG_BSS = 0x0D; // alloc|write|bss
+
+private struct Expr
+{
+ bool isNum;
+ int num;
+ string sym;
+ int offset;
+ bool low;
+ bool high;
+}
+
+private struct DataItem
+{
+ bool isStr;
+ ubyte[] bytes;
+ Expr expr;
+}
+
+private struct Line
+{
+ int lineNo;
+ string text;
+ string label;
+ bool labelOnly;
+ bool isEquate;
+ string eqName;
+ Expr expr; // equate value / .org value / instruction operand
+ bool isDirective;
+ string op; // lowercased mnemonic or directive (with leading dot)
+ OpForm form;
+ bool indexX;
+ bool indexY;
+ Expr[] exprs; // .word list, .res [count, fill]
+ DataItem[] items; // .byte list
+ ubyte[] strBytes; // .ascii / .asciiz
+ bool hasFill;
+ Expr exprFill;
+ string[] names; // .export / .import / .zp
+ string entrySym; // .entry
+ AddrMode mode;
+ bool modeSet;
+}
+
+private struct SymEntry
+{
+ string name;
+ int index = -1;
+ bool imported;
+ bool defined;
+ bool global;
+ bool zeropage;
+ bool valueKnown;
+ ushort value;
+ int seg = -1;
+}
+
+private struct SegState
+{
+ string name;
+ ubyte flags;
+ ushort origin;
+ size_t len;
+ ubyte[] data;
+}
+
+// ---------------------------------------------------------------------------
+// Opcode table
+// ---------------------------------------------------------------------------
+
+private struct OpDef
+{
+ string m;
+ AddrMode a;
+ ubyte c;
+}
+
+private __gshared ubyte[AddrMode][string] gOpTable;
+
+private void add(string m, AddrMode a, ubyte c)
+{
+ gOpTable[m][a] = c;
+}
+
+static this()
+{
+ // ADC
+ add("adc", AddrMode.imm, 0x69); add("adc", AddrMode.zp, 0x65);
+ add("adc", AddrMode.zpx, 0x75); add("adc", AddrMode.abs, 0x6D);
+ add("adc", AddrMode.absx, 0x7D); add("adc", AddrMode.absy, 0x79);
+ add("adc", AddrMode.izx, 0x61); add("adc", AddrMode.izy, 0x71);
+ // AND
+ add("and", AddrMode.imm, 0x29); add("and", AddrMode.zp, 0x25);
+ add("and", AddrMode.zpx, 0x35); add("and", AddrMode.abs, 0x2D);
+ add("and", AddrMode.absx, 0x3D); add("and", AddrMode.absy, 0x39);
+ add("and", AddrMode.izx, 0x21); add("and", AddrMode.izy, 0x31);
+ // ASL
+ add("asl", AddrMode.acc, 0x0A); add("asl", AddrMode.zp, 0x06);
+ add("asl", AddrMode.zpx, 0x16); add("asl", AddrMode.abs, 0x0E);
+ add("asl", AddrMode.absx, 0x1E);
+ // Branches
+ add("bcc", AddrMode.rel, 0x90); add("bcs", AddrMode.rel, 0xB0);
+ add("beq", AddrMode.rel, 0xF0); add("bmi", AddrMode.rel, 0x30);
+ add("bne", AddrMode.rel, 0xD0); add("bpl", AddrMode.rel, 0x10);
+ add("bvc", AddrMode.rel, 0x50); add("bvs", AddrMode.rel, 0x70);
+ // BIT
+ add("bit", AddrMode.zp, 0x24); add("bit", AddrMode.abs, 0x2C);
+ // BRK
+ add("brk", AddrMode.imp, 0x00);
+ // Flags
+ add("clc", AddrMode.imp, 0x18); add("cld", AddrMode.imp, 0xD8);
+ add("cli", AddrMode.imp, 0x58); add("clv", AddrMode.imp, 0xB8);
+ // CMP
+ add("cmp", AddrMode.imm, 0xC9); add("cmp", AddrMode.zp, 0xC5);
+ add("cmp", AddrMode.zpx, 0xD5); add("cmp", AddrMode.abs, 0xCD);
+ add("cmp", AddrMode.absx, 0xDD); add("cmp", AddrMode.absy, 0xD9);
+ add("cmp", AddrMode.izx, 0xC1); add("cmp", AddrMode.izy, 0xD1);
+ // CPX / CPY
+ add("cpx", AddrMode.imm, 0xE0); add("cpx", AddrMode.zp, 0xE4);
+ add("cpx", AddrMode.abs, 0xEC);
+ add("cpy", AddrMode.imm, 0xC0); add("cpy", AddrMode.zp, 0xC4);
+ add("cpy", AddrMode.abs, 0xCC);
+ // DEC / DEX / DEY
+ add("dec", AddrMode.zp, 0xC6); add("dec", AddrMode.zpx, 0xD6);
+ add("dec", AddrMode.abs, 0xCE); add("dec", AddrMode.absx, 0xDE);
+ add("dex", AddrMode.imp, 0xCA); add("dey", AddrMode.imp, 0x88);
+ // EOR
+ add("eor", AddrMode.imm, 0x49); add("eor", AddrMode.zp, 0x45);
+ add("eor", AddrMode.zpx, 0x55); add("eor", AddrMode.abs, 0x4D);
+ add("eor", AddrMode.absx, 0x5D); add("eor", AddrMode.absy, 0x59);
+ add("eor", AddrMode.izx, 0x41); add("eor", AddrMode.izy, 0x51);
+ // INC / INX / INY
+ add("inc", AddrMode.zp, 0xE6); add("inc", AddrMode.zpx, 0xF6);
+ add("inc", AddrMode.abs, 0xEE); add("inc", AddrMode.absx, 0xFE);
+ add("inx", AddrMode.imp, 0xE8); add("iny", AddrMode.imp, 0xC8);
+ // JMP / JSR
+ add("jmp", AddrMode.abs, 0x4C); add("jmp", AddrMode.ind, 0x6C);
+ add("jsr", AddrMode.abs, 0x20);
+ // LDA
+ add("lda", AddrMode.imm, 0xA9); add("lda", AddrMode.zp, 0xA5);
+ add("lda", AddrMode.zpx, 0xB5); add("lda", AddrMode.abs, 0xAD);
+ add("lda", AddrMode.absx, 0xBD); add("lda", AddrMode.absy, 0xB9);
+ add("lda", AddrMode.izx, 0xA1); add("lda", AddrMode.izy, 0xB1);
+ // LDX / LDY
+ add("ldx", AddrMode.imm, 0xA2); add("ldx", AddrMode.zp, 0xA6);
+ add("ldx", AddrMode.zpy, 0xB6); add("ldx", AddrMode.abs, 0xAE);
+ add("ldx", AddrMode.absy, 0xBE);
+ add("ldy", AddrMode.imm, 0xA0); add("ldy", AddrMode.zp, 0xA4);
+ add("ldy", AddrMode.zpx, 0xB4); add("ldy", AddrMode.abs, 0xAC);
+ add("ldy", AddrMode.absx, 0xBC);
+ // LSR
+ add("lsr", AddrMode.acc, 0x4A); add("lsr", AddrMode.zp, 0x46);
+ add("lsr", AddrMode.zpx, 0x56); add("lsr", AddrMode.abs, 0x4E);
+ add("lsr", AddrMode.absx, 0x5E);
+ // NOP
+ add("nop", AddrMode.imp, 0xEA);
+ // ORA
+ add("ora", AddrMode.imm, 0x09); add("ora", AddrMode.zp, 0x05);
+ add("ora", AddrMode.zpx, 0x15); add("ora", AddrMode.abs, 0x0D);
+ add("ora", AddrMode.absx, 0x1D); add("ora", AddrMode.absy, 0x19);
+ add("ora", AddrMode.izx, 0x01); add("ora", AddrMode.izy, 0x11);
+ // Stack
+ add("pha", AddrMode.imp, 0x48); add("php", AddrMode.imp, 0x08);
+ add("pla", AddrMode.imp, 0x68); add("plp", AddrMode.imp, 0x28);
+ // ROL / ROR
+ add("rol", AddrMode.acc, 0x2A); add("rol", AddrMode.zp, 0x26);
+ add("rol", AddrMode.zpx, 0x36); add("rol", AddrMode.abs, 0x2E);
+ add("rol", AddrMode.absx, 0x3E);
+ add("ror", AddrMode.acc, 0x6A); add("ror", AddrMode.zp, 0x66);
+ add("ror", AddrMode.zpx, 0x76); add("ror", AddrMode.abs, 0x6E);
+ add("ror", AddrMode.absx, 0x7E);
+ // RTI / RTS
+ add("rti", AddrMode.imp, 0x40); add("rts", AddrMode.imp, 0x60);
+ // SBC
+ add("sbc", AddrMode.imm, 0xE9); add("sbc", AddrMode.zp, 0xE5);
+ add("sbc", AddrMode.zpx, 0xF5); add("sbc", AddrMode.abs, 0xED);
+ add("sbc", AddrMode.absx, 0xFD); add("sbc", AddrMode.absy, 0xF9);
+ add("sbc", AddrMode.izx, 0xE1); add("sbc", AddrMode.izy, 0xF1);
+ // Flags set
+ add("sec", AddrMode.imp, 0x38); add("sed", AddrMode.imp, 0xF8);
+ add("sei", AddrMode.imp, 0x78);
+ // STA
+ add("sta", AddrMode.zp, 0x85); add("sta", AddrMode.zpx, 0x95);
+ add("sta", AddrMode.abs, 0x8D); add("sta", AddrMode.absx, 0x9D);
+ add("sta", AddrMode.absy, 0x99); add("sta", AddrMode.izx, 0x81);
+ add("sta", AddrMode.izy, 0x91);
+ // STX / STY
+ add("stx", AddrMode.zp, 0x86); add("stx", AddrMode.zpy, 0x96);
+ add("stx", AddrMode.abs, 0x8E);
+ add("sty", AddrMode.zp, 0x84); add("sty", AddrMode.zpx, 0x94);
+ add("sty", AddrMode.abs, 0x8C);
+ // Transfers
+ add("tax", AddrMode.imp, 0xAA); add("tay", AddrMode.imp, 0xA8);
+ add("tsx", AddrMode.imp, 0xBA); add("txa", AddrMode.imp, 0x8A);
+ add("txs", AddrMode.imp, 0x9A); add("tya", AddrMode.imp, 0x98);
+}
+
+// ---------------------------------------------------------------------------
+// Character helpers
+// ---------------------------------------------------------------------------
+
+private bool isIdentStart(char c)
+{
+ return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_';
+}
+
+private bool isIdentChar(char c)
+{
+ return isIdentStart(c) || (c >= '0' && c <= '9');
+}
+
+private int hexVal(char c)
+{
+ if (c >= '0' && c <= '9') return c - '0';
+ if (c >= 'a' && c <= 'f') return c - 'a' + 10;
+ if (c >= 'A' && c <= 'F') return c - 'A' + 10;
+ return 0;
+}
+
+private long parseHex(string s)
+{
+ long v = 0;
+ foreach (c; s) v = v * 16 + hexVal(c);
+ return v;
+}
+
+private void skipWs(string s, ref size_t pos)
+{
+ while (pos < s.length && (s[pos] == ' ' || s[pos] == '\t')) pos++;
+}
+
+private string stripComment(string s)
+{
+ bool inS = false, inD = false;
+ for (size_t i = 0; i < s.length; i++)
+ {
+ char c = s[i];
+ if (inS)
+ {
+ if (c == '\\') i++;
+ else if (c == '\'') inS = false;
+ continue;
+ }
+ if (inD)
+ {
+ if (c == '\\') i++;
+ else if (c == '"') inD = false;
+ continue;
+ }
+ if (c == '\'') inS = true;
+ else if (c == '"') inD = true;
+ else if (c == ';') return s[0 .. i];
+ }
+ return s;
+}
+
+private int findTopLevelComma(string s)
+{
+ int depth = 0;
+ bool inS = false, inD = false;
+ for (size_t i = 0; i < s.length; i++)
+ {
+ char c = s[i];
+ if (inS)
+ {
+ if (c == '\\') i++;
+ else if (c == '\'') inS = false;
+ continue;
+ }
+ if (inD)
+ {
+ if (c == '\\') i++;
+ else if (c == '"') inD = false;
+ continue;
+ }
+ if (c == '\'') inS = true;
+ else if (c == '"') inD = true;
+ else if (c == '(') depth++;
+ else if (c == ')') depth--;
+ else if (c == ',' && depth == 0) return cast(int)i;
+ }
+ return -1;
+}
+
+private int matchParen(string s, size_t start)
+{
+ int depth = 0;
+ bool inS = false, inD = false;
+ for (size_t i = start; i < s.length; i++)
+ {
+ char c = s[i];
+ if (inS)
+ {
+ if (c == '\\') i++;
+ else if (c == '\'') inS = false;
+ continue;
+ }
+ if (inD)
+ {
+ if (c == '\\') i++;
+ else if (c == '"') inD = false;
+ continue;
+ }
+ if (c == '\'') inS = true;
+ else if (c == '"') inD = true;
+ else if (c == '(') depth++;
+ else if (c == ')')
+ {
+ depth--;
+ if (depth == 0) return cast(int)i;
+ }
+ }
+ return -1;
+}
+
+private string[] splitCommas(string s)
+{
+ string[] buf;
+ size_t start = 0;
+ int depth = 0;
+ bool inS = false, inD = false;
+ for (size_t i = 0; i < s.length; i++)
+ {
+ char c = s[i];
+ if (inS)
+ {
+ if (c == '\\') i++;
+ else if (c == '\'') inS = false;
+ continue;
+ }
+ if (inD)
+ {
+ if (c == '\\') i++;
+ else if (c == '"') inD = false;
+ continue;
+ }
+ if (c == '\'') inS = true;
+ else if (c == '"') inD = true;
+ else if (c == '(') depth++;
+ else if (c == ')') depth--;
+ else if (c == ',' && depth == 0)
+ {
+ buf ~= s[start .. i];
+ start = i + 1;
+ }
+ }
+ buf ~= s[start .. $];
+ return buf;
+}
+
+// ---------------------------------------------------------------------------
+// The assembler
+// ---------------------------------------------------------------------------
+
+private struct Asm
+{
+ string filename;
+ Line[] lines;
+ SymEntry[string] syms;
+ string[] defOrder;
+ string[] impOrder;
+ long[string] equates;
+ string entryName;
+ SegState[] segs;
+ int activeSeg;
+ int[string] segIndex;
+ Reloc[] relocs;
+
+ void err(int lineNo, string msg)
+ {
+ throw new Exception(format("%s:%d: %s", filename, lineNo, msg));
+ }
+
+ // ---- driver ----------------------------------------------------------
+
+ Object assemble(string source)
+ {
+ parseLines(source);
+ prepass();
+ assignIndices();
+ sizing();
+ emit();
+ return buildObject();
+ }
+
+ // ---- line parsing ----------------------------------------------------
+
+ void parseLines(string source)
+ {
+ auto raw = source.split("\n");
+ foreach (i, r; raw)
+ {
+ string s = r;
+ if (s.length && s[$ - 1] == '\r') s = s[0 .. $ - 1];
+ Line l;
+ l.lineNo = cast(int)(i + 1);
+ l.text = s;
+ parseLine(s, l);
+ if (l.op.length == 0 && l.label.length == 0 && !l.isEquate && !l.isDirective)
+ continue;
+ lines ~= l;
+ }
+ }
+
+ void parseLine(string src, ref Line l)
+ {
+ string s = strip(stripComment(src));
+ if (s.length == 0) return;
+
+ // label?
+ if (isIdentStart(s[0]))
+ {
+ size_t p = 0;
+ while (p < s.length && isIdentChar(s[p])) p++;
+ if (p < s.length && s[p] == ':')
+ {
+ l.label = s[0 .. p];
+ addLabel(l.label, l.lineNo);
+ s = strip(s[p + 1 .. $]);
+ if (s.length == 0)
+ {
+ l.labelOnly = true;
+ return;
+ }
+ }
+ }
+
+ // equate: ident '=' expr (not '==')
+ if (isIdentStart(s[0]))
+ {
+ size_t p = 0;
+ while (p < s.length && isIdentChar(s[p])) p++;
+ size_t q = p;
+ while (q < s.length && (s[q] == ' ' || s[q] == '\t')) q++;
+ if (q < s.length && s[q] == '=' && (q + 1 >= s.length || s[q + 1] != '='))
+ {
+ string name = s[0 .. p];
+ string rhs = strip(s[q + 1 .. $]);
+ if (rhs.length == 0) err(l.lineNo, "missing value in equate");
+ l.isEquate = true;
+ l.eqName = name;
+ l.expr = parseExpr(rhs, l.lineNo);
+ return;
+ }
+ }
+
+ if (s[0] == '.')
+ {
+ l.isDirective = true;
+ parseDirective(s, l);
+ return;
+ }
+ parseInstruction(s, l);
+ }
+
+ void addLabel(string name, int lineNo)
+ {
+ if (auto p = name in syms)
+ {
+ if (p.imported) err(lineNo, "label '" ~ name ~ "' conflicts with imported symbol");
+ err(lineNo, "duplicate symbol '" ~ name ~ "'");
+ }
+ SymEntry e;
+ e.name = name;
+ e.defined = true;
+ syms[name] = e;
+ defOrder ~= name;
+ }
+
+ void parseInstruction(string s, ref Line l)
+ {
+ size_t p = 0;
+ while (p < s.length && isIdentChar(s[p])) p++;
+ l.op = toLower(s[0 .. p]);
+ parseOperand(strip(s[p .. $]), l);
+ }
+
+ void parseOperand(string rest, ref Line l)
+ {
+ if (rest.length == 0)
+ {
+ l.form = OpForm.imp;
+ return;
+ }
+ if (toLower(rest) == "a")
+ {
+ l.form = OpForm.acc;
+ return;
+ }
+ if (rest[0] == '#')
+ {
+ l.form = OpForm.imm;
+ l.expr = parseExpr(strip(rest[1 .. $]), l.lineNo);
+ return;
+ }
+ if (rest[0] == '(')
+ {
+ int close = matchParen(rest, 0);
+ if (close < 0) err(l.lineNo, "missing ')' in operand");
+ string inner = strip(rest[1 .. close]);
+ string trailing = strip(rest[close + 1 .. $]);
+ if (trailing.length == 0)
+ {
+ int ci = findTopLevelComma(inner);
+ if (ci >= 0)
+ {
+ string a = strip(inner[0 .. ci]);
+ string b = strip(inner[ci + 1 .. $]);
+ if (toLower(b) != "x") err(l.lineNo, "expected ',x' in (zp,x) operand");
+ l.form = OpForm.izx;
+ l.expr = parseExpr(a, l.lineNo);
+ }
+ else
+ {
+ l.form = OpForm.ind;
+ l.expr = parseExpr(inner, l.lineNo);
+ }
+ }
+ else if (toLower(trailing) == ",y")
+ {
+ l.form = OpForm.izy;
+ l.expr = parseExpr(inner, l.lineNo);
+ }
+ else
+ err(l.lineNo, "invalid operand '" ~ rest ~ "'");
+ return;
+ }
+ int ci = findTopLevelComma(rest);
+ if (ci >= 0)
+ {
+ string a = strip(rest[0 .. ci]);
+ string b = strip(rest[ci + 1 .. $]);
+ if (toLower(b) == "x") l.indexX = true;
+ else if (toLower(b) == "y") l.indexY = true;
+ else err(l.lineNo, "invalid index register '" ~ b ~ "'");
+ l.form = OpForm.direct;
+ l.expr = parseExpr(a, l.lineNo);
+ }
+ else
+ {
+ l.form = OpForm.direct;
+ l.expr = parseExpr(rest, l.lineNo);
+ }
+ }
+
+ void parseDirective(string s, ref Line l)
+ {
+ size_t p = 1;
+ while (p < s.length && (isIdentChar(s[p]))) p++;
+ l.op = toLower(s[0 .. p]);
+ string rest = strip(s[p .. $]);
+ switch (l.op)
+ {
+ case ".org":
+ l.expr = parseExpr(rest, l.lineNo);
+ break;
+ case ".entry":
+ l.entrySym = parseIdentName(rest, l.lineNo);
+ break;
+ case ".code", ".data", ".bss":
+ break;
+ case ".byte", ".db":
+ parseDataList(rest, l);
+ break;
+ case ".word", ".dw":
+ parseExprList(rest, l);
+ break;
+ case ".res", ".ds":
+ parseRes(rest, l);
+ break;
+ case ".ascii", ".asciiz":
+ l.strBytes = parseString(rest, l.lineNo);
+ break;
+ case ".export", ".import", ".zp":
+ l.names = parseNameList(rest, l.lineNo);
+ break;
+ default:
+ err(l.lineNo, "unknown directive '" ~ l.op ~ "'");
+ }
+ }
+
+ void parseDataList(string rest, ref Line l)
+ {
+ if (strip(rest).length == 0) err(l.lineNo, "missing operand");
+ foreach (raw; splitCommas(rest))
+ {
+ string pt = strip(raw);
+ if (pt.length == 0) err(l.lineNo, "empty .byte item");
+ DataItem it;
+ if (pt.length >= 2 && pt[0] == '"' && pt[$ - 1] == '"')
+ {
+ it.isStr = true;
+ it.bytes = decodeString(pt, l.lineNo);
+ }
+ else
+ {
+ it.isStr = false;
+ it.expr = parseExpr(pt, l.lineNo);
+ }
+ l.items ~= it;
+ }
+ }
+
+ void parseExprList(string rest, ref Line l)
+ {
+ if (strip(rest).length == 0) err(l.lineNo, "missing operand");
+ foreach (raw; splitCommas(rest))
+ {
+ string pt = strip(raw);
+ if (pt.length == 0) err(l.lineNo, "empty expression");
+ l.exprs ~= parseExpr(pt, l.lineNo);
+ }
+ }
+
+ void parseRes(string rest, ref Line l)
+ {
+ auto parts = splitCommas(rest);
+ if (parts.length == 0 || strip(parts[0]).length == 0) err(l.lineNo, "missing count");
+ l.exprs ~= parseExpr(strip(parts[0]), l.lineNo);
+ if (parts.length >= 2)
+ {
+ l.hasFill = true;
+ l.exprFill = parseExpr(strip(parts[1]), l.lineNo);
+ }
+ if (parts.length > 2) err(l.lineNo, "too many operands for .res");
+ }
+
+ ubyte[] parseString(string rest, int lineNo)
+ {
+ rest = strip(rest);
+ if (rest.length < 2 || rest[0] != '"' || rest[$ - 1] != '"')
+ err(lineNo, "expected a string literal");
+ return decodeString(rest, lineNo);
+ }
+
+ ubyte[] decodeString(string s, int lineNo)
+ {
+ ubyte[] buf;
+ for (size_t i = 1; i + 1 < s.length; i++)
+ {
+ char c = s[i];
+ if (c == '\\')
+ {
+ i++;
+ if (i >= s.length - 1) err(lineNo, "unterminated escape");
+ switch (s[i])
+ {
+ case 'n': buf ~= '\n'; break;
+ case 'r': buf ~= '\r'; break;
+ case 't': buf ~= '\t'; break;
+ case '0': buf ~= 0; break;
+ case '\\': buf ~= '\\'; break;
+ case '\'': buf ~= '\''; break;
+ case '"': buf ~= '"'; break;
+ default: err(lineNo, "unknown escape '\\" ~ s[i] ~ "'");
+ }
+ }
+ else
+ buf ~= cast(ubyte)c;
+ }
+ return buf;
+ }
+
+ string[] parseNameList(string rest, int lineNo)
+ {
+ string[] buf;
+ if (strip(rest).length == 0) err(lineNo, "missing name");
+ foreach (raw; splitCommas(rest))
+ {
+ string n = strip(raw);
+ if (n.length == 0) err(lineNo, "empty name");
+ buf ~= n;
+ }
+ return buf;
+ }
+
+ string parseIdentName(string rest, int lineNo)
+ {
+ rest = strip(rest);
+ if (rest.length == 0 || !isIdentStart(rest[0])) err(lineNo, "expected identifier");
+ size_t p = 0;
+ while (p < rest.length && isIdentChar(rest[p])) p++;
+ if (p != rest.length) err(lineNo, "invalid identifier '" ~ rest ~ "'");
+ return rest;
+ }
+
+ // ---- expressions -----------------------------------------------------
+
+ Expr parseExpr(string s, int lineNo)
+ {
+ size_t pos = 0;
+ Expr e = parseAddSub(s, pos, lineNo);
+ skipWs(s, pos);
+ if (pos != s.length) err(lineNo, "unexpected '" ~ s[pos .. $] ~ "' in expression");
+ return e;
+ }
+
+ Expr parseAddSub(string s, ref size_t pos, int lineNo)
+ {
+ Expr left = parseTerm(s, pos, lineNo);
+ while (true)
+ {
+ skipWs(s, pos);
+ if (pos < s.length && (s[pos] == '+' || s[pos] == '-'))
+ {
+ char op = s[pos];
+ pos++;
+ Expr right = parseTerm(s, pos, lineNo);
+ left = combine(left, right, op, lineNo);
+ }
+ else
+ break;
+ }
+ return left;
+ }
+
+ Expr parseTerm(string s, ref size_t pos, int lineNo)
+ {
+ skipWs(s, pos);
+ bool low = false, high = false;
+ if (pos < s.length && (s[pos] == '<' || s[pos] == '>'))
+ {
+ if (s[pos] == '<') low = true;
+ else high = true;
+ pos++;
+ }
+ skipWs(s, pos);
+ bool neg = false;
+ if (pos < s.length && s[pos] == '-')
+ {
+ neg = true;
+ pos++;
+ }
+ Expr e = parsePrimary(s, pos, lineNo);
+ if (neg)
+ {
+ if (!e.isNum) err(lineNo, "cannot negate a symbol");
+ e.num = -e.num;
+ }
+ if (low) e.low = true;
+ if (high) e.high = true;
+ return e;
+ }
+
+ Expr parsePrimary(string s, ref size_t pos, int lineNo)
+ {
+ skipWs(s, pos);
+ if (pos >= s.length) err(lineNo, "unexpected end of expression");
+ char c = s[pos];
+ if (c == '(')
+ {
+ pos++;
+ Expr e = parseAddSub(s, pos, lineNo);
+ skipWs(s, pos);
+ if (pos >= s.length || s[pos] != ')') err(lineNo, "missing ')'");
+ pos++;
+ return e;
+ }
+ if (c == '\'')
+ {
+ pos++;
+ int v;
+ if (pos < s.length && s[pos] == '\\')
+ {
+ pos++;
+ if (pos >= s.length) err(lineNo, "unterminated char literal");
+ switch (s[pos])
+ {
+ case 'n': v = '\n'; break;
+ case 'r': v = '\r'; break;
+ case 't': v = '\t'; break;
+ case '0': v = 0; break;
+ case '\\': v = '\\'; break;
+ case '\'': v = '\''; break;
+ case '"': v = '"'; break;
+ default: err(lineNo, "unknown escape in char literal");
+ }
+ pos++;
+ }
+ else
+ {
+ if (pos >= s.length) err(lineNo, "unterminated char literal");
+ v = cast(int)cast(ubyte)s[pos];
+ pos++;
+ }
+ if (pos >= s.length || s[pos] != '\'') err(lineNo, "unterminated char literal");
+ pos++;
+ Expr e;
+ e.isNum = true;
+ e.num = v;
+ return e;
+ }
+ if (c == '$')
+ {
+ pos++;
+ size_t st = pos;
+ while (pos < s.length && isHexDigit(s[pos])) pos++;
+ if (pos == st) err(lineNo, "invalid hex literal");
+ Expr e;
+ e.isNum = true;
+ e.num = cast(int)parseHex(s[st .. pos]);
+ return e;
+ }
+ if (c == '%')
+ {
+ pos++;
+ size_t st = pos;
+ while (pos < s.length && (s[pos] == '0' || s[pos] == '1')) pos++;
+ if (pos == st) err(lineNo, "invalid binary literal");
+ int v = 0;
+ foreach (ch; s[st .. pos]) v = v * 2 + (ch - '0');
+ Expr e;
+ e.isNum = true;
+ e.num = v;
+ return e;
+ }
+ if (c >= '0' && c <= '9')
+ {
+ size_t st = pos;
+ if (c == '0' && pos + 1 < s.length && (s[pos + 1] == 'x' || s[pos + 1] == 'X'))
+ {
+ pos += 2;
+ size_t hs = pos;
+ while (pos < s.length && isHexDigit(s[pos])) pos++;
+ if (pos == hs) err(lineNo, "invalid hex literal");
+ Expr e;
+ e.isNum = true;
+ e.num = cast(int)parseHex(s[hs .. pos]);
+ return e;
+ }
+ while (pos < s.length && s[pos] >= '0' && s[pos] <= '9') pos++;
+ Expr e;
+ e.isNum = true;
+ e.num = to!int(s[st .. pos]);
+ return e;
+ }
+ if (isIdentStart(c))
+ {
+ size_t st = pos;
+ while (pos < s.length && isIdentChar(s[pos])) pos++;
+ Expr e;
+ e.isNum = false;
+ e.sym = s[st .. pos];
+ e.offset = 0;
+ return e;
+ }
+ err(lineNo, "invalid character '" ~ c ~ "' in expression");
+ return Expr.init;
+ }
+
+ Expr combine(Expr a, Expr b, char op, int lineNo)
+ {
+ int sign = (op == '+') ? 1 : -1;
+ Expr r;
+ if (a.isNum && b.isNum)
+ {
+ r.isNum = true;
+ r.num = a.num + sign * b.num;
+ return r;
+ }
+ if (!a.isNum && b.isNum)
+ {
+ r.isNum = false;
+ r.sym = a.sym;
+ r.offset = a.offset + sign * b.num;
+ r.low = a.low;
+ r.high = a.high;
+ return r;
+ }
+ if (a.isNum && !b.isNum)
+ {
+ if (op == '-') err(lineNo, "cannot subtract a symbol from a constant");
+ r.isNum = false;
+ r.sym = b.sym;
+ r.offset = b.offset + a.num;
+ r.low = b.low;
+ r.high = b.high;
+ return r;
+ }
+ err(lineNo, "cannot combine two symbols in an expression");
+ return Expr.init;
+ }
+
+ long evalConstExpr(Expr e, int lineNo)
+ {
+ if (e.isNum) return e.num;
+ if (e.sym.length == 0) err(lineNo, "empty expression");
+ if (auto p = e.sym in equates) return *p + e.offset;
+ err(lineNo, "symbol '" ~ e.sym ~ "' is not a constant");
+ return 0;
+ }
+
+ bool resolveValue(Expr e, out int v)
+ {
+ if (e.isNum)
+ {
+ v = e.num;
+ return true;
+ }
+ if (e.sym.length == 0) return false;
+ if (auto p = e.sym in equates)
+ {
+ v = cast(int)*p + e.offset;
+ return true;
+ }
+ if (auto s = e.sym in syms)
+ {
+ if (s.defined && s.valueKnown)
+ {
+ v = cast(int)s.value + e.offset;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // ---- prepass ---------------------------------------------------------
+
+ void prepass()
+ {
+ foreach (ref l; lines)
+ {
+ if (l.isEquate)
+ {
+ if (l.eqName in syms) err(l.lineNo, "duplicate symbol '" ~ l.eqName ~ "'");
+ equates[l.eqName] = evalConstExpr(l.expr, l.lineNo);
+ }
+ else if (l.isDirective)
+ {
+ switch (l.op)
+ {
+ case ".import":
+ foreach (n; l.names)
+ {
+ if (n in syms) err(l.lineNo, "symbol '" ~ n ~ "' already defined");
+ SymEntry e;
+ e.name = n;
+ e.imported = true;
+ e.global = true;
+ syms[n] = e;
+ impOrder ~= n;
+ }
+ break;
+ case ".export":
+ foreach (n; l.names)
+ {
+ auto p = n in syms;
+ if (p is null) err(l.lineNo, "exported symbol '" ~ n ~ "' is not defined");
+ p.global = true;
+ }
+ break;
+ case ".entry":
+ entryName = l.entrySym;
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ foreach (ref l; lines)
+ {
+ if (l.isDirective && l.op == ".zp")
+ {
+ foreach (n; l.names)
+ {
+ auto p = n in syms;
+ if (p is null) err(l.lineNo, "zp symbol '" ~ n ~ "' is not defined");
+ p.zeropage = true;
+ }
+ }
+ }
+ }
+
+ void assignIndices()
+ {
+ int idx = 0;
+ foreach (n; defOrder)
+ {
+ if (auto p = n in syms) p.index = idx++;
+ }
+ foreach (n; impOrder)
+ {
+ if (auto p = n in syms) p.index = idx++;
+ }
+ }
+
+ // ---- segments --------------------------------------------------------
+
+ ubyte segFlags(string name)
+ {
+ switch (name)
+ {
+ case ".code": return SEG_CODE;
+ case ".data": return SEG_DATA;
+ case ".bss": return SEG_BSS;
+ default: return SEG_CODE;
+ }
+ }
+
+ void switchSeg(string name)
+ {
+ if (name !in segIndex)
+ {
+ SegState s;
+ s.name = name;
+ s.flags = segFlags(name);
+ segIndex[name] = cast(int)segs.length;
+ segs ~= s;
+ }
+ activeSeg = segIndex[name];
+ }
+
+ // ---- sizing ----------------------------------------------------------
+
+ void sizing()
+ {
+ switchSeg(".code");
+ foreach (_; 0 .. 1000)
+ {
+ if (!layoutOnce()) break;
+ }
+ }
+
+ bool layoutOnce()
+ {
+ foreach (ref s; segs) s.len = 0;
+ foreach (ref e; syms) e.valueKnown = false;
+ activeSeg = segIndex[".code"];
+ bool changed = false;
+ foreach (ref l; lines)
+ {
+ if (l.label.length)
+ {
+ auto p = l.label in syms;
+ p.value = cast(ushort)segs[activeSeg].len;
+ p.seg = activeSeg;
+ p.defined = true;
+ p.valueKnown = true;
+ }
+ if (l.isEquate || l.labelOnly) continue;
+ if (l.isDirective)
+ {
+ sizingDirective(l);
+ continue;
+ }
+ AddrMode m = resolveInstMode(l);
+ if (!l.modeSet || l.mode != m)
+ {
+ if (l.modeSet && l.mode != m) changed = true;
+ l.mode = m;
+ l.modeSet = true;
+ }
+ segs[activeSeg].len += instSize(m);
+ }
+ return changed;
+ }
+
+ void sizingDirective(ref Line l)
+ {
+ switch (l.op)
+ {
+ case ".org":
+ doOrgLen(cast(int)evalConstExpr(l.expr, l.lineNo), l.lineNo);
+ break;
+ case ".code": switchSeg(".code"); break;
+ case ".data": switchSeg(".data"); break;
+ case ".bss": switchSeg(".bss"); break;
+ case ".byte", ".db":
+ foreach (it; l.items) segs[activeSeg].len += it.isStr ? it.bytes.length : 1;
+ break;
+ case ".word", ".dw":
+ segs[activeSeg].len += l.exprs.length * 2;
+ break;
+ case ".res", ".ds":
+ segs[activeSeg].len += cast(size_t)evalConstExpr(l.exprs[0], l.lineNo);
+ break;
+ case ".ascii":
+ segs[activeSeg].len += l.strBytes.length;
+ break;
+ case ".asciiz":
+ segs[activeSeg].len += l.strBytes.length + 1;
+ break;
+ case ".export", ".import", ".zp", ".entry":
+ break;
+ default:
+ err(l.lineNo, "unknown directive '" ~ l.op ~ "'");
+ }
+ }
+
+ void doOrgLen(int newAbs, int lineNo)
+ {
+ ref SegState s = segs[activeSeg];
+ if (s.len == 0)
+ {
+ s.origin = cast(ushort)newAbs;
+ return;
+ }
+ int off = newAbs - cast(int)s.origin;
+ if (off < cast(int)s.len) err(lineNo, ".org moves backwards");
+ s.len = cast(size_t)off;
+ }
+
+ AddrMode resolveInstMode(ref Line l)
+ {
+ auto op = l.op in gOpTable;
+ if (op is null) err(l.lineNo, "unknown instruction '" ~ l.op ~ "'");
+ auto ops = *op;
+ switch (l.form)
+ {
+ case OpForm.imp:
+ if (AddrMode.acc in ops) return AddrMode.acc;
+ if (AddrMode.imp in ops) return AddrMode.imp;
+ err(l.lineNo, "instruction '" ~ l.op ~ "' has no implied form");
+ break;
+ case OpForm.acc:
+ if (AddrMode.acc in ops) return AddrMode.acc;
+ err(l.lineNo, "instruction '" ~ l.op ~ "' has no accumulator form");
+ break;
+ case OpForm.imm:
+ if (AddrMode.imm in ops) return AddrMode.imm;
+ err(l.lineNo, "instruction '" ~ l.op ~ "' has no immediate form");
+ break;
+ case OpForm.izx:
+ if (AddrMode.izx in ops) return AddrMode.izx;
+ err(l.lineNo, "instruction '" ~ l.op ~ "' has no (zp,x) form");
+ break;
+ case OpForm.izy:
+ if (AddrMode.izy in ops) return AddrMode.izy;
+ err(l.lineNo, "instruction '" ~ l.op ~ "' has no (zp),y form");
+ break;
+ case OpForm.ind:
+ if (AddrMode.ind in ops) return AddrMode.ind;
+ err(l.lineNo, "instruction '" ~ l.op ~ "' has no indirect form");
+ break;
+ case OpForm.direct:
+ if (l.indexX) return pickZpAbs(AddrMode.zpx, AddrMode.absx, l, ops);
+ if (l.indexY) return pickZpAbs(AddrMode.zpy, AddrMode.absy, l, ops);
+ if (AddrMode.rel in ops) return AddrMode.rel;
+ return pickZpAbs(AddrMode.zp, AddrMode.abs, l, ops);
+ default:
+ break;
+ }
+ return AddrMode.imp;
+ }
+
+ AddrMode pickZpAbs(AddrMode zp, AddrMode ab, ref Line l, ubyte[AddrMode] ops)
+ {
+ bool zpOk = (zp in ops) !is null;
+ bool abOk = (ab in ops) !is null;
+ if (zpOk && isZp(l.expr)) return zp;
+ if (abOk) return ab;
+ if (zpOk) return zp;
+ err(l.lineNo, "instruction '" ~ l.op ~ "' has no such addressing mode");
+ return ab;
+ }
+
+ bool isZp(Expr e)
+ {
+ if (e.isNum) return e.num >= 0 && e.num <= 0xFF;
+ if (auto q = e.sym in equates)
+ {
+ int v = cast(int)*q + e.offset;
+ return v >= 0 && v <= 0xFF;
+ }
+ auto s = e.sym in syms;
+ if (s is null) return false;
+ if (s.imported) return s.zeropage;
+ if (s.defined && s.valueKnown && s.seg >= 0 && segs[s.seg].origin != 0)
+ {
+ int addr = cast(int)segs[s.seg].origin + cast(int)s.value + e.offset;
+ return addr >= 0 && addr <= 0xFF;
+ }
+ return false;
+ }
+
+ int instSize(AddrMode m)
+ {
+ switch (m)
+ {
+ case AddrMode.imp, AddrMode.acc:
+ return 1;
+ case AddrMode.imm, AddrMode.zp, AddrMode.zpx, AddrMode.zpy,
+ AddrMode.izx, AddrMode.izy, AddrMode.rel:
+ return 2;
+ default:
+ return 3;
+ }
+ }
+
+ // ---- emission --------------------------------------------------------
+
+ void emit()
+ {
+ foreach (ref s; segs)
+ {
+ s.len = 0;
+ s.data = null;
+ }
+ activeSeg = segIndex[".code"];
+ foreach (ref l; lines)
+ {
+ if (l.label.length)
+ {
+ auto p = l.label in syms;
+ p.value = cast(ushort)segs[activeSeg].len;
+ p.seg = activeSeg;
+ p.defined = true;
+ p.valueKnown = true;
+ }
+ if (l.isEquate || l.labelOnly) continue;
+ if (l.isDirective)
+ {
+ emitDirective(l);
+ continue;
+ }
+ emitInstruction(l);
+ }
+ }
+
+ void emitByte(ubyte b)
+ {
+ segs[activeSeg].data ~= b;
+ segs[activeSeg].len = segs[activeSeg].data.length;
+ }
+
+ void emitInstruction(ref Line l)
+ {
+ ubyte op = gOpTable[l.op][l.mode];
+ emitByte(op);
+ switch (l.mode)
+ {
+ case AddrMode.imp, AddrMode.acc:
+ break;
+ case AddrMode.imm, AddrMode.zp, AddrMode.zpx, AddrMode.zpy,
+ AddrMode.izx, AddrMode.izy:
+ emitRef(l.expr, 1, l.lineNo);
+ break;
+ case AddrMode.rel:
+ emitBranch(l.expr, l.lineNo);
+ break;
+ default:
+ emitRef(l.expr, 2, l.lineNo);
+ break;
+ }
+ }
+
+ void emitConst(int v, int size, bool low, bool high)
+ {
+ if (size == 1)
+ emitByte(high ? cast(ubyte)((v >> 8) & 0xFF) : cast(ubyte)(v & 0xFF));
+ else
+ {
+ emitByte(cast(ubyte)(v & 0xFF));
+ emitByte(cast(ubyte)((v >> 8) & 0xFF));
+ }
+ }
+
+ void emitRef(Expr e, int size, int lineNo)
+ {
+ bool isConst = e.isNum || (e.sym in equates) !is null;
+ if (isConst)
+ {
+ int v;
+ resolveValue(e, v);
+ emitConst(v, size, e.low, e.high);
+ return;
+ }
+ auto p = e.sym in syms;
+ if (p is null) err(lineNo, "undefined symbol '" ~ e.sym ~ "'");
+ // Resolve defined symbols that live in a fixed-origin segment.
+ if (p.defined && p.seg >= 0 && segs[p.seg].origin != 0)
+ {
+ int addr = cast(int)segs[p.seg].origin + cast(int)p.value + e.offset;
+ emitConst(addr, size, e.low, e.high);
+ return;
+ }
+ if (e.offset != 0)
+ err(lineNo, "relocation with offset is not supported for symbol '" ~ e.sym ~ "'");
+ Reloc r;
+ r.seg = cast(ushort)activeSeg;
+ r.sym = cast(ushort)p.index;
+ r.offset = cast(ushort)segs[activeSeg].len;
+ if (size == 2)
+ {
+ if (e.low) r.type = RelocType.lo8;
+ else if (e.high) r.type = RelocType.hi8;
+ else r.type = RelocType.abs16;
+ relocs ~= r;
+ emitByte(0);
+ emitByte(0);
+ }
+ else
+ {
+ r.type = e.high ? RelocType.hi8 : RelocType.lo8;
+ relocs ~= r;
+ emitByte(0);
+ }
+ }
+
+ void emitBranch(Expr e, int lineNo)
+ {
+ int operandOff = cast(int)segs[activeSeg].len;
+ if (e.isNum || (e.sym in equates) !is null)
+ {
+ if (segs[activeSeg].origin == 0)
+ err(lineNo, "branch to a constant in a relocatable segment");
+ int v;
+ resolveValue(e, v);
+ int endAddr = cast(int)segs[activeSeg].origin + operandOff + 1;
+ int disp = v - endAddr;
+ if (disp < -128 || disp > 127) err(lineNo, "branch out of range");
+ emitByte(cast(ubyte)(disp & 0xFF));
+ return;
+ }
+ auto p = e.sym in syms;
+ if (p is null) err(lineNo, "undefined symbol '" ~ e.sym ~ "'");
+ if (p.defined && p.seg == activeSeg)
+ {
+ int disp = cast(int)p.value + e.offset - (operandOff + 1);
+ if (disp < -128 || disp > 127) err(lineNo, "branch out of range");
+ emitByte(cast(ubyte)(disp & 0xFF));
+ return;
+ }
+ if (p.defined && p.seg >= 0 && segs[p.seg].origin != 0 && segs[activeSeg].origin != 0)
+ {
+ int targetAddr = cast(int)segs[p.seg].origin + cast(int)p.value + e.offset;
+ int endAddr = cast(int)segs[activeSeg].origin + operandOff + 1;
+ int disp = targetAddr - endAddr;
+ if (disp < -128 || disp > 127) err(lineNo, "branch out of range");
+ emitByte(cast(ubyte)(disp & 0xFF));
+ return;
+ }
+ if (e.offset != 0) err(lineNo, "relocation with offset is not supported");
+ Reloc r;
+ r.seg = cast(ushort)activeSeg;
+ r.sym = cast(ushort)p.index;
+ r.offset = cast(ushort)operandOff;
+ r.type = RelocType.rel8;
+ relocs ~= r;
+ emitByte(0);
+ }
+
+ void emitDirective(ref Line l)
+ {
+ switch (l.op)
+ {
+ case ".org":
+ doOrgEmit(cast(int)evalConstExpr(l.expr, l.lineNo), l.lineNo);
+ break;
+ case ".code": switchSeg(".code"); break;
+ case ".data": switchSeg(".data"); break;
+ case ".bss": switchSeg(".bss"); break;
+ case ".byte", ".db":
+ foreach (it; l.items)
+ {
+ if (it.isStr)
+ foreach (b; it.bytes) emitByte(b);
+ else
+ emitRef(it.expr, 1, l.lineNo);
+ }
+ break;
+ case ".word", ".dw":
+ foreach (e; l.exprs) emitRef(e, 2, l.lineNo);
+ break;
+ case ".res", ".ds":
+ {
+ int count = cast(int)evalConstExpr(l.exprs[0], l.lineNo);
+ ubyte fill = 0;
+ if (l.hasFill) fill = cast(ubyte)evalConstExpr(l.exprFill, l.lineNo);
+ foreach (_; 0 .. count) emitByte(fill);
+ break;
+ }
+ case ".ascii":
+ foreach (b; l.strBytes) emitByte(b);
+ break;
+ case ".asciiz":
+ foreach (b; l.strBytes) emitByte(b);
+ emitByte(0);
+ break;
+ case ".export", ".import", ".zp", ".entry":
+ break;
+ default:
+ err(l.lineNo, "unknown directive '" ~ l.op ~ "'");
+ }
+ }
+
+ void doOrgEmit(int newAbs, int lineNo)
+ {
+ ref SegState s = segs[activeSeg];
+ if (s.len == 0)
+ {
+ s.origin = cast(ushort)newAbs;
+ return;
+ }
+ int off = newAbs - cast(int)s.origin;
+ if (off < cast(int)s.len) err(lineNo, ".org moves backwards");
+ while (cast(int)s.len < off) emitByte(0);
+ }
+
+ // ---- object construction ---------------------------------------------
+
+ Object buildObject()
+ {
+ Object obj;
+ int[] remap;
+ remap.length = segs.length;
+ foreach (i, ref s; segs) remap[i] = -1;
+
+ foreach (i, ref s; segs)
+ {
+ if (s.len == 0 && s.data.length == 0) continue;
+ Segment seg;
+ seg.name = s.name;
+ seg.flags = s.flags;
+ seg.origin = s.origin;
+ if ((s.flags & 0x08) != 0) // bss bit only (SEG_BSS also carries alloc|write)
+ seg.data = new ubyte[s.len];
+ else
+ seg.data = s.data;
+ remap[i] = cast(int)obj.segments.length;
+ obj.segments ~= seg;
+ }
+
+ foreach (n; defOrder)
+ {
+ auto p = n in syms;
+ if (p is null) continue;
+ Symbol sym;
+ sym.name = p.name;
+ sym.value = p.value;
+ sym.flags = SymFlag.defined;
+ if (p.global) sym.flags |= SymFlag.global;
+ bool zp = false;
+ if (p.seg >= 0 && segs[p.seg].origin != 0)
+ zp = (cast(int)segs[p.seg].origin + cast(int)p.value) <= 0xFF;
+ if (zp) sym.flags |= SymFlag.zeropage;
+ sym.seg = (p.seg >= 0 && remap[p.seg] >= 0) ? cast(ushort)remap[p.seg] : NoIndex;
+ obj.symbols ~= sym;
+ }
+ foreach (n; impOrder)
+ {
+ auto p = n in syms;
+ if (p is null) continue;
+ Symbol sym;
+ sym.name = p.name;
+ sym.value = 0;
+ sym.flags = SymFlag.imported | SymFlag.global;
+ if (p.zeropage) sym.flags |= SymFlag.zeropage;
+ sym.seg = NoIndex;
+ obj.symbols ~= sym;
+ }
+
+ foreach (r; relocs)
+ {
+ Reloc rr = r;
+ if (r.seg >= 0 && remap[r.seg] >= 0) rr.seg = cast(ushort)remap[r.seg];
+ obj.relocs ~= rr;
+ }
+
+ if (entryName.length)
+ {
+ auto p = entryName in syms;
+ if (p is null || !p.defined) err(0, "entry symbol '" ~ entryName ~ "' is not defined");
+ obj.entry = p.value;
+ obj.hasEntry = true;
+ }
+ else if (auto sp = "start" in syms)
+ {
+ if (sp.defined)
+ {
+ obj.entry = sp.value;
+ obj.hasEntry = true;
+ }
+ }
+ return obj;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Unittests
+// ---------------------------------------------------------------------------
+
+unittest
+{
+ // LDA immediate
+ auto o = assembleSource("lda #$42\n");
+ assert(o.segments.length == 1);
+ assert(o.segments[0].data == [0xA9, 0x42]);
+
+ // zero page numeric
+ auto o2 = assembleSource(".org $0000\nlda $10\n");
+ assert(o2.segments[0].data == [0xA5, 0x10]);
+
+ // absolute numeric (fixed origin)
+ auto o3 = assembleSource(".org $0600\nlda $1234\n");
+ assert(o3.segments[0].data == [0xAD, 0x34, 0x12]);
+
+ // zp,x
+ auto o4 = assembleSource(".org $0000\nlda $10,x\n");
+ assert(o4.segments[0].data == [0xB5, 0x10]);
+
+ // abs,x
+ auto o5 = assembleSource(".org $0000\nlda $1234,x\n");
+ assert(o5.segments[0].data == [0xBD, 0x34, 0x12]);
+
+ // (zp,x)
+ auto o6 = assembleSource(".org $0000\nlda ($10,x)\n");
+ assert(o6.segments[0].data == [0xA1, 0x10]);
+
+ // (zp),y
+ auto o7 = assembleSource(".org $0000\nlda ($10),y\n");
+ assert(o7.segments[0].data == [0xB1, 0x10]);
+
+ // (abs) jmp
+ auto o8 = assembleSource(".org $0000\njmp ($1234)\n");
+ assert(o8.segments[0].data == [0x6C, 0x34, 0x12]);
+
+ // backward branch displacement
+ auto o9 = assembleSource(".org $0000\nloop:\ndex\nbne loop\n");
+ assert(o9.segments[0].data == [0xCA, 0xD0, 0xFD]);
+
+ // directives
+ auto o10 = assembleSource(".org $0000\n.byte 1,2,3\n.word $1234\n.res 2\n.ascii \"A\"\n.asciiz \"B\"\n");
+ assert(o10.segments[0].data == [1, 2, 3, 0x34, 0x12, 0, 0, 'A', 'B', 0]);
+
+ // export / import reloc
+ auto o11 = assembleSource(".export start\n.import putc\nstart:\njsr putc\nrts\n");
+ assert(o11.segments[0].data[0] == 0x20);
+ assert(o11.relocs.length == 1);
+ assert(o11.relocs[0].type == RelocType.abs16);
+ assert(o11.relocs[0].offset == 1);
+ int pi = o11.findSymbol("putc");
+ assert(pi >= 0);
+ assert(o11.relocs[0].sym == pi);
+ assert(o11.findSymbol("start") >= 0);
+
+ // accumulator form
+ auto o12 = assembleSource(".org $0000\nasl a\nlsr\n");
+ assert(o12.segments[0].data == [0x0A, 0x4A]);
+
+ // equate + lo/hi
+ auto o13 = assembleSource(".org $0000\nV = $1234\nlda #V\n");
+ assert(o13.segments[0].data == [0xA9, 0x34, 0xA9, 0x12]);
+
+ // char literal
+ auto o14 = assembleSource(".org $0000\nlda #'A'\n");
+ assert(o14.segments[0].data == [0xA9, 0x41]);
+}
diff --git a/src/assembler/main.d b/src/assembler/main.d
index b0e361f..602d50d 100644
--- a/src/assembler/main.d
+++ b/src/assembler/main.d
@@ -1,6 +1,75 @@
-import std.stdio;
+module assembler.main;
-void main()
+import std.format : format;
+import std.getopt : getopt;
+import std.path : setExtension;
+import std.stdio : File, stderr, stdout, writeln;
+import assembler.assembler : assembleFile;
+import wcore.objfmt : Object, writeObject;
+
+private void printHelp()
{
- writeln("same deal");
+ writeln("wiasm - small 6502 assembler");
+ writeln();
+ writeln("Usage: wiasm [options] ");
+ writeln();
+ writeln(" -o, --output output object file (default: input with .wo)");
+ writeln(" -l, --listing write a listing file");
+ writeln(" -h, --help show this help");
+}
+
+private void writeListing(string path, string input, ref Object obj)
+{
+ auto f = File(path, "w");
+ f.writeln("; listing for ", input);
+ foreach (s; obj.segments)
+ {
+ f.writeln(format("; segment %s flags=$%02X origin=$%04X size=%d",
+ s.name, s.flags, s.origin, s.data.length));
+ for (size_t i = 0; i < s.data.length; i += 16)
+ {
+ size_t end = (i + 16 < s.data.length) ? i + 16 : s.data.length;
+ string hexs;
+ foreach (b; s.data[i .. end]) hexs ~= format("%02X ", b);
+ f.writeln(format(" %04X %s", i, hexs));
+ }
+ }
+ f.writeln("; symbols");
+ foreach (sym; obj.symbols)
+ f.writeln(format(" %-24s = $%04X flags=$%02X seg=%d",
+ sym.name, sym.value, sym.flags, sym.seg == ushort.max ? -1 : cast(int)sym.seg));
+}
+
+int main(string[] args)
+{
+ string outPath;
+ string listPath;
+ try
+ {
+ auto helpInfo = getopt(args,
+ "o|output", "output object file", &outPath,
+ "l|listing", "listing file", &listPath,
+ );
+ if (helpInfo.helpWanted)
+ {
+ printHelp();
+ return 0;
+ }
+ if (args.length < 2)
+ {
+ printHelp();
+ return 1;
+ }
+ string input = args[1];
+ if (outPath.length == 0) outPath = setExtension(input, ".wo");
+ auto obj = assembleFile(input);
+ writeObject(outPath, obj);
+ if (listPath.length) writeListing(listPath, input, obj);
+ return 0;
+ }
+ catch (Exception e)
+ {
+ stderr.writeln("wiasm: ", e.msg);
+ return 1;
+ }
}
diff --git a/src/emulator/bus.d b/src/emulator/bus.d
new file mode 100644
index 0000000..0219d2a
--- /dev/null
+++ b/src/emulator/bus.d
@@ -0,0 +1,142 @@
+/**
+ * 6502 system bus: 64 KiB RAM plus memory-mapped terminal I/O.
+ *
+ * Memory-mapped I/O registers:
+ * $F001 write: emit byte to stdout (raw char); read: 0
+ * $F002 read: next byte of buffered stdin (0 when exhausted); write: ignored
+ * $F003 write: set halted flag; read: 0
+ * $F004 read: 0x80 when buffered input remains, else 0; write: ignored
+ */
+module emulator.bus;
+
+import std.stdio : stdout;
+
+/// Transmit register: writing a byte prints it to stdout.
+enum ushort IO_TX = 0xF001;
+/// Receive register: reading consumes the next buffered input byte (0 when empty).
+enum ushort IO_RX = 0xF002;
+/// Halt register: writing any byte sets the halted flag.
+enum ushort IO_HALT = 0xF003;
+/// Receive-ready register: 0x80 when input is still available.
+enum ushort IO_RXRDY = 0xF004;
+
+class Bus
+{
+ /// 64 KiB of RAM.
+ ubyte[65536] ram;
+
+ /// Set when the program writes to IO_HALT.
+ bool halted = false;
+
+ private ubyte[] input;
+ private size_t inputPos = 0;
+
+ /// Load a byte array at an address (wraps within 64 KiB).
+ void loadAt(ushort addr, const(ubyte)[] data)
+ {
+ foreach (i, ubyte b; data)
+ ram[(addr + i) & 0xFFFF] = b;
+ }
+
+ /// Set the buffered input stream consumed by reads of IO_RX.
+ void setInput(const(ubyte)[] data)
+ {
+ input = data.dup;
+ inputPos = 0;
+ }
+
+ /// True when more buffered input remains.
+ bool inputAvailable() const
+ {
+ return inputPos < input.length;
+ }
+
+ /// Read a byte from memory or an I/O register.
+ ubyte read(ushort addr)
+ {
+ switch (addr)
+ {
+ case IO_TX:
+ return 0;
+ case IO_RX:
+ if (inputPos < input.length)
+ return input[inputPos++];
+ return 0;
+ case IO_HALT:
+ return 0;
+ case IO_RXRDY:
+ return inputAvailable() ? 0x80 : 0;
+ default:
+ return ram[addr];
+ }
+ }
+
+ /// Write a byte to memory or an I/O register.
+ void write(ushort addr, ubyte val)
+ {
+ switch (addr)
+ {
+ case IO_TX:
+ {
+ ubyte[1] buf;
+ buf[0] = val;
+ stdout.rawWrite(buf[]);
+ stdout.flush();
+ break;
+ }
+ case IO_RX:
+ break; // ignored
+ case IO_HALT:
+ halted = true;
+ break;
+ case IO_RXRDY:
+ break; // ignored
+ default:
+ ram[addr] = val;
+ break;
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------
+// unit tests
+// ---------------------------------------------------------------------------
+
+unittest
+{
+ auto b = new Bus();
+
+ // plain RAM round trip
+ b.write(0x0200, 0xAB);
+ assert(b.read(0x0200) == 0xAB);
+
+ // loadAt writes a block
+ b.loadAt(0x0300, [0x01, 0x02, 0x03, 0x04]);
+ assert(b.read(0x0300) == 0x01);
+ assert(b.read(0x0301) == 0x02);
+ assert(b.read(0x0303) == 0x04);
+
+ // $F003 write sets halted
+ assert(b.halted == false);
+ b.write(0xF003, 0x00);
+ assert(b.halted == true);
+
+ // $F001 / $F003 / $F004 reads return 0 / 0 / status
+ b.write(0xF001, 0x00);
+ assert(b.read(0xF001) == 0);
+ assert(b.read(0xF003) == 0);
+
+ // input buffer handling
+ assert(b.inputAvailable() == false);
+ assert(b.read(0xF004) == 0x00);
+ assert(b.read(0xF002) == 0x00); // exhausted -> 0
+
+ b.setInput([0x41, 0x42]);
+ assert(b.inputAvailable() == true);
+ assert(b.read(0xF004) == 0x80);
+ assert(b.read(0xF002) == 0x41);
+ assert(b.read(0xF004) == 0x80);
+ assert(b.read(0xF002) == 0x42);
+ assert(b.read(0xF004) == 0x00);
+ assert(b.read(0xF002) == 0x00);
+}
diff --git a/src/emulator/cpu.d b/src/emulator/cpu.d
new file mode 100644
index 0000000..c0d0c95
--- /dev/null
+++ b/src/emulator/cpu.d
@@ -0,0 +1,1014 @@
+/**
+ * NMOS 6502 CPU core.
+ *
+ * Implements all 151 documented legal opcodes with correct flags and base
+ * cycle counts (including page-cross and branch-taken penalties where the
+ * real 6502 applies them). Undocumented/illegal opcodes are treated as a
+ * 1-byte NOP (2 cycles) and never crash.
+ *
+ * Decimal (BCD) mode is implemented for ADC and SBC. The N and V flags in
+ * BCD mode are set from the equivalent binary result (an approximation of
+ * the hardware's undefined behaviour); Z and C are exact.
+ *
+ * The JMP (indirect) page-boundary bug is implemented (low byte is read from
+ * ptr, high byte from (ptr & 0xFF00) | ((ptr + 1) & 0xFF)).
+ */
+module emulator.cpu;
+
+import std.string : format;
+
+import emulator.bus;
+
+/// Processor status flag bit masks.
+enum : ubyte
+{
+ F_N = 0x80, /// negative
+ F_V = 0x40, /// overflow
+ F_U = 0x20, /// unused; always set
+ F_B = 0x10, /// break
+ F_D = 0x08, /// decimal (BCD)
+ F_I = 0x04, /// interrupt disable
+ F_Z = 0x02, /// zero
+ F_C = 0x01, /// carry
+}
+
+class Cpu
+{
+ Bus bus;
+
+ ubyte A, X, Y; /// accumulator, index registers
+ ubyte SP; /// stack pointer (page 1)
+ ushort PC; /// program counter
+ ubyte P; /// processor status
+ ulong cycles; /// total cycle counter
+
+ this(Bus b)
+ {
+ bus = b;
+ }
+
+ /// Reset registers (does NOT set PC; the caller sets the entry point).
+ void reset()
+ {
+ A = 0;
+ X = 0;
+ Y = 0;
+ SP = 0xFD;
+ P = 0x24; // I | unused
+ cycles = 0;
+ }
+
+ /// Set the program counter (entry point).
+ void setPC(ushort pc)
+ {
+ PC = pc;
+ }
+
+ // -- flag helpers -------------------------------------------------------
+
+ private bool getF(ubyte m) const
+ {
+ return (P & m) != 0;
+ }
+
+ private void setF(ubyte m, bool v)
+ {
+ if (v)
+ P |= m;
+ else
+ P &= cast(ubyte) ~m;
+ }
+
+ private void setNZ(ubyte v)
+ {
+ setF(F_Z, v == 0);
+ setF(F_N, (v & 0x80) != 0);
+ }
+
+ private void setZ(bool v) { setF(F_Z, v); }
+ private void setN(bool v) { setF(F_N, v); }
+ private void setC(bool v) { setF(F_C, v); }
+ private void setV(bool v) { setF(F_V, v); }
+
+ // -- bus/stack helpers --------------------------------------------------
+
+ private ubyte fetch()
+ {
+ return bus.read(PC++);
+ }
+
+ private ushort fetch16()
+ {
+ ubyte lo = fetch();
+ ubyte hi = fetch();
+ return cast(ushort) (lo | (hi << 8));
+ }
+
+ private ushort read16(ushort a)
+ {
+ ubyte lo = bus.read(a);
+ ubyte hi = bus.read(cast(ushort) (a + 1));
+ return cast(ushort) (lo | (hi << 8));
+ }
+
+ private void push(ubyte v)
+ {
+ bus.write(cast(ushort) (0x0100 + SP), v);
+ SP--;
+ }
+
+ private ubyte pop()
+ {
+ SP++;
+ return bus.read(cast(ushort) (0x0100 + SP));
+ }
+
+ private void setPFromStack(ubyte v)
+ {
+ P = cast(ubyte) (v | F_U);
+ }
+
+ // -- addressing modes ---------------------------------------------------
+
+ private ushort addrZP() { return fetch(); }
+ private ushort addrZPX() { return cast(ushort) ((fetch() + X) & 0xFF); }
+ private ushort addrZPY() { return cast(ushort) ((fetch() + Y) & 0xFF); }
+ private ushort addrAbs() { return fetch16(); }
+
+ private ushort addrAbsX(out bool cross)
+ {
+ ushort b = fetch16();
+ ushort r = cast(ushort) (b + X);
+ cross = (b & 0xFF00) != (r & 0xFF00);
+ return r;
+ }
+
+ private ushort addrAbsY(out bool cross)
+ {
+ ushort b = fetch16();
+ ushort r = cast(ushort) (b + Y);
+ cross = (b & 0xFF00) != (r & 0xFF00);
+ return r;
+ }
+
+ private ushort addrIzx()
+ {
+ ushort zp = cast(ushort) ((fetch() + X) & 0xFF);
+ return read16(zp);
+ }
+
+ private ushort addrIzy(out bool cross)
+ {
+ ushort zp = fetch();
+ ushort b = read16(zp);
+ ushort r = cast(ushort) (b + Y);
+ cross = (b & 0xFF00) != (r & 0xFF00);
+ return r;
+ }
+
+ // -- ALU / logic operations --------------------------------------------
+
+ private void ldaOp(ubyte m) { A = m; setNZ(A); }
+ private void ldxOp(ubyte m) { X = m; setNZ(X); }
+ private void ldyOp(ubyte m) { Y = m; setNZ(Y); }
+ private void oraOp(ubyte m) { A = cast(ubyte) (A | m); setNZ(A); }
+ private void andOp(ubyte m) { A = cast(ubyte) (A & m); setNZ(A); }
+ private void eorOp(ubyte m) { A = cast(ubyte) (A ^ m); setNZ(A); }
+ private void cmpOp(ubyte m) { setC(A >= m); setNZ(cast(ubyte) (A - m)); }
+ private void cpxOp(ubyte m) { setC(X >= m); setNZ(cast(ubyte) (X - m)); }
+ private void cpyOp(ubyte m) { setC(Y >= m); setNZ(cast(ubyte) (Y - m)); }
+
+ private void bitOp(ubyte m)
+ {
+ setF(F_Z, (A & m) == 0);
+ setF(F_N, (m & 0x80) != 0);
+ setF(F_V, (m & 0x40) != 0);
+ }
+
+ private void adcOp(ubyte m)
+ {
+ if (getF(F_D))
+ adcDec(m);
+ else
+ adcBin(m);
+ }
+
+ private void sbcOp(ubyte m)
+ {
+ if (getF(F_D))
+ sbcDec(m);
+ else
+ sbcBin(m);
+ }
+
+ private void adcBin(ubyte m)
+ {
+ uint a = A;
+ uint b = m;
+ uint c = getF(F_C) ? 1 : 0;
+ uint sum = a + b + c;
+ bool v = ((~(a ^ b)) & (a ^ sum) & 0x80) != 0;
+ A = cast(ubyte) (sum & 0xFF);
+ setC(sum > 0xFF);
+ setV(v);
+ setNZ(A);
+ }
+
+ private void sbcBin(ubyte m)
+ {
+ uint a = A;
+ uint b = m;
+ uint c = getF(F_C) ? 1 : 0;
+ uint inv = b ^ 0xFF;
+ uint sum = a + inv + c;
+ bool v = ((~(a ^ inv)) & (a ^ sum) & 0x80) != 0;
+ A = cast(ubyte) (sum & 0xFF);
+ setC(sum > 0xFF);
+ setV(v);
+ setNZ(A);
+ }
+
+ private void adcDec(ubyte m)
+ {
+ uint a = A;
+ uint b = m;
+ uint carry = getF(F_C) ? 1 : 0;
+ uint sum = a + b + carry;
+ uint lo = (a & 0x0F) + (b & 0x0F) + carry;
+ if (lo > 0x09)
+ sum += 0x06;
+ bool c = sum > 0x99;
+ if (c)
+ sum += 0x60;
+ A = cast(ubyte) (sum & 0xFF);
+ setC(c);
+ setZ(A == 0);
+ setN((A & 0x80) != 0);
+ uint bin = a + b + carry;
+ setV((~(a ^ b) & (a ^ bin) & 0x80) != 0);
+ }
+
+ private void sbcDec(ubyte m)
+ {
+ int a = A;
+ int b = m;
+ int borrow = getF(F_C) ? 0 : 1;
+ int result = a - b - borrow;
+ bool c = result >= 0;
+ if (result < 0)
+ result += 0x100;
+ uint res = cast(uint) result;
+ uint lo = res & 0x0F;
+ if (lo >= 0x0A)
+ res -= 0x06;
+ if (res >= 0xA0)
+ res -= 0x60;
+ A = cast(ubyte) (res & 0xFF);
+ setC(c);
+ setZ(A == 0);
+ setN((A & 0x80) != 0);
+ uint bin = cast(uint) (a - b - borrow) & 0xFF;
+ setV(((a ^ b) & (a ^ bin) & 0x80) != 0);
+ }
+
+ // -- read-modify-write value helpers -----------------------------------
+
+ private ubyte aslVal(ubyte v)
+ {
+ setC((v & 0x80) != 0);
+ ubyte r = cast(ubyte) (v << 1);
+ setNZ(r);
+ return r;
+ }
+
+ private ubyte lsrVal(ubyte v)
+ {
+ setC((v & 0x01) != 0);
+ ubyte r = cast(ubyte) (v >> 1);
+ setNZ(r);
+ return r;
+ }
+
+ private ubyte rolVal(ubyte v)
+ {
+ bool c = (v & 0x80) != 0;
+ ubyte r = cast(ubyte) ((v << 1) | (getF(F_C) ? 1 : 0));
+ setC(c);
+ setNZ(r);
+ return r;
+ }
+
+ private ubyte rorVal(ubyte v)
+ {
+ bool c = (v & 0x01) != 0;
+ ubyte r = cast(ubyte) ((v >> 1) | (getF(F_C) ? 0x80 : 0));
+ setC(c);
+ setNZ(r);
+ return r;
+ }
+
+ private ubyte incVal(ubyte v)
+ {
+ ubyte r = cast(ubyte) (v + 1);
+ setNZ(r);
+ return r;
+ }
+
+ private ubyte decVal(ubyte v)
+ {
+ ubyte r = cast(ubyte) (v - 1);
+ setNZ(r);
+ return r;
+ }
+
+ // -- branches / jumps / interrupts -------------------------------------
+
+ private void branch(bool cond)
+ {
+ int off = cast(byte) fetch();
+ if (cond)
+ {
+ cycles += 1;
+ ushort old = PC;
+ PC = cast(ushort) (PC + off);
+ if ((old & 0xFF00) != (PC & 0xFF00))
+ cycles += 1;
+ }
+ }
+
+ private void brk()
+ {
+ ushort ret = cast(ushort) (PC + 1);
+ push(cast(ubyte) (ret >> 8));
+ push(cast(ubyte) (ret & 0xFF));
+ push(cast(ubyte) (P | F_B | F_U));
+ setF(F_I, true);
+ PC = read16(0xFFFE);
+ cycles += 7;
+ }
+
+ private void jsr()
+ {
+ ushort target = fetch16();
+ ushort ret = cast(ushort) (PC - 1);
+ push(cast(ubyte) (ret >> 8));
+ push(cast(ubyte) (ret & 0xFF));
+ PC = target;
+ cycles += 6;
+ }
+
+ private void rts()
+ {
+ ubyte lo = pop();
+ ubyte hi = pop();
+ PC = cast(ushort) (((hi << 8) | lo) + 1);
+ cycles += 6;
+ }
+
+ private void rti()
+ {
+ setPFromStack(pop());
+ ubyte lo = pop();
+ ubyte hi = pop();
+ PC = cast(ushort) ((hi << 8) | lo);
+ cycles += 6;
+ }
+
+ private void jmpInd()
+ {
+ ushort ptr = fetch16();
+ ushort lo = bus.read(ptr);
+ ushort hi = bus.read(cast(ushort) ((ptr & 0xFF00) | ((ptr + 1) & 0xFF)));
+ PC = cast(ushort) ((hi << 8) | lo);
+ cycles += 5;
+ }
+
+ // -- execute exactly one instruction -----------------------------------
+
+ void step()
+ {
+ ubyte op = fetch();
+ switch (op)
+ {
+ // BRK
+ case 0x00: brk(); break;
+ // ORA
+ case 0x01: cycles += 6; oraOp(bus.read(addrIzx())); break;
+ case 0x05: cycles += 3; oraOp(bus.read(addrZP())); break;
+ case 0x09: cycles += 2; oraOp(fetch()); break;
+ case 0x0D: cycles += 4; oraOp(bus.read(addrAbs())); break;
+ case 0x11: cycles += 5; { bool x; ushort a = addrIzy(x); if (x) cycles++; oraOp(bus.read(a)); } break;
+ case 0x15: cycles += 4; oraOp(bus.read(addrZPX())); break;
+ case 0x19: cycles += 4; { bool x; ushort a = addrAbsY(x); if (x) cycles++; oraOp(bus.read(a)); } break;
+ case 0x1D: cycles += 4; { bool x; ushort a = addrAbsX(x); if (x) cycles++; oraOp(bus.read(a)); } break;
+ // ASL
+ case 0x06: cycles += 5; { ushort a = addrZP(); bus.write(a, aslVal(bus.read(a))); } break;
+ case 0x0A: cycles += 2; A = aslVal(A); break;
+ case 0x0E: cycles += 6; { ushort a = addrAbs(); bus.write(a, aslVal(bus.read(a))); } break;
+ case 0x16: cycles += 6; { ushort a = addrZPX(); bus.write(a, aslVal(bus.read(a))); } break;
+ case 0x1E: cycles += 7; { bool x; ushort a = addrAbsX(x); bus.write(a, aslVal(bus.read(a))); } break;
+ // PHP
+ case 0x08: cycles += 3; push(cast(ubyte) (P | F_B | F_U)); break;
+ // BPL
+ case 0x10: cycles += 2; branch(!getF(F_N)); break;
+ // CLC
+ case 0x18: cycles += 2; setC(false); break;
+ // JSR
+ case 0x20: jsr(); break;
+ // AND
+ case 0x21: cycles += 6; andOp(bus.read(addrIzx())); break;
+ case 0x25: cycles += 3; andOp(bus.read(addrZP())); break;
+ case 0x29: cycles += 2; andOp(fetch()); break;
+ case 0x2D: cycles += 4; andOp(bus.read(addrAbs())); break;
+ case 0x31: cycles += 5; { bool x; ushort a = addrIzy(x); if (x) cycles++; andOp(bus.read(a)); } break;
+ case 0x35: cycles += 4; andOp(bus.read(addrZPX())); break;
+ case 0x39: cycles += 4; { bool x; ushort a = addrAbsY(x); if (x) cycles++; andOp(bus.read(a)); } break;
+ case 0x3D: cycles += 4; { bool x; ushort a = addrAbsX(x); if (x) cycles++; andOp(bus.read(a)); } break;
+ // BIT
+ case 0x24: cycles += 3; bitOp(bus.read(addrZP())); break;
+ case 0x2C: cycles += 4; bitOp(bus.read(addrAbs())); break;
+ // ROL
+ case 0x26: cycles += 5; { ushort a = addrZP(); bus.write(a, rolVal(bus.read(a))); } break;
+ case 0x2A: cycles += 2; A = rolVal(A); break;
+ case 0x2E: cycles += 6; { ushort a = addrAbs(); bus.write(a, rolVal(bus.read(a))); } break;
+ case 0x36: cycles += 6; { ushort a = addrZPX(); bus.write(a, rolVal(bus.read(a))); } break;
+ case 0x3E: cycles += 7; { bool x; ushort a = addrAbsX(x); bus.write(a, rolVal(bus.read(a))); } break;
+ // PLP
+ case 0x28: cycles += 4; setPFromStack(pop()); break;
+ // BMI
+ case 0x30: cycles += 2; branch(getF(F_N)); break;
+ // SEC
+ case 0x38: cycles += 2; setC(true); break;
+ // RTI
+ case 0x40: rti(); break;
+ // EOR
+ case 0x41: cycles += 6; eorOp(bus.read(addrIzx())); break;
+ case 0x45: cycles += 3; eorOp(bus.read(addrZP())); break;
+ case 0x49: cycles += 2; eorOp(fetch()); break;
+ case 0x4D: cycles += 4; eorOp(bus.read(addrAbs())); break;
+ case 0x51: cycles += 5; { bool x; ushort a = addrIzy(x); if (x) cycles++; eorOp(bus.read(a)); } break;
+ case 0x55: cycles += 4; eorOp(bus.read(addrZPX())); break;
+ case 0x59: cycles += 4; { bool x; ushort a = addrAbsY(x); if (x) cycles++; eorOp(bus.read(a)); } break;
+ case 0x5D: cycles += 4; { bool x; ushort a = addrAbsX(x); if (x) cycles++; eorOp(bus.read(a)); } break;
+ // LSR
+ case 0x46: cycles += 5; { ushort a = addrZP(); bus.write(a, lsrVal(bus.read(a))); } break;
+ case 0x4A: cycles += 2; A = lsrVal(A); break;
+ case 0x4E: cycles += 6; { ushort a = addrAbs(); bus.write(a, lsrVal(bus.read(a))); } break;
+ case 0x56: cycles += 6; { ushort a = addrZPX(); bus.write(a, lsrVal(bus.read(a))); } break;
+ case 0x5E: cycles += 7; { bool x; ushort a = addrAbsX(x); bus.write(a, lsrVal(bus.read(a))); } break;
+ // PHA
+ case 0x48: cycles += 3; push(A); break;
+ // JMP
+ case 0x4C: cycles += 3; PC = fetch16(); break;
+ case 0x6C: jmpInd(); break;
+ // BVC
+ case 0x50: cycles += 2; branch(!getF(F_V)); break;
+ // CLI
+ case 0x58: cycles += 2; setF(F_I, false); break;
+ // RTS
+ case 0x60: rts(); break;
+ // ADC
+ case 0x61: cycles += 6; adcOp(bus.read(addrIzx())); break;
+ case 0x65: cycles += 3; adcOp(bus.read(addrZP())); break;
+ case 0x69: cycles += 2; adcOp(fetch()); break;
+ case 0x6D: cycles += 4; adcOp(bus.read(addrAbs())); break;
+ case 0x71: cycles += 5; { bool x; ushort a = addrIzy(x); if (x) cycles++; adcOp(bus.read(a)); } break;
+ case 0x75: cycles += 4; adcOp(bus.read(addrZPX())); break;
+ case 0x79: cycles += 4; { bool x; ushort a = addrAbsY(x); if (x) cycles++; adcOp(bus.read(a)); } break;
+ case 0x7D: cycles += 4; { bool x; ushort a = addrAbsX(x); if (x) cycles++; adcOp(bus.read(a)); } break;
+ // ROR
+ case 0x66: cycles += 5; { ushort a = addrZP(); bus.write(a, rorVal(bus.read(a))); } break;
+ case 0x6A: cycles += 2; A = rorVal(A); break;
+ case 0x6E: cycles += 6; { ushort a = addrAbs(); bus.write(a, rorVal(bus.read(a))); } break;
+ case 0x76: cycles += 6; { ushort a = addrZPX(); bus.write(a, rorVal(bus.read(a))); } break;
+ case 0x7E: cycles += 7; { bool x; ushort a = addrAbsX(x); bus.write(a, rorVal(bus.read(a))); } break;
+ // PLA
+ case 0x68: cycles += 4; A = pop(); setNZ(A); break;
+ // BVS
+ case 0x70: cycles += 2; branch(getF(F_V)); break;
+ // SEI
+ case 0x78: cycles += 2; setF(F_I, true); break;
+ // STA
+ case 0x81: cycles += 6; bus.write(addrIzx(), A); break;
+ case 0x85: cycles += 3; bus.write(addrZP(), A); break;
+ case 0x8D: cycles += 4; bus.write(addrAbs(), A); break;
+ case 0x91: cycles += 6; { bool x; ushort a = addrIzy(x); bus.write(a, A); } break;
+ case 0x95: cycles += 4; bus.write(addrZPX(), A); break;
+ case 0x99: cycles += 5; { bool x; ushort a = addrAbsY(x); bus.write(a, A); } break;
+ case 0x9D: cycles += 5; { bool x; ushort a = addrAbsX(x); bus.write(a, A); } break;
+ // STY
+ case 0x84: cycles += 3; bus.write(addrZP(), Y); break;
+ case 0x8C: cycles += 4; bus.write(addrAbs(), Y); break;
+ case 0x94: cycles += 4; bus.write(addrZPX(), Y); break;
+ // STX
+ case 0x86: cycles += 3; bus.write(addrZP(), X); break;
+ case 0x8E: cycles += 4; bus.write(addrAbs(), X); break;
+ case 0x96: cycles += 4; bus.write(addrZPY(), X); break;
+ // DEY
+ case 0x88: cycles += 2; Y = cast(ubyte) (Y - 1); setNZ(Y); break;
+ // TXA
+ case 0x8A: cycles += 2; A = X; setNZ(A); break;
+ // BCC
+ case 0x90: cycles += 2; branch(!getF(F_C)); break;
+ // TYA
+ case 0x98: cycles += 2; A = Y; setNZ(A); break;
+ // TXS
+ case 0x9A: cycles += 2; SP = X; break;
+ // LDY
+ case 0xA0: cycles += 2; ldyOp(fetch()); break;
+ case 0xA4: cycles += 3; ldyOp(bus.read(addrZP())); break;
+ case 0xAC: cycles += 4; ldyOp(bus.read(addrAbs())); break;
+ case 0xB4: cycles += 4; ldyOp(bus.read(addrZPX())); break;
+ case 0xBC: cycles += 4; { bool x; ushort a = addrAbsX(x); if (x) cycles++; ldyOp(bus.read(a)); } break;
+ // LDA
+ case 0xA1: cycles += 6; ldaOp(bus.read(addrIzx())); break;
+ case 0xA5: cycles += 3; ldaOp(bus.read(addrZP())); break;
+ case 0xA9: cycles += 2; ldaOp(fetch()); break;
+ case 0xAD: cycles += 4; ldaOp(bus.read(addrAbs())); break;
+ case 0xB1: cycles += 5; { bool x; ushort a = addrIzy(x); if (x) cycles++; ldaOp(bus.read(a)); } break;
+ case 0xB5: cycles += 4; ldaOp(bus.read(addrZPX())); break;
+ case 0xB9: cycles += 4; { bool x; ushort a = addrAbsY(x); if (x) cycles++; ldaOp(bus.read(a)); } break;
+ case 0xBD: cycles += 4; { bool x; ushort a = addrAbsX(x); if (x) cycles++; ldaOp(bus.read(a)); } break;
+ // LDX
+ case 0xA2: cycles += 2; ldxOp(fetch()); break;
+ case 0xA6: cycles += 3; ldxOp(bus.read(addrZP())); break;
+ case 0xAE: cycles += 4; ldxOp(bus.read(addrAbs())); break;
+ case 0xB6: cycles += 4; ldxOp(bus.read(addrZPY())); break;
+ case 0xBE: cycles += 4; { bool x; ushort a = addrAbsY(x); if (x) cycles++; ldxOp(bus.read(a)); } break;
+ // TAY
+ case 0xA8: cycles += 2; Y = A; setNZ(Y); break;
+ // TAX
+ case 0xAA: cycles += 2; X = A; setNZ(X); break;
+ // BCS
+ case 0xB0: cycles += 2; branch(getF(F_C)); break;
+ // CLV
+ case 0xB8: cycles += 2; setV(false); break;
+ // TSX
+ case 0xBA: cycles += 2; X = SP; setNZ(X); break;
+ // CPY
+ case 0xC0: cycles += 2; cpyOp(fetch()); break;
+ case 0xC4: cycles += 3; cpyOp(bus.read(addrZP())); break;
+ case 0xCC: cycles += 4; cpyOp(bus.read(addrAbs())); break;
+ // CMP
+ case 0xC1: cycles += 6; cmpOp(bus.read(addrIzx())); break;
+ case 0xC5: cycles += 3; cmpOp(bus.read(addrZP())); break;
+ case 0xC9: cycles += 2; cmpOp(fetch()); break;
+ case 0xCD: cycles += 4; cmpOp(bus.read(addrAbs())); break;
+ case 0xD1: cycles += 5; { bool x; ushort a = addrIzy(x); if (x) cycles++; cmpOp(bus.read(a)); } break;
+ case 0xD5: cycles += 4; cmpOp(bus.read(addrZPX())); break;
+ case 0xD9: cycles += 4; { bool x; ushort a = addrAbsY(x); if (x) cycles++; cmpOp(bus.read(a)); } break;
+ case 0xDD: cycles += 4; { bool x; ushort a = addrAbsX(x); if (x) cycles++; cmpOp(bus.read(a)); } break;
+ // DEC
+ case 0xC6: cycles += 5; { ushort a = addrZP(); bus.write(a, decVal(bus.read(a))); } break;
+ case 0xCE: cycles += 6; { ushort a = addrAbs(); bus.write(a, decVal(bus.read(a))); } break;
+ case 0xD6: cycles += 6; { ushort a = addrZPX(); bus.write(a, decVal(bus.read(a))); } break;
+ case 0xDE: cycles += 7; { bool x; ushort a = addrAbsX(x); bus.write(a, decVal(bus.read(a))); } break;
+ // INY
+ case 0xC8: cycles += 2; Y = cast(ubyte) (Y + 1); setNZ(Y); break;
+ // DEX
+ case 0xCA: cycles += 2; X = cast(ubyte) (X - 1); setNZ(X); break;
+ // BNE
+ case 0xD0: cycles += 2; branch(!getF(F_Z)); break;
+ // CLD
+ case 0xD8: cycles += 2; setF(F_D, false); break;
+ // CPX
+ case 0xE0: cycles += 2; cpxOp(fetch()); break;
+ case 0xE4: cycles += 3; cpxOp(bus.read(addrZP())); break;
+ case 0xEC: cycles += 4; cpxOp(bus.read(addrAbs())); break;
+ // SBC
+ case 0xE1: cycles += 6; sbcOp(bus.read(addrIzx())); break;
+ case 0xE5: cycles += 3; sbcOp(bus.read(addrZP())); break;
+ case 0xE9: cycles += 2; sbcOp(fetch()); break;
+ case 0xED: cycles += 4; sbcOp(bus.read(addrAbs())); break;
+ case 0xF1: cycles += 5; { bool x; ushort a = addrIzy(x); if (x) cycles++; sbcOp(bus.read(a)); } break;
+ case 0xF5: cycles += 4; sbcOp(bus.read(addrZPX())); break;
+ case 0xF9: cycles += 4; { bool x; ushort a = addrAbsY(x); if (x) cycles++; sbcOp(bus.read(a)); } break;
+ case 0xFD: cycles += 4; { bool x; ushort a = addrAbsX(x); if (x) cycles++; sbcOp(bus.read(a)); } break;
+ // INC
+ case 0xE6: cycles += 5; { ushort a = addrZP(); bus.write(a, incVal(bus.read(a))); } break;
+ case 0xEE: cycles += 6; { ushort a = addrAbs(); bus.write(a, incVal(bus.read(a))); } break;
+ case 0xF6: cycles += 6; { ushort a = addrZPX(); bus.write(a, incVal(bus.read(a))); } break;
+ case 0xFE: cycles += 7; { bool x; ushort a = addrAbsX(x); bus.write(a, incVal(bus.read(a))); } break;
+ // INX
+ case 0xE8: cycles += 2; X = cast(ubyte) (X + 1); setNZ(X); break;
+ // NOP
+ case 0xEA: cycles += 2; break;
+ // BEQ
+ case 0xF0: cycles += 2; branch(getF(F_Z)); break;
+ // SED
+ case 0xF8: cycles += 2; setF(F_D, true); break;
+
+ default:
+ // Undocumented/illegal opcode: treat as a 1-byte NOP.
+ cycles += 2;
+ break;
+ }
+ }
+
+ // -- disassembly (for --trace) -----------------------------------------
+
+ string disasm(ushort pc)
+ {
+ ubyte op = bus.read(pc);
+ Am mode = opMode(op);
+ string mn = opMnemonic(op);
+ if (mn == "???")
+ return "???";
+
+ auto rb = (int off) => bus.read(cast(ushort) ((pc + off) & 0xFFFF));
+ auto rw = (int off) => cast(ushort) (rb(off) | (rb(off + 1) << 8));
+
+ string operands;
+ switch (mode)
+ {
+ case Am.imp: operands = ""; break;
+ case Am.acc: operands = "A"; break;
+ case Am.imm: operands = format("#$%02X", rb(1)); break;
+ case Am.zp: operands = format("$%02X", rb(1)); break;
+ case Am.zpx: operands = format("$%02X,X", rb(1)); break;
+ case Am.zpy: operands = format("$%02X,Y", rb(1)); break;
+ case Am.abs: operands = format("$%04X", rw(1)); break;
+ case Am.abx: operands = format("$%04X,X", rw(1)); break;
+ case Am.aby: operands = format("$%04X,Y", rw(1)); break;
+ case Am.ind: operands = format("($%04X)", rw(1)); break;
+ case Am.izx: operands = format("($%02X,X)", rb(1)); break;
+ case Am.izy: operands = format("($%02X),Y", rb(1)); break;
+ case Am.rel:
+ {
+ int off = cast(byte) rb(1);
+ operands = format("$%04X", cast(ushort) (pc + 2 + off));
+ break;
+ }
+ default: operands = ""; break;
+ }
+ return operands.length ? (mn ~ " " ~ operands) : mn;
+ }
+
+ string regs() const
+ {
+ return format("A=%02X X=%02X Y=%02X SP=%02X P=%02X", A, X, Y, SP, P);
+ }
+}
+
+// Addressing modes for disassembly only.
+private enum Am : ubyte { imp, acc, imm, zp, zpx, zpy, abs, abx, aby, ind, izx, izy, rel, ill }
+
+private static string[256] mnTab;
+private static Am[256] modeTab;
+
+private string opMnemonic(ubyte op)
+{
+ auto s = mnTab[op];
+ return s.length ? s : "???";
+}
+
+private Am opMode(ubyte op)
+{
+ return modeTab[op];
+}
+
+shared static this()
+{
+ void d(ubyte op, string mn, Am m)
+ {
+ mnTab[op] = mn;
+ modeTab[op] = m;
+ }
+
+ d(0x00, "BRK", Am.imp);
+ d(0x01, "ORA", Am.izx);
+ d(0x05, "ORA", Am.zp);
+ d(0x06, "ASL", Am.zp);
+ d(0x08, "PHP", Am.imp);
+ d(0x09, "ORA", Am.imm);
+ d(0x0A, "ASL", Am.acc);
+ d(0x0D, "ORA", Am.abs);
+ d(0x0E, "ASL", Am.abs);
+ d(0x10, "BPL", Am.rel);
+ d(0x11, "ORA", Am.izy);
+ d(0x15, "ORA", Am.zpx);
+ d(0x16, "ASL", Am.zpx);
+ d(0x18, "CLC", Am.imp);
+ d(0x19, "ORA", Am.aby);
+ d(0x1D, "ORA", Am.abx);
+ d(0x1E, "ASL", Am.abx);
+ d(0x20, "JSR", Am.abs);
+ d(0x21, "AND", Am.izx);
+ d(0x24, "BIT", Am.zp);
+ d(0x25, "AND", Am.zp);
+ d(0x26, "ROL", Am.zp);
+ d(0x28, "PLP", Am.imp);
+ d(0x29, "AND", Am.imm);
+ d(0x2A, "ROL", Am.acc);
+ d(0x2C, "BIT", Am.abs);
+ d(0x2D, "AND", Am.abs);
+ d(0x2E, "ROL", Am.abs);
+ d(0x30, "BMI", Am.rel);
+ d(0x31, "AND", Am.izy);
+ d(0x35, "AND", Am.zpx);
+ d(0x36, "ROL", Am.zpx);
+ d(0x38, "SEC", Am.imp);
+ d(0x39, "AND", Am.aby);
+ d(0x3D, "AND", Am.abx);
+ d(0x3E, "ROL", Am.abx);
+ d(0x40, "RTI", Am.imp);
+ d(0x41, "EOR", Am.izx);
+ d(0x45, "EOR", Am.zp);
+ d(0x46, "LSR", Am.zp);
+ d(0x48, "PHA", Am.imp);
+ d(0x49, "EOR", Am.imm);
+ d(0x4A, "LSR", Am.acc);
+ d(0x4C, "JMP", Am.abs);
+ d(0x4D, "EOR", Am.abs);
+ d(0x4E, "LSR", Am.abs);
+ d(0x50, "BVC", Am.rel);
+ d(0x51, "EOR", Am.izy);
+ d(0x55, "EOR", Am.zpx);
+ d(0x56, "LSR", Am.zpx);
+ d(0x58, "CLI", Am.imp);
+ d(0x59, "EOR", Am.aby);
+ d(0x5D, "EOR", Am.abx);
+ d(0x5E, "LSR", Am.abx);
+ d(0x60, "RTS", Am.imp);
+ d(0x61, "ADC", Am.izx);
+ d(0x65, "ADC", Am.zp);
+ d(0x66, "ROR", Am.zp);
+ d(0x68, "PLA", Am.imp);
+ d(0x69, "ADC", Am.imm);
+ d(0x6A, "ROR", Am.acc);
+ d(0x6C, "JMP", Am.ind);
+ d(0x6D, "ADC", Am.abs);
+ d(0x6E, "ROR", Am.abs);
+ d(0x70, "BVS", Am.rel);
+ d(0x71, "ADC", Am.izy);
+ d(0x75, "ADC", Am.zpx);
+ d(0x76, "ROR", Am.zpx);
+ d(0x78, "SEI", Am.imp);
+ d(0x79, "ADC", Am.aby);
+ d(0x7D, "ADC", Am.abx);
+ d(0x7E, "ROR", Am.abx);
+ d(0x81, "STA", Am.izx);
+ d(0x84, "STY", Am.zp);
+ d(0x85, "STA", Am.zp);
+ d(0x86, "STX", Am.zp);
+ d(0x88, "DEY", Am.imp);
+ d(0x8A, "TXA", Am.imp);
+ d(0x8C, "STY", Am.abs);
+ d(0x8D, "STA", Am.abs);
+ d(0x8E, "STX", Am.abs);
+ d(0x90, "BCC", Am.rel);
+ d(0x91, "STA", Am.izy);
+ d(0x94, "STY", Am.zpx);
+ d(0x95, "STA", Am.zpx);
+ d(0x96, "STX", Am.zpy);
+ d(0x98, "TYA", Am.imp);
+ d(0x99, "STA", Am.aby);
+ d(0x9A, "TXS", Am.imp);
+ d(0x9D, "STA", Am.abx);
+ d(0xA0, "LDY", Am.imm);
+ d(0xA1, "LDA", Am.izx);
+ d(0xA2, "LDX", Am.imm);
+ d(0xA4, "LDY", Am.zp);
+ d(0xA5, "LDA", Am.zp);
+ d(0xA6, "LDX", Am.zp);
+ d(0xA8, "TAY", Am.imp);
+ d(0xA9, "LDA", Am.imm);
+ d(0xAA, "TAX", Am.imp);
+ d(0xAC, "LDY", Am.abs);
+ d(0xAD, "LDA", Am.abs);
+ d(0xAE, "LDX", Am.abs);
+ d(0xB0, "BCS", Am.rel);
+ d(0xB1, "LDA", Am.izy);
+ d(0xB4, "LDY", Am.zpx);
+ d(0xB5, "LDA", Am.zpx);
+ d(0xB6, "LDX", Am.zpy);
+ d(0xB8, "CLV", Am.imp);
+ d(0xB9, "LDA", Am.aby);
+ d(0xBA, "TSX", Am.imp);
+ d(0xBC, "LDY", Am.abx);
+ d(0xBD, "LDA", Am.abx);
+ d(0xBE, "LDX", Am.aby);
+ d(0xC0, "CPY", Am.imm);
+ d(0xC1, "CMP", Am.izx);
+ d(0xC4, "CPY", Am.zp);
+ d(0xC5, "CMP", Am.zp);
+ d(0xC6, "DEC", Am.zp);
+ d(0xC8, "INY", Am.imp);
+ d(0xC9, "CMP", Am.imm);
+ d(0xCA, "DEX", Am.imp);
+ d(0xCC, "CPY", Am.abs);
+ d(0xCD, "CMP", Am.abs);
+ d(0xCE, "DEC", Am.abs);
+ d(0xD0, "BNE", Am.rel);
+ d(0xD1, "CMP", Am.izy);
+ d(0xD5, "CMP", Am.zpx);
+ d(0xD6, "DEC", Am.zpx);
+ d(0xD8, "CLD", Am.imp);
+ d(0xD9, "CMP", Am.aby);
+ d(0xDD, "CMP", Am.abx);
+ d(0xDE, "DEC", Am.abx);
+ d(0xE0, "CPX", Am.imm);
+ d(0xE1, "SBC", Am.izx);
+ d(0xE4, "CPX", Am.zp);
+ d(0xE5, "SBC", Am.zp);
+ d(0xE6, "INC", Am.zp);
+ d(0xE8, "INX", Am.imp);
+ d(0xE9, "SBC", Am.imm);
+ d(0xEA, "NOP", Am.imp);
+ d(0xEC, "CPX", Am.abs);
+ d(0xED, "SBC", Am.abs);
+ d(0xEE, "INC", Am.abs);
+ d(0xF0, "BEQ", Am.rel);
+ d(0xF1, "SBC", Am.izy);
+ d(0xF5, "SBC", Am.zpx);
+ d(0xF6, "INC", Am.zpx);
+ d(0xF8, "SED", Am.imp);
+ d(0xF9, "SBC", Am.aby);
+ d(0xFD, "SBC", Am.abx);
+ d(0xFE, "INC", Am.abx);
+}
+
+// ---------------------------------------------------------------------------
+// unit tests
+// ---------------------------------------------------------------------------
+
+version (unittest)
+{
+ private Cpu testCpu(const(ubyte)[] prog, ushort addr = 0x0200)
+ {
+ auto b = new Bus();
+ b.loadAt(addr, prog);
+ auto c = new Cpu(b);
+ c.reset();
+ c.setPC(addr);
+ return c;
+ }
+}
+
+unittest
+{
+ // LDA #$42 sets A, clears Z and N
+ auto c = testCpu([0xA9, 0x42]);
+ c.step();
+ assert(c.A == 0x42);
+ assert((c.P & F_Z) == 0);
+ assert((c.P & F_N) == 0);
+ assert(c.cycles == 2);
+
+ // LDA #$00 sets Z
+ auto c2 = testCpu([0xA9, 0x00]);
+ c2.step();
+ assert(c2.A == 0x00);
+ assert((c2.P & F_Z) != 0);
+}
+
+unittest
+{
+ // $7F + $01 = $80: V set, C clear
+ auto c = testCpu([0xA9, 0x7F, 0x69, 0x01]);
+ c.step(); // LDA #$7F
+ c.step(); // ADC #$01
+ assert(c.A == 0x80);
+ assert((c.P & F_V) != 0);
+ assert((c.P & F_C) == 0);
+
+ // $FF + $01 = $00: C set, Z set
+ auto c2 = testCpu([0xA9, 0xFF, 0x69, 0x01]);
+ c2.step();
+ c2.step();
+ assert(c2.A == 0x00);
+ assert((c2.P & F_C) != 0);
+ assert((c2.P & F_Z) != 0);
+}
+
+unittest
+{
+ // SBC: 5 - 3 = 2 with carry set (no borrow)
+ auto c = testCpu([0xA9, 0x05, 0x38, 0xE9, 0x03]);
+ c.step(); // LDA #$05
+ c.step(); // SEC
+ c.step(); // SBC #$03
+ assert(c.A == 0x02);
+ assert((c.P & F_C) != 0);
+ assert((c.P & F_Z) == 0);
+
+ // SBC: 5 - 6 = $FF with borrow (carry clear)
+ auto c2 = testCpu([0xA9, 0x05, 0x38, 0xE9, 0x06]);
+ c2.step();
+ c2.step();
+ c2.step();
+ assert(c2.A == 0xFF);
+ assert((c2.P & F_C) == 0);
+ assert((c2.P & F_N) != 0);
+}
+
+unittest
+{
+ // JSR/RTS round trip
+ // 0200: JSR $0208 (20 08 02)
+ // 0203: LDA #$11 (A9 11)
+ // 0205: BRK? -> instead just leave; RTS returns to 0203
+ // 0208: RTS (60)
+ auto c = testCpu([0x20, 0x08, 0x02, 0xA9, 0x11, 0x00, 0x00, 0x00, 0x60]);
+ c.step(); // JSR -> PC=0x0208, SP=0xFB
+ assert(c.PC == 0x0208);
+ assert(c.SP == 0xFB);
+ c.step(); // RTS -> PC=0x0203
+ assert(c.PC == 0x0203);
+ c.step(); // LDA #$11
+ assert(c.A == 0x11);
+}
+
+unittest
+{
+ // Branch taken (forward) and not-taken
+ // 0200: LDA #$00 (A9 00)
+ // 0202: BNE +2 (D0 02) -> not taken
+ // 0204: LDA #$01 (A9 01)
+ // 0206: NOP
+ auto c = testCpu([0xA9, 0x00, 0xD0, 0x02, 0xA9, 0x01, 0xEA]);
+ c.step(); // LDA #$00
+ c.step(); // BNE not taken -> PC=0x0204
+ assert(c.PC == 0x0204);
+ c.step(); // LDA #$01
+ assert(c.A == 0x01);
+
+ // Backward branch (loop)
+ // 0200: LDA #$03 (A9 03)
+ // 0202: DEX (CA)
+ // 0203: BNE $0202 (D0 FD) -> taken back to 0202
+ auto c2 = testCpu([0xA9, 0x03, 0xCA, 0xD0, 0xFD]);
+ c2.step(); // LDA #$03
+ c2.step(); // DEX -> X=$FF (not zero)
+ c2.step(); // BNE taken -> PC back to 0x0202
+ assert(c2.PC == 0x0202);
+ assert((c2.P & F_Z) == 0);
+}
+
+unittest
+{
+ // Zero-page,X wrap-around: operand $FF with X=1 reads $00
+ auto b = new Bus();
+ b.write(0x0000, 0x77);
+ auto c = new Cpu(b);
+ c.reset();
+ c.setPC(0x0200);
+ b.loadAt(0x0200, [0xB5, 0xFF]); // LDA $FF,X
+ c.X = 1;
+ c.step();
+ assert(c.A == 0x77);
+}
+
+unittest
+{
+ // STA/LDA absolute round trip through the bus
+ auto c = testCpu([0xA9, 0xAB, 0x8D, 0x34, 0x12, 0xA9, 0x00, 0xAD, 0x34, 0x12]);
+ c.step(); // LDA #$AB
+ c.step(); // STA $1234
+ assert(c.bus.read(0x1234) == 0xAB);
+ c.step(); // LDA #$00
+ c.step(); // LDA $1234
+ assert(c.A == 0xAB);
+}
+
+unittest
+{
+ // PHA/PLA and PHP/PLP round trip
+ auto c = testCpu([0xA9, 0x5A, 0x48, 0xA9, 0x00, 0x68]);
+ c.step(); // LDA #$5A
+ c.step(); // PHA
+ assert(c.SP == 0xFC);
+ assert(c.bus.read(0x01FD) == 0x5A);
+ c.step(); // LDA #$00
+ c.step(); // PLA -> A=$5A
+ assert(c.A == 0x5A);
+ assert(c.SP == 0xFD);
+
+ // PHP/PLP: push P (carry set), clear carry, pull -> carry set again
+ auto c2 = testCpu([0x38, 0x08, 0x18, 0x28]);
+ c2.step(); // SEC
+ c2.step(); // PHP
+ c2.step(); // CLC
+ assert((c2.P & F_C) == 0);
+ c2.step(); // PLP -> carry restored
+ assert((c2.P & F_C) != 0);
+}
+
+unittest
+{
+ // A short program that computes something and halts via $F003.
+ // 0200: LDA #$05 (A9 05)
+ // 0202: CLC (18)
+ // 0203: ADC #$03 (69 03) -> A = 8
+ // 0205: STA $40 (85 40)
+ // 0207: STA $F003 (8D 03 F0) -> halt
+ auto c = testCpu([0xA9, 0x05, 0x18, 0x69, 0x03, 0x85, 0x40, 0x8D, 0x03, 0xF0]);
+ while (!c.bus.halted)
+ {
+ c.step();
+ assert(c.cycles < 1000);
+ }
+ assert(c.A == 0x08);
+ assert(c.bus.read(0x0040) == 0x08);
+ assert(c.bus.halted == true);
+}
diff --git a/src/emulator/main.d b/src/emulator/main.d
new file mode 100644
index 0000000..29babfc
--- /dev/null
+++ b/src/emulator/main.d
@@ -0,0 +1,230 @@
+/**
+ * weirdcpu — NMOS 6502 emulator CLI.
+ *
+ * Loads a .wimg image (via wcore.objfmt.readImage) or a raw binary, runs it
+ * until the program halts ($F003 write) or the cycle budget is exhausted.
+ *
+ * Usage: weirdcpu [options]
+ */
+module emulator.main;
+
+import std.stdio : stderr, stdout, stdin, write, writef, writefln, writeln;
+import std.file : read;
+import std.conv : to, ConvException;
+
+import core.sys.posix.unistd : isatty;
+
+import wcore.objfmt;
+import emulator.bus;
+import emulator.cpu;
+
+private void usage()
+{
+ stderr.writeln("weirdcpu — NMOS 6502 emulator");
+ stderr.writeln("Usage: weirdcpu [options] ");
+ stderr.writeln();
+ stderr.writeln("Options:");
+ stderr.writeln(" --raw Load as a raw binary (default: parse .wimg)");
+ stderr.writeln(" --origin Load address for --raw (default 0x0600)");
+ stderr.writeln(" --entry Entry point (default: image entry, or origin for raw)");
+ stderr.writeln(" --max-cycles Cycle budget (default 100000000)");
+ stderr.writeln(" --trace Disassemble each instruction to stderr");
+ stderr.writeln(" --dump Hex-dump memory around load area after halt");
+ stderr.writeln(" -h, --help Show this help");
+}
+
+private bool parseAddr(string s, out ushort v, string opt)
+{
+ long n;
+ try
+ {
+ if (s.length > 2 && (s[0 .. 2] == "0x" || s[0 .. 2] == "0X"))
+ n = to!long(s[2 .. $], 16);
+ else
+ n = to!long(s, 10);
+ }
+ catch (ConvException e)
+ {
+ stderr.writeln("error: invalid value for ", opt, ": ", s);
+ return false;
+ }
+ if (n < 0 || n > 0xFFFF)
+ {
+ stderr.writeln("error: ", opt, " out of range (0..65535): ", s);
+ return false;
+ }
+ v = cast(ushort) n;
+ return true;
+}
+
+private bool parseULong(string s, out ulong v, string opt)
+{
+ ulong n;
+ try
+ {
+ n = to!ulong(s, 10);
+ }
+ catch (ConvException e)
+ {
+ stderr.writeln("error: invalid value for ", opt, ": ", s);
+ return false;
+ }
+ v = n;
+ return true;
+}
+
+private void hexDump(Bus bus, ushort start)
+{
+ const uint count = 256;
+ for (uint i = 0; i < count; i += 16)
+ {
+ ushort a = cast(ushort) (start + i);
+ writef("%04X ", a);
+ foreach (j; 0 .. 16)
+ {
+ ushort aa = cast(ushort) (a + j);
+ writef("%02X ", bus.read(aa));
+ }
+ write(" ");
+ foreach (j; 0 .. 16)
+ {
+ ushort aa = cast(ushort) (a + j);
+ ubyte b = bus.read(aa);
+ char ch = (b >= 32 && b < 127) ? cast(char) b : '.';
+ write(ch);
+ }
+ writeln();
+ }
+}
+
+int main(string[] args)
+{
+ bool raw = false;
+ bool trace = false;
+ bool dump = false;
+ bool hasEntry = false;
+ ushort origin = 0x0600;
+ ushort entry = 0;
+ ulong maxCycles = 100_000_000;
+ string file;
+
+ for (size_t i = 1; i < args.length; i++)
+ {
+ string a = args[i];
+ if (a == "-h" || a == "--help")
+ {
+ usage();
+ return 0;
+ }
+ else if (a == "--raw")
+ raw = true;
+ else if (a == "--trace")
+ trace = true;
+ else if (a == "--dump")
+ dump = true;
+ else if (a == "--origin")
+ {
+ if (++i >= args.length || !parseAddr(args[i], origin, "--origin"))
+ return 1;
+ }
+ else if (a == "--entry")
+ {
+ if (++i >= args.length || !parseAddr(args[i], entry, "--entry"))
+ return 1;
+ hasEntry = true;
+ }
+ else if (a == "--max-cycles")
+ {
+ if (++i >= args.length || !parseULong(args[i], maxCycles, "--max-cycles"))
+ return 1;
+ }
+ else if (a.length > 1 && a[0] == '-')
+ {
+ stderr.writeln("error: unknown option: ", a);
+ usage();
+ return 1;
+ }
+ else
+ file = a;
+ }
+
+ if (file.length == 0)
+ {
+ usage();
+ return 1;
+ }
+
+ Bus bus = new Bus();
+ ushort loadAddr;
+ ushort entryPoint;
+ ubyte[] data;
+
+ if (raw)
+ {
+ try
+ {
+ data = cast(ubyte[]) read(file);
+ }
+ catch (Exception e)
+ {
+ stderr.writeln("error: cannot read ", file, ": ", e.msg);
+ return 1;
+ }
+ loadAddr = origin;
+ entryPoint = hasEntry ? entry : origin;
+ }
+ else
+ {
+ ubyte[] bytes;
+ try
+ {
+ bytes = cast(ubyte[]) read(file);
+ }
+ catch (Exception e)
+ {
+ stderr.writeln("error: cannot read ", file, ": ", e.msg);
+ return 1;
+ }
+ if (bytes.length < 4 || bytes[0] != 'W' || bytes[1] != 'I' || bytes[2] != 'M' || bytes[3] != 'G')
+ {
+ stderr.writeln("error: ", file, " is not a .wimg image (missing WIMG magic); use --raw for raw binaries");
+ return 1;
+ }
+ Image img = readImage(file);
+ loadAddr = img.loadAddr;
+ entryPoint = hasEntry ? entry : img.entry;
+ data = img.data;
+ }
+
+ bus.loadAt(loadAddr, data);
+
+ // Read buffered stdin lazily; never block on a TTY.
+ if (isatty(0) == 0)
+ {
+ ubyte[] input;
+ foreach (ubyte[] chunk; stdin.byChunk(4096))
+ input ~= chunk;
+ bus.setInput(input);
+ }
+
+ Cpu cpu = new Cpu(bus);
+ cpu.reset();
+ cpu.setPC(entryPoint);
+
+ while (!bus.halted)
+ {
+ if (cpu.cycles >= maxCycles)
+ {
+ stderr.writeln("error: cycle budget exceeded (max ", maxCycles, " cycles)");
+ return 2;
+ }
+ if (trace)
+ stderr.writefln("%04X: %-12s %s", cpu.PC, cpu.disasm(cpu.PC), cpu.regs());
+ cpu.step();
+ }
+
+ if (dump)
+ hexDump(bus, loadAddr);
+
+ return 0;
+}
diff --git a/src/linker/linker.d b/src/linker/linker.d
new file mode 100644
index 0000000..bbd6265
--- /dev/null
+++ b/src/linker/linker.d
@@ -0,0 +1,442 @@
+/**
+ * Small 6502 linker for the weirdcpu toolchain.
+ *
+ * Reads one or more `.wo` objects (see `wcore.objfmt`), lays out their
+ * segments, resolves cross-object symbols, applies relocations, and produces
+ * a `.wimg` loadable image plus an optional human-readable symbol map.
+ *
+ * Everything here operates on the in-memory `Object`/`Image` structs defined
+ * in `wcore.objfmt`; the CLI wrapper in `main.d` handles file I/O and argument
+ * parsing.
+ */
+module linker.linker;
+
+import wcore.objfmt;
+import std.algorithm : sort;
+import std.array : join;
+import std.string : format;
+
+/// Disambiguate the object-file `Object` from the root `object.Object` class.
+alias Object = wcore.objfmt.Object;
+
+/// Options controlling layout and entry resolution.
+struct LinkOptions
+{
+ ushort origin = 0x0600; /// base address for relocatable segments
+ string entrySymbol; /// optional: -e symbol
+ bool verbose = false; /// extra diagnostics (currently unused)
+}
+
+/// Result of a link: the loadable image plus a printable symbol map.
+struct LinkResult
+{
+ Image image;
+ string mapText;
+}
+
+/// A segment (or bss reservation) after layout.
+private struct Placed
+{
+ uint base;
+ uint size;
+ string name; /// for diagnostics
+}
+
+/// A symbol line in the map, for sorting.
+private struct SymLine
+{
+ uint address;
+ string name;
+}
+
+/// Link `objects` into a loadable image.
+LinkResult link(Object[] objects, LinkOptions opts = LinkOptions.init)
+{
+ // ------------------------------------------------------------------
+ // 1. Place segments.
+ // ------------------------------------------------------------------
+ uint cursor = opts.origin;
+ Placed[] placed;
+ uint[][] segBase = new uint[][objects.length];
+
+ foreach (objIdx, ref obj; objects)
+ {
+ segBase[objIdx] = new uint[obj.segments.length];
+ foreach (segIdx, ref seg; obj.segments)
+ {
+ uint size = cast(uint) seg.data.length;
+ uint base;
+
+ if (seg.origin != 0)
+ base = seg.origin;
+ else
+ {
+ base = cursor;
+ cursor += size;
+ if (cursor > 0x10000)
+ throw new Exception(format(
+ "image exceeds 64 KiB while placing segment '%s'", seg.name));
+ }
+
+ if (base + size > 0x10000)
+ throw new Exception(format(
+ "segment '%s' exceeds 64 KiB (0x%04X..0x%04X)", seg.name, base, base + size));
+
+ foreach (p; placed)
+ {
+ if (size > 0 && base < p.base + p.size && p.base < base + size)
+ throw new Exception(format(
+ "segment '%s' at 0x%04X overlaps segment '%s' at 0x%04X",
+ seg.name, base, p.name, p.base));
+ }
+
+ placed ~= Placed(base, size, format("obj%d:%s", objIdx, seg.name));
+ segBase[objIdx][segIdx] = base;
+ }
+ }
+
+ // ------------------------------------------------------------------
+ // 2. Compute defined-symbol addresses; build the global symbol table.
+ // ------------------------------------------------------------------
+ uint[][] symAddr = new uint[][objects.length];
+ uint[string] globalAddr; /// name -> address for defined global symbols
+ uint[string] allDefined; /// name -> address for any defined symbol (entry fallback)
+
+ foreach (objIdx, ref obj; objects)
+ {
+ symAddr[objIdx] = new uint[obj.symbols.length];
+ foreach (symIdx, ref sym; obj.symbols)
+ {
+ if (!sym.isDefined())
+ continue;
+
+ if (sym.seg != NoIndex && sym.seg >= obj.segments.length)
+ throw new Exception(format(
+ "symbol '%s' references invalid segment %d", sym.name, sym.seg));
+
+ uint a = (sym.seg != NoIndex)
+ ? segBase[objIdx][sym.seg] + cast(uint) sym.value
+ : cast(uint) sym.value;
+ symAddr[objIdx][symIdx] = a;
+
+ if (sym.isGlobal())
+ {
+ if (auto p = sym.name in globalAddr)
+ {
+ if (*p != a)
+ throw new Exception(format(
+ "duplicate global symbol '%s' defined at 0x%04X and 0x%04X",
+ sym.name, *p, a));
+ }
+ else
+ globalAddr[sym.name] = a;
+ }
+
+ if (sym.name !in allDefined)
+ allDefined[sym.name] = a;
+ }
+ }
+
+ // ------------------------------------------------------------------
+ // 3. Resolve relocations.
+ // ------------------------------------------------------------------
+ foreach (objIdx, ref obj; objects)
+ {
+ foreach (rel; obj.relocs)
+ {
+ if (rel.seg >= obj.segments.length)
+ throw new Exception(format("relocation references invalid segment %d", rel.seg));
+ if (rel.sym >= obj.symbols.length)
+ throw new Exception(format("relocation references invalid symbol %d", rel.sym));
+
+ Segment* seg = &obj.segments[rel.seg];
+ uint base = segBase[objIdx][rel.seg];
+ Symbol* sym = &obj.symbols[rel.sym];
+
+ uint target;
+ if (sym.isDefined())
+ target = symAddr[objIdx][rel.sym];
+ else if (auto p = sym.name in globalAddr)
+ target = *p;
+ else
+ throw new Exception(format(
+ "undefined symbol '%s' referenced by object %d", sym.name, objIdx));
+
+ uint off = rel.offset;
+ ubyte[] data = seg.data;
+
+ final switch (cast(RelocType) rel.type)
+ {
+ case RelocType.abs16:
+ if (off + 2 > data.length)
+ throw new Exception(format(
+ "abs16 relocation out of range in segment '%s'", seg.name));
+ data[off] = cast(ubyte) (target & 0xFF);
+ data[off + 1] = cast(ubyte) ((target >> 8) & 0xFF);
+ break;
+ case RelocType.lo8:
+ if (off + 1 > data.length)
+ throw new Exception(format(
+ "lo8 relocation out of range in segment '%s'", seg.name));
+ data[off] = cast(ubyte) (target & 0xFF);
+ break;
+ case RelocType.hi8:
+ if (off + 1 > data.length)
+ throw new Exception(format(
+ "hi8 relocation out of range in segment '%s'", seg.name));
+ data[off] = cast(ubyte) ((target >> 8) & 0xFF);
+ break;
+ case RelocType.rel8:
+ if (off + 1 > data.length)
+ throw new Exception(format(
+ "rel8 relocation out of range in segment '%s'", seg.name));
+ {
+ int disp = cast(int) target - cast(int) (base + off + 1);
+ if (disp < -128 || disp > 127)
+ throw new Exception(format(
+ "rel8 displacement %d out of range for symbol '%s'", disp, sym.name));
+ data[off] = cast(ubyte) (disp & 0xFF);
+ }
+ break;
+ }
+ }
+ }
+
+ // ------------------------------------------------------------------
+ // 4. Determine entry address.
+ // ------------------------------------------------------------------
+ uint load = opts.origin;
+ uint high = opts.origin;
+ bool any = false;
+ foreach (p; placed)
+ {
+ if (!any || p.base < load)
+ load = p.base;
+ uint h = p.base + p.size;
+ if (!any || h > high)
+ high = h;
+ any = true;
+ }
+
+ ushort entry;
+ if (opts.entrySymbol.length > 0)
+ {
+ if (auto p = opts.entrySymbol in globalAddr)
+ entry = cast(ushort) *p;
+ else if (auto q = opts.entrySymbol in allDefined)
+ entry = cast(ushort) *q;
+ else
+ throw new Exception(format("entry symbol '%s' not defined", opts.entrySymbol));
+ }
+ else if (auto p = "start" in globalAddr)
+ entry = cast(ushort) *p;
+ else if (any)
+ entry = cast(ushort) load;
+ else
+ entry = opts.origin;
+
+ // ------------------------------------------------------------------
+ // 5. Build the output image.
+ // ------------------------------------------------------------------
+ ubyte[] imgdata = new ubyte[any ? (high - load) : 0];
+ foreach (objIdx, ref obj; objects)
+ {
+ foreach (segIdx, ref seg; obj.segments)
+ {
+ if (seg.isBss())
+ continue;
+ uint base = segBase[objIdx][segIdx];
+ uint off = base - load;
+ imgdata[off .. off + seg.data.length] = seg.data[];
+ }
+ }
+
+ Image image;
+ image.loadAddr = cast(ushort) load;
+ image.entry = entry;
+ image.data = imgdata;
+
+ // ------------------------------------------------------------------
+ // 6. Build the symbol map.
+ // ------------------------------------------------------------------
+ string[] lines;
+ lines ~= format("load = $%04X", load);
+ lines ~= format("entry = $%04X", entry);
+ lines ~= "";
+ lines ~= "segments:";
+ foreach (objIdx, ref obj; objects)
+ {
+ foreach (segIdx, ref seg; obj.segments)
+ {
+ uint base = segBase[objIdx][segIdx];
+ uint size = cast(uint) seg.data.length;
+ lines ~= format(" $%04X-$%04X %s%s", base, base + size,
+ (seg.isBss() ? "[bss] " : ""), seg.name);
+ }
+ }
+ lines ~= "";
+ lines ~= "symbols:";
+
+ SymLine[] syms;
+ foreach (objIdx, ref obj; objects)
+ {
+ foreach (symIdx, sym; obj.symbols)
+ {
+ if (sym.isDefined())
+ syms ~= SymLine(symAddr[objIdx][symIdx], sym.name);
+ }
+ }
+ syms.sort!((a, b) => a.address != b.address ? a.address < b.address : a.name < b.name);
+ foreach (s; syms)
+ lines ~= format("$%04X %s", s.address, s.name);
+
+ LinkResult res;
+ res.image = image;
+ res.mapText = lines.join("\n");
+ return res;
+}
+
+// ===========================================================================
+// tests
+// ===========================================================================
+
+unittest
+{
+ // --- abs16 across objects: A exports `foo`, B patches an abs16 to it ---
+ Object a;
+ a.segments ~= Segment(".code", cast(ubyte) (SegFlag.exec | SegFlag.alloc),
+ 0, cast(ubyte[]) [0xEA, 0xEA, 0xEA]);
+ a.symbols ~= Symbol("foo", 0, cast(ubyte) (SymFlag.defined | SymFlag.global), 0);
+
+ Object b;
+ b.segments ~= Segment(".code", cast(ubyte) (SegFlag.exec | SegFlag.alloc),
+ 0, cast(ubyte[]) [0x00, 0x00]);
+ b.symbols ~= Symbol("foo", 0, cast(ubyte) (SymFlag.imported | SymFlag.global), NoIndex);
+ b.relocs ~= Reloc(0, 0, 0, RelocType.abs16);
+
+ LinkResult r = link([a, b]);
+
+ assert(r.image.loadAddr == 0x0600);
+ // foo -> 0x0600; B's segment placed at 0x0603, patched little-endian.
+ assert(r.image.data.length == 5);
+ assert(r.image.data[3] == 0x00, "abs16 low byte");
+ assert(r.image.data[4] == 0x06, "abs16 high byte");
+ // no entrySymbol and no `start` -> lowest placed base.
+ assert(r.image.entry == 0x0600);
+}
+
+unittest
+{
+ // --- rel8 computes signed displacement; out-of-range is rejected ---
+ Object o;
+ o.segments ~= Segment(".code", cast(ubyte) (SegFlag.exec | SegFlag.alloc),
+ 0, cast(ubyte[]) [0x00]);
+ o.symbols ~= Symbol("here", 0, cast(ubyte) (SymFlag.defined | SymFlag.global), 0);
+ o.relocs ~= Reloc(0, 0, 0, RelocType.rel8);
+
+ LinkResult r = link([o]);
+ // disp = 0x0600 - (0x0600 + 0 + 1) = -1 -> 0xFF
+ assert(r.image.data[0] == 0xFF, "rel8 -1 displacement");
+
+ // out of range: target 0x8000 absolute
+ Object p;
+ p.segments ~= Segment(".code", cast(ubyte) (SegFlag.exec | SegFlag.alloc),
+ 0, cast(ubyte[]) [0x00]);
+ p.symbols ~= Symbol("far", 0x8000, cast(ubyte) (SymFlag.defined | SymFlag.global), NoIndex);
+ p.relocs ~= Reloc(0, 0, 0, RelocType.rel8);
+
+ bool threw = false;
+ try
+ link([p]);
+ catch (Exception e)
+ threw = true;
+ assert(threw, "out-of-range rel8 must be rejected");
+}
+
+unittest
+{
+ // --- fixed-origin placement and overlap detection ---
+ Object o;
+ o.segments ~= Segment(".fixed", cast(ubyte) (SegFlag.exec | SegFlag.alloc),
+ 0x4000, cast(ubyte[]) [0x01, 0x02]);
+ o.symbols ~= Symbol("target", 1, cast(ubyte) (SymFlag.defined | SymFlag.global), 0);
+
+ LinkResult r = link([o]);
+ assert(r.image.loadAddr == 0x4000);
+ assert(r.image.data.length == 2);
+ // symbol address = segment base + value = 0x4000 + 1
+ assert(r.image.entry == 0x4000); // no entrySymbol, no start -> lowest base
+
+ // two fixed-origin segments sharing an address must overlap
+ Object ov;
+ ov.segments ~= Segment(".a", cast(ubyte) SegFlag.alloc, 0x2000, cast(ubyte[]) [0x01]);
+ ov.segments ~= Segment(".b", cast(ubyte) SegFlag.alloc, 0x2000, cast(ubyte[]) [0x02]);
+
+ bool threw = false;
+ try
+ link([ov]);
+ catch (Exception e)
+ threw = true;
+ assert(threw, "overlapping fixed-origin segments must be rejected");
+}
+
+unittest
+{
+ // --- entry resolution via `start` ---
+ Object o;
+ o.segments ~= Segment(".code", cast(ubyte) (SegFlag.exec | SegFlag.alloc),
+ 0, cast(ubyte[]) [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
+ o.symbols ~= Symbol("start", 5, cast(ubyte) (SymFlag.defined | SymFlag.global), 0);
+
+ LinkResult r = link([o]);
+ assert(r.image.entry == 0x0605, "entry should come from `start`");
+}
+
+unittest
+{
+ // --- undefined symbol produces an error ---
+ Object o;
+ o.segments ~= Segment(".code", cast(ubyte) (SegFlag.exec | SegFlag.alloc),
+ 0, cast(ubyte[]) [0x00, 0x00]);
+ o.symbols ~= Symbol("missing", 0, cast(ubyte) (SymFlag.imported | SymFlag.global), NoIndex);
+ o.relocs ~= Reloc(0, 0, 0, RelocType.abs16);
+
+ bool threw = false;
+ try
+ link([o]);
+ catch (Exception e)
+ threw = true;
+ assert(threw, "undefined symbol must be an error");
+}
+
+unittest
+{
+ // --- writeObject/readObject round-trip ---
+ Object o;
+ o.segments ~= Segment(".code", cast(ubyte) (SegFlag.exec | SegFlag.alloc),
+ 0x1000, cast(ubyte[]) [0xA9, 0x01, 0xEA]);
+ o.segments ~= Segment(".bss", cast(ubyte) (SegFlag.alloc | SegFlag.bss),
+ 0, cast(ubyte[]) [0, 0, 0, 0]);
+ o.symbols ~= Symbol("main", 0, cast(ubyte) (SymFlag.defined | SymFlag.global), 0);
+ o.relocs ~= Reloc(0, 1, 0, RelocType.lo8);
+ o.entry = 0x1000;
+ o.hasEntry = true;
+
+ string path = "/tmp/link_roundtrip_test.wo";
+ writeObject(path, o);
+ Object back = readObject(path);
+
+ assert(back.segments.length == 2);
+ assert(back.segments[0].name == ".code");
+ assert(back.segments[0].origin == 0x1000);
+ assert(back.segments[0].data == cast(ubyte[]) [0xA9, 0x01, 0xEA]);
+ assert(back.segments[1].isBss());
+ assert(back.segments[1].data.length == 4);
+ assert(back.symbols.length == 1);
+ assert(back.symbols[0].name == "main");
+ assert(back.symbols[0].isGlobal());
+ assert(back.relocs.length == 1);
+ assert(back.relocs[0].type == RelocType.lo8);
+ assert(back.hasEntry);
+ assert(back.entry == 0x1000);
+}
diff --git a/src/linker/main.d b/src/linker/main.d
index 54c0db1..3276806 100644
--- a/src/linker/main.d
+++ b/src/linker/main.d
@@ -1,6 +1,116 @@
-import std.stdio;
+/**
+ * CLI wrapper for the weirdcpu linker.
+ *
+ * Usage:
+ * wlinker [options] -o [b.wo ...]
+ */
+module linker.main;
-void main()
+import linker.linker;
+import wcore.objfmt;
+import std.conv : to;
+import std.file;
+import std.stdio;
+import core.stdc.stdlib : exit;
+
+/// Disambiguate the object-file `Object` from the root `object.Object` class.
+alias Object = wcore.objfmt.Object;
+
+/// Parse an address argument: `0x`-prefixed hex or plain decimal.
+int parseAddr(string s)
{
- writefln("stub for now until i get the actual emulator done");
+ if (s.length > 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X'))
+ {
+ int v = 0;
+ foreach (c; s[2 .. $])
+ {
+ v <<= 4;
+ if (c >= '0' && c <= '9')
+ v |= (c - '0');
+ else if (c >= 'a' && c <= 'f')
+ v |= (c - 'a' + 10);
+ else if (c >= 'A' && c <= 'F')
+ v |= (c - 'A' + 10);
+ else
+ throw new Exception("invalid hex digit in address '" ~ s ~ "'");
+ }
+ return v;
+ }
+ return to!int(s);
+}
+
+/// Return the argument following `args[i]`, advancing `i`; error if absent.
+string needArg(string[] args, ref size_t i)
+{
+ if (i + 1 >= args.length)
+ throw new Exception("missing argument after '" ~ args[i] ~ "'");
+ i++;
+ return args[i];
+}
+
+void usage()
+{
+ writeln("usage: wlinker [options] -o [b.wo ...]");
+ writeln(" -o output image (required)");
+ writeln(" --origin base address for relocatable segments (default 0x0600)");
+ writeln(" -e, --entry entry symbol");
+ writeln(" --map write the symbol map");
+ writeln(" -h, --help");
+}
+
+void main(string[] args)
+{
+ try
+ {
+ string outPath;
+ string mapPath;
+ LinkOptions opts;
+ string[] inputs;
+ bool wantHelp = false;
+
+ size_t i = 1;
+ while (i < args.length)
+ {
+ string a = args[i];
+ if (a == "-h" || a == "--help")
+ wantHelp = true;
+ else if (a == "-o")
+ outPath = needArg(args, i);
+ else if (a == "--origin")
+ opts.origin = cast(ushort) parseAddr(needArg(args, i));
+ else if (a == "-e" || a == "--entry")
+ opts.entrySymbol = needArg(args, i);
+ else if (a == "--map")
+ mapPath = needArg(args, i);
+ else if (a.length > 0 && a[0] == '-')
+ throw new Exception("unknown option: " ~ a);
+ else
+ inputs ~= a;
+ i++;
+ }
+
+ if (wantHelp)
+ {
+ usage();
+ return;
+ }
+ if (outPath.length == 0)
+ throw new Exception("no output file specified (use -o)");
+ if (inputs.length == 0)
+ throw new Exception("no input object files");
+
+ Object[] objs;
+ foreach (f; inputs)
+ objs ~= readObject(f);
+
+ LinkResult res = link(objs, opts);
+ writeImage(outPath, res.image);
+ if (mapPath.length)
+ std.file.write(mapPath, res.mapText ~ "\n");
+ }
+ catch (Exception e)
+ {
+ stderr.writeln("wlinker: error: " ~ e.msg);
+ exit(1);
+ }
}
diff --git a/src/main.d b/src/main.d
deleted file mode 100644
index 524c4f9..0000000
--- a/src/main.d
+++ /dev/null
@@ -1,6 +0,0 @@
-import std.stdio;
-
-void main()
-{
- writeln("hello world!");
-}
diff --git a/src/wcore/objfmt.d b/src/wcore/objfmt.d
new file mode 100644
index 0000000..609c2df
--- /dev/null
+++ b/src/wcore/objfmt.d
@@ -0,0 +1,420 @@
+/**
+ * Shared object-file / loadable-image formats for the weirdcpu toolchain.
+ *
+ * The assembler (wiasm) emits `.wo` objects. The linker (wlinker) reads one or
+ * more `.wo` objects and emits a `.wimg` loadable image. The emulator
+ * (weirdcpu) loads `.wimg` images (or raw binaries with an explicit origin).
+ *
+ * Everything is little-endian.
+ *
+ * Object format "WO01"
+ * --------------------
+ * Header (16 bytes):
+ * char[4] magic = "WO01"
+ * u16 segCount
+ * u16 symCount
+ * u16 relCount
+ * u16 entry ; entry address, meaningless unless flags bit0 set
+ * u16 flags ; bit0 = hasEntry
+ * u16 reserved
+ *
+ * Segment record:
+ * char[8] name ; NUL padded, e.g. ".code"
+ * u8 flags ; SegFlag bits
+ * u8 reserved
+ * u16 origin ; requested origin, 0 = relocatable
+ * u32 size ; byte length of data
+ * u8[size] data ; omitted when SegFlag.bss is set
+ *
+ * Symbol record:
+ * char[32] name ; NUL padded
+ * u16 value
+ * u8 flags ; SymFlag bits
+ * u8 reserved
+ * u16 seg ; segment index, 0xFFFF if none
+ *
+ * Relocation record:
+ * u16 seg ; segment index the patch lives in
+ * u16 offset ; offset within that segment's data
+ * u16 sym ; symbol index
+ * u8 type ; RelocType
+ * u8 reserved
+ *
+ * Image format "WIMG"
+ * -------------------
+ * char[4] magic = "WIMG"
+ * u16 loadAddr
+ * u16 entry
+ * u32 length
+ * u8[length] data
+ */
+module wcore.objfmt;
+
+import std.file : read, write;
+import std.conv : to;
+import std.string : format;
+
+/// Object file magic.
+enum ObjMagic = "WO01";
+/// Loadable image magic.
+enum ImgMagic = "WIMG";
+/// No segment / no symbol sentinel.
+enum NoIndex = ushort.max;
+
+/// Segment attribute bits.
+enum SegFlag : ubyte
+{
+ alloc = 1, /// occupies memory at runtime
+ exec = 2, /// executable
+ write = 4, /// writable
+ bss = 8, /// zero-initialised, no bytes stored on disk
+}
+
+/// Symbol attribute bits.
+enum SymFlag : ubyte
+{
+ defined = 1, /// has a value in this object
+ global = 2, /// visible to / referenceable by other objects
+ imported = 4, /// undefined here, resolved by the linker
+ zeropage = 8, /// value fits in one byte (0x00..0xFF)
+}
+
+/// Relocation kinds.
+enum RelocType : ubyte
+{
+ abs16 = 0, /// patch a 16-bit little-endian absolute address
+ lo8 = 1, /// patch the low byte of an address
+ hi8 = 2, /// patch the high byte of an address
+ rel8 = 3, /// patch an 8-bit signed PC-relative displacement
+}
+
+/// A named chunk of bytes (or bss reservation).
+struct Segment
+{
+ string name; /// e.g. ".code"
+ ubyte flags; /// SegFlag bits
+ ushort origin; /// requested origin, 0 = relocatable
+ ubyte[] data; /// contents; empty for bss
+
+ /// True when the segment reserves memory but stores no bytes.
+ bool isBss() const pure nothrow @nogc
+ {
+ return (flags & SegFlag.bss) != 0;
+ }
+}
+
+/// A symbol defined in, imported by, or exported from an object.
+struct Symbol
+{
+ string name;
+ ushort value;
+ ubyte flags; /// SymFlag bits
+ ushort seg = NoIndex;
+
+ /// True when this object provides the symbol's value.
+ bool isDefined() const pure nothrow @nogc
+ {
+ return (flags & SymFlag.defined) != 0;
+ }
+
+ /// True when the symbol is visible outside its object.
+ bool isGlobal() const pure nothrow @nogc
+ {
+ return (flags & SymFlag.global) != 0;
+ }
+}
+
+/// A request for the linker to patch bytes in a segment.
+struct Reloc
+{
+ ushort seg; /// segment index the patch lives in
+ ushort offset; /// byte offset inside the segment
+ ushort sym; /// symbol index
+ ubyte type; /// RelocType
+}
+
+/// A parsed `.wo` object file.
+struct Object
+{
+ Segment[] segments;
+ Symbol[] symbols;
+ Reloc[] relocs;
+ ushort entry;
+ bool hasEntry;
+
+ /// Find a symbol by name, or NoIndex when absent.
+ int findSymbol(string name) const
+ {
+ foreach (i, s; symbols)
+ if (s.name == name)
+ return cast(int) i;
+ return -1;
+ }
+}
+
+/// A loadable image produced by the linker.
+struct Image
+{
+ ushort loadAddr;
+ ushort entry;
+ ubyte[] data;
+}
+
+// ---------------------------------------------------------------------------
+// little-endian serialisation helpers
+// ---------------------------------------------------------------------------
+
+private void putU8(ref ubyte[] b, ubyte v)
+{
+ b ~= v;
+}
+
+private void putU16(ref ubyte[] b, ushort v)
+{
+ b ~= cast(ubyte) (v & 0xFF);
+ b ~= cast(ubyte) ((v >> 8) & 0xFF);
+}
+
+private void putU32(ref ubyte[] b, uint v)
+{
+ b ~= cast(ubyte) (v & 0xFF);
+ b ~= cast(ubyte) ((v >> 8) & 0xFF);
+ b ~= cast(ubyte) ((v >> 16) & 0xFF);
+ b ~= cast(ubyte) ((v >> 24) & 0xFF);
+}
+
+private void putFixed(ref ubyte[] b, string s, size_t n)
+{
+ foreach (i; 0 .. n)
+ b ~= (i < s.length) ? cast(ubyte) s[i] : cast(ubyte) 0;
+}
+
+/// Extract a NUL-terminated fixed-width name from a byte slice.
+string cstr(const(ubyte)[] buf)
+{
+ size_t n = 0;
+ while (n < buf.length && buf[n] != 0)
+ n++;
+ return cast(string) buf[0 .. n].dup;
+}
+
+private struct Reader
+{
+ const(ubyte)[] buf;
+ size_t pos;
+
+ void need(size_t n) const
+ {
+ if (pos + n > buf.length)
+ throw new Exception("unexpected end of file in object/image data");
+ }
+
+ ubyte u8()
+ {
+ need(1);
+ return buf[pos++];
+ }
+
+ ushort u16()
+ {
+ need(2);
+ ushort v = cast(ushort) (buf[pos] | (buf[pos + 1] << 8));
+ pos += 2;
+ return v;
+ }
+
+ uint u32()
+ {
+ need(4);
+ uint v = cast(uint) buf[pos]
+ | (cast(uint) buf[pos + 1] << 8)
+ | (cast(uint) buf[pos + 2] << 16)
+ | (cast(uint) buf[pos + 3] << 24);
+ pos += 4;
+ return v;
+ }
+
+ ubyte[] bytes(size_t n)
+ {
+ need(n);
+ auto v = buf[pos .. pos + n].dup;
+ pos += n;
+ return v;
+ }
+
+ string fixed(size_t n)
+ {
+ need(n);
+ auto v = cstr(buf[pos .. pos + n]);
+ pos += n;
+ return v;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// object I/O
+// ---------------------------------------------------------------------------
+
+/// Serialise an object to its `.wo` byte representation.
+ubyte[] encodeObject(const ref Object obj)
+{
+ ubyte[] b;
+ putFixed(b, ObjMagic, 4);
+ putU16(b, cast(ushort) obj.segments.length);
+ putU16(b, cast(ushort) obj.symbols.length);
+ putU16(b, cast(ushort) obj.relocs.length);
+ putU16(b, obj.entry);
+ putU16(b, obj.hasEntry ? 1 : 0);
+ putU16(b, 0);
+
+ foreach (seg; obj.segments)
+ {
+ putFixed(b, seg.name, 8);
+ putU8(b, seg.flags);
+ putU8(b, 0);
+ putU16(b, seg.origin);
+ putU32(b, cast(uint) seg.data.length);
+ if (!seg.isBss())
+ b ~= seg.data;
+ }
+
+ foreach (sym; obj.symbols)
+ {
+ putFixed(b, sym.name, 32);
+ putU16(b, sym.value);
+ putU8(b, sym.flags);
+ putU8(b, 0);
+ putU16(b, sym.seg);
+ }
+
+ foreach (r; obj.relocs)
+ {
+ putU16(b, r.seg);
+ putU16(b, r.offset);
+ putU16(b, r.sym);
+ putU8(b, r.type);
+ putU8(b, 0);
+ }
+
+ return b;
+}
+
+/// Decode a `.wo` object from bytes.
+Object decodeObject(const(ubyte)[] bytes)
+{
+ Reader r = Reader(bytes, 0);
+ string magic = cast(string) r.bytes(4);
+ if (magic != ObjMagic)
+ throw new Exception(format("bad object magic %s (expected %s)", magic, ObjMagic));
+
+ ushort segCount = r.u16();
+ ushort symCount = r.u16();
+ ushort relCount = r.u16();
+ ushort entry = r.u16();
+ ushort flags = r.u16();
+ r.u16(); // reserved
+
+ Object obj;
+ obj.entry = entry;
+ obj.hasEntry = (flags & 1) != 0;
+
+ foreach (_; 0 .. segCount)
+ {
+ Segment seg;
+ seg.name = r.fixed(8);
+ seg.flags = r.u8();
+ r.u8();
+ seg.origin = r.u16();
+ uint size = r.u32();
+ if ((seg.flags & SegFlag.bss) != 0)
+ {
+ seg.data = new ubyte[size];
+ seg.data[] = 0;
+ }
+ else
+ {
+ seg.data = r.bytes(size);
+ }
+ obj.segments ~= seg;
+ }
+
+ foreach (_; 0 .. symCount)
+ {
+ Symbol sym;
+ sym.name = r.fixed(32);
+ sym.value = r.u16();
+ sym.flags = r.u8();
+ r.u8();
+ sym.seg = r.u16();
+ obj.symbols ~= sym;
+ }
+
+ foreach (_; 0 .. relCount)
+ {
+ Reloc rel;
+ rel.seg = r.u16();
+ rel.offset = r.u16();
+ rel.sym = r.u16();
+ rel.type = r.u8();
+ r.u8();
+ obj.relocs ~= rel;
+ }
+
+ return obj;
+}
+
+/// Write an object to `path`.
+void writeObject(string path, const ref Object obj)
+{
+ write(path, encodeObject(obj));
+}
+
+/// Read an object from `path`.
+Object readObject(string path)
+{
+ return decodeObject(cast(const(ubyte)[]) read(path));
+}
+
+// ---------------------------------------------------------------------------
+// image I/O
+// ---------------------------------------------------------------------------
+
+/// Serialise a loadable image to its `.wimg` byte representation.
+ubyte[] encodeImage(const ref Image img)
+{
+ ubyte[] b;
+ putFixed(b, ImgMagic, 4);
+ putU16(b, img.loadAddr);
+ putU16(b, img.entry);
+ putU32(b, cast(uint) img.data.length);
+ b ~= img.data;
+ return b;
+}
+
+/// Decode a `.wimg` image from bytes.
+Image decodeImage(const(ubyte)[] bytes)
+{
+ Reader r = Reader(bytes, 0);
+ string magic = cast(string) r.bytes(4);
+ if (magic != ImgMagic)
+ throw new Exception(format("bad image magic %s (expected %s)", magic, ImgMagic));
+
+ Image img;
+ img.loadAddr = r.u16();
+ img.entry = r.u16();
+ uint len = r.u32();
+ img.data = r.bytes(len);
+ return img;
+}
+
+/// Write a loadable image to `path`.
+void writeImage(string path, const ref Image img)
+{
+ write(path, encodeImage(img));
+}
+
+/// Read a loadable image from `path`.
+Image readImage(string path)
+{
+ return decodeImage(cast(const(ubyte)[]) read(path));
+}
diff --git a/tests/e2e.sh b/tests/e2e.sh
new file mode 100755
index 0000000..b93185f
--- /dev/null
+++ b/tests/e2e.sh
@@ -0,0 +1,58 @@
+#!/usr/bin/env bash
+# End-to-end test: assemble -> link -> emulate, and check program output.
+# Usage: tests/e2e.sh
+set -euo pipefail
+cd "$(dirname "$0")/.."
+
+BIN="build/linux/x86_64/debug"
+OUT="build/e2e"
+mkdir -p "$OUT"
+
+echo "== building =="
+xmake -y >/dev/null
+
+WASM="$BIN/wiasm"
+WLINK="$BIN/wlinker"
+WCPU="$BIN/weirdcpu"
+
+for t in "$WASM" "$WLINK" "$WCPU"; do
+ [ -x "$t" ] || { echo "missing binary: $t"; exit 1; }
+done
+
+fail=0
+check() { # name expected actual
+ if [ "$2" = "$3" ]; then
+ echo "PASS $1"
+ else
+ echo "FAIL $1"
+ echo " expected: $(printf '%q' "$2")"
+ echo " actual: $(printf '%q' "$3")"
+ fail=1
+ fi
+}
+
+echo
+echo "== single object: hello =="
+"$WASM" -o "$OUT/hello.wo" examples/hello.asm
+"$WLINK" -o "$OUT/hello.wimg" --map "$OUT/hello.map" "$OUT/hello.wo"
+check "hello" "Hello, World!" "$("$WCPU" "$OUT/hello.wimg")"
+
+echo
+echo "== cross object: putc + main =="
+"$WASM" -o "$OUT/putc.wo" examples/putc.asm
+"$WASM" -o "$OUT/main.wo" examples/main.asm
+"$WLINK" -o "$OUT/main.wimg" -e start --map "$OUT/main.map" "$OUT/putc.wo" "$OUT/main.wo"
+check "cross-object" "Hello, 6502!" "$("$WCPU" "$OUT/main.wimg")"
+
+echo
+echo "== arithmetic loop: count =="
+"$WASM" -o "$OUT/count.wo" examples/count.asm
+"$WLINK" -o "$OUT/count.wimg" "$OUT/count.wo"
+check "count" "0123456789" "$("$WCPU" "$OUT/count.wimg")"
+
+echo
+if [ "$fail" -ne 0 ]; then
+ echo "e2e: FAILED"
+ exit 1
+fi
+echo "e2e: OK"
diff --git a/tests/run.sh b/tests/run.sh
new file mode 100755
index 0000000..9d22d1c
--- /dev/null
+++ b/tests/run.sh
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+# Build and run the D unit tests for the toolchain modules.
+# Usage: tests/run.sh
+set -euo pipefail
+cd "$(dirname "$0")/.."
+
+DMD="${DMD:-dmd}"
+OUT="build/wtests"
+
+mkdir -p build
+
+"$DMD" -unittest -main -Isrc -of"$OUT" \
+ src/wcore/objfmt.d \
+ src/emulator/cpu.d \
+ src/emulator/bus.d \
+ src/assembler/assembler.d \
+ src/linker/linker.d
+
+"$OUT"
diff --git a/xmake.lua b/xmake.lua
index 9ecbbb4..be6ba05 100644
--- a/xmake.lua
+++ b/xmake.lua
@@ -8,88 +8,32 @@ else
set_strip("all")
end
+-- Shared formats used by more than one tool (object files, images).
+local core = "src/wcore/*.d"
+
+-- 6502 emulator: loads a .wimg image (or raw binary) and runs it.
target("weirdcpu")
do
set_kind("binary")
- add_files("src/*.d")
-end
-
-target("wlinker")
-do
- set_kind("binary")
- add_files("src/linker/*.d")
+ add_includedirs("src")
+ add_files("src/emulator/*.d")
+ add_files(core)
end
+-- Assembler: .asm source -> .wo object.
target("wiasm")
do
set_kind("binary")
+ add_includedirs("src")
add_files("src/assembler/*.d")
+ add_files(core)
+end
+
+-- Linker: one or more .wo objects -> .wimg image + .map.
+target("wlinker")
+do
+ set_kind("binary")
+ add_includedirs("src")
+ add_files("src/linker/*.d")
+ add_files(core)
end
---
--- If you want to known more usage about xmake, please see https://xmake.io
---
--- ## FAQ
---
--- You can enter the project directory firstly before building project.
---
--- $ cd projectdir
---
--- 1. How to build project?
---
--- $ xmake
---
--- 2. How to configure project?
---
--- $ xmake f -p [macosx|linux|iphoneos ..] -a [x86_64|i386|arm64 ..] -m [debug|release]
---
--- 3. Where is the build output directory?
---
--- The default output directory is `./build` and you can configure the output directory.
---
--- $ xmake f -o outputdir
--- $ xmake
---
--- 4. How to run and debug target after building project?
---
--- $ xmake run [targetname]
--- $ xmake run -d [targetname]
---
--- 5. How to install target to the system directory or other output directory?
---
--- $ xmake install
--- $ xmake install -o installdir
---
--- 6. Add some frequently-used compilation flags in xmake.lua
---
--- @code
--- -- add debug and release modes
--- add_rules("mode.debug", "mode.release")
---
--- -- add macro definition
--- add_defines("NDEBUG", "_GNU_SOURCE=1")
---
--- -- set warning all as error
--- set_warnings("all", "error")
---
--- -- set language: c99, c++11
--- set_languages("c99", "c++11")
---
--- -- set optimization: none, faster, fastest, smallest
--- set_optimize("fastest")
---
--- -- add include search directories
--- add_includedirs("/usr/include", "/usr/local/include")
---
--- -- add link libraries and search directories
--- add_links("tbox")
--- add_linkdirs("/usr/local/lib", "/usr/lib")
---
--- -- add system link libraries
--- add_syslinks("z", "pthread")
---
--- -- add compilation and link flags
--- add_cxflags("-stdnolib", "-fno-strict-aliasing")
--- add_ldflags("-L/usr/local/lib", "-lpthread", {force = true})
---
--- @endcode
---