@sproutboat/toolchain 0.4.19 → 0.4.20

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.20
4
+
5
+ ### Patch Changes
6
+
7
+ - 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.
8
+
3
9
  ## 0.4.19
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.19",
3
+ "version": "0.4.20",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -120,6 +120,12 @@ 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/builtins/string.ts":
124
+ "export const __Porffor_string_replaceAll = (str: any, searchValue: any, replaceValue: any) => {\n" +
125
+ " let out: any = __Porffor_string_emptyLike(str);\n let appendIndex: i32 = 0;\n let searchIndex: i32 = 0;\n let matched: boolean = false;\n" +
126
+ " out = __Porffor_strcat(out, __Porffor_string_substringLike(str, appendIndex, matchIndex));\n out = __Porffor_strcat(out, __Porffor_string_applyReplacer(str, match, matchIndex, replaceValue));\n" +
127
+ " if (!matched) return str;\n return __Porffor_strcat(out, __Porffor_string_substringLike(str, appendIndex, thisLen));\n};\n" +
128
+ "export const __String_prototype_replaceAll = function (this: string, searchValue: any, replaceValue: any) {};\n",
123
129
  "compiler/builtins/promise.ts":
124
130
  "export const __Porffor_promise_resolve = (value: any, promise: any): void => {\n" +
125
131
  " if (Porffor.type(value) == Porffor.TYPES.object) {\n" +
@@ -202,6 +208,10 @@ test("the default call (no url/expectedSha256 override) never touches the networ
202
208
  },
203
209
  });
204
210
  expect(await readFile(join(dir, "runtime/index.js"), "utf8")).not.toBe("");
211
+ // #237: replaceAll joins its pieces once instead of a strcat per match.
212
+ expect(await readFile(join(dir, "compiler/builtins/string.ts"), "utf8")).toContain(
213
+ "return Porffor.callThis(__Array_prototype_join, pieces, '');",
214
+ );
205
215
  // #236: the native-fetch build edits uWebSockets to accept HTTP/1.0.
206
216
  const index = await readFile(join(dir, "compiler/index.js"), "utf8");
207
217
  expect(index).toContain("import { sbPatchHttp10 } from './sb-http10.js';");
package/src/patch.ts CHANGED
@@ -270,6 +270,27 @@ 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
+
273
294
  // Porffor's TypedArray.from only handles iterables. An array-like input such
274
295
  // as { length: 16 } silently becomes an empty typed array, including HMAC
275
296
  // keys made with Uint8Array.from({ length: 16 }, mapFn). The compiler uses a
@@ -1070,6 +1091,23 @@ export async function patchPromiseResolveNull(root: string): Promise<void> {
1070
1091
  await writeFile(file, src.replace(PROMISE_NULL_ANCHOR, PROMISE_NULL_INJECT));
1071
1092
  }
1072
1093
 
1094
+ /** #237: linear String.prototype.replaceAll. Precompiled by patchTypedArrayFrom. */
1095
+ export async function patchReplaceAll(root: string): Promise<void> {
1096
+ const file = resolve(root, "compiler/builtins/string.ts");
1097
+ let src = await readFile(file, "utf8");
1098
+ if (src.includes(REPLACE_ALL_MARKER)) return;
1099
+ const start = src.indexOf("export const __Porffor_string_replaceAll = ");
1100
+ const end = src.indexOf("export const __String_prototype_replaceAll = ", start);
1101
+ if (start < 0 || end < 0) throw new Error(`could not find Porffor's replaceAll in ${file}`);
1102
+ let body = src.slice(start, end);
1103
+ for (const [anchor, inject] of REPLACE_ALL_EDITS) {
1104
+ if (!body.includes(anchor)) throw new Error(`could not patch Porffor's replaceAll: anchor not found in ${file}`);
1105
+ body = body.replace(anchor, inject);
1106
+ }
1107
+ src = src.slice(0, start) + body + src.slice(end);
1108
+ await writeFile(file, src);
1109
+ }
1110
+
1073
1111
  export async function patchTypedArrayFrom(root: string): Promise<void> {
1074
1112
  const file = resolve(root, "compiler/builtins/typedarray.js");
1075
1113
  const src = await readFile(file, "utf8");
@@ -1146,8 +1184,9 @@ export async function ensurePorfforPatched(root: string): Promise<void> {
1146
1184
  await patchRenderJs(root);
1147
1185
  await patchDateParser(root);
1148
1186
  await patchPromiseResolveNull(root);
1149
- // Last of the builtin edits: its precompile picks up the date and promise
1150
- // patches above too.
1187
+ await patchReplaceAll(root);
1188
+ // Last of the builtin edits: its precompile picks up the date, promise and
1189
+ // replaceAll patches above too.
1151
1190
  await patchTypedArrayFrom(root);
1152
1191
  await patchUnicode(root);
1153
1192
  done.add(root);