@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutboat/toolchain",
3
- "version": "0.4.16",
3
+ "version": "0.4.18",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -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": " ...darwinReleaseCompileArgs,\n uSocketsArchive,\n '-lm'\n",
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 === CFLAGS_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);