Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/compiler/src/frontend/lowering/lower-exprs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2978,6 +2978,13 @@ export function lowerOptionalChain(L: Lowerer, expr: ts.CallExpression | ts.Prop
export function lowerCondition(L: Lowerer, expr: ts.Expression): IrExpr {
let e: ts.Expression = expr;
while (ts.isParenthesizedExpression(e)) e = e.expression;
// Node always installs the supported global Buffer constructor. A
// captured capability probe (`const b = globalThis.Buffer; if (b)`) is
// compile-time true; receiver-position calls through the alias still
// resolve via stdlibGlobalNameOf and keep Buffer's per-member fences.
if (stdlibGlobalNameOf(L, e) === "Buffer") {
return { kind: "boolLit", value: true, type: BOOL, loc: locOf(expr) };
}
if (ts.isBinaryExpression(e)) {
const op = e.operatorToken.kind;
if (op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken) {
Expand Down
14 changes: 13 additions & 1 deletion packages/compiler/src/frontend/lowering/surfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1510,10 +1510,19 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record<string, Record<string, string |
}
if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) {
if (stdlibGlobalNameOf(L, expr.expression) !== "globalThis") return null;
// Capability probes commonly narrow globalThis before reading Buffer:
// `(globalThis as { Buffer?: RuntimeBuffer }).Buffer`. The cast's
// member symbol belongs to the user-declared probe shape rather than
// @types/node, but the receiver is still the unshadowable globalThis
// intrinsic and the runtime Buffer is the same supported global.
if (expr.name.text === "Buffer") return "Buffer";
const symbol = L.checker.getSymbolAtLocation(expr.name);
if (!symbol || !L.isStdlibSymbol(symbol)) return null;
return symbol.name === "global" ? "globalThis" : symbol.name;
}
if (ts.isAsExpression(expr) || ts.isTypeAssertion(expr)) {
return stdlibGlobalNameOf(L, expr.expression);
}
return null;
}

Expand Down Expand Up @@ -1564,7 +1573,10 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record<string, Record<string, string |
// performance — the mockable-clock idiom snapshots it). Function-valued
// globals (setTimeout) taken as values are a different story — the
// ordinary value paths (and their fences) apply.
if (name !== "process" && name !== "console" && name !== "globalThis" && name !== "performance") return false;
if (
name !== "process" && name !== "console" && name !== "globalThis" &&
name !== "performance" && name !== "Buffer"
) return false;
const symbol = L.checker.getSymbolAtLocation(nameNode);
if (!symbol) return false;
L.stdlibGlobalAliases.set(symbol, name);
Expand Down
122 changes: 122 additions & 0 deletions tests/harness/global-buffer-alias.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdirSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
import { describe, expect, test } from "vitest";
import { compile } from "@scriptc/compiler";

const execFileAsync = promisify(execFile);
const sanitize = process.env["SCRIPTC_SAN"] === "1";

interface RunResult {
stdout: Buffer;
stderr: Buffer;
exitCode: number;
}

async function run(cmd: string, args: string[]): Promise<RunResult> {
try {
const { stdout, stderr } = await execFileAsync(cmd, args, { encoding: "buffer" });
return { stdout, stderr, exitCode: 0 };
} catch (err) {
if (
typeof err !== "object" || err === null ||
!("code" in err) || typeof err.code !== "number" ||
!("stdout" in err) || !Buffer.isBuffer(err.stdout) ||
!("stderr" in err) || !Buffer.isBuffer(err.stderr)
) {
throw err;
}
return { stdout: err.stdout, stderr: err.stderr, exitCode: err.code };
}
}

async function compileAndCompare(source: string, backend: "c" | "llvm"): Promise<void> {
const key = createHash("sha256")
.update(source)
.update(`${backend}-${sanitize ? "san" : "plain"}`)
.digest("hex")
.slice(0, 16);
const outDir = join(tmpdir(), "scriptc-tests", `global-buffer-alias-${key}`);
mkdirSync(outDir, { recursive: true });
const file = join(outDir, "main.mts");
writeFileSync(file, source);
const result = await compile(file, {
outPath: join(outDir, "program"),
outDir,
sanitize,
backend,
});
if (!result.ok) {
throw new Error(
"guarded global Buffer alias program failed to compile:\n" +
result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"),
);
}
const [nodeResult, nativeResult] = await Promise.all([
run("node", ["--experimental-transform-types", "--disable-warning=ExperimentalWarning", file]),
run(result.binaryPath, []),
]);
expect(nativeResult.stdout).toEqual(nodeResult.stdout);
expect(nativeResult.stderr).toEqual(nodeResult.stderr);
expect(nativeResult.exitCode).toBe(nodeResult.exitCode);
}

async function compileAndExpectFence(source: string, backend: "c" | "llvm"): Promise<void> {
const key = createHash("sha256").update(source).update(backend).digest("hex").slice(0, 16);
const outDir = join(tmpdir(), "scriptc-tests", `global-buffer-alias-fence-${key}`);
mkdirSync(outDir, { recursive: true });
const file = join(outDir, "main.mts");
writeFileSync(file, source);
const result = await compile(file, {
outPath: join(outDir, "program"),
outDir,
sanitize,
backend,
});
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")).toContain(
"SC1090: the reference to 'runtimeBuffer' (a binding form with no lowering) is not supported yet",
);
}

describe.each(["c", "llvm"] as const)(
`guarded global Buffer alias, %s backend${sanitize ? " (sanitized)" : ""}`,
(backend) => {
test("counts UTF-8 bytes through the guarded constructor alias", async () => {
await compileAndCompare(`
interface RuntimeBuffer {
byteLength(value: string, encoding?: "utf8"): number;
}

const runtimeBuffer = (globalThis as { Buffer?: RuntimeBuffer }).Buffer;

function byteLength(content: string): number {
return runtimeBuffer
? runtimeBuffer.byteLength(content, "utf8")
: content.length;
}

console.log(byteLength("ascii"));
console.log(byteLength("é"));
console.log(byteLength("😀"));
console.log(runtimeBuffer ? runtimeBuffer.byteLength("Aé😀") : -1);
`, backend);
});

test("preserves unsupported member fences through the alias", async () => {
await compileAndExpectFence(`
interface RuntimeBuffer {
byteLength(value: string): number;
poolSize?: number;
}

const runtimeBuffer = (globalThis as { Buffer?: RuntimeBuffer }).Buffer;
console.log(runtimeBuffer ? runtimeBuffer.poolSize : -1);
`, backend);
});
},
);