diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 02bf37192..f83896ad4 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -405,6 +405,8 @@ export const LIB_FN_SYMS: Record = { "buffer.fromDyn": "scr_buffer_from_dyn", "buffer.concat": "scr_buffer_concat", "buffer.concatLen": "scr_buffer_concat_len", + "buffer.concatDyn": "scr_buffer_concat_dyn", + "buffer.concatLenDyn": "scr_buffer_concat_len_dyn", "buffer.alloc": "scr_buffer_alloc", "buffer.allocUnsafe": "scr_buffer_alloc_unsafe", "buffer.allocUnsafeSlow": "scr_buffer_alloc_unsafe_slow", diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index 7a19927de..c41bf5163 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -1511,11 +1511,23 @@ function lowerBufferStaticValue( } else { list = lowerer.lowerExpr(argNode); if (!typeEquals(list.type, arrayOf(BYTES_U8))) { - lowerer.noLowering( - `Buffer.concat of '${lowerer.fmt(list.type)}' values`, - argNode, - "one Uint8Array[]/Buffer[] value (or literal) is the lowered list shape", - ); + // Any other list (JavaScript's untyped `const chunks = []`, or a + // union of arrays): the runtime checks it as Node does (an Array of + // Uint8Arrays, the ERR_INVALID_ARG_TYPE errors otherwise) before + // concatenating. + const value: IrExpr = + list.type.kind === "dyn" ? list : { kind: "dynFrom", value: list, type: DYN, loc }; + if (args.length === 2) { + const total = lowerer.lowerExprExpecting(args[1]!, F64); + return { + kind: "libCall", + fn: "buffer.concatLenDyn", + args: [value, total], + type: BYTES_U8, + loc, + }; + } + return { kind: "libCall", fn: "buffer.concatDyn", args: [value], type: BYTES_U8, loc }; } } if (args.length === 2) { diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 1bb9cbd14..1c691f7d8 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -686,6 +686,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "crypto.scryptCb", "buffer.concat", "buffer.concatLen", + "buffer.concatDyn", + "buffer.concatLenDyn", "buffer.fromDyn", "buffer.fromStrChecked", "buffer.alloc", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 08c493ae4..f673ebd0c 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -2624,6 +2624,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * opaque reference inputs retain explicit runtime refusals. */ "buffer.fromDyn": { argTypes: [DYN, STRING], result: BYTES_U8 }, "buffer.concat": { argTypes: [arrayOf(BYTES_U8)], result: BYTES_U8 }, + "buffer.concatDyn": { argTypes: [DYN], result: BYTES_U8 }, /** The pooled Buffer factories (Node's allocate/fromStringFast/ * fromArrayLike): allocUnsafe, Buffer.from of strings, Uint8Arrays, * number[] and checked values, and concat return views into the @@ -2650,6 +2651,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * zero-padded to the total. THROWS Node's 'length' RangeError on a * negative/non-integer total (may-throw seed). */ "buffer.concatLen": { argTypes: [arrayOf(BYTES_U8), F64], result: BYTES_U8 }, + "buffer.concatLenDyn": { argTypes: [DYN, F64], result: BYTES_U8 }, // The checked-dynamic compare/equals validators (Node's argument // ladders over dyn-boxed invalid-input probes). /** The checked-dynamic compare/equals validators (scr_bytes_io.c) — diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5973-buffer-concat-untyped.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5973-buffer-concat-untyped.js.json new file mode 100644 index 000000000..3fffd75c2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5973-buffer-concat-untyped.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5973-buffer-concat-untyped.js" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h index 4309154c9..993921043 100644 --- a/packages/runtime/src/scr_bytes.h +++ b/packages/runtime/src/scr_bytes.h @@ -389,6 +389,10 @@ ScrBytes *scr_buffer_from_arr(const ScrArr *arr); /* number[] */ ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding); ScrBytes *scr_buffer_concat(const ScrArr *list); ScrBytes *scr_buffer_concat_len(const ScrArr *list, double total); +/* The untyped-list forms: Node's ERR_INVALID_ARG_TYPE checks on the list + * and each element, then the typed concat. NULL with the exception pending. */ +ScrBytes *scr_buffer_concat_dyn(const struct ScrDyn *list); +ScrBytes *scr_buffer_concat_len_dyn(const struct ScrDyn *list, double total); /* Drops the pool's slab reference and restores Buffer.poolSize (process * exit audits and library session resets). */ void scr_buffer_pool_reset(void); diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index 6fe829eeb..4d7ef5f67 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -1249,6 +1249,56 @@ static ScrBytes *scr_buffer_from_array_like(const ScrDyn *value, double length) return out; } +/* Buffer.concat over an untyped list: Node's checks in Node's order (an + * Array, then the total of the two-argument form unless the list is empty, + * then every element a Uint8Array, ERR_INVALID_ARG_TYPE otherwise), then the + * typed concat over the elements. NULL with the exception pending. */ +static ScrBytes *scr_buffer_concat_dyn_impl(const ScrDyn *list, double total, bool has_total) { + ScrDyn *materialized = NULL; + if (list->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_array_ops(list) != NULL) { + materialized = scr_dyn_typed_ref_materialize(list); + list = materialized; + } + if (list->kind != SCR_DYN_ARR) { + scr_dyn_arg_type_fail("list", "an instance of Array", list); + scr_dyn_release(materialized); + return NULL; + } + size_t n = list->v.arr.len; + if (has_total && n > 0 && !scr_bytes_validate_off("length", total, 9007199254740991.0)) { + scr_dyn_release(materialized); + return NULL; + } + ScrArr *parts = scr_arr_new(SCR_ELEM_BYTES, (double)n); + for (size_t i = 0; i < n; i++) { + ScrDyn *item = scr_dyn_index_get(list, (double)i); + const ScrDyn *shown = item != NULL ? item : scr_dyn_undefined(); + if (!scr_dyn_bytes_is(shown, SCR_BYTES_U8) || shown->v.bytes->is_data_view) { + char name[40]; + snprintf(name, sizeof name, "list[%zu]", i); + scr_dyn_arg_type_fail(name, "an instance of Buffer or Uint8Array", shown); + scr_dyn_release(item); + scr_arr_release(parts); + scr_dyn_release(materialized); + return NULL; + } + scr_arr_push_ref(parts, scr_bytes_retain(shown->v.bytes)); + scr_dyn_release(item); + } + ScrBytes *out = has_total ? scr_buffer_concat_len(parts, total) : scr_buffer_concat(parts); + scr_arr_release(parts); + scr_dyn_release(materialized); + return out; +} + +ScrBytes *scr_buffer_concat_dyn(const ScrDyn *list) { + return scr_buffer_concat_dyn_impl(list, 0, false); +} + +ScrBytes *scr_buffer_concat_len_dyn(const ScrDyn *list, double total) { + return scr_buffer_concat_dyn_impl(list, total, true); +} + ScrBytes *scr_buffer_from_dyn(const ScrDyn *value, const ScrStr *encoding) { if (value->kind == SCR_DYN_STR) return scr_bytes_from_str(value->v.str, encoding); if (value->kind == SCR_DYN_BYTES) return scr_bytes_convert(SCR_BYTES_U8, value->v.bytes); diff --git a/tests/corpus/5973-buffer-concat-untyped.js b/tests/corpus/5973-buffer-concat-untyped.js new file mode 100644 index 000000000..116ca0a2b --- /dev/null +++ b/tests/corpus/5973-buffer-concat-untyped.js @@ -0,0 +1,38 @@ +// Buffer.concat over an untyped list (JavaScript's `const chunks = []`): +// Uint8Array elements concatenate, the length form pads or truncates, and +// a non-array list or a non-Uint8Array element throws Node's TypeError. The +// length form checks the length before the elements, and an empty list +// returns before either. +const chunks = []; +chunks.push(Buffer.from("ab")); +chunks.push(new Uint8Array([99, 100])); +console.log(Buffer.concat(chunks).toString()); +console.log(Buffer.concat(chunks, 6)); +console.log(Buffer.concat(chunks, 3).toString()); +console.log(Buffer.concat([]).length); + +const lists = [[1], ["a"], [Buffer.from("x"), null], "x", 5, [new Uint16Array(1)]]; +for (const list of lists) { + try { + Buffer.concat(list); + console.log("no error"); + } catch (e) { + console.log(e.name, e.code, e.message); + } +} + +const withLength = [ + [[1], -1], + [[Buffer.from("a"), "b"], NaN], + [[1], 2], + [[], -1], + [[Buffer.from("a")], 1.5], + [[Buffer.from("a"), Buffer.from("b")], 2], +]; +for (const [list, length] of withLength) { + try { + console.log("length", Buffer.concat(list, length).length); + } catch (e) { + console.log(e.name, e.code, e.message); + } +} diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 7d8030e95..2814896fe 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2160,6 +2160,7 @@ "5940-callback-helper-specialization.ts": 3109, "5941-callback-specialization-recursion.ts": 3112, "5942-callback-specialization-throws.ts": 3221, + "5942-local-class-observed-methods.ts": 5063, "5943-fields-read-before-assignment.ts": 6856, "5944-js-fields-read-before-assignment.js": 4247, "5945-local-class-observed-methods.ts": 5063, @@ -2179,6 +2180,7 @@ "5970-net-connect-host-name.ts": 2400, "5971-buffer-chunks-to-string.ts": 2700, "5972-net-set-encoding-listeners.ts": 2550, + "5973-buffer-concat-untyped.js": 1900, "5973-large-closure-environment.ts": 4499, "5974-typeof-module-exports.cjs": 2000, "5975-zlib-class-values.ts": 2800, @@ -5182,6 +5184,7 @@ "5940-callback-helper-specialization.ts": 4548, "5941-callback-specialization-recursion.ts": 4448, "5942-callback-specialization-throws.ts": 3443, + "5942-local-class-observed-methods.ts": 6212, "5943-fields-read-before-assignment.ts": 72293, "5944-js-fields-read-before-assignment.js": 40310, "5945-local-class-observed-methods.ts": 6212, @@ -5201,6 +5204,7 @@ "5970-net-connect-host-name.ts": 2800, "5971-buffer-chunks-to-string.ts": 3100, "5972-net-set-encoding-listeners.ts": 3000, + "5973-buffer-concat-untyped.js": 2900, "5973-large-closure-environment.ts": 5145, "5974-typeof-module-exports.cjs": 2500, "5975-zlib-class-values.ts": 4200, diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts index a6cdee2b5..71b8a61d1 100644 --- a/tests/harness/npm.test.ts +++ b/tests/harness/npm.test.ts @@ -109,7 +109,6 @@ const KNOWN: Record = { "web-streams": { build: "SC2013" }, "workspace-copied": { build: "SC2013" }, "workspace-unclean": { build: "SC2013" }, - "zlib-shims": { run: "SC2020" }, }; const cases = shardSelect(npmCases(fixturesRoot), (c) => c.name);