cli: rewrite; symbols and tracing

This commit is contained in:
Steven Hugg committed 2026-08-24 11:23:13 +02:00
1 parent 22fc4b8219
commit dea718b91a
8 files changed
+1346 -959

No files matched your search

+2 -1
View File
@@ -10,7 +10,7 @@
},
"license": "GPL-3.0",
"bin": {
"8bws": "./gen/cli/8bws.js"
"8bws": "./gen/tools/8bws.js"
},
"dependencies": {
"@codemirror/commands": "^6.10.2",
@@ -90,6 +90,7 @@
"test-profile": "NODE_PATH=$(pwd) mocha --recursive --timeout 60000 --prof test/cli gen/test",
"test-worker": "NODE_PATH=$(pwd) mocha --timeout 60000 test/cli/testworker.js",
"test-platforms": "NODE_PATH=$(pwd) mocha --timeout 60000 test/cli/testplatforms.js",
"test-cli": "NODE_PATH=$(pwd) mocha --timeout 60000 test/cli/testcli.js",
"test-services": "NODE_PATH=$(pwd) mocha --timeout 60000 test/services",
"test-verilog": "NODE_PATH=$(pwd) mocha --timeout 60000 --reporter mocha-simple-html-reporter --reporter-options output=test/output/verilog.html test/verilog/testverilog.js",
"test-web-quick": "nightwatch -e chrome test/web/testembed.js",
+268 -755
View File
File diff suppressed because it is too large. Load diff
+201
View File
@@ -0,0 +1,201 @@
// Terminal output helpers for the 8bws CLI.
//
// Every command returns a CLIResult; output() renders it either as JSON
// (--json) or as colorized text. Keeping this out of 8bws.ts leaves the
// command implementations free of ANSI noise.
import { hex } from '../common/util';
export interface CLIResult {
success: boolean;
command: string;
data?: any;
error?: string;
}
export const c = {
reset: '\x1b[0m',
bold: '\x1b[1m',
dim: '\x1b[2m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
white: '\x1b[37m',
bgRed: '\x1b[41m',
bgGreen: '\x1b[42m',
};
let jsonMode = false;
let emit = console.log;
/**
* In --json mode stdout carries the CLIResult and nothing else, so the
* compiler's chatter (plain console.log from the worker) is pushed to stderr.
*/
export function setJsonMode(on: boolean) {
jsonMode = on;
if (!on) return;
emit = console.log.bind(console);
console.log = (...args: any[]) => process.stderr.write(args.join(' ') + '\n');
}
export function isJsonMode() { return jsonMode; }
/** Human-facing output: stdout normally, stderr in --json mode. */
export function write(s: string): void {
(jsonMode ? process.stderr : process.stdout).write(s);
}
// progress/status chatter -- suppressed in --json mode so stdout stays parseable
export function note(msg: string): void {
if (!jsonMode) process.stderr.write(`${c.dim}${msg}${c.reset}\n`);
}
let emitted = false;
/** True once a CLIResult has been printed -- lets main() detect a silent exit. */
export function hasOutput(): boolean { return emitted; }
export function output(result: CLIResult): void {
emitted = true;
if (jsonMode) {
emit(JSON.stringify(result, null, 2));
return;
}
process.stderr.write(result.success
? `${c.bgGreen}${c.bold}${c.white} OK ${c.reset} `
: `${c.bgRed}${c.bold}${c.white} FAIL ${c.reset} `);
process.stderr.write(`${c.bold}${c.cyan}${result.command}${c.reset}\n`);
if (result.error) {
process.stderr.write(`${c.red}Error: ${result.error}${c.reset}\n`);
}
if (result.data) {
(FORMATTERS[result.command] || formatGeneric)(result.data);
}
}
// exit with a formatted error message
export function fail(command: string, error: string, data?: any): never {
output({ success: false, command, error, data });
process.exit(1);
}
const FORMATTERS: { [command: string]: (data: any) => void } = {
help: formatHelp,
build: formatBuild,
run: formatRun,
'list-tools': formatList('tools', 'Available tools'),
'list-platforms': formatPlatforms,
};
function formatHelp(data: any): void {
console.log(`\n${c.bold}Usage:${c.reset} 8bws <command> [options] <file>\n`);
console.log(`${c.bold}Commands:${c.reset}`);
for (const [cmd, usage] of Object.entries(data.commands || {})) {
console.log(` ${c.green}${cmd.padEnd(14)}${c.reset}${c.dim}${usage}${c.reset}`);
}
if (data.options) {
for (const [section, opts] of Object.entries(data.options as any)) {
console.log(`\n${c.bold}${section}:${c.reset}`);
for (const [flag, desc] of Object.entries(opts as any)) {
console.log(` ${c.yellow}${flag.padEnd(22)}${c.reset}${c.dim}${desc}${c.reset}`);
}
}
}
if (data.script) {
console.log(`\n${c.bold}Script commands${c.reset} ${c.dim}(-e / --script)${c.reset}\n${data.script}`);
}
console.log();
}
function formatBuild(data: any): void {
if (data.errors) {
for (const err of data.errors) {
let loc = '';
if (err.path) loc += `${c.cyan}${err.path}${c.reset}`;
if (err.line) loc += `${c.dim}:${c.reset}${c.yellow}${err.line}${c.reset}`;
if (loc) loc += ` ${c.dim}-${c.reset} `;
console.log(` ${c.red}●${c.reset} ${loc}${err.msg || err.message || JSON.stringify(err)}`);
}
return;
}
field('Tool', data.tool, c.green);
field('Platform', data.platform, c.green);
field('Source', data.source, c.cyan);
if (data.outputSize != null) field('Size', `${data.outputSize} bytes`, c.yellow);
field('Output', data.outputFile, c.cyan);
if (data.symbolmap) {
console.log(`\n${c.bold}Symbols${c.reset} ${c.dim}(${Object.keys(data.symbolmap).length})${c.reset}`);
for (const [name, addr] of Object.entries(data.symbolmap).sort((a: any, b: any) => a[1] - b[1])) {
console.log(` ${c.cyan}$${hex(addr as number, 4)}${c.reset} ${name}`);
}
}
if (data.segments) {
console.log(`\n${c.bold}Segments${c.reset} ${c.dim}(${data.segments.length})${c.reset}`);
for (const seg of data.segments) {
console.log(` ${c.green}${seg.name.padEnd(16)}${c.reset} ${c.cyan}$${hex(seg.start, 4)}${c.reset} ${c.dim}size${c.reset} ${c.yellow}${seg.size}${c.reset}`);
}
}
if (data.saveDir) {
console.log(`\n${c.bold}Saved to${c.reset} ${c.cyan}${data.saveDir}${c.reset} ${c.dim}(${data.savedFiles.length} files)${c.reset}`);
for (const f of data.savedFiles) console.log(` ${c.dim}●${c.reset} ${f}`);
}
}
function formatRun(data: any): void {
field('Platform', data.platform, c.green);
field('ROM', data.rom, c.cyan);
if (data.frames != null) field('Frames', data.frames, c.yellow);
if (data.width) field('Video', `${data.width}x${data.height}`, c.yellow);
field('Screenshot', data.png, c.cyan);
}
function formatList(key: string, title: string) {
return (data: any) => {
console.log(`\n${c.bold}${title}${c.reset} ${c.dim}(${data[key].length})${c.reset}\n`);
for (const item of data[key]) console.log(` ${c.green}●${c.reset} ${item}`);
console.log();
};
}
function formatPlatforms(data: any): void {
console.log(`\n${c.bold}Available platforms${c.reset} ${c.dim}(${data.count})${c.reset}\n`);
const byArch: { [arch: string]: string[] } = {};
for (const [name, info] of Object.entries(data.platforms) as [string, any][]) {
(byArch[info.arch || 'unknown'] ||= []).push(name);
}
for (const [arch, platforms] of Object.entries(byArch).sort()) {
console.log(` ${c.bold}${c.magenta}${arch}${c.reset}`);
for (const p of platforms.sort()) console.log(` ${c.green}●${c.reset} ${p}`);
}
console.log();
}
function formatGeneric(data: any): void {
for (const [key, value] of Object.entries(data)) {
const v = (typeof value === 'object' && value !== null) ? JSON.stringify(value) : value;
console.log(` ${c.dim}${key}:${c.reset} ${v}`);
}
}
function field(label: string, value: any, color: string): void {
if (value == null || value === false) return;
console.log(` ${c.dim}${(label + ':').padEnd(11)}${c.reset}${color}${value}${c.reset}`);
}
// shared hexdump used by 'mem' and --memdump
export function hexdump(read: (addr: number) => number, start: number, end: number, out = write): void {
for (let ofs = 0; start + ofs <= end; ofs += 16) {
let line = `${hex(start + ofs, 4)}:`;
let ascii = '';
for (let i = 0; i < 16 && start + ofs + i <= end; i++) {
if (i === 8) line += ' ';
const byte = read(start + ofs + i);
line += ` ${hex(byte)}`;
ascii += (byte >= 0x20 && byte < 0x7f) ? String.fromCharCode(byte) : '.';
}
out(`${line.padEnd(60)} ${ascii}\n`);
}
}
+326
View File
@@ -0,0 +1,326 @@
// Unified headless driver for the emulators in this codebase.
//
// A *Platform* (src/platform/*) is the full object the IDE drives: it owns its
// own video/audio/timer and implements the Platform interface in
// common/baseplatform.ts. Most platforms are built on a *Machine* (src/machine/*),
// the bare emulation core made of the interfaces in common/devices.ts -- Bus,
// FrameBased, HasCPU, VideoSource and friends.
//
// EmuTarget drives a Platform headlessly, reaching through to its Machine when
// there is one for the things only the core can do (exact traps, single
// stepping, the probe). The CLI and the run-script interpreter never have to
// ask which of those a target supports; the capability sniffing lives here.
import {
CpuState, DisasmLine, EmuState, Machine, Platform, hasProbe, isDebuggable,
} from "../common/baseplatform";
import { ProbeAll, TrapCondition } from "../common/devices";
import { disassemble6502 } from "../common/cpu/disasm6502";
import { disassembleZ80 } from "../common/cpu/disasmz80";
import { disassembleSM83 } from "../common/cpu/disasmSM83";
import { disassembleF8 } from "../common/cpu/disasmF8";
import { disassembleHuC6280 } from "../common/cpu/disasmHuC6280";
import { CPU6809 } from "../common/cpu/6809";
import { PLATFORMS } from "../common/emu";
import * as emu from "../common/emu";
import { getRootBasePlatform } from "../common/util";
import { mockAudio, mockDOM, mockFetch, mockGlobals } from "./nodemock";
export interface VideoOutput {
pixels: Uint32Array;
width: number;
height: number;
}
export interface DebugSection {
category: string;
text: string;
}
/** Disassemblers, keyed by the `arch` name used in the worker's platform params. */
export const DISASSEMBLERS: { [arch: string]: (addr: number, read: (a: number) => number) => DisasmLine } = {
'6502': (a, r) => disassemble6502(a, r(a), r(a + 1), r(a + 2)),
'huc6280': (a, r) => disassembleHuC6280(a, r(a), r(a + 1), r(a + 2)),
'z80': (a, r) => disassembleZ80(a, r(a), r(a + 1), r(a + 2), r(a + 3)),
'gbz80': (a, r) => disassembleSM83(a, r(a), r(a + 1), r(a + 2)),
'sm83': (a, r) => disassembleSM83(a, r(a), r(a + 1), r(a + 2)),
'f8': (a, r) => disassembleF8(a, r(a), r(a + 1), r(a + 2)),
'6809': (a, r) => Object.create(CPU6809()).disasm(r(a), r(a + 1), r(a + 2), r(a + 3), r(a + 4), a),
};
/** CPU class name -> arch, for the platforms that don't implement disassemble(). */
const CPU_ARCH: { [cls: string]: string } = {
'MOS6502': '6502', 'HuC6280': 'huc6280',
'Z80': 'z80', 'ZilogZ80': 'z80',
'SM83': 'sm83', 'F8CPU': 'f8', 'CPU6809': '6809',
};
/** Work out which disassembler a CPU object wants. */
function archOf(cpu: any): string {
const arch = CPU_ARCH[cpu?.constructor?.name];
if (arch) return arch;
// CPU6809 is a factory returning a plain object, so it has no class name to
// match; it is also the only CPU here that carries its own disassembler.
if (typeof cpu?.disasm === 'function') return '6809';
return '';
}
// Frames to run before giving up on a `runUntil` predicate that never fires.
export const DEFAULT_MAX_FRAMES = 1000;
// Upper bound on clocks in one instruction, so stepping can't spin forever.
const MAX_CYCLES_PER_INSN = 64;
/**
* Headless stand-ins for RasterVideo/VectorVideo/AnimationTimer. Platform
* modules pick these up because Platform.start() reads them off the emu module.
*/
function installHeadlessVideo() {
let pixels: Uint32Array | null = null;
let params: { width: number, height: number } | null = null;
(emu as any).RasterVideo = function (_el: any, width: number, height: number) {
const buffer = new ArrayBuffer(width * height * 4);
const datau8 = new Uint8Array(buffer);
const datau32 = new Uint32Array(buffer);
params = { width, height };
pixels = datau32;
this.create = function () { this.width = width; this.height = height; };
this.setKeyboardEvents = function () { };
this.getFrameData = function () { return datau32; };
this.getImageData = function () { return { data: datau8, width, height }; };
this.updateFrame = function () { };
this.clearRect = function () { };
this.setupMouseEvents = function () { };
this.canvas = this;
this.getContext = function () { return this; };
this.fillRect = function () { };
this.fillStyle = '';
this.putImageData = function () { };
this.style = {};
};
(emu as any).VectorVideo = function () {
this.create = function () { this.drawops = 0; };
this.setKeyboardEvents = function () { };
this.clear = function () { };
this.drawLine = function () { this.drawops++; };
};
(emu as any).AnimationTimer = function () {
this.running = false;
this.start = function () { };
this.stop = function () { };
this.isRunning = function () { return this.running; };
};
return {
get(): VideoOutput | null {
return pixels && params ? { pixels, width: params.width, height: params.height } : null;
}
};
}
export class EmuTarget {
frameCount = 0;
private video: ReturnType<typeof installHeadlessVideo>;
constructor(readonly id: string, readonly platform: Platform) {
this.video = installHeadlessVideo();
}
/**
* The underlying Machine, if there is one. Platforms built on
* BaseMachinePlatform expose theirs, which is what lets us stop mid-frame
* through the TrapCondition the FrameBased interface already defines,
* instead of guessing at cycle counts.
*/
get machine(): Machine | null {
return (this.platform as any).machine || null;
}
async start() { await this.platform.start(); }
reset() { this.platform.reset(); }
loadROM(data: Uint8Array, title = 'ROM') { this.platform.loadROM(title, data); }
loadBIOS(data: Uint8Array, title = 'BIOS'): boolean {
if (!this.platform.loadBIOS) return false;
this.platform.loadBIOS(title, data);
return true;
}
read(addr: number): number {
if (this.platform.readAddress) return this.platform.readAddress(addr);
const m = this.machine;
if (m) return m.readConst ? m.readConst(addr) : m.read(addr);
throw new Error(`platform '${this.id}' cannot read memory`);
}
getCPUState(): CpuState | null {
try { return this.platform.getCPUState ? this.platform.getCPUState() : null; }
catch (e) { return null; }
}
getPC(): number | null {
// Platforms may correct the PC (see debugPCDelta), so prefer getPC().
try { if (this.platform.getPC) return this.platform.getPC(); } catch (e) { }
const s = this.getCPUState();
return s ? s.PC : null;
}
disassemble(addr: number): DisasmLine | null {
const read = (a: number) => this.read(a);
try {
if (this.platform.disassemble) return this.platform.disassemble(addr, read);
// a few platforms leave it out; fall back to the machine's CPU
const disasm = DISASSEMBLERS[archOf(this.machine?.cpu)];
return disasm ? disasm(addr, read) : null;
} catch (e) { return null; }
}
setKeyInput(key: number, code: number, flags: number) {
const target: any = this.machine || this.platform;
if (typeof target.setKeyInput !== 'function') {
throw new Error(`platform '${this.id}' does not accept key input`);
}
target.setKeyInput(key, code, flags);
}
connectProbe(probe: ProbeAll | null): boolean {
const m = this.machine;
if (!m || !hasProbe(m)) return false;
m.connectProbe(probe);
return true;
}
getVideo(): VideoOutput | null { return this.video.get(); }
saveState(): EmuState | null { return this.platform.saveState ? this.platform.saveState() : null; }
getDebugInfo(): DebugSection[] {
const state = this.saveState();
const p: any = this.platform;
if (!state || !isDebuggable(p) || !p.getDebugCategories) return [];
const sections: DebugSection[] = [];
for (const category of p.getDebugCategories() || []) {
try {
const text = p.getDebugInfo(category, state);
if (text) sections.push({ category, text });
} catch (e) { /* a category that doesn't apply to this state */ }
}
return sections;
}
//// execution control, driven by what the target turns out to support
/** True if execution can be stopped mid-frame at an exact instruction. */
get supportsTrap(): boolean { return this.machine != null; }
/** True if single instructions can be stepped. */
get supportsStep(): boolean { return this.supportsTrap; }
/**
* A function that advances the CPU by the smallest amount the machine
* supports -- one clock for ClockBased CPUs, one instruction for
* InstructionBased ones -- without disturbing the frame loop. Null if the
* machine only knows how to run whole frames.
*/
private clockStepper(): (() => void) | null {
const m = this.machine as any;
if (!m) return null;
if (typeof m.advanceCPU === 'function') return () => m.advanceCPU(); // BasicHeadlessMachine
if (typeof m.advanceFrameClock === 'function') return () => m.advanceFrameClock(null, 1); // WASM
return null;
}
/**
* Finish any partly-executed instruction, so getPC() names a real
* instruction. Frames can end mid-instruction on clock-based CPUs.
*/
settle(): void {
const clock = this.clockStepper();
if (!clock) return;
for (let i = 0; i < MAX_CYCLES_PER_INSN && !this.isStable(); i++) clock();
}
advanceFrame(trap?: TrapCondition | null): void {
const m = this.machine;
// With a trap we drive the Machine directly -- Platform.advance() only
// honors traps registered as breakpoints, and installing/removing those
// rewinds BaseDebugPlatform's saved state.
if (trap && m) {
m.advanceFrame(trap);
} else {
const p = this.platform as any;
if (p.nextFrame) p.nextFrame();
else if (p.advance) p.advance(false);
}
this.frameCount++;
}
isStable(): boolean {
return this.machine ? this.machine.cpu.isStable() : true;
}
/** Execute exactly one instruction. Returns false if the target can't step. */
stepInsn(): boolean {
if (!this.machine) return false;
const clock = this.clockStepper();
if (clock) {
this.settle(); // don't count an in-flight instruction as this step
clock();
for (let i = 0; i < MAX_CYCLES_PER_INSN && !this.isStable(); i++) clock();
return true;
}
// No clock-level entry point: stop at the next instruction boundary via
// the frame trap. This restarts the frame, so it is the last resort.
let boundaries = 0;
for (let frames = 0; frames < 2 && boundaries < 2; frames++) {
this.advanceFrame(() => this.isStable() && ++boundaries >= 2);
}
return true;
}
/**
* Run until `pred` is true, up to `maxFrames` frames. Exact to the
* instruction on targets with a Machine; frame-granular otherwise.
*/
runUntil(pred: () => boolean, maxFrames = DEFAULT_MAX_FRAMES): boolean {
const start = this.frameCount;
while (this.frameCount - start < maxFrames) {
if (this.supportsTrap) {
let hit = false;
this.advanceFrame(() => (hit = this.isStable() && pred()));
if (hit) return true;
} else {
this.advanceFrame();
if (pred()) return true;
}
}
return false;
}
/** Run until the PC reaches one of `addrs`. */
runToPC(addrs: Set<number>, maxFrames = DEFAULT_MAX_FRAMES): boolean {
return this.runUntil(() => addrs.has(this.getPC()), maxFrames);
}
}
/** Load a platform module by ID (e.g. "nes", "c64.wasm", "atari8-5200"). */
export async function loadPlatform(platformId: string): Promise<EmuTarget> {
installNodeMocks();
const baseId = getRootBasePlatform(platformId);
await import('../platform/' + baseId);
const PlatformClass = PLATFORMS[platformId] || PLATFORMS[baseId];
if (!PlatformClass) {
throw new Error(`Platform '${platformId}' not found. Available: ${Object.keys(PLATFORMS).sort().join(', ')}`);
}
return new EmuTarget(platformId, new PlatformClass(null));
}
let mocksInstalled = false;
/** Stub out the browser APIs that the platform modules expect. */
export function installNodeMocks() {
if (mocksInstalled) return;
mocksInstalled = true;
mockGlobals();
mockAudio();
mockFetch();
mockDOM();
}
-202
View File
@@ -1,202 +0,0 @@
import { hasAudio, hasSerialIO, hasVideo, Machine, Platform } from "../common/baseplatform";
import { SerialIOInterface } from "../common/devices";
import { PLATFORMS } from "../common/emu";
import * as emu from "../common/emu";
import { getRootBasePlatform } from "../common/util";
import { mockAudio, mockDOM, mockFetch, mockGlobals, NullAudio } from "./nodemock";
// TODO: merge with platform
class SerialTestHarness implements SerialIOInterface {
bufferedRead: boolean = true;
cyclesPerByte = 1000000 / (57600 / 8); // 138.88888 cycles
maxOutputBytes = 4096;
inputBytes: Uint8Array;
outputBytes: number[];
inputIndex: number;
clk: number;
bufin: string;
clearToSend(): boolean {
return this.outputBytes.length < this.maxOutputBytes;
}
sendByte(b: number) {
if (this.clearToSend()) {
this.outputBytes.push(b);
}
}
byteAvailable(): boolean {
return this.readIndex() > this.inputIndex;
}
recvByte(): number {
var index = this.readIndex();
this.inputIndex = index;
var b = this.inputBytes[index] | 0;
return b;
}
readIndex(): number {
return this.bufferedRead ? (this.inputIndex + 1) : Math.floor(this.clk / this.cyclesPerByte);
}
reset() {
this.inputIndex = -1;
this.clk = 0;
this.outputBytes = [];
this.bufin = '';
}
advance(clocks: number) {
this.clk += clocks;
}
}
///
// Headless mock for RasterVideo/VectorVideo (used by Platform.start() in Node)
// Patches the emu module exports so that platform modules pick up the mock.
function installHeadlessVideo() {
var lastPixels: Uint32Array | null = null;
var lastVideoParams: { width: number; height: number } | null = null;
(emu as any).RasterVideo = function (_mainElement: any, width: number, height: number, _options?: any) {
var buffer = new ArrayBuffer(width * height * 4);
var datau8 = new Uint8Array(buffer);
var datau32 = new Uint32Array(buffer);
lastVideoParams = { width, height };
lastPixels = datau32;
this.create = function () { this.width = width; this.height = height; };
this.setKeyboardEvents = function () { };
this.getFrameData = function () { return datau32; };
this.getImageData = function () { return { data: datau8, width, height }; };
this.updateFrame = function () { };
this.clearRect = function () { };
this.setupMouseEvents = function () { };
this.canvas = this;
this.getContext = function () { return this; };
this.fillRect = function () { };
this.fillStyle = '';
this.putImageData = function () { };
this.style = {};
};
(emu as any).VectorVideo = function (_mainElement: any, _width: number, _height: number, _options?: any) {
this.create = function () { this.drawops = 0; };
this.setKeyboardEvents = function () { };
this.clear = function () { };
this.drawLine = function () { this.drawops++; };
};
// Also mock AnimationTimer to be a no-op in headless mode
(emu as any).AnimationTimer = function (_fps: number, _callback: any) {
this.running = false;
this.start = function () { };
this.stop = function () { };
this.isRunning = function () { return this.running; };
};
return { getPixels: () => lastPixels, getVideoParams: () => lastVideoParams };
}
///
export class PlatformRunner {
platform: Platform;
private headless: ReturnType<typeof installHeadlessVideo>;
constructor(platform: Platform) {
mockGlobals();
mockAudio();
mockFetch();
mockDOM();
this.platform = platform;
this.headless = installHeadlessVideo();
}
async start() {
await this.platform.start();
}
loadROM(title: string, data: Uint8Array) {
this.platform.loadROM(title, data);
}
run() {
// nextFrame() is on BaseDebugPlatform, not the Platform interface
if ((this.platform as any).nextFrame) {
(this.platform as any).nextFrame();
} else if (this.platform.advance) {
this.platform.advance(false);
}
}
get pixels(): Uint32Array | null {
return this.headless.getPixels();
}
get videoParams(): { width: number; height: number } | null {
return this.headless.getVideoParams();
}
}
export async function loadPlatform(platformId: string): Promise<Platform> {
// Derive the base module name (e.g. "nes" from "nes-asm", "c64" from "c64.wasm")
var baseId = getRootBasePlatform(platformId);
// Dynamically load the platform module which registers into PLATFORMS
await import('../platform/' + baseId);
var PlatformClass = PLATFORMS[platformId];
if (!PlatformClass) {
// Try the base platform ID
PlatformClass = PLATFORMS[baseId];
}
if (!PlatformClass) {
var available = Object.keys(PLATFORMS).join(', ');
throw new Error(`Platform '${platformId}' not found. Available: ${available}`);
}
var platform = new PlatformClass(null);
return platform;
}
///
export class MachineRunner {
machine: Machine;
pixels: Uint32Array;
serial: SerialTestHarness;
constructor(machine: Machine) {
this.machine = machine;
}
setup() {
if (hasVideo(this.machine)) {
var vid = this.machine.getVideoParams();
this.pixels = new Uint32Array(vid.width * vid.height);
this.machine.connectVideo(this.pixels);
}
if (hasAudio(this.machine)) {
this.machine.connectAudio(new NullAudio());
}
if (hasSerialIO(this.machine)) {
this.serial = new SerialTestHarness();
this.machine.connectSerialIO(this.serial);
}
this.machine.reset();
}
run() {
this.machine.advanceFrame(null);
}
}
export async function loadMachine(modname: string, clsname: string, ...ctorArgs: any[]): Promise<Machine> {
var mod = await import('../machine/' + modname);
var cls = mod[clsname];
if (!cls) {
throw new Error(`Class '${clsname}' not found in module '../machine/${modname}'`);
}
var machine = new cls(...ctorArgs);
return machine;
}
async function runMachine() {
var machine = await loadMachine(process.argv[2], process.argv[3]);
var runner = new MachineRunner(machine);
runner.setup();
runner.run();
console.log(runner.machine.saveState());
}
if (require.main === module) {
runMachine();
}
+382
View File
@@ -0,0 +1,382 @@
// Run-script interpreter: a small command language for driving a headless
// emulator. One command per line, separated by newlines or ';'; '#' or '//'
// starts a comment.
//
// Everything here talks to an EmuTarget, so the same script works against a
// Platform (--platform) or a bare Machine (--machine). Commands that need a
// capability the target lacks report that instead of failing silently.
import { KeyFlags } from '../common/emu';
import { ProbeFlags, ProbeRecorder } from '../common/probe';
import { hex } from '../common/util';
import { hexdump, write } from './cliformat';
import { DEFAULT_MAX_FRAMES, EmuTarget } from './emutarget';
export const RUN_SCRIPT_HELP = [
'Execution:',
' run N | wait N - advance N frames (default 1)',
' break ADDR [MAXFRAMES] - stop when PC==ADDR (alias: runto)',
' step [N] - execute N instructions (default 1)',
' trace [MAXLINES] ADDR - run until PC==ADDR, then log every',
' instruction until the routine returns',
' hist [MAXLINES] - last N instructions from the trace buffer',
'Inspection & input:',
' key KEY - press key (down, 3 frames, up)',
' keydown KEY / keyup KEY - raw key down/up events',
' mem START [LEN] - hexdump memory (default 16 bytes)',
' screen [START] [COLS] [ROWS]- decode screen RAM to text (default $0400 40x25)',
' pc [N] - print PC + disassembly of N instructions',
' info - platform/machine debug info',
' reset - reset the emulator',
' echo TEXT - print message',
'(ADDR: number or symbol name; KEY: char, ENTER/SPACE/arrows/F1.., $hex;',
' prefixes SHIFT+, CTRL+)',
].join('\n');
const KEY_NAMES: { [name: string]: number } = {
'ENTER': 13, 'RETURN': 13, 'CR': 13,
'SPACE': 32, 'ESC': 27, 'TAB': 9, 'BACKSPACE': 8, 'BS': 8,
'DELETE': 46, 'DEL': 46, 'INSERT': 45,
'LEFT': 37, 'UP': 38, 'RIGHT': 39, 'DOWN': 40,
'HOME': 36, 'END': 35, 'PAGEUP': 33, 'PAGEDOWN': 34,
'F1': 112, 'F2': 113, 'F3': 114, 'F4': 115, 'F5': 116, 'F6': 117,
'F7': 118, 'F8': 119, 'F9': 120, 'F10': 121, 'F11': 122, 'F12': 123,
};
export function parseNum(s: string): number {
s = s.trim();
if (s.startsWith('$')) return parseInt(s.substring(1), 16);
if (/^0x[0-9a-f]+$/i.test(s)) return parseInt(s, 16);
if (/^-?\d+$/.test(s)) return parseInt(s, 10);
throw new Error(`bad number '${s}'`);
}
function parseKeyValue(tok: string): { key: number; flags: number } {
let flags = 0;
let t = tok.toUpperCase();
for (; ;) {
if (t.startsWith('SHIFT+')) { flags |= KeyFlags.Shift; t = t.substring(6); }
else if (t.startsWith('CTRL+')) { flags |= KeyFlags.Ctrl; t = t.substring(5); }
else break;
}
if (KEY_NAMES[t] != null) return { key: KEY_NAMES[t], flags };
if (t.length == 1) return { key: t.charCodeAt(0), flags };
if (/^0X[0-9A-F]+$/.test(t)) return { key: parseInt(t, 16), flags };
if (/^\d+$/.test(t)) return { key: parseInt(t, 10), flags };
throw new Error(`unknown key '${tok}'`);
}
// C64-style screen code -> ASCII (also close enough for VIC-20 et al)
function screenCodeToChar(code: number): string {
code &= 0xff;
if (code >= 0x40) code &= 0x3f; // reverse-video / graphics variants
if (code < 0x20) return '@ABCDEFGHIJKLMNOPQRSTUVWXYZ[£]^_'[code];
return String.fromCharCode(code); // $20-$3F identical to ASCII
}
/**
* Registers as "A=$12 X=$34 SP=$ff". CpuState carries whatever fields the CPU
* happens to save, including internals, so pick the set that matches.
*/
const REG_SETS: { when: string[], show: string[], flags?: string }[] = [
{ when: ['A', 'X', 'Y'], show: ['A', 'X', 'Y', 'SP'], flags: 'NVDIZC' }, // 6502
{ when: ['AF', 'HL'], show: ['AF', 'BC', 'DE', 'HL', 'IX', 'IY', 'SP'] }, // Z80 / SM83
{ when: ['A', 'B', 'DP'], show: ['A', 'B', 'X', 'Y', 'U', 'S', 'DP', 'CC'] }, // 6809
];
export function formatRegs(state: any): string {
if (!state) return '';
const set = REG_SETS.find((s) => s.when.every((f) => state[f] != null));
const names = set ? set.show : Object.keys(state).filter((k) => /^[A-Z]/.test(k) && k !== 'PC').slice(0, 6);
const parts: string[] = [];
for (const name of names) {
const value = state[name];
if (typeof value === 'number') parts.push(`${name}=$${hex(value, value > 0xff ? 4 : 2)}`);
}
if (set?.flags) {
// set flags uppercase, clear flags lowercase
const flags = [...set.flags].map((f) => (state[f] ? f : f.toLowerCase())).join('');
parts.push(flags);
}
return parts.join(' ');
}
// Trace buffer size; the most recent half is kept once it fills up.
const PROBE_BUFFER_SIZE = 0x400000;
export class RunScript {
symbols: { [name: string]: number } = {};
private addr2symbol: { [addr: number]: string } = {};
private probe: ProbeRecorder | null = null;
constructor(readonly target: EmuTarget, private out = write) { }
addSymbols(symbols: { [name: string]: number }) {
Object.assign(this.symbols, symbols);
for (const [name, addr] of Object.entries(this.symbols)) this.addr2symbol[addr] = name;
}
/** Start recording executed instructions, for the 'hist' command. */
startTracing(): boolean {
if (!this.target.machine) return false;
const rec = new ProbeRecorder(this.target.machine as any, PROBE_BUFFER_SIZE);
rec.singleFrame = false; // accumulate across frames
if (!this.target.connectProbe(rec)) return false;
this.probe = rec;
return true;
}
run(script: string) {
for (let line of script.split(/\r?\n|;/)) {
line = line.trim();
if (!line || line.startsWith('#') || line.startsWith('//')) continue;
const tokens = line.split(/\s+/);
const cmd = tokens[0].toLowerCase();
const handler = COMMANDS[cmd];
if (!handler) {
throw new Error(`unknown command '${cmd}'\nCommands:\n${RUN_SCRIPT_HELP}`);
}
try {
handler.call(this, tokens, line);
} catch (e: any) {
throw new Error(`script error on '${line}': ${e.message}`);
}
}
}
//// helpers used by the commands
private log(msg: string) { this.out(`[frame ${this.target.frameCount}] ${msg}\n`); }
private addr(tok: string): number {
try { return parseNum(tok); }
catch (e) {
const v = this.symbols[tok] ?? this.symbols[tok.replace(/^\./, '')];
if (v == null) throw new Error(`unknown address or symbol '${tok}'`);
return v;
}
}
private advance(n: number) {
// Keep the trace buffer from overflowing: drop the oldest half.
for (let i = 0; i < n; i++) {
const p = this.probe;
if (p && p.idx >= p.buf.length - 0x1000) {
const half = p.buf.length >> 1;
p.buf.copyWithin(0, p.idx - half, p.idx);
p.idx = half;
}
this.target.advanceFrame();
}
}
/** One disassembly line: "$ADDR bytes mnemonic" plus optional registers. */
private disasmLine(addr: number, withRegs: boolean): string {
const d = this.target.disassemble(addr);
let bytes = '';
if (d) for (let b = 0; b < d.nbytes; b++) bytes += hex(this.target.read(addr + b)) + ' ';
const label = this.addr2symbol[addr] ? `${this.addr2symbol[addr]}:\n` : '';
let line = `${label} $${hex(addr, 4)} ${bytes.padEnd(12)} ${(d ? d.line : '???').padEnd(20)}`;
if (withRegs) line += ' ' + formatRegs(this.target.getCPUState());
return line.replace(/\s+$/, '');
}
private disasmBlock(addr: number, count: number) {
for (let i = 0; i < count; i++) {
const d = this.target.disassemble(addr);
if (!d) { this.out(` $${hex(addr, 4)} (no disassembler for this target)\n`); return; }
this.out(this.disasmLine(addr, false) + '\n');
addr += d.nbytes;
}
}
private requireStep() {
if (!this.target.supportsStep) {
throw new Error(`'${this.target.id}' does not support instruction stepping`);
}
}
//// commands
cmdRun(tokens: string[]) {
const n = tokens[1] ? parseNum(tokens[1]) : 1;
this.advance(n);
this.log(`ran ${n} frame${n == 1 ? '' : 's'}`);
}
cmdStep(tokens: string[]) {
this.requireStep();
this.target.settle();
const n = tokens[1] ? parseNum(tokens[1]) : 1;
for (let i = 0; i < n; i++) {
this.out(this.disasmLine(this.target.getPC(), true) + '\n');
this.target.stepInsn();
}
this.log(`PC=$${hex(this.target.getPC(), 4)}`);
}
cmdBreak(tokens: string[]) {
if (!tokens[1]) throw new Error('break requires an address');
const addr = this.addr(tokens[1]);
const maxFrames = tokens[2] ? parseNum(tokens[2]) : DEFAULT_MAX_FRAMES;
const start = this.target.frameCount;
const hit = this.target.runToPC(new Set([addr]), maxFrames);
const pc = this.target.getPC();
const where = pc != null ? '$' + hex(pc, 4) : '?';
this.log(`break $${hex(addr, 4)}: ${hit ? 'HIT' : 'MISSED'} (pc=${where} after ${this.target.frameCount - start} frames)`);
if (!hit && !this.target.supportsTrap) {
this.out(` (note: '${this.target.id}' only checks the PC at frame boundaries)\n`);
}
}
cmdTrace(tokens: string[]) {
this.requireStep();
let i = 1;
let maxLines = 5000;
if (tokens.length > 2 && /^\d+$/.test(tokens[1])) maxLines = parseNum(tokens[i++]);
if (!tokens[i]) throw new Error('trace requires an address (trace [MAXLINES] ADDR)');
const targets = new Set<number>();
for (; i < tokens.length; i++) targets.add(this.addr(tokens[i]));
const start = this.target.frameCount;
if (!this.target.runToPC(targets)) {
this.log(`trace: address not reached within ${this.target.frameCount - start} frames`);
return;
}
const entrySP = (this.target.getCPUState() as any)?.SP ?? 0;
this.log(`--- trace ON at $${hex(this.target.getPC(), 4)} ---`);
let lines = 0;
let done = 'ran out of frames';
while (this.target.frameCount - start < DEFAULT_MAX_FRAMES) {
this.out(this.disasmLine(this.target.getPC(), true) + '\n');
if (++lines >= maxLines) { done = `line cap (${maxLines}) reached`; break; }
this.target.stepInsn();
// the routine returned once the stack has popped back past entry level
const sp = (this.target.getCPUState() as any)?.SP;
if (sp != null && sp > entrySP) { done = `returned after ${lines} instructions`; break; }
}
this.log(`--- trace OFF: ${done} ---`);
}
cmdHist(tokens: string[]) {
const p = this.probe;
if (!p) throw new Error(`'${this.target.id}' has no trace buffer (needs Probeable)`);
if (p.idx === 0) { this.out('(no trace buffer data)\n'); return; }
const maxlines = tokens[1] ? parseNum(tokens[1]) : 20;
// scan backwards for the start of the last `maxlines` events
let start = p.idx, count = 0;
while (start > 0 && count < maxlines) {
start--;
const op = p.buf[start] & 0xff000000;
if (op === ProbeFlags.EXECUTE || op === ProbeFlags.INTERRUPT) count++;
}
let shown = 0;
for (let i = start; i < p.idx && shown < maxlines; i++) {
const w = p.buf[i];
const op = w & 0xff000000;
if (op === ProbeFlags.EXECUTE) { this.out(this.disasmLine(w & 0xffffff, false) + '\n'); shown++; }
else if (op === ProbeFlags.INTERRUPT) { this.out(' --- INTERRUPT ---\n'); shown++; }
}
this.out(`(${shown} instructions shown, ${p.idx} events recorded)\n`);
}
cmdKey(tokens: string[]) {
if (!tokens[1]) throw new Error('key requires a key name');
const { key, flags } = parseKeyValue(tokens[1]);
this.target.setKeyInput(key, key, flags | KeyFlags.KeyDown);
this.advance(3);
this.target.setKeyInput(key, key, flags | KeyFlags.KeyUp);
this.advance(1);
this.log(`pressed ${tokens[1]} ($${hex(key, 2)})`);
}
cmdKeyDown(tokens: string[]) { this.keyEvent(tokens, KeyFlags.KeyDown, 'keydown'); }
cmdKeyUp(tokens: string[]) { this.keyEvent(tokens, KeyFlags.KeyUp, 'keyup'); }
private keyEvent(tokens: string[], flag: number, name: string) {
if (!tokens[1]) throw new Error(`${name} requires a key name`);
const { key, flags } = parseKeyValue(tokens[1]);
this.target.setKeyInput(key, key, flags | flag);
this.log(`${name} ${tokens[1]} ($${hex(key, 2)})`);
}
cmdMem(tokens: string[]) {
if (!tokens[1]) throw new Error('mem requires START [LEN]');
const start = this.addr(tokens[1]);
const len = tokens[2] ? parseNum(tokens[2]) : 16;
this.log(`mem $${hex(start, 4)}+$${hex(len, 4)}:`);
hexdump((a) => this.target.read(a), start, start + len - 1, this.out);
}
cmdScreen(tokens: string[]) {
const start = tokens[1] ? this.addr(tokens[1]) : 0x400;
const cols = tokens[2] ? parseNum(tokens[2]) : 40;
const rows = tokens[3] ? parseNum(tokens[3]) : 25;
this.log(`screen at $${hex(start, 4)} (${cols}x${rows}):`);
for (let y = 0; y < rows; y++) {
let line = '';
for (let x = 0; x < cols; x++) line += screenCodeToChar(this.target.read(start + y * cols + x));
this.out(`|${line.replace(/\s+$/, '')}|\n`);
}
}
cmdPC(tokens: string[]) {
this.target.settle();
const pc = this.target.getPC();
if (pc == null) throw new Error(`'${this.target.id}' does not report a PC`);
this.log(`PC=$${hex(pc, 4)}`);
this.disasmBlock(pc, tokens[1] ? parseNum(tokens[1]) : 8);
}
cmdInfo() {
this.target.settle();
const sections = this.target.getDebugInfo();
if (!sections.length) this.out(`(no debug info for '${this.target.id}')\n`);
for (const { category, text } of sections) {
this.out(`[${category}]\n${text}${text.endsWith('\n') ? '' : '\n'}`);
}
const pc = this.target.getPC();
if (pc != null && this.target.disassemble(pc)) {
this.out('[Disassembly]\n');
this.disasmBlock(pc, 16);
}
}
cmdReset() { this.target.reset(); this.log('reset'); }
cmdEcho(tokens: string[], line: string) { this.out(line.substring(tokens[0].length).trim() + '\n'); }
}
type Command = (this: RunScript, tokens: string[], line: string) => void;
const COMMANDS: { [name: string]: Command } = {
'run': RunScript.prototype.cmdRun,
'wait': RunScript.prototype.cmdRun,
'frames': RunScript.prototype.cmdRun,
'step': RunScript.prototype.cmdStep,
'break': RunScript.prototype.cmdBreak,
'runto': RunScript.prototype.cmdBreak,
'trace': RunScript.prototype.cmdTrace,
'hist': RunScript.prototype.cmdHist,
'key': RunScript.prototype.cmdKey,
'press': RunScript.prototype.cmdKey,
'keydown': RunScript.prototype.cmdKeyDown,
'keyup': RunScript.prototype.cmdKeyUp,
'mem': RunScript.prototype.cmdMem,
'screen': RunScript.prototype.cmdScreen,
'pc': RunScript.prototype.cmdPC,
'info': RunScript.prototype.cmdInfo,
'reset': RunScript.prototype.cmdReset,
'echo': RunScript.prototype.cmdEcho,
};
/** Parse a cc65/ca65 or VICE label file into a symbol map. */
export function parseSymbolFile(text: string): { [name: string]: number } {
const symbols: { [name: string]: number } = {};
for (const line of text.split(/\r?\n/)) {
const m1 = line.match(/^\s*([A-Za-z_][\w]*)\s*=\s*\$?([0-9A-Fa-f]+)\s*;/); // ca65/cc65 list
const m2 = line.match(/^\s*(?:al|add_label)\s+([0-9A-Fa-f]+)\s+\.?([A-Za-z_][\w]*)/); // VICE
if (m1) symbols[m1[1]] = parseInt(m1[2], 16);
else if (m2) symbols[m2[2]] = parseInt(m2[1], 16);
}
return symbols;
}
+4 -1
View File
@@ -73,11 +73,14 @@ function displayKitty(pngData: Uint8Array, w: number, h: number, scale: number):
const cols = Math.ceil(w * scale / 8);
const rows = Math.ceil(h * scale / 16);
// q=2 = silent mode: suppresses the terminal's OK/error acknowledgements.
// Without it, the terminal writes responses (\x1b_Gi=<id>;OK\x1b\\) to stdin,
// which leak into the shell's input buffer after we exit and garble the prompt.
for (let i = 0; i < b64.length; i += chunkSize) {
const chunk = b64.slice(i, i + chunkSize);
const more = i + chunkSize < b64.length ? 1 : 0;
if (i === 0) {
process.stdout.write(`\x1b_Gf=100,a=T,c=${cols},r=${rows},m=${more};${chunk}\x1b\\`);
process.stdout.write(`\x1b_Gf=100,a=T,c=${cols},r=${rows},q=2,m=${more};${chunk}\x1b\\`);
} else {
process.stdout.write(`\x1b_Gm=${more};${chunk}\x1b\\`);
}
+163
View File
@@ -0,0 +1,163 @@
var assert = require('assert');
var fs = require('fs');
var path = require('path');
var os = require('os');
var { execFileSync } = require('child_process');
const CLI = path.join(__dirname, '../../gen/tools/8bws.js');
// run the CLI and return { stdout, json } -- --json puts the CLIResult on stdout
function cli(...args) {
var stdout = execFileSync('node', [CLI, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
return stdout;
}
// in --json mode stdout is the CLIResult and nothing else
function cliJSON(...args) {
return JSON.parse(cli(...args, '--json'));
}
function cliFails(...args) {
try { cli(...args); } catch (e) { return e; }
assert.fail('expected the command to fail: ' + args.join(' '));
}
describe('8bws CLI', function () {
describe('discovery', function () {
it('should list platforms', function () {
var r = cliJSON('list-platforms');
assert.ok(r.success);
assert.ok(r.data.count > 20);
assert.equal(r.data.platforms['nes'].arch, '6502');
});
it('should put nothing but JSON on stdout in --json mode', function () {
// the compiler chatters on console.log; it must not corrupt the result
var r = cliJSON('run', '--platform', 'gb', 'presets/gb/hello.c', '--frames', '5');
assert.equal(r.data.frames, 5);
});
it('should list tools', function () {
var r = cliJSON('list-tools');
assert.ok(r.data.tools.includes('cc65'));
});
});
describe('build', function () {
it('should build a source file', function () {
var out = path.join(os.tmpdir(), '8bws-test-hello.gb');
var r = cliJSON('build', '--platform', 'gb', 'presets/gb/hello.c', '-o', out);
assert.ok(r.success, r.error);
assert.equal(r.data.tool, 'sdcc');
assert.ok(r.data.outputSize > 0);
assert.equal(fs.statSync(out).size, r.data.outputSize);
});
it('should check without writing output', function () {
var r = cliJSON('build', '--platform', 'gb', 'presets/gb/hello.c', '--check');
assert.ok(r.success, r.error);
assert.equal(r.command, 'check');
});
it('should report compile errors', function () {
var src = path.join(os.tmpdir(), '8bws-test-bad.c');
fs.writeFileSync(src, 'int main() { this is not C }\n');
var e = cliFails('build', '--platform', 'gb', src);
assert.ok(e.status !== 0);
});
it('should require a platform', function () {
cliFails('build', 'presets/gb/hello.c');
});
});
describe('run --platform', function () {
it('should run a ROM for N frames', function () {
var r = cliJSON('run', '--platform', 'nes', '--frames', '30', 'test/roms/nes/shoot2.c.rom');
assert.ok(r.success, r.error);
assert.equal(r.data.frames, 30);
assert.equal(r.data.width, 512);
});
it('should build and run a source file', function () {
var r = cliJSON('run', '--platform', 'gb', 'presets/gb/hello.c', '--frames', '10');
assert.ok(r.success, r.error);
assert.equal(r.data.frames, 10);
});
it('should break on a symbol from the build', function () {
var out = cli('run', '--platform', 'gb', 'presets/gb/hello.c', '-e', 'break _main 600; pc 2');
assert.ok(/break \$[0-9A-F]+: HIT/.test(out), out);
assert.ok(/_main:/.test(out), out);
});
it('should write a screenshot', function () {
var png = path.join(os.tmpdir(), '8bws-test-shot.png');
if (fs.existsSync(png)) fs.unlinkSync(png);
cliJSON('run', '--platform', 'nes', '--frames', '10', '--png', png, 'test/roms/nes/shoot2.c.rom');
assert.ok(fs.statSync(png).size > 0);
});
it('should infer the platform from a ROM extension', function () {
var gb = path.join(os.tmpdir(), '8bws-test-infer.gb');
fs.copyFileSync('test/roms/gb/cpu_instrs.gb', gb);
var r = cliJSON('run', gb, '--frames', '5');
assert.equal(r.data.platform, 'gb');
});
it('should refuse an unrecognized ROM without a platform', function () {
cliFails('run', 'test/roms/nes/shoot2.c.rom');
});
});
describe('run: emulator control', function () {
// these all reach through the Platform to its Machine (common/devices.ts)
it('should reject an unknown platform', function () {
cliFails('run', '--platform', 'nosuchplatform', '--frames', '1', 'test/roms/apple2/cosmic.c.rom');
});
it('should disassemble a 6502 platform', function () {
var out = cli('run', '--platform', 'apple2', '-e', 'run 20; pc 2', 'test/roms/apple2/cosmic.c.rom');
assert.ok(/PC=\$[0-9A-F]{4}/.test(out), out);
assert.ok(/\$[0-9A-F]{4}\s+[0-9A-F]{2}/.test(out), out);
});
it('should disassemble a z80 platform', function () {
var out = cli('run', '--platform', 'vicdual', '-e', 'run 20; pc 2', 'test/roms/vicdual/snake1.c.rom');
assert.ok(/PC=\$[0-9A-F]{4}/.test(out), out);
});
it('should disassemble a 6809 platform', function () {
var out = cli('run', '--platform', 'williams', '-e', 'run 20; pc 2', 'test/roms/williams/vidfill.asm.rom');
assert.ok(!/no disassembler/.test(out), out);
});
it('should step one instruction at a time', function () {
var out = cli('run', '--platform', 'apple2', '-e', 'run 20; step 3', 'test/roms/apple2/cosmic.c.rom');
var addrs = out.match(/^\s+\$[0-9A-F]{4}\s/gm) || [];
assert.equal(addrs.length, 3, out);
// 6502 registers and flags should be shown while stepping
assert.ok(/A=\$[0-9A-F]{2} X=\$[0-9A-F]{2} Y=\$[0-9A-F]{2} SP=\$[0-9A-F]{2} [NnVvDdIiZzCc]{6}/.test(out), out);
});
it('should record an instruction history', function () {
var out = cli('run', '--platform', 'apple2', '-e', 'run 20; hist 5', 'test/roms/apple2/cosmic.c.rom');
assert.ok(/instructions shown/.test(out), out);
});
it('should dump memory', function () {
var out = cli('run', '--platform', 'apple2', '-f', '20', '--memdump', '400,40f', 'test/roms/apple2/cosmic.c.rom');
assert.ok(/^0400:( [0-9A-F]{2}){8}/m.test(out), out);
});
});
describe('run script', function () {
it('should reject an unknown command', function () {
cliFails('run', '--platform', 'apple2', '-e', 'frobnicate', 'test/roms/apple2/cosmic.c.rom');
});
it('should run a script from a file', function () {
var f = path.join(os.tmpdir(), '8bws-test.script');
fs.writeFileSync(f, '# comment\nrun 5\necho hello-from-script\n');
var out = cli('run', '--platform', 'apple2', '--script', f, 'test/roms/apple2/cosmic.c.rom');
assert.ok(/hello-from-script/.test(out), out);
});
it('should resolve symbols from a label file', function () {
var f = path.join(os.tmpdir(), '8bws-test.lbl');
fs.writeFileSync(f, 'al 004000 .mysym\n');
var out = cli('run', '--platform', 'apple2', '--symbols', f, '-e', 'break mysym 2', 'test/roms/apple2/cosmic.c.rom');
assert.ok(/break \$4000/.test(out), out);
});
});
describe('legacy aliases', function () {
it('should accept compile, check and compilerun', function () {
assert.equal(cliJSON('compile', '--platform', 'gb', 'presets/gb/hello.c', '--check').command, 'check');
assert.equal(cliJSON('check', '--platform', 'gb', 'presets/gb/hello.c').command, 'check');
assert.ok(cliJSON('compilerun', '--platform', 'gb', 'presets/gb/hello.c', '--frames', '5').success);
});
});
});