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 <noreply@anthropic.com>
This commit is contained in:
Claude Bot
2026-01-22 13:20:01 +00:00
parent 7f70b01259
commit 3501176242
2 changed files with 91 additions and 2 deletions

View File

@@ -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,
};
}

View File

@@ -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);
});