@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 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.11.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": {
@@ -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 let n: number = 0;\n let nInd: number = 0;\n\n const len: i32 = string.length;\n const endPtr: i32 = Porffor.IR.ptr(string) + len;\n let ptr: i32 = Porffor.IR.ptr(string);\n\n while (ptr <= endPtr) { // <= to include extra null byte to set last n\n const chr: i32 = Porffor.IR.loadU8(ptr++, 4);\n if (Porffor.fastAnd(chr >= 48, chr <= 57)) { // 0-9\n n *= 10;\n n += chr - 48;\n continue;\n }\n\n if (chr == 45) { // -\n if (Porffor.fastOr(ptr == Porffor.IR.ptr(string), nInd == 7)) n = -n;\n }\n\n if (n > 0) {\n if (nInd == 0) y = n;\n else if (nInd == 1) m = n - 1;\n else if (nInd == 2) dt = n;\n else if (nInd == 3) h = n;\n else if (nInd == 4) min = n;\n else if (nInd == 5) s = n;\n else if (nInd == 6) milli = n;\n else if (nInd == 7) tzHour = n;\n else if (nInd == 8) tzMin = n;\n\n n = 0;\n nInd++;\n }\n }\n\n h += tzHour;\n min += tzMin;\n};\n// RFC 7231 or Date.prototype.toString() parser\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
- " builtinCall(func, '__Porffor_object_class_value', [\n" +
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.stringify throws on cycles; typed arrays have their own toString tag.
302
- expect(await readFile(join(dir, "compiler/builtins/json.ts"), "utf8")).toContain("sproutboat: JSON cycle check");
303
- expect(await readFile(join(dir, "compiler/builtins/object.ts"), "utf8")).toContain("sproutboat: typed array toStringTag");
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-deda494.tar.gz" with { type: "file" };
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);
@@ -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 = "66de5cdac773";
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 {
@@ -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-deda494.tar.gz"),
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("base64 patch preserves option tags and truncates full chunks, idempotently", async () => {
369
- const root = await mkdtemp(join(tmpdir(), "sb-base64-patch-"));
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/builtins"), { recursive: true });
372
- const file = join(root, "compiler/builtins/uint8array_base64.ts");
373
- await writeFile(
374
- file,
375
- "let alphabet: string = 'base64';\nlet lastChunkHandling: string = 'loose';\nconst fullChunks: i32 = (len / 3) * 3;",
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).toContain("let alphabet: any =");
380
- expect(patched).toContain("let lastChunkHandling: any =");
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(), ...at },
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
- /** Keep base64 option tags accurate and truncate the complete-chunk count. */
1505
- export async function patchUint8ArrayBase64(root: string): Promise<void> {
1506
- const file = resolve(root, "compiler/builtins/uint8array_base64.ts");
1507
- let src = await readFile(file, "utf8");
1508
- const marker = "sproutboat: base64 option tags and complete chunks";
1509
- if (src.includes(marker)) return;
1510
- const edits = [
1511
- ["let alphabet: string =", "let alphabet: any ="],
1512
- ["let lastChunkHandling: string =", "let lastChunkHandling: any ="],
1513
- [
1514
- "const fullChunks: i32 = (len / 3) * 3;",
1515
- "const fullChunks: i32 = ((len / 3) | 0) * 3;",
1516
- ],
1517
- ] as const;
1518
- for (const [anchor, replacement] of edits) {
1519
- if (!src.includes(anchor))
1520
- throw new Error(
1521
- `could not patch Porffor base64: anchor not found in ${file}`,
1522
- );
1523
- src = src.replaceAll(anchor, replacement);
1524
- }
1525
- await writeFile(file, `// ${marker}\n${src}`);
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 patchUint8ArrayBase64(root);
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 date, promise and
2344
- // replaceAll patches above too.
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-18";
15
- export const PORFFOR_COMMIT_FULL = "deda4945a526d3974ff79e499e8c1dcef0873f3a";
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
- "4626ffba0d5bd417ba2c5b96506d40a152dbbeeaa8a698c904054bdef6bc6e64";
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
- return Response.json({ encoded, into, nested, escaped, growth, negatives, lone, quotedKey });
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 = replace(
54
- source,
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