@sproutboat/toolchain 0.10.2 β 0.12.0
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 +26 -0
- package/PORFFOR_ALPHA19.md +72 -0
- package/package.json +2 -2
- package/src/acquire.test.ts +18 -16
- package/src/acquire.ts +1 -1
- package/src/fingerprint.ts +1 -1
- package/src/idle-gc.test.ts +1 -1
- package/src/patch.test.ts +38 -101
- package/src/patch.ts +67 -329
- package/src/pin.ts +3 -3
- package/src/unicode-native.test.ts +5 -1
- package/src/unicode.ts +3 -14
- package/vendor/porffor-0c135ce.tar.gz +0 -0
- package/vendor/porffor-deda494.tar.gz +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @sproutboat/toolchain
|
|
2
2
|
|
|
3
|
+
## 0.12.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
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.
|
|
12
|
+
|
|
13
|
+
## 0.11.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- 85aa74d: Service-binding calls and outbound `fetch()` no longer block a deployed sprout. In a deployed sprout, each call runs off the event loop on one I/O thread, so the sprout keeps answering other requests while a call is in flight. Concurrent calls overlap: eight 500 ms calls finish in about 510 ms, where they used to queue for about 4 s. A deployment can also call itself through a binding, to any depth, where this used to deadlock. The toolchain installs the loop hook the runtime uses for this; with an older runtime it does nothing.
|
|
18
|
+
|
|
19
|
+
`env.SERVICE.fetch()` now always returns a promise, as in Workers, and reads its arguments as `fetch()` does:
|
|
20
|
+
|
|
21
|
+
- a `URL` object works as input;
|
|
22
|
+
- `fetch(request, init)` keeps the request's body when `init` sets no body of its own;
|
|
23
|
+
- a `ReadableStream` body is sent whole, where it used to arrive as the text `[object ReadableStream]`.
|
|
24
|
+
|
|
25
|
+
Responses from bindings and from `fetch()` keep every `Set-Cookie` header, where they used to keep only the last one.
|
|
26
|
+
|
|
27
|
+
A broker exchange is now resent only when the broker cannot have acted on it: the request never fully left, or the connection had gone stale. A `fetch` or service call that the broker may already have run is no longer repeated.
|
|
28
|
+
|
|
3
29
|
## 0.10.2
|
|
4
30
|
|
|
5
31
|
### Patch 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.0",
|
|
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
|
@@ -63,7 +63,7 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
|
|
|
63
63
|
"const path = require('node:path');\n" +
|
|
64
64
|
"fs.writeFileSync(path.join(__dirname, 'builtins_precompiled.js'), fs.readFileSync(path.join(__dirname, 'builtins/typedarray.js')));\n",
|
|
65
65
|
"compiler/builtins_precompiled.js": "fixture:precompiled\n",
|
|
66
|
-
"compiler/builtins/date.ts": "export const __ecma262_ParseDTSF = (string: string) => {\n
|
|
66
|
+
"compiler/builtins/date.ts": "export const __ecma262_ParseDTSF = (string: string) => {\n};\n",
|
|
67
67
|
"compiler/builtins/typedarray.js":
|
|
68
68
|
"${typedArrayFuncs.reduce((acc, x) => acc + x.replace('// @porf-typed-array\\n', '').replaceAll('Array', name).replaceAll('any[]', name) + '\\n\\n', '')}`;\n" +
|
|
69
69
|
" offset = Math.trunc(offset);\n if (Porffor.fastOr(offset < 0, offset > len)) throw new RangeError('Offset out of bounds');\n" +
|
|
@@ -147,12 +147,10 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
|
|
|
147
147
|
" }\n" +
|
|
148
148
|
"\n" +
|
|
149
149
|
" if (body_owned) free(body_owned);\n" +
|
|
150
|
-
"}\n"
|
|
150
|
+
"}\n" +
|
|
151
|
+
"int main(void) {\n porf_native_fetch_runtime_init();\n porf_native_fetch_static_init();\n\n return 0;\n}\n",
|
|
151
152
|
"compiler/builtins/json.ts":
|
|
152
|
-
"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" +
|
|
153
153
|
"// sb_json_utf16_v1\n" +
|
|
154
|
-
// patchJsonReviver's anchor.
|
|
155
|
-
"export const __JSON_parse = (_: bytestring) => {\n};\n" +
|
|
156
154
|
"export const __Porffor_json_serialize = (_buffer: i32, value: any): i32 => {\n" +
|
|
157
155
|
" for (const x of (value as any[])) {\n }\n" +
|
|
158
156
|
" const val: any = value[key];\n" +
|
|
@@ -167,14 +165,10 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
|
|
|
167
165
|
"}\nfunction setTimeout() {}\nclass TextDecoder {\n}\n" +
|
|
168
166
|
"class URL {\n constructor(input) {\n this.href = '';\n }\n}\n\nclass Request {\n}\n" +
|
|
169
167
|
"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",
|
|
170
|
-
"compiler/builtins/uint8array_base64.ts": "let alphabet: string = 'base64';\nlet lastChunkHandling: string = 'loose';\nconst fullChunks: i32 = (len / 3) * 3;\n",
|
|
171
|
-
"compiler/builtins/object.ts": " return '[object ' + builtinTag + ']';\n};\n",
|
|
172
168
|
"compiler/builtins/console.ts": "export const __Porffor_consolePrint = (arg: any): void => {\n __Porffor_print(arg);\n};\n",
|
|
173
|
-
"compiler/builtins/map.ts": "export const Map = function (iterable: any): Map {\n};\n",
|
|
174
169
|
"compiler/codegen.js":
|
|
175
170
|
"if (!hoistRead && directCallOnlyFunctionBinding(scope, kind, name, pattern, func)) {\n return valUndefined();\n}\nconst func = resolveNamedFunction(scope, name) ?? funcByName(name);\n" +
|
|
176
|
-
"
|
|
177
|
-
" if (kind === 'get' || kind === 'set') initKind = kind;\n" +
|
|
171
|
+
" // 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" +
|
|
178
172
|
" stmt(scope, Store(ctype, addr, 4, ctype === 'f64' || ctype === 'f32' ? f : toUint32(scope, f)));\n" +
|
|
179
173
|
" const taGet = (ctype, size, signed = true) => () => {\n" +
|
|
180
174
|
" const loaded = Load(ctype, taAddr(size), 4);\n" +
|
|
@@ -297,16 +291,15 @@ test("the default call (no url/expectedSha256 override) never touches the networ
|
|
|
297
291
|
expect(await readFile(join(dir, "compiler/uwebsockets.js"), "utf8")).toContain("sproutboat B19: streamed responses");
|
|
298
292
|
// console colours only on a terminal.
|
|
299
293
|
expect(await readFile(join(dir, "compiler/builtins/console.ts"), "utf8")).toContain("__Porffor_consoleColors");
|
|
300
|
-
// JSON
|
|
301
|
-
|
|
302
|
-
|
|
294
|
+
// JSON cycles, the JSON.parse reviver, typed-array toString tags, ISO date offsets,
|
|
295
|
+
// base64 options, Map.groupBy and non-enumerable private fields (#286) are upstream's
|
|
296
|
+
// own since alpha-19; json-cycles, groupby-private, date and base64 native tests cover them.
|
|
297
|
+
// Alpha 19 new Function specialization checks a function body before walking the program.
|
|
298
|
+
expect(await readFile(join(dir, "compiler/codegen.js"), "utf8")).toContain("sproutboat: body check before the whole-program walks");
|
|
303
299
|
// B16: String.prototype.repeat doubles its copies.
|
|
304
300
|
expect(await readFile(join(dir, "compiler/builtins/string.ts"), "utf8")).toContain("sproutboat B16: doubling repeat");
|
|
305
301
|
// #287: full collections under load keep their free pages; the idle collection returns them.
|
|
306
302
|
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;");
|
|
307
|
-
// #286: Map.groupBy, and private fields installed as non-enumerable.
|
|
308
|
-
expect(await readFile(join(dir, "compiler/builtins/map.ts"), "utf8")).toContain("export const __Map_groupBy");
|
|
309
|
-
expect(await readFile(join(dir, "compiler/codegen.js"), "utf8")).toContain("sproutboat #286: private fields");
|
|
310
303
|
// #256: named class expressions inside functions are rewritten at parse time.
|
|
311
304
|
expect(await readFile(join(dir, "compiler/parse.js"), "utf8")).toContain("sbClassSelf(ast); // sproutboat #256");
|
|
312
305
|
expect(await readFile(join(dir, "compiler/sb-class-self.js"), "utf8")).toContain("sproutboat #256");
|
|
@@ -420,3 +413,12 @@ test("download failures are classified without publishing partial state", async
|
|
|
420
413
|
expect(attempts).toBe(2);
|
|
421
414
|
expect(await readdir(cacheRoot)).toEqual([]);
|
|
422
415
|
});
|
|
416
|
+
|
|
417
|
+
test("the vendored archive is the one PORFFOR_ARCHIVE_URL serves: its root is porffor-<full commit>", () => {
|
|
418
|
+
// codeload names the root after the ref in the URL, so an archive fetched by the short
|
|
419
|
+
// commit has another root and digest than the pinned URL's, though the same files.
|
|
420
|
+
const archive = join(import.meta.dir, `../vendor/porffor-${PORFFOR_COMMIT_FULL.slice(0, 7)}.tar.gz`);
|
|
421
|
+
const list = Bun.spawnSync(["tar", "-tzf", archive], { stdout: "pipe" });
|
|
422
|
+
expect(list.exitCode).toBe(0);
|
|
423
|
+
expect(list.stdout.toString().split("\n")[0]).toBe(`porffor-${PORFFOR_COMMIT_FULL}/`);
|
|
424
|
+
});
|
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
|
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 = "7e12e7bba967";
|
|
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,16 +6,14 @@ 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,
|
|
18
15
|
patchStreamingResponses,
|
|
16
|
+
patchAsyncWaker,
|
|
19
17
|
patchSplitGlobals,
|
|
20
18
|
patchBlobClass,
|
|
21
19
|
patchFetchClasses,
|
|
@@ -24,7 +22,6 @@ import {
|
|
|
24
22
|
patchTextDecoder,
|
|
25
23
|
patchResponseDefaults,
|
|
26
24
|
patchURLClass,
|
|
27
|
-
patchUint8ArrayBase64,
|
|
28
25
|
patchRenderJs,
|
|
29
26
|
patchUwebsockets,
|
|
30
27
|
} from "./patch";
|
|
@@ -364,22 +361,21 @@ test("local function type patch registers lexical bindings and is idempotent", a
|
|
|
364
361
|
}
|
|
365
362
|
});
|
|
366
363
|
|
|
367
|
-
test("
|
|
368
|
-
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-"));
|
|
369
366
|
try {
|
|
370
|
-
await mkdir(join(root, "compiler
|
|
371
|
-
const file = join(root, "compiler/
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
);
|
|
376
|
-
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);
|
|
377
373
|
const patched = await readFile(file, "utf8");
|
|
378
|
-
expect(patched
|
|
379
|
-
|
|
380
|
-
expect(patched).toContain("((len / 3) | 0) * 3");
|
|
381
|
-
await patchUint8ArrayBase64(root);
|
|
374
|
+
expect(patched.indexOf("n.callee.name === 'Function').length === 0) continue;")).toBeLessThan(patched.indexOf("find(program,"));
|
|
375
|
+
await patchFunctionSourcesScan(root);
|
|
382
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");
|
|
383
379
|
} finally {
|
|
384
380
|
await rm(root, { recursive: true, force: true });
|
|
385
381
|
}
|
|
@@ -456,47 +452,6 @@ test("URL patch replaces Porffor's URL class, idempotently", async () => {
|
|
|
456
452
|
});
|
|
457
453
|
|
|
458
454
|
|
|
459
|
-
test("Map.groupBy patch appends the builtin once and refuses an upstream one", async () => {
|
|
460
|
-
const root = await mkdtemp(join(tmpdir(), "sb-map-groupby-"));
|
|
461
|
-
try {
|
|
462
|
-
await mkdir(join(root, "compiler/builtins"), { recursive: true });
|
|
463
|
-
const file = join(root, "compiler/builtins/map.ts");
|
|
464
|
-
await writeFile(file, "export const Map = function (iterable: any): Map {\n};\n");
|
|
465
|
-
await patchMapGroupBy(root);
|
|
466
|
-
const patched = await readFile(file, "utf8");
|
|
467
|
-
expect(patched).toContain("export const __Map_groupBy = (items: any, callbackFn: any): Map => {");
|
|
468
|
-
await patchMapGroupBy(root);
|
|
469
|
-
expect(await readFile(file, "utf8")).toBe(patched);
|
|
470
|
-
await writeFile(file, "export const Map = function (iterable: any): Map {\n};\nexport const __Map_groupBy = 1;\n");
|
|
471
|
-
await expect(patchMapGroupBy(root)).rejects.toThrow("drop patchMapGroupBy");
|
|
472
|
-
} finally {
|
|
473
|
-
await rm(root, { recursive: true, force: true });
|
|
474
|
-
}
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
test("private field patch installs instance and static private fields as methods, idempotently", async () => {
|
|
478
|
-
const root = await mkdtemp(join(tmpdir(), "sb-private-fields-"));
|
|
479
|
-
try {
|
|
480
|
-
await mkdir(join(root, "compiler"));
|
|
481
|
-
const file = join(root, "compiler/codegen.js");
|
|
482
|
-
await writeFile(
|
|
483
|
-
file,
|
|
484
|
-
" builtinCall(func, '__Porffor_object_class_value', [\n" +
|
|
485
|
-
" if (kind === 'get' || kind === 'set') initKind = kind;\n",
|
|
486
|
-
);
|
|
487
|
-
await patchPrivateFieldsHidden(root);
|
|
488
|
-
const patched = await readFile(file, "utf8");
|
|
489
|
-
expect(patched).toContain("x.key?.type === 'PrivateIdentifier' ? '__Porffor_object_class_method' : '__Porffor_object_class_value'");
|
|
490
|
-
expect(patched).toContain("if (initKind === 'value' && x.key?.type === 'PrivateIdentifier') initKind = 'method';");
|
|
491
|
-
await patchPrivateFieldsHidden(root);
|
|
492
|
-
expect(await readFile(file, "utf8")).toBe(patched);
|
|
493
|
-
await writeFile(file, "export const x = 1;\n");
|
|
494
|
-
await expect(patchPrivateFieldsHidden(root)).rejects.toThrow("anchor not found");
|
|
495
|
-
} finally {
|
|
496
|
-
await rm(root, { recursive: true, force: true });
|
|
497
|
-
}
|
|
498
|
-
});
|
|
499
|
-
|
|
500
455
|
test("deferred discard patch moves page discard to the idle collection, idempotently", async () => {
|
|
501
456
|
const root = await mkdtemp(join(tmpdir(), "sb-deferred-discard-"));
|
|
502
457
|
try {
|
|
@@ -571,48 +526,6 @@ test("ensurePorfforPatched trusts a cache entry this toolchain version and patch
|
|
|
571
526
|
}
|
|
572
527
|
});
|
|
573
528
|
|
|
574
|
-
test("JSON cycle patch adds an ancestor stack around arrays and objects, idempotently", async () => {
|
|
575
|
-
const root = await mkdtemp(join(tmpdir(), "sb-json-cycles-"));
|
|
576
|
-
try {
|
|
577
|
-
await mkdir(join(root, "compiler/builtins"), { recursive: true });
|
|
578
|
-
const file = join(root, "compiler/builtins/json.ts");
|
|
579
|
-
await writeFile(
|
|
580
|
-
file,
|
|
581
|
-
"let __Porffor_json_bufLimit: i32 = 0;\n depth += 1;\n depth -= 1;\n depth += 1;\n depth -= 1;\n" +
|
|
582
|
-
" let out: i32 = __Porffor_json_serialize(buffer, value, 0, space);\n",
|
|
583
|
-
);
|
|
584
|
-
await patchJsonCycles(root);
|
|
585
|
-
const patched = await readFile(file, "utf8");
|
|
586
|
-
expect(patched.split("__Porffor_json_enter(value);").length).toBe(3);
|
|
587
|
-
expect(patched.split("__Porffor_json_leave();").length).toBe(3);
|
|
588
|
-
expect(patched).toContain("throw new TypeError('Converting circular structure to JSON');");
|
|
589
|
-
await patchJsonCycles(root);
|
|
590
|
-
expect(await readFile(file, "utf8")).toBe(patched);
|
|
591
|
-
await writeFile(file, "let __Porffor_json_bufLimit: i32 = 0;\n");
|
|
592
|
-
await expect(patchJsonCycles(root)).rejects.toThrow("anchor not found");
|
|
593
|
-
} finally {
|
|
594
|
-
await rm(root, { recursive: true, force: true });
|
|
595
|
-
}
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
test("typed array tag patch names every typed array before toString returns, idempotently", async () => {
|
|
599
|
-
const root = await mkdtemp(join(tmpdir(), "sb-ta-tag-"));
|
|
600
|
-
try {
|
|
601
|
-
await mkdir(join(root, "compiler/builtins"), { recursive: true });
|
|
602
|
-
const file = join(root, "compiler/builtins/object.ts");
|
|
603
|
-
await writeFile(file, " return '[object ' + builtinTag + ']';\n};\n");
|
|
604
|
-
await patchTypedArrayTag(root);
|
|
605
|
-
const patched = await readFile(file, "utf8");
|
|
606
|
-
for (const name of ["Uint8Array", "Uint8ClampedArray", "BigInt64Array", "Float64Array"]) expect(patched).toContain(`builtinTag = '${name}';`);
|
|
607
|
-
await patchTypedArrayTag(root);
|
|
608
|
-
expect(await readFile(file, "utf8")).toBe(patched);
|
|
609
|
-
await writeFile(file, "export const x = 1;\n");
|
|
610
|
-
await expect(patchTypedArrayTag(root)).rejects.toThrow("anchor not found");
|
|
611
|
-
} finally {
|
|
612
|
-
await rm(root, { recursive: true, force: true });
|
|
613
|
-
}
|
|
614
|
-
});
|
|
615
|
-
|
|
616
529
|
test("console colour patch colours only on a terminal, idempotently", async () => {
|
|
617
530
|
const root = await mkdtemp(join(tmpdir(), "sb-console-colors-"));
|
|
618
531
|
try {
|
|
@@ -771,6 +684,30 @@ test("streaming patch adds the stream registry and attaches marked responses, id
|
|
|
771
684
|
}
|
|
772
685
|
});
|
|
773
686
|
|
|
687
|
+
test("async waker patch installs a loop waker in the native server, idempotently", async () => {
|
|
688
|
+
const root = await mkdtemp(join(tmpdir(), "sb-async-waker-"));
|
|
689
|
+
try {
|
|
690
|
+
await mkdir(join(root, "compiler"));
|
|
691
|
+
const file = join(root, "compiler/uwebsockets.js");
|
|
692
|
+
await writeFile(
|
|
693
|
+
file,
|
|
694
|
+
"static void on_request() {}\nint main(void) {\n porf_native_fetch_runtime_init();\n porf_native_fetch_static_init();\n\n uWS::App();\n}\n",
|
|
695
|
+
);
|
|
696
|
+
await patchAsyncWaker(root);
|
|
697
|
+
const patched = await readFile(file, "utf8");
|
|
698
|
+
expect(patched).toContain('extern "C" __attribute__((weak)) void sb_async_set_waker(void (*wake)(void))');
|
|
699
|
+
expect(patched).toContain("sb_async_loop->defer([] {");
|
|
700
|
+
expect(patched.indexOf("sb_async_set_waker(sb_async_wake);")).toBeGreaterThan(patched.indexOf("porf_native_fetch_static_init();"));
|
|
701
|
+
expect(patched.indexOf("sb_async_set_waker(sb_async_wake);")).toBeLessThan(patched.indexOf("uWS::App();"));
|
|
702
|
+
await patchAsyncWaker(root);
|
|
703
|
+
expect(await readFile(file, "utf8")).toBe(patched);
|
|
704
|
+
await writeFile(file, "export const x = 1;\n");
|
|
705
|
+
await expect(patchAsyncWaker(root)).rejects.toThrow("main() anchor not found");
|
|
706
|
+
} finally {
|
|
707
|
+
await rm(root, { recursive: true, force: true });
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
|
|
774
711
|
test("split globals patch prefixes module globals so none binds to libc, idempotently", async () => {
|
|
775
712
|
const root = await mkdtemp(join(tmpdir(), "sb-split-globals-"));
|
|
776
713
|
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
|
|
@@ -1841,6 +1673,39 @@ const DEFERRED_DISCARD_EDITS: [string, string][] = [
|
|
|
1841
1673
|
],
|
|
1842
1674
|
];
|
|
1843
1675
|
|
|
1676
|
+
const ASYNC_WAKER_MARKER = "// sproutboat: async broker exchanges";
|
|
1677
|
+
const ASYNC_WAKER_MAIN =
|
|
1678
|
+
"int main(void) {\n porf_native_fetch_runtime_init();\n porf_native_fetch_static_init();\n";
|
|
1679
|
+
|
|
1680
|
+
/**
|
|
1681
|
+
* The broker transport runs fetch() and service-binding calls on its own threads
|
|
1682
|
+
* once the host gives it a way back onto the loop, so a sprout keeps serving while
|
|
1683
|
+
* a call is in flight. uWS's `defer()` is that way: it is the loop's one thread-safe
|
|
1684
|
+
* entry (it locks, queues and wakes the loop). Builds without the broker transport
|
|
1685
|
+
* link the weak no-op defaults and stay synchronous.
|
|
1686
|
+
*/
|
|
1687
|
+
export async function patchAsyncWaker(root: string): Promise<void> {
|
|
1688
|
+
const shimFile = resolve(root, "compiler/uwebsockets.js");
|
|
1689
|
+
const shim = await readFile(shimFile, "utf8");
|
|
1690
|
+
if (shim.includes(ASYNC_WAKER_MARKER)) return;
|
|
1691
|
+
if (!shim.includes(ASYNC_WAKER_MAIN))
|
|
1692
|
+
throw new Error(`could not patch Porffor's native server for async calls: main() anchor not found in ${shimFile}`);
|
|
1693
|
+
const waker = `${ASYNC_WAKER_MARKER}
|
|
1694
|
+
extern "C" __attribute__((weak)) void sb_async_set_waker(void (*wake)(void)) { (void)wake; }
|
|
1695
|
+
extern "C" __attribute__((weak)) void sb_async_drain(void) {}
|
|
1696
|
+
static uWS::Loop* sb_async_loop = nullptr;
|
|
1697
|
+
static void sb_async_wake(void) {
|
|
1698
|
+
sb_async_loop->defer([] {
|
|
1699
|
+
__porffor_js_enter();
|
|
1700
|
+
sb_async_drain();
|
|
1701
|
+
__porffor_js_exit();
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
`;
|
|
1705
|
+
const install = " sb_async_loop = uWS::Loop::get();\n sb_async_set_waker(sb_async_wake);\n";
|
|
1706
|
+
await writeFile(shimFile, shim.replace(ASYNC_WAKER_MAIN, waker + ASYNC_WAKER_MAIN + install));
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1844
1709
|
/** #287: deferred discard and idle return. After patchQuietCollection, which adds the quiet collector. */
|
|
1845
1710
|
export async function patchDeferredDiscard(root: string): Promise<void> {
|
|
1846
1711
|
const file = resolve(root, "compiler/render.js");
|
|
@@ -1887,128 +1752,6 @@ export async function patchRepeatDoubling(root: string): Promise<void> {
|
|
|
1887
1752
|
await writeFile(file, src.replaceAll(REPEAT_LOOP, REPEAT_DOUBLING));
|
|
1888
1753
|
}
|
|
1889
1754
|
|
|
1890
|
-
// JSON.stringify of a cyclic object recursed until the native stack overflowed: the
|
|
1891
|
-
// process died (SIGBUS/SIGSEGV) instead of throwing, so one request could take a
|
|
1892
|
-
// worker down (found by the W2 conformance suite). The replacer path already checks
|
|
1893
|
-
// cycles; the plain serializer now keeps its own ancestor stack and throws the same
|
|
1894
|
-
// TypeError. Arrays and objects push on entry and pop on exit; JSON.stringify clears
|
|
1895
|
-
// the stack first, so a throw mid-serialization leaves nothing behind.
|
|
1896
|
-
const JSON_CYCLE_MARKER = "sproutboat: JSON cycle check";
|
|
1897
|
-
const JSON_CYCLE_EDITS: [string, string, number][] = [
|
|
1898
|
-
[
|
|
1899
|
-
"let __Porffor_json_bufLimit: i32 = 0;\n",
|
|
1900
|
-
"let __Porffor_json_bufLimit: i32 = 0;\n" +
|
|
1901
|
-
"// sproutboat: JSON cycle check\n" +
|
|
1902
|
-
"let __Porffor_json_ancestors: any = 0;\n" +
|
|
1903
|
-
"export const __Porffor_json_enter = (value: any): void => {\n" +
|
|
1904
|
-
" if (__Porffor_json_ancestors === 0) __Porffor_json_ancestors = Porffor.array.new(8);\n" +
|
|
1905
|
-
" const stack: any[] = __Porffor_json_ancestors;\n" +
|
|
1906
|
-
" if (stack.includes(value)) {\n" +
|
|
1907
|
-
" stack.length = 0;\n" +
|
|
1908
|
-
" throw new TypeError('Converting circular structure to JSON');\n" +
|
|
1909
|
-
" }\n" +
|
|
1910
|
-
" Porffor.array.fastPush(stack, value);\n" +
|
|
1911
|
-
"};\n" +
|
|
1912
|
-
"export const __Porffor_json_leave = (): void => {\n" +
|
|
1913
|
-
" const stack: any[] = __Porffor_json_ancestors;\n" +
|
|
1914
|
-
" stack.length = stack.length - 1;\n" +
|
|
1915
|
-
"};\n",
|
|
1916
|
-
1,
|
|
1917
|
-
],
|
|
1918
|
-
[" depth += 1;\n", " depth += 1;\n __Porffor_json_enter(value);\n", 2],
|
|
1919
|
-
[" depth -= 1;\n", " __Porffor_json_leave();\n depth -= 1;\n", 2],
|
|
1920
|
-
[
|
|
1921
|
-
" let out: i32 = __Porffor_json_serialize(buffer, value, 0, space);\n",
|
|
1922
|
-
" if (__Porffor_json_ancestors !== 0) (__Porffor_json_ancestors as any[]).length = 0;\n" +
|
|
1923
|
-
" let out: i32 = __Porffor_json_serialize(buffer, value, 0, space);\n",
|
|
1924
|
-
1,
|
|
1925
|
-
],
|
|
1926
|
-
];
|
|
1927
|
-
|
|
1928
|
-
/** JSON.stringify throws on cycles instead of overflowing the stack. Before the precompile. */
|
|
1929
|
-
export async function patchJsonCycles(root: string): Promise<void> {
|
|
1930
|
-
const file = resolve(root, "compiler/builtins/json.ts");
|
|
1931
|
-
let src = await readFile(file, "utf8");
|
|
1932
|
-
if (src.includes(JSON_CYCLE_MARKER)) return;
|
|
1933
|
-
for (const [anchor, inject, count] of JSON_CYCLE_EDITS) {
|
|
1934
|
-
if (src.split(anchor).length !== count + 1)
|
|
1935
|
-
throw new Error(`could not patch Porffor's JSON cycle check: anchor not found ${count}x in ${file}`);
|
|
1936
|
-
src = src.replaceAll(anchor, inject);
|
|
1937
|
-
}
|
|
1938
|
-
await writeFile(file, src);
|
|
1939
|
-
}
|
|
1940
|
-
|
|
1941
|
-
// JSON.parse ignored its reviver (W2 conformance: json/reviver). The parser keeps its body
|
|
1942
|
-
// under a new name, and __JSON_parse walks the result as the spec's InternalizeJSONProperty
|
|
1943
|
-
// does: children first, a reviver answer of undefined deletes the property.
|
|
1944
|
-
const JSON_REVIVER_MARKER = "sproutboat: JSON.parse reviver";
|
|
1945
|
-
// Porffor declares (_: bytestring); patchUnicode widens it to any later, on the renamed parser.
|
|
1946
|
-
const JSON_PARSE_ANCHOR = /export const __JSON_parse = \(_: (bytestring|any)\) => \{\n/;
|
|
1947
|
-
const JSON_REVIVER_SOURCE = `
|
|
1948
|
-
// ${JSON_REVIVER_MARKER}
|
|
1949
|
-
export const __Porffor_json_revive = (holder: any, key: any, reviver: any): any => {
|
|
1950
|
-
const value: any = holder[key];
|
|
1951
|
-
if (Porffor.type(value) == Porffor.TYPES.array) {
|
|
1952
|
-
const len: i32 = value.length;
|
|
1953
|
-
for (let i: i32 = 0; i < len; i++) {
|
|
1954
|
-
const next: any = __Porffor_json_revive(value, ecma262.ToString(i), reviver);
|
|
1955
|
-
if (next === undefined) delete value[i];
|
|
1956
|
-
else value[i] = next;
|
|
1957
|
-
}
|
|
1958
|
-
} else if (Porffor.type(value) == Porffor.TYPES.object) {
|
|
1959
|
-
for (const k: any in value) {
|
|
1960
|
-
const next: any = __Porffor_json_revive(value, k, reviver);
|
|
1961
|
-
if (next === undefined) delete value[k];
|
|
1962
|
-
else value[k] = next;
|
|
1963
|
-
}
|
|
1964
|
-
}
|
|
1965
|
-
return reviver.call(holder, key, value);
|
|
1966
|
-
};
|
|
1967
|
-
|
|
1968
|
-
export const __JSON_parse = (text: any, reviver: any) => {
|
|
1969
|
-
const value: any = __Porffor_json_parseText(text);
|
|
1970
|
-
if (Porffor.type(reviver) != Porffor.TYPES.function) return value;
|
|
1971
|
-
const root: object = {};
|
|
1972
|
-
root[''] = value;
|
|
1973
|
-
return __Porffor_json_revive(root, '', reviver);
|
|
1974
|
-
};
|
|
1975
|
-
`;
|
|
1976
|
-
|
|
1977
|
-
/** JSON.parse applies its reviver, as InternalizeJSONProperty does. */
|
|
1978
|
-
export async function patchJsonReviver(root: string): Promise<void> {
|
|
1979
|
-
const file = resolve(root, "compiler/builtins/json.ts");
|
|
1980
|
-
const src = await readFile(file, "utf8");
|
|
1981
|
-
if (src.includes(JSON_REVIVER_MARKER)) return;
|
|
1982
|
-
if ((src.match(new RegExp(JSON_PARSE_ANCHOR, "g")) ?? []).length !== 1)
|
|
1983
|
-
throw new Error(`could not patch Porffor's JSON.parse reviver: anchor not found in ${file}`);
|
|
1984
|
-
await writeFile(
|
|
1985
|
-
file,
|
|
1986
|
-
src.replace(JSON_PARSE_ANCHOR, (_line, type) => `export const __Porffor_json_parseText = (_: ${type}) => {\n`).trimEnd() + "\n" + JSON_REVIVER_SOURCE,
|
|
1987
|
-
);
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
|
-
// Object.prototype.toString gave "[object Object]" for typed arrays: their lazily
|
|
1991
|
-
// built prototypes have no toStringTag, and the fallback list of builtin tags left
|
|
1992
|
-
// them out (W2 conformance; V8 gives "[object Uint8Array]").
|
|
1993
|
-
const TA_TAG_MARKER = "sproutboat: typed array toStringTag";
|
|
1994
|
-
const TA_TAG_ANCHOR = " return '[object ' + builtinTag + ']';\n};\n";
|
|
1995
|
-
const TA_TAG_INJECT =
|
|
1996
|
-
" // sproutboat: typed array toStringTag\n" +
|
|
1997
|
-
" if (Porffor.fastAnd(tag === undefined, t >= Porffor.TYPES.uint8clampedarray, t <= Porffor.TYPES.float64array)) {\n" +
|
|
1998
|
-
" 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" +
|
|
1999
|
-
" }\n" +
|
|
2000
|
-
TA_TAG_ANCHOR;
|
|
2001
|
-
|
|
2002
|
-
/** Typed arrays get their own toString tag. Before the precompile. */
|
|
2003
|
-
export async function patchTypedArrayTag(root: string): Promise<void> {
|
|
2004
|
-
const file = resolve(root, "compiler/builtins/object.ts");
|
|
2005
|
-
const src = await readFile(file, "utf8");
|
|
2006
|
-
if (src.includes(TA_TAG_MARKER)) return;
|
|
2007
|
-
if (src.split(TA_TAG_ANCHOR).length !== 2)
|
|
2008
|
-
throw new Error(`could not patch Porffor's typed array toStringTag: anchor not found in ${file}`);
|
|
2009
|
-
await writeFile(file, src.replace(TA_TAG_ANCHOR, TA_TAG_INJECT));
|
|
2010
|
-
}
|
|
2011
|
-
|
|
2012
1755
|
// console.log coloured numbers, booleans and objects with ANSI escapes even when
|
|
2013
1756
|
// stdout was a file or pipe, so sproutd's per-app logs (B21) and piped sprout output
|
|
2014
1757
|
// were full of \x1b[33m. Colour only when stdout is a terminal, as Node and Bun do.
|
|
@@ -2282,12 +2025,12 @@ export async function ensurePorfforPatched(root: string): Promise<void> {
|
|
|
2282
2025
|
await patchRenderJs(root);
|
|
2283
2026
|
await patchStreamingResponses(root);
|
|
2284
2027
|
await patchQuietCollection(root);
|
|
2028
|
+
await patchAsyncWaker(root);
|
|
2285
2029
|
await patchDeferredDiscard(root);
|
|
2286
|
-
await patchDateParser(root);
|
|
2287
2030
|
await patchPromiseResolveNull(root);
|
|
2288
2031
|
await patchReplaceAll(root);
|
|
2289
2032
|
await patchLocalFunctionTypes(root);
|
|
2290
|
-
await
|
|
2033
|
+
await patchFunctionSourcesScan(root);
|
|
2291
2034
|
await patchResponseDefaults(root);
|
|
2292
2035
|
await patchHeadersArrayInit(root);
|
|
2293
2036
|
await patchHeadersClass(root);
|
|
@@ -2296,18 +2039,13 @@ export async function ensurePorfforPatched(root: string): Promise<void> {
|
|
|
2296
2039
|
await patchResponseBytes(root);
|
|
2297
2040
|
await patchURLClass(root);
|
|
2298
2041
|
await patchTextDecoder(root);
|
|
2299
|
-
await patchMapGroupBy(root);
|
|
2300
|
-
await patchPrivateFieldsHidden(root);
|
|
2301
2042
|
await patchRepeatDoubling(root);
|
|
2302
|
-
await patchJsonCycles(root);
|
|
2303
|
-
await patchJsonReviver(root);
|
|
2304
|
-
await patchTypedArrayTag(root);
|
|
2305
2043
|
await patchConsoleColors(root);
|
|
2306
2044
|
await patchCaseMapping(root);
|
|
2307
2045
|
await patchZeroDelayTimers(root);
|
|
2308
2046
|
await patchSplitGlobals(root);
|
|
2309
|
-
// Last of the builtin edits: its precompile picks up the
|
|
2310
|
-
//
|
|
2047
|
+
// Last of the builtin edits: its precompile picks up the promise, replaceAll,
|
|
2048
|
+
// local function and repeat patches above too.
|
|
2311
2049
|
await precompileBuiltins(root);
|
|
2312
2050
|
await patchUnicode(root);
|
|
2313
2051
|
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
|
|
|
Binary file
|
|
Binary file
|