diff --git a/CHANGELOG.md b/CHANGELOG.md index 6398e6d7..b642a6a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,7 @@ See [extension/CHANGELOG.md](extension/CHANGELOG.md). - cc65 toolchain (cc65/ca65/ld65 WASI builds and the per-platform include/lib zips) updated from the 2021 build (`6ac4aa4`) to cc65 `d8a486a` (V2.19-3867, 2026-09-26). The compiler runs with `--disable-opt OptLoadStore1` (the new step drops stores after loads of the same address, which breaks hardware registers and inline asm) and `-unreachable-code`. - cc65 `a2.lo.s` (Apple II lo-res TGI) patched in `8bitworkshop-compilers/patches/cc65-apple2-lo-tgi.patch`: a stray `bpl :+` in `INSTALL` made `tgi_install(a2_lo_tgi)` crash on a non-enhanced Apple II. - Presets adjusted for the newer cc65: NES `chase` closes its zero-page `#pragma` region, C64 `side_scroller` uses an unsigned fixed-point constant, and the Atari vector presets no longer recurse into `main()`. `neslib.h` no longer redefines `NULL` if `stddef.h` already did. +- SDCC: both 3.6.5 and 4.x are bundled; 3.6.5 is the default (several times faster to build) and `//#tooldef c sdcc=4` opts in to 4.x (the default is `SDCC_DEFAULT_VERSION` in `toolmeta.ts`). Programs without an `opt_code*` pragma now build with `--no-peep --nolospre --max-allocs-per-node 500`, including Game Boy, which previously always got the full optimizer. See *SDCC versions* in the build directives docs. - Build is now only `esbuild` through `scripts/build.mjs`; code split into a headless `src/common/projectcore.ts` / `toolselect.ts`. - New `buildpresets` script that builds and runs every preset. diff --git a/presets/msx/siegegame.c b/presets/msx/siegegame.c index 125596be..c9a97d04 100644 --- a/presets/msx/siegegame.c +++ b/presets/msx/siegegame.c @@ -27,7 +27,7 @@ void putstring(byte x, byte y, const char* str) { CHPUT(*str++); } } -byte getchar(byte x, byte y) { +byte getchar_at(byte x, byte y) { word addr = 0x1800 | (x+1) | y*32; // TODO: use variable for base address byte result; result = RDVRM(addr); @@ -130,7 +130,7 @@ void move_player(Player* p) { cputcxy(p->x, p->y, p->tail_attr); p->x += DIR_X[p->dir]; p->y += DIR_Y[p->dir]; - if (getchar(p->x, p->y) != CHAR(' ')) + if (getchar_at(p->x, p->y) != CHAR(' ')) p->collided = 1; draw_player(p); } @@ -154,7 +154,7 @@ byte ai_try_dir(Player* p, dir_t dir, byte shift) { dir &= 3; x = p->x + (DIR_X[dir] << shift); y = p->y + (DIR_Y[dir] << shift); - if (x < 29 && y < 27 && getchar(x, y) == CHAR(' ')) { + if (x < 29 && y < 27 && getchar_at(x, y) == CHAR(' ')) { p->dir = dir; return 1; } else { diff --git a/src/common/toolmeta.ts b/src/common/toolmeta.ts index eb39344e..84c41437 100644 --- a/src/common/toolmeta.ts +++ b/src/common/toolmeta.ts @@ -268,6 +268,31 @@ const TOOL_PLATFORM_HELPURL: { [tool: string]: { [platform: string]: string } } }, }; +/** + * Which SDCC a program gets unless it says `//#tooldef c sdcc=3|4`. To make + * 4.x the default, change this to 4: nothing else needs editing. + * 3 = 3.6.5, Emscripten, preloadFS 'sdcc' (builds several times faster) + * 4 = 4.x, WASI, sdcc-fs.zip (smaller code) + */ +export const SDCC_DEFAULT_VERSION: 3 | 4 = 3; + +/** + * 'default' lists both filesystems; the one for SDCC_DEFAULT_VERSION leads + * (getSharedFileSystemName prefers preloadFS). Platforms whose prebuilt + * libraries were compiled by SDCC 3.x (libcv, gb.lib, cpctelera) have only + * preloadFS: 4.x's default calling convention, sdcccall(1), can't call them. + */ +const SDCC_FS: { [platform: string]: PlatformToolConfig } = { + 'default': SDCC_DEFAULT_VERSION === 3 + ? { preloadFS: 'sdcc', wasiFSZip: 'sdcc-fs.zip' } + : { wasiFSZip: 'sdcc-fs.zip' }, + 'coleco': { preloadFS: 'sdcc' }, + 'msx-libcv': { preloadFS: 'sdcc' }, + 'sms': { preloadFS: 'sdcc' }, + 'gb': { preloadFS: 'sdcc' }, + 'cpc': { preloadFS: 'sdcc' }, +}; + const CC65_WASIFS: { [platform: string]: PlatformToolConfig } = { 'apple2': { wasiFSZip: 'cc65-fs-apple2.zip' }, 'c64': { wasiFSZip: 'cc65-fs-c64.zip' }, @@ -386,13 +411,13 @@ export const TOOL_META: { [id: string]: ToolMeta } = { sdcc: { id: 'sdcc', name: 'SDCC', kind: 'compiler', arch: 'z80', extensions: ['.c', '.h'], - includeDirs: ['/include'], + includeDirs: ['/share/sdcc/include', '/include'], editorStyle: 'text/x-csrc', helpURL: 'http://sdcc.sourceforge.net/doc/sdccman.pdf', - wasmModule: 'sdcc', - version: '3.6.5', + wasmModule: 'sdcc4', + version: SDCC_DEFAULT_VERSION === 3 ? '3.6.5' : '4.6.0', defineFlag: '-D', defineInline: true, - platforms: { default: { preloadFS: 'sdcc' } }, + platforms: SDCC_FS, includePatterns: SHARED_INCLUDE_PATTERNS, linkPatterns: SHARED_LINK_PATTERNS, }, @@ -400,11 +425,11 @@ export const TOOL_META: { [id: string]: ToolMeta } = { sdasz80: { id: 'sdasz80', name: 'sdasz80', kind: 'assembler', arch: 'z80', extensions: ['.s'], - includeDirs: ['/include'], + includeDirs: ['/share/sdcc/include', '/include'], editorStyle: 'z80', - wasmModule: 'sdasz80', + wasmModule: 'sdasz80-4', version: '02.00', - platforms: { default: { preloadFS: 'sdcc' } }, + platforms: SDCC_FS, includePatterns: SHARED_INCLUDE_PATTERNS, linkPatterns: SHARED_LINK_PATTERNS, }, @@ -423,7 +448,7 @@ export const TOOL_META: { [id: string]: ToolMeta } = { sdldz80: { id: 'sdldz80', name: 'sdldz80', kind: 'linker', arch: 'z80', extensions: [], - wasmModule: 'sdldz80', + wasmModule: 'sdld4', version: '03.00', linkSymbolFlag: '-g', linkSymbolInline: false, }, diff --git a/src/common/wasi/wasishim.ts b/src/common/wasi/wasishim.ts index ad2abb85..6b03498d 100644 --- a/src/common/wasi/wasishim.ts +++ b/src/common/wasi/wasishim.ts @@ -336,6 +336,18 @@ export class WASIMemoryFilesystem implements WASIFilesystem { if (this.dirs.has(name)) return WASIErrors.ISDIR; return this.files.delete(name) ? WASIErrors.SUCCESS : WASIErrors.NOENT; } + // Rename a file within this (writable) layer, replacing any file at newName. + renameFile(oldName: string, newName: string) { + oldName = normalizeWASIPath(oldName); + newName = normalizeWASIPath(newName); + if (this.dirs.has(oldName) || this.dirs.has(newName)) return WASIErrors.ISDIR; + const file = this.files.get(oldName); + if (!file) return WASIErrors.NOENT; + this.files.delete(oldName); + file.name = newName; + this.files.set(newName, file); + return WASIErrors.SUCCESS; + } removeDirectory(name: string) { name = normalizeWASIPath(name); if (name === '/') return WASIErrors.BUSY; // can't remove the root @@ -732,6 +744,17 @@ export class WASIRunner { debug("path_unlink_file", dir+"", path); return this.fs.removeFile(path); } + path_rename(old_dirfd: number, old_path_ptr: number, old_path_len: number, + new_dirfd: number, new_path_ptr: number, new_path_len: number) { + const olddir = this.fds[old_dirfd]; + const newdir = this.fds[new_dirfd]; + if (olddir == null || newdir == null) return WASIErrors.BADF; + if (olddir.type !== FDType.DIRECTORY || newdir.type !== FDType.DIRECTORY) return WASIErrors.NOTDIR; + const oldpath = olddir.name + '/' + this.peekUTF8(old_path_ptr, old_path_len); + const newpath = newdir.name + '/' + this.peekUTF8(new_path_ptr, new_path_len); + debug("path_rename", oldpath, newpath); + return this.fs.renameFile(oldpath, newpath); + } path_remove_directory(dirfd: number, path_ptr: number, path_len: number) { const dir = this.fds[dirfd]; if (dir == null) return WASIErrors.BADF; @@ -778,6 +801,7 @@ export class WASIRunner { path_readlink: this.path_readlink.bind(this), path_unlink_file: this.path_unlink_file.bind(this), path_remove_directory: this.path_remove_directory.bind(this), + path_rename: this.path_rename.bind(this), path_create_directory: this.path_create_directory.bind(this), clock_time_get: this.clock_time_get.bind(this), fd_fdstat_set_flags() { warning("TODO: fd_fdstat_set_flags"); return WASIErrors.NOTSUP; }, diff --git a/src/docs/build-directives.md b/src/docs/build-directives.md index 73a8e9ab..1712daac 100644 --- a/src/docs/build-directives.md +++ b/src/docs/build-directives.md @@ -129,16 +129,18 @@ Sets a named build parameter that the build system understands: //#tooldef NAME=VALUE ~~~ -For the linker phase, the supported parameters are: +The supported parameters are: -| Name | Meaning | -| --- | --- | -| `cfgfile` | linker configuration file to use (e.g. for CC65's ld65) | -| `libargs` | comma-separated list of library/symbol arguments | +| Phase | Name | Meaning | +| --- | --- | --- | +| `ld` | `cfgfile` | linker configuration file to use (e.g. for CC65's ld65) | +| `ld` | `libargs` | comma-separated list of library/symbol arguments | +| `c` | `sdcc` | SDCC version, `3` or `4` (see *SDCC versions* below) | ~~~c //#tooldef ld cfgfile=apple2-hgr2.cfg //#tooldef ld libargs=,nes.lib +//#tooldef c sdcc=3 ~~~ ### Commenting out a directive @@ -202,6 +204,63 @@ of the binary file. The `#embed` directive is also supported in Oscar64 and Wiz files. +## SDCC versions + +Z80 C files build with SDCC 3.6.5 by default, which compiles several times +faster. SDCC 4 produces smaller code (roughly 10-20% on the samples we +tried) but compiles more slowly, mostly in the register allocator. To build +a project with SDCC 4, add this line to the main file: + +~~~c +//#tooldef c sdcc=4 +~~~ + +The whole project then compiles, assembles, and links with SDCC 4. +Platforms whose libraries were compiled with SDCC 3.6.5 can't use SDCC 4, +and `//#tooldef c sdcc=4` on one of them is a build error: + +| Platform | Library | +| --- | --- | +| `coleco`, `msx-libcv`, `sms-*` | libcv / libcvu | +| `gb` | gb.lib | +| `cpc.*` | cpctelera, cpcrslib | + +`//#tooldef c sdcc=3` selects SDCC 3.6.5 explicitly, which keeps working if +the default changes. The default is `SDCC_DEFAULT_VERSION` in +`src/common/toolmeta.ts`. + +Unless the source has a `#pragma opt_code_size`, `opt_code_speed` or +`opt_code_balanced`, SDCC runs with `--no-peep --nolospre +--max-allocs-per-node 500` for fast builds. Adding one of those pragmas +turns the full optimizer back on. + +### Calling convention + +SDCC 4 passes function arguments in registers; SDCC 3 passed them on the +stack. C code works either way, but an assembly function that reads its +arguments from the stack breaks under SDCC 4. Mark such a function +`__sdcccall(0)`, in its prototype too, to keep the old convention: + +~~~c +void set_color(byte c) __sdcccall(0) __naked { + __asm + ld hl, #2 + add hl, sp + ld a, (hl) ; c is on the stack + out (0x40), a + ret + __endasm; +} +~~~ + +Without `__sdcccall(0)`, SDCC 4 passes `c` in register `A`. + +### Warnings + +SDCC 4 warns about more code than SDCC 3, e.g. `int main(int argc)`. +Warnings don't stop an SDCC 4 build and aren't marked in the editor. When +a build fails, the IDE marks its warnings along with its errors. + ## SDCC `#pragma` With SDCC, enable additional optimization by adding one of these lines to @@ -212,11 +271,13 @@ the top of the file: #pragma opt_code_speed // for faster code ~~~ -Either pragma slows builds but produces faster and smaller code. +Without a pragma, the IDE turns off some optimizations to keep builds +fast. Either pragma slows builds but produces faster and smaller code. ### Game Boy ROM banks -On the Game Boy, `#pragma bank N` at the top of a file puts its code and +The Game Boy builds with SDCC 3.6.5 (see *SDCC versions* above). +`#pragma bank N` at the top of a file puts its code and constant data in ROM bank *N*, which the CPU sees at `$4000-$7FFF`. Link the file from your main file, and call its functions through `__banked` prototypes. Each call switches to the function's bank and back again: diff --git a/src/worker/builder.ts b/src/worker/builder.ts index 0ae3674c..df9a86e0 100644 --- a/src/worker/builder.ts +++ b/src/worker/builder.ts @@ -466,7 +466,8 @@ export function applyAsmProjectParams(params) { * * //#symbol [] NAME[=VALUE] phase in c|as|ld (default c) * //#flag raw argv for one phase - * //#tooldef NAME=VALUE typed knob (linker: cfgfile, libargs) + * //#tooldef NAME=VALUE typed knob (linker: cfgfile, libargs; + * compiler: sdcc=3|4) * * Symbols are phase-scoped because they are different things: a preprocessor * macro (`c`) takes text, while an assembler/linker global (`as`/`ld`) is an @@ -732,6 +733,10 @@ export function applyBuildDirectives(dir: SourceDirectives, params) { params.cfgfile = td.value; } else if (td.phase === 'linker' && td.name === 'libargs') { params.libargs = td.value.split(',').filter((s) => s !== ''); + } else if (td.phase === 'compiler' && td.name === 'sdcc') { + // SDCC major version; every sdcc/sdas/sdld step of the build reads it + if (td.value === '3' || td.value === '4') params.sdcc_version = parseInt(td.value); + else dir.errors.push(`#tooldef c sdcc: expected 3 or 4, got '${td.value}'`); } else { dir.errors.push(`#tooldef: unknown ${td.phase} param '${td.name}'`); } diff --git a/src/worker/fs/sdcc-fs.zip b/src/worker/fs/sdcc-fs.zip new file mode 100644 index 00000000..63d841a1 Binary files /dev/null and b/src/worker/fs/sdcc-fs.zip differ diff --git a/src/worker/tools/cc65.ts b/src/worker/tools/cc65.ts index 6a27e97d..83eef8eb 100644 --- a/src/worker/tools/cc65.ts +++ b/src/worker/tools/cc65.ts @@ -1,12 +1,10 @@ import { defineArgs, extraArgsFor, linkSymbolArgs, getSharedFileSystemName } from "../../common/toolmeta"; import { CodeListingMap, WorkerError } from "../../common/workertypes"; -import { WASIRunner } from "../../common/wasi/wasishim"; import { BuildStep, BuildStepResult, gatherFiles, staleFiles, populateFiles, fixParamsWithDefines, applyAsmProjectParams, putWorkFile, populateExtraFiles, store, populateEntry, anyTargetChanged, processEmbedDirective } from "../builder"; import { re_crlf, makeErrorMatcher } from "../listingutils"; import { parseCC65DbgSizes } from "./cc65dbg"; -import { loadWASMBinary, ensureWasiFilesystem } from "../wasmutils"; -import { wasiFSAdapter } from "../wasiutils"; +import { wasiFSAdapter, runWASITool, checkExitCode, readWASIOutput, readWASIOutputString } from "../wasiutils"; // the WASI builds look for their data under share/cc65 in the per-platform // cc65-fs-.zip, preopened at '.' @@ -29,44 +27,9 @@ async function runCC65Tool(step: BuildStep, tool: string, args: string[], const fsname = getSharedFileSystemName('cc65', step.platform); if (!fsname || !fsname.startsWith('wasi:')) throw new Error("No cc65 filesystem for platform " + step.platform); - const sharefs = await ensureWasiFilesystem(fsname.substring(5)); - if (!sharefs) - throw new Error("Could not load cc65 filesystem " + fsname); - if (!wasiModules[tool]) { - wasiModules[tool] = new WebAssembly.Module(loadWASMBinary(tool)); - } - const wasi = new WASIRunner(); - wasi.initSync(wasiModules[tool]); - wasi.fs.setParent(sharefs); - populate(wasiFSAdapter(wasi)); - wasi.addPreopenDirectory("."); - wasi.setArgs([tool, ...args]); - const errno = wasi.run(); - console.log('exec', tool, args.join(' ')); - const stdout = wasi.fds[1].getBytesAsString(); - if (stdout) console.log(stdout); - const stderr = wasi.fds[2].getBytesAsString().split(re_crlf).filter(s => s != ''); - return { wasi, errno, stderr }; + return runWASITool(tool, args, { sharedFS: fsname.substring(5), populate }); } -/** Report a failed tool run that printed no parseable error message. */ -function checkExitCode(tool: string, errno: number, stderr: string[], errors: WorkerError[]) { - if (errno && !errors.length) { - errors.push({ line: 0, msg: tool + " exited with code " + errno + (stderr.length ? ": " + stderr.join('\n') : '') }); - } -} - -function readWASIOutput(wasi: WASIRunner, path: string): Uint8Array { - const fd = wasi.fs.getFile(path); - if (!fd) throw new Error("Missing output file " + path); - return fd.getBytes().slice(); -} - -function readWASIOutputString(wasi: WASIRunner, path: string): string { - return new TextDecoder().decode(readWASIOutput(wasi, path)); -} - - /* 000000r 1 .segment "CODE" 000000r 1 .proc _rasterWait: near diff --git a/src/worker/tools/mcpp.ts b/src/worker/tools/mcpp.ts index 15462688..73b7e3ae 100644 --- a/src/worker/tools/mcpp.ts +++ b/src/worker/tools/mcpp.ts @@ -4,12 +4,46 @@ import { BuildStep, populateFiles, populateExtraFiles, errorResult, processEmbed import { makeErrorMatcher, extractErrors } from "../listingutils"; import { PLATFORM_PARAMS } from "../platforms"; import { load, print_fn, setupFS, execMain, emglobal, EmscriptenModule } from "../wasmutils"; +import type { WASIMemoryFilesystem } from "../../common/wasi/wasishim"; function makeCPPSafe(s: string): string { return s.replace(/[^A-Za-z0-9_]/g, '_'); } -export function preprocessMCPP(step: BuildStep, filesys: string) { +/** Headers in a WASI filesystem zip, under `dir`, for mcpp's /share/include. */ +export interface MCPPIncludeSource { + fs: WASIMemoryFilesystem; + dir: string; +} + +/** Copy the headers under src.dir into an Emscripten FS at /share/include. */ +function copyWASIIncludes(FS, src: MCPPIncludeSource) { + const prefix = src.dir + '/'; + const made = new Set(); + const mkdirs = (path: string) => { + let parts = path.split('/'); + for (let i = 2; i < parts.length; i++) { + let dir = parts.slice(0, i).join('/'); + if (!made.has(dir)) { + if (!FS.analyzePath(dir).exists) FS.mkdir(dir); + made.add(dir); + } + } + }; + for (let file of src.fs.getFiles()) { + if (file.name.startsWith(prefix)) { + let path = '/share/include/' + file.name.substring(prefix.length); + mkdirs(path); + FS.writeFile(path, file.getBytes(), { encoding: 'binary' }); + } + } +} + +/** + * Preprocess the step's main source. `filesys` names the Emscripten FS + * package mounted at /share, or gives headers to copy into /share/include. + */ +export function preprocessMCPP(step: BuildStep, filesys: string | MCPPIncludeSource, extraArgs: string[] = []) { load("mcpp"); var platform = step.platform; var params = PLATFORM_PARAMS[platform] || PLATFORM_PARAMS[getBasePlatform(platform)]; @@ -24,7 +58,8 @@ export function preprocessMCPP(step: BuildStep, filesys: string) { printErr: match_fn, }); var FS = MCPP.FS; - if (filesys) setupFS(FS, filesys); + if (typeof filesys === 'string') setupFS(FS, filesys); + else if (filesys) copyWASIIncludes(FS, filesys); populateFiles(step, FS, { mainFilePath: step.path, processFn: (path, code) => { @@ -49,6 +84,7 @@ export function preprocessMCPP(step: BuildStep, filesys: string) { } let platform_def = (platform.toUpperCase() as any).replaceAll(/[^a-zA-Z0-9]/g, '_'); args.unshift.apply(args, ["-D", `__PLATFORM_${platform_def}__`]); + args.unshift(...extraArgs); if (params.extra_preproc_args) { args.push.apply(args, params.extra_preproc_args); } diff --git a/src/worker/tools/sdcc.ts b/src/worker/tools/sdcc.ts index 8ccd7904..8a1fdec0 100644 --- a/src/worker/tools/sdcc.ts +++ b/src/worker/tools/sdcc.ts @@ -1,11 +1,54 @@ -import { Worker } from "node:worker_threads"; -import { defineArgs, extraArgsFor, linkSymbolArgs } from "../../common/toolmeta"; +import { defineArgs, extraArgsFor, linkSymbolArgs, getPlatformToolConfig, SDCC_DEFAULT_VERSION } from "../../common/toolmeta"; import { CodeListingMap, WorkerError } from "../../common/workertypes"; import { BuildStep, BuildStepResult, gatherFiles, staleFiles, populateFiles, putWorkFile, populateExtraFiles, anyTargetChanged, getWorkFileAsString, fixParamsWithDefines, applyAsmProjectParams } from "../builder"; import { parseListing, parseSourceLines, msvcErrorMatcher } from "../listingutils"; -import { EmscriptenModule, emglobal, execMain, loadNative, moduleInstFn, print_fn, setupFS, setupStdin } from "../wasmutils"; +import { EmscriptenModule, emglobal, ensureFilesystem, ensureWasiFilesystem, execMain, loadNative, moduleInstFn, print_fn, setupFS, setupStdin } from "../wasmutils"; +import { runWASITool, checkExitCode, readWASIOutputString } from "../wasiutils"; import { preprocessMCPP } from "./mcpp"; +// SDCC 4.x runs as WASI over sdcc-fs.zip, which holds share/sdcc/{include,lib/}. +// Platforms with libraries built by SDCC 3.x stay on the 3.6.5 Emscripten build +// (see SDCC_FS in toolmeta). +const SDCC4_INCLUDE = 'share/sdcc/include'; +const SDCC4_LIB = 'share/sdcc/lib'; + +// what the sdcc driver predefines when it runs sdcpp; mcpp must supply them +const SDCC4_DEFINES = [ + '-D', '__SDCC=4_6_3', + '-D', '__SDCC_VERSION_MAJOR=4', + '-D', '__SDCC_VERSION_MINOR=6', + '-D', '__SDCC_VERSION_PATCH=3', + '-D', '__SDCCCALL=1', + '-D', '__STDC_NO_COMPLEX__=1', + '-D', '__STDC_NO_THREADS__=1', + '-D', '__STDC_NO_ATOMICS__=1', + '-D', '__STDC_NO_VLA__=1', +]; + +/** + * The SDCC 4.x filesystem zip for this build, or null for SDCC 3.6.5. The + * version is `//#tooldef c sdcc=3|4` (params.sdcc_version) or else + * SDCC_DEFAULT_VERSION. Platforms with 3.x-only libraries always get 3.x, + * and asking for 4.x there is an error. + */ +function sdcc4FS(step: BuildStep): string | null { + const requested = step.params.sdcc_version; + if ((requested ?? SDCC_DEFAULT_VERSION) !== 4) return null; + const zip = getPlatformToolConfig('sdcc', step.platform)?.wasiFSZip; + if (!zip && requested === 4) { + throw new Error(`SDCC 4 can't build for ${step.platform}: its libraries were compiled by SDCC 3. Remove "//#tooldef c sdcc=4".`); + } + return zip || null; +} + +/** True if the step's cached asm output came from the other SDCC version. */ +function builtByOtherSDCC(outpath: string, fs4: string | null): boolean { + const asm = getWorkFileAsString(outpath); + if (typeof asm !== 'string') return false; + const m = /^; Version (\d+)\./m.exec(asm); + return !!m && (m[1] === '4') !== !!fs4; +} + function hexToArray(s, ofs) { var buf = new ArrayBuffer(s.length / 2); var arr = new Uint8Array(buf); @@ -41,6 +84,19 @@ export function bankedAreaArgs(rels: string[], banking: ROMBanking): string[] { return args; } +/** + * True if any of the object files declares the area, or if one of them can't + * be read (so the caller keeps its default). + */ +export function objectsDefineArea(objfiles: string[], area: string): boolean { + for (let fn of objfiles) { + if (!fn.endsWith('.rel')) continue; + let rel = getWorkFileAsString(fn); + if (typeof rel !== 'string' || new RegExp(`^A ${area} `, 'm').test(rel)) return true; + } + return false; +} + /** * Convert Intel HEX to a ROM image of rom_size bytes. With banking, records * above 64 KB are placed by bank, and the image grows to the next power of 2 @@ -157,8 +213,6 @@ function errorMatcherSDASZ80(path: string, errors: WorkerError[]) { } async function assembleSDAS(step: BuildStep, tool: 'sdasz80' | 'sdasgb'): Promise { - loadNative(tool); - var objout, lstout, symout; var errors = []; gatherFiles(step, { mainFilePath: "main.asm" }); var objpath = step.prefix + ".rel"; @@ -169,24 +223,41 @@ async function assembleSDAS(step: BuildStep, tool: 'sdasz80' | 'sdasgb'): Promis } if (staleFiles(step, [objpath, lstpath])) { const match_asm_fn = errorMatcherSDASZ80(step.path, errors); - var AS: EmscriptenModule = emglobal[tool]({ - instantiateWasm: moduleInstFn(tool), - noInitialRun: true, - //logReadFiles:true, - print: match_asm_fn, - printErr: match_asm_fn, - }); - // old-style Emscripten modules return the Module object, whose .then() - // only resolves after main() runs; newer MODULARIZE factories return a Promise - if (AS instanceof Promise) AS = await AS; - var FS = AS.FS; - populateFiles(step, FS); - execMain(step, AS, ['-plosgffwy', step.path]); - if (errors.length) { - return { errors: errors }; + const args = ['-plosgffwy', step.path]; + var objout, lstout; + if (tool == 'sdasz80' && sdcc4FS(step)) { + const { wasi, errno, stdout, stderr } = await runWASITool(tool, args, { + module: 'sdasz80-4', + populate: (fs) => populateFiles(step, fs), + }); + stdout.concat(stderr).forEach(match_asm_fn); + checkExitCode(tool, errno, stderr, errors); + if (errors.length) { + return { errors: errors }; + } + objout = readWASIOutputString(wasi, objpath); + lstout = readWASIOutputString(wasi, lstpath); + } else { + loadNative(tool); + var AS: EmscriptenModule = emglobal[tool]({ + instantiateWasm: moduleInstFn(tool), + noInitialRun: true, + //logReadFiles:true, + print: match_asm_fn, + printErr: match_asm_fn, + }); + // old-style Emscripten modules return the Module object, whose .then() + // only resolves after main() runs; newer MODULARIZE factories return a Promise + if (AS instanceof Promise) AS = await AS; + var FS = AS.FS; + populateFiles(step, FS); + execMain(step, AS, args); + if (errors.length) { + return { errors: errors }; + } + objout = FS.readFile(objpath, { encoding: 'utf8' }); + lstout = FS.readFile(lstpath, { encoding: 'utf8' }); } - objout = FS.readFile(objpath, { encoding: 'utf8' }); - lstout = FS.readFile(lstpath, { encoding: 'utf8' }); putWorkFile(objpath, objout); putWorkFile(lstpath, lstout); } @@ -195,7 +266,6 @@ async function assembleSDAS(step: BuildStep, tool: 'sdasz80' | 'sdasgb'): Promis files: [objpath, lstpath], args: [objpath] }; - //symout = FS.readFile("main.sym", {encoding:'utf8'}); } export function assembleSDASZ80(step: BuildStep): Promise { @@ -206,9 +276,9 @@ export function assembleSDASGB(step: BuildStep): Promise { return assembleSDAS(step, 'sdasgb'); } -export function linkSDLDZ80(step: BuildStep) { - loadNative("sdldz80"); +export async function linkSDLDZ80(step: BuildStep) { const arch = step.params.arch || 'z80'; + const fs4 = sdcc4FS(step); var errors = []; gatherFiles(step); var binpath = "main.ihx"; @@ -225,28 +295,16 @@ export function linkSDLDZ80(step: BuildStep) { } } var params = step.params; - var LDZ80: EmscriptenModule = emglobal.sdldz80({ - instantiateWasm: moduleInstFn('sdldz80'), - noInitialRun: true, - //logReadFiles:true, - print: match_aslink_fn, - printErr: match_aslink_fn, - }); - var FS = LDZ80.FS; - setupFS(FS, 'sdcc'); - populateFiles(step, FS); - populateExtraFiles(step, FS, params.extra_link_files); - // TODO: coleco hack so that -u flag works - if (step.platform.startsWith("coleco")) { - FS.writeFile('crt0.rel', FS.readFile('/share/lib/coleco/crt0.rel', { encoding: 'utf8' })); - FS.writeFile('crt0.lst', '\n'); // TODO: needed so -u flag works + var libdir = fs4 ? `${SDCC4_LIB}/${arch}` : arch === 'z80' ? '/share/lib/z80' : '.'; // sm83.lib copied to current (.) directory + var args = ['-mjwxyu', '-i', 'main.ihx']; + // sdld 4.x fails on -b for an area no module defines (an asm-only + // program may have no _DATA) + var bases = { _CODE: params.codeseg_start || params.code_start, _DATA: params.data_start }; + for (let area in bases) { + if (!fs4 || objectsDefineArea(step.args.concat(params.extra_link_args || []), area)) + args.push('-b', `${area}=0x${bases[area].toString(16)}`); } - var args = ['-mjwxyu', - '-i', 'main.ihx', - '-b', '_CODE=0x' + (params.codeseg_start||params.code_start).toString(16), - '-b', '_DATA=0x' + params.data_start.toString(16), - '-k', arch === 'z80' ? '/share/lib/z80' : '.', // sm83.lib copied to current (.) directory - '-l', arch]; + args.push('-k', libdir, '-l', arch); if (params.extra_link_args) args.push.apply(args, params.extra_link_args); // //#symbol ld (sdldz80 uses -g sym=expr) and //#flag ld @@ -274,13 +332,50 @@ export function linkSDLDZ80(step: BuildStep) { } objargs = withStartupObjects(params.startup_objs, objargs); args.push.apply(args, objargs); - //console.log(args); - execMain(step, LDZ80, args); - if (errors.length) { - return { errors: errors }; + var readText: (path: string) => string; + if (fs4) { + // one sdld binary for all targets: it picks z80 from argv[0] + const { wasi, errno, stdout, stderr } = await runWASITool('sdldz80', args, { + module: 'sdld4', + sharedFS: fs4, + populate: (fs) => { + populateFiles(step, fs); + populateExtraFiles(step, fs, params.extra_link_files); + }, + }); + stdout.concat(stderr).forEach(match_aslink_fn); + checkExitCode('sdldz80', errno, stderr, errors); + if (errors.length) { + return { errors: errors }; + } + readText = (path) => readWASIOutputString(wasi, path); + } else { + loadNative("sdldz80"); + var LDZ80: EmscriptenModule = emglobal.sdldz80({ + instantiateWasm: moduleInstFn('sdldz80'), + noInitialRun: true, + //logReadFiles:true, + print: match_aslink_fn, + printErr: match_aslink_fn, + }); + var FS = LDZ80.FS; + ensureFilesystem('sdcc'); + setupFS(FS, 'sdcc'); + populateFiles(step, FS); + populateExtraFiles(step, FS, params.extra_link_files); + // TODO: coleco hack so that -u flag works + if (step.platform.startsWith("coleco")) { + FS.writeFile('crt0.rel', FS.readFile('/share/lib/coleco/crt0.rel', { encoding: 'utf8' })); + FS.writeFile('crt0.lst', '\n'); // TODO: needed so -u flag works + } + execMain(step, LDZ80, args); + if (errors.length) { + return { errors: errors }; + } + readText = (path) => FS.readFile(path, { encoding: 'utf8' }); } - var hexout = FS.readFile("main.ihx", { encoding: 'utf8' }); - var noiout = FS.readFile("main.noi", { encoding: 'utf8' }); + var hexout = readText("main.ihx"); + var noiout = readText("main.noi"); putWorkFile("main.ihx", hexout); putWorkFile("main.noi", noiout); // return unchanged if no files changed @@ -295,17 +390,9 @@ export function linkSDLDZ80(step: BuildStep) { var listings: CodeListingMap = {}; for (var fn of step.files) { if (fn.endsWith('.lst')) { - var rstout = FS.readFile(fn.replace('.lst', '.rst'), { encoding: 'utf8' }); - // 0000 21 02 00 [10] 52 ld hl, #2 - var asmlines = parseListing(rstout, /^\s*([0-9A-F]{4,6})\s+([0-9A-F][0-9A-F r]*[0-9A-F])\s+\[([0-9 ]+)\]?\s+(\d+) (.*)/i, 4, 1, 2, 3); - var srclines = parseSourceLines(rstout, /^\s+\d+ ;:(\d+):/i, /^\s*([0-9A-F]{4,6})/i); + var rstout = readText(fn.replace('.lst', '.rst')); putWorkFile(fn, rstout); - // TODO: you have to get rid of all source lines to get asm listing - listings[fn] = { - asmlines: srclines.length ? asmlines : null, - lines: srclines.length ? srclines : asmlines, - text: rstout - }; + listings[fn] = parseRSTListing(rstout, fn.replace(/\.lst$/, '')); } } // parse symbol map @@ -361,6 +448,27 @@ export function linkSDLDZ80(step: BuildStep) { } } +/** + * Parse a linked (.rst) listing into asm lines and, when the compiler left + * source comments, source lines. `srcprefix` is the source file name without + * its extension: SDCC 3.6.5 writes `;:N:` comments, SDCC 4.x writes + * `;file.c:N: ` (with mcpp's `//` path prefix), and 4.x addresses + * have 8 hex digits instead of 4. + */ +export function parseRSTListing(rstout: string, srcprefix: string) { + // 0000 21 02 00 [10] 52 ld hl, #2 + var asmlines = parseListing(rstout, /^\s*([0-9A-F]{4,8})\s+([0-9A-F][0-9A-F r]*[0-9A-F])\s+\[([0-9 ]+)\]?\s+(\d+) (.*)/i, 4, 1, 2, 3); + const name = srcprefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const srcre = new RegExp(`^\\s+\\d+ ;(?:|/*${name}\\.[^:\\s]+):(\\d+):`, 'i'); + var srclines = parseSourceLines(rstout, srcre, /^\s*([0-9A-F]{4,8})/i); + // TODO: you have to get rid of all source lines to get asm listing + return { + asmlines: srclines.length ? asmlines : null, + lines: srclines.length ? srclines : asmlines, + text: rstout + }; +} + /** * sdcc 3.6.5 compiles a __banked call to `call banked_call; .dw _fn; .dw 0`, * with a "PENDING: bank support" comment where the bank number belongs. Supply @@ -392,7 +500,7 @@ export function withStartupObjects(startup: string[] | undefined, objargs: strin return startup.filter((fn) => !own.has(fn)).concat(objargs); } -export function compileSDCC(step: BuildStep): BuildStepResult { +export async function compileSDCC(step: BuildStep): Promise { gatherFiles(step, { mainFilePath: "main.c" // not used @@ -401,29 +509,25 @@ export function compileSDCC(step: BuildStep): BuildStepResult { var isGBZ80 = params.arch === 'gbz80'; var outpath = step.prefix + ".asm"; fixParamsWithDefines(step.path, params); // //#symbol, //#flag, //#tooldef - if (staleFiles(step, [outpath])) { + const fs4 = sdcc4FS(step); // after the directives: //#tooldef c sdcc=3|4 + if (staleFiles(step, [outpath]) || builtByOtherSDCC(outpath, fs4)) { var errors = []; - loadNative('sdcc'); - var SDCC: EmscriptenModule = emglobal.sdcc({ - instantiateWasm: moduleInstFn('sdcc'), - noInitialRun: true, - noFSInit: true, - print: print_fn, - printErr: msvcErrorMatcher(errors), - //TOTAL_MEMORY:256*1024*1024, - }); - var FS = SDCC.FS; - populateFiles(step, FS); // load source file and preprocess var code = getWorkFileAsString(step.path); - var preproc = preprocessMCPP(step, 'sdcc'); + var preproc; + if (fs4) { + const sharefs = await ensureWasiFilesystem(fs4); + if (!sharefs) throw new Error("Could not load SDCC filesystem " + fs4); + preproc = preprocessMCPP(step, { fs: sharefs, dir: SDCC4_INCLUDE }, SDCC4_DEFINES); + } else { + ensureFilesystem('sdcc'); + preproc = preprocessMCPP(step, 'sdcc'); + } if (preproc.errors) { return { errors: preproc.errors }; } - else code = preproc.code; - // pipe file to stdin - setupStdin(FS, code); - setupFS(FS, 'sdcc'); + // mcpp keeps a UTF-8 byte order mark, which 4.x rejects + else code = preproc.code.replace(/\uFEFF/g, ''); const machineFlags = isGBZ80 ? '-mgbz80' : '-mz80'; var args = ['--vc', '--std-sdcc99', machineFlags, //'-Wall', '--c1mode', @@ -458,13 +562,50 @@ export function compileSDCC(step: BuildStep): BuildStepResult { // //#symbol c and //#flag c args.push.apply(args, defineArgs('sdcc', params.symbols && params.symbols.compiler)); args.push.apply(args, extraArgsFor('sdcc', params.buildArgs)); - execMain(step, SDCC, args); - // TODO: preprocessor errors w/ correct file - if (errors.length /* && nwarnings < msvc_errors.length*/) { - return { errors: errors }; + var asmout: string; + if (fs4) { + // --c1mode reads the preprocessed source from stdin + const { wasi, errno, stdout, stderr } = await runWASITool('sdcc', args, { + module: 'sdcc4', + stdin: code, + populate: (fs) => populateFiles(step, fs), + }); + // 4.x warns more than 3.6.5 did (e.g. `int main(int argc)`), and + // WorkerError has no severity: only a failed run reports them + stderr.forEach(msvcErrorMatcher(errors)); + if (!errno) { + errors.forEach((e) => console.log('sdcc warning:', e.path + ':' + e.line, e.msg)); + errors = []; + } + checkExitCode('sdcc', errno, stderr, errors); + if (errors.length) { + return { errors: errors }; + } + asmout = readWASIOutputString(wasi, outpath); + } else { + loadNative('sdcc'); + var SDCC: EmscriptenModule = emglobal.sdcc({ + instantiateWasm: moduleInstFn('sdcc'), + noInitialRun: true, + noFSInit: true, + print: print_fn, + printErr: msvcErrorMatcher(errors), + //TOTAL_MEMORY:256*1024*1024, + }); + var FS = SDCC.FS; + populateFiles(step, FS); + // pipe file to stdin + setupStdin(FS, code); + ensureFilesystem('sdcc'); // not preloaded on a 4.x platform built with sdcc=3 + setupFS(FS, 'sdcc'); + execMain(step, SDCC, args); + // TODO: preprocessor errors w/ correct file + if (errors.length /* && nwarnings < msvc_errors.length*/) { + return { errors: errors }; + } + asmout = FS.readFile(outpath, { encoding: 'utf8' }); } // massage the asm output - var asmout = FS.readFile(outpath, { encoding: 'utf8' }); asmout = " .area _HOME\n .area _CODE\n .area _INITIALIZER\n .area _DATA\n .area _INITIALIZED\n .area _BSEG\n .area _BSS\n .area _HEAP\n" + asmout; if (isGBZ80) asmout = fixBankedCalls(asmout); putWorkFile(outpath, asmout); diff --git a/src/worker/wasiutils.ts b/src/worker/wasiutils.ts index 99d0077b..a4a6cc39 100644 --- a/src/worker/wasiutils.ts +++ b/src/worker/wasiutils.ts @@ -2,6 +2,8 @@ import JSZip from 'jszip'; import { WASIRunner, WASIMemoryFilesystem } from "../common/wasi/wasishim"; import { WorkerError } from "../common/workertypes"; import { BuildStep, store } from "./builder"; +import { re_crlf } from "./listingutils"; +import { ensureWasiFilesystem, loadWASMBinary } from "./wasmutils"; export function loadBlobSync(path: string) { var xhr = new XMLHttpRequest(); @@ -73,3 +75,63 @@ export function wasiFSAdapter(wasi: WASIRunner) { utime: () => { }, }; } + +const wasiModules: { [module: string]: WebAssembly.Module } = {}; + +export interface WASIToolOptions { + module?: string; // wasm module name, if not the same as argv[0] + sharedFS?: string; // filesystem zip layered under the runner (parent layer) + stdin?: string; // text fed to the tool's stdin + populate?: (fs: ReturnType) => void; // copy inputs in +} + +/** + * Run a WASI tool on a fresh runner with '.' preopened, optionally layered over + * a shared filesystem zip. argv[0] is `tool` (some tools, like sdld, pick + * their target from it). Returns the runner (for reading outputs), its exit + * code and its stdout and stderr lines. + */ +export async function runWASITool(tool: string, args: string[], opts: WASIToolOptions = {}) { + const module = opts.module || tool; + const wasi = new WASIRunner(); + if (opts.sharedFS) { + const sharefs = await ensureWasiFilesystem(opts.sharedFS); + if (!sharefs) + throw new Error("Could not load filesystem " + opts.sharedFS); + wasi.fs.setParent(sharefs); + } + if (!wasiModules[module]) { + wasiModules[module] = new WebAssembly.Module(loadWASMBinary(module) as Uint8Array); + } + wasi.initSync(wasiModules[module]); + if (opts.populate) opts.populate(wasiFSAdapter(wasi)); + if (opts.stdin != null) { + wasi.stdin.write(new TextEncoder().encode(opts.stdin)); + wasi.stdin.offset = 0; // so fd_read starts at the beginning + } + wasi.addPreopenDirectory("."); + wasi.setArgs([tool, ...args]); + const errno = wasi.run(); + console.log('exec', tool, args.join(' ')); + const stdout = wasi.fds[1].getBytesAsString().split(re_crlf).filter(s => s != ''); + if (stdout.length) console.log(stdout.join('\n')); + const stderr = wasi.fds[2].getBytesAsString().split(re_crlf).filter(s => s != ''); + return { wasi, errno, stdout, stderr }; +} + +/** Report a failed tool run that printed no parseable error message. */ +export function checkExitCode(tool: string, errno: number, stderr: string[], errors: WorkerError[]) { + if (errno && !errors.length) { + errors.push({ line: 0, msg: tool + " exited with code " + errno + (stderr.length ? ": " + stderr.join('\n') : '') }); + } +} + +export function readWASIOutput(wasi: WASIRunner, path: string): Uint8Array { + const fd = wasi.fs.getFile(path); + if (!fd) throw new Error("Missing output file " + path); + return fd.getBytes().slice(); +} + +export function readWASIOutputString(wasi: WASIRunner, path: string): string { + return new TextDecoder().decode(readWASIOutput(wasi, path)); +} diff --git a/src/worker/wasm/sdasz80-4.wasm b/src/worker/wasm/sdasz80-4.wasm new file mode 100755 index 00000000..eaa79278 Binary files /dev/null and b/src/worker/wasm/sdasz80-4.wasm differ diff --git a/src/worker/wasm/sdcc4.wasm b/src/worker/wasm/sdcc4.wasm new file mode 100755 index 00000000..5f935406 Binary files /dev/null and b/src/worker/wasm/sdcc4.wasm differ diff --git a/src/worker/wasm/sdld4.wasm b/src/worker/wasm/sdld4.wasm new file mode 100755 index 00000000..604280d3 Binary files /dev/null and b/src/worker/wasm/sdld4.wasm differ diff --git a/test/cli/testworker.js b/test/cli/testworker.js index a07f80ec..17b4bfee 100644 --- a/test/cli/testworker.js +++ b/test/cli/testworker.js @@ -18,6 +18,7 @@ global.onmessage({data:{preload:'ca65', platform:'atari2600'}}); global.onmessage({data:{preload:'cc65', platform:'pce'}}); global.onmessage({data:{preload:'ca65', platform:'pce'}}); global.onmessage({data:{preload:'sdcc'}}); +global.onmessage({data:{preload:'sdcc', platform:'coleco'}}); // SDCC 3.6.5 (Emscripten) filesystem global.onmessage({data:{preload:'inform6'}}); // TODO: check msg against spec @@ -183,11 +184,45 @@ describe('Worker', function() { it('should NOT link SDASZ80', function(done) { compile('sdasz80', '\tcall divxxx\n', 'mw8080bw', done, 0, 0, 1, {ignoreErrorPath:true}); }); - it('should compile SDCC', function(done) { - compile('sdcc', 'int foo=0; // comment\n#if defined(__8BITWORKSHOP__) && defined(__MAIN__)\nint main(int argc) {\nint x=1;\nint y=2+argc;\nreturn x+y+argc;\n}\n#endif\n', 'mw8080bw', done, 8192, 3, 0); + // 3.6.5 marks fewer source lines than 4.x (3 here, vs 4) + const SDCC_TEST_SRC = 'int foo=0; // comment\n#if defined(__8BITWORKSHOP__) && defined(__MAIN__)\nint main(int argc) {\nint x=1;\nint y=2+argc;\nreturn x+y+argc;\n}\n#endif\n'; + const SDCC_DEFAULT = +/SDCC_DEFAULT_VERSION[^=]*= *(\d)/.exec(fs.readFileSync('src/common/toolmeta.ts', 'utf8'))[1]; + const sdccLines = (n3, n4) => SDCC_DEFAULT == 4 ? n4 : n3; // listing line counts differ by version + it('should compile SDCC with the default version', function(done) { + compile('sdcc', SDCC_TEST_SRC, 'mw8080bw', done, 8192, sdccLines(3, 4), 0); + }); + it('should compile SDCC 3 with //#tooldef c sdcc=3', function(done) { + compile('sdcc', '//#tooldef c sdcc=3\n' + SDCC_TEST_SRC, 'mw8080bw', done, 8192, 3, 0); + }); + it('should compile SDCC 4 with //#tooldef c sdcc=4', function(done) { + compile('sdcc', '//#tooldef c sdcc=4\n' + SDCC_TEST_SRC, 'mw8080bw', done, 8192, 4, 0); + }); + it('should pass --max-allocs-per-node unless #pragma opt_code', async function() { + for (const platform of ['mw8080bw', 'gb']) { + for (const ver of platform == 'gb' ? [3] : [3, 4]) { + for (const pragma of ['', '#pragma opt_code_speed\n']) { + var result; + global.postMessage = (msg) => { result = msg; }; + await global.onmessage({data:{reset:true}}); + const logs = []; const ol = console.log; console.log = (...a) => logs.push(a.join(' ')); + try { + await global.onmessage({data:{code:'//#tooldef c sdcc=' + ver + '\n' + pragma + 'void main() {\n}\n', + platform:platform, tool:'sdcc', path:'src.sdcc', mainfile:true}}); + } finally { console.log = ol; } + const what = [platform, 'sdcc', ver, JSON.stringify(pragma)].join(' '); + // the test worker has no gb filesystem package, so only the compile command is checked there + if (platform != 'gb') assert.ok(!result.errors || !result.errors.length, what + ': ' + JSON.stringify(result.errors)); + const cmd = logs.find(l => /^exec sdcc --vc/.test(l)) || ''; + assert.equal(/--no-peep --nolospre --max-allocs-per-node 500/.test(cmd), !pragma, what + ': ' + cmd); + } + } + } + }); + it('should NOT compile SDCC 4 for a 3.x-library platform', function(done) { + compile('sdcc', '//#tooldef c sdcc=4\nvoid main() {\n}\n', 'coleco', done, 0, 0, 1, {ignoreErrorPath:true}); }); it('should compile SDCC w/ include', function(done) { - compile('sdcc', '#include \nvoid main() {\nstrlen(0);\n}\n', 'mw8080bw', done, 8192, 2, 0); + compile('sdcc', '#include \nvoid main() {\nstrlen(0);\n}\n', 'mw8080bw', done, 8192, sdccLines(2, 3), 0); }); it('should compile oscar64 and return listings/symbols/segments', async function() { var msgs = [{code:'#include \nint main() { printf("FOO"); return 0; }', platform:'c64', tool:'oscar64', path:'main.c', mainfile:true}]; @@ -276,23 +311,23 @@ describe('Worker', function() { it('should compile mw8080 skeleton', function(done) { var csource = ab2str(fs.readFileSync('presets/mw8080bw/skeleton.sdcc')); - compile('sdcc', csource, 'mw8080bw', done, 8192, 84, 0); + compile('sdcc', csource, 'mw8080bw', done, 8192, sdccLines(84, 97), 0); }); it('should compile galaxian skeleton', function(done) { var csource = ab2str(fs.readFileSync('presets/galaxian-scramble/skeleton.sdcc')); - compile('sdcc', csource, 'galaxian-scramble', done, 20512, 28, 0); + compile('sdcc', csource, 'galaxian-scramble', done, 20512, sdccLines(28, 36), 0); }); it('should compile vector skeleton', function(done) { var csource = ab2str(fs.readFileSync('presets/vector-z80color/skeleton.sdcc')); - compile('sdcc', csource, 'vector-z80color', done, 32768, 23, 0); + compile('sdcc', csource, 'vector-z80color', done, 32768, sdccLines(23, 26), 0); }); it('should compile williams skeleton', function(done) { var csource = ab2str(fs.readFileSync('presets/williams-z80/skeleton.sdcc')); - compile('sdcc', csource, 'williams-z80', done, 38912, 40, 0); + compile('sdcc', csource, 'williams-z80', done, 38912, sdccLines(40, 43), 0); }); it('should compile williams_sound skeleton', function(done) { var csource = ab2str(fs.readFileSync('presets/sound_williams-z80/skeleton.sdcc')); - compile('sdcc', csource, 'sound_williams-z80', done, 16384, 6, 0); + compile('sdcc', csource, 'sound_williams-z80', done, 16384, sdccLines(6, 7), 0); }); it('should compile coleco skeleton', function(done) { var csource = ab2str(fs.readFileSync('presets/coleco/cursorsmooth.c')); @@ -370,6 +405,29 @@ describe('Worker', function() { var msgs = [m, m, m2]; doBuild(msgs, done, 8192, [1,1], 0); }); + it('should rebuild unchanged files when //#tooldef c sdcc changes', async function() { + var build = (main) => ({ + "updates":[ + {"path":"main.c", "data":main + "extern int mul2(int x);\nint main() { return mul2(2); }\n"}, + {"path":"fn.c", "data":"int mul2(int x) { return x*x; }\n"} + ], + "buildsteps":[ + {"path":"main.c", "platform":"mw8080bw", "tool":"sdcc"}, + {"path":"fn.c", "platform":"mw8080bw", "tool":"sdcc"} + ] + }); + // fn.c has no directive, and doesn't change: its asm banner shows which SDCC built it + var fnVersion = async (main) => { + var result; + global.postMessage = (msg) => { result = msg; }; + await global.onmessage({data:build(main)}); + assert.ok(!result.errors || !result.errors.length, JSON.stringify(result.errors)); + return /; Version (\d+)\./.exec(result.listings['fn.lst'].text)[1]; + }; + await global.onmessage({data:{reset:true}}); + assert.equal(await fnVersion("//#tooldef c sdcc=4\n"), '4'); + assert.equal(await fnVersion("//#tooldef c sdcc=3\n"), '3'); + }); it('should include filename in compile errors', function(done) { var m = { "updates":[ @@ -387,7 +445,7 @@ describe('Worker', function() { }); it('should compile vicdual skeleton', function(done) { var files = ['skeleton.sdcc', 'cp437.c']; - compileFiles('sdcc', files, 'vicdual', done, 16416, [0,45], 0); // TODO? + compileFiles('sdcc', files, 'vicdual', done, 16416, [0, sdccLines(45, 55)], 0); // TODO? }); it('should compile apple2 skeleton with CC65', function(done) { var csource = ab2str(fs.readFileSync('presets/apple2/skeleton.cc65')); diff --git a/test/unit/testbuildparams.ts b/test/unit/testbuildparams.ts index 453d21c6..ba879cbf 100644 --- a/test/unit/testbuildparams.ts +++ b/test/unit/testbuildparams.ts @@ -248,6 +248,15 @@ describe("explicit build directives (//#symbol, //#flag, //#tooldef)", function assert.deepStrictEqual(params.libargs, ["a.lib", "b.lib"]); }); + it("#tooldef c sdcc picks the SDCC version", function () { + const params: any = {}; + applyDirectives("//#tooldef c sdcc=3\n", params); + assert.strictEqual(params.sdcc_version, 3); + const dir = applyDirectives("//#tooldef c sdcc=5\n", params); + assert.strictEqual(dir.errors.length, 1); + assert.strictEqual(params.sdcc_version, 3); + }); + it("#tooldef rejects unknown knobs", function () { const params = c64Params(); const dir = applyDirectives("//#tooldef ld nonsense=1\n", params); @@ -298,7 +307,8 @@ describe("platform tool config resolution", function () { it("resolves per-platform, root-base, and default configs", function () { assert.strictEqual(getPlatformToolConfig('cc65', 'nes').wasiFSZip, 'cc65-fs-nes.zip'); assert.strictEqual(getPlatformToolConfig('cc65', 'atari8-800').wasiFSZip, 'cc65-fs-atari8.zip'); - assert.strictEqual(getPlatformToolConfig('sdcc').preloadFS, 'sdcc'); + assert.strictEqual(getPlatformToolConfig('sdcc').wasiFSZip, 'sdcc-fs.zip'); + assert.strictEqual(getPlatformToolConfig('sdcc', 'sms-sms-libcv').preloadFS, 'sdcc'); // root base 'sms' assert.strictEqual(getPlatformToolConfig('dasm', 'nes'), undefined); }); diff --git a/test/unit/testsdcclisting.ts b/test/unit/testsdcclisting.ts new file mode 100644 index 00000000..e44eaad9 --- /dev/null +++ b/test/unit/testsdcclisting.ts @@ -0,0 +1,36 @@ +import assert from "assert"; +import { describe, it } from "mocha"; +import { parseRSTListing } from "../../src/worker/tools/sdcc"; + +// linked listings of `clrscr();` at skeleton.sdcc line 172, then a call from +// a header the file includes +const RST_SDCC3 = [ + " 1274 _main::", + " 1275 ;:172: clrscr();", + " 056C CD 3C 00 [17] 1276 \tcall\t_clrscr", +].join("\n"); + +const RST_SDCC4 = [ + " 0000056C 1274 _main::", + " 1275 ;//skeleton.sdcc:172: clrscr();", + " 0000056C CD 3C 00 [17] 1276 \tcall\t_clrscr", + " 1277 ;//defs.h:9: inline_fn();", + " 0000056F CD 40 00 [17] 1278 \tcall\t_inline_fn", +].join("\n"); + +describe("sdcc listings", function () { + + it("maps SDCC 3.6.5 source comments", function () { + const l = parseRSTListing(RST_SDCC3, "skeleton"); + assert.deepStrictEqual(l.lines.map((x) => [x.line, x.offset]), [[172, 0x56c]]); + assert.strictEqual(l.asmlines[0].offset, 0x56c); + }); + + it("maps SDCC 4.x file source comments with 8-digit addresses", function () { + const l = parseRSTListing(RST_SDCC4, "skeleton"); + // the defs.h line is not attributed to skeleton.sdcc + assert.deepStrictEqual(l.lines.map((x) => [x.line, x.offset]), [[172, 0x56c]]); + assert.deepStrictEqual(l.asmlines.map((x) => x.offset), [0x56c, 0x56f]); + }); + +}); diff --git a/test/unit/testtoolmeta.ts b/test/unit/testtoolmeta.ts index a2263d1c..92474f0e 100644 --- a/test/unit/testtoolmeta.ts +++ b/test/unit/testtoolmeta.ts @@ -51,7 +51,12 @@ describe('Tool metadata registry', function () { assert.strictEqual(getSharedFileSystemName('cc65', 'nes'), 'wasi:cc65-fs-nes.zip'); assert.strictEqual(getSharedFileSystemName('cc65', 'c64'), 'wasi:cc65-fs-c64.zip'); assert.strictEqual(getSharedFileSystemName('cc65', 'nonexistent'), undefined); - assert.strictEqual(getPreloadFSName('sdcc'), 'sdcc'); // default config + // SDCC 4.x (WASI zip) by default; 3.x-library platforms keep the 3.6.5 preloadFS + assert.strictEqual(getPreloadFSName('sdcc'), undefined); + assert.strictEqual(getSharedFileSystemName('sdcc', 'mw8080bw'), 'wasi:sdcc-fs.zip'); + assert.strictEqual(getPreloadFSName('sdcc', 'gb'), 'sdcc'); + assert.strictEqual(getPreloadFSName('sdcc', 'msx-libcv'), 'sdcc'); + assert.strictEqual(getSharedFileSystemName('sdcc', 'msx'), 'wasi:sdcc-fs.zip'); assert.strictEqual(getPreloadFSName('dasm'), undefined); }); diff --git a/test/unit/testwasishim.ts b/test/unit/testwasishim.ts index 6d365445..25983e7e 100644 --- a/test/unit/testwasishim.ts +++ b/test/unit/testwasishim.ts @@ -192,6 +192,17 @@ describe('WASIMemoryFilesystem path handling', function () { assert.strictEqual(fsys.removeDirectory('./sub'), WASIErrors.SUCCESS); assert.strictEqual(fsys.removeDirectory('/'), WASIErrors.BUSY); }); + it('renames files, replacing the target', function () { + const fsys = new WASIMemoryFilesystem(); + fsys.putFile('./a.tmp', 'new'); + fsys.putFile('./a.s', 'old'); + fsys.putDirectory('./sub'); + assert.strictEqual(fsys.renameFile('./a.tmp', './a.s'), WASIErrors.SUCCESS); + assert.strictEqual(fsys.getFile('./a.s').getBytesAsString(), 'new'); + assert.strictEqual(fsys.getFile('./a.tmp'), undefined); + assert.strictEqual(fsys.renameFile('./a.tmp', './b.s'), WASIErrors.NOENT); + assert.strictEqual(fsys.renameFile('./a.s', './sub'), WASIErrors.ISDIR); + }); it('deletes only from the writable layer, not the parent', function () { const parent = new WASIMemoryFilesystem(); parent.putFile('./shared.h', 'shared');