sdcc: support hybrid 3.x and 4.x compilers, 3.x is the default

This commit is contained in:
Steven Hugg committed 2026-10-03 14:55:01 +02:00
1 parent 01a47873ae
commit 1327bc5bef
19 files changed
+594 -156

No files matched your search

+1
View File
@@ -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.
+3 -3
View File
@@ -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 {
+33 -8
View File
@@ -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,
},
+24
View File
@@ -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; },
+68 -7
View File
@@ -129,16 +129,18 @@ Sets a named build parameter that the build system understands:
//#tooldef <phase> 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:
+6 -1
View File
@@ -466,7 +466,8 @@ export function applyAsmProjectParams(params) {
*
* //#symbol [<phase>] NAME[=VALUE] phase in c|as|ld (default c)
* //#flag <phase> <args...> raw argv for one phase
* //#tooldef <phase> NAME=VALUE typed knob (linker: cfgfile, libargs)
* //#tooldef <phase> 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}'`);
}
Binary file not shown.
+2 -39
View File
@@ -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-<platform>.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
+38 -2
View File
@@ -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<string>();
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);
}
+226 -85
View File
@@ -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/<port>}.
// 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<BuildStepResult> {
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<BuildStepResult> {
@@ -206,9 +276,9 @@ export function assembleSDASGB(step: BuildStep): Promise<BuildStepResult> {
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+ ;<stdin>:(\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 `;<stdin>:N:` comments, SDCC 4.x writes
* `;file.c:N: <source>` (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+ ;(?:<stdin>|/*${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<BuildStepResult> {
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);
+62
View File
@@ -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<typeof wasiFSAdapter>) => 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<ArrayBuffer>);
}
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));
}
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+67 -9
View File
@@ -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 <string.h>\nvoid main() {\nstrlen(0);\n}\n', 'mw8080bw', done, 8192, 2, 0);
compile('sdcc', '#include <string.h>\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 <stdio.h>\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'));
+11 -1
View File
@@ -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);
});
+36
View File
@@ -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 ;<stdin>: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 <stdin> 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]);
});
});
+6 -1
View File
@@ -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);
});
+11
View File
@@ -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');