From 23d4eec3db157228db087403b40a153ee0ff74c8 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 9 Nov 2026 13:02:24 +0000 Subject: [PATCH 1/3] test/aot: write no copy of Bun for each proxy trap scene and pin what each scene prints a-proxy-is-described-and-defined-on-by-its-traps.test.ts builds one program of all 43 scenes and one program for each scene. Each build wrote a copy of Bun. On Linux the 43 builds of one scene now write the program alone, as run-parallel.ts builds the stored programs. The program of all scenes stays an executable, and its case runs beside the others. Each scene stores what it says. Plain Bun and the compiled program must each print that. A `bun check` case compares the verdict lines of the report and the exit code. --- ...cribed-and-defined-on-by-its-traps.test.ts | 289 ++++++++++++++++-- 1 file changed, 258 insertions(+), 31 deletions(-) diff --git a/test/aot/a-proxy-is-described-and-defined-on-by-its-traps.test.ts b/test/aot/a-proxy-is-described-and-defined-on-by-its-traps.test.ts index 5ee926bee7ea..1dcffe619802 100644 --- a/test/aot/a-proxy-is-described-and-defined-on-by-its-traps.test.ts +++ b/test/aot/a-proxy-is-described-and-defined-on-by-its-traps.test.ts @@ -3,35 +3,43 @@ // what the traps get and what they answer has to be what plain Bun hands them, and a program that does no more than this to a proxy // uses no more of the built-ins than it names. JSON.stringify asks a proxy by get, ownKeys and getOwnPropertyDescriptor in the same way. // console.log prints the target of a proxy and asks nothing. Each scene has a proxy and a handler of its own. +// +// Behind its text a scene has what it says: how it ended, and the lines that it logged, as the program of all scenes prints them behind +// the name of the scene. A scene that prints through console itself has what its own program writes to stdout behind that, and to stderr. +// Plain Bun has to say that, and so has the compiled program. import { expect } from "bun:test"; -import { bunEnv, bunExe, isWindows, tempDir } from "harness"; +import { bunEnv, bunExe, isLinux, isWindows, tempDir } from "harness"; import { join } from "node:path"; import { built, slot, spawned, told } from "./harness/commands"; import { sayGoal } from "./harness/goals"; import { test } from "./supported"; -const scenes: [name: string, does: string][] = [ +const scenes: [name: string, does: string, says: string, stdout?: string, stderr?: string][] = [ [ "the answer holds what the trap's answer holds", `const proxy = new Proxy({}, { getOwnPropertyDescriptor(t, k) { log("describe", k); return { value: { mark: "made by the trap" }, configurable: true }; } }); const d = Object.getOwnPropertyDescriptor(proxy, "a"); log(d.value.mark, d.writable, d.enumerable, d.configurable, Object.keys(d));`, + "returned: describe a | made by the trap false false true [value,writable,enumerable,configurable]", ], [ "the answer holds the accessors that the trap answers", `const proxy = new Proxy({}, { getOwnPropertyDescriptor(t, k) { return { get() { return "got by " + typeof this; }, set(v) { log("set", v); }, configurable: true }; } }); const d = Reflect.getOwnPropertyDescriptor(proxy, "a"); log(d.get(), d.set("x"), Object.keys(d));`, + "returned: set x | got by object undefined [get,set,enumerable,configurable]", ], [ "a trap that answers nothing", `const proxy = new Proxy({ a: 1 }, { getOwnPropertyDescriptor(t, k) { log("describe", k); } }); log(Object.getOwnPropertyDescriptor(proxy, "a"), Object.hasOwn(proxy, "a"));`, + "returned: describe a | describe a | undefined false", ], [ "a getter of the trap's answer runs", `const proxy = new Proxy({}, { getOwnPropertyDescriptor() { return { get value() { log("value is read"); return { mark: "from the getter" }; }, configurable: true }; } }); log(Object.getOwnPropertyDescriptor(proxy, "a").value.mark);`, + "returned: value is read | from the getter", ], [ "what was described is defined elsewhere under the same key", @@ -39,12 +47,14 @@ const scenes: [name: string, does: string][] = [ const other = {}; Object.defineProperty(other, "a", Object.getOwnPropertyDescriptor(proxy, "a")); log(other.a.mark, Object.keys(other));`, + "returned: described [a]", ], [ "a field of the target that only the trap's built-in sees", `const target = { hidden: { mark: "on the target" }, other: 2 }; const proxy = new Proxy(target, { getOwnPropertyDescriptor(t, k) { return Reflect.getOwnPropertyDescriptor(t, k); } }); log(Object.getOwnPropertyDescriptor(proxy, "hidden").value.mark, Object.hasOwn(proxy, "other"), Object.hasOwn(proxy, "none"));`, + "returned: on the target true false", ], [ "the trap gets a descriptor of the engine's own", @@ -52,6 +62,7 @@ const scenes: [name: string, does: string][] = [ const proxy = new Proxy({}, { defineProperty(t, k, d) { kept = d; log("define", k, Object.keys(d), "get" in d, d.value.mark, d.writable, d.enumerable, d.configurable); return true; } }); const given = { value: { mark: "given" }, enumerable: true, extra: 1 }; log(Object.defineProperty(proxy, "a", given) === proxy, kept === given, Object.getPrototypeOf(kept) === Object.prototype, kept.extra);`, + "returned: define a [value,enumerable] false given undefined true undefined | true false true undefined", ], [ "a trap that writes to its descriptor leaves the program's alone", @@ -59,6 +70,7 @@ const scenes: [name: string, does: string][] = [ const given = { value: { mark: "given" }, configurable: true }; Reflect.defineProperty(proxy, "a", given); log(given.value.mark, Object.keys(given));`, + "returned: given [value,configurable]", ], [ "the value that is defined goes where the trap puts it", @@ -66,22 +78,26 @@ const scenes: [name: string, does: string][] = [ const proxy = new Proxy({}, { get(t, k) { return side[k]; }, defineProperty(t, k, d) { side[k] = d.value; return true; } }); Object.defineProperty(proxy, "a", { value: { mark: "through the trap" }, configurable: true }); log(proxy.a.mark, side.a.mark);`, + "returned: through the trap through the trap", ], [ "the accessors that are defined reach the trap", `const proxy = new Proxy({}, { defineProperty(t, k, d) { log(Object.keys(d), d.get(), d.set("y")); return true; } }); Object.defineProperty(proxy, "a", { get() { return "the getter ran"; }, set(v) { log("the setter got", v); }, configurable: true });`, + "returned: the setter got y | [get,set,configurable] the getter ran undefined", ], [ "the proxy that Object.defineProperty answers is the proxy", `const proxy = new Proxy({ a: "of the target" }, { get(t, k) { log("get", k); return t[k]; }, defineProperty() { return true; } }); log(Object.defineProperty(proxy, "b", { value: 1, configurable: true }).a);`, + "returned: get a | of the target", ], [ "a trap that refuses", `const proxy = new Proxy({}, { defineProperty() { return false; } }); log(Reflect.defineProperty(proxy, "a", { value: 1 })); Object.defineProperty(proxy, "a", { value: 1 });`, + "TypeError: false", ], [ "a trap that throws", @@ -90,6 +106,7 @@ const scenes: [name: string, does: string][] = [ try { Object.getOwnPropertyDescriptor(proxy, "a"); } catch (e) { log(e.mark); } try { Object.hasOwn(proxy, "a"); } catch (e) { log(e.mark); } try { Reflect.set({}, "a", 1, proxy); } catch (e) { log(e.mark); }`, + "returned: thrown by define | thrown by describe | thrown by describe | thrown by describe", ], [ "Reflect.set asks its receiver what is there and defines the value", @@ -100,11 +117,13 @@ const scenes: [name: string, does: string][] = [ log(Reflect.set({}, "a", { mark: "first" }, proxy)); log(Reflect.set({}, "a", { mark: "second" }, proxy)); log(Object.getOwnPropertyDescriptor(proxy, "a").value.mark);`, + "returned: describe a | define a [value,writable,enumerable,configurable] first | true | describe a | define a [value] second | true | describe a | second", ], [ "a setter that Reflect.set finds runs for the proxy", `const proxy = new Proxy({ name: "the target" }, { get(t, k) { log("get", k); return t[k]; }, getOwnPropertyDescriptor() { log("describe"); }, defineProperty() { log("define"); return true; } }); log(Reflect.set({ set a(v) { log("setter", v, this === proxy, this.name); } }, "a", 1, proxy));`, + "returned: get name | setter 1 true the target | true", ], [ "a store on the proxy and on its heir", @@ -120,6 +139,7 @@ const scenes: [name: string, does: string][] = [ heir.b = "of the heir"; log(proxy.a, proxy.b, heir.a, heir.b, Object.keys(heir), Object.keys(values)); log(Reflect.set(proxy, "c", "for another proxy", new Proxy({}, { defineProperty(t, k, d) { log("the other defines", k, d.value); return true; }, getOwnPropertyDescriptor() {} })));`, + "returned: STORED undefined STORED of the heir [b] [a] | the other defines c for another proxy | true", ], [ "a store under a key of the command line", @@ -132,6 +152,7 @@ const scenes: [name: string, does: string][] = [ }); proxy[process.argv[2] ?? "a"] = 1; log(proxy.a, Object.keys(values));`, + "returned: 1 [a]", ], [ "the three tests for an own property", @@ -140,6 +161,7 @@ const scenes: [name: string, does: string][] = [ log(Object.prototype.hasOwnProperty.call(proxy, "a"), Object.prototype.hasOwnProperty.call(proxy, "none")); log(Object.prototype.propertyIsEnumerable.call(proxy, "a"), Object.prototype.propertyIsEnumerable.call(proxy, "made")); log({}.hasOwnProperty.call(proxy, Symbol.iterator));`, + "returned: describe a | describe made | describe none | true true false | describe a | describe none | true false | describe a | describe made | true false | describe Symbol(Symbol.iterator) | false", ], [ "a handler without the trap hands the question on to the target", @@ -149,6 +171,7 @@ const scenes: [name: string, does: string][] = [ Object.defineProperty(proxy, "b", { value: { mark: "defined through" }, enumerable: true }); log(target.b.mark, Object.keys(target)); log(Reflect.set({}, "c", { mark: "set through" }, proxy), target.c.mark);`, + "returned: of the target true | defined through [a,b] | true set through", ], [ "a trap that is put there later", @@ -159,6 +182,7 @@ const scenes: [name: string, does: string][] = [ handler.defineProperty = (t, k, d) => { log("the later trap defines", d.value); return true; }; log(Object.getOwnPropertyDescriptor(proxy, "a").value.mark); Object.defineProperty(proxy, "a", { value: 2 });`, + "returned: 1 | from the later trap | the later trap defines 2", ], [ "traps that are built-ins", @@ -166,6 +190,7 @@ const scenes: [name: string, does: string][] = [ const proxy = new Proxy(target, { getOwnPropertyDescriptor: Reflect.getOwnPropertyDescriptor, defineProperty: Reflect.defineProperty }); Object.defineProperty(proxy, "a", { value: { mark: "by Reflect" }, configurable: true }); log(target.a.mark, Object.getOwnPropertyDescriptor(proxy, "a").value.mark, Reflect.set({}, "b", { mark: "set" }, proxy), target.b.mark);`, + "returned: by Reflect by Reflect true set", ], [ "a proxy of a proxy", @@ -173,6 +198,7 @@ const scenes: [name: string, does: string][] = [ const outer = new Proxy(inner, { getOwnPropertyDescriptor(t, k) { log("outer describes", k); return Reflect.getOwnPropertyDescriptor(t, k); }, defineProperty(t, k, d) { log("outer defines", k); return Reflect.defineProperty(t, k, d); } }); Object.defineProperty(outer, "a", { value: { mark: "through both" }, configurable: true }); log(Object.getOwnPropertyDescriptor(outer, "a").value.mark);`, + "returned: outer defines a | inner defines a through both | inner describes a | outer describes a | inner describes a | inner describes a | through both", ], [ "what the engine holds against the target", @@ -187,6 +213,7 @@ const scenes: [name: string, does: string][] = [ const overClosed = new Proxy(closed, { getOwnPropertyDescriptor() {}, defineProperty() { return true; } }); try { Object.hasOwn(overClosed, "a"); } catch (e) { log(e.constructor.name); } try { Object.defineProperty(overClosed, "b", { value: 1 }); } catch (e) { log(e.constructor.name); }`, + "returned: TypeError | TypeError | TypeError | undefined | TypeError | TypeError", ], [ "a key that is converted", @@ -196,6 +223,7 @@ const scenes: [name: string, does: string][] = [ Object.defineProperty(proxy, key, {}); Object.hasOwn(proxy, 1); Reflect.set({}, Symbol.for("s"), 1, proxy);`, + "returned: the key is converted | describe string made | the key is converted | define string made | describe string 1 | describe symbol Symbol(s) | define symbol Symbol(s)", ], [ "JSON.stringify lists the proxy and reads what it lists", @@ -205,39 +233,46 @@ const scenes: [name: string, does: string][] = [ getOwnPropertyDescriptor(t, k) { log("describe", k); return k === "made" ? { value: 1, enumerable: true, configurable: true } : Reflect.getOwnPropertyDescriptor(t, k); }, }); log(JSON.stringify(proxy), JSON.stringify({ inside: proxy }, null, 1).length);`, + 'returned: get toJSON | ownKeys | describe a | describe b | describe made | get a | get b | get made | get toJSON | ownKeys | describe a | describe b | describe made | get a | get b | get made | {"a":{"mark":"of the target"},"b":2,"made":{"mark":"made by get"}} 111', ], [ "JSON.stringify calls the toJSON that get answers, on the proxy", `const proxy = new Proxy({ a: 1 }, { get(t, k) { log("get", k); return k === "toJSON" ? function (key) { log("toJSON", key, this === proxy); return { mark: "from toJSON" }; } : t[k]; } }); log(JSON.stringify(proxy), JSON.stringify([proxy]));`, + 'returned: get toJSON | toJSON true | get toJSON | toJSON 0 true | {"mark":"from toJSON"} [{"mark":"from toJSON"}]', ], [ "JSON.stringify goes on to what the traps answer", `const inner = { toJSON() { log("the inner toJSON runs"); return "inner"; } }; const proxy = new Proxy({}, { get(t, k) { return k === "x" ? inner : undefined; }, ownKeys() { return ["x"]; }, getOwnPropertyDescriptor() { return { value: 0, enumerable: true, configurable: true }; } }); log(JSON.stringify(proxy));`, + 'returned: the inner toJSON runs | {"x":"inner"}', ], [ "JSON.stringify of a proxy of a list", `const proxy = new Proxy([{ mark: "first" }, 2], { get(t, k) { log("get", k); return t[k]; } }); log(JSON.stringify(proxy));`, + 'returned: get toJSON | get length | get 0 | get 1 | [{"mark":"first"},2]', ], [ "JSON.stringify without a trap lists the target", `const target = { seen: { mark: "only by JSON" }, get computed() { log("the getter runs for", this === proxy); return 1; } }; const proxy = new Proxy(target, {}); log(JSON.stringify(proxy));`, + 'returned: the getter runs for true | {"seen":{"mark":"only by JSON"},"computed":1}', ], [ "JSON.stringify with a function or a list beside the proxy", `const proxy = new Proxy({ a: 1, b: 2 }, { get(t, k) { log("get", k); return t[k]; }, ownKeys(t) { log("ownKeys"); return Reflect.ownKeys(t); } }); log(JSON.stringify(proxy, (key, value) => { log("replacer", key, value === proxy); return value; })); log(JSON.stringify(proxy, ["b"]));`, + 'returned: get toJSON | replacer true | ownKeys | get a | replacer a false | get b | replacer b false | {"a":1,"b":2} | get toJSON | get b | {"b":2}', ], [ "a trap that throws while JSON.stringify lists", `const proxy = new Proxy({ a: 1 }, { ownKeys() { throw { mark: "thrown by ownKeys" }; } }); try { JSON.stringify(proxy); } catch (e) { log(e.mark); }`, + "returned: thrown by ownKeys", ], [ "console.log prints the target and asks no trap", @@ -257,6 +292,61 @@ const scenes: [name: string, does: string][] = [ console.warn(proxy); console.debug(proxy); log(seen);`, + "returned: []", + `{ + a: { + mark: "of the target", + }, + onlyPrinted: 2, +} +a text { + a: { + mark: "of the target", + }, + onlyPrinted: 2, +} 1 +{ + inside: { + a: { + mark: "of the target", + }, + onlyPrinted: 2, + }, +} [ + { + a: { + mark: "of the target", + }, + onlyPrinted: 2, + } +] +{ + a: { + mark: "of the target", + }, + onlyPrinted: 2, +} +{ + a: { + mark: "of the target", + }, + onlyPrinted: 2, +} +[] +`, + `{ + a: { + mark: "of the target", + }, + onlyPrinted: 2, +} +{ + a: { + mark: "of the target", + }, + onlyPrinted: 2, +} +`, ], [ "console.log calls what the target has for the formatter", @@ -265,6 +355,20 @@ const scenes: [name: string, does: string][] = [ const proxy = new Proxy(target, { get(t, k) { log("get", k); return t[k]; } }); console.log(proxy); console.log(new Proxy(proxy, {}));`, + "returned: the target's own printer runs for the target true | the target's own printer runs for the target true", + `the target's own printer runs for the target true +{ + printed: { + mark: "by the target", + }, +} +the target's own printer runs for the target true +{ + printed: { + mark: "by the target", + }, +} +`, ], [ "a text with a % converts the proxy behind it", @@ -276,11 +380,32 @@ const scenes: [name: string, does: string][] = [ console.log("%s", proxy); console.log("%j", proxy); console.log("%o and %d", proxy, proxy);`, + "returned: get Symbol(Symbol.toPrimitive) | get toString | get toJSON | ownKeys | describe a | get a | get Symbol(Symbol.toPrimitive) | get valueOf | get toString", + `get Symbol(Symbol.toPrimitive) +get toString +converted +get toJSON +ownKeys +describe a +get a +{"a":1} +{ + a: 1, +} and get Symbol(Symbol.toPrimitive) +get valueOf +get toString +NaN +`, ], [ "a text of the command line in front of a proxy", `const proxy = new Proxy({ a: 1 }, { get(t, k) { log("get", k); return k === "toString" ? () => "converted" : k === Symbol.toPrimitive ? undefined : t[k]; } }); console.log(process.argv[2] ?? "%s", proxy);`, + "returned: get Symbol(Symbol.toPrimitive) | get toString", + `get Symbol(Symbol.toPrimitive) +get toString +converted +`, ], [ "console.dir and console.table of a proxy", @@ -288,6 +413,29 @@ const scenes: [name: string, does: string][] = [ console.dir(proxy); console.dir({ a: { b: { c: {} } } }, new Proxy({ depth: 0 }, { get(t, k) { log("the options are asked for", k); return t[k]; } })); console.table(proxy);`, + "returned: the options are asked for depth | the options are asked for colors | get Symbol(Symbol.iterator) | ownKeys | get a | get b", + `{ + a: 1, + b: { + c: 2, + }, +} +the options are asked for depth +the options are asked for colors +{ + a: [Object ...], +} +get Symbol(Symbol.iterator) +ownKeys +get a +get b +┌───┬───┬────────┐ +│ │ c │ Values │ +├───┼───┼────────┤ +│ a │ │ 1 │ +│ b │ 2 │ │ +└───┴───┴────────┘ +`, ], [ "the methods of Object.prototype that only look at their receiver", @@ -297,6 +445,7 @@ const scenes: [name: string, does: string][] = [ log(Object.prototype.isPrototypeOf.call(proxy, {}), Object.prototype.isPrototypeOf.call(proxy, Object.create(proxy))); log(Object.prototype.toLocaleString.call(proxy)); log(proxy.valueOf() === proxy, proxy.toString(), typeof Object.call(proxy, "x"));`, + "returned: get a | true 1 | get Symbol(Symbol.toStringTag) | [object Tagged] | false true | get toString | get Symbol(Symbol.toStringTag) | [object Tagged] | get valueOf | get toString | get Symbol(Symbol.toStringTag) | true [object Tagged] object", ], [ "__defineGetter__ and __defineSetter__ reach the trap that defines", @@ -305,6 +454,7 @@ const scenes: [name: string, does: string][] = [ Object.prototype.__defineSetter__.call(proxy, "b", value => { log("the setter got", value); }); log(proxy.a); Object.prototype.__defineGetter__.call(proxy, "c");`, + "TypeError: define a [get,enumerable,configurable] the getter ran undefined | the setter got given | define b [set,enumerable,configurable] undefined undefined | the getter ran", ], [ "__lookupGetter__ and __lookupSetter__ ask the trap that describes and go on above", @@ -315,6 +465,7 @@ const scenes: [name: string, does: string][] = [ log(typeof Object.prototype.__lookupSetter__.call(proxy, "inherited"), Object.prototype.__lookupSetter__.call(proxy, "none")); const elsewhere = new Proxy({}, { getPrototypeOf() { log("getPrototypeOf"); return above; } }); log(Object.prototype.__lookupGetter__.call(elsewhere, "inherited")());`, + "returned: describe made | describe own | describe inherited | made by the trap of the target from above | describe inherited | describe none | function undefined | getPrototypeOf | from above", ], [ "the methods of Function.prototype refuse a proxy of what is no function", @@ -323,12 +474,14 @@ const scenes: [name: string, does: string][] = [ try { method.call(proxy); } catch (e) { log(e.constructor.name); } } log(Function.prototype[Symbol.hasInstance].call(proxy, {}));`, + "returned: TypeError | TypeError | TypeError | TypeError | false", ], [ "a proxy of a function is bound, called and printed", `const proxy = new Proxy(function named(a, b) { return "called with " + a; }, { get(t, k) { log("get", k); return t[k]; }, getOwnPropertyDescriptor(t, k) { log("describe", k); return Reflect.getOwnPropertyDescriptor(t, k); }, apply(t, s, l) { log("apply", l); return Reflect.apply(t, s, l); } }); const bound = Function.prototype.bind.call(proxy, null, "the bound operand"); log(bound(), bound.name, bound.length, Function.prototype.call.call(proxy, null, "x"), typeof Function.prototype.toString.call(proxy));`, + "returned: describe length | get length | get name | apply [the bound operand] | apply [x] | called with the bound operand bound named 1 called with x string", ], [ "a get that answers under the name in upper case", @@ -340,14 +493,44 @@ const scenes: [name: string, does: string][] = [ getOwnPropertyDescriptor(t, k) { log("describe", k); return Reflect.getOwnPropertyDescriptor(t, k); }, }); log(proxy.a, proxy.hasOwnProperty("A"), proxy.hasOwnProperty("a"), JSON.stringify(proxy), proxy.toString(), proxy.valueOf() === proxy);`, + 'returned: describe A | describe a | b true false {"A":"b","by":"toJSON"} [object Object] true', ], [ "a trap's answer that is no object", `const proxy = new Proxy({}, { getOwnPropertyDescriptor() { return 1; } }); try { Object.getOwnPropertyDescriptor(proxy, "a"); } catch (e) { log(e.constructor.name); }`, + "returned: TypeError", ], ]; +/** How a scene ended, and the lines that it logged. */ +function saidBy(says: string) { + const behindEnd = says.indexOf(": "); + if (behindEnd === -1) throw new Error(`${JSON.stringify(says)} does not say how the scene ended`); + const logged = says.slice(behindEnd + 2); + return { ended: says.slice(0, behindEnd), logged: logged ? logged.split(" | ") : [] }; +} + +/** What the program of one scene prints when nothing but `log` prints: its lines, and behind them the name of the error that ended it. */ +function printedAlone(says: string) { + const { ended, logged } = saidBy(says); + return [...logged, ...(ended === "returned" ? [] : [ended])].map(line => line + "\n").join(""); +} + +/** + * What console itself prints for a scene: `stdout` without the lines of `log`, which the program of all scenes keeps for its end. + * A line of `log` may begin in the middle of a line of console, so it is looked for anywhere behind the last one. + */ +function printedByConsole(stdout: string, says: string) { + let at = 0; + for (const line of saidBy(says).logged) { + at = stdout.indexOf(line + "\n", at); + if (at === -1) throw new Error(`${JSON.stringify(line)} is no line of what the scene prints`); + stdout = stdout.slice(0, at) + stdout.slice(at + line.length + 1); + } + return stdout; +} + const program = ` const lines = []; function show(value) { @@ -370,24 +553,44 @@ await 0; for (const line of lines) console.log(line); `; -test("the two traps get and answer what they do in plain Bun", async () => { - using dir = tempDir("aot-own-property-traps", { "main.mjs": program }); - const folder = String(dir); - const executable = join(folder, isWindows ? "main.exe" : "main"); - await slot(async () => { - built( - await spawned([bunExe(), "build", "--compile", "--aot", "./main.mjs", "--outfile", executable], folder, bunEnv), - ); - const plain = told(await spawned([bunExe(), "./main.mjs"], folder, bunEnv)); - expect(plain.stdout.split("\n").length).toBeGreaterThan(scenes.length); - expect(told(await spawned([executable], folder, bunEnv))).toEqual(plain); - }); -}, 240_000); +// The programs of one scene ask what compiled code does, and nothing of the file that holds it. So on Linux each of their builds writes +// its program alone (harness/program-alone.ts), as the builds of the stored programs do (run-parallel.ts): the compile is the same, and +// no copy of Bun is written for each of them. AOT_REAL_EXECUTABLES=1 around a run asks for the executables, as it does there. The +// program of all scenes is built as before: an executable, unless AOT_PROGRAMS_ALONE asks for every program alone. +const atTheBuildOfOneScene = + isLinux && !process.env.AOT_REAL_EXECUTABLES ? { ...bunEnv, BUN_AOT_WRITES_PROGRAM_ALONE: "1" } : bunEnv; + +test.concurrent( + "the two traps get and answer what they do in plain Bun", + async () => { + using dir = tempDir("aot-own-property-traps", { "main.mjs": program }); + const folder = String(dir); + const executable = join(folder, isWindows ? "main.exe" : "main"); + // What console prints for a scene comes as the scene runs, and the line of each scene at the end. + const said = { + stdout: + scenes.map(([, , says, stdout]) => (stdout === undefined ? "" : printedByConsole(stdout, says))).join("") + + scenes.map(([name, , says]) => `${name}: ${says}\n`).join(""), + stderr: scenes.map(([, , , , stderr = ""]) => stderr).join(""), + exitCode: 0, + }; + await slot(async () => { + built( + await spawned([bunExe(), "build", "--compile", "--aot", "./main.mjs", "--outfile", executable], folder, bunEnv), + ); + const plain = told(await spawned([bunExe(), "./main.mjs"], folder, bunEnv)); + const compiled = told(await spawned([executable], folder, bunEnv)); + expect({ plain, compiled }).toEqual({ plain: said, compiled: said }); + }); + }, + 240_000, +); // One scene to a program, so that nothing another scene does to its proxy hides what this one's compiled code does. test.concurrent.each(scenes)( "alone: %s", - async (name, does) => { + // bun:test hands `done` to a case that declares more parameters than its row has, so the two that not every row has get a default. + async (name, does, says, stdout = printedAlone(says), stderr = "") => { const alone = ` function show(value) { if (typeof value === "symbol") return String(value); @@ -404,13 +607,18 @@ ${does} using dir = tempDir("aot-own-property-trap", { "main.mjs": alone }); const folder = String(dir); const executable = join(folder, isWindows ? "main.exe" : "main"); + const said = { stdout, stderr, exitCode: 0 }; await slot(async () => { built( - await spawned([bunExe(), "build", "--compile", "--aot", "./main.mjs", "--outfile", executable], folder, bunEnv), - ); - expect(told(await spawned([executable], folder, bunEnv))).toEqual( - told(await spawned([bunExe(), "./main.mjs"], folder, bunEnv)), + await spawned( + [bunExe(), "build", "--compile", "--aot", "./main.mjs", "--outfile", executable], + folder, + atTheBuildOfOneScene, + ), ); + const plain = told(await spawned([bunExe(), "./main.mjs"], folder, bunEnv)); + const compiled = told(await spawned([executable], folder, bunEnv)); + expect({ plain, compiled }).toEqual({ plain: said, compiled: said }); }); }, 240_000, @@ -449,11 +657,12 @@ const answersInUpperCase = `{ getOwnPropertyDescriptor(target, key) { return Reflect.getOwnPropertyDescriptor(target, key); }, defineProperty(target, key, descriptor) { target[key] = String(descriptor.value); return true; }, }`; +type Report = { lines: string[]; exitCode: number }; // One run of `bun check --aot --explain=trim` for the cases that read its lines. -function explainTrim(main: string): () => Promise { - let lines: Promise | undefined; +function explainTrim(main: string): () => Promise { + let report: Promise | undefined; return () => - (lines ??= (async () => { + (report ??= (async () => { using dir = tempDir("aot-own-property-report", { "main.js": main }); await using proc = Bun.spawn({ cmd: [bunExe(), "check", "--aot", "--no-pretty", "--explain=trim", "main.js"], @@ -462,27 +671,45 @@ function explainTrim(main: string): () => Promise { stdout: "pipe", stderr: "pipe", }); - const [stdout, stderr] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - return (stdout + stderr).split("\n"); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + return { lines: (stdout + stderr).split("\n"), exitCode }; })()); } +/** The lines of a report that say how much the compiler sees, and how the check ended. A check that failed shows what else it wrote. */ +const verdictOf = ({ lines, exitCode }: Report) => ({ + verdict: lines.filter(line => /^aot: trim: (?:the compiler |blocked by )/.test(line)), + exitCode, + ...(exitCode !== 0 && { wrote: lines.filter(line => !line.startsWith("aot: trim: ")) }), +}); +const seesEveryApi = { verdict: ["aot: trim: the compiler can see every API this program uses"], exitCode: 0 }; +/** + * The lines of a report that came whole. Any other report throws here, which a `failing` goal takes for not met yet: a goal never + * looks met on a report without its lines, and the case beside it is the one that fails. + */ +async function linesOfWhole(report: () => Promise) { + const checked = await report(); + expect(verdictOf(checked)).toEqual(seesEveryApi); + return checked.lines; +} for (const [what, text] of Object.entries({ "a method that the proxy inherits": `console.log(proxy.hasOwnProperty("A"));`, "JSON.stringify": `console.log(JSON.stringify(proxy));`, })) { - const lines = explainTrim( + const report = explainTrim( `const proxy = new Proxy({ A: "a", toJSON: () => ({}) }, ${answersInUpperCase});\n${text}\n`, ); test.concurrent(`${what}, found by a get that answers in upper case: the compiler sees every API`, async () => { - expect(await lines()).toContain("aot: trim: the compiler can see every API this program uses"); + expect(verdictOf(await report())).toEqual(seesEveryApi); }); // A goal that is not met yet. Once it is met this case fails: change `test.concurrent.failing` to `test.concurrent`. test.concurrent.failing( `${what}, found by a get that answers in upper case, gives no trap away (3,698 built-ins escape today, the bound is 20)`, async () => { // What every object inherits, at the most. - const escaped = (await lines()).filter(line => line.endsWith("(escapes, so any member may be used)")); + const escaped = (await linesOfWhole(report)).filter(line => + line.endsWith("(escapes, so any member may be used)"), + ); sayGoal(`${what}, found by a get that answers in upper case, gives no trap away`, escaped.length, 20); expect(escaped.length).toBeLessThanOrEqual(20); }, @@ -490,15 +717,15 @@ for (const [what, text] of Object.entries({ } for (const [what, text] of Object.entries(closed)) { - const lines = explainTrim(`const proxy = new Proxy({ A: "a" }, ${handler});\n${text}\n`); + const report = explainTrim(`const proxy = new Proxy({ A: "a" }, ${handler});\n${text}\n`); test.concurrent(`${what}: the compiler sees every API`, async () => { - expect(await lines()).toContain("aot: trim: the compiler can see every API this program uses"); + expect(verdictOf(await report())).toEqual(seesEveryApi); }); // A goal that is not met yet. Once it is met this case fails: change `test.concurrent.failing` to `test.concurrent`. test.concurrent.failing( `${what} uses no built-in that the program does not name (3,698 escape today, the bound is none)`, async () => { - const escaped = (await lines()).flatMap( + const escaped = (await linesOfWhole(report)).flatMap( line => /trim: uses (.*) \(escapes, so any member may be used\)$/.exec(line)?.[1] ?? [], ); sayGoal(`${what} uses no built-in that the program does not name`, escaped.length, 0); From 70a69c45f13a412e4831e9951729db3fae9026b0 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 9 Nov 2026 14:18:56 +0000 Subject: [PATCH 2/3] test/aot: build each proxy trap scene on one thread and hold console scenes to what console writes The 43 builds of one scene get --aot-threads=1 and the 15 checks get --threads=1, as run-parallel.ts gives a build a thread for each slot that it holds. The bytes are the same on any number of threads. A scene that prints through console stores what console writes and the lines of its traps. Where a line of a trap falls between what console writes is not stored: there the compiled program is held to plain Bun. AOT_REAL_EXECUTABLES=1 asks for an executable of every build of the file, also where AOT_PROGRAMS_ALONE=1 is set. --- ...cribed-and-defined-on-by-its-traps.test.ts | 102 +++++++++--------- 1 file changed, 52 insertions(+), 50 deletions(-) diff --git a/test/aot/a-proxy-is-described-and-defined-on-by-its-traps.test.ts b/test/aot/a-proxy-is-described-and-defined-on-by-its-traps.test.ts index 1dcffe619802..ab85d585e676 100644 --- a/test/aot/a-proxy-is-described-and-defined-on-by-its-traps.test.ts +++ b/test/aot/a-proxy-is-described-and-defined-on-by-its-traps.test.ts @@ -5,12 +5,13 @@ // console.log prints the target of a proxy and asks nothing. Each scene has a proxy and a handler of its own. // // Behind its text a scene has what it says: how it ended, and the lines that it logged, as the program of all scenes prints them behind -// the name of the scene. A scene that prints through console itself has what its own program writes to stdout behind that, and to stderr. +// the name of the scene. A scene that prints through console itself has behind that what console writes for it, to stdout and to stderr. // Plain Bun has to say that, and so has the compiled program. import { expect } from "bun:test"; import { bunEnv, bunExe, isLinux, isWindows, tempDir } from "harness"; import { join } from "node:path"; import { built, slot, spawned, told } from "./harness/commands"; +import { withThreads } from "./harness/flags"; import { sayGoal } from "./harness/goals"; import { test } from "./supported"; @@ -332,7 +333,6 @@ a text { }, onlyPrinted: 2, } -[] `, `{ a: { @@ -356,13 +356,11 @@ a text { console.log(proxy); console.log(new Proxy(proxy, {}));`, "returned: the target's own printer runs for the target true | the target's own printer runs for the target true", - `the target's own printer runs for the target true -{ + `{ printed: { mark: "by the target", }, } -the target's own printer runs for the target true { printed: { mark: "by the target", @@ -381,20 +379,11 @@ the target's own printer runs for the target true console.log("%j", proxy); console.log("%o and %d", proxy, proxy);`, "returned: get Symbol(Symbol.toPrimitive) | get toString | get toJSON | ownKeys | describe a | get a | get Symbol(Symbol.toPrimitive) | get valueOf | get toString", - `get Symbol(Symbol.toPrimitive) -get toString -converted -get toJSON -ownKeys -describe a -get a + `converted {"a":1} { a: 1, -} and get Symbol(Symbol.toPrimitive) -get valueOf -get toString -NaN +} and NaN `, ], [ @@ -402,9 +391,7 @@ NaN `const proxy = new Proxy({ a: 1 }, { get(t, k) { log("get", k); return k === "toString" ? () => "converted" : k === Symbol.toPrimitive ? undefined : t[k]; } }); console.log(process.argv[2] ?? "%s", proxy);`, "returned: get Symbol(Symbol.toPrimitive) | get toString", - `get Symbol(Symbol.toPrimitive) -get toString -converted + `converted `, ], [ @@ -420,15 +407,9 @@ converted c: 2, }, } -the options are asked for depth -the options are asked for colors { a: [Object ...], } -get Symbol(Symbol.iterator) -ownKeys -get a -get b ┌───┬───┬────────┐ │ │ c │ Values │ ├───┼───┼────────┤ @@ -511,21 +492,16 @@ function saidBy(says: string) { return { ended: says.slice(0, behindEnd), logged: logged ? logged.split(" | ") : [] }; } -/** What the program of one scene prints when nothing but `log` prints: its lines, and behind them the name of the error that ended it. */ -function printedAlone(says: string) { - const { ended, logged } = saidBy(says); - return [...logged, ...(ended === "returned" ? [] : [ended])].map(line => line + "\n").join(""); -} - /** - * What console itself prints for a scene: `stdout` without the lines of `log`, which the program of all scenes keeps for its end. - * A line of `log` may begin in the middle of a line of console, so it is looked for anywhere behind the last one. + * What a program printed, without the lines that its scene logged: what console itself wrote. Where the line of a trap falls between what + * console writes for the call that asked it is console's own matter, so a line is looked for anywhere behind the last one, also in the + * middle of a line of console. A line that is not there is named in front of the text. */ -function printedByConsole(stdout: string, says: string) { +function withoutLogged(stdout: string, logged: string[]) { let at = 0; - for (const line of saidBy(says).logged) { + for (const line of logged) { at = stdout.indexOf(line + "\n", at); - if (at === -1) throw new Error(`${JSON.stringify(line)} is no line of what the scene prints`); + if (at === -1) return `${JSON.stringify(line)} is not logged behind the lines before it:\n${stdout}`; stdout = stdout.slice(0, at) + stdout.slice(at + line.length + 1); } return stdout; @@ -553,12 +529,21 @@ await 0; for (const line of lines) console.log(line); `; -// The programs of one scene ask what compiled code does, and nothing of the file that holds it. So on Linux each of their builds writes -// its program alone (harness/program-alone.ts), as the builds of the stored programs do (run-parallel.ts): the compile is the same, and -// no copy of Bun is written for each of them. AOT_REAL_EXECUTABLES=1 around a run asks for the executables, as it does there. The -// program of all scenes is built as before: an executable, unless AOT_PROGRAMS_ALONE asks for every program alone. +// The program of all scenes is built as anybody builds it. The programs of one scene ask what compiled code does, and nothing of the +// file that holds it or of the threads that made it. So each is built as run-parallel.ts builds a stored program: +// - on one thread, for the slot that its build holds. CI has as many cases under way as the machine has processors, so more threads for +// one build are taken from the build beside it. The bytes are the same on any number of threads +// (builds-on-one-thread-and-on-several-give-the-same-bytes.test.ts). +// - on Linux, as the program alone (harness/program-alone.ts): the compile is the same, and no copy of Bun is written for each scene. +// AOT_REAL_EXECUTABLES=1 around a run asks for an executable of every build of this file. Without it the program of all scenes is an +// executable too, unless AOT_PROGRAMS_ALONE asks for every program alone. +const atTheBuildOfAllScenes = process.env.AOT_REAL_EXECUTABLES + ? { ...bunEnv, BUN_AOT_WRITES_PROGRAM_ALONE: undefined } + : bunEnv; const atTheBuildOfOneScene = - isLinux && !process.env.AOT_REAL_EXECUTABLES ? { ...bunEnv, BUN_AOT_WRITES_PROGRAM_ALONE: "1" } : bunEnv; + isLinux && !process.env.AOT_REAL_EXECUTABLES + ? { ...bunEnv, BUN_AOT_WRITES_PROGRAM_ALONE: "1" } + : atTheBuildOfAllScenes; test.concurrent( "the two traps get and answer what they do in plain Bun", @@ -566,17 +551,21 @@ test.concurrent( using dir = tempDir("aot-own-property-traps", { "main.mjs": program }); const folder = String(dir); const executable = join(folder, isWindows ? "main.exe" : "main"); - // What console prints for a scene comes as the scene runs, and the line of each scene at the end. + // What console writes for a scene comes as the scene runs, and the line of each scene at the end. const said = { stdout: - scenes.map(([, , says, stdout]) => (stdout === undefined ? "" : printedByConsole(stdout, says))).join("") + + scenes.map(([, , , stdout = ""]) => stdout).join("") + scenes.map(([name, , says]) => `${name}: ${says}\n`).join(""), stderr: scenes.map(([, , , , stderr = ""]) => stderr).join(""), exitCode: 0, }; await slot(async () => { built( - await spawned([bunExe(), "build", "--compile", "--aot", "./main.mjs", "--outfile", executable], folder, bunEnv), + await spawned( + [bunExe(), "build", "--compile", "--aot", "./main.mjs", "--outfile", executable], + folder, + atTheBuildOfAllScenes, + ), ); const plain = told(await spawned([bunExe(), "./main.mjs"], folder, bunEnv)); const compiled = told(await spawned([executable], folder, bunEnv)); @@ -590,7 +579,7 @@ test.concurrent( test.concurrent.each(scenes)( "alone: %s", // bun:test hands `done` to a case that declares more parameters than its row has, so the two that not every row has get a default. - async (name, does, says, stdout = printedAlone(says), stderr = "") => { + async (name, does, says, stdout: string | undefined = undefined, stderr = "") => { const alone = ` function show(value) { if (typeof value === "symbol") return String(value); @@ -607,18 +596,30 @@ ${does} using dir = tempDir("aot-own-property-trap", { "main.mjs": alone }); const folder = String(dir); const executable = join(folder, isWindows ? "main.exe" : "main"); - const said = { stdout, stderr, exitCode: 0 }; + const { ended, logged } = saidBy(says); + // Here `log` prints too: the lines of the scene one by one, and behind them the name of the error that ended it. A scene that prints + // through console itself is held to its lines and to what console wrote, and not to where its lines fall between that. + const printsItself = stdout !== undefined; + const said = { + stdout: (stdout ?? logged.map(line => line + "\n").join("")) + (ended === "returned" ? "" : ended + "\n"), + stderr, + exitCode: 0, + }; + const heard = (run: ReturnType) => + printsItself ? { ...run, stdout: withoutLogged(run.stdout, logged) } : run; await slot(async () => { built( await spawned( - [bunExe(), "build", "--compile", "--aot", "./main.mjs", "--outfile", executable], + [bunExe(), "build", "--compile", "--aot", withThreads(1), "./main.mjs", "--outfile", executable], folder, atTheBuildOfOneScene, ), ); const plain = told(await spawned([bunExe(), "./main.mjs"], folder, bunEnv)); const compiled = told(await spawned([executable], folder, bunEnv)); - expect({ plain, compiled }).toEqual({ plain: said, compiled: said }); + expect({ plain: heard(plain), compiled: heard(compiled) }).toEqual({ plain: said, compiled: said }); + // Where its lines fall, the compiled program is held to plain Bun. + expect(compiled).toEqual(plain); }); }, 240_000, @@ -658,14 +659,15 @@ const answersInUpperCase = `{ defineProperty(target, key, descriptor) { target[key] = String(descriptor.value); return true; }, }`; type Report = { lines: string[]; exitCode: number }; -// One run of `bun check --aot --explain=trim` for the cases that read its lines. +// One run of `bun check --aot --explain=trim` for the cases that read its lines. On one thread, as the build of one scene: the report +// is the same on any number. function explainTrim(main: string): () => Promise { let report: Promise | undefined; return () => (report ??= (async () => { using dir = tempDir("aot-own-property-report", { "main.js": main }); await using proc = Bun.spawn({ - cmd: [bunExe(), "check", "--aot", "--no-pretty", "--explain=trim", "main.js"], + cmd: [bunExe(), "check", "--aot", "--threads=1", "--no-pretty", "--explain=trim", "main.js"], cwd: String(dir), env: { ...bunEnv, NO_COLOR: "1" }, stdout: "pipe", From 6f4ac92721957eec2246fc6da8dcb0a5e32db179 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 9 Nov 2026 14:53:07 +0000 Subject: [PATCH 3/3] test/aot: build the proxy trap scenes on the default number of threads again One thread for each build and each check made the file slower in CI: 79.9 s with ASAN where the commit before took 55.6 s, 17.2 s on x64 where it took 16.5 s, and 12.8 s on aarch64 where it took 11.4 s. The builds and the checks take the default number of threads, as on main. The program of one scene is still written alone on Linux. --- ...cribed-and-defined-on-by-its-traps.test.ts | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/test/aot/a-proxy-is-described-and-defined-on-by-its-traps.test.ts b/test/aot/a-proxy-is-described-and-defined-on-by-its-traps.test.ts index ab85d585e676..c00345d8a0ca 100644 --- a/test/aot/a-proxy-is-described-and-defined-on-by-its-traps.test.ts +++ b/test/aot/a-proxy-is-described-and-defined-on-by-its-traps.test.ts @@ -11,7 +11,6 @@ import { expect } from "bun:test"; import { bunEnv, bunExe, isLinux, isWindows, tempDir } from "harness"; import { join } from "node:path"; import { built, slot, spawned, told } from "./harness/commands"; -import { withThreads } from "./harness/flags"; import { sayGoal } from "./harness/goals"; import { test } from "./supported"; @@ -529,14 +528,10 @@ await 0; for (const line of lines) console.log(line); `; -// The program of all scenes is built as anybody builds it. The programs of one scene ask what compiled code does, and nothing of the -// file that holds it or of the threads that made it. So each is built as run-parallel.ts builds a stored program: -// - on one thread, for the slot that its build holds. CI has as many cases under way as the machine has processors, so more threads for -// one build are taken from the build beside it. The bytes are the same on any number of threads -// (builds-on-one-thread-and-on-several-give-the-same-bytes.test.ts). -// - on Linux, as the program alone (harness/program-alone.ts): the compile is the same, and no copy of Bun is written for each scene. -// AOT_REAL_EXECUTABLES=1 around a run asks for an executable of every build of this file. Without it the program of all scenes is an -// executable too, unless AOT_PROGRAMS_ALONE asks for every program alone. +// The programs of one scene ask what compiled code does, and nothing of the file that holds it. So on Linux each of their builds writes +// its program alone (harness/program-alone.ts), as the builds of the stored programs do (run-parallel.ts): the compile is the same, and +// no copy of Bun is written for each of them. The program of all scenes is built as before: an executable, unless AOT_PROGRAMS_ALONE +// asks for every program alone. AOT_REAL_EXECUTABLES=1 around a run asks for an executable of every build of this file. const atTheBuildOfAllScenes = process.env.AOT_REAL_EXECUTABLES ? { ...bunEnv, BUN_AOT_WRITES_PROGRAM_ALONE: undefined } : bunEnv; @@ -610,7 +605,7 @@ ${does} await slot(async () => { built( await spawned( - [bunExe(), "build", "--compile", "--aot", withThreads(1), "./main.mjs", "--outfile", executable], + [bunExe(), "build", "--compile", "--aot", "./main.mjs", "--outfile", executable], folder, atTheBuildOfOneScene, ), @@ -659,15 +654,14 @@ const answersInUpperCase = `{ defineProperty(target, key, descriptor) { target[key] = String(descriptor.value); return true; }, }`; type Report = { lines: string[]; exitCode: number }; -// One run of `bun check --aot --explain=trim` for the cases that read its lines. On one thread, as the build of one scene: the report -// is the same on any number. +// One run of `bun check --aot --explain=trim` for the cases that read its lines. function explainTrim(main: string): () => Promise { let report: Promise | undefined; return () => (report ??= (async () => { using dir = tempDir("aot-own-property-report", { "main.js": main }); await using proc = Bun.spawn({ - cmd: [bunExe(), "check", "--aot", "--threads=1", "--no-pretty", "--explain=trim", "main.js"], + cmd: [bunExe(), "check", "--aot", "--no-pretty", "--explain=trim", "main.js"], cwd: String(dir), env: { ...bunEnv, NO_COLOR: "1" }, stdout: "pipe",