@sproutboat/toolchain 0.4.15 → 0.4.17

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,17 @@
1
1
  # @sproutboat/toolchain
2
2
 
3
+ ## 0.4.17
4
+
5
+ ### Patch Changes
6
+
7
+ - 2cda218: Fix async handlers failing permanently with `TypeError: Cannot get property of null` after a few requests (baronunread/sproutboat#168). Porffor represents `null` as an object-typed value with pointer 0, so a promise resolving to `null` went through the `then` probe and looked `then` up at memory offset 0. Once `Date.prototype.toISOString` had written there, the probe matched, read `null.then` and rejected, and every later async function returning `null` failed the same way. A patch to `compiler/builtins/promise.ts` keeps `null` out of the probe.
8
+
9
+ ## 0.4.16
10
+
11
+ ### Patch Changes
12
+
13
+ - 7bab275: Pin and vendor Porffor alpha-13 (`547c781`). Alpha-11 to alpha-13 bring array, typed array and module codegen fixes, a basic `RegExp` search, a `String.prototype.normalize` stub and lowercase JSON hex escapes. Every Sproutboat patch applies unchanged, uWebSockets stays on the same commit, and the compatibility suite is unchanged at 30/32.
14
+
3
15
  ## 0.4.15
4
16
 
5
17
  ### Patch Changes
@@ -0,0 +1,9 @@
1
+ # Porffor alpha-13 upgrade audit
2
+
3
+ The source pin moves from alpha-10 (`08ac7ee`) to the [alpha-13 release](https://github.com/CanadaHonk/porffor/releases/tag/alpha-13), commit `547c7815125b6f02474591950f8b0dd7031a03f0`, published September 30, 2026. The vendored codeload archive has SHA-256 `d0ff4bf7778f85445ff99bb8f6b2e07f2b83a33a109612f9eba6f2e5cba291b5`.
4
+
5
+ Alpha-11 brings array and typed array fast paths (merge sort, `shift`, same-type `set`) and codegen fixes for closures in loops and function literals. Alpha-12 fixes module handling (reassigned exports, strict-mode analysis, module-init closures). Alpha-13 adds a basic `RegExp` search, a `String.prototype.normalize` stub, more reserved C keywords and lowercase JSON hex escapes. Its `compiler/uwebsockets.js` still pins `360c276d609d59af56ae6932adb95154ace9f15f`, so the CLI's vendored uWebSockets archive does not change. All 28 Sproutboat patches apply without changing their anchors or markers. `PORF_CORO_STACK_SIZE` is still 256 KiB upstream, so the #178 patch still applies and still matters.
6
+
7
+ The native package suite passed 152 tests, plus the native crypto vectors. Linked to this toolchain, the CLI passed 103 tests (`SURFACE.md` only needs the new provenance stamp), 28 broker checks, 30 standalone checks and all 21 examples. The platform's 32-handler comparison compiled all 32 and matched Bun on 30, unchanged from alpha-10. The remaining two failures are non-ISO date parsing in `15-date-iso.js` and `16-date-parts.js`. See [the scorecard](PORFFOR_SCORECARD.md) for the exact results.
8
+
9
+ The open runtime bugs reproduce unchanged: baronunread/sproutboat#168 (an async chain using `new Date().toISOString()` flips to permanent 500s after 5 to 12 requests) and the read half of #189 (non-ASCII D1 and KV values come back as Latin-1 bytes).
@@ -1,29 +1,29 @@
1
1
  # Porffor pin scorecard
2
2
 
3
- Pin: alpha-10 (08ac7ee1077c05da2bec18dcca15197051e87b62). Compatibility report: 2026-09-28T07:08:44.821Z.
3
+ Pin: alpha-13 (547c7815125b6f02474591950f8b0dd7031a03f0). Compatibility report: 2026-10-01T10:57:31.463Z.
4
4
 
5
5
  | Measure | Result |
6
6
  | --- | ---: |
7
7
  | Fixtures compiled | 32/32 |
8
8
  | Behavior matches | 30/32 |
9
- | Median / p95 compile, ms | 4468 / 7436 |
10
- | Median / p95 binary, bytes | 983392 / 1016464 |
11
- | Patch markers | 27 |
9
+ | Median / p95 compile, ms | 3212 / 4703 |
10
+ | Median / p95 binary, bytes | 966864 / 999936 |
11
+ | Patch markers | 28 |
12
12
  | Upstream files written | 6 |
13
13
  | Fresh patch application | pass |
14
14
 
15
- Previous snapshot: alpha-9-de4eb58.json. Matches 30/32 to 30/32; patch markers 27 to 27.
15
+ Previous snapshot: alpha-10-08ac7ee.json. Matches 30/32 to 30/32; patch markers 27 to 28.
16
16
 
17
17
  ## Patch inventory
18
18
 
19
- - `compiler/render.js`: PORT_MARKER, CONSOLE_MARKER, BYTESTRING_MARKER, READ_RAW_MARKER
19
+ - `compiler/render.js`: PORT_MARKER, CONSOLE_MARKER, BYTESTRING_MARKER, READ_RAW_MARKER, CORO_STACK_MARKER
20
20
  - `compiler/index.js`: LINK_MARKER, CFLAGS_MARKER
21
21
  - `compiler/builtins/typedarray.js`: TYPED_ARRAY_FROM_MARKER
22
22
  - `compiler/builtins/date.ts`: DATE_PARSER_MARKER
23
23
  - `compiler/uwebsockets.js`: BODY_MARKER, STATUS_SIG_MARKER, STATUS_303_MARKER, STATUS_DEFAULT_MARKER, HDR_SIG_MARKER, HDR_CALL_MARKER, HDR_CAP_MARKER, HDR_SKIP_MARKER, HDR_APPEND_MARKER, READ_RAW_DECL_MARKER, FORBIDDEN_HDR_MARKER, WRITE_RESPONSE_MARKER, R2_TRANSFER_MARKER, R2_TRANSFER_CALL_MARKER, R2_DOWNLOAD_CACHE_MARKER, R2_RANGE_CACHE_MARKER, R2_CONDITIONAL_CACHE_MARKER, R2_STREAMING_CACHE_MARKER, R2_RANGE_HEADER_CACHE_MARKER
24
24
  - `compiler/builtins_precompiled.js`: regenerated builtin table
25
25
 
26
- Patch source SHA-256: `edf60b7104e2d0b8efb64e38459fd2d5616ccdf24995ad1d4ca07fa232db4eaf`.
26
+ Patch source SHA-256: `6f607ac6b66fc4a51c2bf9e9525d0ab7f4e9b9dd3f942a820bbad46b7e3dbf56`.
27
27
 
28
28
  ## Known behavior gaps
29
29
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutboat/toolchain",
3
- "version": "0.4.15",
3
+ "version": "0.4.17",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -0,0 +1,59 @@
1
+ {
2
+ "pin": "alpha-13 (547c7815125b6f02474591950f8b0dd7031a03f0)",
3
+ "reportGeneratedAt": "2026-10-01T10:57:31.463Z",
4
+ "fixtures": 32,
5
+ "compiled": 32,
6
+ "matched": 30,
7
+ "medianCompileMs": 3212,
8
+ "p95CompileMs": 4703,
9
+ "medianBinaryBytes": 966864,
10
+ "p95BinaryBytes": 999936,
11
+ "patchSourceSha256": "6f607ac6b66fc4a51c2bf9e9525d0ab7f4e9b9dd3f942a820bbad46b7e3dbf56",
12
+ "patchMarkerCount": 28,
13
+ "patchTargets": {
14
+ "compiler/render.js": [
15
+ "PORT_MARKER",
16
+ "CONSOLE_MARKER",
17
+ "BYTESTRING_MARKER",
18
+ "READ_RAW_MARKER",
19
+ "CORO_STACK_MARKER"
20
+ ],
21
+ "compiler/index.js": [
22
+ "LINK_MARKER",
23
+ "CFLAGS_MARKER"
24
+ ],
25
+ "compiler/builtins/typedarray.js": [
26
+ "TYPED_ARRAY_FROM_MARKER"
27
+ ],
28
+ "compiler/builtins/date.ts": [
29
+ "DATE_PARSER_MARKER"
30
+ ],
31
+ "compiler/uwebsockets.js": [
32
+ "BODY_MARKER",
33
+ "STATUS_SIG_MARKER",
34
+ "STATUS_303_MARKER",
35
+ "STATUS_DEFAULT_MARKER",
36
+ "HDR_SIG_MARKER",
37
+ "HDR_CALL_MARKER",
38
+ "HDR_CAP_MARKER",
39
+ "HDR_SKIP_MARKER",
40
+ "HDR_APPEND_MARKER",
41
+ "READ_RAW_DECL_MARKER",
42
+ "FORBIDDEN_HDR_MARKER",
43
+ "WRITE_RESPONSE_MARKER",
44
+ "R2_TRANSFER_MARKER",
45
+ "R2_TRANSFER_CALL_MARKER",
46
+ "R2_DOWNLOAD_CACHE_MARKER",
47
+ "R2_RANGE_CACHE_MARKER",
48
+ "R2_CONDITIONAL_CACHE_MARKER",
49
+ "R2_STREAMING_CACHE_MARKER",
50
+ "R2_RANGE_HEADER_CACHE_MARKER"
51
+ ],
52
+ "compiler/builtins_precompiled.js": []
53
+ },
54
+ "patchApplication": "pass",
55
+ "mismatches": [
56
+ "15-date-iso.js: request 3 mismatch: expected {\"status\":200,\"body\":\"2008-03-04T23:00:00.000Z\",\"content-type\":null}, got {\"status\":200,\"body\":\"0003-05-08T00:00:00.000Z\",\"content-type\":null}",
57
+ "16-date-parts.js: request 3 mismatch: expected {\"status\":200,\"body\":\"{\\\"year\\\":2008,\\\"month\\\":3,\\\"day\\\":4}\",\"content-type\":\"application/json;charset=utf-8\"}, got {\"status\":200,\"body\":\"{\\\"year\\\":3,\\\"month\\\":5,\\\"day\\\":8}\",\"content-type\":\"application/json;charset=utf-8\"}"
58
+ ]
59
+ }
@@ -119,6 +119,7 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
119
119
  "compiler/builtins/promise.ts":
120
120
  "export const __Porffor_promise_resolve = (value: any, promise: any): void => {\n" +
121
121
  " if (Porffor.type(value) == Porffor.TYPES.object) {\n" +
122
+ " // cheap prototype-chain probe for 'then' before the expensive Get below, does not invoke getters\n" +
122
123
  " const thenHash: i32 = __Porffor_object_hash('then');\n" +
123
124
  " let probe: any = value;\n" +
124
125
  " while (Porffor.type(probe) == Porffor.TYPES.object) {\n" +
@@ -197,6 +198,10 @@ test("the default call (no url/expectedSha256 override) never touches the networ
197
198
  },
198
199
  });
199
200
  expect(await readFile(join(dir, "runtime/index.js"), "utf8")).not.toBe("");
201
+ // #168: the real vendored source takes the null guard on the `then` probe.
202
+ expect(await readFile(join(dir, "compiler/builtins/promise.ts"), "utf8")).toContain(
203
+ "if (Porffor.type(value) == Porffor.TYPES.object && value != null) {",
204
+ );
200
205
  });
201
206
 
202
207
  test("a corrupted warm cache is replaced from the verified archive", async () => {
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-08ac7ee.tar.gz" with { type: "file" };
8
+ import vendoredPorfforArchive from "../vendor/porffor-547c781.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/patch.ts CHANGED
@@ -133,10 +133,11 @@ const READ_RAW_MARKER = "porf_native_fetch_read_raw_bytes(jsval value";
133
133
 
134
134
  /**
135
135
  * baronunread/sproutboat#178: every async function runs on its own fiber
136
- * stack, and alpha-10 fixes those at 256 KiB. A large handler compiled at -O0
137
- * (what `sproutboat dev` builds) spends ~40 KiB of stack per call into the
138
- * biggest generated functions, so a few nested awaits run off the end into
139
- * the guard page and the sprout dies with SIGBUS on its first request, no log.
136
+ * stack, and Porffor (alpha-10 through alpha-13) fixes those at 256 KiB. A
137
+ * large handler compiled at -O0 (what `sproutboat dev` builds) spends ~40 KiB
138
+ * of stack per call into the biggest generated functions, so a few nested
139
+ * awaits run off the end into the guard page and the sprout dies with SIGBUS
140
+ * on its first request, no log.
140
141
  * -O3 frames are smaller, which only raises the threshold: a deep enough
141
142
  * async chain hits it in a release build too.
142
143
  *
@@ -191,6 +192,21 @@ const CFLAGS_INJECT =
191
192
  " ...(process.env.SB_EXTRA_CFLAGS ? process.env.SB_EXTRA_CFLAGS.split(' ').filter(Boolean) : []),\n";
192
193
  const CFLAGS_MARKER = "SB_EXTRA_CFLAGS";
193
194
 
195
+ // baronunread/sproutboat#168: Porffor represents null as an object-typed value
196
+ // with pointer 0, so __Porffor_promise_resolve's cheap `then` probe treated a
197
+ // promise resolving to null as an object and looked `then` up at memory offset
198
+ // 0. Once something (Date.prototype.toISOString's scratch writes, in practice)
199
+ // left bytes there that hash-match, the probe "found" a then, read null.then,
200
+ // and rejected with "Cannot get property of null". Every later async function
201
+ // resolving to null then failed, permanently. Only non-null objects can be
202
+ // thenables (ECMA-262 Promise Resolve Functions step 8), so exclude null.
203
+ const PROMISE_NULL_ANCHOR =
204
+ " if (Porffor.type(value) == Porffor.TYPES.object) {\n // cheap prototype-chain probe for 'then'";
205
+ const PROMISE_NULL_INJECT =
206
+ " // sproutboat #168: null is object-typed with pointer 0, never a thenable.\n" +
207
+ " if (Porffor.type(value) == Porffor.TYPES.object && value != null) {\n // cheap prototype-chain probe for 'then'";
208
+ const PROMISE_NULL_MARKER = "sproutboat #168: null is object-typed";
209
+
194
210
  // Porffor's TypedArray.from only handles iterables. An array-like input such
195
211
  // as { length: 16 } silently becomes an empty typed array, including HMAC
196
212
  // keys made with Uint8Array.from({ length: 16 }, mapFn). The compiler uses a
@@ -976,6 +992,16 @@ export async function patchDateParser(root: string): Promise<void> {
976
992
  await writeFile(file, src.slice(0, start) + parser.replace(DATE_PARSER_ANCHOR, DATE_PARSER_INJECT) + src.slice(end));
977
993
  }
978
994
 
995
+ /** #168: keep a promise resolving to null out of the `then` probe. Precompiled by patchTypedArrayFrom. */
996
+ export async function patchPromiseResolveNull(root: string): Promise<void> {
997
+ const file = resolve(root, "compiler/builtins/promise.ts");
998
+ const src = await readFile(file, "utf8");
999
+ if (src.includes(PROMISE_NULL_MARKER)) return;
1000
+ if (!src.includes(PROMISE_NULL_ANCHOR))
1001
+ throw new Error(`could not patch Porffor's promise resolve for null: anchor not found in ${file}`);
1002
+ await writeFile(file, src.replace(PROMISE_NULL_ANCHOR, PROMISE_NULL_INJECT));
1003
+ }
1004
+
979
1005
  export async function patchTypedArrayFrom(root: string): Promise<void> {
980
1006
  const file = resolve(root, "compiler/builtins/typedarray.js");
981
1007
  const src = await readFile(file, "utf8");
@@ -1051,6 +1077,9 @@ export async function ensurePorfforPatched(root: string): Promise<void> {
1051
1077
  await patchUwebsockets(root);
1052
1078
  await patchRenderJs(root);
1053
1079
  await patchDateParser(root);
1080
+ await patchPromiseResolveNull(root);
1081
+ // Last of the builtin edits: its precompile picks up the date and promise
1082
+ // patches above too.
1054
1083
  await patchTypedArrayFrom(root);
1055
1084
  await patchUnicode(root);
1056
1085
  done.add(root);
package/src/pin.ts CHANGED
@@ -11,14 +11,14 @@
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-10";
15
- export const PORFFOR_COMMIT_FULL = "08ac7ee1077c05da2bec18dcca15197051e87b62";
14
+ export const PORFFOR_CHANNEL = "alpha-13";
15
+ export const PORFFOR_COMMIT_FULL = "547c7815125b6f02474591950f8b0dd7031a03f0";
16
16
  export const PORFFOR_COMMIT = PORFFOR_COMMIT_FULL.slice(0, 7);
17
17
  export const PORFFOR_ARCHIVE_SHA256 =
18
- "d49ce6724efde555b4cdeea0d2610baddd4310153956c79b352d86ba9cf0f60f";
18
+ "d0ff4bf7778f85445ff99bb8f6b2e07f2b83a33a109612f9eba6f2e5cba291b5";
19
19
  export const PORFFOR_ARCHIVE_URL = `https://codeload.github.com/CanadaHonk/porffor/tar.gz/${PORFFOR_COMMIT_FULL}`;
20
20
 
21
- /** A compact identity string for a manifest / report (`alpha-10 (08ac7ee)`). */
21
+ /** A compact identity string for a manifest / report (`alpha-13 (547c781)`). */
22
22
  export function porfforVersion(): string {
23
23
  return process.env.PORFFOR_VERSION || `${PORFFOR_CHANNEL} (${PORFFOR_COMMIT})`;
24
24
  }
Binary file
Binary file