@sproutboat/toolchain 0.4.16 → 0.4.18
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 +12 -0
- package/package.json +1 -1
- package/src/acquire.test.ts +9 -1
- package/src/patch.ts +38 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @sproutboat/toolchain
|
|
2
2
|
|
|
3
|
+
## 0.4.18
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- e0d0e17: Compile every unit with `-ffp-contract=off` (baronunread/sproutboat#235). On arm64, clang fused `a * b + c` into a single FMA that rounds once, where JavaScript rounds after the multiply and again after the add, so native sprouts could compute different doubles from V8 for the same code. A seeded LCG diverged at step 23.
|
|
8
|
+
|
|
9
|
+
## 0.4.17
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 2cda218: Fix async handlers failing permanently with `TypeError: Cannot get property of null` after a few requests (baronunread/sproutboat#168). Porffor represents `null` as an object-typed value with pointer 0, so a promise resolving to `null` went through the `then` probe and looked `then` up at memory offset 0. Once `Date.prototype.toISOString` had written there, the probe matched, read `null.then` and rejected, and every later async function returning `null` failed the same way. A patch to `compiler/builtins/promise.ts` keeps `null` out of the probe.
|
|
14
|
+
|
|
3
15
|
## 0.4.16
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
package/src/acquire.test.ts
CHANGED
|
@@ -29,7 +29,8 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
|
|
|
29
29
|
" return -1;\n" +
|
|
30
30
|
"}\n" +
|
|
31
31
|
"#define PORF_CORO_STACK_SIZE (256u * 1024u)\n",
|
|
32
|
-
"compiler/index.js":
|
|
32
|
+
"compiler/index.js":
|
|
33
|
+
" '-fno-ident', '-ffunction-sections', '-fdata-sections',\n ...darwinReleaseCompileArgs,\n uSocketsArchive,\n '-lm'\n",
|
|
33
34
|
"compiler/precompile.js":
|
|
34
35
|
"const fs = require('node:fs');\n" +
|
|
35
36
|
"const path = require('node:path');\n" +
|
|
@@ -119,6 +120,7 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
|
|
|
119
120
|
"compiler/builtins/promise.ts":
|
|
120
121
|
"export const __Porffor_promise_resolve = (value: any, promise: any): void => {\n" +
|
|
121
122
|
" if (Porffor.type(value) == Porffor.TYPES.object) {\n" +
|
|
123
|
+
" // cheap prototype-chain probe for 'then' before the expensive Get below, does not invoke getters\n" +
|
|
122
124
|
" const thenHash: i32 = __Porffor_object_hash('then');\n" +
|
|
123
125
|
" let probe: any = value;\n" +
|
|
124
126
|
" while (Porffor.type(probe) == Porffor.TYPES.object) {\n" +
|
|
@@ -197,6 +199,12 @@ test("the default call (no url/expectedSha256 override) never touches the networ
|
|
|
197
199
|
},
|
|
198
200
|
});
|
|
199
201
|
expect(await readFile(join(dir, "runtime/index.js"), "utf8")).not.toBe("");
|
|
202
|
+
// #235: every compiled unit keeps JavaScript's separate float roundings.
|
|
203
|
+
expect(await readFile(join(dir, "compiler/index.js"), "utf8")).toContain("'-ffp-contract=off',");
|
|
204
|
+
// #168: the real vendored source takes the null guard on the `then` probe.
|
|
205
|
+
expect(await readFile(join(dir, "compiler/builtins/promise.ts"), "utf8")).toContain(
|
|
206
|
+
"if (Porffor.type(value) == Porffor.TYPES.object && value != null) {",
|
|
207
|
+
);
|
|
200
208
|
});
|
|
201
209
|
|
|
202
210
|
test("a corrupted warm cache is replaced from the verified archive", async () => {
|
package/src/patch.ts
CHANGED
|
@@ -192,6 +192,29 @@ const CFLAGS_INJECT =
|
|
|
192
192
|
" ...(process.env.SB_EXTRA_CFLAGS ? process.env.SB_EXTRA_CFLAGS.split(' ').filter(Boolean) : []),\n";
|
|
193
193
|
const CFLAGS_MARKER = "SB_EXTRA_CFLAGS";
|
|
194
194
|
|
|
195
|
+
// baronunread/sproutboat#235: clang's default -ffp-contract=on fuses `a * b + c`
|
|
196
|
+
// into one FMA on arm64, rounding once where JavaScript rounds twice. A seeded
|
|
197
|
+
// LCG (seed * 1103515245 + 12345, past 2^53) diverged from V8 at step 23.
|
|
198
|
+
// Every compiled unit keeps JS's separate roundings with contraction off.
|
|
199
|
+
const FP_CONTRACT_ANCHOR = " '-fno-ident', '-ffunction-sections', '-fdata-sections',\n";
|
|
200
|
+
const FP_CONTRACT_INJECT = " '-ffp-contract=off', // sproutboat #235: JS rounds a * b + c twice\n";
|
|
201
|
+
const FP_CONTRACT_MARKER = "sproutboat #235";
|
|
202
|
+
|
|
203
|
+
// baronunread/sproutboat#168: Porffor represents null as an object-typed value
|
|
204
|
+
// with pointer 0, so __Porffor_promise_resolve's cheap `then` probe treated a
|
|
205
|
+
// promise resolving to null as an object and looked `then` up at memory offset
|
|
206
|
+
// 0. Once something (Date.prototype.toISOString's scratch writes, in practice)
|
|
207
|
+
// left bytes there that hash-match, the probe "found" a then, read null.then,
|
|
208
|
+
// and rejected with "Cannot get property of null". Every later async function
|
|
209
|
+
// resolving to null then failed, permanently. Only non-null objects can be
|
|
210
|
+
// thenables (ECMA-262 Promise Resolve Functions step 8), so exclude null.
|
|
211
|
+
const PROMISE_NULL_ANCHOR =
|
|
212
|
+
" if (Porffor.type(value) == Porffor.TYPES.object) {\n // cheap prototype-chain probe for 'then'";
|
|
213
|
+
const PROMISE_NULL_INJECT =
|
|
214
|
+
" // sproutboat #168: null is object-typed with pointer 0, never a thenable.\n" +
|
|
215
|
+
" if (Porffor.type(value) == Porffor.TYPES.object && value != null) {\n // cheap prototype-chain probe for 'then'";
|
|
216
|
+
const PROMISE_NULL_MARKER = "sproutboat #168: null is object-typed";
|
|
217
|
+
|
|
195
218
|
// Porffor's TypedArray.from only handles iterables. An array-like input such
|
|
196
219
|
// as { length: 16 } silently becomes an empty typed array, including HMAC
|
|
197
220
|
// keys made with Uint8Array.from({ length: 16 }, mapFn). The compiler uses a
|
|
@@ -944,6 +967,7 @@ async function patchCompilerArgs(root: string): Promise<void> {
|
|
|
944
967
|
for (const [marker, anchor, inject, what] of [
|
|
945
968
|
[LINK_MARKER, LINK_ANCHOR, LINK_INJECT, "extra link args"],
|
|
946
969
|
[CFLAGS_MARKER, CFLAGS_ANCHOR, CFLAGS_INJECT, "extra compiler flags"],
|
|
970
|
+
[FP_CONTRACT_MARKER, FP_CONTRACT_ANCHOR, FP_CONTRACT_INJECT, "floating-point contraction"],
|
|
947
971
|
] as const) {
|
|
948
972
|
if (src.includes(marker)) continue;
|
|
949
973
|
const at = src.indexOf(anchor);
|
|
@@ -956,7 +980,7 @@ async function patchCompilerArgs(root: string): Promise<void> {
|
|
|
956
980
|
// Add flags to compileOnlyArgs so every generated C unit sees them and
|
|
957
981
|
// Porffor's module-build cache includes them in its compiler command.
|
|
958
982
|
src =
|
|
959
|
-
marker
|
|
983
|
+
marker !== LINK_MARKER
|
|
960
984
|
? src.slice(0, at + anchor.length) + inject + src.slice(at + anchor.length)
|
|
961
985
|
: src.slice(0, at) + inject + src.slice(at);
|
|
962
986
|
changed = true;
|
|
@@ -977,6 +1001,16 @@ export async function patchDateParser(root: string): Promise<void> {
|
|
|
977
1001
|
await writeFile(file, src.slice(0, start) + parser.replace(DATE_PARSER_ANCHOR, DATE_PARSER_INJECT) + src.slice(end));
|
|
978
1002
|
}
|
|
979
1003
|
|
|
1004
|
+
/** #168: keep a promise resolving to null out of the `then` probe. Precompiled by patchTypedArrayFrom. */
|
|
1005
|
+
export async function patchPromiseResolveNull(root: string): Promise<void> {
|
|
1006
|
+
const file = resolve(root, "compiler/builtins/promise.ts");
|
|
1007
|
+
const src = await readFile(file, "utf8");
|
|
1008
|
+
if (src.includes(PROMISE_NULL_MARKER)) return;
|
|
1009
|
+
if (!src.includes(PROMISE_NULL_ANCHOR))
|
|
1010
|
+
throw new Error(`could not patch Porffor's promise resolve for null: anchor not found in ${file}`);
|
|
1011
|
+
await writeFile(file, src.replace(PROMISE_NULL_ANCHOR, PROMISE_NULL_INJECT));
|
|
1012
|
+
}
|
|
1013
|
+
|
|
980
1014
|
export async function patchTypedArrayFrom(root: string): Promise<void> {
|
|
981
1015
|
const file = resolve(root, "compiler/builtins/typedarray.js");
|
|
982
1016
|
const src = await readFile(file, "utf8");
|
|
@@ -1052,6 +1086,9 @@ export async function ensurePorfforPatched(root: string): Promise<void> {
|
|
|
1052
1086
|
await patchUwebsockets(root);
|
|
1053
1087
|
await patchRenderJs(root);
|
|
1054
1088
|
await patchDateParser(root);
|
|
1089
|
+
await patchPromiseResolveNull(root);
|
|
1090
|
+
// Last of the builtin edits: its precompile picks up the date and promise
|
|
1091
|
+
// patches above too.
|
|
1055
1092
|
await patchTypedArrayFrom(root);
|
|
1056
1093
|
await patchUnicode(root);
|
|
1057
1094
|
done.add(root);
|