updated worker with result type-checking

This commit is contained in:
Steven Hugg 2021-08-08 13:40:19 -05:00
parent 505492d1c7
commit f073ce2350
6 changed files with 103 additions and 62 deletions

View File

@ -100,18 +100,36 @@ export type CodeListingMap = {[path:string]:CodeListing};
export type VerilogOutput = export type VerilogOutput =
{program_rom_variable:string, program_rom:Uint8Array, code:string, name:string, ports:any[], signals:any[]}; {program_rom_variable:string, program_rom:Uint8Array, code:string, name:string, ports:any[], signals:any[]};
export type WorkerOutput = Uint8Array | VerilogOutput;
export type Segment = {name:string, start:number, size:number, last?:number, type?:string}; export type Segment = {name:string, start:number, size:number, last?:number, type?:string};
export interface WorkerResult { export type WorkerResult = WorkerErrorResult | WorkerOutputResult<any> | WorkerUnchangedResult;
errors:WorkerError[]
output?:WorkerOutput export interface WorkerUnchangedResult {
listings?:CodeListingMap unchanged: true;
symbolmap?:{[sym:string]:number}
params?:{}
segments?:Segment[]
unchanged?:boolean
debuginfo?:{} // optional info
} }
export interface WorkerErrorResult {
errors: WorkerError[]
listings?: CodeListingMap
}
export interface WorkerOutputResult<T> {
output: T
listings?: CodeListingMap
symbolmap?: {[sym:string]:number}
params?: {}
segments?: Segment[]
debuginfo?: {} // optional info
}
export function isUnchanged(result: WorkerResult) : result is WorkerUnchangedResult {
return ('unchanged' in result);
}
export function isErrorResult(result: WorkerResult) : result is WorkerErrorResult {
return ('errors' in result);
}
export function isOutputResult(result: WorkerResult) : result is WorkerOutputResult<any> {
return ('output' in result);
}

View File

@ -1,5 +1,5 @@
import { FileData, Dependency, SourceLine, SourceFile, CodeListing, CodeListingMap, WorkerError, Segment, WorkerResult } from "../common/workertypes"; import { FileData, Dependency, SourceLine, SourceFile, CodeListing, CodeListingMap, WorkerError, Segment, WorkerResult, WorkerOutputResult, isUnchanged, isOutputResult } from "../common/workertypes";
import { getFilenamePrefix, getFolderForPath, isProbablyBinary, getBasePlatform, getWithBinary } from "../common/util"; import { getFilenamePrefix, getFolderForPath, isProbablyBinary, getBasePlatform, getWithBinary } from "../common/util";
import { Platform } from "../common/baseplatform"; import { Platform } from "../common/baseplatform";
import localforage from "localforage"; import localforage from "localforage";
@ -126,7 +126,7 @@ export class CodeProject {
this.isCompiling = false; this.isCompiling = false;
this.pendingWorkerMessages = 0; this.pendingWorkerMessages = 0;
} }
if (data && !data.unchanged) { if (data && isOutputResult(data)) {
this.processBuildResult(data); this.processBuildResult(data);
} }
this.callbackBuildResult(data); this.callbackBuildResult(data);
@ -362,7 +362,7 @@ export class CodeProject {
this.sendBuild(); this.sendBuild();
} }
processBuildResult(data:WorkerResult) { processBuildResult(data: WorkerOutputResult<any>) {
// TODO: link listings with source files // TODO: link listings with source files
if (data.listings) { if (data.listings) {
this.listings = data.listings; this.listings = data.listings;

View File

@ -3,7 +3,7 @@
import * as localforage from "localforage"; import * as localforage from "localforage";
import { CodeProject, createNewPersistentStore, LocalForageFilesystem, OverlayFilesystem, ProjectFilesystem, WebPresetsFileSystem } from "./project"; import { CodeProject, createNewPersistentStore, LocalForageFilesystem, OverlayFilesystem, ProjectFilesystem, WebPresetsFileSystem } from "./project";
import { WorkerResult, WorkerOutput, WorkerError, FileData } from "../common/workertypes"; import { WorkerResult, WorkerOutputResult, WorkerError, FileData, WorkerErrorResult } from "../common/workertypes";
import { ProjectWindows } from "./windows"; import { ProjectWindows } from "./windows";
import { Platform, Preset, DebugSymbols, DebugEvalCondition, isDebuggable, EmuState } from "../common/baseplatform"; import { Platform, Preset, DebugSymbols, DebugEvalCondition, isDebuggable, EmuState } from "../common/baseplatform";
import { PLATFORMS, EmuHalt, Toolbar } from "../common/emu"; import { PLATFORMS, EmuHalt, Toolbar } from "../common/emu";
@ -71,7 +71,7 @@ var stateRecorder : StateRecorderImpl;
var userPaused : boolean; // did user explicitly pause? var userPaused : boolean; // did user explicitly pause?
var current_output : WorkerOutput; // current ROM var current_output : any; // current ROM (or other object)
var current_preset : Preset; // current preset object (if selected) var current_preset : Preset; // current preset object (if selected)
var store : LocalForage; // persistent store var store : LocalForage; // persistent store
@ -1294,7 +1294,7 @@ function measureBuildTime() {
async function setCompileOutput(data: WorkerResult) { async function setCompileOutput(data: WorkerResult) {
// errors? mark them in editor // errors? mark them in editor
if (data && data.errors && data.errors.length > 0) { if ('errors' in data && data.errors.length > 0) {
toolbar.addClass("has-errors"); toolbar.addClass("has-errors");
projectWindows.setErrors(data.errors); projectWindows.setErrors(data.errors);
refreshWindowList(); // to make sure windows are created for showErrorAlert() refreshWindowList(); // to make sure windows are created for showErrorAlert()
@ -1304,13 +1304,15 @@ async function setCompileOutput(data: WorkerResult) {
projectWindows.setErrors(null); projectWindows.setErrors(null);
hideErrorAlerts(); hideErrorAlerts();
// exit if compile output unchanged // exit if compile output unchanged
if (data == null || data.unchanged) return; if (data == null || ('unchanged' in data && data.unchanged)) return;
// make sure it's a WorkerOutputResult
if (!('output' in data)) return;
// process symbol map // process symbol map
platform.debugSymbols = new DebugSymbols(data.symbolmap, data.debuginfo); platform.debugSymbols = new DebugSymbols(data.symbolmap, data.debuginfo);
compparams = data.params; compparams = data.params;
// load ROM // load ROM
var rom = data.output; var rom = data.output;
if (rom) { if (rom != null) {
try { try {
clearBreakpoint(); // so we can replace memory (TODO: change toolbar btn) clearBreakpoint(); // so we can replace memory (TODO: change toolbar btn)
_resetRecording(); _resetRecording();

View File

@ -1,6 +1,6 @@
/// <reference types="emscripten" /> /// <reference types="emscripten" />
import type { WorkerResult, WorkerFileUpdate, WorkerBuildStep, WorkerMessage, WorkerError, Dependency, SourceLine, CodeListing, CodeListingMap, Segment, WorkerOutput, SourceLocation } from "../common/workertypes"; import type { WorkerResult, WorkerBuildStep, WorkerMessage, WorkerError, SourceLine, CodeListingMap, Segment, SourceLocation } from "../common/workertypes";
import { getBasePlatform, getRootBasePlatform, hex } from "../common/util"; import { getBasePlatform, getRootBasePlatform, hex } from "../common/util";
import { Assembler } from "./assembler"; import { Assembler } from "./assembler";
import * as vxmlparser from '../common/hdl/vxmlparser'; import * as vxmlparser from '../common/hdl/vxmlparser';
@ -372,13 +372,24 @@ type BuildOptions = {
}; };
// TODO // TODO
export type BuildStepResult = WorkerResult | WorkerNextToolResult;
export interface WorkerNextToolResult {
nexttool?: string
linktool?: string
path?: string
args: string[]
files: string[]
bblines?: boolean
}
interface BuildStep extends WorkerBuildStep { interface BuildStep extends WorkerBuildStep {
files? : string[] files? : string[]
args? : string[] args? : string[]
nextstep? : BuildStep nextstep? : BuildStep
linkstep? : BuildStep linkstep? : BuildStep
params? params?
result? // : WorkerResult | BuildStep ? result? : BuildStepResult
code? code?
prefix? prefix?
maxts? maxts?
@ -460,18 +471,18 @@ class Builder {
return {errors:[{line:0, msg:e+""}]}; // TODO: catch errors already generated? return {errors:[{line:0, msg:e+""}]}; // TODO: catch errors already generated?
} }
if (step.result) { if (step.result) {
step.result.params = step.params; (step.result as any).params = step.params; // TODO: type check
// errors? return them // errors? return them
if (step.result.errors && step.result.errors.length) { if ('errors' in step.result && step.result.errors.length) {
applyDefaultErrorPath(step.result.errors, step.path); applyDefaultErrorPath(step.result.errors, step.path);
return step.result; return step.result;
} }
// if we got some output, return it immediately // if we got some output, return it immediately
if (step.result.output) { if ('output' in step.result && step.result.output) {
return step.result; return step.result;
} }
// combine files with a link tool? // combine files with a link tool?
if (step.result.linktool) { if ('linktool' in step.result) {
if (linkstep) { if (linkstep) {
linkstep.files = linkstep.files.concat(step.result.files); linkstep.files = linkstep.files.concat(step.result.files);
linkstep.args = linkstep.args.concat(step.result.args); linkstep.args = linkstep.args.concat(step.result.args);
@ -485,10 +496,12 @@ class Builder {
} }
} }
// process with another tool? // process with another tool?
if (step.result.nexttool) { if ('nexttool' in step.result) {
var asmstep : BuildStep = step.result; var asmstep : BuildStep = {
asmstep.tool = step.result.nexttool; tool: step.result.nexttool,
asmstep.platform = platform; platform: platform,
...step.result
}
this.steps.push(asmstep); this.steps.push(asmstep);
} }
// process final step? // process final step?
@ -1004,7 +1017,7 @@ function parseDASMListing(lstpath:string, lsttext:string, listings:CodeListingMa
} }
} }
function assembleDASM(step:BuildStep) { function assembleDASM(step:BuildStep) : BuildStepResult {
load("dasm"); load("dasm");
var re_usl = /(\w+)\s+0000\s+[?][?][?][?]/; var re_usl = /(\w+)\s+0000\s+[?][?][?][?]/;
var unresolved = {}; var unresolved = {};
@ -1195,7 +1208,7 @@ function parseCA65Listing(code, symbols, params, dbg) {
return lines; return lines;
} }
function assembleCA65(step:BuildStep) { function assembleCA65(step:BuildStep) : BuildStepResult {
loadNative("ca65"); loadNative("ca65");
var errors = []; var errors = [];
gatherFiles(step, {mainFilePath:"main.s"}); gatherFiles(step, {mainFilePath:"main.s"});
@ -1234,7 +1247,7 @@ function assembleCA65(step:BuildStep) {
}; };
} }
function linkLD65(step:BuildStep) { function linkLD65(step:BuildStep) : BuildStepResult {
loadNative("ld65"); loadNative("ld65");
var params = step.params; var params = step.params;
gatherFiles(step); gatherFiles(step);
@ -1382,7 +1395,7 @@ function fixParamsWithDefines(path:string, params){
} }
} }
function compileCC65(step:BuildStep) { function compileCC65(step:BuildStep) : BuildStepResult {
loadNative("cc65"); loadNative("cc65");
var params = step.params; var params = step.params;
// stderr // stderr
@ -1483,7 +1496,7 @@ function parseIHX(ihx, rom_start, rom_size, errors) {
return output; return output;
} }
function assembleSDASZ80(step:BuildStep) { function assembleSDASZ80(step:BuildStep) : BuildStepResult {
loadNative("sdasz80"); loadNative("sdasz80");
var objout, lstout, symout; var objout, lstout, symout;
var errors = []; var errors = [];
@ -1654,7 +1667,7 @@ function linkSDLDZ80(step:BuildStep)
} }
} }
function compileSDCC(step:BuildStep) { function compileSDCC(step:BuildStep) : BuildStepResult {
gatherFiles(step, { gatherFiles(step, {
mainFilePath:"main.c" // not used mainFilePath:"main.c" // not used
@ -1677,7 +1690,9 @@ function compileSDCC(step:BuildStep) {
// load source file and preprocess // load source file and preprocess
var code = getWorkFileAsString(step.path); var code = getWorkFileAsString(step.path);
var preproc = preprocessMCPP(step, 'sdcc'); var preproc = preprocessMCPP(step, 'sdcc');
if (preproc.errors) return preproc; if (preproc.errors) {
return { errors: preproc.errors };
}
else code = preproc.code; else code = preproc.code;
// pipe file to stdin // pipe file to stdin
setupStdin(FS, code); setupStdin(FS, code);
@ -1860,7 +1875,7 @@ function compileJSASM(asmcode:string, platform, options, is_inline) {
} }
} }
function compileJSASMStep(step:BuildStep) { function compileJSASMStep(step:BuildStep) : BuildStepResult {
gatherFiles(step); gatherFiles(step);
var code = getWorkFileAsString(step.path); var code = getWorkFileAsString(step.path);
var platform = step.platform || 'verilog'; var platform = step.platform || 'verilog';
@ -1900,7 +1915,7 @@ function compileInlineASM(code:string, platform, options, errors, asmlines) {
return code; return code;
} }
function compileVerilator(step:BuildStep) { function compileVerilator(step:BuildStep) : BuildStepResult {
loadNative("verilator_bin"); loadNative("verilator_bin");
var platform = step.platform || 'verilog'; var platform = step.platform || 'verilog';
var errors : WorkerError[] = []; var errors : WorkerError[] = [];
@ -1991,7 +2006,7 @@ function compileVerilator(step:BuildStep) {
} }
// TODO: test // TODO: test
function compileYosys(step:BuildStep) { function compileYosys(step:BuildStep) : BuildStepResult {
loadNative("yosys"); loadNative("yosys");
var code = step.code; var code = step.code;
var errors = []; var errors = [];
@ -2022,14 +2037,14 @@ function compileYosys(step:BuildStep) {
var json_file = FS.readFile(topmod+".json", {encoding:'utf8'}); var json_file = FS.readFile(topmod+".json", {encoding:'utf8'});
var json = JSON.parse(json_file); var json = JSON.parse(json_file);
console.log(json); console.log(json);
return {yosys_json:json, errors:errors}; // TODO return {output:json, errors:errors}; // TODO
} catch(e) { } catch(e) {
console.log(e); console.log(e);
return {errors:errors}; return {errors:errors};
} }
} }
function assembleZMAC(step:BuildStep) { function assembleZMAC(step:BuildStep) : BuildStepResult {
loadNative("zmac"); loadNative("zmac");
var hexout, lstout, binout; var hexout, lstout, binout;
var errors = []; var errors = [];
@ -2112,7 +2127,7 @@ function preprocessBatariBasic(code:string) : string {
return bbout; return bbout;
} }
function compileBatariBasic(step:BuildStep) { function compileBatariBasic(step:BuildStep) : BuildStepResult {
load("bb2600basic"); load("bb2600basic");
var params = step.params; var params = step.params;
// stdout // stdout
@ -2202,7 +2217,7 @@ function setupRequireFunction() {
} }
} }
function translateShowdown(step:BuildStep) { function translateShowdown(step:BuildStep) : BuildStepResult {
setupRequireFunction(); setupRequireFunction();
load("showdown.min"); load("showdown.min");
var showdown = emglobal['showdown']; var showdown = emglobal['showdown'];
@ -2221,7 +2236,7 @@ function translateShowdown(step:BuildStep) {
} }
// http://datapipe-blackbeltsystems.com/windows/flex/asm09.html // http://datapipe-blackbeltsystems.com/windows/flex/asm09.html
function assembleXASM6809(step:BuildStep) { function assembleXASM6809(step:BuildStep) : BuildStepResult {
load("xasm6809"); load("xasm6809");
var alst = ""; var alst = "";
var lasterror = null; var lasterror = null;
@ -2282,7 +2297,7 @@ function assembleXASM6809(step:BuildStep) {
} }
// http://www.nespowerpak.com/nesasm/ // http://www.nespowerpak.com/nesasm/
function assembleNESASM(step:BuildStep) { function assembleNESASM(step:BuildStep) : BuildStepResult {
loadNative("nesasm"); loadNative("nesasm");
var re_filename = /\#\[(\d+)\]\s+(\S+)/; var re_filename = /\#\[(\d+)\]\s+(\S+)/;
var re_insn = /\s+(\d+)\s+([0-9A-F]+):([0-9A-F]+)/; var re_insn = /\s+(\d+)\s+([0-9A-F]+):([0-9A-F]+)/;
@ -2378,7 +2393,7 @@ function assembleNESASM(step:BuildStep) {
}; };
} }
function compileCMOC(step:BuildStep) { function compileCMOC(step:BuildStep) : BuildStepResult {
loadNative("cmoc"); loadNative("cmoc");
var params = step.params; var params = step.params;
// stderr // stderr
@ -2414,7 +2429,9 @@ function compileCMOC(step:BuildStep) {
// load source file and preprocess // load source file and preprocess
var code = getWorkFileAsString(step.path); var code = getWorkFileAsString(step.path);
var preproc = preprocessMCPP(step, null); var preproc = preprocessMCPP(step, null);
if (preproc.errors) return preproc; if (preproc.errors) {
return {errors: preproc.errors}
}
else code = preproc.code; else code = preproc.code;
// set up filesystem // set up filesystem
var FS = CMOC.FS; var FS = CMOC.FS;
@ -2439,7 +2456,7 @@ function compileCMOC(step:BuildStep) {
}; };
} }
function assembleLWASM(step:BuildStep) { function assembleLWASM(step:BuildStep) : BuildStepResult {
loadNative("lwasm"); loadNative("lwasm");
var errors = []; var errors = [];
gatherFiles(step, {mainFilePath:"main.s"}); gatherFiles(step, {mainFilePath:"main.s"});
@ -2474,7 +2491,7 @@ function assembleLWASM(step:BuildStep) {
}; };
} }
function linkLWLINK(step:BuildStep) { function linkLWLINK(step:BuildStep) : BuildStepResult {
loadNative("lwlink"); loadNative("lwlink");
var params = step.params; var params = step.params;
gatherFiles(step); gatherFiles(step);
@ -2565,7 +2582,7 @@ function linkLWLINK(step:BuildStep) {
} }
// http://www.techhelpmanual.com/829-program_startup___exit.html // http://www.techhelpmanual.com/829-program_startup___exit.html
function compileSmallerC(step:BuildStep) { function compileSmallerC(step:BuildStep) : BuildStepResult {
loadNative("smlrc"); loadNative("smlrc");
var params = step.params; var params = step.params;
// stderr // stderr
@ -2603,7 +2620,9 @@ function compileSmallerC(step:BuildStep) {
// load source file and preprocess // load source file and preprocess
var code = getWorkFileAsString(step.path); var code = getWorkFileAsString(step.path);
var preproc = preprocessMCPP(step, null); var preproc = preprocessMCPP(step, null);
if (preproc.errors) return preproc; if (preproc.errors) {
return {errors: preproc.errors};
}
else code = preproc.code; else code = preproc.code;
// set up filesystem // set up filesystem
var FS = smlrc.FS; var FS = smlrc.FS;
@ -2627,7 +2646,8 @@ function compileSmallerC(step:BuildStep) {
files:[destpath], files:[destpath],
}; };
} }
function assembleYASM(step:BuildStep) {
function assembleYASM(step:BuildStep) : BuildStepResult {
loadNative("yasm"); loadNative("yasm");
var errors = []; var errors = [];
gatherFiles(step, {mainFilePath:"main.asm"}); gatherFiles(step, {mainFilePath:"main.asm"});
@ -2708,7 +2728,7 @@ function parseXMLPoorly(s: string) : XMLNode {
return top; return top;
} }
function compileInform6(step:BuildStep) { function compileInform6(step:BuildStep) : BuildStepResult {
loadNative("inform"); loadNative("inform");
var errors = []; var errors = [];
gatherFiles(step, {mainFilePath:"main.inf"}); gatherFiles(step, {mainFilePath:"main.inf"});
@ -2812,7 +2832,7 @@ function compileInform6(step:BuildStep) {
=> Creating Output file 'pcs.bin_S01__Output.txt' => Creating Output file 'pcs.bin_S01__Output.txt'
*/ */
function assembleMerlin32(step:BuildStep) { function assembleMerlin32(step:BuildStep) : BuildStepResult {
loadNative("merlin32"); loadNative("merlin32");
var errors = []; var errors = [];
var lstfiles = []; var lstfiles = [];
@ -2898,7 +2918,7 @@ function assembleMerlin32(step:BuildStep) {
} }
// README.md:2:5: parse error, expected: statement or variable assignment, integer variable, variable assignment // README.md:2:5: parse error, expected: statement or variable assignment, integer variable, variable assignment
function compileFastBasic(step:BuildStep) { function compileFastBasic(step:BuildStep) : BuildStepResult {
// TODO: fastbasic-fp? // TODO: fastbasic-fp?
loadNative("fastbasic-int"); loadNative("fastbasic-int");
var params = step.params; var params = step.params;
@ -2935,7 +2955,7 @@ function compileFastBasic(step:BuildStep) {
}; };
} }
function compileBASIC(step:BuildStep) { function compileBASIC(step:BuildStep) : WorkerResult {
var jsonpath = step.path + ".json"; var jsonpath = step.path + ".json";
gatherFiles(step); gatherFiles(step);
if (staleFiles(step, [jsonpath])) { if (staleFiles(step, [jsonpath])) {
@ -2960,7 +2980,7 @@ function compileBASIC(step:BuildStep) {
} }
} }
function compileSilice(step:BuildStep) { function compileSilice(step:BuildStep) : BuildStepResult {
loadNative("silice"); loadNative("silice");
var params = step.params; var params = step.params;
gatherFiles(step, {mainFilePath:"main.ice"}); gatherFiles(step, {mainFilePath:"main.ice"});
@ -3021,7 +3041,7 @@ function compileSilice(step:BuildStep) {
}; };
} }
function compileWiz(step:BuildStep) { function compileWiz(step:BuildStep) : WorkerResult {
loadNative("wiz"); loadNative("wiz");
var params = step.params; var params = step.params;
gatherFiles(step, {mainFilePath:"main.wiz"}); gatherFiles(step, {mainFilePath:"main.wiz"});
@ -3072,7 +3092,7 @@ function compileWiz(step:BuildStep) {
} }
} }
function assembleARMIPS(step:BuildStep) { function assembleARMIPS(step:BuildStep) : WorkerResult {
loadNative("armips"); loadNative("armips");
var errors = []; var errors = [];
gatherFiles(step, {mainFilePath:"main.asm"}); gatherFiles(step, {mainFilePath:"main.asm"});
@ -3162,7 +3182,7 @@ function assembleARMIPS(step:BuildStep) {
} }
} }
function assembleVASMARM(step:BuildStep) { function assembleVASMARM(step:BuildStep) : BuildStepResult {
loadNative("vasmarm_std"); loadNative("vasmarm_std");
/// error 2 in line 8 of "gfxtest.c": unknown mnemonic <ew> /// error 2 in line 8 of "gfxtest.c": unknown mnemonic <ew>
/// error 3007: undefined symbol <XXLOOP> /// error 3007: undefined symbol <XXLOOP>
@ -3371,7 +3391,7 @@ var TOOL_PRELOADFS = {
'wiz': 'wiz', 'wiz': 'wiz',
} }
function handleMessage(data : WorkerMessage) : WorkerResult | {unchanged:true} { function handleMessage(data : WorkerMessage) : WorkerResult {
// preload file system // preload file system
if (data.preload) { if (data.preload) {
var fs = TOOL_PRELOADFS[data.preload]; var fs = TOOL_PRELOADFS[data.preload];

View File

@ -82,6 +82,7 @@ describe('Store', function () {
project.callbackBuildStatus = function (b) { msgs.push(b) }; project.callbackBuildStatus = function (b) { msgs.push(b) };
project.callbackBuildResult = function (b) { msgs.push(1) }; project.callbackBuildResult = function (b) { msgs.push(1) };
var buildresult = { var buildresult = {
output: [0],
listings: { listings: {
test: { test: {
lines: [{ line: 3, offset: 61440, insns: 'a9 00', iscode: true }] lines: [{ line: 3, offset: 61440, insns: 'a9 00', iscode: true }]

View File

@ -94,7 +94,7 @@ testPlatform(ex, 'vcs', 'Atari 2600', 35);
testPlatform(ex, 'nes', 'NES', 30); testPlatform(ex, 'nes', 'NES', 30);
testPlatform(ex, 'vicdual', 'VIC Dual', 7); testPlatform(ex, 'vicdual', 'VIC Dual', 7);
testPlatform(ex, 'mw8080bw', 'Midway 8080', 3); testPlatform(ex, 'mw8080bw', 'Midway 8080', 3);
testPlatform(ex, 'galaxian-scramble', 'Galaxian/Scramble', 3); testPlatform(ex, 'galaxian-scramble', 'Galaxian/Scramble', 2);
testPlatform(ex, 'vector-z80color', 'Atari Color Vector (Z80)', 3); testPlatform(ex, 'vector-z80color', 'Atari Color Vector (Z80)', 3);
testPlatform(ex, 'williams-z80', 'Williams (Z80)', 3); testPlatform(ex, 'williams-z80', 'Williams (Z80)', 3);
// TODO testPlatform(ex, 'sound_williams-z80', 'Williams Sound (Z80)', 1); // TODO testPlatform(ex, 'sound_williams-z80', 'Williams Sound (Z80)', 1);