@sproutboat/toolchain 0.4.22 → 0.4.24
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 +7 -0
- package/src/patch.ts +34 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @sproutboat/toolchain
|
|
2
2
|
|
|
3
|
+
## 0.4.24
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 9225026: `join()` and `toString()` work on typed arrays (baronunread/sproutboat#242). Porffor generates typed-array methods from the Array ones by replacing `any[]` with the array's type, which also retyped `join`'s internal helper array. `new Uint8Array([1,2]).join("-")` returned garbage, and a three-element `Uint16Array.join` crashed. The generator now keeps that helper a plain array.
|
|
8
|
+
|
|
9
|
+
## 0.4.23
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- a479611: Typed-array writes outside the array are ignored, as the spec requires, instead of overwriting the next allocation in memory (baronunread/sproutboat#241). `a[length] = x`, `a[-1] = x` (which wrote `a[0]`) and `a[1.5] = x` no longer touch any memory. The check uses the same index validation as reads and applies to user code; Porffor's own builtins write within bounds.
|
|
14
|
+
|
|
3
15
|
## 0.4.22
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
package/src/acquire.test.ts
CHANGED
|
@@ -40,6 +40,7 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
|
|
|
40
40
|
"compiler/builtins_precompiled.js": "fixture:precompiled\n",
|
|
41
41
|
"compiler/builtins/date.ts": "export const __ecma262_ParseDTSF = (string: string) => {\n let n: number = 0;\n let nInd: number = 0;\n\n const len: i32 = string.length;\n const endPtr: i32 = Porffor.IR.ptr(string) + len;\n let ptr: i32 = Porffor.IR.ptr(string);\n\n while (ptr <= endPtr) { // <= to include extra null byte to set last n\n const chr: i32 = Porffor.IR.loadU8(ptr++, 4);\n if (Porffor.fastAnd(chr >= 48, chr <= 57)) { // 0-9\n n *= 10;\n n += chr - 48;\n continue;\n }\n\n if (chr == 45) { // -\n if (Porffor.fastOr(ptr == Porffor.IR.ptr(string), nInd == 7)) n = -n;\n }\n\n if (n > 0) {\n if (nInd == 0) y = n;\n else if (nInd == 1) m = n - 1;\n else if (nInd == 2) dt = n;\n else if (nInd == 3) h = n;\n else if (nInd == 4) min = n;\n else if (nInd == 5) s = n;\n else if (nInd == 6) milli = n;\n else if (nInd == 7) tzHour = n;\n else if (nInd == 8) tzMin = n;\n\n n = 0;\n nInd++;\n }\n }\n\n h += tzHour;\n min += tzMin;\n};\n// RFC 7231 or Date.prototype.toString() parser\n",
|
|
42
42
|
"compiler/builtins/typedarray.js":
|
|
43
|
+
"${typedArrayFuncs.reduce((acc, x) => acc + x.replace('// @porf-typed-array\\n', '').replaceAll('Array', name).replaceAll('any[]', name) + '\\n\\n', '')}`;\n" +
|
|
43
44
|
" offset = Math.trunc(offset);\n if (Porffor.fastOr(offset < 0, offset > len)) throw new RangeError('Offset out of bounds');\n" +
|
|
44
45
|
"export const __${name}_from = (arg: any, mapFn: any): ${name} => {\n" +
|
|
45
46
|
" const arr: any[] = Porffor.array.new(4);\n" +
|
|
@@ -217,6 +218,10 @@ test("the default call (no url/expectedSha256 override) never touches the networ
|
|
|
217
218
|
},
|
|
218
219
|
});
|
|
219
220
|
expect(await readFile(join(dir, "runtime/index.js"), "utf8")).not.toBe("");
|
|
221
|
+
// #242: generated typed-array join/toString keep a plain-array `parts`.
|
|
222
|
+
expect(await readFile(join(dir, "compiler/builtins/typedarray.js"), "utf8")).toContain(
|
|
223
|
+
".replaceAll('const parts: ' + name + ' =', 'const parts: any[] =')",
|
|
224
|
+
);
|
|
220
225
|
// #238: TypedArray.prototype.set(source) with no offset copies to index 0.
|
|
221
226
|
expect(await readFile(join(dir, "compiler/builtins/typedarray.js"), "utf8")).toContain(
|
|
222
227
|
"offset = ecma262.ToIntegerOrInfinity(offset);",
|
|
@@ -225,6 +230,8 @@ test("the default call (no url/expectedSha256 override) never touches the networ
|
|
|
225
230
|
const codegen = await readFile(join(dir, "compiler/codegen.js"), "utf8");
|
|
226
231
|
expect(codegen).toContain("Convert(T.i32, toUint32(scope, numValue(toNumeric())), CONVERT_RANGE_KNOWN | CONVERT_SIGNED)");
|
|
227
232
|
expect(codegen).toContain("Box(f, Const(T.i32, TYPES.number)), valUndefined());");
|
|
233
|
+
// #241: out-of-range typed-array writes are ignored in user code.
|
|
234
|
+
expect(codegen).toContain("if (globalThis.precompile) store();");
|
|
228
235
|
// #238: integer typed-array stores wrap modulo 2^n instead of saturating.
|
|
229
236
|
expect(await readFile(join(dir, "compiler/codegen.js"), "utf8")).toContain(
|
|
230
237
|
"signed ? Convert(T.i32, toUint32(scope, f), CONVERT_RANGE_KNOWN | CONVERT_SIGNED) : toUint32(scope, f)",
|
package/src/patch.ts
CHANGED
|
@@ -346,6 +346,23 @@ const TA_GET_INJECT =
|
|
|
346
346
|
" Box(f, Const(T.i32, TYPES.number)), valUndefined());\n" +
|
|
347
347
|
" };";
|
|
348
348
|
const TA_GET_MARKER = "sproutboat #238: only a valid in-range integer index reads";
|
|
349
|
+
// baronunread/sproutboat#241: a typed-array write outside the array stored into
|
|
350
|
+
// whatever memory followed it (and a[-1] = x wrote a[0], via the saturating
|
|
351
|
+
// index conversion). ECMA-262 ignores the write. Same check as taGet, in user
|
|
352
|
+
// code only: builtins such as the constructor store elements before they set
|
|
353
|
+
// the length, and they only write in bounds anyway. Applies after TA_STORE,
|
|
354
|
+
// whose output line is the anchor.
|
|
355
|
+
const TA_SET_BOUNDS_ANCHOR =
|
|
356
|
+
" 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)));";
|
|
357
|
+
const TA_SET_BOUNDS_INJECT =
|
|
358
|
+
" const store = () => 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)));\n" +
|
|
359
|
+
" // sproutboat #241: a write outside the array is ignored, not stored into the next allocation.\n" +
|
|
360
|
+
" if (globalThis.precompile) store();\n" +
|
|
361
|
+
" else {\n" +
|
|
362
|
+
" const { idx: sbIdx, valid: sbValid } = denseArrayIndexKey(scope, prop);\n" +
|
|
363
|
+
" emitIf(scope, Bin('&&', T.i32, sbValid, Bin('<', T.i32, sbIdx, Load('u32', JvPtr(obj), 0))), store);\n" +
|
|
364
|
+
" }";
|
|
365
|
+
const TA_SET_BOUNDS_MARKER = "sproutboat #241: a write outside the array is ignored";
|
|
349
366
|
|
|
350
367
|
// baronunread/sproutboat#238: TypedArray.prototype.set(source) with no offset
|
|
351
368
|
// ran `offset = Math.trunc(undefined)`, which is NaN. The same-type fast path
|
|
@@ -358,6 +375,17 @@ const TA_SET_OFFSET_INJECT =
|
|
|
358
375
|
" offset = ecma262.ToIntegerOrInfinity(offset); // sproutboat #238: undefined offset is 0, not NaN\n if (Porffor.fastOr(offset < 0, offset > len)) throw new RangeError('Offset out of bounds');";
|
|
359
376
|
const TA_SET_OFFSET_MARKER = "sproutboat #238: undefined offset is 0";
|
|
360
377
|
|
|
378
|
+
// baronunread/sproutboat#242: typedarray.js generates each TypedArray method
|
|
379
|
+
// from array.ts with `.replaceAll('any[]', name)`. That also retyped join's and
|
|
380
|
+
// toString's internal `const parts: any[]` helper as e.g. a Uint8Array, so the
|
|
381
|
+
// joined strings were stored into a "typed array": Uint8Array join returned
|
|
382
|
+
// garbage and Uint16Array join crashed. Put `parts` back to a plain array after
|
|
383
|
+
// the replacement; map/filter's intentionally typed `out` is untouched.
|
|
384
|
+
const TA_JOIN_ANCHOR = ".replaceAll('Array', name).replaceAll('any[]', name) + '\\n\\n'";
|
|
385
|
+
const TA_JOIN_INJECT =
|
|
386
|
+
".replaceAll('Array', name).replaceAll('any[]', name).replaceAll('const parts: ' + name + ' =', 'const parts: any[] =') /* sproutboat #242 */ + '\\n\\n'";
|
|
387
|
+
const TA_JOIN_MARKER = "sproutboat #242";
|
|
388
|
+
|
|
361
389
|
// Porffor's TypedArray.from only handles iterables. An array-like input such
|
|
362
390
|
// as { length: 16 } silently becomes an empty typed array, including HMAC
|
|
363
391
|
// keys made with Uint8Array.from({ length: 16 }, mapFn). The compiler uses a
|
|
@@ -1184,6 +1212,7 @@ export async function patchTypedArrayStore(root: string): Promise<void> {
|
|
|
1184
1212
|
[TA_STORE_MARKER, TA_STORE_ANCHOR, TA_STORE_INJECT, "typed-array store conversion"],
|
|
1185
1213
|
[BITNOT_MARKER, BITNOT_ANCHOR, BITNOT_INJECT, "bitwise NOT conversion"],
|
|
1186
1214
|
[TA_GET_MARKER, TA_GET_ANCHOR, TA_GET_INJECT, "typed-array read bounds"],
|
|
1215
|
+
[TA_SET_BOUNDS_MARKER, TA_SET_BOUNDS_ANCHOR, TA_SET_BOUNDS_INJECT, "typed-array write bounds"],
|
|
1187
1216
|
] as const) {
|
|
1188
1217
|
if (src.includes(marker)) continue;
|
|
1189
1218
|
if (!src.includes(anchor)) throw new Error(`could not patch Porffor's ${what}: anchor not found in ${file}`);
|
|
@@ -1201,6 +1230,11 @@ export async function patchTypedArrayFrom(root: string): Promise<void> {
|
|
|
1201
1230
|
}
|
|
1202
1231
|
let edited = src;
|
|
1203
1232
|
if (!edited.includes(TYPED_ARRAY_FROM_MARKER)) edited = edited.replace(TYPED_ARRAY_FROM_ANCHOR, TYPED_ARRAY_FROM_INJECT);
|
|
1233
|
+
if (!edited.includes(TA_JOIN_MARKER)) {
|
|
1234
|
+
if (!edited.includes(TA_JOIN_ANCHOR))
|
|
1235
|
+
throw new Error(`could not patch Porffor's typed-array method generator: anchor not found in ${file}`);
|
|
1236
|
+
edited = edited.replace(TA_JOIN_ANCHOR, TA_JOIN_INJECT);
|
|
1237
|
+
}
|
|
1204
1238
|
if (!edited.includes(TA_SET_OFFSET_MARKER)) {
|
|
1205
1239
|
if (!edited.includes(TA_SET_OFFSET_ANCHOR))
|
|
1206
1240
|
throw new Error(`could not patch Porffor's TypedArray.prototype.set offset: anchor not found in ${file}`);
|