@sproutboat/toolchain 0.4.24 → 0.6.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 CHANGED
@@ -1,5 +1,23 @@
1
1
  # @sproutboat/toolchain
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Pin and vendor Porffor alpha-16 (43087d4). Enable Proxy and revocable Proxy in handler validation, add native regression coverage for traps and iterators, and drop three typed-array patches now implemented upstream. See PORFFOR_ALPHA16.md for the complete change audit and remaining package compatibility limits.
8
+
9
+ ## 0.5.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 16f9b79: Pin and vendor Porffor alpha-15 (`72d048d`). Alpha-14 and alpha-15 bring stackless coroutines, iterator support, class hoisting and typed-array fixes. Three Sproutboat patches are dropped because upstream now covers them: the typed-array store and `~` conversions (#238) and the coroutine stack size (#178). The #241 write-bounds guard is re-anchored. uWebSockets stays on the same commit and the compatibility suite is unchanged at 30/32. See `PORFFOR_ALPHA15.md`.
14
+
15
+ ### Patch Changes
16
+
17
+ - bf4ac00: Named class expressions work inside handlers again (baronunread/sproutboat#256). Inside a function, Porffor gave a class expression's own name, as seen from its methods, a new function object instead of the class, so `var R = class l { static lex() { return new l() } }` failed with "value is not a constructor". Every handler has run inside a function since #238, and bundlers emit this shape for any class whose static members refer to themselves, so marked and similar packages broke. A parse-time rewrite turns such a class into the equivalent `(() => { const l = class { ... }; return l; })()`, which Porffor handles correctly.
18
+
19
+ The ISO date parser no longer reads one byte past the end of the string. When that byte happened to be `+` or `-`, a timezone offset's sign flipped, so `-00:30` could parse as `+00:30` depending on what sat next in memory.
20
+
3
21
  ## 0.4.24
4
22
 
5
23
  ### Patch Changes
@@ -0,0 +1,9 @@
1
+ # Porffor alpha-15 upgrade audit
2
+
3
+ The source pin moves from alpha-13 (`547c781`) to the [alpha-15 release](https://github.com/CanadaHonk/porffor/releases/tag/alpha-15), commit `72d048d73d49e217631ac8a1bc4fa9070d22fbc9`, published October 2, 2026. The vendored codeload archive has SHA-256 `1e12be943b29691ad84bb25baecffa810655dcf7d9a490b8399352509da7a1d5`.
4
+
5
+ Alpha-14 and alpha-15 bring stackless coroutines (async functions compile to state machines), initial iterator support, hoisting of referenced classes, a fix for function-expression self-references, integer typed-array stores that wrap, `~` built on ToUint32, a `parseInt` length check, `Map#forEach` seeing added entries, and ANSI colour only on a TTY. `compiler/uwebsockets.js` still pins uWebSockets `360c276d609d59af56ae6932adb95154ace9f15f`.
6
+
7
+ Three patches are dropped because upstream now does the same thing: the typed-array store conversion and `~` from #238 (upstream emits the same `toUint32` code), and the coroutine stack size from #178 (there are no coroutine stacks any more). The #241 write-bounds guard is re-anchored on upstream's new store line. Two fixes ride along and also apply to alpha-13: the class self-name rewrite for #256 (alpha-15's self-reference fix covers function expressions, not class expressions), and the ISO date parser no longer reads past the end of its string, which flipped an offset's sign whenever the next byte in memory was `+` or `-`. That last bug surfaced on alpha-15 because its memory layout puts a `+` there in the date test.
8
+
9
+ The package suite passed 173 tests, plus the native crypto and UTF-8 suites. Linked to this toolchain, the CLI passed 101 of 102 tests (`SURFACE.md` only needs the new provenance stamp) and both kitchen-sink harnesses: 35 broker checks and 38 standalone checks. The platform's 32-handler comparison compiled all 32 and matched Bun on 30, unchanged; the two misses are still non-ISO date parsing in `15-date-iso.js` and `16-date-parts.js`. `bench/pin-compare.ts` against alpha-13 showed equal binary size, compile time and cold start. Its throughput reading swung between runs; ten alternating runs of each binary afterwards put both at about 25,000 requests a second.
@@ -0,0 +1,67 @@
1
+ # Porffor alpha-16 upgrade audit
2
+
3
+ Source: [alpha-16](https://github.com/CanadaHonk/porffor/releases/tag/alpha-16), published October 10, 2026, commit `43087d42f0b90d14f04f3cddac034822560aefad`. The vendored codeload archive has SHA-256 `84398fd214183801d252a78e69a44fac1baeba7cafc7dbbcfd5c6ec3b0372032`. The [complete alpha-15 comparison](https://github.com/CanadaHonk/porffor/compare/alpha-15...alpha-16) contains 32 commits. uWebSockets stays at `360c276d609d59af56ae6932adb95154ace9f15f`.
4
+
5
+ ## Supported changes
6
+
7
+ The runtime accepts `new Proxy` and `Proxy.revocable`. Native output matches Bun for get/set/has/deleteProperty, ownKeys/getOwnPropertyDescriptor through Object.keys, apply, construct, revocation, and a frozen-property invariant. This is regression coverage for these operations, not certification of all Proxy invariants or exotic targets.
8
+
9
+ Array and Map iterators and lazy matchAll now expose next/done, and Set spread works. Sproutboat's URLSearchParams and FormData shims now provide live, self-iterable keys/values/entries and default iteration. Appending after creating an iterator is observed on its next step, duplicate fields remain intact, and FormData filenames do not become iterator values.
10
+
11
+ Three local patches are removed because the shared typed-array implementation handles array-like from(), an omitted set() offset and join()/toString(). Builtin precompilation remains necessary for the date, promise, replaceAll and Unicode patches. Existing typed-array bounds and class self-name patches still apply. New native regressions cover clamped ties-to-even, class static this, null-prototype __proto__, binary evaluation order and Unicode console output.
12
+
13
+ ## Package probes and opportunities
14
+
15
+ Native standalone HTTP probes on macOS arm64, pinned dependencies:
16
+
17
+ | Package | Operation | Result |
18
+ | --- | --- | --- |
19
+ | qs 6.16.0 | Nested parse and stringify, including array parameters | Passed |
20
+ | @trpc/server 11.19.0 | initTRPC, router and createCaller().hello() | Passed; HTTP adapters, clients, middleware and subscriptions untested |
21
+ | itty-router 5.0.24 Router | Parameter route, POST and fallback handler | Compiler error: labelled break crossing iterator cleanup (`target.brk`) |
22
+ | itty-router 5.0.24 IttyRouter | Parameter route, POST and fallback handler | Passed GET parameters, Unicode POST and 404 fallback after adding live Web API iterators |
23
+
24
+ Proxy-based object facades, method dispatch and server callers are now practical candidates. tRPC's basic caller path is verified, so a fetch-adapter and middleware matrix is a useful next step. Reactive stores and schema libraries must be tested on their own inputs before being listed as supported. Better Auth still needs a separate crypto and framework compatibility evaluation; Proxy alone does not establish auth support. Streams and WebSockets remain separate host API gaps.
25
+
26
+ String iteration still splits astral characters into UTF-16 halves: Array.from("🚤x") produces two surrogate entries and x rather than one emoji and x. Unicode console output itself works. Standard itty-router Router also remains blocked by a separate compiler control-flow bug. No upstream issue was filed.
27
+
28
+ ## Complete change review
29
+
30
+ | Commit | Upstream change | Sproutboat impact |
31
+ | --- | --- | --- |
32
+ | [c730268](https://github.com/CanadaHonk/porffor/commit/c730268d087766a87502de0045eb08b1f966d7d2) | codegen: evaluate binary lhs before rhs statements | Fixes operand side-effect order; native regression. |
33
+ | [d24dc0a](https://github.com/CanadaHonk/porffor/commit/d24dc0ad569bfd73a2dd6e3627c0cc21619bffc2) | codegen: balance template literal concats | Balances concatenation trees; compiler improvement, no new API. |
34
+ | [4598bec](https://github.com/CanadaHonk/porffor/commit/4598becfc46865d9ea18ad338a650c98f354af51) | precompile: store builtin table as arrays | Changes builtin table representation; patched builtins still precompile. |
35
+ | [f9eb038](https://github.com/CanadaHonk/porffor/commit/f9eb038c3aa4856d861530beb2423236536beb77) | builtins: bucket auto func keys by prefix | Compiler lookup optimization; no new API. |
36
+ | [fd8f58a](https://github.com/CanadaHonk/porffor/commit/fd8f58a8374f6adf308056b40cae8d59fc31dcb0) | builtins: check builtin existence without decoding | Compiler lookup optimization; no new API. |
37
+ | [b7b8ba2](https://github.com/CanadaHonk/porffor/commit/b7b8ba2fdd4fd12340aba048c578ac2b5c3d3aef) | builtins: add constructor to all builtin prototypes | Builtin prototype constructor identity becomes available; do not infer subclass compatibility. |
38
+ | [95522a4](https://github.com/CanadaHonk/porffor/commit/95522a421ddedb5b6801d4c1f7c6706fab3cebe0) | selfhost: label bench old/new with --compare | Upstream benchmark reporting only. |
39
+ | [860485e](https://github.com/CanadaHonk/porffor/commit/860485e9821894864fb88389f7aba5750c9f09a6) | builtins/console: print strings as utf-8 | Native Unicode console strings; native regression. |
40
+ | [2213f53](https://github.com/CanadaHonk/porffor/commit/2213f531bda917ee3ec865c1dd92b9709dfd0d27) | codegen: include String.prototype methods for bytestrings | String method dispatch on bytestrings; existing handler tests cover common methods. |
41
+ | [3502f8a](https://github.com/CanadaHonk/porffor/commit/3502f8a213b7de2479ba8673a92fc4eb8013fe51) | codegen: generate reassigned hoisted function bodies later | Hoisted function reassignment codegen fix; no new host API. |
42
+ | [77ee767](https://github.com/CanadaHonk/porffor/commit/77ee767b6c8c1ea59ff82e59d7b660c942b75047) | semantic: class static blocks and fields use the class as this | Static fields and blocks see the class as this; native regression. |
43
+ | [2e28260](https://github.com/CanadaHonk/porffor/commit/2e28260bc77048324437e53406c6a7041dba12e0) | fetch: treat null response body as empty | Null Response body becomes empty; native HTTP regression. |
44
+ | [641cd59](https://github.com/CanadaHonk/porffor/commit/641cd5911142d65d7f00d70f39360139349f707c) | builtins: call builtin Symbol directly for well-known symbols | Well-known symbol identity; enables iterator protocols. |
45
+ | [e684407](https://github.com/CanadaHonk/porffor/commit/e68440705a35a2d5e1bee9c67586523f281e80f6) | builtins: add statics to builtin constructors used as values | Builtin constructors used as values retain static methods. |
46
+ | [1e3428e](https://github.com/CanadaHonk/porffor/commit/1e3428e167505f969167f70d1ed0bd3c049256af) | codegen: only type computed string reads with number keys as strings | More accurate computed-string read types; no new API. |
47
+ | [a52e55b](https://github.com/CanadaHonk/porffor/commit/a52e55b59db43170db7831905afb3450c8404d71) | builtins/console: print maps and sets from the hashtable | Console Map/Set formatting follows hashtable representation. |
48
+ | [9579dbb](https://github.com/CanadaHonk/porffor/commit/9579dbb6379617ac8d35dc5177e035b4c6aa7127) | builtins/array: spread sets and maps directly | Map/Set spread; native Set regression. |
49
+ | [fceed9a](https://github.com/CanadaHonk/porffor/commit/fceed9a7f84afc5619da34938f89475f05f7b63d) | builtins: return iterators from array, map, set and string iterator methods | Real collection iterators; native next/done and Map keys regressions. Astral string iteration remains incorrect. |
50
+ | [532d8f8](https://github.com/CanadaHonk/porffor/commit/532d8f85f3961037d2673aaf06fbf008ec57ff07) | builtins/generator: add Symbol.iterator and Symbol.asyncIterator | Generator iterator protocol; infrastructure for iterable libraries. Async generator coverage remains unestablished. |
51
+ | [872edc0](https://github.com/CanadaHonk/porffor/commit/872edc027a220f18baa82edff4166e5e38e9ded3) | builtins/regexp: make matchAll a lazy iterator | Lazy matchAll next/done; native regression. |
52
+ | [64f5299](https://github.com/CanadaHonk/porffor/commit/64f529913c3ea39a162da9516b0371665107347e) | builtins: add Proxy | Proxy implementation; lift source ban and test object/call/construct/revocation behavior. |
53
+ | [2056abb](https://github.com/CanadaHonk/porffor/commit/2056abb1b8c8fad65f50f0ad4b0ccf00f2799150) | repl: force new line | Upstream REPL formatting only. |
54
+ | [905b193](https://github.com/CanadaHonk/porffor/commit/905b193b2d5626fe796dda1290b39f176f4ba191) | builtins/hashtable: allocate the container after its arrays | Allocation ordering fix; exercise Map/Set in native regression. |
55
+ | [1f6959b](https://github.com/CanadaHonk/porffor/commit/1f6959b82c2479a4c003be03d6ac829ccf3f9c7d) | codegen: round half to even in Uint8ClampedArray stores | Uint8ClampedArray ties-to-even stores; native regression. |
56
+ | [8e76e7b](https://github.com/CanadaHonk/porffor/commit/8e76e7b8f0ea64a30974cc116bbe9c9671e7c9f4) | builtins: define builtin $get funcs as accessors | Builtin getters become accessors; typed-array prototype rewrite replaces old generation anchors. |
57
+ | [05199be](https://github.com/CanadaHonk/porffor/commit/05199be91c124f5936c2e40d5164227ddb9b0fb9) | builtins: get builtin constructors via their getters | Builtin constructors are obtained through getters; source patches reviewed. |
58
+ | [79bb578](https://github.com/CanadaHonk/porffor/commit/79bb5789b4fe9d1892ca322be6327984f2d40ca5) | builtins/typedarray: share %TypedArray% and %TypedArray%.prototype | Shared typed-array methods/prototypes; drop from, set-offset and join patches, retain behavioral regressions. |
59
+ | [0a22e14](https://github.com/CanadaHonk/porffor/commit/0a22e1494ddf793fb3bb0c4b0b3740531931f3ef) | codegen: sort builtin proto call cases by type | Codegen case ordering optimization; no new API. |
60
+ | [d5e95d0](https://github.com/CanadaHonk/porffor/commit/d5e95d0a7f91c9cc64b7766980109b8d862b4470) | codegen: only set prototype for plain __proto__ props in object literals | Only plain __proto__ literal keys change prototypes; dictionary semantics improve. |
61
+ | [1b51d58](https://github.com/CanadaHonk/porffor/commit/1b51d58b859741688b87551470d2260606ab9698) | builtins/object: treat __proto__ as a normal key on null prototype objects | Null-prototype __proto__ is an ordinary key; native regression. |
62
+ | [7a6ca5d](https://github.com/CanadaHonk/porffor/commit/7a6ca5d32f56ff7d9b1fbaaf59fafede5a496771) | parser: drop __proto__ scope slot workaround | Removes parser workaround after __proto__ fixes; class self-name patch still needed. |
63
+ | [43087d4](https://github.com/CanadaHonk/porffor/commit/43087d42f0b90d14f04f3cddac034822560aefad) | ci: update expected test262 harness passes | Upstream conformance expectation update; does not establish Sproutboat support. |
64
+
65
+ ## Validation
66
+
67
+ The isolated release package suite passed 174 tests, and the CLI release suite passed 103. Both release kitchen-sink harnesses passed: 35 broker checks and 38 standalone checks. All 21 small examples passed on alpha-16. Native crypto, Unicode broker round trips and live Web API iterator checks passed. All 32 compatibility fixtures compile and 30 match Bun; the two existing non-ISO date parsing mismatches remain. The full native feature regression is in src/alpha16-native.test.ts; live Web API iterator coverage is in ../runtime/test/native-iterators.ts. The scorecard ties compatibility results to this exact source pin and patch inventory.
@@ -1,29 +1,32 @@
1
1
  # Porffor pin scorecard
2
2
 
3
- Pin: alpha-13 (547c7815125b6f02474591950f8b0dd7031a03f0). Compatibility report: 2026-10-01T10:57:31.463Z.
3
+ Pin: alpha-16 (43087d42f0b90d14f04f3cddac034822560aefad). Compatibility report: 2026-10-10T08:51:03.637Z.
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 | 3212 / 4703 |
10
- | Median / p95 binary, bytes | 966864 / 999936 |
11
- | Patch markers | 28 |
12
- | Upstream files written | 6 |
9
+ | Median / p95 compile, ms | 13266 / 33619 |
10
+ | Median / p95 binary, bytes | 1082960 / 1099536 |
11
+ | Patch markers | 35 |
12
+ | Upstream files written | 9 |
13
13
  | Fresh patch application | pass |
14
14
 
15
- Previous snapshot: alpha-10-08ac7ee.json. Matches 30/32 to 30/32; patch markers 27 to 28.
15
+ Previous snapshot: alpha-15-72d048d.json. Matches 30/32 to 30/32; patch markers 38 to 35.
16
16
 
17
17
  ## Patch inventory
18
18
 
19
- - `compiler/render.js`: PORT_MARKER, CONSOLE_MARKER, BYTESTRING_MARKER, READ_RAW_MARKER, CORO_STACK_MARKER
20
- - `compiler/index.js`: LINK_MARKER, CFLAGS_MARKER
21
- - `compiler/builtins/typedarray.js`: TYPED_ARRAY_FROM_MARKER
19
+ - `compiler/render.js`: PORT_MARKER, CONSOLE_MARKER, BYTESTRING_MARKER, READ_RAW_MARKER
20
+ - `compiler/index.js`: LINK_MARKER, CFLAGS_MARKER, FP_CONTRACT_MARKER, HTTP10_IMPORT_MARKER, HTTP10_CALL_MARKER
22
21
  - `compiler/builtins/date.ts`: DATE_PARSER_MARKER
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
22
+ - `compiler/builtins/promise.ts`: PROMISE_NULL_MARKER
23
+ - `compiler/builtins/string.ts`: REPLACE_ALL_MARKER
24
+ - `compiler/codegen.js`: TA_GET_MARKER, TA_SET_BOUNDS_MARKER
25
+ - `compiler/parse.js`: CLASS_SELF_MARKER
26
+ - `compiler/uwebsockets.js`: HTTP10_SHIM_MARKER, 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
27
  - `compiler/builtins_precompiled.js`: regenerated builtin table
25
28
 
26
- Patch source SHA-256: `6f607ac6b66fc4a51c2bf9e9525d0ab7f4e9b9dd3f942a820bbad46b7e3dbf56`.
29
+ Patch source SHA-256: `65a48b098fb6bd355785684774be43524d31491c89be1f6b0dee17f24b9cdd20`.
27
30
 
28
31
  ## Known behavior gaps
29
32
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutboat/toolchain",
3
- "version": "0.4.24",
3
+ "version": "0.6.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -0,0 +1,77 @@
1
+ {
2
+ "pin": "alpha-15 (72d048d73d49e217631ac8a1bc4fa9070d22fbc9)",
3
+ "reportGeneratedAt": "2026-10-05T00:30:02.525Z",
4
+ "fixtures": 32,
5
+ "compiled": 32,
6
+ "matched": 30,
7
+ "medianCompileMs": 420,
8
+ "p95CompileMs": 822,
9
+ "medianBinaryBytes": 1033344,
10
+ "p95BinaryBytes": 1049904,
11
+ "patchSourceSha256": "849997ab545a760019941886780b3fc70c89b67581cc9acf2b51ca6ff8a37d17",
12
+ "patchMarkerCount": 38,
13
+ "patchTargets": {
14
+ "compiler/render.js": [
15
+ "PORT_MARKER",
16
+ "CONSOLE_MARKER",
17
+ "BYTESTRING_MARKER",
18
+ "READ_RAW_MARKER"
19
+ ],
20
+ "compiler/index.js": [
21
+ "LINK_MARKER",
22
+ "CFLAGS_MARKER",
23
+ "FP_CONTRACT_MARKER",
24
+ "HTTP10_IMPORT_MARKER",
25
+ "HTTP10_CALL_MARKER"
26
+ ],
27
+ "compiler/builtins/typedarray.js": [
28
+ "TYPED_ARRAY_FROM_MARKER",
29
+ "TA_SET_OFFSET_MARKER",
30
+ "TA_JOIN_MARKER"
31
+ ],
32
+ "compiler/builtins/date.ts": [
33
+ "DATE_PARSER_MARKER"
34
+ ],
35
+ "compiler/builtins/promise.ts": [
36
+ "PROMISE_NULL_MARKER"
37
+ ],
38
+ "compiler/builtins/string.ts": [
39
+ "REPLACE_ALL_MARKER"
40
+ ],
41
+ "compiler/codegen.js": [
42
+ "TA_GET_MARKER",
43
+ "TA_SET_BOUNDS_MARKER"
44
+ ],
45
+ "compiler/parse.js": [
46
+ "CLASS_SELF_MARKER"
47
+ ],
48
+ "compiler/uwebsockets.js": [
49
+ "HTTP10_SHIM_MARKER",
50
+ "BODY_MARKER",
51
+ "STATUS_SIG_MARKER",
52
+ "STATUS_303_MARKER",
53
+ "STATUS_DEFAULT_MARKER",
54
+ "HDR_SIG_MARKER",
55
+ "HDR_CALL_MARKER",
56
+ "HDR_CAP_MARKER",
57
+ "HDR_SKIP_MARKER",
58
+ "HDR_APPEND_MARKER",
59
+ "READ_RAW_DECL_MARKER",
60
+ "FORBIDDEN_HDR_MARKER",
61
+ "WRITE_RESPONSE_MARKER",
62
+ "R2_TRANSFER_MARKER",
63
+ "R2_TRANSFER_CALL_MARKER",
64
+ "R2_DOWNLOAD_CACHE_MARKER",
65
+ "R2_RANGE_CACHE_MARKER",
66
+ "R2_CONDITIONAL_CACHE_MARKER",
67
+ "R2_STREAMING_CACHE_MARKER",
68
+ "R2_RANGE_HEADER_CACHE_MARKER"
69
+ ],
70
+ "compiler/builtins_precompiled.js": []
71
+ },
72
+ "patchApplication": "pass",
73
+ "mismatches": [
74
+ "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}",
75
+ "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\"}"
76
+ ]
77
+ }
@@ -0,0 +1,72 @@
1
+ {
2
+ "pin": "alpha-16 (43087d42f0b90d14f04f3cddac034822560aefad)",
3
+ "reportGeneratedAt": "2026-10-10T08:51:03.637Z",
4
+ "fixtures": 32,
5
+ "compiled": 32,
6
+ "matched": 30,
7
+ "medianCompileMs": 13266,
8
+ "p95CompileMs": 33619,
9
+ "medianBinaryBytes": 1082960,
10
+ "p95BinaryBytes": 1099536,
11
+ "patchSourceSha256": "65a48b098fb6bd355785684774be43524d31491c89be1f6b0dee17f24b9cdd20",
12
+ "patchMarkerCount": 35,
13
+ "patchTargets": {
14
+ "compiler/render.js": [
15
+ "PORT_MARKER",
16
+ "CONSOLE_MARKER",
17
+ "BYTESTRING_MARKER",
18
+ "READ_RAW_MARKER"
19
+ ],
20
+ "compiler/index.js": [
21
+ "LINK_MARKER",
22
+ "CFLAGS_MARKER",
23
+ "FP_CONTRACT_MARKER",
24
+ "HTTP10_IMPORT_MARKER",
25
+ "HTTP10_CALL_MARKER"
26
+ ],
27
+ "compiler/builtins/date.ts": [
28
+ "DATE_PARSER_MARKER"
29
+ ],
30
+ "compiler/builtins/promise.ts": [
31
+ "PROMISE_NULL_MARKER"
32
+ ],
33
+ "compiler/builtins/string.ts": [
34
+ "REPLACE_ALL_MARKER"
35
+ ],
36
+ "compiler/codegen.js": [
37
+ "TA_GET_MARKER",
38
+ "TA_SET_BOUNDS_MARKER"
39
+ ],
40
+ "compiler/parse.js": [
41
+ "CLASS_SELF_MARKER"
42
+ ],
43
+ "compiler/uwebsockets.js": [
44
+ "HTTP10_SHIM_MARKER",
45
+ "BODY_MARKER",
46
+ "STATUS_SIG_MARKER",
47
+ "STATUS_303_MARKER",
48
+ "STATUS_DEFAULT_MARKER",
49
+ "HDR_SIG_MARKER",
50
+ "HDR_CALL_MARKER",
51
+ "HDR_CAP_MARKER",
52
+ "HDR_SKIP_MARKER",
53
+ "HDR_APPEND_MARKER",
54
+ "READ_RAW_DECL_MARKER",
55
+ "FORBIDDEN_HDR_MARKER",
56
+ "WRITE_RESPONSE_MARKER",
57
+ "R2_TRANSFER_MARKER",
58
+ "R2_TRANSFER_CALL_MARKER",
59
+ "R2_DOWNLOAD_CACHE_MARKER",
60
+ "R2_RANGE_CACHE_MARKER",
61
+ "R2_CONDITIONAL_CACHE_MARKER",
62
+ "R2_STREAMING_CACHE_MARKER",
63
+ "R2_RANGE_HEADER_CACHE_MARKER"
64
+ ],
65
+ "compiler/builtins_precompiled.js": []
66
+ },
67
+ "patchApplication": "pass",
68
+ "mismatches": [
69
+ "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}",
70
+ "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\"}"
71
+ ]
72
+ }
@@ -27,8 +27,14 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
27
27
  " return 0;\n" +
28
28
  " }\n" +
29
29
  " return -1;\n" +
30
- "}\n" +
31
- "#define PORF_CORO_STACK_SIZE (256u * 1024u)\n",
30
+ "}\n",
31
+ "compiler/parse.js":
32
+ "import link from './modules.js';\n" +
33
+ "export default (input) => {\n" +
34
+ " const ast = parse(input);\n" +
35
+ " if (ast._ts) globalThis.typedInput = Prefs.optTypes;\n" +
36
+ " return ast;\n" +
37
+ "};\n",
32
38
  "compiler/index.js":
33
39
  "import { hashId } from './modules.js';\n" +
34
40
  " const uwsDir = uwebsockets.ensureUWebSockets();\n" +
@@ -123,8 +129,7 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
123
129
  "compiler/builtins/json.ts": "// sb_json_utf16_v1\n",
124
130
  "runtime/fetch-globals.js": "// sb_text_encoder_scalar_v1\n",
125
131
  "compiler/codegen.js":
126
- " stmt(scope, Store(ctype, addr, 4, ctype === 'f64' || ctype === 'f32' ? f : signed ? Convert(T.i32, f) : Convert(T.u32, f, 0)));\n" +
127
- " return Box(Convert(T.f64, Un('~', T.i32, Convert(T.i32, numValue(toNumeric())))), Const(T.i32, TYPES.number));\n" +
132
+ " stmt(scope, Store(ctype, addr, 4, ctype === 'f64' || ctype === 'f32' ? f : toUint32(scope, f)));\n" +
128
133
  " const taGet = (ctype, size, signed = true) => () => {\n" +
129
134
  " const loaded = Load(ctype, taAddr(size), 4);\n" +
130
135
  " const f = ctype === 'f32' || ctype === 'f64' ? loaded : Convert(T.f64, loaded, signed ? CONVERT_SIGNED : 0);\n" +
@@ -220,22 +225,21 @@ test("the default call (no url/expectedSha256 override) never touches the networ
220
225
  expect(await readFile(join(dir, "runtime/index.js"), "utf8")).not.toBe("");
221
226
  // #242: generated typed-array join/toString keep a plain-array `parts`.
222
227
  expect(await readFile(join(dir, "compiler/builtins/typedarray.js"), "utf8")).toContain(
223
- ".replaceAll('const parts: ' + name + ' =', 'const parts: any[] =')",
228
+ "return Porffor.callThis(__Array_prototype_join, out, sep);",
224
229
  );
225
230
  // #238: TypedArray.prototype.set(source) with no offset copies to index 0.
226
231
  expect(await readFile(join(dir, "compiler/builtins/typedarray.js"), "utf8")).toContain(
227
- "offset = ecma262.ToIntegerOrInfinity(offset);",
232
+ "const targetOffsetNum: number = ecma262.ToIntegerOrInfinity(offset);",
228
233
  );
229
- // #238: ~ wraps (ToInt32), and out-of-range typed-array reads are undefined.
234
+ // #238: out-of-range typed-array reads are undefined. (The store and `~`
235
+ // conversions #238 also patched are upstream's own code since alpha-14.)
230
236
  const codegen = await readFile(join(dir, "compiler/codegen.js"), "utf8");
231
- expect(codegen).toContain("Convert(T.i32, toUint32(scope, numValue(toNumeric())), CONVERT_RANGE_KNOWN | CONVERT_SIGNED)");
232
237
  expect(codegen).toContain("Box(f, Const(T.i32, TYPES.number)), valUndefined());");
233
238
  // #241: out-of-range typed-array writes are ignored in user code.
234
239
  expect(codegen).toContain("if (globalThis.precompile) store();");
235
- // #238: integer typed-array stores wrap modulo 2^n instead of saturating.
236
- expect(await readFile(join(dir, "compiler/codegen.js"), "utf8")).toContain(
237
- "signed ? Convert(T.i32, toUint32(scope, f), CONVERT_RANGE_KNOWN | CONVERT_SIGNED) : toUint32(scope, f)",
238
- );
240
+ // #256: named class expressions inside functions are rewritten at parse time.
241
+ expect(await readFile(join(dir, "compiler/parse.js"), "utf8")).toContain("sbClassSelf(ast); // sproutboat #256");
242
+ expect(await readFile(join(dir, "compiler/sb-class-self.js"), "utf8")).toContain("sproutboat #256");
239
243
  // #237: replaceAll joins its pieces once instead of a strcat per match.
240
244
  expect(await readFile(join(dir, "compiler/builtins/string.ts"), "utf8")).toContain(
241
245
  "return Porffor.callThis(__Array_prototype_join, pieces, '');",
@@ -251,7 +255,7 @@ test("the default call (no url/expectedSha256 override) never touches the networ
251
255
  expect(await readFile(join(dir, "compiler/builtins/promise.ts"), "utf8")).toContain(
252
256
  "if (Porffor.type(value) == Porffor.TYPES.object && value != null) {",
253
257
  );
254
- });
258
+ }, 60_000);
255
259
 
256
260
  test("a corrupted warm cache is replaced from the verified archive", async () => {
257
261
  const { archive, sha256 } = await fixture();
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-547c781.tar.gz" with { type: "file" };
8
+ import vendoredPorfforArchive from "../vendor/porffor-43087d4.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,83 @@
1
+ import { expect, test } from "bun:test";
2
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { resolve } from "node:path";
5
+ import { ensurePorffor } from "./acquire";
6
+
7
+ // Compare native output with Bun, including trap effects and iterator state.
8
+ const program = `
9
+ const target = { x: 1 };
10
+ let calls = 0;
11
+ const proxy = new Proxy(target, {
12
+ get(t, key, receiver) { calls++; return key === "virtual" ? 7 : Reflect.get(t, key, receiver); },
13
+ set(t, key, value) { t[key] = value * 2; return true; },
14
+ has(t, key) { return key === "virtual" || key in t; },
15
+ deleteProperty(t, key) { delete t[key]; return true; }
16
+ });
17
+ proxy.x = 3;
18
+ console.log(proxy.x, proxy.virtual, calls, "virtual" in proxy);
19
+ delete proxy.x;
20
+ console.log("x" in target);
21
+ const keys = new Proxy({ a: 1 }, {
22
+ ownKeys() { return ["a"]; },
23
+ getOwnPropertyDescriptor(t, key) { return Object.getOwnPropertyDescriptor(t, key); }
24
+ });
25
+ console.log(Object.keys(keys).join(","));
26
+ const fn = new Proxy(function (x) { return x + 1; }, {
27
+ apply(t, self, args) { return t(args[0]) * 2; }
28
+ });
29
+ console.log(fn(3));
30
+ const ctor = new Proxy(function Item(x) { this.x = x; }, {
31
+ construct(t, args) { return { x: args[0] * 3 }; }
32
+ });
33
+ console.log(new ctor(4).x);
34
+ const revocable = Proxy.revocable({ x: 1 }, {});
35
+ console.log(revocable.proxy.x);
36
+ revocable.revoke();
37
+ try { console.log(revocable.proxy.x); } catch (e) { console.log(e instanceof TypeError); }
38
+ const frozen = Object.freeze({ x: 1 });
39
+ try { console.log(new Proxy(frozen, { get() { return 2; } }).x); } catch (e) { console.log(e instanceof TypeError); }
40
+ const iterator = [1, 2].values();
41
+ console.log(iterator.next().value, iterator.next().value, iterator.next().done);
42
+ console.log(Array.from(new Map([["a", 1], ["b", 2]]).keys()).join(","));
43
+ console.log([...new Set([1, 2, 1])].join(","));
44
+ console.log(Array.from("ab").join(","));
45
+ const matches = "a1a2".matchAll(/a(\\d)/g);
46
+ console.log(matches.next().value[1], matches.next().value[1], matches.next().done);
47
+ console.log(Uint8Array.from({ length: 3 }, (_, i) => i + 1).join("-"));
48
+ const bytes = new Uint8Array(3); bytes.set([4, 5]); console.log(bytes.toString());
49
+ console.log(new Uint8ClampedArray([0.5, 1.5, 2.5, 3.5]).join(","));
50
+ class Static { static value = this; static { this.x = 9; } }
51
+ console.log(Static.value === Static, Static.x);
52
+ const dict = Object.create(null); dict.__proto__ = 5;
53
+ console.log(dict.__proto__, Object.getPrototypeOf(dict) === null);
54
+ let order = "";
55
+ function left() { order += "l"; return 1; }
56
+ function right() { order += "r"; return 2; }
57
+ console.log(left() + right(), order);
58
+ console.log("caffè 東京 🚤");
59
+ `;
60
+
61
+ test("alpha-16: native Proxy, iterators and upstream fixes match Bun", async () => {
62
+ const root = await mkdtemp(resolve(tmpdir(), "sb-alpha16-native-"));
63
+ try {
64
+ const porffor = await ensurePorffor();
65
+ const source = resolve(root, "features.js");
66
+ const binary = resolve(root, "features");
67
+ await writeFile(source, program);
68
+ const reference = Bun.spawnSync(["bun", source], { stdout: "pipe", stderr: "pipe" });
69
+ expect(reference.exitCode).toBe(0);
70
+ await rm(binary, { force: true });
71
+ const compile = Bun.spawnSync(["bun", resolve(porffor, "runtime/index.js"), "native", source, "-o", binary, "-s"], {
72
+ cwd: root, stdout: "pipe", stderr: "pipe",
73
+ });
74
+ expect(compile.exitCode, Buffer.from(compile.stderr).toString()).toBe(0);
75
+ const run = Bun.spawnSync([binary], { stdout: "pipe", stderr: "pipe", timeout: 10_000 });
76
+ expect(run.exitCode, Buffer.from(run.stderr).toString()).toBe(0);
77
+ const ansi = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
78
+ const output = Buffer.from(run.stdout).toString().replace(ansi, "");
79
+ expect(output).toBe(Buffer.from(reference.stdout).toString());
80
+ } finally {
81
+ await rm(root, { recursive: true, force: true });
82
+ }
83
+ }, 60_000);
@@ -0,0 +1,43 @@
1
+ import { expect, test } from "bun:test";
2
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { resolve } from "node:path";
5
+ import { ensurePorffor } from "./acquire";
6
+
7
+ // baronunread/sproutboat#256: inside a function, a named class expression's own
8
+ // name must be the class itself. marked's minified Lexer has exactly this shape.
9
+ const program = `function inner() {
10
+ var R = class l { constructor(o) { this.o = o || E; } static make(o) { return new l(o); } lex(s) { return s.length; } static lex(s) { return new l().lex(s); } };
11
+ var E = { d: 1 };
12
+ var C = class k extends R { lex(s) { return super.lex(s) + 1; } static make() { return new k(); } };
13
+ var plain = class { static make() { return 7; } };
14
+ return [R === new R().constructor, R.make().o.d, R.lex("abc"), C.make().lex("ab"), C.name, plain.make()].join(" ");
15
+ }
16
+ async function viaAwait() {
17
+ var A = class m { static async load() { await null; return new m(); } ok() { return "ok"; } };
18
+ return (await A.load()).ok();
19
+ }
20
+ console.log(inner());
21
+ viaAwait().then((v) => console.log(v));
22
+ `;
23
+
24
+ test("#256: a named class expression inside a function sees itself", async () => {
25
+ const root = await mkdtemp(resolve(tmpdir(), "sb-class-self-"));
26
+ try {
27
+ const porffor = await ensurePorffor();
28
+ const source = resolve(root, "classes.js");
29
+ const binary = resolve(root, "classes");
30
+ await writeFile(source, program);
31
+ const compile = Bun.spawnSync(["bun", resolve(porffor, "runtime/index.js"), "native", source, "-o", binary, "-s"], {
32
+ cwd: root,
33
+ stdout: "pipe",
34
+ stderr: "pipe",
35
+ });
36
+ expect(compile.exitCode, Buffer.from(compile.stderr).toString()).toBe(0);
37
+ const run = Bun.spawnSync([binary], { stdout: "pipe", stderr: "pipe" });
38
+ const lines = Buffer.from(run.stdout).toString().replace(/\u001b\[[0-9;]*m/g, "").trim().split("\n");
39
+ expect(lines).toEqual(["true 1 3 3 k 7", "ok"]);
40
+ } finally {
41
+ await rm(root, { recursive: true, force: true });
42
+ }
43
+ }, 60_000);
package/src/patch.test.ts CHANGED
@@ -36,7 +36,6 @@ int porf_native_fetch_read_value(jsval value, const char** out_buf, size_t* out_
36
36
  return -1;
37
37
  }
38
38
 
39
- #define PORF_CORO_STACK_SIZE (256u * 1024u)
40
39
  #define PORF_CORO_MAX 16384
41
40
  `;
42
41
 
@@ -294,10 +293,6 @@ test("#165: render.js routes console output to stderr, unbuffered, idempotently"
294
293
  "int porf_native_fetch_read_value(jsval value, const char** out_buf, size_t* out_len, char** out_owned) {",
295
294
  );
296
295
 
297
- // #178: fiber stacks big enough for -O0 frames in a few nested awaits.
298
- expect(once).toContain("#define PORF_CORO_STACK_SIZE (8u * 1024u * 1024u)");
299
- expect(once).not.toContain("(256u * 1024u)");
300
-
301
296
  await patchRenderJs(root);
302
297
  expect(await readFile(join(root, "compiler/render.js"), "utf8")).toBe(once);
303
298
  } finally {
package/src/patch.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Idempotent, marker-guarded in-place edits to Porffor's generated C
3
3
  * (`compiler/render.js`, `compiler/index.js`, `compiler/uwebsockets.js`,
4
- * `compiler/builtins/typedarray.js`, `compiler/builtins/date.ts`). Run
4
+ * `compiler/builtins/date.ts`). Run
5
5
  * from the build path, not a `postinstall` hook: package managers block
6
6
  * dependency lifecycle scripts by default, so a published `postinstall` would
7
7
  * silently not run.
@@ -131,24 +131,6 @@ const READ_RAW_INJECT =
131
131
  READ_RAW_ANCHOR;
132
132
  const READ_RAW_MARKER = "porf_native_fetch_read_raw_bytes(jsval value";
133
133
 
134
- /**
135
- * baronunread/sproutboat#178: every async function runs on its own fiber
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.
141
- * -O3 frames are smaller, which only raises the threshold: a deep enough
142
- * async chain hits it in a release build too.
143
- *
144
- * Match a thread's usual 8 MiB. The reservation is MAP_NORESERVE with a guard
145
- * page, so only touched pages cost memory and a shallow handler pays nothing.
146
- */
147
- const CORO_STACK_ANCHOR = "#define PORF_CORO_STACK_SIZE (256u * 1024u)";
148
- const CORO_STACK_INJECT =
149
- "// sproutboat #178: 256 KiB overflows -O0 frames in a few nested awaits.\n" +
150
- "#define PORF_CORO_STACK_SIZE (8u * 1024u * 1024u)";
151
- const CORO_STACK_MARKER = "sproutboat #178";
152
134
 
153
135
  /**
154
136
  * render.js full-block replacements: `[marker, anchor, inject, what]`, each a
@@ -158,7 +140,6 @@ const CORO_STACK_MARKER = "sproutboat #178";
158
140
  const RENDER_REPLACEMENTS = [
159
141
  [BYTESTRING_MARKER, BYTESTRING_ANCHOR, BYTESTRING_INJECT, "bytestring UTF-8 encoding (#172)"],
160
142
  [READ_RAW_MARKER, READ_RAW_ANCHOR, READ_RAW_INJECT, "raw bytestring passthrough function (#176)"],
161
- [CORO_STACK_MARKER, CORO_STACK_ANCHOR, CORO_STACK_INJECT, "coroutine stack size (#178)"],
162
143
  ] as const;
163
144
 
164
145
  // #15 — no `--port` flag: Porffor's native-fetch entry point calls
@@ -180,6 +161,95 @@ const LINK_INJECT =
180
161
  " ...(process.env.SB_EXTRA_LINK ? process.env.SB_EXTRA_LINK.split(' ').filter(Boolean) : []),\n";
181
162
  const LINK_MARKER = "SB_EXTRA_LINK";
182
163
 
164
+ /**
165
+ * baronunread/sproutboat#256: inside a function, Porffor gives a named class
166
+ * expression's own name, as seen from its methods, a fresh function object
167
+ * instead of the class. So `var R = class l { static lex() { return new l() } }`
168
+ * builds an object with none of the class's methods ("value is not a
169
+ * constructor"). Top level is fine. Since #238 every handler runs inside a
170
+ * function, and bundlers emit exactly this shape for classes whose static
171
+ * members refer to themselves (marked's Lexer and Parser, esbuild's
172
+ * `class _X`).
173
+ *
174
+ * The rewrite keeps the meaning: the name becomes an ordinary const, which
175
+ * Porffor captures correctly, and `const l = class {}` still names it "l".
176
+ *
177
+ * class l { ... } -> (() => { const l = class { ... }; return l; })()
178
+ *
179
+ * Only named class expressions inside a function whose body refers to the name.
180
+ * Skipped when `extends` or a computed key holds await/yield, which an arrow
181
+ * would change.
182
+ */
183
+ const CLASS_SELF_HELPER = String.raw`// sproutboat #256: a named class expression's own name inside a function.
184
+ const isFn = (n) => n.type === 'FunctionDeclaration' || n.type === 'FunctionExpression' || n.type === 'ArrowFunctionExpression';
185
+
186
+ const mentions = (node, name) => {
187
+ if (node == null || typeof node !== 'object') return false;
188
+ if (Array.isArray(node)) return node.some((x) => mentions(x, name));
189
+ if (node.type === 'Identifier') return node.name === name;
190
+ for (const key in node) {
191
+ if (key[0] === '_' || key === 'start' || key === 'end' || key === 'loc' || key === 'range') continue;
192
+ if (mentions(node[key], name)) return true;
193
+ }
194
+ return false;
195
+ };
196
+
197
+ const suspends = (node) => {
198
+ if (node == null || typeof node !== 'object') return false;
199
+ if (Array.isArray(node)) return node.some(suspends);
200
+ if (node.type === 'AwaitExpression' || node.type === 'YieldExpression') return true;
201
+ if (isFn(node) || node.type === 'ClassExpression' || node.type === 'ClassDeclaration') return false;
202
+ for (const key in node) {
203
+ if (key[0] === '_' || key === 'start' || key === 'end' || key === 'loc' || key === 'range') continue;
204
+ if (suspends(node[key])) return true;
205
+ }
206
+ return false;
207
+ };
208
+
209
+ const rewrite = (cls) => {
210
+ const name = cls.id.name;
211
+ const at = { start: cls.start, end: cls.end };
212
+ const id = () => ({ type: 'Identifier', name, ...at });
213
+ return {
214
+ type: 'CallExpression', optional: false, arguments: [], ...at,
215
+ callee: {
216
+ type: 'ArrowFunctionExpression', id: null, params: [], async: false, generator: false, expression: false, ...at,
217
+ body: {
218
+ type: 'BlockStatement', ...at,
219
+ body: [
220
+ { type: 'VariableDeclaration', kind: 'const', ...at, declarations: [
221
+ { type: 'VariableDeclarator', id: id(), init: { ...cls, id: null }, ...at },
222
+ ] },
223
+ { type: 'ReturnStatement', argument: id(), ...at },
224
+ ],
225
+ },
226
+ },
227
+ };
228
+ };
229
+
230
+ const walk = (node, inFn) => {
231
+ if (node == null || typeof node !== 'object') return node;
232
+ if (Array.isArray(node)) {
233
+ for (let i = 0; i < node.length; i++) node[i] = walk(node[i], inFn);
234
+ return node;
235
+ }
236
+ const childInFn = inFn || isFn(node);
237
+ for (const key in node) {
238
+ if (key[0] === '_' || key === 'start' || key === 'end' || key === 'loc' || key === 'range') continue;
239
+ const value = node[key];
240
+ if (value && typeof value === 'object') node[key] = walk(value, childInFn);
241
+ }
242
+ if (
243
+ inFn && node.type === 'ClassExpression' && node.id?.name &&
244
+ mentions(node.body, node.id.name) &&
245
+ !suspends(node.superClass) && !node.body.body.some((m) => m.computed && suspends(m.key))
246
+ ) return rewrite(node);
247
+ return node;
248
+ };
249
+
250
+ export default (ast) => (globalThis.precompile ? ast : walk(ast, false));
251
+ `;
252
+
183
253
  /**
184
254
  * #15 — and the same for the compile step, so the prelude's inline C can
185
255
  * `#include <bearssl.h>`. The link patch alone is not enough: Porffor's
@@ -291,30 +361,6 @@ const REPLACE_ALL_EDITS = [
291
361
  ] as const;
292
362
  const REPLACE_ALL_MARKER = "sproutboat #237: pieces joined once";
293
363
 
294
- // baronunread/sproutboat#238: an integer typed-array element store converted
295
- // its value with a saturating f64 -> int Convert, so a negative value stored
296
- // into a Uint32Array became 0 and 4e9 into an Int32Array became 2147483647.
297
- // ECMA-262 (IntegerIndexedElementSet -> ToUint32 / ToInt32 / ToUint16 ...) wraps
298
- // modulo 2^n instead. noble-hashes' SHA-256 stores `x | 0` into a Uint32Array,
299
- // so every negative schedule word was zeroed and the digest came out wrong.
300
- // Route the store through codegen's own modular toUint32 (what bitwise ops use);
301
- // narrower stores keep the low bits, which is exactly the modular result.
302
- const TA_STORE_ANCHOR =
303
- " stmt(scope, Store(ctype, addr, 4, ctype === 'f64' || ctype === 'f32' ? f : signed ? Convert(T.i32, f) : Convert(T.u32, f, 0)));";
304
- const TA_STORE_INJECT =
305
- " // sproutboat #238: typed-array stores wrap modulo 2^n (ToUint32), not saturate.\n" +
306
- " stmt(scope, Store(ctype, addr, 4, ctype === 'f64' || ctype === 'f32' ? f : signed ? Convert(T.i32, toUint32(scope, f), CONVERT_RANGE_KNOWN | CONVERT_SIGNED) : toUint32(scope, f)));";
307
- const TA_STORE_MARKER = "sproutboat #238: typed-array stores wrap";
308
- // baronunread/sproutboat#238: unary `~` had the same saturating conversion, so
309
- // ~x for x >= 2^31 (or ~~x, the common truncation idiom) clamped to
310
- // +/-2147483648, and ~Infinity gave -2147483648 instead of -1. uuid's SHA-1
311
- // computes `x & y ^ ~x & z` on words above 2^31, so every v5 UUID was wrong.
312
- const BITNOT_ANCHOR =
313
- " return Box(Convert(T.f64, Un('~', T.i32, Convert(T.i32, numValue(toNumeric())))), Const(T.i32, TYPES.number));";
314
- const BITNOT_INJECT =
315
- " // sproutboat #238: ~ applies ToInt32 (wrap modulo 2^32), not a saturating cast.\n" +
316
- " return Box(Convert(T.f64, Un('~', T.i32, Convert(T.i32, toUint32(scope, numValue(toNumeric())), CONVERT_RANGE_KNOWN | CONVERT_SIGNED))), Const(T.i32, TYPES.number));";
317
- const BITNOT_MARKER = "sproutboat #238: ~ applies ToInt32";
318
364
  // baronunread/sproutboat#238: typed-array element reads and writes had no
319
365
  // bounds check. A read past the end returned whatever sat in the next
320
366
  // allocation instead of undefined (uuid's SHA-1 reads past its buffer, so v5
@@ -350,12 +396,13 @@ const TA_GET_MARKER = "sproutboat #238: only a valid in-range integer index read
350
396
  // whatever memory followed it (and a[-1] = x wrote a[0], via the saturating
351
397
  // index conversion). ECMA-262 ignores the write. Same check as taGet, in user
352
398
  // code only: builtins such as the constructor store elements before they set
353
- // the length, and they only write in bounds anyway. Applies after TA_STORE,
354
- // whose output line is the anchor.
399
+ // the length, and they only write in bounds anyway. Alpha-14 fixed the store's
400
+ // conversion upstream (ToUint32, which our #238 patch used to add); the bounds
401
+ // check is still missing there.
355
402
  const TA_SET_BOUNDS_ANCHOR =
356
- " stmt(scope, Store(ctype, addr, 4, ctype === 'f64' || ctype === 'f32' ? f : signed ? Convert(T.i32, toUint32(scope, f), CONVERT_RANGE_KNOWN | CONVERT_SIGNED) : toUint32(scope, f)));";
403
+ " stmt(scope, Store(ctype, addr, 4, ctype === 'f64' || ctype === 'f32' ? f : toUint32(scope, f)));";
357
404
  const TA_SET_BOUNDS_INJECT =
358
- " const store = () => stmt(scope, Store(ctype, addr, 4, ctype === 'f64' || ctype === 'f32' ? f : signed ? Convert(T.i32, toUint32(scope, f), CONVERT_RANGE_KNOWN | CONVERT_SIGNED) : toUint32(scope, f)));\n" +
405
+ " const store = () => stmt(scope, Store(ctype, addr, 4, ctype === 'f64' || ctype === 'f32' ? f : toUint32(scope, f)));\n" +
359
406
  " // sproutboat #241: a write outside the array is ignored, not stored into the next allocation.\n" +
360
407
  " if (globalThis.precompile) store();\n" +
361
408
  " else {\n" +
@@ -364,50 +411,6 @@ const TA_SET_BOUNDS_INJECT =
364
411
  " }";
365
412
  const TA_SET_BOUNDS_MARKER = "sproutboat #241: a write outside the array is ignored";
366
413
 
367
- // baronunread/sproutboat#238: TypedArray.prototype.set(source) with no offset
368
- // ran `offset = Math.trunc(undefined)`, which is NaN. The same-type fast path
369
- // then copied to `base + NaN * BYTES_PER_ELEMENT`: nothing landed where it
370
- // should (noble's HMAC lost its key in `pad.set(key)`), and the write went
371
- // somewhere else in memory. ECMA-262 uses ToIntegerOrInfinity, which maps
372
- // undefined and NaN to 0; subarray right below already does.
373
- const TA_SET_OFFSET_ANCHOR = " offset = Math.trunc(offset);\n if (Porffor.fastOr(offset < 0, offset > len)) throw new RangeError('Offset out of bounds');";
374
- const TA_SET_OFFSET_INJECT =
375
- " offset = ecma262.ToIntegerOrInfinity(offset); // sproutboat #238: undefined offset is 0, not NaN\n if (Porffor.fastOr(offset < 0, offset > len)) throw new RangeError('Offset out of bounds');";
376
- const TA_SET_OFFSET_MARKER = "sproutboat #238: undefined offset is 0";
377
-
378
- // baronunread/sproutboat#242: typedarray.js generates each TypedArray method
379
- // from array.ts with `.replaceAll('any[]', name)`. That also retyped join's and
380
- // toString's internal `const parts: any[]` helper as e.g. a Uint8Array, so the
381
- // joined strings were stored into a "typed array": Uint8Array join returned
382
- // garbage and Uint16Array join crashed. Put `parts` back to a plain array after
383
- // the replacement; map/filter's intentionally typed `out` is untouched.
384
- const TA_JOIN_ANCHOR = ".replaceAll('Array', name).replaceAll('any[]', name) + '\\n\\n'";
385
- const TA_JOIN_INJECT =
386
- ".replaceAll('Array', name).replaceAll('any[]', name).replaceAll('const parts: ' + name + ' =', 'const parts: any[] =') /* sproutboat #242 */ + '\\n\\n'";
387
- const TA_JOIN_MARKER = "sproutboat #242";
388
-
389
- // Porffor's TypedArray.from only handles iterables. An array-like input such
390
- // as { length: 16 } silently becomes an empty typed array, including HMAC
391
- // keys made with Uint8Array.from({ length: 16 }, mapFn). The compiler uses a
392
- // precompiled builtin table, so the edited source must be precompiled again.
393
- const TYPED_ARRAY_FROM_ANCHOR =
394
- " len = i;\n }\n\n arr.length = len;\n\n return new ${name}(arr);\n};";
395
- const TYPED_ARRAY_FROM_INJECT =
396
- " len = i;\n" +
397
- " } else {\n" +
398
- " // sproutboat: TypedArray.from accepts array-like objects.\n" +
399
- " let count = ecma262.ToIntegerOrInfinity(Porffor.object.get(arg, 'length'));\n" +
400
- " if (count < 0) count = 0;\n" +
401
- " if (count > 2147483643) throw new RangeError('Invalid TypedArray length (over maximum supported length)');\n" +
402
- " if (Porffor.type(mapFn) != Porffor.TYPES.undefined && Porffor.type(mapFn) != Porffor.TYPES.function)\n" +
403
- " throw new TypeError('Called TypedArray.from with a non-function mapFn');\n" +
404
- " for (let i: i32 = 0; i < count; i++) {\n" +
405
- " arr[i] = Porffor.type(mapFn) == Porffor.TYPES.undefined ? arg[i] : mapFn(arg[i], i);\n" +
406
- " }\n" +
407
- " len = count;\n" +
408
- " }\n\n arr.length = len;\n\n return new ${name}(arr);\n};";
409
- const TYPED_ARRAY_FROM_MARKER = "sproutboat: TypedArray.from accepts array-like objects";
410
-
411
414
  // Porffor alpha 9 treats +02:00 as two milliseconds and adds offset hours
412
415
  // instead of subtracting them. Count fields even when they contain zero, and
413
416
  // apply the signed offset after parsing the local wall-clock fields.
@@ -459,8 +462,12 @@ const DATE_PARSER_INJECT = ` // sproutboat: ISO timezone offset
459
462
  const endPtr: i32 = Porffor.IR.ptr(string) + len;
460
463
  let ptr: i32 = Porffor.IR.ptr(string);
461
464
 
465
+ // Loop once past the last character to flush the final number, but read a
466
+ // 0 there, not the byte after the string: if that byte happened to be '+' or
467
+ // '-' it flipped the offset's sign.
462
468
  while (ptr <= endPtr) {
463
- const chr: i32 = Porffor.IR.loadU8(ptr++, 4);
469
+ const chr: i32 = ptr < endPtr ? Porffor.IR.loadU8(ptr, 4) : 0;
470
+ ptr++;
464
471
  if (Porffor.fastAnd(chr >= 48, chr <= 57)) {
465
472
  n = n * 10 + chr - 48;
466
473
  digits++;
@@ -1132,6 +1139,28 @@ export async function patchUwebsockets(root: string): Promise<void> {
1132
1139
  if (changed) await writeFile(file, src);
1133
1140
  }
1134
1141
 
1142
+ const CLASS_SELF_IMPORT_ANCHOR = "import link from './modules.js';";
1143
+ const CLASS_SELF_IMPORT_INJECT = "import link from './modules.js';\nimport sbClassSelf from './sb-class-self.js';";
1144
+ const CLASS_SELF_CALL_ANCHOR = " if (ast._ts) globalThis.typedInput = Prefs.optTypes;";
1145
+ const CLASS_SELF_CALL_INJECT = " sbClassSelf(ast); // sproutboat #256\n if (ast._ts) globalThis.typedInput = Prefs.optTypes;";
1146
+ const CLASS_SELF_MARKER = "sproutboat #256";
1147
+
1148
+ /** #256 — see CLASS_SELF_HELPER. */
1149
+ export async function patchClassSelfName(root: string): Promise<void> {
1150
+ await writeFile(resolve(root, "compiler/sb-class-self.js"), CLASS_SELF_HELPER);
1151
+ const file = resolve(root, "compiler/parse.js");
1152
+ let src = await readFile(file, "utf8");
1153
+ if (src.includes(CLASS_SELF_MARKER)) return;
1154
+ for (const [anchor, what] of [
1155
+ [CLASS_SELF_IMPORT_ANCHOR, "import"],
1156
+ [CLASS_SELF_CALL_ANCHOR, "call"],
1157
+ ] as const) {
1158
+ if (!src.includes(anchor)) throw new Error(`could not patch Porffor's class self-name rewrite (${what}): anchor not found in ${file}`);
1159
+ }
1160
+ src = src.replace(CLASS_SELF_IMPORT_ANCHOR, CLASS_SELF_IMPORT_INJECT).replace(CLASS_SELF_CALL_ANCHOR, CLASS_SELF_CALL_INJECT);
1161
+ await writeFile(file, src);
1162
+ }
1163
+
1135
1164
  async function patchCompilerArgs(root: string): Promise<void> {
1136
1165
  await writeFile(resolve(root, "compiler/sb-http10.js"), HTTP10_HELPER);
1137
1166
  const file = resolve(root, "compiler/index.js");
@@ -1176,7 +1205,7 @@ export async function patchDateParser(root: string): Promise<void> {
1176
1205
  await writeFile(file, src.slice(0, start) + parser.replace(DATE_PARSER_ANCHOR, DATE_PARSER_INJECT) + src.slice(end));
1177
1206
  }
1178
1207
 
1179
- /** #168: keep a promise resolving to null out of the `then` probe. Precompiled by patchTypedArrayFrom. */
1208
+ /** #168: keep a promise resolving to null out of the `then` probe. */
1180
1209
  export async function patchPromiseResolveNull(root: string): Promise<void> {
1181
1210
  const file = resolve(root, "compiler/builtins/promise.ts");
1182
1211
  const src = await readFile(file, "utf8");
@@ -1186,7 +1215,7 @@ export async function patchPromiseResolveNull(root: string): Promise<void> {
1186
1215
  await writeFile(file, src.replace(PROMISE_NULL_ANCHOR, PROMISE_NULL_INJECT));
1187
1216
  }
1188
1217
 
1189
- /** #237: linear String.prototype.replaceAll. Precompiled by patchTypedArrayFrom. */
1218
+ /** #237: linear String.prototype.replaceAll. Precompiled by precompileBuiltins. */
1190
1219
  export async function patchReplaceAll(root: string): Promise<void> {
1191
1220
  const file = resolve(root, "compiler/builtins/string.ts");
1192
1221
  let src = await readFile(file, "utf8");
@@ -1209,8 +1238,6 @@ export async function patchTypedArrayStore(root: string): Promise<void> {
1209
1238
  let src = await readFile(file, "utf8");
1210
1239
  let changed = false;
1211
1240
  for (const [marker, anchor, inject, what] of [
1212
- [TA_STORE_MARKER, TA_STORE_ANCHOR, TA_STORE_INJECT, "typed-array store conversion"],
1213
- [BITNOT_MARKER, BITNOT_ANCHOR, BITNOT_INJECT, "bitwise NOT conversion"],
1214
1241
  [TA_GET_MARKER, TA_GET_ANCHOR, TA_GET_INJECT, "typed-array read bounds"],
1215
1242
  [TA_SET_BOUNDS_MARKER, TA_SET_BOUNDS_ANCHOR, TA_SET_BOUNDS_INJECT, "typed-array write bounds"],
1216
1243
  ] as const) {
@@ -1222,27 +1249,8 @@ export async function patchTypedArrayStore(root: string): Promise<void> {
1222
1249
  if (changed) await writeFile(file, src);
1223
1250
  }
1224
1251
 
1225
- export async function patchTypedArrayFrom(root: string): Promise<void> {
1226
- const file = resolve(root, "compiler/builtins/typedarray.js");
1227
- const src = await readFile(file, "utf8");
1228
- if (!src.includes(TYPED_ARRAY_FROM_MARKER) && !src.includes(TYPED_ARRAY_FROM_ANCHOR)) {
1229
- throw new Error(`could not patch Porffor's TypedArray.from: anchor not found in ${file}`);
1230
- }
1231
- let edited = src;
1232
- if (!edited.includes(TYPED_ARRAY_FROM_MARKER)) edited = edited.replace(TYPED_ARRAY_FROM_ANCHOR, TYPED_ARRAY_FROM_INJECT);
1233
- if (!edited.includes(TA_JOIN_MARKER)) {
1234
- if (!edited.includes(TA_JOIN_ANCHOR))
1235
- throw new Error(`could not patch Porffor's typed-array method generator: anchor not found in ${file}`);
1236
- edited = edited.replace(TA_JOIN_ANCHOR, TA_JOIN_INJECT);
1237
- }
1238
- if (!edited.includes(TA_SET_OFFSET_MARKER)) {
1239
- if (!edited.includes(TA_SET_OFFSET_ANCHOR))
1240
- throw new Error(`could not patch Porffor's TypedArray.prototype.set offset: anchor not found in ${file}`);
1241
- edited = edited.replace(TA_SET_OFFSET_ANCHOR, TA_SET_OFFSET_INJECT);
1242
- }
1243
- if (edited !== src) await writeFile(file, edited);
1252
+ export async function precompileBuiltins(root: string): Promise<void> {
1244
1253
  const table = resolve(root, "compiler/builtins_precompiled.js");
1245
- const before = await readFile(table);
1246
1254
  // Porffor only writes the table when import.meta.url matches argv[1].
1247
1255
  // macOS's /var -> /private/var symlink makes a noncanonical argv[1] skip it.
1248
1256
  const precompile = await realpath(resolve(root, "compiler/precompile.js"));
@@ -1256,9 +1264,8 @@ export async function patchTypedArrayFrom(root: string): Promise<void> {
1256
1264
  new Response(child.stdout).text(),
1257
1265
  new Response(child.stderr).text(),
1258
1266
  ]);
1259
- if (code !== 0) throw new Error(`could not precompile Porffor's TypedArray.from: ${stderr || stdout}`);
1260
- if (edited !== src && before.equals(await readFile(table)))
1261
- throw new Error(`Porffor precompile did not update ${table}`);
1267
+ if (code !== 0) throw new Error(`could not precompile Porffor's patched builtins: ${stderr || stdout}`);
1268
+ if ((await readFile(table)).length === 0) throw new Error(`Porffor precompile produced an empty ${table}`);
1262
1269
  }
1263
1270
 
1264
1271
  /** The native-fetch server source: $PORT support + the #165 console sink. Exported for tests. */
@@ -1305,6 +1312,7 @@ export async function patchRenderJs(root: string): Promise<void> {
1305
1312
  export async function ensurePorfforPatched(root: string): Promise<void> {
1306
1313
  if (done.has(root)) return;
1307
1314
  await patchCompilerArgs(root);
1315
+ await patchClassSelfName(root);
1308
1316
  await patchUwebsockets(root);
1309
1317
  await patchRenderJs(root);
1310
1318
  await patchDateParser(root);
@@ -1313,7 +1321,7 @@ export async function ensurePorfforPatched(root: string): Promise<void> {
1313
1321
  await patchTypedArrayStore(root);
1314
1322
  // Last of the builtin edits: its precompile picks up the date, promise and
1315
1323
  // replaceAll patches above too.
1316
- await patchTypedArrayFrom(root);
1324
+ await precompileBuiltins(root);
1317
1325
  await patchUnicode(root);
1318
1326
  done.add(root);
1319
1327
  }
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-13";
15
- export const PORFFOR_COMMIT_FULL = "547c7815125b6f02474591950f8b0dd7031a03f0";
14
+ export const PORFFOR_CHANNEL = "alpha-16";
15
+ export const PORFFOR_COMMIT_FULL = "43087d42f0b90d14f04f3cddac034822560aefad";
16
16
  export const PORFFOR_COMMIT = PORFFOR_COMMIT_FULL.slice(0, 7);
17
17
  export const PORFFOR_ARCHIVE_SHA256 =
18
- "d0ff4bf7778f85445ff99bb8f6b2e07f2b83a33a109612f9eba6f2e5cba291b5";
18
+ "84398fd214183801d252a78e69a44fac1baeba7cafc7dbbcfd5c6ec3b0372032";
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-13 (547c781)`). */
21
+ /** A compact identity string for a manifest / report (`alpha-16 (43087d4)`). */
22
22
  export function porfforVersion(): string {
23
23
  return process.env.PORFFOR_VERSION || `${PORFFOR_CHANNEL} (${PORFFOR_COMMIT})`;
24
24
  }
Binary file
Binary file