diff --git a/docs/how/wasm/test/.gitignore b/docs/how/wasm/test/.gitignore new file mode 100644 index 000000000..39ce2cd40 --- /dev/null +++ b/docs/how/wasm/test/.gitignore @@ -0,0 +1,2 @@ +out*/ +wasm/ diff --git a/docs/how/wasm/test/README.md b/docs/how/wasm/test/README.md new file mode 100644 index 000000000..7936309e1 --- /dev/null +++ b/docs/how/wasm/test/README.md @@ -0,0 +1,60 @@ +# Running tslang programs and tests as WebAssembly + +`wasm32-unknown-unknown` and `wasm64-unknown-unknown` have no C library. tslang links a wasm program +with `--allow-undefined`, so whatever the C library and the default library's C++ wrappers provide on +other targets (`malloc`, `snprintf`, `puts`, `regexp_*`, `stdio_write`, the time functions, ...) is an +import from the `env` module. [host.mjs](host.mjs) implements them in JavaScript, for Node and for +the browser. + +## The default library + +Built by the default library's `scripts\build_wasm.bat` (Windows) or `scripts/build_wasm.sh` (Linux) +for `-mm=none`, `-mm=rc` and `-mm=own`, release and debug: + + __build\defaultlib\lib\\unknown\unknown\unknown\\\libTypeScriptDefaultLib.a + +It is `lib.ts` plus the OS layer `lib.wasm.ts`, with no C++ wrappers. `-mm=gc` (tslang's default) is +an error for a wasm triple, because the collector has no wasm build. Pass `-mm` explicitly. + +## One program + + tslang --emit=exe -mm=rc --opt -mtriple=wasm32-unknown-unknown --default-lib-path=<...>\__build -o=hello.wasm hello.ts + node run.mjs hello.wasm + +The exit code is the program's own (`main`'s result, or `exit()`'s argument), or 1 for a trap or an +uncaught exception. + +wasm64 needs Node 24 or later. LLVM gives a wasm64 module 64-bit tables, which older V8 cannot +instantiate. + +## The tests + + node run_tests.mjs [--suite=tester,defaultlib] [--triple=wasm32,wasm64] [--mm=none,rc,own] [--debug] [-j] [filter] + +For each triple and model, this compiles the tests and runs each program under Node. The summary +and every failure are written to `out/results.txt`. + +- `tester`: every single-file test the tester's CMakeLists.txt runs, built as test-runner builds them + (`--no-default-lib`). A test passes when it exits with 0 and prints `done.`. +- `defaultlib`: the default library's own tests (`TypeScriptCompilerDefaultLib/tests`), linked with + the wasm default library and run in that repository, with `tests/.stdin` as standard input. + A test passes when it prints `ALL DONE`, writes nothing to stderr and exits with 0, as `tests.ps1` + checks. + +Some tests are skipped, each with its reason: + +- tests that catch an exception (`try`/`catch` does not catch on wasm yet; a throw ends the program) +- async tests (the async runtime has no wasm build) +- tests that use threads +- default-library tests marked `// gc only`, `// linux only`, or (under own) `// not own` +- the tests in [wasm_skips.txt](wasm_skips.txt) + +Under `-mm=own`, a test that doesn't compile for wasm is compiled again natively. If that fails too, +the test is listed as NATIVE and not counted as a failure: it is something `-mm=own` cannot prove +yet. Native ctest runs only a few tests under own, but the whole corpus under none and rc, so under +those two models every wasm compile error counts as a failure. + +## In a browser + +Put the `.wasm` files in `wasm\`, serve this folder over HTTP (e.g. `npx http-server .`) and open +[index.html](index.html). It runs each program with the same host. diff --git a/docs/how/wasm/test/host.mjs b/docs/how/wasm/test/host.mjs new file mode 100644 index 000000000..29ec1b35e --- /dev/null +++ b/docs/how/wasm/test/host.mjs @@ -0,0 +1,906 @@ +// The host of a tslang wasm program (wasm32-unknown-unknown or wasm64-unknown-unknown). +// +// These targets have no C library. tslang links a wasm program with --allow-undefined, so what the C +// library and the default library's C++ wrappers provide on other targets (malloc, snprintf, puts, +// regexp_*, stdio_write, the time functions, ...) are imports from the `env` module, implemented +// here in JavaScript. One module for Node (run.mjs) and the browser (index.html): Node-only parts +// (stdin, fs, process) are used only when `node` is passed in. +// +// Pointers and size_t are i32 on wasm32 and i64 on wasm64, which JavaScript sees as a Number and a +// BigInt. Every import takes them through N() and returns them through P(); C's `long long` (time_t, +// tslang's `long`) is an i64 on both and so always a BigInt. +// +// const host = createHost({ args, write, node }); +// const { instance } = await WebAssembly.instantiate(bytes, host.imports); +// const exitCode = host.run(instance); + +export class ExitSignal extends Error +{ + constructor(code) + { + super(`exit(${code})`); + this.code = code; + } +} + +// a C++ exception nothing caught: on wasm try/catch does not catch yet, so every throw ends here +export class UncaughtThrow extends Error +{ + constructor(objectAddress) + { + super("uncaught exception"); + this.objectAddress = objectAddress; + } +} + +const WEEKDAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; +const MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; + +export function createHost(options = {}) +{ + const args = options.args ?? ["program.wasm"]; + const write = options.write ?? ((fd, text) => (fd === 2 ? console.error : console.log)(text)); + const node = options.node; // { fs, process } under Node + + let instance; + let memory; + let is64 = false; + + const N = (v) => Number(v); + const P = (v) => (is64 ? BigInt(v) : v); + + const u8 = () => new Uint8Array(memory.buffer); + const dv = () => new DataView(memory.buffer); + + const utf8Decoder = new TextDecoder("utf-8"); + const utf8Encoder = new TextEncoder(); + + const cstrEnd = (p) => { const m = u8(); while (m[p] !== 0) p++; return p; }; + const bytesOf = (p, n) => u8().slice(p, p + n); + const readStr = (p) => { p = N(p); return p === 0 ? "" : utf8Decoder.decode(u8().subarray(p, cstrEnd(p))); }; + // one char per byte: the regex wrappers count positions in bytes, as std::cmatch does over a char* + const readLatin1 = (p) => { p = N(p); let s = ""; const m = u8(); for (let e = cstrEnd(p); p < e; p++) s += String.fromCharCode(m[p]); return s; }; + const latin1Bytes = (s) => { const b = new Uint8Array(s.length); for (let i = 0; i < s.length; i++) b[i] = s.charCodeAt(i) & 0xff; return b; }; + // `bytes` and a 0 at `p`, at most `max` bytes with the 0 + const writeBytesZ = (p, bytes, max = Infinity) => + { + p = N(p); + if (max <= 0) return 0; + const n = Math.min(bytes.length, max - 1); + u8().set(bytes.subarray(0, n), p); + u8()[p + n] = 0; + return n; + }; + const writeStrZ = (p, s, max) => writeBytesZ(p, utf8Encoder.encode(s), max); + + // ---- the heap: free lists per 16-byte size class above a bump pointer, the memory grown as needed ---- + + const ALIGN = 16; + let heapTop = 0; + const sizes = new Map(); + const freeLists = new Map(); + + const ensure = (end) => + { + const have = memory.buffer.byteLength; + if (end <= have) return; + const pages = Math.ceil((end - have) / 65536); + memory.grow(is64 ? BigInt(pages) : pages); + }; + + const malloc = (size) => + { + size = Math.max(ALIGN, Math.ceil(N(size) / ALIGN) * ALIGN); + const list = freeLists.get(size); + let p; + if (list && list.length) + { + p = list.pop(); + } + else + { + p = heapTop; + heapTop += size; + ensure(heapTop); + } + + sizes.set(p, size); + return p; + }; + + const free = (p) => + { + p = N(p); + if (p === 0) return; + const size = sizes.get(p); + if (size === undefined) throw new Error(`free of ${p}: not a block the heap gave out`); + sizes.delete(p); + let list = freeLists.get(size); + if (!list) freeLists.set(size, list = []); + list.push(p); + }; + + const realloc = (p, size) => + { + p = N(p); + size = N(size); + if (p === 0) return malloc(size); + const old = sizes.get(p); + if (old === undefined) throw new Error(`realloc of ${p}: not a block the heap gave out`); + if (size <= old) return p; + const q = malloc(size); + u8().copyWithin(q, p, p + old); + free(p); + return q; + }; + + const calloc = (count, size) => + { + const n = N(count) * N(size); + const p = malloc(n); + u8().fill(0, p, p + n); + return p; + }; + + // ---- printf ---- + + // C's %e: at least two exponent digits + const expForm = (v, prec, upper) => + { + let s = v.toExponential(prec).replace(/e([+-])(\d)$/, "e$10$2"); + return upper ? s.toUpperCase() : s; + }; + + const stripZeros = (s) => + { + if (s.indexOf(".") < 0) return s; + const e = s.search(/[eE]/); + let mantissa = e < 0 ? s : s.slice(0, e); + const exp = e < 0 ? "" : s.slice(e); + mantissa = mantissa.replace(/0+$/, "").replace(/\.$/, ""); + return mantissa + exp; + }; + + const fixedForm = (v, prec) => + { + if (prec <= 100 && Math.abs(v) < 1e21) return v.toFixed(prec); + // toFixed gives exponent notation from 1e21 on; C prints every digit + let s = BigInt(Math.trunc(v)).toString(); + if (v < 0 && s[0] !== "-") s = "-" + s; + return prec > 0 ? s + "." + "0".repeat(prec) : s; + }; + + const hexFloat = (v, prec, upper) => + { + if (v === 0) return (Object.is(v, -0) ? "-" : "") + "0x0p+0"; + const sign = v < 0 ? "-" : ""; + v = Math.abs(v); + const buf = new DataView(new ArrayBuffer(8)); + buf.setFloat64(0, v); + const bits = buf.getBigUint64(0); + let exp = Number((bits >> 52n) & 0x7ffn); + let mant = bits & 0xfffffffffffffn; + let lead = 1n; + if (exp === 0) { lead = 0n; exp = -1022; } else exp -= 1023; + let hex = mant.toString(16).padStart(13, "0"); + if (prec === undefined) hex = hex.replace(/0+$/, ""); + else hex = hex.slice(0, prec).padEnd(prec, "0"); + const s = `${sign}0x${lead}${hex ? "." + hex : ""}p${exp >= 0 ? "+" : ""}${exp}`; + return upper ? s.toUpperCase().replace("0X", "0X") : s; + }; + + const formatFloat = (v, conv, prec, alt) => + { + const upper = conv === conv.toUpperCase(); + if (!isFinite(v)) + { + const s = isNaN(v) ? "nan" : (v < 0 ? "-inf" : "inf"); + return upper ? s.toUpperCase() : s; + } + + switch (conv.toLowerCase()) + { + case "f": return fixedForm(v, prec ?? 6); + case "e": return expForm(v, prec ?? 6, upper); + case "a": return hexFloat(v, prec, upper); + case "g": + { + let p = prec ?? 6; + if (p === 0) p = 1; + if (v === 0) return alt ? (0).toFixed(p - 1) : (Object.is(v, -0) ? "-0" : "0"); + const x = Number(v.toExponential(p - 1).split("e")[1]); + let s = (p > x && x >= -4) ? fixedForm(v, p - 1 - x) : expForm(v, p - 1, upper); + return alt ? s : stripZeros(s); + } + } + + return String(v); + }; + + // C's printf over a format and the wasm vararg buffer: each argument at its own alignment + const format = (fmt, va) => + { + va = N(va); + const view = dv(); + const ptrSize = is64 ? 8 : 4; + const take = (size, kind) => + { + va = Math.ceil(va / size) * size; + let v; + if (kind === "f") v = view.getFloat64(va, true); + else if (size === 8) v = kind === "u" ? view.getBigUint64(va, true) : view.getBigInt64(va, true); + else v = kind === "u" ? view.getUint32(va, true) : view.getInt32(va, true); + va += size; + return v; + }; + + let out = ""; + const re = /%([-+ #0]*)(\*|\d+)?(?:\.(\*|\d*))?(hh|h|ll|l|L|z|j|t|q)?([diuoxXcspfFeEgGaAn%])/g; + let last = 0; + let m; + while ((m = re.exec(fmt)) !== null) + { + out += fmt.slice(last, m.index); + last = re.lastIndex; + let [, flags, width, prec, len, conv] = m; + if (conv === "%") { out += "%"; continue; } + if (width === "*") width = take(4, "i"); + if (prec === "*") prec = take(4, "i"); + width = width === undefined ? 0 : Number(width); + if (width < 0) { flags += "-"; width = -width; } + prec = prec === undefined ? undefined : (prec === "" ? 0 : Number(prec)); + if (prec !== undefined && prec < 0) prec = undefined; + + const intSize = (len === "ll" || len === "q" || len === "j") ? 8 : (len === "l" || len === "z" || len === "t") ? ptrSize : 4; + let body; + let sign = ""; + switch (conv) + { + case "d": + case "i": + { + let v = BigInt(take(intSize, "i")); + if (len === "h") v = BigInt.asIntN(16, v); + if (len === "hh") v = BigInt.asIntN(8, v); + if (v < 0n) { sign = "-"; v = -v; } + else if (flags.includes("+")) sign = "+"; + else if (flags.includes(" ")) sign = " "; + body = v.toString(); + if (prec !== undefined) body = (prec === 0 && v === 0n) ? "" : body.padStart(prec, "0"); + break; + } + case "u": + case "o": + case "x": + case "X": + { + let v = BigInt(take(intSize, "u")); + v = BigInt.asUintN(intSize * 8, v); + if (len === "h") v = BigInt.asUintN(16, v); + if (len === "hh") v = BigInt.asUintN(8, v); + body = v.toString(conv === "u" ? 10 : conv === "o" ? 8 : 16); + if (conv === "X") body = body.toUpperCase(); + if (prec !== undefined) body = (prec === 0 && v === 0n) ? "" : body.padStart(prec, "0"); + if (flags.includes("#") && v !== 0n) + { + if (conv === "x") sign = "0x"; + else if (conv === "X") sign = "0X"; + else if (conv === "o" && body[0] !== "0") body = "0" + body; + } + break; + } + case "c": + body = String.fromCharCode(take(4, "i") & 0xff); + break; + case "s": + { + const p = Number(take(ptrSize, "u")); + body = p === 0 ? "(null)" : readStr(p); + if (prec !== undefined) body = body.slice(0, prec); + break; + } + case "p": + body = "0x" + BigInt(take(ptrSize, "u")).toString(16); + break; + case "n": + take(ptrSize, "u"); + body = ""; + break; + default: + { + let v = take(8, "f"); + body = formatFloat(v, conv, prec, flags.includes("#")); + if (body[0] === "-") { sign = "-"; body = body.slice(1); } + else if (flags.includes("+")) sign = "+"; + else if (flags.includes(" ")) sign = " "; + break; + } + } + + const length = sign.length + body.length; + if (length < width) + { + if (flags.includes("-")) body = body + " ".repeat(width - length); + else if (flags.includes("0") && conv !== "s" && conv !== "c" && !(prec !== undefined && "diuoxX".includes(conv)) && /^[0-9a-fA-F.]/.test(body)) + body = "0".repeat(width - length) + body; + else sign = " ".repeat(width - length) + sign; + } + + out += sign + body; + } + + return out + fmt.slice(last); + }; + + // ---- strtod / strtol ---- + + const strtod = (str, endPtr) => + { + const p = N(str); + const s = readLatin1(p); + const m = /^[ \t\n\v\f\r]*([+-]?(?:0[xX](?:[0-9a-fA-F]+\.?[0-9a-fA-F]*|\.[0-9a-fA-F]+)(?:[pP][+-]?\d+)?|(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?|inf(?:inity)?|nan(?:\([0-9A-Za-z_]*\))?))/i.exec(s); + let value = 0; + let used = 0; + if (m) + { + used = m[0].length; + let t = m[1]; + let sign = 1; + if (t[0] === "-" || t[0] === "+") { if (t[0] === "-") sign = -1; t = t.slice(1); } + if (/^inf/i.test(t)) value = Infinity; + else if (/^nan/i.test(t)) value = NaN; + else if (/^0x/i.test(t)) + { + const hm = /^0x([0-9a-f]*)(?:\.([0-9a-f]*))?(?:p([+-]?\d+))?$/i.exec(t); + const intPart = hm[1] || "0"; + const frac = hm[2] || ""; + value = parseInt(intPart, 16) + (frac ? parseInt(frac, 16) / Math.pow(16, frac.length) : 0); + value *= Math.pow(2, Number(hm[3] || 0)); + } + else value = Number(t); + value *= sign; + } + + if (N(endPtr) !== 0) + { + const view = dv(); + const e = p + used; + if (is64) view.setBigUint64(N(endPtr), BigInt(e), true); else view.setUint32(N(endPtr), e, true); + } + + return value; + }; + + const strtol = (str, endPtr, base) => + { + const p = N(str); + const s = readLatin1(p); + let i = 0; + while (/[ \t\n\v\f\r]/.test(s[i] ?? "")) i++; + let sign = 1n; + if (s[i] === "+" || s[i] === "-") { if (s[i] === "-") sign = -1n; i++; } + if ((base === 0 || base === 16) && s[i] === "0" && /[xX]/.test(s[i + 1] ?? "") && /[0-9a-fA-F]/.test(s[i + 2] ?? "")) { i += 2; base = 16; } + else if (base === 0 && s[i] === "0") base = 8; + else if (base === 0) base = 10; + const start = i; + let v = 0n; + for (; i < s.length; i++) + { + const d = parseInt(s[i], 36); + if (isNaN(d) || d >= base) break; + v = v * BigInt(base) + BigInt(d); + } + + const used = i === start ? 0 : i; + if (N(endPtr) !== 0) + { + const view = dv(); + if (is64) view.setBigUint64(N(endPtr), BigInt(p + used), true); else view.setUint32(N(endPtr), p + used, true); + } + + v *= sign; + if (v > 2147483647n) return 2147483647; + if (v < -2147483648n) return -2147483648; + return Number(v); + }; + + // strtoll / strtoull: strtol's parse, clamped to 64 bits; a BigInt + const strtolWide = (str, endPtr, base, signed) => + { + const p = N(str); + const s = readLatin1(p); + const m = /^[ \t\n\v\f\r]*([+-]?)(0[xX](?=[0-9a-fA-F]))?([0-9a-zA-Z]*)/.exec(s); + let radix = base; + let digits = m[3]; + if (m[2] && (radix === 0 || radix === 16)) radix = 16; + else if (radix === 0) radix = digits[0] === "0" ? 8 : 10; + let v = 0n, used = 0; + let i = 0; + for (; i < digits.length; i++) + { + const d = parseInt(digits[i], 36); + if (isNaN(d) || d >= radix) break; + v = v * BigInt(radix) + BigInt(d); + } + + if (i > 0) used = m[0].length - (digits.length - i); + if (m[1] === "-") v = -v; + if (N(endPtr) !== 0) + { + const view = dv(); + if (is64) view.setBigUint64(N(endPtr), BigInt(p + used), true); else view.setUint32(N(endPtr), p + used, true); + } + + if (signed) return v > 0x7fffffffffffffffn ? 0x7fffffffffffffffn : v < -0x8000000000000000n ? -0x8000000000000000n : v; + return BigInt.asUintN(64, v); + }; + + // a string that stays: strerror's result + const staticStrings = new Map(); + const staticString = (s) => + { + if (!staticStrings.has(s)) staticStrings.set(s, mallocBytesZ(utf8Encoder.encode(s))); + return staticStrings.get(s); + }; + + // ---- time ---- + + const readI64 = (p) => dv().getBigInt64(N(p), true); + const TM_FIELDS = 9; + const readTm = (p) => { p = N(p); const v = dv(); const t = []; for (let i = 0; i < TM_FIELDS; i++) t.push(v.getInt32(p + i * 4, true)); return t; }; + const writeTm = (p, t) => { p = N(p); const v = dv(); for (let i = 0; i < TM_FIELDS; i++) v.setInt32(p + i * 4, t[i], true); }; + // [sec, min, hour, mday, mon, year - 1900, wday, yday, isdst] + const tmOf = (date, utc) => + { + const g = (name) => date[(utc ? "getUTC" : "get") + name](); + const year = g("FullYear"); + const startOfYear = utc ? Date.UTC(year, 0, 1) : new Date(year, 0, 1).getTime(); + const startOfDay = utc ? Date.UTC(year, g("Month"), g("Date")) : new Date(year, g("Month"), g("Date")).getTime(); + const yday = Math.round((startOfDay - startOfYear) / 86400000); + return [g("Seconds"), g("Minutes"), g("Hours"), g("Date"), g("Month"), year - 1900, g("Day"), yday, utc ? 0 : (isDst(date) ? 1 : 0)]; + }; + const standardOffset = (year) => Math.max(new Date(year, 0, 1).getTimezoneOffset(), new Date(year, 6, 1).getTimezoneOffset()); + const isDst = (date) => date.getTimezoneOffset() < standardOffset(date.getFullYear()); + const dateOfTm = (t, utc) => + { + const [sec, min, hour, mday, mon, year] = t; + const d = new Date(0); + if (utc) { d.setUTCFullYear(year + 1900, mon, mday); d.setUTCHours(hour, min, sec, 0); } + else { d.setFullYear(year + 1900, mon, mday); d.setHours(hour, min, sec, 0); } + return d; + }; + const pad = (n, w = 2, c = "0") => String(n).padStart(w, c); + const asctime = (t) => `${WEEKDAYS[t[6]].slice(0, 3)} ${MONTHS[t[4]].slice(0, 3)} ${pad(t[3], 2, " ")} ${pad(t[2])}:${pad(t[1])}:${pad(t[0])} ${t[5] + 1900}\n`; + const zoneOf = (t) => + { + // a tm from gmtime has tm_isdst 0 and is read as UTC by glibc's %z/%Z only with tm_gmtoff; + // here every tm is local unless it is the UTC one gmtime_r gave + const d = dateOfTm(t, false); + const offset = -d.getTimezoneOffset(); + const sign = offset < 0 ? "-" : "+"; + const z = `${sign}${pad(Math.floor(Math.abs(offset) / 60))}${pad(Math.abs(offset) % 60)}`; + let name = ""; + try { name = new Intl.DateTimeFormat("en-US", { timeZoneName: "short" }).formatToParts(d).find((x) => x.type === "timeZoneName")?.value ?? ""; } catch { } + return { z, name }; + }; + const strftime = (fmt, t) => + { + const [sec, min, hour, mday, mon, year, wday, yday] = t; + const Y = year + 1900; + return fmt.replace(/%([EO]?)([a-zA-Z%])/g, (all, mod, c) => + { + switch (c) + { + case "a": return WEEKDAYS[wday].slice(0, 3); + case "A": return WEEKDAYS[wday]; + case "b": case "h": return MONTHS[mon].slice(0, 3); + case "B": return MONTHS[mon]; + case "c": return asctime(t).slice(0, -1); + case "C": return pad(Math.floor(Y / 100)); + case "d": return pad(mday); + case "D": return `${pad(mon + 1)}/${pad(mday)}/${pad(Y % 100)}`; + case "e": return pad(mday, 2, " "); + case "F": return `${Y}-${pad(mon + 1)}-${pad(mday)}`; + case "H": return pad(hour); + case "I": return pad(hour % 12 === 0 ? 12 : hour % 12); + case "j": return pad(yday + 1, 3); + case "k": return pad(hour, 2, " "); + case "l": return pad(hour % 12 === 0 ? 12 : hour % 12, 2, " "); + case "m": return pad(mon + 1); + case "M": return pad(min); + case "n": return "\n"; + case "p": return hour < 12 ? "AM" : "PM"; + case "P": return hour < 12 ? "am" : "pm"; + case "r": return `${pad(hour % 12 === 0 ? 12 : hour % 12)}:${pad(min)}:${pad(sec)} ${hour < 12 ? "AM" : "PM"}`; + case "R": return `${pad(hour)}:${pad(min)}`; + case "S": return pad(sec); + case "t": return "\t"; + case "T": return `${pad(hour)}:${pad(min)}:${pad(sec)}`; + case "u": return String(wday === 0 ? 7 : wday); + case "w": return String(wday); + case "x": return `${pad(mon + 1)}/${pad(mday)}/${pad(Y % 100)}`; + case "X": return `${pad(hour)}:${pad(min)}:${pad(sec)}`; + case "y": return pad(Y % 100); + case "Y": return String(Y); + case "z": return zoneOf(t).z; + case "Z": return zoneOf(t).name; + case "%": return "%"; + default: return all; + } + }); + }; + + // ---- regex: std::regex's results, by handle ---- + + const matches = new Map(); + let nextMatch = 1; + const jsFlags = (flags) => "d" + (flags.includes("i") ? "i" : "") + (flags.includes("m") ? "m" : ""); + const regexError = (e) => { write(2, `Some other regex exception happened.\n${e.message}\n`); }; + // std::match_results::format, ECMAScript rules: $&, $n, $nn, $`, $', $$ + const formatMatch = (m, s, replacement) => + replacement.replace(/\$(\$|&|`|'|\d{1,2})/g, (all, x) => + { + if (x === "$") return "$"; + if (x === "&") return m[0]; + if (x === "`") return s.slice(0, m.index); + if (x === "'") return s.slice(m.index + m[0].length); + let n = Number(x); + if (x.length === 2 && n >= m.length) { n = Number(x[0]); return (n < m.length ? (m[n] ?? "") : all.slice(0, 2)) + x[1]; } + return n < m.length ? (m[n] ?? "") : all; + }); + const mallocBytesZ = (bytes) => { const p = malloc(bytes.length + 1); writeBytesZ(p, bytes, bytes.length + 1); return p; }; + + // ---- http: no network from a wasm program; every request fails ---- + + const responses = new Map(); + let nextResponse = 1; + + // ---- process and fs (Node) ---- + + let result = ""; + let resultBytes = new Uint8Array(0); + let entries = []; + const setResult = (bytes) => { resultBytes = typeof bytes === "string" ? utf8Encoder.encode(bytes) : bytes; return resultBytes.length; }; + const fsFail = (e, op, path, dest) => + { + const code = e?.code ?? "EIO"; + const text = (e?.message ?? String(e)).replace(/^[A-Z]+: /, "").replace(/, .*$/, ""); + setResult(`${code}: ${text}, ${op}${path !== undefined ? ` '${path}'` : ""}${dest !== undefined ? ` -> '${dest}'` : ""}`); + return -1; + }; + const needNode = (what) => { if (!node) throw new Error(`${what} needs the Node host`); }; + + let stdinLine = new Uint8Array(0); + const stdinBuffer = { bytes: new Uint8Array(0), pos: 0, eof: false }; + const stdinByte = () => + { + if (stdinBuffer.pos >= stdinBuffer.bytes.length) + { + if (stdinBuffer.eof || !node) return -1; + const chunk = new Uint8Array(65536); + let n = 0; + try { n = node.fs.readSync(0, chunk, 0, chunk.length, null); } catch (e) { if (e.code === "EAGAIN") return stdinByte(); if (e.code !== "EOF") n = 0; } + if (n <= 0) { stdinBuffer.eof = true; return -1; } + stdinBuffer.bytes = chunk.subarray(0, n); + stdinBuffer.pos = 0; + } + + return stdinBuffer.bytes[stdinBuffer.pos++]; + }; + + const env = { + // memory + malloc: (size) => P(malloc(size)), + calloc: (count, size) => P(calloc(count, size)), + realloc: (p, size) => P(realloc(p, size)), + free: (p) => free(p), + boundedCopy: (dest, destSize, src, count) => { const n = Math.min(N(destSize), N(count)); u8().copyWithin(N(dest), N(src), N(src) + n); }, + memset: (p, value, n) => { u8().fill(value & 0xff, N(p), N(p) + N(n)); return p; }, + memcpy: (d, s, n) => { u8().copyWithin(N(d), N(s), N(s) + N(n)); return d; }, + memmove: (d, s, n) => { u8().copyWithin(N(d), N(s), N(s) + N(n)); return d; }, + memcmp: (a, b, n) => { const m = u8(); a = N(a); b = N(b); for (let i = 0; i < N(n); i++) if (m[a + i] !== m[b + i]) return m[a + i] - m[b + i]; return 0; }, + + // strings + strlen: (p) => P(cstrEnd(N(p)) - N(p)), + strcmp: (a, b) => + { + const m = u8(); + a = N(a); b = N(b); + while (m[a] !== 0 && m[a] === m[b]) { a++; b++; } + return m[a] - m[b]; + }, + strncmp: (a, b, n) => + { + const m = u8(); + a = N(a); b = N(b); n = N(n); + for (let i = 0; i < n; i++, a++, b++) { if (m[a] !== m[b] || m[a] === 0) return m[a] - m[b]; } + return 0; + }, + strcoll: (a, b) => { const x = readStr(a), y = readStr(b); return x < y ? -1 : x > y ? 1 : 0; }, + strchr: (p, c) => + { + const m = u8(); + p = N(p); c &= 0xff; + for (; ; p++) { if (m[p] === c) return P(p); if (m[p] === 0) return P(0); } + }, + strstr: (h, n) => + { + const hs = readLatin1(h), ns = readLatin1(n); + const i = hs.indexOf(ns); + return P(i < 0 ? 0 : N(h) + i); + }, + strcpy: (d, s) => { const n = cstrEnd(N(s)) - N(s); u8().copyWithin(N(d), N(s), N(s) + n + 1); return d; }, + strcat: (d, s) => { const e = cstrEnd(N(d)); const n = cstrEnd(N(s)) - N(s); u8().copyWithin(e, N(s), N(s) + n + 1); return d; }, + tolower: (c) => (c >= 65 && c <= 90 ? c + 32 : c), + toupper: (c) => (c >= 97 && c <= 122 ? c - 32 : c), + isspace: (c) => ((c === 32 || (c >= 9 && c <= 13)) ? 1 : 0), + isdigit: (c) => ((c >= 48 && c <= 57) ? 1 : 0), + isalpha: (c) => (((c | 32) >= 97 && (c | 32) <= 122) ? 1 : 0), + atoi: (p) => strtol(p, P(0), 10), + atof: (p) => strtod(p, P(0)), + strtod, + strtol, + strtoll: (str, endPtr, base) => BigInt(strtolWide(str, endPtr, base, true)), + strtoull: (str, endPtr, base) => BigInt.asIntN(64, strtolWide(str, endPtr, base, false)), + strerror: (errnum) => P(staticString(`errno ${errnum}`)), + getenv: (name) => { const v = node?.process.env[readStr(name)]; return P(v === undefined ? 0 : staticString(v)); }, + qsort: (base, count, size, compare) => + { + base = N(base); count = N(count); size = N(size); + const cmp = instance.exports.__indirect_function_table.get(N(compare)); + const items = []; + for (let i = 0; i < count; i++) items.push(u8().slice(base + i * size, base + (i + 1) * size)); + // each pair is compared in two scratch blocks, as C's qsort hands out pointers into the array + const a = malloc(size), b = malloc(size); + items.sort((x, y) => { u8().set(x, a); u8().set(y, b); return cmp(P(a), P(b)); }); + free(a); free(b); + items.forEach((item, i) => u8().set(item, base + i * size)); + }, + snprintf: (out, n, fmt, va) => + { + const s = format(readStr(fmt), va); + const bytes = utf8Encoder.encode(s); + if (N(out) !== 0 && N(n) > 0) writeBytesZ(out, bytes, N(n)); + return bytes.length; + }, + sprintf: (out, fmt, va) => { const bytes = utf8Encoder.encode(format(readStr(fmt), va)); return writeBytesZ(out, bytes); }, + printf: (fmt, va) => { const s = format(readStr(fmt), va); write(1, s); return s.length; }, + setlocale: () => P(0), + + // output + puts: (p) => { write(1, readStr(p) + "\n"); return 1; }, + putchar: (c) => { write(1, String.fromCharCode(c)); return c; }, + fflush: () => 0, + stdio_write: (fd, s, count) => { if (count > 0) write(fd === 2 ? 2 : 1, utf8Decoder.decode(bytesOf(N(s), count))); }, + stdio_write_bytes: (fd, s, count) => { if (count > 0) write(fd === 2 ? 2 : 1, utf8Decoder.decode(bytesOf(N(s), count))); return Math.max(0, count); }, + stdio_read_line: () => + { + const bytes = []; + let c; + let read = false; + while ((c = stdinByte()) >= 0) { read = true; if (c === 10) break; bytes.push(c); } + if (!read) return -1; + if (bytes.length && bytes[bytes.length - 1] === 13) bytes.pop(); + stdinLine = Uint8Array.from(bytes); + return stdinLine.length; + }, + stdio_line_copy_to: (buffer, count) => { if (N(buffer) && count > 0) u8().set(stdinLine.subarray(0, count), N(buffer)); }, + stdio_read: (buffer, count) => + { + let n = 0; + const m = u8(); + for (; n < count; n++) { const c = stdinByte(); if (c < 0) break; m[N(buffer) + n] = c; } + return n; + }, + + // C++ exceptions: nothing catches them on wasm yet + __cxa_allocate_exception: (size) => P(malloc(size)), + __cxa_free_exception: (p) => free(p), + __cxa_throw: (object) => { throw new UncaughtThrow(N(object)); }, + __assert_fail: (msg, file, line, fn) => { throw new Error(`Assertion failed: ${readStr(msg)}, file ${readStr(file)}, line ${line}`); }, + abort: () => { throw new Error("abort()"); }, + exit: (code) => { throw new ExitSignal(code); }, + + // math + acos: Math.acos, acosh: Math.acosh, asin: Math.asin, asinh: Math.asinh, atan: Math.atan, atan2: Math.atan2, atanh: Math.atanh, + cbrt: Math.cbrt, cos: Math.cos, cosh: Math.cosh, exp: Math.exp, expm1: Math.expm1, log: Math.log, log10: Math.log10, + log1p: Math.log1p, log2: Math.log2, pow: Math.pow, sin: Math.sin, sinh: Math.sinh, tan: Math.tan, tanh: Math.tanh, + sqrt: Math.sqrt, floor: Math.floor, ceil: Math.ceil, trunc: Math.trunc, fabs: Math.abs, hypot: Math.hypot, + // C's round: halves away from zero (Math.round takes them up) + round: (x) => (x < 0 ? -Math.round(-x) : Math.round(x)), + fmod: (x, y) => x % y, + fmin: Math.min, fmax: Math.max, + abs: (x) => (x < 0 ? -x | 0 : x), + labs: (x) => (typeof x === "bigint" ? (x < 0n ? -x : x) : (x < 0 ? -x | 0 : x)), + llabs: (x) => (x < 0n ? -x : x), + rand: () => Math.floor(Math.random() * 2147483648), + srand: () => { }, + + // time + time: (p) => { const t = BigInt(Math.floor(Date.now() / 1000)); if (N(p) !== 0) dv().setBigInt64(N(p), t, true); return t; }, + tslang_wasm_now: () => Date.now(), + tslang_wasm_timezone: () => standardOffset(new Date().getFullYear()) * 60, + gmtime_r: (timePtr, tmPtr) => { writeTm(tmPtr, tmOf(new Date(Number(readI64(timePtr)) * 1000), true)); return tmPtr; }, + localtime_r: (timePtr, tmPtr) => { writeTm(tmPtr, tmOf(new Date(Number(readI64(timePtr)) * 1000), false)); return tmPtr; }, + timegm: (tmPtr) => { const d = dateOfTm(readTm(tmPtr), true); return isNaN(d.getTime()) ? -1n : BigInt(Math.floor(d.getTime() / 1000)); }, + mktime: (tmPtr) => { const d = dateOfTm(readTm(tmPtr), false); return isNaN(d.getTime()) ? -1n : BigInt(Math.floor(d.getTime() / 1000)); }, + ctime_r: (timePtr, buffer) => { writeStrZ(buffer, asctime(tmOf(new Date(Number(readI64(timePtr)) * 1000), false)), 26); return buffer; }, + asctime_r: (tmPtr, buffer) => { writeStrZ(buffer, asctime(readTm(tmPtr)), 26); return buffer; }, + strftime: (out, max, fmt, tmPtr) => + { + const bytes = utf8Encoder.encode(strftime(readStr(fmt), readTm(tmPtr))); + if (bytes.length + 1 > N(max)) { if (N(max) > 0) u8()[N(out)] = 0; return P(0); } + return P(writeBytesZ(out, bytes, N(max))); + }, + parse_date: (p) => { const t = Date.parse(readStr(p)); return isNaN(t) ? -1n : BigInt(t); }, + + // regex (regex.cpp) + regexp_test: (expr, flags, s) => + { + try + { + const m = new RegExp(readLatin1(expr), jsFlags(readStr(flags))).exec(readLatin1(s)); + return m ? m.index + m[0].length : -1; + } + catch (e) { regexError(e); return -1; } + }, + regexp_exec: (expr, flags, s, cm) => + { + try + { + const text = readLatin1(s); + const m = new RegExp(readLatin1(expr), jsFlags(readStr(flags))).exec(text); + if (!m) return P(0); + let handle = N(cm); + if (handle === 0) handle = nextMatch++; + matches.set(handle, { m, text }); + return P(handle); + } + catch (e) { regexError(e); return P(0); } + }, + regexp_match_results_size: (cm) => P(matches.get(N(cm)).m.length), + regexp_match_results_prefix_length: (cm) => P(matches.get(N(cm)).m.index), + regexp_match_results_suffix_position: (cm) => { const { m } = matches.get(N(cm)); return P(m.index + m[0].length); }, + regexp_match_results_sub_match_str_length: (cm, i) => P((matches.get(N(cm)).m[N(i)] ?? "").length), + regexp_match_results_sub_match_str_copy_to: (cm, i, buffer, count) => { const b = latin1Bytes(matches.get(N(cm)).m[N(i)] ?? ""); u8().set(b.subarray(0, N(count)), N(buffer)); }, + regexp_match_results_sub_match_position: (cm, i) => { const { m } = matches.get(N(cm)); const ix = m.indices[N(i)]; return P(ix ? ix[0] : m.index + m[0].length); }, + regexp_match_results_sub_match_length: (cm, i) => P((matches.get(N(cm)).m[N(i)] ?? "").length), + regexp_match_results_format: (cm, replacement) => { const { m, text } = matches.get(N(cm)); return P(mallocBytesZ(latin1Bytes(formatMatch(m, text, readLatin1(replacement))))); }, + regexp_replace: (expr, flags, s, replacement) => + { + try + { + const text = readLatin1(s); + const repl = readLatin1(replacement); + const re = new RegExp(readLatin1(expr), "g" + jsFlags(readStr(flags))); + const result = text.replace(re, (...a) => + { + const groupsAt = typeof a[a.length - 1] === "object" ? a.length - 3 : a.length - 2; + const m = a.slice(0, groupsAt); + m.index = a[groupsAt]; + return formatMatch(m, text, repl); + }); + return P(mallocBytesZ(latin1Bytes(result))); + } + catch (e) { regexError(e); return P(0); } + }, + regexp_free: (cm) => { matches.delete(N(cm)); }, + regexp_free_string: (p) => free(p), + + // http (http.cpp): fails with an error code, as a request that could not connect + http_request: () => { const h = nextResponse++; responses.set(h, {}); return P(h); }, + http_response_success: () => 0, + http_response_error_code: () => 12029, + http_response_status: () => 0, + http_response_headers_length: () => P(0), + http_response_headers_copy_to: () => { }, + http_response_body_length: () => P(0), + http_response_body_copy_to: () => { }, + http_response_free: (r) => { responses.delete(N(r)); }, + + // process and fs (os.cpp) + tslang_os_result_length: () => resultBytes.length, + tslang_os_result_copy_to: (buffer, count) => { if (N(buffer) && count > 0) u8().set(resultBytes.subarray(0, count), N(buffer)); }, + tslang_os_entry: (i) => (i < 0 || i >= entries.length ? -1 : setResult(entries[i])), + tslang_process_argc: () => args.length, + tslang_process_argv: (i) => (i < 0 || i >= args.length ? -1 : setResult(args[i])), + tslang_process_getenv: (name) => { const v = node?.process.env[readStr(name)]; return v === undefined ? -1 : setResult(v); }, + tslang_process_setenv: (name, value) => { if (node) node.process.env[readStr(name)] = readStr(value); return 0; }, + tslang_process_unsetenv: (name) => { if (node) delete node.process.env[readStr(name)]; return 0; }, + tslang_process_env_count: () => { entries = node ? Object.entries(node.process.env).map(([k, v]) => `${k}=${v}`) : []; return entries.length; }, + tslang_process_cwd: () => setResult(node ? node.process.cwd() : "/"), + tslang_process_chdir: (path) => { needNode("process.chdir"); try { node.process.chdir(readStr(path)); return 0; } catch (e) { return fsFail(e, "chdir", readStr(path)); } }, + tslang_process_pid: () => (node ? node.process.pid : 1), + tslang_process_platform: () => setResult(node ? node.process.platform : "browser"), + tslang_process_exit: (code) => { throw new ExitSignal(code); }, + tslang_fs_read_file: (path) => + { + needNode("fs"); + try { return setResult(new Uint8Array(node.fs.readFileSync(readStr(path)))); } + catch (e) { return fsFail(e, e.code === "EISDIR" ? "read" : "open", readStr(path)); } + }, + tslang_fs_write_file: (path, bytes, count, append) => + { + needNode("fs"); + try { (append ? node.fs.appendFileSync : node.fs.writeFileSync)(readStr(path), bytesOf(N(bytes), Math.max(0, count))); return 0; } + catch (e) { return fsFail(e, "open", readStr(path)); } + }, + tslang_fs_stat: (path, sizePtr, mtimePtr) => + { + needNode("fs"); + try + { + const st = node.fs.statSync(readStr(path)); + dv().setFloat64(N(sizePtr), st.size, true); + dv().setFloat64(N(mtimePtr), Math.floor(st.mtimeMs), true); + return st.isFile() ? 1 : st.isDirectory() ? 2 : 3; + } + catch (e) { return fsFail(e, "stat", readStr(path)); } + }, + tslang_fs_read_dir: (path) => + { + needNode("fs"); + try { entries = node.fs.readdirSync(readStr(path)).sort(); return entries.length; } + catch (e) { entries = []; return fsFail(e, "scandir", readStr(path)); } + }, + tslang_fs_mkdir: (path, recursive) => { needNode("fs"); try { node.fs.mkdirSync(readStr(path), { recursive: !!recursive }); return 0; } catch (e) { return fsFail(e, "mkdir", readStr(path)); } }, + tslang_fs_remove: (path, recursive, force) => { needNode("fs"); try { node.fs.rmSync(readStr(path), { recursive: !!recursive, force: !!force }); return 0; } catch (e) { return fsFail(e, "rm", readStr(path)); } }, + tslang_fs_rmdir: (path) => { needNode("fs"); try { node.fs.rmdirSync(readStr(path)); return 0; } catch (e) { return fsFail(e, "rmdir", readStr(path)); } }, + tslang_fs_rename: (from, to) => { needNode("fs"); try { node.fs.renameSync(readStr(from), readStr(to)); return 0; } catch (e) { return fsFail(e, "rename", readStr(from), readStr(to)); } }, + tslang_fs_copy_file: (from, to) => { needNode("fs"); try { node.fs.copyFileSync(readStr(from), readStr(to)); return 0; } catch (e) { return fsFail(e, "copyfile", readStr(from), readStr(to)); } }, + }; + + // an import nobody wrote: a clear error when the program calls it, not a failed instantiation + const imports = { + env: new Proxy(env, { + get: (target, name) => + name in target ? target[name] : (...a) => { throw new Error(`the wasm host has no import env.${String(name)}`); }, + has: () => true, + }), + }; + + const run = (inst) => + { + instance = inst; + memory = inst.exports.memory; + is64 = typeof inst.exports.__heap_base?.value === "bigint"; + heapTop = Math.ceil(Number(inst.exports.__heap_base.value) / ALIGN) * ALIGN; + + try + { + inst.exports.__wasm_call_ctors?.(); + let code; + const main = inst.exports.__main_void ?? inst.exports.__original_main; + if (main) + { + code = main(); + } + else if (inst.exports.main.length >= 2) + { + // main(argc, argv): C's char **, its strings in the module's memory + const ptrSize = is64 ? 8 : 4; + const argv = malloc((args.length + 1) * ptrSize); + args.forEach((arg, i) => + { + const p = mallocBytesZ(utf8Encoder.encode(arg)); + if (is64) dv().setBigUint64(argv + i * ptrSize, BigInt(p), true); else dv().setUint32(argv + i * ptrSize, p, true); + }); + if (is64) dv().setBigUint64(argv + args.length * ptrSize, 0n, true); else dv().setUint32(argv + args.length * ptrSize, 0, true); + code = inst.exports.main(args.length, P(argv)); + } + else + { + code = inst.exports.main(); + } + + return typeof code === "number" ? code : (typeof code === "bigint" ? Number(code) : 0); + } + catch (e) + { + if (e instanceof ExitSignal) return e.code; + throw e; + } + }; + + return { imports, run, env }; +} diff --git a/docs/how/wasm/test/index.html b/docs/how/wasm/test/index.html index fb0a53aed..8c4e745f8 100644 --- a/docs/how/wasm/test/index.html +++ b/docs/how/wasm/test/index.html @@ -1,162 +1,50 @@ - + + + tslang wasm tests + - - \ No newline at end of file + diff --git a/docs/how/wasm/test/run.mjs b/docs/how/wasm/test/run.mjs new file mode 100644 index 000000000..0eb0e43db --- /dev/null +++ b/docs/how/wasm/test/run.mjs @@ -0,0 +1,43 @@ +// Runs one tslang wasm program under Node: node run.mjs program.wasm [args...] +// +// The exit code is the program's: what main returned or exit() was given, 1 for a trap or an +// uncaught exception. A wasm64 program needs Node's memory64 support, on by default from Node 24 +// and behind --experimental-wasm-memory64 before it (run_tests.mjs passes it). +import fs from "node:fs"; +import process from "node:process"; +import { createHost, UncaughtThrow } from "./host.mjs"; + +const file = process.argv[2]; +if (!file) +{ + console.error("usage: node run.mjs program.wasm [args...]"); + process.exit(2); +} + +const host = createHost({ + args: process.argv.slice(2), + node: { fs, process }, + write: (fd, text) => fs.writeSync(fd, text), +}); + +let code; +try +{ + const { instance } = await WebAssembly.instantiate(fs.readFileSync(file), host.imports); + code = host.run(instance); +} +catch (e) +{ + if (e instanceof UncaughtThrow) + { + fs.writeSync(2, "Uncaught exception\n"); + } + else + { + fs.writeSync(2, `${e?.stack ?? e}\n`); + } + + code = 1; +} + +process.exitCode = code & 0xff; diff --git a/docs/how/wasm/test/run_tests.mjs b/docs/how/wasm/test/run_tests.mjs new file mode 100644 index 000000000..4e526b525 --- /dev/null +++ b/docs/how/wasm/test/run_tests.mjs @@ -0,0 +1,283 @@ +// Compiles tests for wasm and runs each under Node with the JavaScript host (host.mjs). +// +// node run_tests.mjs [options] [filter] +// +// --suite=tester,defaultlib which tests (default: both) +// tester the tester's tests (tslang/test/tester/tests), as +// test-runner builds them: --no-default-lib. A test passes +// when its program exits with 0 and prints "done.". +// defaultlib the default library's tests (TypeScriptCompilerDefaultLib/tests), +// linked with the wasm default library, as its tests.ps1 runs +// them: a test passes when it prints "ALL DONE", writes +// nothing to stderr and exits with 0. +// --triple=wasm32,wasm64 targets (default: both) +// --mm=none,rc,own memory models (default: all three; gc has no wasm build) +// --debug --di --opt_level=0 programs and the debug default library (default: --opt) +// --tslang= tslang (default: the release build tree, else tslang on PATH) +// --default-lib-repo= the default library's repository (default: ../TypeScriptCompilerDefaultLib +// next to this one); its __build holds the wasm default library +// -j tests at a time (default: the number of CPUs) +// --out= where the .wasm files and results.txt go (default: ./out) +// --timeout= per program run (default: 60) +// filter a regular expression the test's file name has to match +// +// The tester's tests are the single-file ones its CMakeLists.txt runs (`test-compile-*`): the others +// in the folder are multi-file tests, tests that expect a compile error, or not run at all. +// +// Skipped, and listed as such: tests that catch an exception (try/catch does not catch on wasm yet: +// a throw ends the program), async tests (no async runtime for wasm), threads (none on wasm), the +// default library's tests marked "// gc only", "// linux only" or (under own) "// not own", and the +// tests in wasm_skips.txt, each with its reason. +// +// Under -mm=own, a test that does not compile for wasm is compiled once more for the host +// (--emit=obj): if that fails too, the failure is not wasm's - what -mm=own cannot prove yet - and it +// is listed apart (NATIVE), not counted as a failure. ctest runs only a few tests under own natively; +// it runs the whole corpus under none and rc, so there a wasm compile error is always a failure. +import { spawn } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const repo = path.resolve(here, "../../../.."); + +const options = { + suite: ["tester", "defaultlib"], triple: ["wasm32", "wasm64"], mm: ["none", "rc", "own"], debug: false, + jobs: os.cpus().length, out: path.join(here, "out"), timeout: 60, filter: null, +}; +for (const arg of process.argv.slice(2)) +{ + let m; + if ((m = /^--suite=(.+)$/.exec(arg))) options.suite = m[1].split(","); + else if ((m = /^--triple=(.+)$/.exec(arg))) options.triple = m[1].split(","); + else if ((m = /^--mm=(.+)$/.exec(arg))) options.mm = m[1].split(","); + else if (arg === "--debug") options.debug = true; + else if ((m = /^--tslang=(.+)$/.exec(arg))) options.tslang = m[1]; + else if ((m = /^--default-lib-repo=(.+)$/.exec(arg))) options.defaultLibRepo = path.resolve(m[1]); + else if ((m = /^-j(\d+)$/.exec(arg))) options.jobs = Number(m[1]); + else if ((m = /^--out=(.+)$/.exec(arg))) options.out = path.resolve(m[1]); + else if ((m = /^--timeout=(\d+)$/.exec(arg))) options.timeout = Number(m[1]); + else if (!arg.startsWith("-")) options.filter = new RegExp(arg); + else { console.error(`unknown option ${arg}`); process.exit(2); } +} + +if (options.mm.includes("gc")) +{ + console.error("-mm=gc has no wasm build (the garbage collector is not built for wasm): use none, rc or own"); + process.exit(2); +} + +const exe = process.platform === "win32" ? "tslang.exe" : "tslang"; +options.tslang ??= [ + path.join(repo, "__build/tslang/windows-msbuild-2026-release/bin", exe), + path.join(repo, "__build/tslang/ninja/release/bin", exe), +].find((p) => fs.existsSync(p)) ?? exe; +options.defaultLibRepo ??= path.resolve(repo, "../TypeScriptCompilerDefaultLib"); +const defaultLibPath = path.join(options.defaultLibRepo, "__build"); + +// wasm_skips.txt: one test a line, ` [,...] # `. Triples and models +// given limit it to those (wasm32,own: wasm32 under own); none given, every leg. +const skips = new Map(); +const skipsFile = path.join(here, "wasm_skips.txt"); +if (fs.existsSync(skipsFile)) +{ + for (const line of fs.readFileSync(skipsFile, "utf8").split(/\r?\n/)) + { + const m = /^\s*([^#\s]+)(?:\s+([\w,]+))?\s*(?:#\s*(.*))?$/.exec(line); + if (!m) continue; + const only = m[2] ? m[2].split(",") : []; + skips.set(m[1], { + triples: only.filter((x) => x.startsWith("wasm")), + models: only.filter((x) => !x.startsWith("wasm")), + reason: m[3] ?? "listed in wasm_skips.txt", + }); + } +} + +// the single-file tests ctest runs: `tslang_add_test(NAME test-compile-... COMMAND test-runner [options] "")` +const testerFiles = () => +{ + const names = new Set(); + const cmake = fs.readFileSync(path.join(repo, "tslang/test/tester/CMakeLists.txt"), "utf8"); + const re = /tslang_add_test\(NAME test-compile-\S+ COMMAND test-runner((?: -[\w=-]+)*) ((?:"[^"]+"\s*)+)\)/g; + let m; + while ((m = re.exec(cmake)) !== null) + { + const testFiles = m[2].match(/"[^"]+"/g); + if (testFiles.length !== 1 || / -shared\b/.test(m[1])) continue; + names.add(testFiles[0].slice(1, -1).replace(/^.*\//, "")); + } + + // a name built in CMake (`${arc_test}.ts`) is not a file + return [...names].filter((name) => fs.existsSync(path.join(repo, "tslang/test/tester/tests", name))); +}; + +const suites = { + tester: { + dir: path.join(repo, "tslang/test/tester/tests"), + files: testerFiles, + compileArgs: ["--no-default-lib"], + passed: (run) => run.code === 0 && (run.stdout + run.stderr).includes("done.") ? null + : run.code !== 0 ? `exit code ${run.code}` : "no 'done.' msg.", + }, + defaultlib: { + dir: path.join(options.defaultLibRepo, "tests"), + files: () => fs.readdirSync(path.join(options.defaultLibRepo, "tests")).filter((f) => f.endsWith(".ts")), + compileArgs: ["--nowarn"], + cwd: options.defaultLibRepo, + marker: (source, leg) => + { + const first = source.split(/\r?\n/, 1)[0]; + if (first.startsWith("// gc only")) return "marked '// gc only'"; + if (first.startsWith("// linux only")) return "marked '// linux only'"; + if (first.startsWith("// not own") && leg.mm === "own") return "marked '// not own'"; + return null; + }, + passed: (run) => run.code !== 0 ? `exit code ${run.code}` + : !run.stdout.includes("ALL DONE") ? "no 'ALL DONE' marker" + : run.stderr.trim() ? "wrote to stderr" : null, + }, +}; + +const skipReason = (suite, file, source, leg) => +{ + const skip = skips.get(file); + if (skip && (!skip.triples.length || skip.triples.includes(leg.triple)) && (!skip.models.length || skip.models.includes(leg.mm))) + { + return skip.reason; + } + + const marked = suite.marker?.(source, leg); + if (marked) return marked; + if (/\bcatch\s*[({]/.test(source)) return "catches an exception: try/catch does not catch on wasm yet"; + if (/\basync\b|\bawait\b|\bPromise\b/.test(source)) return "async: the async runtime (TypeScriptAsyncRuntime) has no wasm build"; + if (/\bThread\.spawn\b|\bnew Worker\b|\bAtomics\.wait\b/.test(source)) return "threads: none on wasm"; + return null; +}; + +const runProcess = (command, args, { env, timeout, cwd, input } = {}) => new Promise((resolve) => +{ + const child = spawn(command, args, { env: { ...process.env, ...env }, cwd, stdio: [input === undefined ? "ignore" : "pipe", "pipe", "pipe"] }); + let stdout = "", stderr = "", timedOut = false; + child.stdout.on("data", (d) => (stdout += d)); + child.stderr.on("data", (d) => (stderr += d)); + if (input !== undefined) { child.stdin.on("error", () => { }); child.stdin.end(input); } + const timer = timeout ? setTimeout(() => { timedOut = true; child.kill(); }, timeout * 1000) : null; + child.on("error", (e) => { stderr += String(e); }); + child.on("close", (code) => { if (timer) clearTimeout(timer); resolve({ code, stdout, stderr, timedOut }); }); +}); + +// the environment the compiler looks the default library up in, and nothing of the user's own lib paths +const compileEnv = { DEFAULT_LIB_PATH: defaultLibPath, GC_LIB_PATH: "", LLVM_LIB_PATH: "", TSLANG_LIB_PATH: "" }; +const buildArgs = options.debug ? ["--di", "--opt_level=0"] : ["--opt"]; + +const legs = []; +for (const suiteName of options.suite) +{ + if (!suites[suiteName]) { console.error(`unknown suite ${suiteName}: tester or defaultlib`); process.exit(2); } + for (const triple of options.triple) + for (const mm of options.mm) + legs.push({ suiteName, suite: suites[suiteName], triple, mm, name: `${suiteName}-${triple}-${mm}${options.debug ? "-debug" : ""}` }); +} + +const jobs = []; +const results = []; +for (const leg of legs) +{ + const outDir = path.join(options.out, leg.name); + fs.mkdirSync(outDir, { recursive: true }); + const files = leg.suite.files().filter((f) => !options.filter || options.filter.test(f)).sort(); + for (const file of files) + { + const source = fs.readFileSync(path.join(leg.suite.dir, file), "utf8"); + const reason = skipReason(leg.suite, file, source, leg); + if (reason) { results.push({ leg: leg.name, file, status: "SKIP", detail: reason }); continue; } + jobs.push({ leg, file, outDir }); + } +} + +// wasm64: LLVM gives a wasm64 module 64-bit tables (table64), which V8 runs from Node 24 on; Node 22 +// has memory64 behind a flag, but not the tables. Before 24 a wasm64 test is compiled, not run. +const nodeMajor = Number(process.versions.node.split(".")[0]); +const nodeFlags = nodeMajor < 24 ? ["--experimental-wasm-memory64"] : []; +const canRun = (leg) => leg.triple !== "wasm64" || nodeMajor >= 24; +const runner = path.join(here, "run.mjs"); +const lastLines = (text, n) => text.trim().split(/\r?\n/).slice(-n).join("\n"); + +const runOne = async ({ leg, file, outDir }) => +{ + const source = path.join(leg.suite.dir, file); + const wasm = path.join(outDir, file.replace(/\.ts$/, ".wasm")); + fs.rmSync(wasm, { force: true }); + const compiled = await runProcess(options.tslang, ["--emit=exe", `-mm=${leg.mm}`, `-mtriple=${leg.triple}-unknown-unknown`, + ...buildArgs, ...leg.suite.compileArgs, `-o=${wasm}`, source], { env: compileEnv, timeout: 300 }); + if (compiled.code !== 0 || !fs.existsSync(wasm)) + { + let detail = (compiled.stderr + compiled.stdout).trim().split(/\r?\n/).slice(0, 6).join("\n"); + if (leg.mm !== "own") return { leg: leg.name, file, status: "COMPILE", detail }; + + const obj = path.join(outDir, file.replace(/\.ts$/, ".native.o")); + const native = await runProcess(options.tslang, ["--emit=obj", `-mm=${leg.mm}`, ...buildArgs, ...leg.suite.compileArgs, + `-o=${obj}`, source], { env: compileEnv, timeout: 300 }); + fs.rmSync(obj, { force: true }); + if (native.code === 0) return { leg: leg.name, file, status: "COMPILE", detail }; + return { leg: leg.name, file, status: "NATIVE", detail: "native: " + (native.stderr + native.stdout).trim().split(/\r?\n/).slice(0, 4).join("\n") }; + } + + if (!canRun(leg)) + { + return { leg: leg.name, file, status: "BUILT", detail: `compiled; not run: ${leg.triple} needs Node 24 or later (this is ${process.version})` }; + } + + const stdinFile = source.replace(/\.ts$/, ".stdin"); + const input = fs.existsSync(stdinFile) ? fs.readFileSync(stdinFile) : undefined; + const run = await runProcess(process.execPath, ["--no-warnings", ...nodeFlags, runner, wasm], + { timeout: options.timeout, cwd: leg.suite.cwd, input }); + if (run.timedOut) return { leg: leg.name, file, status: "TIMEOUT", detail: lastLines(run.stdout + run.stderr, 6) }; + const failure = leg.suite.passed(run); + if (failure) return { leg: leg.name, file, status: "FAIL", detail: `${failure}\n` + lastLines(run.stdout + run.stderr, 8) }; + return { leg: leg.name, file, status: "PASS" }; +}; + +let next = 0, finished = 0; +const total = jobs.length; +const worker = async () => +{ + while (next < jobs.length) + { + const job = jobs[next++]; + const result = await runOne(job); + results.push(result); + finished++; + if (["FAIL", "COMPILE", "TIMEOUT"].includes(result.status)) console.log(`[${finished}/${total}] ${result.status} ${result.leg} ${result.file}`); + else if (finished % 100 === 0) console.log(`[${finished}/${total}] ...`); + } +}; + +console.log(`tslang: ${options.tslang}\ndefault lib: ${defaultLibPath}\nlegs: ${legs.map((l) => l.name).join(", ")}; ${total} runs, ${results.length} skips`); +await Promise.all(Array.from({ length: Math.max(1, options.jobs) }, worker)); + +results.sort((a, b) => a.leg.localeCompare(b.leg) || a.file.localeCompare(b.file)); +const lines = []; +for (const leg of legs) +{ + const mine = results.filter((r) => r.leg === leg.name); + const count = (s) => mine.filter((r) => r.status === s).length; + lines.push(`== ${leg.name}: ${count("PASS")} passed, ${count("FAIL") + count("COMPILE") + count("TIMEOUT")} failed (${count("COMPILE")} to compile, ${count("TIMEOUT")} timed out), ${count("SKIP")} skipped` + + (count("NATIVE") ? `, ${count("NATIVE")} do not compile natively under own either` : "") + + (count("BUILT") ? `, ${count("BUILT")} compiled but not run` : "")); +} + +const report = [...lines, ""]; +for (const r of results.filter((r) => r.status !== "PASS")) +{ + report.push(`${r.status} ${r.leg} ${r.file}${r.detail ? "\n " + r.detail.replace(/\n/g, "\n ") : ""}`); +} + +fs.mkdirSync(options.out, { recursive: true }); +fs.writeFileSync(path.join(options.out, "results.txt"), report.join("\n") + "\n"); +console.log(lines.join("\n")); +console.log(`details: ${path.join(options.out, "results.txt")}`); +process.exitCode = results.some((r) => ["FAIL", "COMPILE", "TIMEOUT"].includes(r.status)) ? 1 : 0; diff --git a/docs/how/wasm/test/wasm_skips.txt b/docs/how/wasm/test/wasm_skips.txt new file mode 100644 index 000000000..a2468fa71 --- /dev/null +++ b/docs/how/wasm/test/wasm_skips.txt @@ -0,0 +1,26 @@ +# Tests run_tests.mjs does not run for wasm: ` [,...] # `, every leg +# when none is given. Tests that catch an exception and async tests are skipped without being +# listed here. + +# cannot work on wasm +internals.ts # inline_asm with x86 instructions +00any_to_opaque_narrow.ts wasm32 # stores a u64 in an Opaque: a pointer is 4 bytes on wasm32 +00tslang_issue_29_pthread.ts # starts threads through pthread_create: no threads on wasm +00tslang_issue_29_thread.ts # starts threads through _beginthreadex: no threads on wasm +00cppvtable_hidden_return.ts # a @cpp class returning a struct needs the MSVC or a 64-bit Itanium ABI (excluded on i686 too) +fetch.ts # (default library) fetch(): the host has no network, so the request throws + +# compiler gaps on wasm, each to be fixed and taken off this list +00object_func2.ts # a function called through a pointer of another signature: wasm's call_indirect traps +00class_accessor_implements_field.ts wasm32 # interface accessor cell tag (address + 0x80000000) folded into an i32.const: wasm-ld "LEB is outside Varint32 range" +00interface_field_getter_owned.ts wasm32 # interface accessor cell tag (address + 0x80000000) folded into an i32.const: wasm-ld "LEB is outside Varint32 range" +00class_accessor_increment.ts wasm32 # the same accessor cell tag (--di --opt_level=0 programs) +00class_redeclared_field.ts wasm32 # the same accessor cell tag (--di --opt_level=0 programs) +00generator3.ts wasm64 # wasm-ld: invalid data relocation for an object literal's generator method `next` +01iterator.ts wasm64 # wasm-ld: invalid data relocation for an object literal's generator method `next` +00owned_generator_locals.ts wasm64 # wasm-ld: invalid data relocation for a generator's `next` (under none) +path.ts # a field of Readonly read through Maybe<...> is read wrong: the loop never ends + +# fails natively under the same model too (not run natively under it by ctest) +01tuple.ts own # frees a block it does not own (native own: the program does not finish either) +00global_interface_initializer.ts own # asserts "a field read" (native own: the same assert) diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h b/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h index 9cbd4d0a9..bcd5732c9 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRGenStore.h @@ -207,6 +207,14 @@ struct VirtualMethodOrInterfaceVTableInfo StaticFieldInfo staticFieldInfo; bool isStaticField; bool isInterfaceVTable; + // A method's slot: the type of the method that made it, which every call through it is made with + // (a call through the class that declared it). An override of another arity has a slot of its own, + // and this one then holds a thunk of this type calling it (adaptTo): called with the arguments the + // slot's type says, the override would read them where it does not expect them, and wasm's + // call_indirect traps on the signature. + mlir_ts::FunctionType slotType; + // the override's own type when the slot holds a thunk calling it (methodInfo.funcName); null otherwise + mlir_ts::FunctionType adaptTo; }; struct AccessorInfo @@ -981,11 +989,21 @@ struct ClassInfo // find_if searched both kinds through methodInfo.name). llvm::StringMap methodSlots; llvm::StringMap interfaceSlots; - getVirtualTable(vtable, methodSlots, interfaceSlots); + llvm::StringMap> methodSlotsOfArity; + getVirtualTable(vtable, methodSlots, interfaceSlots, methodSlotsOfArity); + } + + // the same parameters and results as far as a call through a slot goes: `this` aside (a class of + // its own in each), as many of each + static bool sameSlotArity(mlir_ts::FunctionType slotType, mlir_ts::FunctionType funcType) + { + return slotType.getNumInputs() == funcType.getNumInputs() && slotType.getNumResults() == funcType.getNumResults() + && slotType.isVarArg() == funcType.isVarArg(); } void getVirtualTable(llvm::SmallVector &vtable, - llvm::StringMap &methodSlots, llvm::StringMap &interfaceSlots) + llvm::StringMap &methodSlots, llvm::StringMap &interfaceSlots, + llvm::StringMap> &methodSlotsOfArity) { // in static class I don't want to have virtual table if (isStatic) @@ -995,7 +1013,7 @@ struct ClassInfo for (auto &base : baseClasses) { - base->getVirtualTable(vtable, methodSlots, interfaceSlots); + base->getVirtualTable(vtable, methodSlots, interfaceSlots, methodSlotsOfArity); } // `.instanceOf` is slot 0 of every class vtable: an object reached with no static type (an @@ -1053,7 +1071,57 @@ struct ClassInfo #endif auto it = methodSlots.find(method.name); - if (it != methodSlots.end()) + if (it != methodSlots.end() && !isCppVTable && method.funcType && methodSlotsOfArity.contains(method.name)) + { + // an override: every slot of the name calls it, each through its own type. One of the + // same arity takes it as it is; one of another (`get()` overridden by `get(scale?)`, + // `foo(x, y, ...z)` by `foo()`) a thunk; and with none of its arity it has a slot of + // its own, which calls made through its class use. + auto &slots = methodSlotsOfArity[method.name]; + auto ownSlot = -1; + for (auto index : slots) + { + auto &record = vtable[index]; + auto slotType = record.slotType ? record.slotType : record.methodInfo.funcType; + record.methodInfo.funcName = method.funcName; + record.methodInfo.isAbstract = method.isAbstract; + if (!slotType || sameSlotArity(slotType, method.funcType)) + { + record.methodInfo.funcType = method.funcType; + record.adaptTo = mlir_ts::FunctionType(); + ownSlot = index; + } + else + { + // the thunk: the override's `this`, the slot's parameters and the override's results + llvm::SmallVector inputs; + inputs.push_back(method.funcType.getNumInputs() > 0 ? method.funcType.getInput(0) : slotType.getInput(0)); + inputs.append(slotType.getInputs().begin() + 1, slotType.getInputs().end()); + record.methodInfo.funcType = mlir_ts::FunctionType::get( + method.funcType.getContext(), inputs, method.funcType.getResults(), slotType.isVarArg()); + record.adaptTo = method.funcType; + } + } + + method.isVirtual = true; + if (ownSlot >= 0) + { + method.virtualIndex = ownSlot; + it->second = ownSlot; + } + else + { + method.virtualIndex = vtable.size(); + it->second = vtable.size(); + slots.push_back(vtable.size()); + VirtualMethodOrInterfaceVTableInfo record{method, false}; + // a field of its own name in the vtable's tuple, beside the slot of the other arity + record.methodInfo.name = method.name + ".args" + std::to_string(method.funcType.getNumInputs()); + record.slotType = method.funcType; + vtable.push_back(record); + } + } + else if (it != methodSlots.end()) { // found method - override the inherited slot (name and type together: // the overriding function's type must win over the inherited one) @@ -1128,7 +1196,14 @@ struct ClassInfo { method.virtualIndex = vtable.size(); methodSlots[method.name] = vtable.size(); - vtable.push_back({method, false}); + if (!isCppVTable && method.funcType) + { + methodSlotsOfArity[method.name].push_back(vtable.size()); + } + + VirtualMethodOrInterfaceVTableInfo record{method, false}; + record.slotType = method.funcType; + vtable.push_back(record); } } diff --git a/tslang/lib/TypeScript/MLIRGenClasses.cpp b/tslang/lib/TypeScript/MLIRGenClasses.cpp index 4ae764ee4..a9a724081 100644 --- a/tslang/lib/TypeScript/MLIRGenClasses.cpp +++ b/tslang/lib/TypeScript/MLIRGenClasses.cpp @@ -3171,6 +3171,109 @@ genContext); mlir::FlatSymbolRefAttr::get(builder.getContext(), thunkName)); } + // A slot of a class's vtable whose type has another arity than the override it calls (see + // VirtualMethodOrInterfaceVTableInfo::slotType): a function of the slot's type that calls the override + // with the arguments it takes - the slot's, as many as the override has, then for a parameter the slot + // does not have `undefined` (an optional one) or an empty array (the rest). Called with the slot's + // arguments, the override itself would read them where it does not expect them; natively it got away + // with that, wasm's call_indirect checks the signature and traps. + mlir::Value MLIRGenImpl::virtualSlotAdapterThunk(mlir::Location location, mlir::StringRef calleeName, + mlir_ts::FunctionType calleeType, mlir_ts::FunctionType thunkType, + const GenContext &genContext) + { + auto thunkName = (calleeName + ".slot" + std::to_string(thunkType.getNumInputs())).str(); + if (!theModule.lookupSymbol(thunkName)) + { + mlir::OpBuilder::InsertionGuard guard(builder); + + // compiler glue, as virtualDispatchThunk's: with --di a subprogram of its own at the file's + // place, never the scope of the function `location` is in + mlir::Location noLocation = mlir::UnknownLoc::get(builder.getContext()); + auto funcOp = mlir_ts::FuncOp::create(noLocation, thunkName, thunkType); + funcOp.setPrivate(); + funcOp->setAttr("internal_linkage", builder.getUnitAttr()); + + DITableScopeT debugFuncScope(debugScope); + auto fileLoc = location->findInstanceOf(); + if (!fileLoc) + { + fileLoc = mlir::FileLineColLoc::get(builder.getStringAttr(mainSourceFileName), 1, 1); + } + + if (compileOptions.generateDebugInfo && fileLoc) + { + MLIRDebugInfoHelper mdi(builder, debugScope); + mlir::Location plainLocation = fileLoc; + funcOp->setLoc(mdi.getSubprogram(plainLocation, thunkName, thunkName, plainLocation)); + noLocation = locFuseWithScope(plainLocation); + } + + auto &entryBlock = *funcOp.addEntryBlock(); + builder.setInsertionPointToStart(&entryBlock); + + auto results = calleeType.getResults(); + auto hasResult = !results.empty() && !isa(results.front()); + auto entryOp = builder.create( + noLocation, hasResult ? mlir::Type(mlir_ts::RefType::get(results.front())) : mlir::Type()); + + SmallVector operands; + for (auto [index, paramType] : llvm::enumerate(calleeType.getInputs())) + { + if (index < entryBlock.getNumArguments()) + { + mlir::Value argument = entryBlock.getArgument(index); + if (argument.getType() != paramType) + { + auto castResult = cast(noLocation, paramType, argument, genContext); + if (castResult.failed_or_no_value()) + { + funcOp.erase(); + return mlir::Value(); + } + + argument = V(castResult); + } + + operands.push_back(argument); + } + else if (isa(paramType)) + { + operands.push_back(builder.create(noLocation, paramType)); + } + else if (calleeType.isVarArg() && index + 1 == calleeType.getNumInputs() && isa(paramType)) + { + operands.push_back(builder.create(noLocation, paramType, mlir::ValueRange{})); + } + else + { + operands.push_back(builder.create(noLocation, paramType)); + } + } + + auto callee = builder.create(noLocation, calleeType, + mlir::FlatSymbolRefAttr::get(builder.getContext(), calleeName)); + auto callOp = builder.create(noLocation, callee, operands); + if (hasResult) + { + // handed on as `return this.m(x)` hands it: owned, and taken over by the return + if (mth.ownsHeapMemory(noLocation, results.front())) + { + callOp->setAttr(OWNED_RESULT_ATTR_NAME, builder.getUnitAttr()); + callOp->setAttr(OWNED_RESULT_CONSUMED_ATTR_NAME, builder.getUnitAttr()); + } + + builder.create(noLocation, callOp.getResult(0), entryOp.getReference()); + } + + builder.create(noLocation, hasResult ? entryOp.getReference() : mlir::Value()); + + theModule.push_back(funcOp); + } + + return builder.create(location, thunkType, + mlir::FlatSymbolRefAttr::get(builder.getContext(), thunkName)); + } + std::pair MLIRGenImpl::findErasedGenericMethod(mlir::Location location, ClassInfo::TypePtr classInfo, mlir::StringRef name, @@ -4299,6 +4402,16 @@ genContext); methodOrFieldNameRef = V(castResult); } + else if (!vtRecord.isStaticField && vtRecord.adaptTo) + { + // an override of another arity than the slot: a thunk of the slot's type + methodOrFieldNameRef = virtualSlotAdapterThunk( + location, symbolName, vtRecord.adaptTo, vtRecord.methodInfo.funcType, genContext); + if (!methodOrFieldNameRef) + { + return TypeValueInitType{mlir::Type(), mlir::Value(), TypeProvided::No}; + } + } else { methodOrFieldNameRef = builder.create( diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index c6ea806de..188cae34a 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -14022,6 +14022,10 @@ class MLIRGenImpl mlir::Value virtualDispatchThunk(mlir::Location location, ClassInfo::TypePtr newClassPtr, MethodInfo &methodInfo, mlir::StringRef thunkName, const GenContext &genContext); + mlir::Value virtualSlotAdapterThunk(mlir::Location location, mlir::StringRef calleeName, + mlir_ts::FunctionType calleeType, mlir_ts::FunctionType thunkType, + const GenContext &genContext); + ValueOrLogicalResult mlirGenCreateInterfaceVTableForObject(mlir::Location location, mlir::Value in, mlir_ts::ObjectType objectType, InterfaceInfo::TypePtr newInterfacePtr, const GenContext &genContext, mlir::Value vtableStorage = mlir::Value()); diff --git a/tslang/lib/TypeScript/ObjDumper.cpp b/tslang/lib/TypeScript/ObjDumper.cpp index 00adffe88..689d27d5b 100644 --- a/tslang/lib/TypeScript/ObjDumper.cpp +++ b/tslang/lib/TypeScript/ObjDumper.cpp @@ -4,6 +4,7 @@ #include "llvm/Object/Binary.h" #include "llvm/Object/COFFImportFile.h" #include "llvm/Object/ObjectFile.h" +#include "llvm/Object/Wasm.h" #include "llvm/Support/Endian.h" #include "llvm/Support/Errc.h" #include "llvm/Support/Error.h" @@ -121,6 +122,36 @@ static void readCStringSymbolsOf(const ObjectFile &objFile, Symbols symbols, Str continue; } + // A wasm data symbol is an offset into its data segment. Its section is the whole DATA + // section, whose contents begin with the segments' headers, so the offset below would read + // the wrong bytes: an own program linked with a wasm default library got none of its + // borrow facts. + if (auto *wasmObj = dyn_cast(&objFile)) + { + const auto &wasmSymbol = wasmObj->getWasmSymbol(symbol); + if (!wasmSymbol.isTypeData() || wasmSymbol.isUndefined()) + { + continue; + } + + auto segmentIndex = wasmSymbol.Info.DataRef.Segment; + if (segmentIndex >= wasmObj->dataSegments().size()) + { + continue; + } + + auto segment = wasmObj->dataSegments()[segmentIndex].Data.Content; + auto offset = wasmSymbol.Info.DataRef.Offset; + if (offset >= segment.size()) + { + continue; + } + + StringRef text(reinterpret_cast(segment.data()) + offset, segment.size() - offset); + result.push_back({name->str(), text.substr(0, text.find(char(0))).str()}); + continue; + } + auto section = symbol.getSection(); if (!section) { diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 7fb8072be..c5c6ab9a4 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -297,6 +297,7 @@ tslang_add_test(NAME test-compile-00-funcs-capture COMMAND test-runner "${PROJEC tslang_add_test(NAME test-compile-00-funcs-vararg COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00funcs_vararg.ts") tslang_add_test(NAME test-compile-00-declare-function-twice COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00declare_function_twice.ts") tslang_add_test(NAME test-compile-00-function-overloads COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00function_overloads.ts") +tslang_add_test(NAME test-compile-00-class-override-arity COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_override_arity.ts") tslang_add_test(NAME test-compile-00-gc-allocations-distinct COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00gc_allocations_distinct.ts") tslang_add_test(NAME test-compile-00-assert-message COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00assert_message.ts") tslang_add_test(NAME test-compile-01-funcs-vararg COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/01funcs_vararg.ts") @@ -976,6 +977,7 @@ tslang_add_test(NAME test-jit-00-funcs-capture COMMAND test-runner -jit "${PROJE tslang_add_test(NAME test-jit-00-funcs-vararg COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00funcs_vararg.ts") tslang_add_test(NAME test-jit-00-declare-function-twice COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00declare_function_twice.ts") tslang_add_test(NAME test-jit-00-function-overloads COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00function_overloads.ts") +tslang_add_test(NAME test-jit-00-class-override-arity COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_override_arity.ts") tslang_add_test(NAME test-jit-00-gc-allocations-distinct COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00gc_allocations_distinct.ts") tslang_add_test(NAME test-jit-00-assert-message COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00assert_message.ts") tslang_add_test(NAME test-jit-01-funcs-vararg COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/01funcs_vararg.ts") @@ -2593,6 +2595,7 @@ set(TSLANG_CORPUS 00nested_implicit_generics.ts 00funcs_vararg.ts 00function_overloads.ts + 00class_override_arity.ts 00gc_allocations_distinct.ts 00assert_message.ts 00funcs.ts diff --git a/tslang/test/tester/tests/00class_override_arity.ts b/tslang/test/tester/tests/00class_override_arity.ts new file mode 100644 index 000000000..502c891f3 --- /dev/null +++ b/tslang/test/tester/tests/00class_override_arity.ts @@ -0,0 +1,85 @@ +// overrides of another arity than the method they override, called through the base class and +// through their own class, inside and outside the class bodies + +abstract class Base { + abstract get(): number; + + twice() { + return this.get() * 2; + } +} + +class Impl extends Base { + get(): number; + get(scale: number): number; + get(scale?: number) { + return scale === undefined ? 3 : 3 * scale; + } + + scaled() { + return this.get(5); + } +} + +class Deeper extends Impl { + get(scale?: number) { + return scale === undefined ? 4 : 4 * scale; + } +} + +class C { + foo(x: number, y: number, ...z: string[]): number { + return x + y + z.length; + } + + callFoo() { + return this.foo(1, 2, "a", "b"); + } +} + +class D extends C { + foo(): number { + return 100; + } + + callOwn() { + return this.foo(); + } +} + +class E extends D { + foo(x?: number): number { + return x === undefined ? 200 : 200 + x; + } +} + +function main() { + const b: Base = new Impl(); + assert(b.twice() == 6, "base slot calls get(scale?) with undefined"); + + const i = new Impl(); + assert(i.get() == 3); + assert(i.get(2) == 6, "own slot keeps the argument"); + assert(i.scaled() == 15, "call in the class body"); + + const deep: Base = new Deeper(); + assert(deep.twice() == 8, "base slot calls the latest override"); + const deepImpl: Impl = new Deeper(); + assert(deepImpl.get(2) == 8, "own slot of Impl calls Deeper's"); + assert(deepImpl.scaled() == 20); + + const c: C = new D(); + assert(c.callFoo() == 100, "extra arguments dropped"); + assert(c.foo(1, 2) == 100); + const d = new D(); + assert(d.foo() == 100); + assert(d.callOwn() == 100); + + const e: C = new E(); + assert(e.callFoo() == 201, "base slot of E calls foo(x?) with x"); + const ed: D = new E(); + assert(ed.foo() == 200, "D's slot calls E's with undefined"); + assert(new E().foo(5) == 205); + + print("done."); +} diff --git a/tslang/tslang/tslang.cpp b/tslang/tslang/tslang.cpp index d8543bd33..434afe9a6 100644 --- a/tslang/tslang/tslang.cpp +++ b/tslang/tslang/tslang.cpp @@ -529,6 +529,18 @@ int main(int argc, char **argv) auto compileOptions = prepareOptions(); + // The collector has no wasm build, so a wasm program under gc would import GC_malloc and the + // rest from a host that has none - and the link would first fail on a default library that is + // never built for it, an error that names a folder rather than the cause. + if (compileOptions.isWasm && compileOptions.needsGCRuntime() + && (emitAction == Action::BuildExe || emitAction == Action::BuildDll)) + { + llvm::WithColor::error(llvm::errs(), "tslang") + << "-mm=gc (the default) is not available for " << compileOptions.moduleTargetTriple + << ": the garbage collector has no wasm build. Use -mm=rc, -mm=own or -mm=none.\n"; + return 1; + } + if (!prepareDefaultLib(compileOptions)) { return 0;