@sproutboat/toolchain 0.7.0 → 0.8.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 +67 -0
- package/PORFFOR_ALPHA17.md +7 -0
- package/PORFFOR_ALPHA18.md +64 -0
- package/PORFFOR_SCORECARD.md +5 -5
- package/package.json +1 -1
- package/scorecards/alpha-17-731f1b2.json +72 -0
- package/src/acquire.test.ts +84 -10
- package/src/acquire.ts +11 -6
- package/src/alpha18-native.test.ts +43 -0
- package/src/case-map.ts +9 -0
- package/src/fetch-classes.ts +242 -0
- package/src/fingerprint.test.ts +28 -0
- package/src/fingerprint.ts +44 -0
- package/src/gen-case-map.ts +35 -0
- package/src/groupby-private-native.test.ts +37 -0
- package/src/headers-class.ts +120 -0
- package/src/idle-gc.test.ts +38 -0
- package/src/json-cycles-native.test.ts +39 -0
- package/src/patch.test.ts +487 -16
- package/src/patch.ts +1092 -195
- package/src/pin.ts +3 -3
- package/src/repeat-native.test.ts +34 -0
- package/src/streaming.ts +125 -0
- package/src/text-decoder.ts +92 -0
- package/src/url-class.ts +342 -0
- package/vendor/porffor-deda494.tar.gz +0 -0
- package/vendor/porffor-731f1b2.tar.gz +0 -0
package/src/patch.test.ts
CHANGED
|
@@ -3,7 +3,31 @@ import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { EMPTY_BINDINGS, wrapNativeFetchHandler } from "../../runtime/src/wrap";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
patchHeadersArrayInit,
|
|
8
|
+
patchLocalFunctionTypes,
|
|
9
|
+
patchDeferredDiscard,
|
|
10
|
+
patchMapGroupBy,
|
|
11
|
+
patchPrivateFieldsHidden,
|
|
12
|
+
patchRepeatDoubling,
|
|
13
|
+
patchJsonCycles,
|
|
14
|
+
patchTypedArrayTag,
|
|
15
|
+
patchConsoleColors,
|
|
16
|
+
patchCaseMapping,
|
|
17
|
+
patchZeroDelayTimers,
|
|
18
|
+
patchStreamingResponses,
|
|
19
|
+
patchSplitGlobals,
|
|
20
|
+
patchBlobClass,
|
|
21
|
+
patchFetchClasses,
|
|
22
|
+
patchResponseBytes,
|
|
23
|
+
patchHeadersClass,
|
|
24
|
+
patchTextDecoder,
|
|
25
|
+
patchResponseDefaults,
|
|
26
|
+
patchURLClass,
|
|
27
|
+
patchUint8ArrayBase64,
|
|
28
|
+
patchRenderJs,
|
|
29
|
+
patchUwebsockets,
|
|
30
|
+
} from "./patch";
|
|
7
31
|
|
|
8
32
|
// The parts of Porffor's compiler/render.js the patch anchors to.
|
|
9
33
|
const RENDER = `void porf_native_fetch_runtime_init(void) {
|
|
@@ -159,9 +183,13 @@ test("#156: status-line fallback synthesizes a line for unlisted codes, idempote
|
|
|
159
183
|
const once = await readFile(join(root, "compiler/uwebsockets.js"), "utf8");
|
|
160
184
|
|
|
161
185
|
// Return type widened, empty fallback replaced, known cases untouched.
|
|
162
|
-
expect(once).toContain(
|
|
186
|
+
expect(once).toContain(
|
|
187
|
+
"static std::string lookup_status_line(i32 status) {",
|
|
188
|
+
);
|
|
163
189
|
expect(once).not.toContain("std::string_view lookup_status_line");
|
|
164
|
-
expect(once).toContain(
|
|
190
|
+
expect(once).toContain(
|
|
191
|
+
'default: return std::to_string(status) + " Status";',
|
|
192
|
+
);
|
|
165
193
|
expect(once).not.toContain("default: return {};");
|
|
166
194
|
expect(once).toContain('case 302: return "302 Found";');
|
|
167
195
|
// 303 gets its real phrase; the switch stays well-formed (302 still present once).
|
|
@@ -184,10 +212,14 @@ test("#156: status-line fallback synthesizes a line for unlisted codes, idempote
|
|
|
184
212
|
expect(once).toContain(
|
|
185
213
|
"static i32 collect_headers(uWS::HttpRequest* req, uWS::HttpResponse<false>* res) {",
|
|
186
214
|
);
|
|
187
|
-
expect(once).toContain(
|
|
215
|
+
expect(once).toContain(
|
|
216
|
+
"const i32 headers_ptr = collect_headers(req, res);",
|
|
217
|
+
);
|
|
188
218
|
expect(once).toContain('if (key == "x-sb-remote-addr") continue;');
|
|
189
219
|
expect(once).toContain("res->getRemoteAddressAsText()");
|
|
190
|
-
expect(once).toContain(
|
|
220
|
+
expect(once).toContain(
|
|
221
|
+
'porf_native_fetch_alloc_bytestring("x-sb-remote-addr", 16)',
|
|
222
|
+
);
|
|
191
223
|
expect(once).toContain("header_capacity += 2;");
|
|
192
224
|
|
|
193
225
|
// #176: a dedicated porf_native_fetch_read_raw_bytes, declared alongside
|
|
@@ -204,10 +236,12 @@ test("#156: status-line fallback synthesizes a line for unlisted codes, idempote
|
|
|
204
236
|
'if (std::string_view(scan_buf, scan_bytes) == "x-sb-raw-body") raw_body = true;',
|
|
205
237
|
);
|
|
206
238
|
expect(once).toContain(
|
|
207
|
-
"if (raw_body
|
|
239
|
+
"if (raw_body && porf_native_fetch_read_raw_bytes(body_value, &body_buf, &body_len) != 0 && body_value.type == 67) {",
|
|
208
240
|
);
|
|
241
|
+
// A UTF-16 body (type 67) is narrowed unit by unit, never UTF-8 encoded.
|
|
242
|
+
expect(once).toContain("body_owned[i] = (char)(units[i] & 0xff);");
|
|
209
243
|
expect(once).toContain(
|
|
210
|
-
"else porf_native_fetch_read_value(body_value, &body_buf, &body_len, &body_owned);",
|
|
244
|
+
"} else if (!raw_body || !body_buf) porf_native_fetch_read_value(body_value, &body_buf, &body_len, &body_owned);",
|
|
211
245
|
);
|
|
212
246
|
// Header reads are the original 4-arg call, unchanged.
|
|
213
247
|
expect(once).toContain(
|
|
@@ -218,7 +252,9 @@ test("#156: status-line fallback synthesizes a line for unlisted codes, idempote
|
|
|
218
252
|
);
|
|
219
253
|
|
|
220
254
|
await patchUwebsockets(root);
|
|
221
|
-
expect(await readFile(join(root, "compiler/uwebsockets.js"), "utf8")).toBe(
|
|
255
|
+
expect(await readFile(join(root, "compiler/uwebsockets.js"), "utf8")).toBe(
|
|
256
|
+
once,
|
|
257
|
+
);
|
|
222
258
|
} finally {
|
|
223
259
|
await rm(root, { recursive: true, force: true });
|
|
224
260
|
}
|
|
@@ -228,7 +264,10 @@ test("a drifted shim fails loudly instead of silently no-op'ing", async () => {
|
|
|
228
264
|
const root = await mkdtemp(join(tmpdir(), "sb-patch-uws-drift-"));
|
|
229
265
|
try {
|
|
230
266
|
await mkdir(join(root, "compiler"), { recursive: true });
|
|
231
|
-
await writeFile(
|
|
267
|
+
await writeFile(
|
|
268
|
+
join(root, "compiler/uwebsockets.js"),
|
|
269
|
+
"// nothing the patch recognizes\n",
|
|
270
|
+
);
|
|
232
271
|
await expect(patchUwebsockets(root)).rejects.toThrow(/anchor not found/);
|
|
233
272
|
} finally {
|
|
234
273
|
await rm(root, { recursive: true, force: true });
|
|
@@ -241,8 +280,11 @@ test("broker transport defines every direct R2 transfer symbol the patched shim
|
|
|
241
280
|
await patchUwebsockets(root);
|
|
242
281
|
const shim = await readFile(join(root, "compiler/uwebsockets.js"), "utf8");
|
|
243
282
|
const abi = shim.match(/extern "C" \{([\s\S]*?)\n\}/)?.[1];
|
|
244
|
-
if (abi === undefined)
|
|
245
|
-
|
|
283
|
+
if (abi === undefined)
|
|
284
|
+
throw new Error("patched shim did not contain the R2 transfer ABI");
|
|
285
|
+
const required = [...abi.matchAll(/\b(sb_r2_transfer_[a-z_]+)\s*\(/g)].map(
|
|
286
|
+
(match) => match[1],
|
|
287
|
+
);
|
|
246
288
|
const wrapped = wrapNativeFetchHandler(
|
|
247
289
|
"export default { fetch() { return new Response('ok'); } }",
|
|
248
290
|
"",
|
|
@@ -274,7 +316,9 @@ test("#165: render.js routes console output to stderr, unbuffered, idempotently"
|
|
|
274
316
|
expect(once).toContain("dup2(2, 1);");
|
|
275
317
|
expect(once).toContain("setvbuf(stdout, NULL, _IONBF, 0);");
|
|
276
318
|
// Injected inside runtime_init, right after the SIGPIPE line.
|
|
277
|
-
expect(once).toMatch(
|
|
319
|
+
expect(once).toMatch(
|
|
320
|
+
/signal\(SIGPIPE, SIG_IGN\);\n {2}\/\* sproutboat #165/,
|
|
321
|
+
);
|
|
278
322
|
// The $PORT edit still lands too.
|
|
279
323
|
expect(once).toContain('getenv("PORT")');
|
|
280
324
|
|
|
@@ -300,13 +344,15 @@ test("#165: render.js routes console output to stderr, unbuffered, idempotently"
|
|
|
300
344
|
}
|
|
301
345
|
});
|
|
302
346
|
|
|
303
|
-
|
|
304
347
|
test("local function type patch registers lexical bindings and is idempotent", async () => {
|
|
305
348
|
const root = await mkdtemp(join(tmpdir(), "sb-local-functions-"));
|
|
306
349
|
try {
|
|
307
350
|
await mkdir(join(root, "compiler"));
|
|
308
351
|
const file = join(root, "compiler/codegen.js");
|
|
309
|
-
await writeFile(
|
|
352
|
+
await writeFile(
|
|
353
|
+
file,
|
|
354
|
+
"if (!hoistRead && directCallOnlyFunctionBinding(scope, kind, name, pattern, func)) {\n return valUndefined();\n}\nconst func = resolveNamedFunction(scope, name) ?? funcByName(name);",
|
|
355
|
+
);
|
|
310
356
|
await patchLocalFunctionTypes(root);
|
|
311
357
|
const patched = await readFile(file, "utf8");
|
|
312
358
|
expect(patched).toContain("bindNamedFunction(scope, name, func);");
|
|
@@ -318,13 +364,15 @@ test("local function type patch registers lexical bindings and is idempotent", a
|
|
|
318
364
|
}
|
|
319
365
|
});
|
|
320
366
|
|
|
321
|
-
|
|
322
367
|
test("base64 patch preserves option tags and truncates full chunks, idempotently", async () => {
|
|
323
368
|
const root = await mkdtemp(join(tmpdir(), "sb-base64-patch-"));
|
|
324
369
|
try {
|
|
325
370
|
await mkdir(join(root, "compiler/builtins"), { recursive: true });
|
|
326
371
|
const file = join(root, "compiler/builtins/uint8array_base64.ts");
|
|
327
|
-
await writeFile(
|
|
372
|
+
await writeFile(
|
|
373
|
+
file,
|
|
374
|
+
"let alphabet: string = 'base64';\nlet lastChunkHandling: string = 'loose';\nconst fullChunks: i32 = (len / 3) * 3;",
|
|
375
|
+
);
|
|
328
376
|
await patchUint8ArrayBase64(root);
|
|
329
377
|
const patched = await readFile(file, "utf8");
|
|
330
378
|
expect(patched).toContain("let alphabet: any =");
|
|
@@ -336,3 +384,426 @@ test("base64 patch preserves option tags and truncates full chunks, idempotently
|
|
|
336
384
|
await rm(root, { recursive: true, force: true });
|
|
337
385
|
}
|
|
338
386
|
});
|
|
387
|
+
|
|
388
|
+
test("Response defaults patch types string bodies only, idempotently", async () => {
|
|
389
|
+
const root = await mkdtemp(join(tmpdir(), "sb-response-defaults-"));
|
|
390
|
+
try {
|
|
391
|
+
await mkdir(join(root, "runtime"));
|
|
392
|
+
const file = join(root, "runtime/fetch-globals.js");
|
|
393
|
+
await writeFile(
|
|
394
|
+
file,
|
|
395
|
+
"class Response {\n constructor(body = null, init = undefined) {\n this.body = body;\n this.status = Number(init?.status ?? 200);\n this.headers = new Headers(init?.headers);\n }\n}\n",
|
|
396
|
+
);
|
|
397
|
+
await patchResponseDefaults(root);
|
|
398
|
+
const patched = await readFile(file, "utf8");
|
|
399
|
+
expect(patched).toContain(
|
|
400
|
+
"if (typeof body === 'string' && !this.headers.has('content-type')) this.headers.set('content-type', 'text/plain;charset=UTF-8');",
|
|
401
|
+
);
|
|
402
|
+
await patchResponseDefaults(root);
|
|
403
|
+
expect(await readFile(file, "utf8")).toBe(patched);
|
|
404
|
+
await writeFile(file, "class Response {}\n");
|
|
405
|
+
await expect(patchResponseDefaults(root)).rejects.toThrow(
|
|
406
|
+
"anchor not found",
|
|
407
|
+
);
|
|
408
|
+
} finally {
|
|
409
|
+
await rm(root, { recursive: true, force: true });
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test("Headers array-init patch sends arrays to the sequence branch, idempotently", async () => {
|
|
414
|
+
const root = await mkdtemp(join(tmpdir(), "sb-headers-array-"));
|
|
415
|
+
try {
|
|
416
|
+
await mkdir(join(root, "runtime"));
|
|
417
|
+
const file = join(root, "runtime/fetch-globals.js");
|
|
418
|
+
await writeFile(
|
|
419
|
+
file,
|
|
420
|
+
" if (Porffor.type(init.entries) == Porffor.TYPES.function) {\n this._entries = Porffor.array.new(8);\n for (const entry of init.entries()) {\n",
|
|
421
|
+
);
|
|
422
|
+
await patchHeadersArrayInit(root);
|
|
423
|
+
const patched = await readFile(file, "utf8");
|
|
424
|
+
expect(patched).toContain(
|
|
425
|
+
"if (!Array.isArray(init) && Porffor.type(init.entries) == Porffor.TYPES.function) {",
|
|
426
|
+
);
|
|
427
|
+
await patchHeadersArrayInit(root);
|
|
428
|
+
expect(await readFile(file, "utf8")).toBe(patched);
|
|
429
|
+
} finally {
|
|
430
|
+
await rm(root, { recursive: true, force: true });
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("URL patch replaces Porffor's URL class, idempotently", async () => {
|
|
435
|
+
const root = await mkdtemp(join(tmpdir(), "sb-url-class-"));
|
|
436
|
+
try {
|
|
437
|
+
await mkdir(join(root, "runtime"));
|
|
438
|
+
const file = join(root, "runtime/fetch-globals.js");
|
|
439
|
+
await writeFile(
|
|
440
|
+
file,
|
|
441
|
+
"class TextDecoder {}\n\nclass URL {\n constructor(input) {\n this.href = '';\n }\n}\n\nclass Request {\n constructor() {}\n}\n",
|
|
442
|
+
);
|
|
443
|
+
await patchURLClass(root);
|
|
444
|
+
const patched = await readFile(file, "utf8");
|
|
445
|
+
expect(patched).toContain("sproutboat: WHATWG-style URL");
|
|
446
|
+
expect(patched).not.toContain("this.href = '';");
|
|
447
|
+
expect(patched.startsWith("class TextDecoder {}\n\nclass URL {")).toBe(true);
|
|
448
|
+
expect(patched.endsWith("}\n\nclass Request {\n constructor() {}\n}\n")).toBe(true);
|
|
449
|
+
await patchURLClass(root);
|
|
450
|
+
expect(await readFile(file, "utf8")).toBe(patched);
|
|
451
|
+
await writeFile(file, "class URL {}\n");
|
|
452
|
+
await expect(patchURLClass(root)).rejects.toThrow("anchor not found");
|
|
453
|
+
} finally {
|
|
454
|
+
await rm(root, { recursive: true, force: true });
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
test("Map.groupBy patch appends the builtin once and refuses an upstream one", async () => {
|
|
460
|
+
const root = await mkdtemp(join(tmpdir(), "sb-map-groupby-"));
|
|
461
|
+
try {
|
|
462
|
+
await mkdir(join(root, "compiler/builtins"), { recursive: true });
|
|
463
|
+
const file = join(root, "compiler/builtins/map.ts");
|
|
464
|
+
await writeFile(file, "export const Map = function (iterable: any): Map {\n};\n");
|
|
465
|
+
await patchMapGroupBy(root);
|
|
466
|
+
const patched = await readFile(file, "utf8");
|
|
467
|
+
expect(patched).toContain("export const __Map_groupBy = (items: any, callbackFn: any): Map => {");
|
|
468
|
+
await patchMapGroupBy(root);
|
|
469
|
+
expect(await readFile(file, "utf8")).toBe(patched);
|
|
470
|
+
await writeFile(file, "export const Map = function (iterable: any): Map {\n};\nexport const __Map_groupBy = 1;\n");
|
|
471
|
+
await expect(patchMapGroupBy(root)).rejects.toThrow("drop patchMapGroupBy");
|
|
472
|
+
} finally {
|
|
473
|
+
await rm(root, { recursive: true, force: true });
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
test("private field patch installs instance and static private fields as methods, idempotently", async () => {
|
|
478
|
+
const root = await mkdtemp(join(tmpdir(), "sb-private-fields-"));
|
|
479
|
+
try {
|
|
480
|
+
await mkdir(join(root, "compiler"));
|
|
481
|
+
const file = join(root, "compiler/codegen.js");
|
|
482
|
+
await writeFile(
|
|
483
|
+
file,
|
|
484
|
+
" builtinCall(func, '__Porffor_object_class_value', [\n" +
|
|
485
|
+
" if (kind === 'get' || kind === 'set') initKind = kind;\n",
|
|
486
|
+
);
|
|
487
|
+
await patchPrivateFieldsHidden(root);
|
|
488
|
+
const patched = await readFile(file, "utf8");
|
|
489
|
+
expect(patched).toContain("x.key?.type === 'PrivateIdentifier' ? '__Porffor_object_class_method' : '__Porffor_object_class_value'");
|
|
490
|
+
expect(patched).toContain("if (initKind === 'value' && x.key?.type === 'PrivateIdentifier') initKind = 'method';");
|
|
491
|
+
await patchPrivateFieldsHidden(root);
|
|
492
|
+
expect(await readFile(file, "utf8")).toBe(patched);
|
|
493
|
+
await writeFile(file, "export const x = 1;\n");
|
|
494
|
+
await expect(patchPrivateFieldsHidden(root)).rejects.toThrow("anchor not found");
|
|
495
|
+
} finally {
|
|
496
|
+
await rm(root, { recursive: true, force: true });
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
test("deferred discard patch moves page discard to the idle collection, idempotently", async () => {
|
|
501
|
+
const root = await mkdtemp(join(tmpdir(), "sb-deferred-discard-"));
|
|
502
|
+
try {
|
|
503
|
+
await mkdir(join(root, "compiler"));
|
|
504
|
+
const file = join(root, "compiler/render.js");
|
|
505
|
+
await writeFile(
|
|
506
|
+
file,
|
|
507
|
+
"static i32 porf_gc_mark_queue_cap = 0;\n" +
|
|
508
|
+
" porf_gc_discard_free_runs();\n" +
|
|
509
|
+
"porf_gc_mark_queue_cap > 65536);' : ' return 0;'\n" +
|
|
510
|
+
" porf_c_stack_top = stack_top;\n porf_gc_collect(0);\n // The mark queue is empty\n",
|
|
511
|
+
);
|
|
512
|
+
await patchDeferredDiscard(root);
|
|
513
|
+
const patched = await readFile(file, "utf8");
|
|
514
|
+
expect(patched).toContain("if (porf_gc_discard_now) porf_gc_discard_free_runs(); else porf_gc_discard_pending = 1;");
|
|
515
|
+
expect(patched).toContain("porf_gc_discard_pending || porf_gc_claimed_since_full > 0 || porf_gc_promoted_since_full > 0);");
|
|
516
|
+
expect(patched).toContain(" porf_gc_discard_now = 1;\n porf_gc_collect(0);\n porf_gc_discard_now = 0;\n porf_gc_discard_pending = 0;");
|
|
517
|
+
await patchDeferredDiscard(root);
|
|
518
|
+
expect(await readFile(file, "utf8")).toBe(patched);
|
|
519
|
+
await writeFile(file, "export const x = 1;\n");
|
|
520
|
+
await expect(patchDeferredDiscard(root)).rejects.toThrow("anchor not found");
|
|
521
|
+
} finally {
|
|
522
|
+
await rm(root, { recursive: true, force: true });
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test("repeat patch replaces both per-repetition loops once", async () => {
|
|
527
|
+
const root = await mkdtemp(join(tmpdir(), "sb-repeat-patch-"));
|
|
528
|
+
try {
|
|
529
|
+
await mkdir(join(root, "compiler/builtins"), { recursive: true });
|
|
530
|
+
const file = join(root, "compiler/builtins/string.ts");
|
|
531
|
+
const loop =
|
|
532
|
+
" for (let i: i32 = 0; i < count; i++) {\n" +
|
|
533
|
+
" Porffor.IR.copy(Porffor.IR.ptr(out) + 4 + i * thisLen, Porffor.IR.ptr(this) + 4, thisLen);\n" +
|
|
534
|
+
" }\n";
|
|
535
|
+
await writeFile(file, "a\n" + loop + "b\n" + loop);
|
|
536
|
+
await patchRepeatDoubling(root);
|
|
537
|
+
const patched = await readFile(file, "utf8");
|
|
538
|
+
expect(patched.split("sproutboat B16: doubling repeat").length).toBe(3);
|
|
539
|
+
expect(patched).not.toContain("i * thisLen");
|
|
540
|
+
await patchRepeatDoubling(root);
|
|
541
|
+
expect(await readFile(file, "utf8")).toBe(patched);
|
|
542
|
+
await writeFile(file, "a\n" + loop);
|
|
543
|
+
await expect(patchRepeatDoubling(root)).rejects.toThrow("anchor not found twice");
|
|
544
|
+
} finally {
|
|
545
|
+
await rm(root, { recursive: true, force: true });
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
test("ensurePorfforPatched trusts a cache entry this toolchain version and patch chain patched, and nothing else", async () => {
|
|
550
|
+
const { ensurePorfforPatched } = await import("./patch");
|
|
551
|
+
const { PORFFOR_COMMIT_FULL } = await import("./pin");
|
|
552
|
+
const { PATCH_FINGERPRINT } = await import("./fingerprint");
|
|
553
|
+
const { version } = await import("../package.json");
|
|
554
|
+
const root = await mkdtemp(join(tmpdir(), "sb-patched-stamp-"));
|
|
555
|
+
try {
|
|
556
|
+
// An empty tree: running the chain would throw on the first missing file.
|
|
557
|
+
await writeFile(join(root, ".sproutboat-complete"), JSON.stringify({ commit: PORFFOR_COMMIT_FULL, toolchainVersion: version, patchFingerprint: PATCH_FINGERPRINT }));
|
|
558
|
+
await ensurePorfforPatched(root);
|
|
559
|
+
const stale = await mkdtemp(join(tmpdir(), "sb-patched-stale-"));
|
|
560
|
+
try {
|
|
561
|
+
await writeFile(join(stale, ".sproutboat-complete"), JSON.stringify({ commit: PORFFOR_COMMIT_FULL, toolchainVersion: "0.0.0" }));
|
|
562
|
+
await expect(ensurePorfforPatched(stale)).rejects.toThrow();
|
|
563
|
+
// Same release, older patch chain (B19): the chain runs again.
|
|
564
|
+
await writeFile(join(stale, ".sproutboat-complete"), JSON.stringify({ commit: PORFFOR_COMMIT_FULL, toolchainVersion: version, patchFingerprint: "000000000000" }));
|
|
565
|
+
await expect(ensurePorfforPatched(stale)).rejects.toThrow();
|
|
566
|
+
} finally {
|
|
567
|
+
await rm(stale, { recursive: true, force: true });
|
|
568
|
+
}
|
|
569
|
+
} finally {
|
|
570
|
+
await rm(root, { recursive: true, force: true });
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
test("JSON cycle patch adds an ancestor stack around arrays and objects, idempotently", async () => {
|
|
575
|
+
const root = await mkdtemp(join(tmpdir(), "sb-json-cycles-"));
|
|
576
|
+
try {
|
|
577
|
+
await mkdir(join(root, "compiler/builtins"), { recursive: true });
|
|
578
|
+
const file = join(root, "compiler/builtins/json.ts");
|
|
579
|
+
await writeFile(
|
|
580
|
+
file,
|
|
581
|
+
"let __Porffor_json_bufLimit: i32 = 0;\n depth += 1;\n depth -= 1;\n depth += 1;\n depth -= 1;\n" +
|
|
582
|
+
" let out: i32 = __Porffor_json_serialize(buffer, value, 0, space);\n",
|
|
583
|
+
);
|
|
584
|
+
await patchJsonCycles(root);
|
|
585
|
+
const patched = await readFile(file, "utf8");
|
|
586
|
+
expect(patched.split("__Porffor_json_enter(value);").length).toBe(3);
|
|
587
|
+
expect(patched.split("__Porffor_json_leave();").length).toBe(3);
|
|
588
|
+
expect(patched).toContain("throw new TypeError('Converting circular structure to JSON');");
|
|
589
|
+
await patchJsonCycles(root);
|
|
590
|
+
expect(await readFile(file, "utf8")).toBe(patched);
|
|
591
|
+
await writeFile(file, "let __Porffor_json_bufLimit: i32 = 0;\n");
|
|
592
|
+
await expect(patchJsonCycles(root)).rejects.toThrow("anchor not found");
|
|
593
|
+
} finally {
|
|
594
|
+
await rm(root, { recursive: true, force: true });
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
test("typed array tag patch names every typed array before toString returns, idempotently", async () => {
|
|
599
|
+
const root = await mkdtemp(join(tmpdir(), "sb-ta-tag-"));
|
|
600
|
+
try {
|
|
601
|
+
await mkdir(join(root, "compiler/builtins"), { recursive: true });
|
|
602
|
+
const file = join(root, "compiler/builtins/object.ts");
|
|
603
|
+
await writeFile(file, " return '[object ' + builtinTag + ']';\n};\n");
|
|
604
|
+
await patchTypedArrayTag(root);
|
|
605
|
+
const patched = await readFile(file, "utf8");
|
|
606
|
+
for (const name of ["Uint8Array", "Uint8ClampedArray", "BigInt64Array", "Float64Array"]) expect(patched).toContain(`builtinTag = '${name}';`);
|
|
607
|
+
await patchTypedArrayTag(root);
|
|
608
|
+
expect(await readFile(file, "utf8")).toBe(patched);
|
|
609
|
+
await writeFile(file, "export const x = 1;\n");
|
|
610
|
+
await expect(patchTypedArrayTag(root)).rejects.toThrow("anchor not found");
|
|
611
|
+
} finally {
|
|
612
|
+
await rm(root, { recursive: true, force: true });
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
test("console colour patch colours only on a terminal, idempotently", async () => {
|
|
617
|
+
const root = await mkdtemp(join(tmpdir(), "sb-console-colors-"));
|
|
618
|
+
try {
|
|
619
|
+
await mkdir(join(root, "compiler/builtins"), { recursive: true });
|
|
620
|
+
const file = join(root, "compiler/builtins/console.ts");
|
|
621
|
+
await writeFile(file, "export const __Porffor_consolePrint = (arg: any): void => {\n if (x) return;\n __Porffor_print(arg);\n};\n");
|
|
622
|
+
await patchConsoleColors(root);
|
|
623
|
+
const patched = await readFile(file, "utf8");
|
|
624
|
+
expect(patched).toContain("Porffor.c`tty = isatty(1) ? 1 : 0;`;");
|
|
625
|
+
expect(patched).toContain(" __Porffor_print(arg, __Porffor_consoleColors());\n};");
|
|
626
|
+
await patchConsoleColors(root);
|
|
627
|
+
expect(await readFile(file, "utf8")).toBe(patched);
|
|
628
|
+
await writeFile(file, "export const x = 1;\n");
|
|
629
|
+
await expect(patchConsoleColors(root)).rejects.toThrow("anchor not found");
|
|
630
|
+
} finally {
|
|
631
|
+
await rm(root, { recursive: true, force: true });
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
test("TextDecoder and Headers patches replace Porffor's classes once", async () => {
|
|
636
|
+
const root = await mkdtemp(join(tmpdir(), "sb-web-classes-"));
|
|
637
|
+
try {
|
|
638
|
+
await mkdir(join(root, "runtime"));
|
|
639
|
+
const file = join(root, "runtime/fetch-globals.js");
|
|
640
|
+
await writeFile(file, "class Headers {\n old() {}\n}\nfunction setTimeout() {}\nclass TextDecoder {\n old() {}\n}\nclass URL {\n}\n");
|
|
641
|
+
await patchHeadersClass(root);
|
|
642
|
+
await patchTextDecoder(root);
|
|
643
|
+
const patched = await readFile(file, "utf8");
|
|
644
|
+
expect(patched).toContain("sproutboat: Fetch-spec Headers");
|
|
645
|
+
expect(patched).toContain("sproutboat: WHATWG UTF-8 decoder");
|
|
646
|
+
expect(patched).not.toContain("old() {}");
|
|
647
|
+
expect(patched).toContain("\nfunction setTimeout() {}\nclass TextDecoder {");
|
|
648
|
+
await patchHeadersClass(root);
|
|
649
|
+
await patchTextDecoder(root);
|
|
650
|
+
expect(await readFile(file, "utf8")).toBe(patched);
|
|
651
|
+
await writeFile(file, "export const x = 1;\n");
|
|
652
|
+
await expect(patchHeadersClass(root)).rejects.toThrow("anchor not found");
|
|
653
|
+
await expect(patchTextDecoder(root)).rejects.toThrow("anchor not found");
|
|
654
|
+
} finally {
|
|
655
|
+
await rm(root, { recursive: true, force: true });
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
test("case mapping patch adds the lookup and routes non-ASCII units through it, idempotently", async () => {
|
|
660
|
+
const root = await mkdtemp(join(tmpdir(), "sb-case-map-"));
|
|
661
|
+
try {
|
|
662
|
+
await mkdir(join(root, "compiler/builtins"), { recursive: true });
|
|
663
|
+
const file = join(root, "compiler/builtins/string.ts");
|
|
664
|
+
await writeFile(
|
|
665
|
+
file,
|
|
666
|
+
"export const __String_prototype_toUpperCase = function (this: string) {\n" +
|
|
667
|
+
" if (chr >= 97) if (chr <= 122) chr -= 32;\n\n Porffor.IR.storeU16(j, 4, chr);\n" +
|
|
668
|
+
" if (chr >= 97) if (chr <= 122) chr -= 32;\n\n Porffor.IR.storeU8(j++, 4, chr);\n" +
|
|
669
|
+
" if (chr >= 65) if (chr <= 90) chr += 32;\n\n Porffor.IR.storeU16(j, 4, chr);\n" +
|
|
670
|
+
" if (chr >= 65) if (chr <= 90) chr += 32;\n\n Porffor.IR.storeU8(j++, 4, chr);\n",
|
|
671
|
+
);
|
|
672
|
+
await patchCaseMapping(root);
|
|
673
|
+
const patched = await readFile(file, "utf8");
|
|
674
|
+
expect(patched).toContain("export const __Porffor_caseFind = (chr: i32, runs: string): i32 => {");
|
|
675
|
+
expect(patched).toContain("else chr = __Porffor_caseUpper(chr);");
|
|
676
|
+
expect(patched).toContain("else chr = __Porffor_caseLower(chr);");
|
|
677
|
+
expect(patched).toContain("if (chr != 247) chr -= 32;");
|
|
678
|
+
await patchCaseMapping(root);
|
|
679
|
+
expect(await readFile(file, "utf8")).toBe(patched);
|
|
680
|
+
await writeFile(file, "export const x = 1;\n");
|
|
681
|
+
await expect(patchCaseMapping(root)).rejects.toThrow("anchor not found");
|
|
682
|
+
} finally {
|
|
683
|
+
await rm(root, { recursive: true, force: true });
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
test("zero-delay timer patch arms timers at 1 ms, idempotently", async () => {
|
|
688
|
+
const root = await mkdtemp(join(tmpdir(), "sb-zero-timers-"));
|
|
689
|
+
try {
|
|
690
|
+
await mkdir(join(root, "compiler"));
|
|
691
|
+
const file = join(root, "compiler/render.js");
|
|
692
|
+
await writeFile(file, "x\n porf_native_fetch_timer_start(id, (i32)delay_ms, timer->repeat ? (i32)delay_ms : 0);\n");
|
|
693
|
+
await patchZeroDelayTimers(root);
|
|
694
|
+
const patched = await readFile(file, "utf8");
|
|
695
|
+
expect(patched).toContain("delay_ms < 1 ? 1 : (i32)delay_ms");
|
|
696
|
+
await patchZeroDelayTimers(root);
|
|
697
|
+
expect(await readFile(file, "utf8")).toBe(patched);
|
|
698
|
+
await writeFile(file, "export const x = 1;\n");
|
|
699
|
+
await expect(patchZeroDelayTimers(root)).rejects.toThrow("anchor not found");
|
|
700
|
+
} finally {
|
|
701
|
+
await rm(root, { recursive: true, force: true });
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
test("Request/Response, Blob and finalizer patches replace their targets once", async () => {
|
|
706
|
+
const root = await mkdtemp(join(tmpdir(), "sb-fetch-classes-"));
|
|
707
|
+
try {
|
|
708
|
+
await mkdir(join(root, "runtime"));
|
|
709
|
+
const globals = join(root, "runtime/fetch-globals.js"), entry = join(root, "runtime/native-fetch.js");
|
|
710
|
+
await writeFile(globals, "class Blob {\n old() {}\n}\nclass TextEncoder {\n}\nclass Request {\n old() {}\n}\nclass Response {\n old() {}\n}\n");
|
|
711
|
+
await writeFile(entry, "x\n const body = response.body == null ? '' : String(response.body);\n const headersEntries = response.headers._entries;\n");
|
|
712
|
+
await patchFetchClasses(root);
|
|
713
|
+
await patchBlobClass(root);
|
|
714
|
+
await patchResponseBytes(root);
|
|
715
|
+
const g = await readFile(globals, "utf8"), e = await readFile(entry, "utf8");
|
|
716
|
+
expect(g).toContain("sproutboat: Fetch-spec Request and Response");
|
|
717
|
+
expect(g).toContain("sproutboat: byte-backed Blob and File");
|
|
718
|
+
expect(g).toContain("class File extends Blob {");
|
|
719
|
+
expect(g).not.toContain("old() {}");
|
|
720
|
+
expect(e).toContain("headersEntries.push('x-sb-raw-body', '1');");
|
|
721
|
+
await patchFetchClasses(root);
|
|
722
|
+
await patchBlobClass(root);
|
|
723
|
+
await patchResponseBytes(root);
|
|
724
|
+
expect(await readFile(globals, "utf8")).toBe(g);
|
|
725
|
+
expect(await readFile(entry, "utf8")).toBe(e);
|
|
726
|
+
await writeFile(globals, "export const x = 1;\n");
|
|
727
|
+
await writeFile(entry, "export const x = 1;\n");
|
|
728
|
+
await expect(patchFetchClasses(root)).rejects.toThrow("anchor not found");
|
|
729
|
+
await expect(patchBlobClass(root)).rejects.toThrow("anchor not found");
|
|
730
|
+
await expect(patchResponseBytes(root)).rejects.toThrow("anchor not found");
|
|
731
|
+
} finally {
|
|
732
|
+
await rm(root, { recursive: true, force: true });
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
test("streaming patch adds the stream registry and attaches marked responses, idempotently", async () => {
|
|
737
|
+
const streaming = await import("./streaming");
|
|
738
|
+
const root = await mkdtemp(join(tmpdir(), "sb-streaming-"));
|
|
739
|
+
try {
|
|
740
|
+
await mkdir(join(root, "compiler"));
|
|
741
|
+
const file = join(root, "compiler/uwebsockets.js");
|
|
742
|
+
const render = join(root, "compiler/render.js");
|
|
743
|
+
await writeFile(render, "f64 x;\n" + streaming.STREAM_STUBS_ANCHOR + " for a bytestring\n");
|
|
744
|
+
await writeFile(
|
|
745
|
+
file,
|
|
746
|
+
streaming.STREAM_FORBIDDEN_ANCHOR +
|
|
747
|
+
streaming.STREAM_REGISTRY_ANCHOR +
|
|
748
|
+
streaming.STREAM_FLAG_ANCHOR +
|
|
749
|
+
streaming.STREAM_SCAN_ANCHOR +
|
|
750
|
+
streaming.STREAM_CORK_ANCHOR +
|
|
751
|
+
streaming.STREAM_END_ANCHOR,
|
|
752
|
+
);
|
|
753
|
+
await patchStreamingResponses(root);
|
|
754
|
+
const patched = await readFile(file, "utf8");
|
|
755
|
+
expect(patched).toContain('extern "C" u32 sb_stream_open(void) {');
|
|
756
|
+
expect(patched).toContain("if (stream_id) sb_stream_attach(stream_id, res);");
|
|
757
|
+
expect(patched).toContain('key == "x-sb-stream"; // sproutboat B19');
|
|
758
|
+
expect(patched.indexOf("sproutboat B19: streamed responses")).toBeLessThan(patched.indexOf("static void write_response_value"));
|
|
759
|
+
const stubs = await readFile(render, "utf8");
|
|
760
|
+
expect(stubs).toContain("__attribute__((weak)) int sb_stream_supported(void) { return 0; }");
|
|
761
|
+
expect(stubs.indexOf("streaming fallbacks")).toBeLessThan(stubs.indexOf(streaming.STREAM_STUBS_ANCHOR));
|
|
762
|
+
await patchStreamingResponses(root);
|
|
763
|
+
expect(await readFile(file, "utf8")).toBe(patched);
|
|
764
|
+
expect(await readFile(render, "utf8")).toBe(stubs);
|
|
765
|
+
await writeFile(file, "export const x = 1;\n");
|
|
766
|
+
await expect(patchStreamingResponses(root)).rejects.toThrow("anchor not found");
|
|
767
|
+
await writeFile(render, "export const x = 1;\n");
|
|
768
|
+
await expect(patchStreamingResponses(root)).rejects.toThrow("streaming fallbacks: anchor not found");
|
|
769
|
+
} finally {
|
|
770
|
+
await rm(root, { recursive: true, force: true });
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
test("split globals patch prefixes module globals so none binds to libc, idempotently", async () => {
|
|
775
|
+
const root = await mkdtemp(join(tmpdir(), "sb-split-globals-"));
|
|
776
|
+
try {
|
|
777
|
+
await mkdir(join(root, "compiler"));
|
|
778
|
+
const file = join(root, "compiler/render.js");
|
|
779
|
+
await writeFile(
|
|
780
|
+
file,
|
|
781
|
+
[
|
|
782
|
+
" const st = split ? '' : 'static ';",
|
|
783
|
+
" case K.Global:\n cur.globals[node[N_A]] = true;\n case K.Local:\n return [sanitize(node[N_A]), P_PRIM];",
|
|
784
|
+
" emit(`${ind()}${sanitize(node[N_A][N_A])} = ${rx(node[N_B], P_COMMA)};\\n`);",
|
|
785
|
+
" ? `${sanitize(update[N_A][N_A])} = ${rx(update[N_B], P_COMMA)}`",
|
|
786
|
+
" const t = sanitize(target[N_A]);",
|
|
787
|
+
" for (const g of globals) link.push(`${st}${CT[g.type]} ${sanitize(g.name)};`);",
|
|
788
|
+
" const name = sanitize(g.name);",
|
|
789
|
+
" const buckets = sanitize(g.name.replace(/underlyingStore$/, 'underlyingBuckets'));",
|
|
790
|
+
" const bucketsCap = sanitize(g.name.replace(/underlyingStore$/, 'underlyingBucketsCap'));",
|
|
791
|
+
" for (const x of Object.keys(parts.globals).sort()) text.push(`extern ${CT[globalTypes[x]]} ${sanitize(x)};\\n`);",
|
|
792
|
+
"",
|
|
793
|
+
].join("\n"),
|
|
794
|
+
);
|
|
795
|
+
await patchSplitGlobals(root);
|
|
796
|
+
const patched = await readFile(file, "utf8");
|
|
797
|
+
expect(patched).toContain("const gname = n => sanitize(split && !n.startsWith('__') ? 'pg_' + n : n);");
|
|
798
|
+
expect(patched).toContain("return [gname(node[N_A]), P_PRIM];\n case K.Local:\n return [sanitize(node[N_A]), P_PRIM];");
|
|
799
|
+
expect(patched).toContain("${st}${CT[g.type]} ${gname(g.name)}");
|
|
800
|
+
expect(patched).toContain("text.push(`extern ${CT[globalTypes[x]]} ${gname(x)};\\n`);");
|
|
801
|
+
expect(patched.match(/gname\(/g)?.length).toBe(9);
|
|
802
|
+
await patchSplitGlobals(root);
|
|
803
|
+
expect(await readFile(file, "utf8")).toBe(patched);
|
|
804
|
+
await writeFile(file, "export const x = 1;\n");
|
|
805
|
+
await expect(patchSplitGlobals(root)).rejects.toThrow("anchor not found");
|
|
806
|
+
} finally {
|
|
807
|
+
await rm(root, { recursive: true, force: true });
|
|
808
|
+
}
|
|
809
|
+
});
|