@sproutboat/toolchain 0.12.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/acquire.test.ts +5 -0
- package/src/bigint-gc-native.test.ts +30 -0
- package/src/fingerprint.ts +1 -1
- package/src/patch.ts +31 -0
- package/test/gc-bigint.js.txt +44 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @sproutboat/toolchain
|
|
2
2
|
|
|
3
|
+
## 0.12.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 1ff15e6: Keep heap-backed BigInts alive during conservative garbage collection. The native scanner now recognizes the encoded floating-point pointer used by BigInt arithmetic, preventing crashes and incorrect checksums in workloads such as CRC64 and LTX encoding.
|
|
8
|
+
|
|
3
9
|
## 0.12.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/package.json
CHANGED
package/src/acquire.test.ts
CHANGED
|
@@ -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);
|
package/src/fingerprint.ts
CHANGED
|
@@ -21,7 +21,7 @@ export const PATCH_SOURCES = [
|
|
|
21
21
|
* patch.ts as the module patch.ts is everywhere else. Kept current by fingerprint.test.ts
|
|
22
22
|
* (UPDATE_FINGERPRINT=1 bun test packages/toolchain/src/fingerprint.test.ts).
|
|
23
23
|
*/
|
|
24
|
-
export const RELEASED_FINGERPRINT = "
|
|
24
|
+
export const RELEASED_FINGERPRINT = "ee724d4de68f";
|
|
25
25
|
|
|
26
26
|
/** The fingerprint of the sources in dir, or null where they are not on disk. */
|
|
27
27
|
export function sourceFingerprint(dir: string): string | null {
|
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");
|