worker: converted merlin32/nesasm to WASI

This commit is contained in:
Steven Hugg committed 2026-09-06 10:50:11 +02:00
1 parent 66376e5900
commit 2e8202fd87
7 files changed
+156 -5616

No files matched your search

+71 -44
View File
@@ -1,13 +1,15 @@
import { WorkerError, CodeListingMap } from "../../common/workertypes";
import { BuildStep, BuildStepResult, populateFiles, putWorkFile, anyTargetChanged, gatherFiles, staleFiles } from "../builder";
import { parseListing, makeErrorMatcher } from "../listingutils";
import { loadNative, emglobal, moduleInstFn, execMain, print_fn } from "../wasmutils";
import { BuildStep, BuildStepResult, populateFiles, putWorkFile, anyTargetChanged, gatherFiles, staleFiles, store } from "../builder";
import { parseListing, makeErrorMatcher, re_crlf } from "../listingutils";
import { loadNative, emglobal, moduleInstFn, execMain, print_fn, loadWASMBinary } from "../wasmutils";
import { EmscriptenModule } from "../wasmutils";
import { WASIRunner } from "../../common/wasi/wasishim";
// http://www.nespowerpak.com/nesasm/
let nesasmWasiModule: WebAssembly.Module | null = null;
export function assembleNESASM(step: BuildStep): BuildStepResult {
loadNative("nesasm");
var re_filename = /\#\[(\d+)\]\s+(\S+)/;
var re_insn = /\s+(\d+)\s+([0-9A-F]+):([0-9A-F]+)/;
var re_error = /\s+(.+)/;
@@ -38,23 +40,36 @@ export function assembleNESASM(step: BuildStep): BuildStepResult {
break;
}
}
var Module: EmscriptenModule = emglobal.nesasm({
instantiateWasm: moduleInstFn('nesasm'),
noInitialRun: true,
print: match_fn
});
var FS = Module.FS;
populateFiles(step, FS, {
gatherFiles(step, {
mainFilePath: 'main.a'
});
var binpath = step.prefix + '.nes';
var lstpath = step.prefix + '.lst';
var sympath = step.prefix + '.fns';
execMain(step, Module, [step.path, '-s', "-l", "2"]);
if (!nesasmWasiModule) {
nesasmWasiModule = new WebAssembly.Module(loadWASMBinary("nesasm"));
}
const wasi = new WASIRunner();
wasi.initSync(nesasmWasiModule);
for (let file of step.files) {
wasi.fs.putFile("./" + file, store.getFileData(file));
}
wasi.addPreopenDirectory(".");
wasi.setArgs(['nesasm', step.path, '-s', "-l", "2"]);
try {
wasi.run();
} catch (e) {
errors.push({ line: 0, msg: "" + e });
}
const stdout = wasi.fds[1].getBytesAsString();
for (let line of stdout.split(re_crlf)) {
match_fn(line);
}
// parse main listing, get errors and listings for each file
var listings: CodeListingMap = {};
var alst;
try {
var alst = FS.readFile(lstpath, { 'encoding': 'utf8' });
alst = wasi.fs.getFile("./" + lstpath).getBytesAsString();
// 16 00:C004 8E 17 40 STX $4017 ; disable APU frame IRQ
var asmlines = parseListing(alst, /^\s*(\d+)\s+([0-9A-F]+):([0-9A-F]+)\s+([0-9A-F ]+?) (.*)/i, 1, 3, 4);
putWorkFile(lstpath, alst);
@@ -70,9 +85,9 @@ export function assembleNESASM(step: BuildStep): BuildStepResult {
}
// read binary rom output and symbols
var aout, asym;
aout = FS.readFile(binpath);
aout = wasi.fs.getFile("./" + binpath).getBytes();
try {
asym = FS.readFile(sympath, { 'encoding': 'utf8' });
asym = wasi.fs.getFile("./" + sympath).getBytesAsString();
} catch (e) {
console.log(e);
errors.push({ line: 0, msg: "No symbol table generated, maybe missing ENDM or segment overflow?" });
@@ -116,50 +131,62 @@ Line | # File Line | Line Type | MX | Reloc | Size | Address Object
=> Creating Output file 'pcs.bin_S01__Output.txt'
*/
let merlin32WasiModule: WebAssembly.Module | null = null;
export function assembleMerlin32(step: BuildStep): BuildStepResult {
loadNative("merlin32");
var errors = [];
var lstfiles = [];
gatherFiles(step, { mainFilePath: "main.lnk" });
var objpath = step.prefix + ".bin";
if (staleFiles(step, [objpath])) {
var args = ['-v', step.path];
var merlin32: EmscriptenModule = emglobal.merlin32({
instantiateWasm: moduleInstFn('merlin32'),
noInitialRun: true,
print: (s: string) => {
var m = /\s*=>\s*Creating Output file '(.+?)'/.exec(s);
if (m) {
lstfiles.push(m[1]);
}
var errpos = s.indexOf('Error');
if (errpos >= 0) {
s = s.slice(errpos + 6).trim();
var mline = /\bline (\d+)\b/.exec(s);
var mpath = /\bfile '(.+?)'/.exec(s);
errors.push({
line: parseInt(mline[1]) || 0,
msg: s,
path: mpath[1] || step.path,
});
}
},
printErr: print_fn,
});
var FS = merlin32.FS;
populateFiles(step, FS);
execMain(step, merlin32, args);
if (!merlin32WasiModule) {
merlin32WasiModule = new WebAssembly.Module(loadWASMBinary("merlin32"));
}
const wasi = new WASIRunner();
wasi.initSync(merlin32WasiModule);
for (let file of step.files) {
wasi.fs.putFile("./" + file, store.getFileData(file));
}
wasi.addPreopenDirectory(".");
wasi.setArgs(['merlin32', ...args]);
try {
wasi.run();
} catch (e) {
errors.push({ line: 0, msg: "" + e });
}
const stdout = wasi.fds[1].getBytesAsString();
const stderr = wasi.fds[2].getBytesAsString();
for (let s of stdout.split(re_crlf)) {
var m = /\s*=>\s*Creating Output file '(.+?)'/.exec(s);
if (m) {
lstfiles.push(m[1]);
}
var errpos = s.indexOf('Error');
if (errpos >= 0) {
s = s.slice(errpos + 6).trim();
var mline = /\bline (\d+)\b/.exec(s);
var mpath = /\bfile '(.+?)'/.exec(s);
errors.push({
line: mline ? parseInt(mline[1]) : 0,
msg: s,
path: mpath ? mpath[1] : step.path,
});
}
}
if (stderr)
console.log(stderr);
if (errors.length)
return { errors: errors };
var errout = null;
try {
errout = FS.readFile("error_output.txt", { encoding: 'utf8' });
errout = wasi.fs.getFile("./error_output.txt").getBytesAsString();
} catch (e) {
//
}
var objout = FS.readFile(objpath, { encoding: 'binary' });
var objout = wasi.fs.getFile("./" + objpath).getBytes();
putWorkFile(objpath, objout);
if (!anyTargetChanged(step, [objpath]))
return;
@@ -168,7 +195,7 @@ export function assembleMerlin32(step: BuildStep): BuildStepResult {
var segments = [];
var listings: CodeListingMap = {};
lstfiles.forEach((lstfn) => {
var lst = FS.readFile(lstfn, { encoding: 'utf8' }) as string;
var lst = wasi.fs.getFile("./" + lstfn).getBytesAsString() as string;
lst.split('\n').forEach((line) => {
var toks = line.split(/\s*\|\s*/);
if (toks && toks[6]) {
File diff suppressed because it is too large. Load diff
Binary file not shown.
-21
View File
@@ -1,21 +0,0 @@
var nesasm = (function() {
var _scriptDir = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : undefined;
return (
function(nesasm) {
nesasm = nesasm || {};
var Module=typeof nesasm!=="undefined"?nesasm:{};var moduleOverrides={};var key;for(key in Module){if(Module.hasOwnProperty(key)){moduleOverrides[key]=Module[key]}}Module["arguments"]=[];Module["thisProgram"]="./this.program";Module["quit"]=function(status,toThrow){throw toThrow};Module["preRun"]=[];Module["postRun"]=[];var ENVIRONMENT_IS_WEB=false;var ENVIRONMENT_IS_WORKER=false;var ENVIRONMENT_IS_NODE=false;var ENVIRONMENT_HAS_NODE=false;var ENVIRONMENT_IS_SHELL=false;ENVIRONMENT_IS_WEB=typeof window==="object";ENVIRONMENT_IS_WORKER=typeof importScripts==="function";ENVIRONMENT_HAS_NODE=typeof process==="object"&&typeof process.versions==="object"&&typeof process.versions.node==="string";ENVIRONMENT_IS_NODE=ENVIRONMENT_HAS_NODE&&!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_WORKER;ENVIRONMENT_IS_SHELL=!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_NODE&&!ENVIRONMENT_IS_WORKER;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}else{return scriptDirectory+path}}var read_,readAsync,readBinary,setWindowTitle;if(ENVIRONMENT_IS_NODE){scriptDirectory=__dirname+"/";var nodeFS;var nodePath;read_=function shell_read(filename,binary){var ret;if(!nodeFS)nodeFS=require("fs");if(!nodePath)nodePath=require("path");filename=nodePath["normalize"](filename);ret=nodeFS["readFileSync"](filename);return binary?ret:ret.toString()};readBinary=function readBinary(filename){var ret=read_(filename,true);if(!ret.buffer){ret=new Uint8Array(ret)}assert(ret.buffer);return ret};if(process["argv"].length>1){Module["thisProgram"]=process["argv"][1].replace(/\\/g,"/")}Module["arguments"]=process["argv"].slice(2);process["on"]("uncaughtException",function(ex){if(!(ex instanceof ExitStatus)){throw ex}});process["on"]("unhandledRejection",abort);Module["quit"]=function(status){process["exit"](status)};Module["inspect"]=function(){return"[Emscripten Module object]"}}else if(ENVIRONMENT_IS_SHELL){if(typeof read!="undefined"){read_=function shell_read(f){return read(f)}}readBinary=function readBinary(f){var data;if(typeof readbuffer==="function"){return new Uint8Array(readbuffer(f))}data=read(f,"binary");assert(typeof data==="object");return data};if(typeof scriptArgs!="undefined"){Module["arguments"]=scriptArgs}else if(typeof arguments!="undefined"){Module["arguments"]=arguments}if(typeof quit==="function"){Module["quit"]=function(status){quit(status)}}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptDir){scriptDirectory=_scriptDir}if(scriptDirectory.indexOf("blob:")!==0){scriptDirectory=scriptDirectory.substr(0,scriptDirectory.lastIndexOf("/")+1)}else{scriptDirectory=""}read_=function shell_read(url){var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.send(null);return xhr.responseText};if(ENVIRONMENT_IS_WORKER){readBinary=function readBinary(url){var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=function readAsync(url,onload,onerror){var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=function xhr_onload(){if(xhr.status==200||xhr.status==0&&xhr.response){onload(xhr.response);return}onerror()};xhr.onerror=onerror;xhr.send(null)};setWindowTitle=function(title){document.title=title}}else{}var out=Module["print"]||(typeof console!=="undefined"?console.log.bind(console):typeof print!=="undefined"?print:null);var err=Module["printErr"]||(typeof printErr!=="undefined"?printErr:typeof console!=="undefined"&&console.warn.bind(console)||out);for(key in moduleOverrides){if(moduleOverrides.hasOwnProperty(key)){Module[key]=moduleOverrides[key]}}moduleOverrides=undefined;var STACK_ALIGN=16;function dynamicAlloc(size){var ret=HEAP32[DYNAMICTOP_PTR>>2];var end=ret+size+15&-16;if(end>_emscripten_get_heap_size()){abort()}HEAP32[DYNAMICTOP_PTR>>2]=end;return ret}function getNativeTypeSize(type){switch(type){case"i1":case"i8":return 1;case"i16":return 2;case"i32":return 4;case"i64":return 8;case"float":return 4;case"double":return 8;default:{if(type[type.length-1]==="*"){return 4}else if(type[0]==="i"){var bits=parseInt(type.substr(1));assert(bits%8===0,"getNativeTypeSize invalid bits "+bits+", type "+type);return bits/8}else{return 0}}}}function warnOnce(text){if(!warnOnce.shown)warnOnce.shown={};if(!warnOnce.shown[text]){warnOnce.shown[text]=1;err(text)}}var asm2wasmImports={"f64-rem":function(x,y){return x%y},"debugger":function(){debugger}};var jsCallStartIndex=1;var functionPointers=new Array(0);function convertJsFunctionToWasm(func,sig){var typeSection=[1,0,1,96];var sigRet=sig.slice(0,1);var sigParam=sig.slice(1);var typeCodes={"i":127,"j":126,"f":125,"d":124};typeSection.push(sigParam.length);for(var i=0;i<sigParam.length;++i){typeSection.push(typeCodes[sigParam[i]])}if(sigRet=="v"){typeSection.push(0)}else{typeSLine truncated
return nesasm
}
);
})();
if (typeof exports === 'object' && typeof module === 'object')
module.exports = nesasm;
else if (typeof define === 'function' && define['amd'])
define([], function() { return nesasm; });
else if (typeof exports === 'object')
exports["nesasm"] = nesasm;
Binary file not shown.
+82
View File
@@ -345,6 +345,88 @@ describe('Worker', function() {
it('should compile CC65 flags', function(done) {
compile('cc65', '#define CC65_FLAGS -Or,-g,-j\nint main() {\nint x=1;\nreturn x+2;\n}', 'apple2', done, 416+58, 3);
});
// merlin32 is driven by a .lnk link file that names the sources to assemble
var MERLIN32_LNK = ' TYP $06\n' +
' DSK main.bin\n' +
' ORG $2000\n' +
' ASM hello.asm\n';
function merlin32Msgs(asm) {
return [{
updates: [
{path:'main.lnk', data:MERLIN32_LNK},
{path:'hello.asm', data:asm}
],
buildsteps: [
{path:'main.lnk', platform:'apple2', tool:'merlin32', mainfile:true,
files:['main.lnk', 'hello.asm']}
]
}];
}
it('should assemble Merlin32', function(done) {
var asm = ' ORG $2000\n' +
'START LDA #$C1\n' +
' STA $0400\n' +
' RTS\n';
doBuild(merlin32Msgs(asm), function(err, msg) {
if (err) return done(err);
try {
// LDA #$C1 / STA $0400 / RTS
assert.deepEqual(Array.from(msg.output), [0xa9, 0xc1, 0x8d, 0x00, 0x04, 0x60]);
var lst = msg.listings['hello.asm'];
assert.equal(lst.lines[1].line, 2);
assert.equal(lst.lines[1].offset, 0x2000);
assert.equal(lst.lines[2].offset, 0x2002);
done();
} catch (e) { done(e); }
}, 6, [5], 0);
});
it('should NOT assemble Merlin32 (syntax error)', function(done) {
var asm = ' ORG $2000\n' +
'START LDA #$C1\n' +
' BOGUS $0400\n' +
' RTS\n';
doBuild(merlin32Msgs(asm), function(err, msg) {
if (err) return done(err);
try {
assert.equal(msg.errors[0].line, 3);
assert.equal(msg.errors[0].path, 'hello.asm');
done();
} catch (e) { done(e); }
}, 0, 0, 1, {ignoreErrorPath:true});
});
// nesasm needs the iNES header directives and a bank/org before any code
var NESASM_HEADER = '\t.inesprg 1\n\t.ineschr 0\n\t.inesmap 0\n\t.inesmir 0\n';
it('should assemble NESASM', function(done) {
var asm = NESASM_HEADER +
'\t.list\n\t.bank 0\n\t.org $8000\n' +
'Start:\n\tlda #$c1\n\tsta $0400\n\trts\n' +
'\t.bank 1\n\t.org $fffa\n\t.dw 0,Start,0\n';
compile('nesasm', asm, 'nes', function(err, msg) {
if (err) return done(err);
try {
assert.deepEqual(Array.from(msg.output.slice(0,4)), [0x4e, 0x45, 0x53, 0x1a]); // "NES\x1a"
assert.equal(msg.symbolmap['Start'], 0x8000);
var lst = msg.listings['src.lst'];
assert.equal(lst.lines[1].offset, 0x8000);
assert.equal(lst.lines[1].insns, 'A9 C1');
assert.equal(lst.lines[2].offset, 0x8002);
done();
} catch (e) { done(e); }
}, 16400, [5], 0);
});
it('should NOT assemble NESASM (unknown instruction)', function(done) {
var asm = NESASM_HEADER + '\t.bank 0\n\t.org $8000\n\tbogus #$c1\n\trts\n';
compile('nesasm', asm, 'nes', function(err, msg) {
if (err) return done(err);
try {
assert.equal(msg.errors[0].line, 7);
assert.ok(/Unknown instruction/.test(msg.errors[0].msg));
done();
} catch (e) { done(e); }
}, 0, 0, 1);
});
/*
it('should compile gameboy', function(done) {
compile('sdcc', '//#link "gb/sfr.sgb"\n//#link "gb/crt0.sgb"\nvoid main(void){}\n', 'gb', done, 416+58, 3);
+3 -1
View File
@@ -70,7 +70,7 @@ describe('Tool metadata registry', function () {
it('covers the previously-scattered editor styles and help URLs', function () {
// spot-check ids that used to live in TOOL_TO_SOURCE_STYLE / TOOL_TO_HELPURL
for (let id of ['dasm', 'acme', 'cc65', 'ca65', 'sdcc', 'cmoc', 'zmac',
'nesasm', 'bataribasic', 'fastbasic', 'wiz', 'silice', 'ecs', 'dialog']) {
'nesasm', 'merlin32', 'bataribasic', 'fastbasic', 'wiz', 'silice', 'ecs', 'dialog']) {
assert.ok(TOOL_META[id], `expected tool '${id}' in registry`);
}
assert.strictEqual(TOOL_META['cc65'].editorStyle, 'text/x-csrc');
@@ -95,6 +95,8 @@ describe('Tool metadata registry', function () {
assert.deepStrictEqual(deps('!src "foo.acme"\n', 'acme'), ['foo.acme']);
assert.deepStrictEqual(deps('%% #include "foo.dg"\n', 'dialog'), ['foo.dg']);
assert.deepStrictEqual(deps(' USE foo.xasm\n', 'xasm6809'), ['foo.xasm']);
// merlin32 link files name their sources with ASM
assert.deepStrictEqual(deps(' ASM hello.asm\n', 'merlin32'), ['hello.asm']);
// ecs uses both its own import and the shared .include directive
assert.deepStrictEqual(deps('.include "vcs.h"\nimport "lib.ecs"\n', 'ecs'),
['vcs.h', 'lib.ecs']);