@sproutboat/toolchain 0.11.0 β 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/PORFFOR_ALPHA19.md +72 -0
- package/package.json +2 -2
- package/src/acquire.test.ts +21 -15
- package/src/acquire.ts +1 -1
- package/src/bigint-gc-native.test.ts +30 -0
- package/src/fingerprint.ts +1 -1
- package/src/idle-gc.test.ts +1 -1
- package/src/patch.test.ts +13 -101
- package/src/patch.ts +64 -329
- package/src/pin.ts +3 -3
- package/src/unicode-native.test.ts +5 -1
- package/src/unicode.ts +3 -14
- package/test/gc-bigint.js.txt +44 -0
- package/vendor/porffor-0c135ce.tar.gz +0 -0
- package/vendor/porffor-deda494.tar.gz +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @sproutboat/toolchain
|
|
2
2
|
|
|
3
|
+
## 0.12.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 1ff15e6: Keep heap-backed BigInts alive during conservative garbage collection. The native scanner now recognizes the encoded floating-point pointer used by BigInt arithmetic, preventing crashes and incorrect checksums in workloads such as CRC64 and LTX encoding.
|
|
8
|
+
|
|
9
|
+
## 0.12.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- feba29b: Upgrade the verified vendored Porffor pin to Alpha 19 (`0c135ce`). Alpha 19 brings `Map.groupBy`, private names that stay out of `Object.keys`, `for-in` and `JSON.stringify`, `JSON.stringify` throwing on cycles, a spec `JSON.parse` reviver (with source text), spec ISO date parsing, spec base64 and hex on `Uint8Array`, typed-array `toString` tags, the `Error.prototype.stack` accessor, spec `instanceof`, `+` and template literals converting objects with ToPrimitive, sloppy functions boxing a primitive `this`, unresolvable names read from the global object, `with`, and many String, Date, Function and Object spec fixes. These move compiled handlers closer to V8 (Workers), so they are not gated on a compatibility date, as with earlier Porffor upgrades.
|
|
14
|
+
|
|
15
|
+
Seven downstream workarounds are dropped because upstream now does the same thing, each proven by its native test without the patch: the ISO date offset parser, the base64 option tags, `Map.groupBy` and non-enumerable private fields (sproutboat#286), the `JSON.stringify` cycle check, the `JSON.parse` reviver, and the typed-array `toString` tag. The Unicode JSON patch is re-anchored on the new `JSON.parse`: upstream now rejects trailing input itself, and the reviver's source-text records keep UTF-16 text. The #256 class self-name rewrite is kept (Alpha 19 still has the bug) and now places its synthesized read after the class, so Alpha 19's new TDZ check does not reject it. #287 (GC page discard), B16 (`repeat`), #237 (`replaceAll`), #168 (promise resolving to `null`) and the local function types patch are still needed.
|
|
16
|
+
|
|
17
|
+
One patch is new: Alpha 19 added a `new Function` source specialization that walked the whole program twice per top-level function, so compiling the runtime prelude took 3.7 s instead of 0.6 s before any C was generated. The patch checks the function body first; the generated code is byte-identical.
|
|
18
|
+
|
|
3
19
|
## 0.11.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Porffor Alpha 19 evaluation
|
|
2
|
+
|
|
3
|
+
Verified on 2026-10-21. Released 2026-10-21:
|
|
4
|
+
https://github.com/CanadaHonk/porffor/releases/tag/alpha-19
|
|
5
|
+
|
|
6
|
+
Pinned commit: `0c135ce2f36fdd3467d5a58daa92283810705a51`.
|
|
7
|
+
Vendored archive SHA256:
|
|
8
|
+
`b54da2297ad9e03c5bbac0e0d156f1179ac3a09a2a6fd702475cef605bab97b9`.
|
|
9
|
+
`compiler/uwebsockets.js` is unchanged, so uWebSockets stays on `360c276d` and the
|
|
10
|
+
vendored uWebSockets archive is unchanged.
|
|
11
|
+
|
|
12
|
+
## Changes since Alpha 18
|
|
13
|
+
|
|
14
|
+
91 commits. The ones that matter to handlers:
|
|
15
|
+
|
|
16
|
+
- `Map.groupBy`; private names are brands, so `#x` stays out of `Object.keys`,
|
|
17
|
+
`for-in`, `Object.entries` and `JSON.stringify` (baronunread/sproutboat#286).
|
|
18
|
+
- JSON: `stringify` throws on cycles and unwraps boxed primitives; `parse` applies a
|
|
19
|
+
reviver with source text (`context.source`) and rejects trailing input;
|
|
20
|
+
`JSON.rawJSON`.
|
|
21
|
+
- Spec ISO date string parsing, `Date.prototype[Symbol.toPrimitive]`, Date setters.
|
|
22
|
+
- Spec base64 and hex on `Uint8Array`; `%TypedArray%.prototype[Symbol.toStringTag]`.
|
|
23
|
+
- `Error.prototype.stack` accessor (`Name: message`, settable). The runtime prelude's
|
|
24
|
+
own getter only installs when `stack` is missing, so it now stands down.
|
|
25
|
+
- Language: spec `instanceof` and `Symbol.hasInstance`, `+` and template literals use
|
|
26
|
+
ToPrimitive / ToString, sloppy functions box a primitive `this`, unresolvable names
|
|
27
|
+
are global object properties, lexical bindings are in the TDZ before their
|
|
28
|
+
declaration, `with`, `new Function` from literal sources, eval completion values.
|
|
29
|
+
- Many String, Function, Object, Number and Promise spec fixes; string index reads
|
|
30
|
+
are bounds checked.
|
|
31
|
+
|
|
32
|
+
## Downstream patches
|
|
33
|
+
|
|
34
|
+
Each workaround was removed from the chain and its native test run without it.
|
|
35
|
+
|
|
36
|
+
Dropped, now upstream:
|
|
37
|
+
|
|
38
|
+
- ISO date timezone offsets (`patchDateParser`): its anchor is gone; `date-native`
|
|
39
|
+
passes on upstream's parser.
|
|
40
|
+
- Base64 option tags and complete chunks (`patchUint8ArrayBase64`): rewritten upstream;
|
|
41
|
+
`test:native-base64` passes.
|
|
42
|
+
- `Map.groupBy` and non-enumerable private fields (#286): `groupby-private-native`
|
|
43
|
+
passes.
|
|
44
|
+
- `JSON.stringify` cycle check and the typed-array `toString` tag: `json-cycles-native`
|
|
45
|
+
passes. The tag patch still applied, but upstream's own tag gives the same result.
|
|
46
|
+
- `JSON.parse` reviver: `test:native-json` passes on upstream's reviver.
|
|
47
|
+
|
|
48
|
+
Re-anchored:
|
|
49
|
+
|
|
50
|
+
- Unicode JSON (`unicode.ts`): upstream's `JSON.parse` takes `(_: any, reviver)` and
|
|
51
|
+
rejects trailing input itself, so those two edits go. The bytestring slice is widened
|
|
52
|
+
everywhere `text` is sliced, which now includes the reviver's source-text records;
|
|
53
|
+
without it `context.source` is garbled for non-ASCII input (`unicode-native` covers it).
|
|
54
|
+
- #256 class self name: Alpha 19 still has the bug (unpatched, the test case throws a
|
|
55
|
+
TypeError), but its new TDZ check rejected the rewrite's synthesized `return l`,
|
|
56
|
+
which carried the class's start offset. The returned name is now positioned after
|
|
57
|
+
the class, as it would be in source.
|
|
58
|
+
|
|
59
|
+
New:
|
|
60
|
+
|
|
61
|
+
- `patchFunctionSourcesScan`: Alpha 19's `specializeFunctionSources` (b2b9bf6) walks the
|
|
62
|
+
whole program twice for every top-level function declaration before checking whether
|
|
63
|
+
its body calls `Function`. On the runtime prelude IR generation took 3.7 s instead of
|
|
64
|
+
0.6 s (sproutd admission went from 1.1 to 4.2 s per app). The patch checks the body
|
|
65
|
+
first; the generated C is byte-identical (compared on a sproutd admission compile).
|
|
66
|
+
|
|
67
|
+
Retained, still needed on Alpha 19:
|
|
68
|
+
|
|
69
|
+
- Local function types: `alpha18-native` throws a TypeError without it.
|
|
70
|
+
- #287 GC page discard, B16 `repeat`, #237 `replaceAll`: upstream's code is unchanged.
|
|
71
|
+
- #168 promise resolving to `null`: the `then` probe still lacks the null guard.
|
|
72
|
+
- The native-fetch server patches are Sproutboat features, not Porffor bugs.
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sproutboat/toolchain",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"type": "module",
|
|
6
5
|
"repository": {
|
|
7
6
|
"type": "git",
|
|
8
7
|
"url": "git+https://github.com/baronunread/sproutboat-packages.git",
|
|
9
8
|
"directory": "packages/toolchain"
|
|
10
9
|
},
|
|
10
|
+
"type": "module",
|
|
11
11
|
"main": "./src/index.ts",
|
|
12
12
|
"types": "./src/index.ts",
|
|
13
13
|
"exports": {
|
package/src/acquire.test.ts
CHANGED
|
@@ -18,6 +18,11 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
|
|
|
18
18
|
const files = {
|
|
19
19
|
"runtime/index.js": "fixture:runtime/index.js\n",
|
|
20
20
|
"compiler/render.js": "// sb_native_utf8_scalar_v1\n" +
|
|
21
|
+
"static void porf_gc_cons_candidate(u32 c);\n" +
|
|
22
|
+
// Both conservative scanners consume raw f64 values.
|
|
23
|
+
(" if (d > 0.0 && d < 4294967296.0) {\n" +
|
|
24
|
+
" const i64 iv = (i64)d;\n" +
|
|
25
|
+
" if ((f64)iv == d) porf_gc_cons_candidate((u32)(u64)iv);\n }\n").repeat(2) +
|
|
21
26
|
"static i32 porf_gc_mark_queue_cap = 0;\n porf_gc_discard_free_runs();\n" +
|
|
22
27
|
" porf_native_fetch_timer_start(id, (i32)delay_ms, timer->repeat ? (i32)delay_ms : 0);\n" +
|
|
23
28
|
"void porf_native_fetch_collect_normal_from(void* stack_top) {\n" +
|
|
@@ -63,7 +68,7 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
|
|
|
63
68
|
"const path = require('node:path');\n" +
|
|
64
69
|
"fs.writeFileSync(path.join(__dirname, 'builtins_precompiled.js'), fs.readFileSync(path.join(__dirname, 'builtins/typedarray.js')));\n",
|
|
65
70
|
"compiler/builtins_precompiled.js": "fixture:precompiled\n",
|
|
66
|
-
"compiler/builtins/date.ts": "export const __ecma262_ParseDTSF = (string: string) => {\n
|
|
71
|
+
"compiler/builtins/date.ts": "export const __ecma262_ParseDTSF = (string: string) => {\n};\n",
|
|
67
72
|
"compiler/builtins/typedarray.js":
|
|
68
73
|
"${typedArrayFuncs.reduce((acc, x) => acc + x.replace('// @porf-typed-array\\n', '').replaceAll('Array', name).replaceAll('any[]', name) + '\\n\\n', '')}`;\n" +
|
|
69
74
|
" offset = Math.trunc(offset);\n if (Porffor.fastOr(offset < 0, offset > len)) throw new RangeError('Offset out of bounds');\n" +
|
|
@@ -150,10 +155,7 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
|
|
|
150
155
|
"}\n" +
|
|
151
156
|
"int main(void) {\n porf_native_fetch_runtime_init();\n porf_native_fetch_static_init();\n\n return 0;\n}\n",
|
|
152
157
|
"compiler/builtins/json.ts":
|
|
153
|
-
"let __Porffor_json_bufLimit: i32 = 0;\n depth += 1;\n depth -= 1;\n depth += 1;\n depth -= 1;\n let out: i32 = __Porffor_json_serialize(buffer, value, 0, space);\n" +
|
|
154
158
|
"// sb_json_utf16_v1\n" +
|
|
155
|
-
// patchJsonReviver's anchor.
|
|
156
|
-
"export const __JSON_parse = (_: bytestring) => {\n};\n" +
|
|
157
159
|
"export const __Porffor_json_serialize = (_buffer: i32, value: any): i32 => {\n" +
|
|
158
160
|
" for (const x of (value as any[])) {\n }\n" +
|
|
159
161
|
" const val: any = value[key];\n" +
|
|
@@ -168,14 +170,10 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
|
|
|
168
170
|
"}\nfunction setTimeout() {}\nclass TextDecoder {\n}\n" +
|
|
169
171
|
"class URL {\n constructor(input) {\n this.href = '';\n }\n}\n\nclass Request {\n}\n" +
|
|
170
172
|
"class Response {\n constructor(body = null, init = undefined) {\n this.body = body;\n this.status = Number(init?.status ?? 200);\n this.headers = new Headers(init?.headers);\n }\n}\n",
|
|
171
|
-
"compiler/builtins/uint8array_base64.ts": "let alphabet: string = 'base64';\nlet lastChunkHandling: string = 'loose';\nconst fullChunks: i32 = (len / 3) * 3;\n",
|
|
172
|
-
"compiler/builtins/object.ts": " return '[object ' + builtinTag + ']';\n};\n",
|
|
173
173
|
"compiler/builtins/console.ts": "export const __Porffor_consolePrint = (arg: any): void => {\n __Porffor_print(arg);\n};\n",
|
|
174
|
-
"compiler/builtins/map.ts": "export const Map = function (iterable: any): Map {\n};\n",
|
|
175
174
|
"compiler/codegen.js":
|
|
176
175
|
"if (!hoistRead && directCallOnlyFunctionBinding(scope, kind, name, pattern, func)) {\n return valUndefined();\n}\nconst func = resolveNamedFunction(scope, name) ?? funcByName(name);\n" +
|
|
177
|
-
"
|
|
178
|
-
" if (kind === 'get' || kind === 'set') initKind = kind;\n" +
|
|
176
|
+
" // only names that are nothing but this declaration and its direct calls\n if (find(program, n => n.name === x.id.name).length !== 1) continue;\n" +
|
|
179
177
|
" stmt(scope, Store(ctype, addr, 4, ctype === 'f64' || ctype === 'f32' ? f : toUint32(scope, f)));\n" +
|
|
180
178
|
" const taGet = (ctype, size, signed = true) => () => {\n" +
|
|
181
179
|
" const loaded = Load(ctype, taAddr(size), 4);\n" +
|
|
@@ -298,16 +296,15 @@ test("the default call (no url/expectedSha256 override) never touches the networ
|
|
|
298
296
|
expect(await readFile(join(dir, "compiler/uwebsockets.js"), "utf8")).toContain("sproutboat B19: streamed responses");
|
|
299
297
|
// console colours only on a terminal.
|
|
300
298
|
expect(await readFile(join(dir, "compiler/builtins/console.ts"), "utf8")).toContain("__Porffor_consoleColors");
|
|
301
|
-
// JSON
|
|
302
|
-
|
|
303
|
-
|
|
299
|
+
// JSON cycles, the JSON.parse reviver, typed-array toString tags, ISO date offsets,
|
|
300
|
+
// base64 options, Map.groupBy and non-enumerable private fields (#286) are upstream's
|
|
301
|
+
// own since alpha-19; json-cycles, groupby-private, date and base64 native tests cover them.
|
|
302
|
+
// Alpha 19 new Function specialization checks a function body before walking the program.
|
|
303
|
+
expect(await readFile(join(dir, "compiler/codegen.js"), "utf8")).toContain("sproutboat: body check before the whole-program walks");
|
|
304
304
|
// B16: String.prototype.repeat doubles its copies.
|
|
305
305
|
expect(await readFile(join(dir, "compiler/builtins/string.ts"), "utf8")).toContain("sproutboat B16: doubling repeat");
|
|
306
306
|
// #287: full collections under load keep their free pages; the idle collection returns them.
|
|
307
307
|
expect(await readFile(join(dir, "compiler/render.js"), "utf8")).toContain("if (porf_gc_discard_now) porf_gc_discard_free_runs(); else porf_gc_discard_pending = 1;");
|
|
308
|
-
// #286: Map.groupBy, and private fields installed as non-enumerable.
|
|
309
|
-
expect(await readFile(join(dir, "compiler/builtins/map.ts"), "utf8")).toContain("export const __Map_groupBy");
|
|
310
|
-
expect(await readFile(join(dir, "compiler/codegen.js"), "utf8")).toContain("sproutboat #286: private fields");
|
|
311
308
|
// #256: named class expressions inside functions are rewritten at parse time.
|
|
312
309
|
expect(await readFile(join(dir, "compiler/parse.js"), "utf8")).toContain("sbClassSelf(ast); // sproutboat #256");
|
|
313
310
|
expect(await readFile(join(dir, "compiler/sb-class-self.js"), "utf8")).toContain("sproutboat #256");
|
|
@@ -421,3 +418,12 @@ test("download failures are classified without publishing partial state", async
|
|
|
421
418
|
expect(attempts).toBe(2);
|
|
422
419
|
expect(await readdir(cacheRoot)).toEqual([]);
|
|
423
420
|
});
|
|
421
|
+
|
|
422
|
+
test("the vendored archive is the one PORFFOR_ARCHIVE_URL serves: its root is porffor-<full commit>", () => {
|
|
423
|
+
// codeload names the root after the ref in the URL, so an archive fetched by the short
|
|
424
|
+
// commit has another root and digest than the pinned URL's, though the same files.
|
|
425
|
+
const archive = join(import.meta.dir, `../vendor/porffor-${PORFFOR_COMMIT_FULL.slice(0, 7)}.tar.gz`);
|
|
426
|
+
const list = Bun.spawnSync(["tar", "-tzf", archive], { stdout: "pipe" });
|
|
427
|
+
expect(list.exitCode).toBe(0);
|
|
428
|
+
expect(list.stdout.toString().split("\n")[0]).toBe(`porffor-${PORFFOR_COMMIT_FULL}/`);
|
|
429
|
+
});
|
package/src/acquire.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { homedir } from "node:os";
|
|
|
5
5
|
import { resolve } from "node:path";
|
|
6
6
|
// @ts-expect-error Bun's file loader supplies a path; see the pinned-commit
|
|
7
7
|
// check below for why this is safe to trust unconditionally.
|
|
8
|
-
import vendoredPorfforArchive from "../vendor/porffor-
|
|
8
|
+
import vendoredPorfforArchive from "../vendor/porffor-0c135ce.tar.gz" with { type: "file" };
|
|
9
9
|
import { ensurePorfforPatched } from "./patch";
|
|
10
10
|
import { PORFFOR_ARCHIVE_SHA256, PORFFOR_ARCHIVE_URL, PORFFOR_COMMIT_FULL } from "./pin";
|
|
11
11
|
// Folded into the cache manifest below so a toolchain release that changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { ensurePorffor } from "./acquire";
|
|
6
|
+
|
|
7
|
+
test("conservative roots retain encoded heap BigInts on the stack and in untyped blocks", async () => {
|
|
8
|
+
const workdir = await mkdtemp(resolve(tmpdir(), "sb-bigint-gc-"));
|
|
9
|
+
try {
|
|
10
|
+
const porffor = await ensurePorffor();
|
|
11
|
+
const source = resolve(import.meta.dir, "../test/gc-bigint.js.txt");
|
|
12
|
+
const binary = resolve(workdir, "bigint-gc");
|
|
13
|
+
const compile = Bun.spawn(
|
|
14
|
+
["bun", resolve(porffor, "runtime/index.js"), "native", source, "-o", binary, "-O0", "-s"],
|
|
15
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
16
|
+
);
|
|
17
|
+
const [code, stdout, stderr] = await Promise.all([
|
|
18
|
+
compile.exited,
|
|
19
|
+
new Response(compile.stdout).text(),
|
|
20
|
+
new Response(compile.stderr).text(),
|
|
21
|
+
]);
|
|
22
|
+
expect(code, stderr + stdout).toBe(0);
|
|
23
|
+
const run = Bun.spawnSync([binary], { stdout: "pipe", stderr: "pipe", timeout: 10_000 });
|
|
24
|
+
expect(run.exitCode, Buffer.from(run.stderr).toString()).toBe(0);
|
|
25
|
+
const ansi = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
26
|
+
expect(Buffer.from(run.stdout).toString().replace(ansi, "").trim()).toBe("3");
|
|
27
|
+
} finally {
|
|
28
|
+
await rm(workdir, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
}, 120_000);
|
package/src/fingerprint.ts
CHANGED
|
@@ -21,7 +21,7 @@ export const PATCH_SOURCES = [
|
|
|
21
21
|
* patch.ts as the module patch.ts is everywhere else. Kept current by fingerprint.test.ts
|
|
22
22
|
* (UPDATE_FINGERPRINT=1 bun test packages/toolchain/src/fingerprint.test.ts).
|
|
23
23
|
*/
|
|
24
|
-
export const RELEASED_FINGERPRINT = "
|
|
24
|
+
export const RELEASED_FINGERPRINT = "ee724d4de68f";
|
|
25
25
|
|
|
26
26
|
/** The fingerprint of the sources in dir, or null where they are not on disk. */
|
|
27
27
|
export function sourceFingerprint(dir: string): string | null {
|
package/src/idle-gc.test.ts
CHANGED
|
@@ -10,7 +10,7 @@ test("idle reclamation preserves unconditional request GC and is idempotent on t
|
|
|
10
10
|
try {
|
|
11
11
|
execFileSync("tar", [
|
|
12
12
|
"-xzf",
|
|
13
|
-
resolve(import.meta.dir, "../vendor/porffor-
|
|
13
|
+
resolve(import.meta.dir, "../vendor/porffor-0c135ce.tar.gz"),
|
|
14
14
|
"--strip-components=1",
|
|
15
15
|
"-C",
|
|
16
16
|
root,
|
package/src/patch.test.ts
CHANGED
|
@@ -6,12 +6,9 @@ import { EMPTY_BINDINGS, wrapNativeFetchHandler } from "../../runtime/src/wrap";
|
|
|
6
6
|
import {
|
|
7
7
|
patchHeadersArrayInit,
|
|
8
8
|
patchLocalFunctionTypes,
|
|
9
|
+
patchFunctionSourcesScan,
|
|
9
10
|
patchDeferredDiscard,
|
|
10
|
-
patchMapGroupBy,
|
|
11
|
-
patchPrivateFieldsHidden,
|
|
12
11
|
patchRepeatDoubling,
|
|
13
|
-
patchJsonCycles,
|
|
14
|
-
patchTypedArrayTag,
|
|
15
12
|
patchConsoleColors,
|
|
16
13
|
patchCaseMapping,
|
|
17
14
|
patchZeroDelayTimers,
|
|
@@ -25,7 +22,6 @@ import {
|
|
|
25
22
|
patchTextDecoder,
|
|
26
23
|
patchResponseDefaults,
|
|
27
24
|
patchURLClass,
|
|
28
|
-
patchUint8ArrayBase64,
|
|
29
25
|
patchRenderJs,
|
|
30
26
|
patchUwebsockets,
|
|
31
27
|
} from "./patch";
|
|
@@ -365,22 +361,21 @@ test("local function type patch registers lexical bindings and is idempotent", a
|
|
|
365
361
|
}
|
|
366
362
|
});
|
|
367
363
|
|
|
368
|
-
test("
|
|
369
|
-
const root = await mkdtemp(join(tmpdir(), "sb-
|
|
364
|
+
test("function source patch checks the body before walking the program, idempotently", async () => {
|
|
365
|
+
const root = await mkdtemp(join(tmpdir(), "sb-function-sources-"));
|
|
370
366
|
try {
|
|
371
|
-
await mkdir(join(root, "compiler
|
|
372
|
-
const file = join(root, "compiler/
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
);
|
|
377
|
-
await patchUint8ArrayBase64(root);
|
|
367
|
+
await mkdir(join(root, "compiler"));
|
|
368
|
+
const file = join(root, "compiler/codegen.js");
|
|
369
|
+
const anchor =
|
|
370
|
+
" // only names that are nothing but this declaration and its direct calls\n if (find(program, n => n.name === x.id.name).length !== 1) continue;\n";
|
|
371
|
+
await writeFile(file, anchor);
|
|
372
|
+
await patchFunctionSourcesScan(root);
|
|
378
373
|
const patched = await readFile(file, "utf8");
|
|
379
|
-
expect(patched
|
|
380
|
-
|
|
381
|
-
expect(patched).toContain("((len / 3) | 0) * 3");
|
|
382
|
-
await patchUint8ArrayBase64(root);
|
|
374
|
+
expect(patched.indexOf("n.callee.name === 'Function').length === 0) continue;")).toBeLessThan(patched.indexOf("find(program,"));
|
|
375
|
+
await patchFunctionSourcesScan(root);
|
|
383
376
|
expect(await readFile(file, "utf8")).toBe(patched);
|
|
377
|
+
await writeFile(file, "const unrelated = 1;\n");
|
|
378
|
+
await expect(patchFunctionSourcesScan(root)).rejects.toThrow("anchor not found");
|
|
384
379
|
} finally {
|
|
385
380
|
await rm(root, { recursive: true, force: true });
|
|
386
381
|
}
|
|
@@ -457,47 +452,6 @@ test("URL patch replaces Porffor's URL class, idempotently", async () => {
|
|
|
457
452
|
});
|
|
458
453
|
|
|
459
454
|
|
|
460
|
-
test("Map.groupBy patch appends the builtin once and refuses an upstream one", async () => {
|
|
461
|
-
const root = await mkdtemp(join(tmpdir(), "sb-map-groupby-"));
|
|
462
|
-
try {
|
|
463
|
-
await mkdir(join(root, "compiler/builtins"), { recursive: true });
|
|
464
|
-
const file = join(root, "compiler/builtins/map.ts");
|
|
465
|
-
await writeFile(file, "export const Map = function (iterable: any): Map {\n};\n");
|
|
466
|
-
await patchMapGroupBy(root);
|
|
467
|
-
const patched = await readFile(file, "utf8");
|
|
468
|
-
expect(patched).toContain("export const __Map_groupBy = (items: any, callbackFn: any): Map => {");
|
|
469
|
-
await patchMapGroupBy(root);
|
|
470
|
-
expect(await readFile(file, "utf8")).toBe(patched);
|
|
471
|
-
await writeFile(file, "export const Map = function (iterable: any): Map {\n};\nexport const __Map_groupBy = 1;\n");
|
|
472
|
-
await expect(patchMapGroupBy(root)).rejects.toThrow("drop patchMapGroupBy");
|
|
473
|
-
} finally {
|
|
474
|
-
await rm(root, { recursive: true, force: true });
|
|
475
|
-
}
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
test("private field patch installs instance and static private fields as methods, idempotently", async () => {
|
|
479
|
-
const root = await mkdtemp(join(tmpdir(), "sb-private-fields-"));
|
|
480
|
-
try {
|
|
481
|
-
await mkdir(join(root, "compiler"));
|
|
482
|
-
const file = join(root, "compiler/codegen.js");
|
|
483
|
-
await writeFile(
|
|
484
|
-
file,
|
|
485
|
-
" builtinCall(func, '__Porffor_object_class_value', [\n" +
|
|
486
|
-
" if (kind === 'get' || kind === 'set') initKind = kind;\n",
|
|
487
|
-
);
|
|
488
|
-
await patchPrivateFieldsHidden(root);
|
|
489
|
-
const patched = await readFile(file, "utf8");
|
|
490
|
-
expect(patched).toContain("x.key?.type === 'PrivateIdentifier' ? '__Porffor_object_class_method' : '__Porffor_object_class_value'");
|
|
491
|
-
expect(patched).toContain("if (initKind === 'value' && x.key?.type === 'PrivateIdentifier') initKind = 'method';");
|
|
492
|
-
await patchPrivateFieldsHidden(root);
|
|
493
|
-
expect(await readFile(file, "utf8")).toBe(patched);
|
|
494
|
-
await writeFile(file, "export const x = 1;\n");
|
|
495
|
-
await expect(patchPrivateFieldsHidden(root)).rejects.toThrow("anchor not found");
|
|
496
|
-
} finally {
|
|
497
|
-
await rm(root, { recursive: true, force: true });
|
|
498
|
-
}
|
|
499
|
-
});
|
|
500
|
-
|
|
501
455
|
test("deferred discard patch moves page discard to the idle collection, idempotently", async () => {
|
|
502
456
|
const root = await mkdtemp(join(tmpdir(), "sb-deferred-discard-"));
|
|
503
457
|
try {
|
|
@@ -572,48 +526,6 @@ test("ensurePorfforPatched trusts a cache entry this toolchain version and patch
|
|
|
572
526
|
}
|
|
573
527
|
});
|
|
574
528
|
|
|
575
|
-
test("JSON cycle patch adds an ancestor stack around arrays and objects, idempotently", async () => {
|
|
576
|
-
const root = await mkdtemp(join(tmpdir(), "sb-json-cycles-"));
|
|
577
|
-
try {
|
|
578
|
-
await mkdir(join(root, "compiler/builtins"), { recursive: true });
|
|
579
|
-
const file = join(root, "compiler/builtins/json.ts");
|
|
580
|
-
await writeFile(
|
|
581
|
-
file,
|
|
582
|
-
"let __Porffor_json_bufLimit: i32 = 0;\n depth += 1;\n depth -= 1;\n depth += 1;\n depth -= 1;\n" +
|
|
583
|
-
" let out: i32 = __Porffor_json_serialize(buffer, value, 0, space);\n",
|
|
584
|
-
);
|
|
585
|
-
await patchJsonCycles(root);
|
|
586
|
-
const patched = await readFile(file, "utf8");
|
|
587
|
-
expect(patched.split("__Porffor_json_enter(value);").length).toBe(3);
|
|
588
|
-
expect(patched.split("__Porffor_json_leave();").length).toBe(3);
|
|
589
|
-
expect(patched).toContain("throw new TypeError('Converting circular structure to JSON');");
|
|
590
|
-
await patchJsonCycles(root);
|
|
591
|
-
expect(await readFile(file, "utf8")).toBe(patched);
|
|
592
|
-
await writeFile(file, "let __Porffor_json_bufLimit: i32 = 0;\n");
|
|
593
|
-
await expect(patchJsonCycles(root)).rejects.toThrow("anchor not found");
|
|
594
|
-
} finally {
|
|
595
|
-
await rm(root, { recursive: true, force: true });
|
|
596
|
-
}
|
|
597
|
-
});
|
|
598
|
-
|
|
599
|
-
test("typed array tag patch names every typed array before toString returns, idempotently", async () => {
|
|
600
|
-
const root = await mkdtemp(join(tmpdir(), "sb-ta-tag-"));
|
|
601
|
-
try {
|
|
602
|
-
await mkdir(join(root, "compiler/builtins"), { recursive: true });
|
|
603
|
-
const file = join(root, "compiler/builtins/object.ts");
|
|
604
|
-
await writeFile(file, " return '[object ' + builtinTag + ']';\n};\n");
|
|
605
|
-
await patchTypedArrayTag(root);
|
|
606
|
-
const patched = await readFile(file, "utf8");
|
|
607
|
-
for (const name of ["Uint8Array", "Uint8ClampedArray", "BigInt64Array", "Float64Array"]) expect(patched).toContain(`builtinTag = '${name}';`);
|
|
608
|
-
await patchTypedArrayTag(root);
|
|
609
|
-
expect(await readFile(file, "utf8")).toBe(patched);
|
|
610
|
-
await writeFile(file, "export const x = 1;\n");
|
|
611
|
-
await expect(patchTypedArrayTag(root)).rejects.toThrow("anchor not found");
|
|
612
|
-
} finally {
|
|
613
|
-
await rm(root, { recursive: true, force: true });
|
|
614
|
-
}
|
|
615
|
-
});
|
|
616
|
-
|
|
617
529
|
test("console colour patch colours only on a terminal, idempotently", async () => {
|
|
618
530
|
const root = await mkdtemp(join(tmpdir(), "sb-console-colors-"));
|
|
619
531
|
try {
|
package/src/patch.ts
CHANGED
|
@@ -233,6 +233,9 @@ const rewrite = (cls) => {
|
|
|
233
233
|
const name = cls.id.name;
|
|
234
234
|
const at = { start: cls.start, end: cls.end };
|
|
235
235
|
const id = () => ({ type: 'Identifier', name, ...at });
|
|
236
|
+
// Porffor's TDZ check (Alpha 19) flags a same-function read that starts before its
|
|
237
|
+
// declarator ends, so the returned name sits at the end of the class, as in source.
|
|
238
|
+
const after = { start: cls.end, end: cls.end };
|
|
236
239
|
return {
|
|
237
240
|
type: 'CallExpression', optional: false, arguments: [], ...at,
|
|
238
241
|
callee: {
|
|
@@ -243,7 +246,7 @@ const rewrite = (cls) => {
|
|
|
243
246
|
{ type: 'VariableDeclaration', kind: 'const', ...at, declarations: [
|
|
244
247
|
{ type: 'VariableDeclarator', id: id(), init: { ...cls, id: null }, ...at },
|
|
245
248
|
] },
|
|
246
|
-
{ type: 'ReturnStatement', argument: id(), ...
|
|
249
|
+
{ type: 'ReturnStatement', argument: { ...id(), ...after }, ...after },
|
|
247
250
|
],
|
|
248
251
|
},
|
|
249
252
|
},
|
|
@@ -389,97 +392,6 @@ const REPLACE_ALL_EDITS = [
|
|
|
389
392
|
] as const;
|
|
390
393
|
const REPLACE_ALL_MARKER = "sproutboat #237: pieces joined once";
|
|
391
394
|
|
|
392
|
-
// Porffor alpha 9 treats +02:00 as two milliseconds and adds offset hours
|
|
393
|
-
// instead of subtracting them. Count fields even when they contain zero, and
|
|
394
|
-
// apply the signed offset after parsing the local wall-clock fields.
|
|
395
|
-
const DATE_PARSER_MARKER = "sproutboat: ISO timezone offset";
|
|
396
|
-
const DATE_PARSER_ANCHOR = ` let n: number = 0;
|
|
397
|
-
let nInd: number = 0;
|
|
398
|
-
|
|
399
|
-
const len: i32 = string.length;
|
|
400
|
-
const endPtr: i32 = Porffor.IR.ptr(string) + len;
|
|
401
|
-
let ptr: i32 = Porffor.IR.ptr(string);
|
|
402
|
-
|
|
403
|
-
while (ptr <= endPtr) { // <= to include extra null byte to set last n
|
|
404
|
-
const chr: i32 = Porffor.IR.loadU8(ptr++, 4);
|
|
405
|
-
if (Porffor.fastAnd(chr >= 48, chr <= 57)) { // 0-9
|
|
406
|
-
n *= 10;
|
|
407
|
-
n += chr - 48;
|
|
408
|
-
continue;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
if (chr == 45) { // -
|
|
412
|
-
if (Porffor.fastOr(ptr == Porffor.IR.ptr(string), nInd == 7)) n = -n;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
if (n > 0) {
|
|
416
|
-
if (nInd == 0) y = n;
|
|
417
|
-
else if (nInd == 1) m = n - 1;
|
|
418
|
-
else if (nInd == 2) dt = n;
|
|
419
|
-
else if (nInd == 3) h = n;
|
|
420
|
-
else if (nInd == 4) min = n;
|
|
421
|
-
else if (nInd == 5) s = n;
|
|
422
|
-
else if (nInd == 6) milli = n;
|
|
423
|
-
else if (nInd == 7) tzHour = n;
|
|
424
|
-
else if (nInd == 8) tzMin = n;
|
|
425
|
-
|
|
426
|
-
n = 0;
|
|
427
|
-
nInd++;
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
h += tzHour;
|
|
432
|
-
min += tzMin;`;
|
|
433
|
-
const DATE_PARSER_INJECT = ` // sproutboat: ISO timezone offset
|
|
434
|
-
let n: number = 0;
|
|
435
|
-
let nInd: number = 0;
|
|
436
|
-
let digits: i32 = 0;
|
|
437
|
-
let offsetSign: i32 = 0;
|
|
438
|
-
|
|
439
|
-
const len: i32 = string.length;
|
|
440
|
-
const endPtr: i32 = Porffor.IR.ptr(string) + len;
|
|
441
|
-
let ptr: i32 = Porffor.IR.ptr(string);
|
|
442
|
-
|
|
443
|
-
// Loop once past the last character to flush the final number, but read a
|
|
444
|
-
// 0 there, not the byte after the string: if that byte happened to be '+' or
|
|
445
|
-
// '-' it flipped the offset's sign.
|
|
446
|
-
while (ptr <= endPtr) {
|
|
447
|
-
const chr: i32 = ptr < endPtr ? Porffor.IR.loadU8(ptr, 4) : 0;
|
|
448
|
-
ptr++;
|
|
449
|
-
if (Porffor.fastAnd(chr >= 48, chr <= 57)) {
|
|
450
|
-
n = n * 10 + chr - 48;
|
|
451
|
-
digits++;
|
|
452
|
-
continue;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
if (digits > 0) {
|
|
456
|
-
if (nInd == 0) y = n;
|
|
457
|
-
else if (nInd == 1) m = n - 1;
|
|
458
|
-
else if (nInd == 2) dt = n;
|
|
459
|
-
else if (nInd == 3) h = n;
|
|
460
|
-
else if (nInd == 4) min = n;
|
|
461
|
-
else if (nInd == 5) s = n;
|
|
462
|
-
else if (nInd == 6) {
|
|
463
|
-
while (digits < 3) { n *= 10; digits++; }
|
|
464
|
-
while (digits > 3) { n = Math.trunc(n / 10); digits--; }
|
|
465
|
-
milli = n;
|
|
466
|
-
}
|
|
467
|
-
else if (nInd == 7) tzHour = n;
|
|
468
|
-
else if (nInd == 8) tzMin = n;
|
|
469
|
-
n = 0;
|
|
470
|
-
nInd++;
|
|
471
|
-
digits = 0;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
if (Porffor.fastOr(chr == 43, chr == 45) && nInd >= 6) {
|
|
475
|
-
offsetSign = chr == 43 ? 1 : -1;
|
|
476
|
-
nInd = 7;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
h -= offsetSign * tzHour;
|
|
481
|
-
min -= offsetSign * tzMin;`;
|
|
482
|
-
|
|
483
395
|
/**
|
|
484
396
|
* #56 β make the inbound request-body limit configurable.
|
|
485
397
|
*
|
|
@@ -1407,30 +1319,6 @@ async function patchCompilerArgs(root: string): Promise<void> {
|
|
|
1407
1319
|
if (changed) await writeFile(file, src);
|
|
1408
1320
|
}
|
|
1409
1321
|
|
|
1410
|
-
export async function patchDateParser(root: string): Promise<void> {
|
|
1411
|
-
const file = resolve(root, "compiler/builtins/date.ts");
|
|
1412
|
-
const src = await readFile(file, "utf8");
|
|
1413
|
-
if (src.includes(DATE_PARSER_MARKER)) return;
|
|
1414
|
-
const start = src.indexOf("export const __ecma262_ParseDTSF");
|
|
1415
|
-
const end = src.indexOf(
|
|
1416
|
-
"// RFC 7231 or Date.prototype.toString() parser",
|
|
1417
|
-
start,
|
|
1418
|
-
);
|
|
1419
|
-
if (start < 0 || end < 0)
|
|
1420
|
-
throw new Error(`could not find Porffor's ISO date parser in ${file}`);
|
|
1421
|
-
const parser = src.slice(start, end);
|
|
1422
|
-
if (!parser.includes(DATE_PARSER_ANCHOR))
|
|
1423
|
-
throw new Error(
|
|
1424
|
-
`could not patch Porffor's ISO date parser: anchor not found in ${file}`,
|
|
1425
|
-
);
|
|
1426
|
-
await writeFile(
|
|
1427
|
-
file,
|
|
1428
|
-
src.slice(0, start) +
|
|
1429
|
-
parser.replace(DATE_PARSER_ANCHOR, DATE_PARSER_INJECT) +
|
|
1430
|
-
src.slice(end),
|
|
1431
|
-
);
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
1322
|
/** #168: keep a promise resolving to null out of the `then` probe. */
|
|
1435
1323
|
export async function patchPromiseResolveNull(root: string): Promise<void> {
|
|
1436
1324
|
const file = resolve(root, "compiler/builtins/promise.ts");
|
|
@@ -1501,28 +1389,32 @@ export async function patchLocalFunctionTypes(root: string): Promise<void> {
|
|
|
1501
1389
|
await writeFile(file, src);
|
|
1502
1390
|
}
|
|
1503
1391
|
|
|
1504
|
-
/**
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
src
|
|
1524
|
-
|
|
1525
|
-
|
|
1392
|
+
/**
|
|
1393
|
+
* Alpha 19's specializeFunctionSources (codegen.js) walks the whole program twice for
|
|
1394
|
+
* every top-level function declaration before it checks whether that function's body
|
|
1395
|
+
* calls `Function` at all, so a program with many top-level functions compiled
|
|
1396
|
+
* quadratically: IR generation for the runtime prelude went from 0.6 s on Alpha 18 to
|
|
1397
|
+
* 3.7 s (sproutd admission: 1.1 s to 4.2 s per app). Check the body first; every check
|
|
1398
|
+
* is a pure walk, so the generated code is unchanged.
|
|
1399
|
+
*/
|
|
1400
|
+
const FUNCTION_SOURCES_MARKER = "sproutboat: body check before the whole-program walks";
|
|
1401
|
+
const FUNCTION_SOURCES_ANCHOR =
|
|
1402
|
+
" // only names that are nothing but this declaration and its direct calls\n if (find(program, n => n.name === x.id.name)";
|
|
1403
|
+
export async function patchFunctionSourcesScan(root: string): Promise<void> {
|
|
1404
|
+
const file = resolve(root, "compiler/codegen.js");
|
|
1405
|
+
const src = await readFile(file, "utf8");
|
|
1406
|
+
if (src.includes(FUNCTION_SOURCES_MARKER)) return;
|
|
1407
|
+
if (src.split(FUNCTION_SOURCES_ANCHOR).length !== 2)
|
|
1408
|
+
throw new Error(`could not patch Porffor's new Function source specialization: anchor not found in ${file}`);
|
|
1409
|
+
await writeFile(
|
|
1410
|
+
file,
|
|
1411
|
+
src.replace(
|
|
1412
|
+
FUNCTION_SOURCES_ANCHOR,
|
|
1413
|
+
` // ${FUNCTION_SOURCES_MARKER}\n` +
|
|
1414
|
+
" if (find(x.body, n => (n.type === 'CallExpression' || n.type === 'NewExpression') && n.callee.name === 'Function').length === 0) continue;\n" +
|
|
1415
|
+
FUNCTION_SOURCES_ANCHOR,
|
|
1416
|
+
),
|
|
1417
|
+
);
|
|
1526
1418
|
}
|
|
1527
1419
|
|
|
1528
1420
|
export async function precompileBuiltins(root: string): Promise<void> {
|
|
@@ -1752,66 +1644,6 @@ export async function patchHeadersArrayInit(root: string): Promise<void> {
|
|
|
1752
1644
|
);
|
|
1753
1645
|
}
|
|
1754
1646
|
|
|
1755
|
-
// baronunread/sproutboat#286: Map.groupBy is missing (Object.groupBy exists). Same
|
|
1756
|
-
// shape as __Object_groupBy, keyed by SameValueZero through the Map itself.
|
|
1757
|
-
const MAP_GROUPBY_MARKER = "sproutboat #286: Map.groupBy";
|
|
1758
|
-
const MAP_GROUPBY_SOURCE =
|
|
1759
|
-
"\n// sproutboat #286: Map.groupBy\n" +
|
|
1760
|
-
"export const __Map_groupBy = (items: any, callbackFn: any): Map => {\n" +
|
|
1761
|
-
" const out: Map = __Porffor_hashtableNew(true);\n" +
|
|
1762
|
-
" Porffor.IR.gcBarrier(out, Porffor.TYPES.map);\n" +
|
|
1763
|
-
" let i: i32 = 0;\n" +
|
|
1764
|
-
" for (const x of items) {\n" +
|
|
1765
|
-
" const k: any = callbackFn(x, i++);\n" +
|
|
1766
|
-
" if (!Porffor.callThis(__Map_prototype_has, out, k)) Porffor.callThis(__Map_prototype_set, out, k, Porffor.array.new(4));\n" +
|
|
1767
|
-
" Porffor.array.fastPush(Porffor.callThis(__Map_prototype_get, out, k), x);\n" +
|
|
1768
|
-
" }\n" +
|
|
1769
|
-
" return out;\n" +
|
|
1770
|
-
"};\n";
|
|
1771
|
-
|
|
1772
|
-
/** #286: add Map.groupBy. Before the precompile, which compiles it with the other builtins. */
|
|
1773
|
-
export async function patchMapGroupBy(root: string): Promise<void> {
|
|
1774
|
-
const file = resolve(root, "compiler/builtins/map.ts");
|
|
1775
|
-
const src = await readFile(file, "utf8");
|
|
1776
|
-
if (src.includes(MAP_GROUPBY_MARKER)) return;
|
|
1777
|
-
if (!src.includes("export const Map = function (iterable: any): Map {"))
|
|
1778
|
-
throw new Error(`could not patch Porffor's Map.groupBy: anchor not found in ${file}`);
|
|
1779
|
-
if (src.includes("export const __Map_groupBy"))
|
|
1780
|
-
throw new Error(`Porffor now defines Map.groupBy itself: drop patchMapGroupBy (${file})`);
|
|
1781
|
-
await writeFile(file, src.trimEnd() + "\n" + MAP_GROUPBY_SOURCE);
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
|
-
// baronunread/sproutboat#286: a private field (#x) is stored as an ordinary property
|
|
1785
|
-
// named "__#x", installed like a public field, so Object.keys, for-in and
|
|
1786
|
-
// JSON.stringify listed it. Install private fields the way methods are installed:
|
|
1787
|
-
// writable and configurable but not enumerable. Object.getOwnPropertyNames still
|
|
1788
|
-
// sees the internal name.
|
|
1789
|
-
const PRIVATE_FIELD_MARKER = "sproutboat #286: private fields";
|
|
1790
|
-
const PRIVATE_FIELD_EDITS: [string, string][] = [
|
|
1791
|
-
[
|
|
1792
|
-
" builtinCall(func, '__Porffor_object_class_value', [\n",
|
|
1793
|
-
" // sproutboat #286: private fields are not enumerable\n" +
|
|
1794
|
-
" builtinCall(func, x.key?.type === 'PrivateIdentifier' ? '__Porffor_object_class_method' : '__Porffor_object_class_value', [\n",
|
|
1795
|
-
],
|
|
1796
|
-
[
|
|
1797
|
-
" if (kind === 'get' || kind === 'set') initKind = kind;\n",
|
|
1798
|
-
" if (kind === 'get' || kind === 'set') initKind = kind;\n" +
|
|
1799
|
-
" if (initKind === 'value' && x.key?.type === 'PrivateIdentifier') initKind = 'method'; // sproutboat #286: private fields\n",
|
|
1800
|
-
],
|
|
1801
|
-
];
|
|
1802
|
-
|
|
1803
|
-
export async function patchPrivateFieldsHidden(root: string): Promise<void> {
|
|
1804
|
-
const file = resolve(root, "compiler/codegen.js");
|
|
1805
|
-
let src = await readFile(file, "utf8");
|
|
1806
|
-
if (src.includes(PRIVATE_FIELD_MARKER)) return;
|
|
1807
|
-
for (const [anchor, inject] of PRIVATE_FIELD_EDITS) {
|
|
1808
|
-
if (src.split(anchor).length !== 2)
|
|
1809
|
-
throw new Error(`could not patch Porffor's private fields: anchor not found in ${file}`);
|
|
1810
|
-
src = src.replace(anchor, inject);
|
|
1811
|
-
}
|
|
1812
|
-
await writeFile(file, src);
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
1647
|
// baronunread/sproutboat#287 (sproutd B9/B13): native-fetch mode runs frequent full
|
|
1816
1648
|
// collections, and each returned its free pages to the OS (porf_gc_discard_free_runs),
|
|
1817
1649
|
// so the next large allocation faulted them back in: allocation-heavy handlers ran up
|
|
@@ -1920,128 +1752,6 @@ export async function patchRepeatDoubling(root: string): Promise<void> {
|
|
|
1920
1752
|
await writeFile(file, src.replaceAll(REPEAT_LOOP, REPEAT_DOUBLING));
|
|
1921
1753
|
}
|
|
1922
1754
|
|
|
1923
|
-
// JSON.stringify of a cyclic object recursed until the native stack overflowed: the
|
|
1924
|
-
// process died (SIGBUS/SIGSEGV) instead of throwing, so one request could take a
|
|
1925
|
-
// worker down (found by the W2 conformance suite). The replacer path already checks
|
|
1926
|
-
// cycles; the plain serializer now keeps its own ancestor stack and throws the same
|
|
1927
|
-
// TypeError. Arrays and objects push on entry and pop on exit; JSON.stringify clears
|
|
1928
|
-
// the stack first, so a throw mid-serialization leaves nothing behind.
|
|
1929
|
-
const JSON_CYCLE_MARKER = "sproutboat: JSON cycle check";
|
|
1930
|
-
const JSON_CYCLE_EDITS: [string, string, number][] = [
|
|
1931
|
-
[
|
|
1932
|
-
"let __Porffor_json_bufLimit: i32 = 0;\n",
|
|
1933
|
-
"let __Porffor_json_bufLimit: i32 = 0;\n" +
|
|
1934
|
-
"// sproutboat: JSON cycle check\n" +
|
|
1935
|
-
"let __Porffor_json_ancestors: any = 0;\n" +
|
|
1936
|
-
"export const __Porffor_json_enter = (value: any): void => {\n" +
|
|
1937
|
-
" if (__Porffor_json_ancestors === 0) __Porffor_json_ancestors = Porffor.array.new(8);\n" +
|
|
1938
|
-
" const stack: any[] = __Porffor_json_ancestors;\n" +
|
|
1939
|
-
" if (stack.includes(value)) {\n" +
|
|
1940
|
-
" stack.length = 0;\n" +
|
|
1941
|
-
" throw new TypeError('Converting circular structure to JSON');\n" +
|
|
1942
|
-
" }\n" +
|
|
1943
|
-
" Porffor.array.fastPush(stack, value);\n" +
|
|
1944
|
-
"};\n" +
|
|
1945
|
-
"export const __Porffor_json_leave = (): void => {\n" +
|
|
1946
|
-
" const stack: any[] = __Porffor_json_ancestors;\n" +
|
|
1947
|
-
" stack.length = stack.length - 1;\n" +
|
|
1948
|
-
"};\n",
|
|
1949
|
-
1,
|
|
1950
|
-
],
|
|
1951
|
-
[" depth += 1;\n", " depth += 1;\n __Porffor_json_enter(value);\n", 2],
|
|
1952
|
-
[" depth -= 1;\n", " __Porffor_json_leave();\n depth -= 1;\n", 2],
|
|
1953
|
-
[
|
|
1954
|
-
" let out: i32 = __Porffor_json_serialize(buffer, value, 0, space);\n",
|
|
1955
|
-
" if (__Porffor_json_ancestors !== 0) (__Porffor_json_ancestors as any[]).length = 0;\n" +
|
|
1956
|
-
" let out: i32 = __Porffor_json_serialize(buffer, value, 0, space);\n",
|
|
1957
|
-
1,
|
|
1958
|
-
],
|
|
1959
|
-
];
|
|
1960
|
-
|
|
1961
|
-
/** JSON.stringify throws on cycles instead of overflowing the stack. Before the precompile. */
|
|
1962
|
-
export async function patchJsonCycles(root: string): Promise<void> {
|
|
1963
|
-
const file = resolve(root, "compiler/builtins/json.ts");
|
|
1964
|
-
let src = await readFile(file, "utf8");
|
|
1965
|
-
if (src.includes(JSON_CYCLE_MARKER)) return;
|
|
1966
|
-
for (const [anchor, inject, count] of JSON_CYCLE_EDITS) {
|
|
1967
|
-
if (src.split(anchor).length !== count + 1)
|
|
1968
|
-
throw new Error(`could not patch Porffor's JSON cycle check: anchor not found ${count}x in ${file}`);
|
|
1969
|
-
src = src.replaceAll(anchor, inject);
|
|
1970
|
-
}
|
|
1971
|
-
await writeFile(file, src);
|
|
1972
|
-
}
|
|
1973
|
-
|
|
1974
|
-
// JSON.parse ignored its reviver (W2 conformance: json/reviver). The parser keeps its body
|
|
1975
|
-
// under a new name, and __JSON_parse walks the result as the spec's InternalizeJSONProperty
|
|
1976
|
-
// does: children first, a reviver answer of undefined deletes the property.
|
|
1977
|
-
const JSON_REVIVER_MARKER = "sproutboat: JSON.parse reviver";
|
|
1978
|
-
// Porffor declares (_: bytestring); patchUnicode widens it to any later, on the renamed parser.
|
|
1979
|
-
const JSON_PARSE_ANCHOR = /export const __JSON_parse = \(_: (bytestring|any)\) => \{\n/;
|
|
1980
|
-
const JSON_REVIVER_SOURCE = `
|
|
1981
|
-
// ${JSON_REVIVER_MARKER}
|
|
1982
|
-
export const __Porffor_json_revive = (holder: any, key: any, reviver: any): any => {
|
|
1983
|
-
const value: any = holder[key];
|
|
1984
|
-
if (Porffor.type(value) == Porffor.TYPES.array) {
|
|
1985
|
-
const len: i32 = value.length;
|
|
1986
|
-
for (let i: i32 = 0; i < len; i++) {
|
|
1987
|
-
const next: any = __Porffor_json_revive(value, ecma262.ToString(i), reviver);
|
|
1988
|
-
if (next === undefined) delete value[i];
|
|
1989
|
-
else value[i] = next;
|
|
1990
|
-
}
|
|
1991
|
-
} else if (Porffor.type(value) == Porffor.TYPES.object) {
|
|
1992
|
-
for (const k: any in value) {
|
|
1993
|
-
const next: any = __Porffor_json_revive(value, k, reviver);
|
|
1994
|
-
if (next === undefined) delete value[k];
|
|
1995
|
-
else value[k] = next;
|
|
1996
|
-
}
|
|
1997
|
-
}
|
|
1998
|
-
return reviver.call(holder, key, value);
|
|
1999
|
-
};
|
|
2000
|
-
|
|
2001
|
-
export const __JSON_parse = (text: any, reviver: any) => {
|
|
2002
|
-
const value: any = __Porffor_json_parseText(text);
|
|
2003
|
-
if (Porffor.type(reviver) != Porffor.TYPES.function) return value;
|
|
2004
|
-
const root: object = {};
|
|
2005
|
-
root[''] = value;
|
|
2006
|
-
return __Porffor_json_revive(root, '', reviver);
|
|
2007
|
-
};
|
|
2008
|
-
`;
|
|
2009
|
-
|
|
2010
|
-
/** JSON.parse applies its reviver, as InternalizeJSONProperty does. */
|
|
2011
|
-
export async function patchJsonReviver(root: string): Promise<void> {
|
|
2012
|
-
const file = resolve(root, "compiler/builtins/json.ts");
|
|
2013
|
-
const src = await readFile(file, "utf8");
|
|
2014
|
-
if (src.includes(JSON_REVIVER_MARKER)) return;
|
|
2015
|
-
if ((src.match(new RegExp(JSON_PARSE_ANCHOR, "g")) ?? []).length !== 1)
|
|
2016
|
-
throw new Error(`could not patch Porffor's JSON.parse reviver: anchor not found in ${file}`);
|
|
2017
|
-
await writeFile(
|
|
2018
|
-
file,
|
|
2019
|
-
src.replace(JSON_PARSE_ANCHOR, (_line, type) => `export const __Porffor_json_parseText = (_: ${type}) => {\n`).trimEnd() + "\n" + JSON_REVIVER_SOURCE,
|
|
2020
|
-
);
|
|
2021
|
-
}
|
|
2022
|
-
|
|
2023
|
-
// Object.prototype.toString gave "[object Object]" for typed arrays: their lazily
|
|
2024
|
-
// built prototypes have no toStringTag, and the fallback list of builtin tags left
|
|
2025
|
-
// them out (W2 conformance; V8 gives "[object Uint8Array]").
|
|
2026
|
-
const TA_TAG_MARKER = "sproutboat: typed array toStringTag";
|
|
2027
|
-
const TA_TAG_ANCHOR = " return '[object ' + builtinTag + ']';\n};\n";
|
|
2028
|
-
const TA_TAG_INJECT =
|
|
2029
|
-
" // sproutboat: typed array toStringTag\n" +
|
|
2030
|
-
" if (Porffor.fastAnd(tag === undefined, t >= Porffor.TYPES.uint8clampedarray, t <= Porffor.TYPES.float64array)) {\n" +
|
|
2031
|
-
" if (t == Porffor.TYPES.uint8clampedarray) builtinTag = 'Uint8ClampedArray';\n else if (t == Porffor.TYPES.uint8array) builtinTag = 'Uint8Array';\n else if (t == Porffor.TYPES.int8array) builtinTag = 'Int8Array';\n else if (t == Porffor.TYPES.uint16array) builtinTag = 'Uint16Array';\n else if (t == Porffor.TYPES.int16array) builtinTag = 'Int16Array';\n else if (t == Porffor.TYPES.uint32array) builtinTag = 'Uint32Array';\n else if (t == Porffor.TYPES.int32array) builtinTag = 'Int32Array';\n else if (t == Porffor.TYPES.biguint64array) builtinTag = 'BigUint64Array';\n else if (t == Porffor.TYPES.bigint64array) builtinTag = 'BigInt64Array';\n else if (t == Porffor.TYPES.float32array) builtinTag = 'Float32Array';\n else if (t == Porffor.TYPES.float64array) builtinTag = 'Float64Array';\n" +
|
|
2032
|
-
" }\n" +
|
|
2033
|
-
TA_TAG_ANCHOR;
|
|
2034
|
-
|
|
2035
|
-
/** Typed arrays get their own toString tag. Before the precompile. */
|
|
2036
|
-
export async function patchTypedArrayTag(root: string): Promise<void> {
|
|
2037
|
-
const file = resolve(root, "compiler/builtins/object.ts");
|
|
2038
|
-
const src = await readFile(file, "utf8");
|
|
2039
|
-
if (src.includes(TA_TAG_MARKER)) return;
|
|
2040
|
-
if (src.split(TA_TAG_ANCHOR).length !== 2)
|
|
2041
|
-
throw new Error(`could not patch Porffor's typed array toStringTag: anchor not found in ${file}`);
|
|
2042
|
-
await writeFile(file, src.replace(TA_TAG_ANCHOR, TA_TAG_INJECT));
|
|
2043
|
-
}
|
|
2044
|
-
|
|
2045
1755
|
// console.log coloured numbers, booleans and objects with ANSI escapes even when
|
|
2046
1756
|
// stdout was a file or pipe, so sproutd's per-app logs (B21) and piped sprout output
|
|
2047
1757
|
// were full of \x1b[33m. Colour only when stdout is a terminal, as Node and Bun do.
|
|
@@ -2215,6 +1925,36 @@ export async function patchSplitGlobals(root: string): Promise<void> {
|
|
|
2215
1925
|
await writeFile(file, src);
|
|
2216
1926
|
}
|
|
2217
1927
|
|
|
1928
|
+
// The conservative scanner must recognize the encoded f64 form used by BigInt builtins.
|
|
1929
|
+
const BIGINT_GC_ANCHOR = "static void porf_gc_cons_candidate(u32 c);\n";
|
|
1930
|
+
const BIGINT_GC_HELPER = `static void porf_gc_cons_f64(f64 d) {
|
|
1931
|
+
// Heap BigInts encode their offset above 2^51, including in f64 builtin arguments.
|
|
1932
|
+
if (d >= 2251799813685248.0 && d < 2251804108652544.0) d -= 2251799813685248.0;
|
|
1933
|
+
if (d > 0.0 && d < 4294967296.0) {
|
|
1934
|
+
const u32 ptr = (u32)d;
|
|
1935
|
+
if ((f64)ptr == d) porf_gc_cons_candidate(ptr);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
`;
|
|
1939
|
+
const BIGINT_GC_SCAN = ` if (d > 0.0 && d < 4294967296.0) {
|
|
1940
|
+
const i64 iv = (i64)d;
|
|
1941
|
+
if ((f64)iv == d) porf_gc_cons_candidate((u32)(u64)iv);
|
|
1942
|
+
}`;
|
|
1943
|
+
|
|
1944
|
+
export async function patchBigIntRoots(root: string): Promise<void> {
|
|
1945
|
+
const file = resolve(root, "compiler/render.js");
|
|
1946
|
+
const src = await readFile(file, "utf8");
|
|
1947
|
+
if (src.includes(BIGINT_GC_HELPER) && src.split(" porf_gc_cons_f64(d);").length === 3) return;
|
|
1948
|
+
if (src.split(BIGINT_GC_ANCHOR).length !== 2 || src.split(BIGINT_GC_SCAN).length !== 3)
|
|
1949
|
+
throw new Error(`could not patch Porffor's BigInt GC roots: anchor not found in ${file}`);
|
|
1950
|
+
await writeFile(
|
|
1951
|
+
file,
|
|
1952
|
+
src
|
|
1953
|
+
.replace(BIGINT_GC_ANCHOR, BIGINT_GC_ANCHOR + BIGINT_GC_HELPER)
|
|
1954
|
+
.replaceAll(BIGINT_GC_SCAN, " porf_gc_cons_f64(d);"),
|
|
1955
|
+
);
|
|
1956
|
+
}
|
|
1957
|
+
|
|
2218
1958
|
const FETCH_CLASSES_MARKER = "sproutboat: Fetch-spec Request and Response";
|
|
2219
1959
|
/** Replaces Porffor's Request and Response (fetch-classes.ts); covers patchResponseDefaults. */
|
|
2220
1960
|
export async function patchFetchClasses(root: string): Promise<void> {
|
|
@@ -2313,15 +2053,15 @@ export async function ensurePorfforPatched(root: string): Promise<void> {
|
|
|
2313
2053
|
await patchClassSelfName(root);
|
|
2314
2054
|
await patchUwebsockets(root);
|
|
2315
2055
|
await patchRenderJs(root);
|
|
2056
|
+
await patchBigIntRoots(root);
|
|
2316
2057
|
await patchStreamingResponses(root);
|
|
2317
2058
|
await patchQuietCollection(root);
|
|
2318
2059
|
await patchAsyncWaker(root);
|
|
2319
2060
|
await patchDeferredDiscard(root);
|
|
2320
|
-
await patchDateParser(root);
|
|
2321
2061
|
await patchPromiseResolveNull(root);
|
|
2322
2062
|
await patchReplaceAll(root);
|
|
2323
2063
|
await patchLocalFunctionTypes(root);
|
|
2324
|
-
await
|
|
2064
|
+
await patchFunctionSourcesScan(root);
|
|
2325
2065
|
await patchResponseDefaults(root);
|
|
2326
2066
|
await patchHeadersArrayInit(root);
|
|
2327
2067
|
await patchHeadersClass(root);
|
|
@@ -2330,18 +2070,13 @@ export async function ensurePorfforPatched(root: string): Promise<void> {
|
|
|
2330
2070
|
await patchResponseBytes(root);
|
|
2331
2071
|
await patchURLClass(root);
|
|
2332
2072
|
await patchTextDecoder(root);
|
|
2333
|
-
await patchMapGroupBy(root);
|
|
2334
|
-
await patchPrivateFieldsHidden(root);
|
|
2335
2073
|
await patchRepeatDoubling(root);
|
|
2336
|
-
await patchJsonCycles(root);
|
|
2337
|
-
await patchJsonReviver(root);
|
|
2338
|
-
await patchTypedArrayTag(root);
|
|
2339
2074
|
await patchConsoleColors(root);
|
|
2340
2075
|
await patchCaseMapping(root);
|
|
2341
2076
|
await patchZeroDelayTimers(root);
|
|
2342
2077
|
await patchSplitGlobals(root);
|
|
2343
|
-
// Last of the builtin edits: its precompile picks up the
|
|
2344
|
-
//
|
|
2078
|
+
// Last of the builtin edits: its precompile picks up the promise, replaceAll,
|
|
2079
|
+
// local function and repeat patches above too.
|
|
2345
2080
|
await precompileBuiltins(root);
|
|
2346
2081
|
await patchUnicode(root);
|
|
2347
2082
|
done.add(root);
|
package/src/pin.ts
CHANGED
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
* uWebSockets archive in `sproutboat-cli/vendor/` β then run both repos'
|
|
12
12
|
* test + conformance suites. See sproutboat-cli/MIGRATION.md.
|
|
13
13
|
*/
|
|
14
|
-
export const PORFFOR_CHANNEL = "alpha-
|
|
15
|
-
export const PORFFOR_COMMIT_FULL = "
|
|
14
|
+
export const PORFFOR_CHANNEL = "alpha-19";
|
|
15
|
+
export const PORFFOR_COMMIT_FULL = "0c135ce2f36fdd3467d5a58daa92283810705a51";
|
|
16
16
|
export const PORFFOR_COMMIT = PORFFOR_COMMIT_FULL.slice(0, 7);
|
|
17
17
|
export const PORFFOR_ARCHIVE_SHA256 =
|
|
18
|
-
"
|
|
18
|
+
"b54da2297ad9e03c5bbac0e0d156f1179ac3a09a2a6fd702475cef605bab97b9";
|
|
19
19
|
export const PORFFOR_ARCHIVE_URL = `https://codeload.github.com/CanadaHonk/porffor/tar.gz/${PORFFOR_COMMIT_FULL}`;
|
|
20
20
|
|
|
21
21
|
/** A compact identity string for a manifest / report (`alpha-17 (731f1b2)`). */
|
|
@@ -52,7 +52,10 @@ test("native Unicode JSON and UTF-8 encoding preserve code points and buffer bou
|
|
|
52
52
|
}
|
|
53
53
|
const lone = JSON.parse(JSON.stringify({ value: String.fromCharCode(0xd800) }));
|
|
54
54
|
const quotedKey = JSON.parse(JSON.stringify({ ['quote"key']: "π€" }));
|
|
55
|
-
|
|
55
|
+
// The reviver sees each primitive's source text, sliced from UTF-16 input.
|
|
56
|
+
const sources = [];
|
|
57
|
+
JSON.parse('{"ζ±δΊ¬":{"id":"π€","n":1.25},"l":[-3e2,"Γ©"]}', (k, v, ctx) => { sources.push([k, ctx.source ?? null]); return v; });
|
|
58
|
+
return Response.json({ encoded, into, nested, escaped, growth, negatives, lone, quotedKey, sources });
|
|
56
59
|
} };`;
|
|
57
60
|
const [prelude, transport] = await Promise.all([
|
|
58
61
|
readFile(preludePath, "utf8"),
|
|
@@ -103,6 +106,7 @@ test("native Unicode JSON and UTF-8 encoding preserve code points and buffer bou
|
|
|
103
106
|
expect(actual.lone).toEqual({ value: "\ud800" });
|
|
104
107
|
expect(actual.quotedKey).toEqual({ 'quote"key': "π€" });
|
|
105
108
|
expect(actual.negatives).toEqual([true, true, true, true]);
|
|
109
|
+
expect(actual.sources).toEqual([["id", '"π€"'], ["n", "1.25"], ["ζ±δΊ¬", null], ["0", "-3e2"], ["1", '"Γ©"'], ["l", null], ["", null]]);
|
|
106
110
|
} finally {
|
|
107
111
|
if (child) {
|
|
108
112
|
child.kill(9);
|
package/src/unicode.ts
CHANGED
|
@@ -42,19 +42,16 @@ export const __Porffor_json_appendChar = (out: string, char: i32): void => {
|
|
|
42
42
|
};
|
|
43
43
|
` +
|
|
44
44
|
source.slice(end);
|
|
45
|
+
// Every parser helper and JSON.parse's own `const text` (Alpha 19 types the argument any).
|
|
45
46
|
source = replace(source, "text: bytestring", "text: any");
|
|
46
|
-
source = replace(source, "(_: bytestring)", "(_: any)");
|
|
47
47
|
source = replace(
|
|
48
48
|
source,
|
|
49
49
|
"const out: bytestring = Porffor.malloc(6 + (strEnd - pos));",
|
|
50
50
|
"const out: string = Porffor.malloc(4 + (strEnd - pos) * 2);",
|
|
51
51
|
);
|
|
52
52
|
source = replace(source, "Porffor.bytestring.appendChar(out,", "__Porffor_json_appendChar(out,");
|
|
53
|
-
source
|
|
54
|
-
|
|
55
|
-
"Porffor.callThis(__ByteString_prototype_slice, text, start, pos)",
|
|
56
|
-
"text.slice(start, pos)",
|
|
57
|
-
);
|
|
53
|
+
// Number literals and, since Alpha 19, the reviver's source text records.
|
|
54
|
+
source = replace(source, "Porffor.callThis(__ByteString_prototype_slice, text, start, ", "text.slice(start, ");
|
|
58
55
|
source = replace(
|
|
59
56
|
source,
|
|
60
57
|
"let buffer: bytestring = Porffor.malloc(6 + cap);",
|
|
@@ -97,14 +94,6 @@ export const __Porffor_json_appendChar = (out: string, char: i32): void => {
|
|
|
97
94
|
}
|
|
98
95
|
buffer = __Porffor_bytestring_bufferChar(buffer, c);`,
|
|
99
96
|
);
|
|
100
|
-
// JSON.parse must reject trailing non-whitespace, not just parse a prefix.
|
|
101
|
-
source = replace(
|
|
102
|
-
source,
|
|
103
|
-
"return __Porffor_json_parseValue(_, posPtr, _.length);",
|
|
104
|
-
`const result: any = __Porffor_json_parseValue(_, posPtr, _.length);
|
|
105
|
-
if (__Porffor_json_skipWhitespace(_, Porffor.IR.loadI32(posPtr, 0), _.length) != _.length) throw new SyntaxError('Unexpected trailing JSON input');
|
|
106
|
-
return result;`,
|
|
107
|
-
);
|
|
108
97
|
return source;
|
|
109
98
|
}
|
|
110
99
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Native GC scanner regression: bit 0 is the stack root, bit 1 the untyped block.
|
|
2
|
+
// Bit 2 is set if a value just outside the heap-BigInt encoding marks the allocation.
|
|
3
|
+
function probe() {
|
|
4
|
+
let stackMarked = 0;
|
|
5
|
+
let blockMarked = 0;
|
|
6
|
+
let falseMarked = 0;
|
|
7
|
+
Porffor.c`
|
|
8
|
+
u32 bigint = porf_alloc(16, 4);
|
|
9
|
+
u32 holder = porf_alloc(32, 0);
|
|
10
|
+
for (i32 ci = 0; ci < PORF_GC_NCLASSES; ci++) porf_gc_publish_window(ci);
|
|
11
|
+
const u32 gran = porf_gc_gran(bigint);
|
|
12
|
+
volatile f64 encoded = 2251799813685248.0 + (f64)bigint;
|
|
13
|
+
volatile f64 outside[4] = {
|
|
14
|
+
2251799813685248.0 + (f64)bigint + 0.5,
|
|
15
|
+
2251804108652544.0 + (f64)bigint,
|
|
16
|
+
-(2251799813685248.0 + (f64)bigint),
|
|
17
|
+
2251799813685248.0
|
|
18
|
+
};
|
|
19
|
+
for (i32 i = 0; i < 4; i++) {
|
|
20
|
+
porf_gc_bit_clear(PORF_GC_B_MARK, gran);
|
|
21
|
+
f64 d = outside[i];
|
|
22
|
+
u64 bits;
|
|
23
|
+
memcpy(&bits, &d, 8);
|
|
24
|
+
// Only the f64 decoding is under test: skip values whose 32-bit halves alias the block.
|
|
25
|
+
if (porf_gc_gran((u32)bits) == gran || porf_gc_gran((u32)(bits >> 32)) == gran) continue;
|
|
26
|
+
porf_gc_cons_scan_range((const u64*)&outside[i], (const u64*)&outside[i] + 1);
|
|
27
|
+
if (porf_gc_bit(PORF_GC_B_MARK, gran) != 0u) falseMarked = 1;
|
|
28
|
+
}
|
|
29
|
+
porf_gc_bit_clear(PORF_GC_B_MARK, gran);
|
|
30
|
+
porf_gc_cons_scan_range((const u64*)&encoded, (const u64*)((const char*)&encoded + sizeof(encoded)));
|
|
31
|
+
stackMarked = porf_gc_bit(PORF_GC_B_MARK, gran) != 0u;
|
|
32
|
+
porf_gc_bit_clear(PORF_GC_B_MARK, gran);
|
|
33
|
+
*(u64*)(MEM + holder) = 0xffffffffffffffffull;
|
|
34
|
+
*(f64*)(MEM + holder + 8) = encoded;
|
|
35
|
+
porf_gc_bit_clear(PORF_GC_B_MARK, porf_gc_gran(holder));
|
|
36
|
+
porf_gc_kinds[porf_gc_gran(holder)] = 0;
|
|
37
|
+
porf_gc_cons_mark_block((i32)holder);
|
|
38
|
+
blockMarked = porf_gc_bit(PORF_GC_B_MARK, gran) != 0u;
|
|
39
|
+
`;
|
|
40
|
+
return stackMarked + blockMarked * 2 + falseMarked * 4;
|
|
41
|
+
}
|
|
42
|
+
const marked = probe();
|
|
43
|
+
console.log(marked);
|
|
44
|
+
if (marked !== 3) throw new Error("heap BigInt was not marked, or a value outside its encoding was");
|
|
Binary file
|
|
Binary file
|