@sproutboat/toolchain 0.4.20 → 0.4.21

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,11 @@
1
1
  # @sproutboat/toolchain
2
2
 
3
+ ## 0.4.21
4
+
5
+ ### Patch Changes
6
+
7
+ - 1b80c4b: Integer typed-array stores wrap modulo 2^n as the spec requires, instead of saturating (part of baronunread/sproutboat#238). Porffor converted the stored value with a saturating float-to-int conversion, so `uint32[i] = x | 0` stored 0 for any negative `x`, `int32[i] = 4e9` stored 2147483647, and `fill`/`set`/`from` had the same bug. This broke pure-JS hashing such as @noble/hashes' SHA-256, which now matches Node.
8
+
3
9
  ## 0.4.20
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutboat/toolchain",
3
- "version": "0.4.20",
3
+ "version": "0.4.21",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -120,6 +120,8 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
120
120
  "}\n",
121
121
  "compiler/builtins/json.ts": "// sb_json_utf16_v1\n",
122
122
  "runtime/fetch-globals.js": "// sb_text_encoder_scalar_v1\n",
123
+ "compiler/codegen.js":
124
+ " stmt(scope, Store(ctype, addr, 4, ctype === 'f64' || ctype === 'f32' ? f : signed ? Convert(T.i32, f) : Convert(T.u32, f, 0)));\n",
123
125
  "compiler/builtins/string.ts":
124
126
  "export const __Porffor_string_replaceAll = (str: any, searchValue: any, replaceValue: any) => {\n" +
125
127
  " let out: any = __Porffor_string_emptyLike(str);\n let appendIndex: i32 = 0;\n let searchIndex: i32 = 0;\n let matched: boolean = false;\n" +
@@ -208,6 +210,10 @@ test("the default call (no url/expectedSha256 override) never touches the networ
208
210
  },
209
211
  });
210
212
  expect(await readFile(join(dir, "runtime/index.js"), "utf8")).not.toBe("");
213
+ // #238: integer typed-array stores wrap modulo 2^n instead of saturating.
214
+ expect(await readFile(join(dir, "compiler/codegen.js"), "utf8")).toContain(
215
+ "signed ? Convert(T.i32, toUint32(scope, f), CONVERT_RANGE_KNOWN | CONVERT_SIGNED) : toUint32(scope, f)",
216
+ );
211
217
  // #237: replaceAll joins its pieces once instead of a strcat per match.
212
218
  expect(await readFile(join(dir, "compiler/builtins/string.ts"), "utf8")).toContain(
213
219
  "return Porffor.callThis(__Array_prototype_join, pieces, '');",
package/src/patch.ts CHANGED
@@ -291,6 +291,21 @@ const REPLACE_ALL_EDITS = [
291
291
  ] as const;
292
292
  const REPLACE_ALL_MARKER = "sproutboat #237: pieces joined once";
293
293
 
294
+ // baronunread/sproutboat#238: an integer typed-array element store converted
295
+ // its value with a saturating f64 -> int Convert, so a negative value stored
296
+ // into a Uint32Array became 0 and 4e9 into an Int32Array became 2147483647.
297
+ // ECMA-262 (IntegerIndexedElementSet -> ToUint32 / ToInt32 / ToUint16 ...) wraps
298
+ // modulo 2^n instead. noble-hashes' SHA-256 stores `x | 0` into a Uint32Array,
299
+ // so every negative schedule word was zeroed and the digest came out wrong.
300
+ // Route the store through codegen's own modular toUint32 (what bitwise ops use);
301
+ // narrower stores keep the low bits, which is exactly the modular result.
302
+ const TA_STORE_ANCHOR =
303
+ " stmt(scope, Store(ctype, addr, 4, ctype === 'f64' || ctype === 'f32' ? f : signed ? Convert(T.i32, f) : Convert(T.u32, f, 0)));";
304
+ const TA_STORE_INJECT =
305
+ " // sproutboat #238: typed-array stores wrap modulo 2^n (ToUint32), not saturate.\n" +
306
+ " stmt(scope, Store(ctype, addr, 4, ctype === 'f64' || ctype === 'f32' ? f : signed ? Convert(T.i32, toUint32(scope, f), CONVERT_RANGE_KNOWN | CONVERT_SIGNED) : toUint32(scope, f)));";
307
+ const TA_STORE_MARKER = "sproutboat #238: typed-array stores wrap";
308
+
294
309
  // Porffor's TypedArray.from only handles iterables. An array-like input such
295
310
  // as { length: 16 } silently becomes an empty typed array, including HMAC
296
311
  // keys made with Uint8Array.from({ length: 16 }, mapFn). The compiler uses a
@@ -1108,6 +1123,16 @@ export async function patchReplaceAll(root: string): Promise<void> {
1108
1123
  await writeFile(file, src);
1109
1124
  }
1110
1125
 
1126
+ /** #238: modular typed-array stores. Before the precompile so builtins get it too. */
1127
+ export async function patchTypedArrayStore(root: string): Promise<void> {
1128
+ const file = resolve(root, "compiler/codegen.js");
1129
+ const src = await readFile(file, "utf8");
1130
+ if (src.includes(TA_STORE_MARKER)) return;
1131
+ if (!src.includes(TA_STORE_ANCHOR))
1132
+ throw new Error(`could not patch Porffor's typed-array store conversion: anchor not found in ${file}`);
1133
+ await writeFile(file, src.replace(TA_STORE_ANCHOR, TA_STORE_INJECT));
1134
+ }
1135
+
1111
1136
  export async function patchTypedArrayFrom(root: string): Promise<void> {
1112
1137
  const file = resolve(root, "compiler/builtins/typedarray.js");
1113
1138
  const src = await readFile(file, "utf8");
@@ -1185,6 +1210,7 @@ export async function ensurePorfforPatched(root: string): Promise<void> {
1185
1210
  await patchDateParser(root);
1186
1211
  await patchPromiseResolveNull(root);
1187
1212
  await patchReplaceAll(root);
1213
+ await patchTypedArrayStore(root);
1188
1214
  // Last of the builtin edits: its precompile picks up the date, promise and
1189
1215
  // replaceAll patches above too.
1190
1216
  await patchTypedArrayFrom(root);