@sproutboat/toolchain 0.12.0 → 0.13.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,37 @@
1
1
  # @sproutboat/toolchain
2
2
 
3
+ ## 0.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ - df612fc: `fetch(Request)`, `fetch(Request, init)` and `new Request(request, init)` behave as in workerd from compatibility date `2026-10-23`. This covers outgoing `fetch()`, service-binding `fetch()` and Durable Object stubs, on the broker and embedded transports alike. Every behaviour was checked against workerd 2026-09-27 by the platform's service-binding suite.
8
+
9
+ From `2026-10-23`:
10
+
11
+ - `fetch(input, init)` is `fetch(new Request(input, init))`, as the Fetch spec says. The Request's method, headers, body, redirect mode and signal apply unless `init` replaces them, and `init.headers` replaces all of the Request's headers.
12
+ - A Request given as `init` brings its method, headers and body.
13
+ - A body can be used once. `fetch(request)`, `new Request(request)` and reading it through `request.body` all use it, after which `bodyUsed` is true. Sending or copying a used Request throws `TypeError: Cannot reconstruct a Request with a used body.`, and reading it again throws workerd's "Body has already been used..." message. With a body in `init`, the source Request is left alone.
14
+ - A GET or HEAD Request with any body throws, even `""` or one inherited from another Request. An incoming GET or HEAD has `request.body === null`.
15
+ - Methods are workerd's: known names in any case are uppercased, and anything else throws `Invalid HTTP method string`. That includes `CONNECT` and custom names.
16
+ - `redirect` must be `"follow"` or `"manual"`. As in workerd, `"error"` throws when the Request is made.
17
+ - The headers of an incoming request, a fetched or service-binding response, and `Response.redirect()` are immutable: `set`, `append` and `delete` throw `TypeError: Can't modify immutable headers.` Copies are mutable, so `new Request(request)`, `new Response(body, response)` and `new Headers(h)` can still be changed.
18
+ - `AbortSignal.abort()`'s default reason reads "The operation was aborted", as in workerd.
19
+
20
+ With an earlier `compatibility_date` these keep their old behaviour: a copy shares the body, any method name and redirect mode passes, and headers stay mutable. To get the new behaviour, move `compatibility_date` to `2026-10-23` and rebuild.
21
+
22
+ These are fixed at every date, since no app can depend on the old behaviour:
23
+
24
+ - `Request.signal` exists. A Request made with `signal` passes it on to copies and to `fetch()`, which aborts with it.
25
+ - A `ReadableStream` body in an outgoing `fetch()` is sent as its bytes. Before, it went out as the text `[object ReadableStream]`.
26
+ - `request.clone()` and `response.clone()` tee a stream body, so both copies can read it. Before, the second reader failed.
27
+ - A copy of an incoming request keeps its raw bytes. Its `text()` decodes UTF-8, and `fetch()` sends the bytes unchanged.
28
+
29
+ ## 0.12.1
30
+
31
+ ### Patch Changes
32
+
33
+ - 1ff15e6: Keep heap-backed BigInts alive during conservative garbage collection. The native scanner now recognizes the encoded floating-point pointer used by BigInt arithmetic, preventing crashes and incorrect checksums in workloads such as CRC64 and LTX encoding.
34
+
3
35
  ## 0.12.0
4
36
 
5
37
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutboat/toolchain",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,6 +18,11 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
18
18
  const files = {
19
19
  "runtime/index.js": "fixture:runtime/index.js\n",
20
20
  "compiler/render.js": "// sb_native_utf8_scalar_v1\n" +
21
+ "static void porf_gc_cons_candidate(u32 c);\n" +
22
+ // Both conservative scanners consume raw f64 values.
23
+ (" if (d > 0.0 && d < 4294967296.0) {\n" +
24
+ " const i64 iv = (i64)d;\n" +
25
+ " if ((f64)iv == d) porf_gc_cons_candidate((u32)(u64)iv);\n }\n").repeat(2) +
21
26
  "static i32 porf_gc_mark_queue_cap = 0;\n porf_gc_discard_free_runs();\n" +
22
27
  " porf_native_fetch_timer_start(id, (i32)delay_ms, timer->repeat ? (i32)delay_ms : 0);\n" +
23
28
  "void porf_native_fetch_collect_normal_from(void* stack_top) {\n" +
@@ -0,0 +1,30 @@
1
+ import { expect, test } from "bun:test";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { resolve } from "node:path";
5
+ import { ensurePorffor } from "./acquire";
6
+
7
+ test("conservative roots retain encoded heap BigInts on the stack and in untyped blocks", async () => {
8
+ const workdir = await mkdtemp(resolve(tmpdir(), "sb-bigint-gc-"));
9
+ try {
10
+ const porffor = await ensurePorffor();
11
+ const source = resolve(import.meta.dir, "../test/gc-bigint.js.txt");
12
+ const binary = resolve(workdir, "bigint-gc");
13
+ const compile = Bun.spawn(
14
+ ["bun", resolve(porffor, "runtime/index.js"), "native", source, "-o", binary, "-O0", "-s"],
15
+ { stdout: "pipe", stderr: "pipe" },
16
+ );
17
+ const [code, stdout, stderr] = await Promise.all([
18
+ compile.exited,
19
+ new Response(compile.stdout).text(),
20
+ new Response(compile.stderr).text(),
21
+ ]);
22
+ expect(code, stderr + stdout).toBe(0);
23
+ const run = Bun.spawnSync([binary], { stdout: "pipe", stderr: "pipe", timeout: 10_000 });
24
+ expect(run.exitCode, Buffer.from(run.stderr).toString()).toBe(0);
25
+ const ansi = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
26
+ expect(Buffer.from(run.stdout).toString().replace(ansi, "").trim()).toBe("3");
27
+ } finally {
28
+ await rm(workdir, { recursive: true, force: true });
29
+ }
30
+ }, 120_000);
@@ -61,9 +61,43 @@ export const REQUEST_CLASS_SOURCE = String.raw`class Request {
61
61
  return new TextEncoder().encode(text);
62
62
  }
63
63
 
64
+ // Fetch as workerd implements it, from compatibility date 2026-10-23: a Request made from
65
+ // another takes its body (the source becomes used, and a used one cannot be copied),
66
+ // GET and HEAD refuse any body, methods and redirect modes are workerd's, and errors
67
+ // carry workerd's messages. Before it, a copy shared the body, as it always did here.
68
+ static _strict() { return globalThis.__sbCompat >= '2026-10-23'; }
69
+ static _usedError() {
70
+ return new TypeError(Request._strict() ? 'Body has already been used. It can only be used once. Use tee() first if you need to read it twice.' : 'Body has already been used');
71
+ }
72
+ // workerd's methods (kj's list): known names in any case, uppercased; anything else throws.
73
+ static _method(method) {
74
+ const upper = method.toUpperCase();
75
+ const known = ' GET HEAD POST PUT DELETE PATCH PURGE OPTIONS TRACE COPY LOCK MKCOL MOVE PROPFIND PROPPATCH SEARCH UNLOCK ACL REPORT MKACTIVITY CHECKOUT MERGE MSEARCH NOTIFY SUBSCRIBE UNSUBSCRIBE QUERY ';
76
+ if (method.indexOf(' ') < 0 && known.indexOf(' ' + upper + ' ') >= 0) return upper;
77
+ throw new TypeError('Invalid HTTP method string: ' + method);
78
+ }
79
+ // Whether the body was used: read through this message, or (from 2026-10-23, as in
80
+ // workerd) through its stream by anyone, such as a Request given it as init.
81
+ static _used(message) {
82
+ if (message._bodyUsed === true) return true;
83
+ if (!Request._strict()) return false;
84
+ const stream = message._bodyStream != null ? message._bodyStream : message._bodyInit;
85
+ return Request._isStream(stream) && stream._disturbed === true;
86
+ }
87
+ // A body for two readers: a stream is teed, the first branch staying with the source.
88
+ // Values are read by copying, so both share them.
89
+ static _split(message) {
90
+ const init = message._bodyStream != null ? message._bodyStream : message._bodyInit;
91
+ if (!Request._isStream(init)) return init;
92
+ const pair = init.tee();
93
+ message._bodyInit = pair[0];
94
+ message._bodyStream = null;
95
+ return pair[1];
96
+ }
97
+
64
98
  // A body is read once: the first reader marks it used, a second throws, as in Fetch.
65
99
  static _consume(message) {
66
- if (message._bodyUsed === true) throw new TypeError('Body has already been used');
100
+ if (Request._used(message)) throw Request._usedError();
67
101
  message._bodyUsed = true;
68
102
  // A stream the body getter handed out is what the reader reads: a locked one throws.
69
103
  return message._bodyStream != null ? message._bodyStream : message._bodyInit;
@@ -89,6 +123,12 @@ export const REQUEST_CLASS_SOURCE = String.raw`class Request {
89
123
  return message._bodyStream;
90
124
  }
91
125
 
126
+ // Raw ingress bytes (a copy of an incoming request) decode as UTF-8, like any byte body.
127
+ static _messageText(message, body) {
128
+ if (message._bodyLatin1 === true && typeof body == 'string') return new TextDecoder().decode(Request._messageBytes(message, body));
129
+ return Request._bodyText(body);
130
+ }
131
+
92
132
  static _bodyText(body) {
93
133
  if (body == null) return '';
94
134
  if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return new TextDecoder().decode(body);
@@ -140,11 +180,15 @@ export const REQUEST_CLASS_SOURCE = String.raw`class Request {
140
180
  }
141
181
 
142
182
  constructor(input, init = undefined) {
183
+ const strict = Request._strict();
143
184
  let url = input;
144
185
  let method = 'GET';
145
186
  let headers = undefined;
146
187
  let body = null;
147
188
  let redirect = undefined;
189
+ let signal = null;
190
+ // The Request whose body this one takes, when init gives none.
191
+ let source = null;
148
192
 
149
193
  if (input instanceof Request) {
150
194
  url = input.url;
@@ -152,13 +196,19 @@ export const REQUEST_CLASS_SOURCE = String.raw`class Request {
152
196
  headers = input.headers;
153
197
  body = input._bodyStream != null ? input._bodyStream : input._bodyInit;
154
198
  redirect = input.redirect;
199
+ if (input._signal != null) signal = input._signal;
200
+ if (body != null) source = input;
155
201
  } else if (input != null && Porffor.type(input) == Porffor.TYPES.object && input.href != null) url = input.href;
156
202
 
157
203
  if (init != null) {
158
204
  if (init.method != null) method = init.method;
159
205
  if (init.headers != null) headers = init.headers;
160
- if (init.body !== undefined) body = init.body;
206
+ if (init.body !== undefined) {
207
+ body = init.body;
208
+ source = null;
209
+ }
161
210
  if (init.redirect != null) redirect = init.redirect;
211
+ if (init.signal !== undefined) signal = init.signal;
162
212
  }
163
213
 
164
214
  const raw = (Porffor.type(url) | 0b10000000) == Porffor.TYPES.bytestring ? url : String(url);
@@ -169,34 +219,58 @@ export const REQUEST_CLASS_SOURCE = String.raw`class Request {
169
219
  throw new TypeError('Invalid URL: ' + raw);
170
220
  }
171
221
  this.url = href;
172
- this.method = ((Porffor.type(method) | 0b10000000) == Porffor.TYPES.bytestring ? method : String(method)).toUpperCase();
173
- if (init != null && init.body != null && init.body !== '' && (this.method == 'GET' || this.method == 'HEAD'))
222
+ const methodText = (Porffor.type(method) | 0b10000000) == Porffor.TYPES.bytestring ? method : String(method);
223
+ this.method = strict ? Request._method(methodText) : methodText.toUpperCase();
224
+ if (strict) {
225
+ if (redirect != null && redirect != 'follow' && redirect != 'manual')
226
+ throw new TypeError('Invalid redirect value, must be one of "follow" or "manual" ("error" won\'t be implemented since it does not make sense at the edge; use "manual" and check the response status code).');
227
+ if (body != null && (this.method == 'GET' || this.method == 'HEAD'))
228
+ throw new TypeError('Request with a GET or HEAD method cannot have a body.');
229
+ if (source != null && Request._used(source)) throw new TypeError('Cannot reconstruct a Request with a used body.');
230
+ } else if (init != null && init.body != null && init.body !== '' && (this.method == 'GET' || this.method == 'HEAD'))
174
231
  throw new TypeError('Request with GET/HEAD method cannot have body.');
175
232
  this.headers = new Headers(headers);
176
- this._bodyInit = Request._extractBody(body, this.headers);
233
+ if (source != null) {
234
+ // The body as the source holds it, already extracted: no content type is added, and
235
+ // raw ingress bytes stay bytes. From 2026-10-23 the source gives it up.
236
+ this._bodyInit = body;
237
+ if (source._bodyLatin1 === true) this._bodyLatin1 = true;
238
+ if (strict) source._bodyUsed = true;
239
+ } else this._bodyInit = Request._extractBody(body, this.headers);
177
240
  if (redirect != null) this._redirect = String(redirect);
241
+ if (signal != null) this._signal = signal;
178
242
  }
179
243
 
180
244
  get redirect() { return this._redirect ?? 'follow'; }
245
+ // A signal that never aborts unless one was given, made when first asked for.
246
+ get signal() {
247
+ if (this._signal == null) this._signal = new globalThis.AbortController().signal;
248
+ return this._signal;
249
+ }
181
250
  get body() { return Request._bodyAsStream(this); }
182
251
  set body(value) { this._bodyInit = value; this._bodyStream = null; }
183
- get bodyUsed() { return this._bodyUsed === true; }
252
+ get bodyUsed() { return Request._used(this); }
184
253
 
185
254
  arrayBuffer() { return Request._read(this, (bytes) => bytes.buffer); }
186
255
  blob() { return Request._read(this, (bytes) => new Blob([bytes])); }
256
+ // A copy with its own branch of the body. workerd says the used body's stream is locked.
187
257
  clone() {
188
- if (this._bodyUsed === true) throw new TypeError('Body has already been used');
189
- return new Request(this);
258
+ if (Request._used(this))
259
+ throw new TypeError(Request._strict() ? 'This ReadableStream is currently locked to a reader,' : 'Body has already been used');
260
+ const copy = new Request(this.url, { method: this.method, headers: this.headers, redirect: this._redirect, signal: this._signal });
261
+ copy._bodyInit = Request._split(this);
262
+ if (this._bodyLatin1 === true) copy._bodyLatin1 = true;
263
+ return copy;
190
264
  }
191
265
  json() {
192
266
  const body = Request._consume(this);
193
267
  if (Request._isStream(body)) return Request._streamBytes(body).then((bytes) => JSON.parse(new TextDecoder().decode(bytes)));
194
- return JSON.parse(Request._bodyText(body));
268
+ return JSON.parse(Request._messageText(this, body));
195
269
  }
196
270
  text() {
197
271
  const body = Request._consume(this);
198
272
  if (Request._isStream(body)) return Request._streamBytes(body).then((bytes) => new TextDecoder().decode(bytes));
199
- return Request._bodyText(body);
273
+ return Request._messageText(this, body);
200
274
  }
201
275
  }
202
276
 
@@ -227,14 +301,14 @@ export const RESPONSE_CLASS_SOURCE = String.raw`class Response {
227
301
  get ok() { return this.status >= 200 && this.status < 300; }
228
302
  get body() { return Request._bodyAsStream(this); }
229
303
  set body(value) { this._bodyInit = value; this._bodyStream = null; }
230
- get bodyUsed() { return this._bodyUsed === true; }
304
+ get bodyUsed() { return Request._used(this); }
231
305
 
232
306
  arrayBuffer() { return Request._read(this, (bytes) => bytes.buffer); }
233
307
  blob() { return Request._read(this, (bytes) => new Blob([bytes])); }
234
308
  clone() {
235
- if (this._bodyUsed === true) throw new TypeError('Body has already been used');
309
+ if (Request._used(this)) throw new TypeError('Body has already been used');
236
310
  const copy = new Response(null, { status: this.status == 0 ? 200 : this.status, statusText: this._statusText, headers: this.headers });
237
- copy._bodyInit = this._bodyInit;
311
+ copy._bodyInit = Request._split(this);
238
312
  copy.status = this.status;
239
313
  if (this._type != null) copy._type = this._type;
240
314
  return copy;
@@ -242,12 +316,12 @@ export const RESPONSE_CLASS_SOURCE = String.raw`class Response {
242
316
  json() {
243
317
  const body = Request._consume(this);
244
318
  if (Request._isStream(body)) return Request._streamBytes(body).then((bytes) => JSON.parse(new TextDecoder().decode(bytes)));
245
- return JSON.parse(Request._bodyText(body));
319
+ return JSON.parse(Request._messageText(this, body));
246
320
  }
247
321
  text() {
248
322
  const body = Request._consume(this);
249
323
  if (Request._isStream(body)) return Request._streamBytes(body).then((bytes) => new TextDecoder().decode(bytes));
250
- return Request._bodyText(body);
324
+ return Request._messageText(this, body);
251
325
  }
252
326
  }
253
327
  `;
@@ -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 = "7e12e7bba967";
24
+ export const RELEASED_FINGERPRINT = "147f6e5e6655";
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 {
@@ -7,6 +7,10 @@
7
7
  * Storage stays Porffor's: `_entries` holds each appended name (lowercased) and value in
8
8
  * order, flat, because the native response writer reads it, and each Set-Cookie must reach
9
9
  * the wire as its own header. The Fetch spec's combined, sorted view is computed on read.
10
+ *
11
+ * `_immutable`, when the runtime sets it, makes append, set and delete throw as workerd's
12
+ * immutable guard does (an incoming request's headers, a fetched response's). The runtime
13
+ * sets it from compatibility date 2026-10-23; a copy (`new Headers(h)`) is always mutable.
10
14
  */
11
15
  export const HEADERS_CLASS_SOURCE = String.raw`class Headers {
12
16
  // sproutboat: Fetch-spec Headers
@@ -52,11 +56,17 @@ export const HEADERS_CLASS_SOURCE = String.raw`class Headers {
52
56
  Porffor.array.fastPush(this._entries, value);
53
57
  }
54
58
 
59
+ _writable() {
60
+ if (this._immutable === true) throw new TypeError("Can't modify immutable headers.");
61
+ }
62
+
55
63
  append(name, value) {
64
+ this._writable();
56
65
  this._push(Headers._name(name), Headers._value(value));
57
66
  }
58
67
 
59
68
  delete(name) {
69
+ this._writable();
60
70
  const normalized = Headers._name(name);
61
71
  const next = Porffor.array.new(this._entries.length);
62
72
  for (let i = 0; i < this._entries.length; i += 2) {
@@ -90,6 +100,7 @@ export const HEADERS_CLASS_SOURCE = String.raw`class Headers {
90
100
  }
91
101
 
92
102
  set(name, value) {
103
+ this._writable();
93
104
  const normalized = Headers._name(name), v = Headers._value(value);
94
105
  this.delete(normalized);
95
106
  this._push(normalized, v);
package/src/patch.ts CHANGED
@@ -1925,6 +1925,36 @@ export async function patchSplitGlobals(root: string): Promise<void> {
1925
1925
  await writeFile(file, src);
1926
1926
  }
1927
1927
 
1928
+ // The conservative scanner must recognize the encoded f64 form used by BigInt builtins.
1929
+ const BIGINT_GC_ANCHOR = "static void porf_gc_cons_candidate(u32 c);\n";
1930
+ const BIGINT_GC_HELPER = `static void porf_gc_cons_f64(f64 d) {
1931
+ // Heap BigInts encode their offset above 2^51, including in f64 builtin arguments.
1932
+ if (d >= 2251799813685248.0 && d < 2251804108652544.0) d -= 2251799813685248.0;
1933
+ if (d > 0.0 && d < 4294967296.0) {
1934
+ const u32 ptr = (u32)d;
1935
+ if ((f64)ptr == d) porf_gc_cons_candidate(ptr);
1936
+ }
1937
+ }
1938
+ `;
1939
+ const BIGINT_GC_SCAN = ` if (d > 0.0 && d < 4294967296.0) {
1940
+ const i64 iv = (i64)d;
1941
+ if ((f64)iv == d) porf_gc_cons_candidate((u32)(u64)iv);
1942
+ }`;
1943
+
1944
+ export async function patchBigIntRoots(root: string): Promise<void> {
1945
+ const file = resolve(root, "compiler/render.js");
1946
+ const src = await readFile(file, "utf8");
1947
+ if (src.includes(BIGINT_GC_HELPER) && src.split(" porf_gc_cons_f64(d);").length === 3) return;
1948
+ if (src.split(BIGINT_GC_ANCHOR).length !== 2 || src.split(BIGINT_GC_SCAN).length !== 3)
1949
+ throw new Error(`could not patch Porffor's BigInt GC roots: anchor not found in ${file}`);
1950
+ await writeFile(
1951
+ file,
1952
+ src
1953
+ .replace(BIGINT_GC_ANCHOR, BIGINT_GC_ANCHOR + BIGINT_GC_HELPER)
1954
+ .replaceAll(BIGINT_GC_SCAN, " porf_gc_cons_f64(d);"),
1955
+ );
1956
+ }
1957
+
1928
1958
  const FETCH_CLASSES_MARKER = "sproutboat: Fetch-spec Request and Response";
1929
1959
  /** Replaces Porffor's Request and Response (fetch-classes.ts); covers patchResponseDefaults. */
1930
1960
  export async function patchFetchClasses(root: string): Promise<void> {
@@ -2023,6 +2053,7 @@ export async function ensurePorfforPatched(root: string): Promise<void> {
2023
2053
  await patchClassSelfName(root);
2024
2054
  await patchUwebsockets(root);
2025
2055
  await patchRenderJs(root);
2056
+ await patchBigIntRoots(root);
2026
2057
  await patchStreamingResponses(root);
2027
2058
  await patchQuietCollection(root);
2028
2059
  await patchAsyncWaker(root);
@@ -0,0 +1,44 @@
1
+ // Native GC scanner regression: bit 0 is the stack root, bit 1 the untyped block.
2
+ // Bit 2 is set if a value just outside the heap-BigInt encoding marks the allocation.
3
+ function probe() {
4
+ let stackMarked = 0;
5
+ let blockMarked = 0;
6
+ let falseMarked = 0;
7
+ Porffor.c`
8
+ u32 bigint = porf_alloc(16, 4);
9
+ u32 holder = porf_alloc(32, 0);
10
+ for (i32 ci = 0; ci < PORF_GC_NCLASSES; ci++) porf_gc_publish_window(ci);
11
+ const u32 gran = porf_gc_gran(bigint);
12
+ volatile f64 encoded = 2251799813685248.0 + (f64)bigint;
13
+ volatile f64 outside[4] = {
14
+ 2251799813685248.0 + (f64)bigint + 0.5,
15
+ 2251804108652544.0 + (f64)bigint,
16
+ -(2251799813685248.0 + (f64)bigint),
17
+ 2251799813685248.0
18
+ };
19
+ for (i32 i = 0; i < 4; i++) {
20
+ porf_gc_bit_clear(PORF_GC_B_MARK, gran);
21
+ f64 d = outside[i];
22
+ u64 bits;
23
+ memcpy(&bits, &d, 8);
24
+ // Only the f64 decoding is under test: skip values whose 32-bit halves alias the block.
25
+ if (porf_gc_gran((u32)bits) == gran || porf_gc_gran((u32)(bits >> 32)) == gran) continue;
26
+ porf_gc_cons_scan_range((const u64*)&outside[i], (const u64*)&outside[i] + 1);
27
+ if (porf_gc_bit(PORF_GC_B_MARK, gran) != 0u) falseMarked = 1;
28
+ }
29
+ porf_gc_bit_clear(PORF_GC_B_MARK, gran);
30
+ porf_gc_cons_scan_range((const u64*)&encoded, (const u64*)((const char*)&encoded + sizeof(encoded)));
31
+ stackMarked = porf_gc_bit(PORF_GC_B_MARK, gran) != 0u;
32
+ porf_gc_bit_clear(PORF_GC_B_MARK, gran);
33
+ *(u64*)(MEM + holder) = 0xffffffffffffffffull;
34
+ *(f64*)(MEM + holder + 8) = encoded;
35
+ porf_gc_bit_clear(PORF_GC_B_MARK, porf_gc_gran(holder));
36
+ porf_gc_kinds[porf_gc_gran(holder)] = 0;
37
+ porf_gc_cons_mark_block((i32)holder);
38
+ blockMarked = porf_gc_bit(PORF_GC_B_MARK, gran) != 0u;
39
+ `;
40
+ return stackMarked + blockMarked * 2 + falseMarked * 4;
41
+ }
42
+ const marked = probe();
43
+ console.log(marked);
44
+ if (marked !== 3) throw new Error("heap BigInt was not marked, or a value outside its encoding was");