From 350117624262cf12857b0d43896bfc06ade47570 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Thu, 22 Jan 2026 13:20:01 +0000 Subject: [PATCH] fix(macros): prevent incorrect const inlining of macro-returned objects/arrays Objects and arrays returned from macros were being treated as "const values" for constant propagation, causing each reference to be replaced with a fresh copy. This broke mutation semantics where obj.foo = x followed by reading obj.foo would read from a different object. Fixes #26362 Co-Authored-By: Claude Opus 4.5 --- src/ast/Expr.zig | 7 ++- test/regression/issue/26362.test.ts | 86 +++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 test/regression/issue/26362.test.ts diff --git a/src/ast/Expr.zig b/src/ast/Expr.zig index aa9f7eb5b5..fb8336d85e 100644 --- a/src/ast/Expr.zig +++ b/src/ast/Expr.zig @@ -2657,8 +2657,11 @@ pub const Data = union(Tag) { .e_inlined_enum, => true, .e_string => |str| str.next == null, - .e_array => |array| array.was_originally_macro, - .e_object => |object| object.was_originally_macro, + // Objects and arrays are NOT const values because they are mutable. + // Inlining them would create fresh copies at each reference site, + // breaking mutation semantics (e.g., obj.foo = 1 would modify a + // different object than a subsequent read of obj.foo). + // See: https://github.com/oven-sh/bun/issues/26362 else => false, }; } diff --git a/test/regression/issue/26362.test.ts b/test/regression/issue/26362.test.ts new file mode 100644 index 0000000000..05d27f55ca --- /dev/null +++ b/test/regression/issue/26362.test.ts @@ -0,0 +1,86 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; + +test("objects returned from macros should be mutable", async () => { + using dir = tempDir("issue-26362", { + "macro.ts": `export const getObj = () => ({});`, + "index.ts": ` +import { getObj } from "./macro.ts" with { type: "macro" }; + +const obj = getObj(); + +// Object should be extensible (not frozen/sealed) +if (!Object.isExtensible(obj)) { + throw new Error("Object is not extensible"); +} + +// Should be able to add properties +obj.foo = "bar"; + +// Should be able to read back the property we just set +if (obj.foo !== "bar") { + throw new Error("Property was not set correctly, got: " + obj.foo); +} + +console.log("success"); +`, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "index.ts"], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stderr).toBe(""); + expect(stdout).toInclude("success"); + expect(exitCode).toBe(0); +}); + +test("arrays returned from macros should be mutable", async () => { + using dir = tempDir("issue-26362-array", { + "macro.ts": `export const getArr = () => [];`, + "index.ts": ` +import { getArr } from "./macro.ts" with { type: "macro" }; + +const arr = getArr(); + +// Array should be extensible +if (!Object.isExtensible(arr)) { + throw new Error("Array is not extensible"); +} + +// Should be able to push elements +arr.push("first"); + +// Should be able to read back what we pushed +if (arr[0] !== "first") { + throw new Error("Element was not pushed correctly, got: " + arr[0]); +} + +if (arr.length !== 1) { + throw new Error("Array length is wrong, got: " + arr.length); +} + +console.log("success"); +`, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "index.ts"], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stderr).toBe(""); + expect(stdout).toInclude("success"); + expect(exitCode).toBe(0); +});