@sproutboat/toolchain 0.4.19 → 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 +12 -0
- package/package.json +1 -1
- package/src/acquire.test.ts +16 -0
- package/src/patch.ts +67 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
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
|
+
|
|
9
|
+
## 0.4.20
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 0cdd803: `String.prototype.replaceAll` is linear instead of quadratic (baronunread/sproutboat#237). Porffor grew the result with one full-copy `strcat` per match; a patch to `compiler/builtins/string.ts` collects the pieces and joins them once. Replacing in a 2.1 MB string went from about 42 s to 71 ms (Node: 36 ms), with identical output.
|
|
14
|
+
|
|
3
15
|
## 0.4.19
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
package/src/acquire.test.ts
CHANGED
|
@@ -120,6 +120,14 @@ 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",
|
|
125
|
+
"compiler/builtins/string.ts":
|
|
126
|
+
"export const __Porffor_string_replaceAll = (str: any, searchValue: any, replaceValue: any) => {\n" +
|
|
127
|
+
" let out: any = __Porffor_string_emptyLike(str);\n let appendIndex: i32 = 0;\n let searchIndex: i32 = 0;\n let matched: boolean = false;\n" +
|
|
128
|
+
" out = __Porffor_strcat(out, __Porffor_string_substringLike(str, appendIndex, matchIndex));\n out = __Porffor_strcat(out, __Porffor_string_applyReplacer(str, match, matchIndex, replaceValue));\n" +
|
|
129
|
+
" if (!matched) return str;\n return __Porffor_strcat(out, __Porffor_string_substringLike(str, appendIndex, thisLen));\n};\n" +
|
|
130
|
+
"export const __String_prototype_replaceAll = function (this: string, searchValue: any, replaceValue: any) {};\n",
|
|
123
131
|
"compiler/builtins/promise.ts":
|
|
124
132
|
"export const __Porffor_promise_resolve = (value: any, promise: any): void => {\n" +
|
|
125
133
|
" if (Porffor.type(value) == Porffor.TYPES.object) {\n" +
|
|
@@ -202,6 +210,14 @@ test("the default call (no url/expectedSha256 override) never touches the networ
|
|
|
202
210
|
},
|
|
203
211
|
});
|
|
204
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
|
+
);
|
|
217
|
+
// #237: replaceAll joins its pieces once instead of a strcat per match.
|
|
218
|
+
expect(await readFile(join(dir, "compiler/builtins/string.ts"), "utf8")).toContain(
|
|
219
|
+
"return Porffor.callThis(__Array_prototype_join, pieces, '');",
|
|
220
|
+
);
|
|
205
221
|
// #236: the native-fetch build edits uWebSockets to accept HTTP/1.0.
|
|
206
222
|
const index = await readFile(join(dir, "compiler/index.js"), "utf8");
|
|
207
223
|
expect(index).toContain("import { sbPatchHttp10 } from './sb-http10.js';");
|
package/src/patch.ts
CHANGED
|
@@ -270,6 +270,42 @@ const PROMISE_NULL_INJECT =
|
|
|
270
270
|
" if (Porffor.type(value) == Porffor.TYPES.object && value != null) {\n // cheap prototype-chain probe for 'then'";
|
|
271
271
|
const PROMISE_NULL_MARKER = "sproutboat #168: null is object-typed";
|
|
272
272
|
|
|
273
|
+
// baronunread/sproutboat#237: String.prototype.replaceAll grew its result with
|
|
274
|
+
// one __Porffor_strcat per match, and each strcat copies everything so far, so
|
|
275
|
+
// a 2.7 MB string with many matches took ~22 s (Node: 17 ms). Collect the
|
|
276
|
+
// pieces and join once: Array.prototype.join sizes the result up front and
|
|
277
|
+
// copies each piece a single time.
|
|
278
|
+
const REPLACE_ALL_EDITS = [
|
|
279
|
+
[
|
|
280
|
+
" let out: any = __Porffor_string_emptyLike(str);\n let appendIndex: i32 = 0;\n let searchIndex: i32 = 0;\n let matched: boolean = false;",
|
|
281
|
+
" // sproutboat #237: pieces joined once, not a strcat (full copy) per match.\n const pieces: any[] = Porffor.array.new(0);\n let appendIndex: i32 = 0;\n let searchIndex: i32 = 0;\n let matched: boolean = false;",
|
|
282
|
+
],
|
|
283
|
+
[
|
|
284
|
+
" out = __Porffor_strcat(out, __Porffor_string_substringLike(str, appendIndex, matchIndex));\n out = __Porffor_strcat(out, __Porffor_string_applyReplacer(str, match, matchIndex, replaceValue));",
|
|
285
|
+
" Porffor.array.fastPush(pieces, __Porffor_string_substringLike(str, appendIndex, matchIndex));\n Porffor.array.fastPush(pieces, __Porffor_string_applyReplacer(str, match, matchIndex, replaceValue));",
|
|
286
|
+
],
|
|
287
|
+
[
|
|
288
|
+
" if (!matched) return str;\n return __Porffor_strcat(out, __Porffor_string_substringLike(str, appendIndex, thisLen));\n};",
|
|
289
|
+
" if (!matched) return str;\n Porffor.array.fastPush(pieces, __Porffor_string_substringLike(str, appendIndex, thisLen));\n return Porffor.callThis(__Array_prototype_join, pieces, '');\n};",
|
|
290
|
+
],
|
|
291
|
+
] as const;
|
|
292
|
+
const REPLACE_ALL_MARKER = "sproutboat #237: pieces joined once";
|
|
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
|
+
|
|
273
309
|
// Porffor's TypedArray.from only handles iterables. An array-like input such
|
|
274
310
|
// as { length: 16 } silently becomes an empty typed array, including HMAC
|
|
275
311
|
// keys made with Uint8Array.from({ length: 16 }, mapFn). The compiler uses a
|
|
@@ -1070,6 +1106,33 @@ export async function patchPromiseResolveNull(root: string): Promise<void> {
|
|
|
1070
1106
|
await writeFile(file, src.replace(PROMISE_NULL_ANCHOR, PROMISE_NULL_INJECT));
|
|
1071
1107
|
}
|
|
1072
1108
|
|
|
1109
|
+
/** #237: linear String.prototype.replaceAll. Precompiled by patchTypedArrayFrom. */
|
|
1110
|
+
export async function patchReplaceAll(root: string): Promise<void> {
|
|
1111
|
+
const file = resolve(root, "compiler/builtins/string.ts");
|
|
1112
|
+
let src = await readFile(file, "utf8");
|
|
1113
|
+
if (src.includes(REPLACE_ALL_MARKER)) return;
|
|
1114
|
+
const start = src.indexOf("export const __Porffor_string_replaceAll = ");
|
|
1115
|
+
const end = src.indexOf("export const __String_prototype_replaceAll = ", start);
|
|
1116
|
+
if (start < 0 || end < 0) throw new Error(`could not find Porffor's replaceAll in ${file}`);
|
|
1117
|
+
let body = src.slice(start, end);
|
|
1118
|
+
for (const [anchor, inject] of REPLACE_ALL_EDITS) {
|
|
1119
|
+
if (!body.includes(anchor)) throw new Error(`could not patch Porffor's replaceAll: anchor not found in ${file}`);
|
|
1120
|
+
body = body.replace(anchor, inject);
|
|
1121
|
+
}
|
|
1122
|
+
src = src.slice(0, start) + body + src.slice(end);
|
|
1123
|
+
await writeFile(file, src);
|
|
1124
|
+
}
|
|
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
|
+
|
|
1073
1136
|
export async function patchTypedArrayFrom(root: string): Promise<void> {
|
|
1074
1137
|
const file = resolve(root, "compiler/builtins/typedarray.js");
|
|
1075
1138
|
const src = await readFile(file, "utf8");
|
|
@@ -1146,8 +1209,10 @@ export async function ensurePorfforPatched(root: string): Promise<void> {
|
|
|
1146
1209
|
await patchRenderJs(root);
|
|
1147
1210
|
await patchDateParser(root);
|
|
1148
1211
|
await patchPromiseResolveNull(root);
|
|
1149
|
-
|
|
1150
|
-
|
|
1212
|
+
await patchReplaceAll(root);
|
|
1213
|
+
await patchTypedArrayStore(root);
|
|
1214
|
+
// Last of the builtin edits: its precompile picks up the date, promise and
|
|
1215
|
+
// replaceAll patches above too.
|
|
1151
1216
|
await patchTypedArrayFrom(root);
|
|
1152
1217
|
await patchUnicode(root);
|
|
1153
1218
|
done.add(root);
|