diff --git a/client/src/machine/FileBlob.ts b/client/src/machine/FileBlob.ts index 3bdc5f9..89945f9 100644 --- a/client/src/machine/FileBlob.ts +++ b/client/src/machine/FileBlob.ts @@ -1,4 +1,5 @@ -import {Addr, ArrayMemory, BE, Byteable, Endian} from "./core"; +import {Addr, BE, Byteable, Endian} from "./core"; +import {ArrayMemory} from "./Memory.ts"; /** * Abstraction over a file-like thing which stores binary content and has a name and size. Contents can be accessed diff --git a/client/src/machine/Memory.ts b/client/src/machine/Memory.ts new file mode 100644 index 0000000..e67b16e --- /dev/null +++ b/client/src/machine/Memory.ts @@ -0,0 +1,134 @@ +import {Addr, Byteable, Endian, MB_8} from "./core.ts"; + +/** + * Contiguous, fixed-sized 0-based Memory with {@link Endian Endianness}. + */ +interface Memory { + + writeable(): boolean; + + executable(): boolean; + + /** + * Read from the offset a 16 bit word in the right {@link Endian Endianness}. + * @param byteOffset + */ + read16(byteOffset: Addr): number; + + + read8(byteOffset: Addr): number; + + /** + * Gets the {@link Endian endianness}. + */ + endianness(): T; + + getLength(): number; + + submatch(seq: Uint8Array, atOffset: number): boolean; + + contains(location: Addr): boolean; +} + +/** + * Represents a contiguous, {@link Endian} Memory, backed by an array. + */ +class ArrayMemory implements Memory, Byteable { + /** Arbitrary size, plenty for retro computers. */ + private static MAX: number = MB_8; + private readonly _bytes: number[]; + private readonly endian: T; + private readonly _writeable: boolean; + private readonly _executable: boolean; + + /** + * Construct with an array of values or a desired size. + * + * @param bytes if a size, must be sensible, if an array, we use that. + * @param endian byte order for word interpretation. + * @param writeable whether this memory is marked as writeable by user code (does not imply immutable) + * @param executable whether this memory is marked as executable for user code + */ + constructor(bytes: number | number[], endian: T, writeable = true, executable = true) { + this._writeable = writeable; + this._executable = executable; + if (typeof bytes === "number") { + if (bytes < 0 || bytes > ArrayMemory.MAX) { + throw Error(`Memory size ${bytes} is not supported`); + } + this._bytes = new Array(bytes); + // arbitrary conspicuous (0b1010 = 0xa = 10) double-endian fill constant to aid debugging + this._bytes.fill(0b1010); + } else { + if (bytes.length > ArrayMemory.MAX) { + throw Error(`Memory size ${bytes.length} is greater than maximum ${ArrayMemory.MAX}`); + } + this._bytes = bytes; + } + this.endian = endian; + } + + static zeroes(size: number, endian: T, writeable: boolean, executable: boolean): ArrayMemory { + return new ArrayMemory(Array(size).fill(0), endian, writeable, executable); + } + + executable = (): boolean => this._executable; + + writeable = (): boolean => this._writeable; + + getLength = (): number => this._bytes.length; + + getBytes = () => this._bytes; + + submatch(seq: Uint8Array, atOffset: number): boolean { + if (seq.length + atOffset <= this._bytes.length && seq.length > 0) { + for (let i = 0; i < seq.length; i++) { + if (seq[i] !== this._bytes[i + atOffset]) { + return false; + } + } + // sequence has matched at offset + return true; + } else { + console.log("Sequence length out of range (" + seq.length + ") for memory size " + this._bytes.length); + return false; + } + } + + read16(byteOffset: Addr) { + // last possible word offset must fit word + const lastWordAddress = this._bytes.length - 2; + if (byteOffset < 0 || byteOffset > lastWordAddress) { + throw Error("offset out of range for word read"); + } + return this.endian.twoBytesToWord([this._bytes[byteOffset], this._bytes[byteOffset + 1]]); + } + + read8(byteOffset: Addr): number { + if (!this.contains(byteOffset)) { + throw Error("offset out of range for vector read"); + } + return this._bytes[byteOffset]; + } + + endianness = (): T => this.endian; + + contains = (location: Addr) => location >= 0 && location < this._bytes.length; + + /** + * Loads the given data to the given address. + * @param data + * @param location + */ + load(data: number[], location: Addr) { + if (data.length + location > this._bytes.length) { + throw Error(`Not enough room to load ${data.length} bytes at ${location}`); + } + data.forEach((b, index) => { + this._bytes[index + location] = b; + }) + } +} + +export {ArrayMemory}; +export {type Memory}; \ No newline at end of file diff --git a/client/src/machine/Op.ts b/client/src/machine/Op.ts index e86bfb1..04b5418 100644 --- a/client/src/machine/Op.ts +++ b/client/src/machine/Op.ts @@ -1,18 +1,50 @@ +export enum OpSemantics { + IS_UNCONDITIONAL_JUMP, // will modify PC + IS_CONDITIONAL_JUMP, // may modify PC + IS_BREAK, // legit break + IS_JAM, // illegal break + IS_ILLEGAL, // undocumented but may execute + IS_STORE, // modifies memory + IS_RETURN, // return from subroutine or interrupt +} + export class Op { mnemonic: string; description: string; /** mnemonic category */ cat: string; - private readonly _isJump: boolean; + private semantics: OpSemantics[] = []; + - constructor(mnemonic: string, description: string, cat: string, isJump = false) { + constructor(mnemonic: string, description: string, cat: string, semantics: OpSemantics[] = []) { + this.semantics = semantics; this.mnemonic = mnemonic; this.description = description; this.cat = cat; - this._isJump = isJump; } - get isJump(): boolean { - return this._isJump; + /** + * Returns true only if this {@see Op} has the given semantics. + * @param semantics + */ + has(semantics: OpSemantics): boolean { + return this.semantics.includes(semantics); + } + + /** + * Returns true only if this {@see Op} has all the given semantics. + * @param semantics + */ + all(semantics: OpSemantics[]): boolean { + return semantics.every(s => this.semantics.includes(s)); } + + /** + * Returns true only if this {@see Op} has at least one of the given semantics. + * @param semantics + */ + any(semantics: OpSemantics[]): boolean { + return semantics.some(s => this.semantics.includes(s)); + } + } \ No newline at end of file diff --git a/client/src/machine/Thread.ts b/client/src/machine/Thread.ts index 5d78868..847f7f5 100644 --- a/client/src/machine/Thread.ts +++ b/client/src/machine/Thread.ts @@ -1,5 +1,7 @@ import {Disassembler} from "./asm/Disassembler"; -import {Addr, Endian, Memory} from "./core.ts"; +import {Addr, Endian} from "./core.ts"; +import {OpSemantics} from "./Op.ts"; +import {Memory} from "./Memory.ts"; /** * A single thread of execution which records all executed addresses and all written locations. @@ -65,7 +67,6 @@ export class Thread { throw new Error("cannot step if stopped"); } this.execute(); - this.executed.push(this.pc++); } /** @@ -77,17 +78,26 @@ export class Thread { * If an branching instruction occurs, the new {@link Thread} is returned, otherwise undefined. */ private execute(): Thread | undefined { - console.log(`executing: ${this.descriptor}`); - // TODO use this.disasm to disassemble current instruction - - // TODO disassemble instruction at PC - + // console.log(`executing: ${this.descriptor}/${this.pc}`); + // TODO confirm this.pc is the memory offset - what is the base address? + const inst = this.disasm.disassemble1(this.memory, this.pc); + // by default, increment PC by length of this instruction + let nextPc = this.pc + inst.getLength(); + if (this.executed.includes(this.pc)) { + console.log(`already executed ${this.pc}, terminating thread ${this}`); + this._running = false; + } else { + const op = inst.instruction.op; + if (op.any([OpSemantics.IS_BREAK, OpSemantics.IS_JAM])) { + this._running = false; + } else if (op.any([OpSemantics.IS_UNCONDITIONAL_JUMP])) { + nextPc = inst.operandValue(); + } + } - // TODO handle stop cases, i.e. BRK or CPU JAM // TODO handle join case, i.e. reaching already traced code // TODO handle fork case, i.e. conditional branch - // TODO handle simple recording of execution at instruction address, must keep track of first byte and - // instruction length. + // TODO handle tracing interrupt handlers - these are tricky - perhaps we can just always trace them // TODO edge case: execution at an address could be byte-misaligned with previous execution resulting in // different instruction decoding, so execution records should hold the first byte of the decoded instruction // and coverage measurements imply that coverage of any subsequent bytes of the instruction is predicated on @@ -96,6 +106,8 @@ export class Thread { // instructions may be rare enough to simply report as anomalies at first and may even be more likely be a // theoretical bug in the analysed code. This tracer will not detect all unreachable code paths since only a // degenerate runtime state is represented. + this.executed.push(this.pc); + this.pc = nextPc; // increment PC by length of this instruction return undefined; } @@ -106,4 +118,8 @@ export class Thread { getWritten(): Array { return [...this.written]; } + + getPc() { + return this.pc; + } } \ No newline at end of file diff --git a/client/src/machine/Tracer.ts b/client/src/machine/Tracer.ts index 7bc0157..6618e62 100644 --- a/client/src/machine/Tracer.ts +++ b/client/src/machine/Tracer.ts @@ -8,9 +8,10 @@ * idioms may be recognised this same way across different code bases. */ -import {Endian, Memory} from "./core"; +import {Endian, hex16} from "./core"; import {Disassembler} from "./asm/Disassembler"; import {Thread} from "./Thread.ts"; +import {Memory} from "./Memory.ts"; /** * Analyser that approximates code execution to some degree. Static analysis ignores register and memory contents, @@ -39,10 +40,11 @@ class Tracer { * @param memory the Memory */ constructor(disasm: Disassembler, pc: number, memory: Memory) { + const relativePc = pc - disasm.getSegmentBaseAddress(); if (Math.round(pc) !== pc) { throw Error(`startLocation must be integral`); - } else if (pc < 0 || memory.getLength() <= pc) { - throw Error(`startLocation ${pc} not inside memory of size ${memory.getLength()}`); + } else if (relativePc < 0 || memory.getLength() <= relativePc) { + throw Error(`startLocation 0x${hex16(pc)} not inside memory of size ${memory.getLength()} at base 0x${hex16(disasm.getSegmentBaseAddress())}`); } else if (!memory.executable()) { throw Error("memory not marked for execution"); } diff --git a/client/src/machine/api.ts b/client/src/machine/api.ts index 405ef5c..e048ef8 100644 --- a/client/src/machine/api.ts +++ b/client/src/machine/api.ts @@ -1,9 +1,10 @@ -import {Addr, BigEndian, hex8, LittleEndian, Memory} from "./core"; +import {Addr, BigEndian, hex8, LittleEndian} from "./core"; import {FileBlob} from "./FileBlob"; import {Mos6502} from "./mos6502"; import {InstructionLike} from "./asm/instructions.ts"; import {DataView, DataViewImpl} from "./DataView.ts"; import {BlobSniffer} from "./BlobSniffer.ts"; +import {Memory} from "./Memory.ts"; /** * Renderable output of structured text with html-friendly structure and internal text renderer. diff --git a/client/src/machine/asm/Disassembler.ts b/client/src/machine/asm/Disassembler.ts index 65140d9..981700a 100644 --- a/client/src/machine/asm/Disassembler.ts +++ b/client/src/machine/asm/Disassembler.ts @@ -1,11 +1,13 @@ import {FileBlob} from "../FileBlob.ts"; import {FullInstruction, Mos6502} from "../mos6502.ts"; -import {Addr, asByte, Endian, Memory} from "../core.ts"; +import {Addr, asByte, Endian} from "../core.ts"; import {ByteDeclaration, Edict, FullInstructionLine, InstructionLike, SymDef} from "./instructions.ts"; import * as R from "ramda"; import {LabelsComments} from "./asm.ts"; import {DisassemblyMeta} from "./DisassemblyMeta.ts"; import {InstructionSet} from "../InstructionSet.ts"; +import {Memory} from "../Memory.ts"; +import {OpSemantics} from "../Op.ts"; @@ -30,7 +32,7 @@ class Disassembler { const index = dm.contentStartOffset(); const bytes: number[] = fb.getBytes(); if (index >= bytes.length || index < 0) { - throw Error(`index '${index}' out of range`); + throw Error(`index '${index}' out of range for fb size: ${fb.getLength()}`); } this.originalIndex = index; this.currentIndex = index; @@ -160,6 +162,7 @@ class Disassembler { return lc.reduce((p, c) => p.merge(c), new LabelsComments()); } + // noinspection JSUnusedGlobalSymbols /** * Determine all jump targets both statically defined and implied by the given sequence of address,instruction * pairs. Only those targets that lie within the address range of our loaded binary are returned. @@ -171,13 +174,11 @@ class Disassembler { // instructions that are jumps and have a resolvable destination const resolvableJump = (addrInst: [number, FullInstruction]) => { - return addrInst[1].instruction.op.isJump && addrInst[1].staticallyResolvableOperand(); + const isJump = addrInst[1].instruction.op.any([OpSemantics.IS_UNCONDITIONAL_JUMP, OpSemantics.IS_CONDITIONAL_JUMP]); + return isJump && addrInst[1].staticallyResolvableOperand(); }; - // collect targets of all current jump instructions - // for all jump instructions, collect the destination address - return instructions .filter(addrInst => resolvableJump(addrInst)) // only jumps, only statically resolvable .map(j => j[1].resolveOperandAddress(j[0])) // resolve pc-relative operands @@ -222,34 +223,16 @@ class Disassembler { private eatBytes(count: number): number[] { const bytes: number[] = []; for (let i = 1; i <= count; i++) { - bytes.push(this.eatByte()); - } - return bytes; - } - - /** - * @deprecated side effect - * @private - */ - private eatByteOrDie() { - if (this.currentIndex >= this.fb.getBytes().length) { - throw Error("No more bytes"); - } - return this.eatByte(); - } + const value = this.fb.read8(this.currentIndex); // side effect + if (typeof value === "undefined") { + throw Error(`Illegal state, no byte at index ${this.currentIndex}`); + } else { + this.currentIndex++; + bytes.push(value & 0xff); + } - /** - * @deprecated side effect - * @private - */ - private eatByte(): number { - const value = this.fb.read8(this.currentIndex); // side effect - if (typeof value === "undefined") { - throw Error(`Illegal state, no byte at index ${this.currentIndex}`); - } else { - this.currentIndex++; - return (value & 0xff); } + return bytes; } private peekByte = (): number => this.fb.read8(this.currentIndex); @@ -355,6 +338,10 @@ class Disassembler { } return undefined; } + + getSegmentBaseAddress(): Addr { + return this.segmentBaseAddress; + } } export {Disassembler}; \ No newline at end of file diff --git a/client/src/machine/cbm/c64.ts b/client/src/machine/cbm/c64.ts index c719f1f..cadfe4f 100644 --- a/client/src/machine/cbm/c64.ts +++ b/client/src/machine/cbm/c64.ts @@ -3,7 +3,7 @@ import {BlobToActions, Computer, hexDumper, MemoryConfiguration} from "../api.ts"; import {CartSniffer} from "./cbm.ts"; -import {ArrayMemory, KB_64, LE} from "../core.ts"; +import {KB_64, LE} from "../core.ts"; import {FileBlob} from "../FileBlob.ts"; import {Mos6502} from "../mos6502.ts"; import {Petscii} from "./petscii.ts"; @@ -11,6 +11,7 @@ import {DisassemblyMetaImpl} from "../asm/DisassemblyMetaImpl.ts"; import {BlobType} from "../BlobType.ts"; import {ByteDefinitionEdict, VectorDefinitionEdict} from "../asm/instructions.ts"; import {JumpTargetFetcher, LabelsComments, mkLabels, SymbolTable} from "../asm/asm.ts"; +import {ArrayMemory} from "../Memory.ts"; class C64 extends Computer { constructor(memoryConfig: MemoryConfiguration, tags: string[]) { diff --git a/client/src/machine/cbm/vic20.ts b/client/src/machine/cbm/vic20.ts index e39caa8..c205419 100644 --- a/client/src/machine/cbm/vic20.ts +++ b/client/src/machine/cbm/vic20.ts @@ -3,7 +3,7 @@ import {Computer, LogicalLine, MemoryConfiguration, Tag, TAG_ADDRESS, TAG_LINE_NUMBER} from "../api"; import {CBM_BASIC_2_0} from "./basic"; import {CartSniffer, prg} from "./cbm"; -import {ArrayMemory, KB_64, LE, lsb, msb} from "../core"; +import {KB_64, LE, lsb, msb} from "../core"; import {FileBlob} from "../FileBlob"; import {Mos6502} from "../mos6502"; import {DisassemblyMetaImpl} from "../asm/DisassemblyMetaImpl"; @@ -11,6 +11,7 @@ import {JumpTargetFetcher, LabelsComments, mkLabels, SymbolTable} from "../asm/a import {ByteDefinitionEdict, VectorDefinitionEdict} from "../asm/instructions.ts"; import {DisassemblyMeta} from "../asm/DisassemblyMeta.ts"; import {BlobSniffer} from "../BlobSniffer.ts"; +import {ArrayMemory} from "../Memory.ts"; const VIC20_KERNAL = new SymbolTable("vic20"); diff --git a/client/src/machine/core.ts b/client/src/machine/core.ts index b6828d4..daf2367 100644 --- a/client/src/machine/core.ts +++ b/client/src/machine/core.ts @@ -110,118 +110,6 @@ class BigEndian implements Endian { const LE: LittleEndian = new LittleEndian(); const BE: BigEndian = new BigEndian(); -/** - * Contiguous, fixed-sized 0-based Memory with {@link Endian Endianness}. - */ -interface Memory { - - writeable(): boolean; - - executable(): boolean; - - /** - * Read from the offset a 16 bit word in the right {@link Endian Endianness}. - * @param byteOffset - */ - read16(byteOffset: Addr): number; - - - read8(byteOffset: Addr): number; - - /** - * Gets the {@link Endian endianness}. - */ - endianness(): T; - - getLength(): number; - - submatch(seq: Uint8Array, atOffset: number): boolean; - - contains(location: Addr): boolean; -} - -/** - * Represents a contiguous, {@link Endian} Memory, backed by an array. - */ -class ArrayMemory implements Memory, Byteable { - /** Arbitrary size, plenty for retro computers. */ - private static MAX: number = MB_8; - private readonly _bytes: number[]; - private readonly endian: T; - private readonly _writeable: boolean; - private readonly _executable: boolean; - - /** - * Construct with an array of values or a desired size. - * - * @param bytes if a size, must be sensible, if an array, we use that. - * @param endian byte order for word interpretation. - * @param writeable whether this memory is marked as writeable by user code (does not imply immutable) - * @param executable whether this memory is marked as executable for user code - */ - constructor(bytes: number | number[], endian: T, writeable = true, executable = true) { - this._writeable = writeable; - this._executable = executable; - if (typeof bytes === "number") { - if (bytes < 0 || bytes > ArrayMemory.MAX) { - throw Error(`Memory size ${bytes} is not supported`); - } - this._bytes = new Array(bytes); - // arbitrary conspicuous (0b1010 = 0xa = 10) double-endian fill constant to aid debugging - this._bytes.fill(0b1010); - } else { - if (bytes.length > ArrayMemory.MAX) { - throw Error(`Memory size ${bytes.length} is greater than maximum ${ArrayMemory.MAX}`); - } - this._bytes = bytes; - } - this.endian = endian; - } - - executable = (): boolean => this._executable; - - writeable = (): boolean => this._writeable; - - getLength = (): number => this._bytes.length; - - getBytes = () => this._bytes; - - submatch(seq: Uint8Array, atOffset: number): boolean { - if (seq.length + atOffset <= this._bytes.length && seq.length > 0) { - for (let i = 0; i < seq.length; i++) { - if (seq[i] !== this._bytes[i + atOffset]) { - return false; - } - } - // sequence has matched at offset - return true; - } else { - console.log("Sequence length out of range (" + seq.length + ") for memory size " + this._bytes.length); - return false; - } - } - - read16(byteOffset: Addr) { - // last possible word offset must fit word - const lastWordAddress = this._bytes.length - 2; - if (byteOffset < 0 || byteOffset > lastWordAddress) { - throw Error("offset out of range for word read"); - } - return this.endian.twoBytesToWord([this._bytes[byteOffset], this._bytes[byteOffset + 1]]); - } - - read8(byteOffset: Addr): number { - if (!this.contains(byteOffset)) { - throw Error("offset out of range for vector read"); - } - return this._bytes[byteOffset]; - } - - endianness = (): T => this.endian; - - contains = (location: Addr) => location >= 0 && location < this._bytes.length; -} - /** * Takes a byte value in the range 0-255 and interprets its numeric value as an 8 bit two's complement value * between -128 and 127. @@ -249,7 +137,6 @@ export { BE, toStringArray, toNumberArray, - ArrayMemory, BigEndian, LittleEndian, KB_64, @@ -257,5 +144,5 @@ export { MB_8, } -export type {Byteable, Addr, Endian, Memory}; +export type {Byteable, Addr, Endian}; export {msb, lsb}; diff --git a/client/src/machine/mos6502.ts b/client/src/machine/mos6502.ts index 336486a..f68fbe1 100644 --- a/client/src/machine/mos6502.ts +++ b/client/src/machine/mos6502.ts @@ -11,7 +11,7 @@ import {Addr, assertByte, Byteable, unToSigned} from "./core.ts"; import {InstructionSet} from "./InstructionSet.ts"; -import {Op} from "./Op.ts"; +import {Op, OpSemantics} from "./Op.ts"; type M6502OperandLength = 0 | 1 | 2; @@ -136,16 +136,16 @@ const MS = "ms"; const ADC = new Op("ADD", "add with carry", MATH); const AND = new Op("AND", "and (with accumulator)", LG); const ASL = new Op("ASL", "arithmetic shift left", MATH); -const BCC = new Op("BCC", "branch on carry clear", BRA, true); -const BCS = new Op("BCS", "branch on carry set", BRA, true); -const BEQ = new Op("BEQ", "branch on equal (zero set)", BRA, true); +const BCC = new Op("BCC", "branch on carry clear", BRA, [OpSemantics.IS_CONDITIONAL_JUMP]); +const BCS = new Op("BCS", "branch on carry set", BRA, [OpSemantics.IS_CONDITIONAL_JUMP]); +const BEQ = new Op("BEQ", "branch on equal (zero set)", BRA, [OpSemantics.IS_CONDITIONAL_JUMP]); const BIT = new Op("BIT", "bit test", LG); -const BMI = new Op("BMI", "branch on minus (negative set)", BRA, true); -const BNE = new Op("BNE", "branch on not equal (zero clear)", BRA, true); -const BPL = new Op("BPL", "branch on plus (negative clear)", BRA, true); -const BRK = new Op("BRK", "break / interrupt", FL); -const BVC = new Op("BVC", "branch on overflow clear", BRA, true); -const BVS = new Op("BVS", "branch on overflow set", BRA, true); +const BMI = new Op("BMI", "branch on minus (negative set)", BRA, [OpSemantics.IS_CONDITIONAL_JUMP]); +const BNE = new Op("BNE", "branch on not equal (zero clear)", BRA, [OpSemantics.IS_CONDITIONAL_JUMP]); +const BPL = new Op("BPL", "branch on plus (negative clear)", BRA, [OpSemantics.IS_CONDITIONAL_JUMP]); +const BRK = new Op("BRK", "break / interrupt", FL, [OpSemantics.IS_BREAK]); +const BVC = new Op("BVC", "branch on overflow clear", BRA, [OpSemantics.IS_CONDITIONAL_JUMP]); +const BVS = new Op("BVS", "branch on overflow set", BRA, [OpSemantics.IS_CONDITIONAL_JUMP]); const CLC = new Op("CLC", "clear carry", MATH); const CLD = new Op("CLD", "clear decimal", SR); const CLI = new Op("CLI", "clear interrupt disable", INT); @@ -160,8 +160,8 @@ const EOR = new Op("EOR", "exclusive or (with accumulator)", LG); const INC = new Op("INC", "increment", MATH); const INX = new Op("INX", "increment X", MATH); const INY = new Op("INY", "increment Y", MATH); -const JMP = new Op("JMP", "jump", BRA, true); -const JSR = new Op("JSR", "jump subroutine", SUB, true); +const JMP = new Op("JMP", "jump", BRA, [OpSemantics.IS_UNCONDITIONAL_JUMP]); +const JSR = new Op("JSR", "jump subroutine", SUB, [OpSemantics.IS_UNCONDITIONAL_JUMP]); const LDA = new Op("LDA", "load accumulator", MEM); const LDX = new Op("LDX", "load X", MEM); const LDY = new Op("LDY", "load Y", MEM); @@ -174,15 +174,15 @@ const PLA = new Op("PLA", "pull accumulator", ST); const PLP = new Op("PLP", "pull processor status (SR)", ST); const ROL = new Op("ROL", "rotate left", MATH); const ROR = new Op("ROR", "rotate right", MATH); -const RTI = new Op("RTI", "return from interrupt", INT); -const RTS = new Op("RTS", "return from subroutine", SUB); +const RTI = new Op("RTI", "return from interrupt", INT, [OpSemantics.IS_RETURN]); +const RTS = new Op("RTS", "return from subroutine", SUB, [OpSemantics.IS_RETURN]); const SBC = new Op("SBC", "subtract with carry", MATH); const SEC = new Op("SEC", "set carry", SR); const SED = new Op("SED", "set decimal", SR); const SEI = new Op("SEI", "set interrupt disable", INT); -const STA = new Op("STA", "store accumulator", MEM); -const STX = new Op("STX", "store X", MEM); -const STY = new Op("STY", "store Y", MEM); +const STA = new Op("STA", "store accumulator", MEM, [OpSemantics.IS_STORE]); +const STX = new Op("STX", "store X", MEM, [OpSemantics.IS_STORE]); +const STY = new Op("STY", "store Y", MEM, [OpSemantics.IS_STORE]); const TAX = new Op("TAX", "transfer accumulator to X", TR); const TAY = new Op("TAY", "transfer accumulator to Y", TR); const TSX = new Op("TSX", "transfer stack pointer to X", TR); diff --git a/client/test/machine/Thread.test.ts b/client/test/machine/Thread.test.ts index 02efefa..f7878a4 100644 --- a/client/test/machine/Thread.test.ts +++ b/client/test/machine/Thread.test.ts @@ -1,7 +1,9 @@ import {expect} from 'chai'; -import {ArrayMemory, LE} from "../../src/machine/core"; +import {LE} from "../../src/machine/core"; import {Thread} from "../../src/machine/Thread"; import {createDisassembler} from "./util"; +import {ArrayMemory} from "../../src/machine/Memory"; +import {Mos6502} from "../../src/machine/mos6502"; describe("thread", () => { it("records executed instructions", () => { @@ -10,6 +12,27 @@ describe("thread", () => { const d = createDisassembler(contents, 2); const t = new Thread("test", d, 0, memory); t.step(); - expect(t.getExecuted().length).to.eq(1); + expect(t.getExecuted().length).to.eq(1, "expected single step to have executed one instruction"); + }); + + it("executes JMP", () => { + const brk = Mos6502.ISA.byName("BRK").getBytes()[0]; + const contents = [ + 0, 0, // 0, 1 - load address + 0x4c, 0x06, 0x00, // 2, 3, 4 - JMP $0006 + brk, // 5 + 0xea, // 6 NOP + brk // 7 + ]; + const memory = new ArrayMemory(contents, LE, true, true); + const d = createDisassembler(contents, 2); + const t = new Thread("test", d, 2, memory); + expect(t.getPc()).to.eq(2, "start pc should be received by constructor"); + t.step(); // execute jump, should be at 6 NOP + expect(t.getPc()).to.eq(6); + t.step(); // should have executed + expect(t.getPc()).to.eq(7); + t.step(); + expect(t.getPc()).to.eq(8); }); }); diff --git a/client/test/machine/Tracer.test.ts b/client/test/machine/Tracer.test.ts index ef3a27c..a42b6f0 100644 --- a/client/test/machine/Tracer.test.ts +++ b/client/test/machine/Tracer.test.ts @@ -41,20 +41,46 @@ describe("tracer", () => { it.skip("handles unconditional jump", () => { // little endian addresses const bytes: number[] = [ - 0, 0, // 0, 1 base address + 0, 0, // $1000 base address // TODO assemble single line useful here - 0x4c, 0x06, 0x00, // 2, 3, 4 JMP $0006 - ...Mos6502.ISA.byName("BRK").getBytes(), // 5 hits this if no jump - ...Mos6502.ISA.byName("NOP").getBytes(), // 6 jump target - ...Mos6502.ISA.byName("BRK").getBytes(), // 7 stop + 0x4c, 0x06, 0x10, // $1000, $1001, $1002 JMP $1006 + ...Mos6502.ISA.byName("BRK").getBytes(), // $1003 stops here if no jump + ...Mos6502.ISA.byName("NOP").getBytes(), // $1004 jump target + ...Mos6502.ISA.byName("BRK").getBytes(), // $1005 stop ]; const d = createDisassembler(bytes, 2); const t = new Tracer(d, 2, mem(bytes)); t.step(); // execute JMP t.step(); // execute NOP t.step(); // execute BRK + // currently fails because JMP is not implemented in Tracer + expect(t.executed()).to.not.have.members([2, 5], "JMP was ignored"); expect(t.executed()).to.have.members([2, 6, 7]); + }); + + // TODO decide how base address is supposed to work for a trace + // a trace is an emulator so the code needs to be "loaded" at an address. + // A binary expects to be loaded at a fixed address, otherwise the addresses are wrong. + it.skip("handles unconditional jump with base address", () => { + const bytes: number[] = [ + 0, 0x10, // $1000 base address + // TODO assemble single line useful here + 0x4c, 0x06, 0x10, // $1000, $1001, $1002 JMP $1006 + ...Mos6502.ISA.byName("BRK").getBytes(), // $1003 stops here if no jump + ...Mos6502.ISA.byName("NOP").getBytes(), // $1004 jump target + ...Mos6502.ISA.byName("BRK").getBytes(), // $1005 stop + ]; + const d = createDisassembler(bytes, 0x1000); + const t = new Tracer(d, 0x1000, mem(bytes)); + t.step(); // execute JMP + t.step(); // execute NOP + t.step(); // execute BRK + // currently fails because JMP is not implemented in Tracer + const executed = t.executed(); + expect(executed[0]).to.not.equal(2, "base address was ignored"); + expect(t.executed()).to.not.have.members([0x1000, 0x1003]); + expect(t.executed()).to.have.members([0x1000, 0x0004, 0x0005], "expected execution of jump"); }) }); diff --git a/client/test/machine/asm/Disassembler.test.ts b/client/test/machine/asm/Disassembler.test.ts index 71a02de..5ddd771 100644 --- a/client/test/machine/asm/Disassembler.test.ts +++ b/client/test/machine/asm/Disassembler.test.ts @@ -1,7 +1,9 @@ import {expect} from 'chai'; -import {ArrayMemory, LE} from "../../../src/machine/core"; +import {LE} from "../../../src/machine/core"; import {createDisassembler, niladicOpcodes} from "../util"; import {AddressingMode, MODE_ABSOLUTE, Mos6502} from "../../../src/machine/mos6502"; +import {OpSemantics} from "../../../src/machine/Op"; +import {ArrayMemory} from "../../../src/machine/Memory"; describe("disassembler", () => { it("disassembles single niladic instruction", () => { @@ -22,10 +24,10 @@ describe("disassembler", () => { const disassembled = d.disassemble1(mem, 2); const instruction = disassembled.instruction; const op = instruction.op; - expect(instruction.op.isJump).to.equal(true); + expect(instruction.op.has(OpSemantics.IS_UNCONDITIONAL_JUMP)).to.equal(true); expect(op.mnemonic).to.equal("JMP"); expect(instruction.mode).to.equal(MODE_ABSOLUTE); expect(disassembled.getBytes().length).to.equal(3); - expect(disassembled.operand16()).to.equal(0x6502); + expect(disassembled.operandValue()).to.equal(0x6502); }); }) \ No newline at end of file diff --git a/client/test/machine/util.ts b/client/test/machine/util.ts index ae86944..4f31bc1 100644 --- a/client/test/machine/util.ts +++ b/client/test/machine/util.ts @@ -1,8 +1,9 @@ import {Mos6502} from "../../src/machine/mos6502"; -import {ArrayMemory, LE} from "../../src/machine/core"; +import {LE} from "../../src/machine/core"; import {FileBlob} from "../../src/machine/FileBlob"; import {DisassemblyMetaImpl} from "../../src/machine/asm/DisassemblyMetaImpl"; import {Disassembler} from "../../src/machine/asm/Disassembler"; +import {ArrayMemory} from "../../src/machine/Memory"; /** * Return the bytes of each opcode in sequence - if there are several, chooses one "randomly" @@ -12,8 +13,12 @@ export function niladicOpcodes(niladics: string[]): number[] { return niladics.flatMap(op => Mos6502.ISA.byName(op).getBytes()) } -export function mem(contents: number[]) { - return new ArrayMemory(contents, LE, true, true); +export function mem(contents: number[], offset: number = 0) { + const arrayMemory = new ArrayMemory(contents, LE, true, true); + if (offset > 0) { + throw Error("not implemented"); + } + return arrayMemory; } export function createDisassembler(bytes: number[], contentStartOffset: number) {