Skip to content
Merged
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
5 changes: 2 additions & 3 deletions packages/compiler/src/backend/llvm/byte-numbers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { IrExpr } from "../../ir/ir.js";
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
import { f64Lit } from "./common.js";
import { exactInteger, widenInteger } from "./integer-values.js";
import { emitTruncF64 } from "./trunc.js";

/** Supported LLVM targets are little-endian. All memory accesses use align
* 1: Buffer fields and DataView windows can start at arbitrary byte offsets. */
Expand Down Expand Up @@ -101,11 +102,9 @@ export function emitByteNumber(
if (spec.dataView) {
const nan = B.tmp(),
finiteOrZero = B.tmp();
normalized = B.tmp();
B.line(`${nan} = fcmp uno double ${offset.name}, ${offset.name}`);
B.line(`${finiteOrZero} = select i1 ${nan}, double ${f64Lit(0)}, double ${offset.name}`);
host.declare("declare double @llvm.trunc.f64(double)");
B.line(`${normalized} = call double @llvm.trunc.f64(double ${finiteOrZero})`);
normalized = emitTruncF64(host, finiteOrZero);
}
const cap = B.tmp(),
positive = B.tmp(),
Expand Down
12 changes: 12 additions & 0 deletions packages/compiler/src/backend/llvm/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,10 @@ export class LlEmitter {
readonly ffiExtendNarrowIntegers: boolean;
readonly cycleColorOffset: number;
readonly wasi: boolean;
/** The target lacks a rounding instruction at its baseline CPU (x86-64
* before SSE4.1), so `llvm.trunc` would lower to a libm call: emit
* truncation inline through an integer round trip instead. */
readonly inlineTrunc: boolean;
/** ELF worker executables give thread-locals the executable TLS models. */
private readonly executableTls: boolean;
private readonly emitLibraryIdentity: boolean;
Expand Down Expand Up @@ -807,6 +811,14 @@ export class LlEmitter {
this.ffiExtendNarrowIntegers =
options.wasi === true || ffiExtendsNarrowIntegers(options.targetTriple);
this.wasi = options.wasi === true;
// An empty triple targets the host, as for executableTls below: the
// Node-hosted CLI passes none, the native CLI passes its toolchain's, and
// both must emit the same module for the same target.
this.inlineTrunc =
!this.wasi &&
(options.targetTriple
? /^(x86_64|amd64)\b/i.test(options.targetTriple)
: process.arch === "x64");
this.executableTls =
mod.workers === true &&
mod.lib === undefined &&
Expand Down
7 changes: 3 additions & 4 deletions packages/compiler/src/backend/llvm/expr-bytes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,15 +371,14 @@ export function emitToUint32(
B.condBr(finite, finiteLabel, nonfiniteLabel);

B.startBlock(finiteLabel);
host.declare(`declare double @llvm.trunc.f64(double)`);
const truncated = B.tmp();
// Only magnitudes above 2^53 reach this block, and every such double is
// already an integer: no truncation is needed before the reduction.
const residue = B.tmp();
const negative = B.tmp();
const wrapped = B.tmp();
const normalized = B.tmp();
const finiteU32 = B.tmp();
B.line(`${truncated} = call double @llvm.trunc.f64(double ${value})`);
B.line(`${residue} = frem double ${truncated}, ${f64Lit(4294967296)}`);
B.line(`${residue} = frem double ${value}, ${f64Lit(4294967296)}`);
B.line(`${negative} = fcmp olt double ${residue}, ${f64Lit(0)}`);
B.line(`${wrapped} = fadd double ${residue}, ${f64Lit(4294967296)}`);
B.line(`${normalized} = select i1 ${negative}, double ${wrapped}, double ${residue}`);
Expand Down
20 changes: 18 additions & 2 deletions packages/compiler/src/backend/llvm/lib-filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { arrNewCall, traceArg, vAdapters } from "./shapes.js";
import type { LlvmEmitterContext, LibCallExpr, LlValue } from "./expr-context.js";
import { f64Lit } from "./common.js";
import { emitAlwaysThrowLibCall } from "./lib-shared.js";
import { emitTruncF64 } from "./trunc.js";

const FS_ALWAYS_THROW_SYMS: Readonly<Record<string, string>> = {
"fs.mkdtempChk": "scr_fs_mkdtemp_chk",
Expand Down Expand Up @@ -663,8 +664,23 @@ export function emitPathUrlLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll

export function emitPrimitiveLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
const B = host.B;
if (e.fn === "math.floor" || e.fn === "math.trunc" || e.fn === "math.ceil") {
const intr = e.fn === "math.floor" ? "floor" : e.fn === "math.trunc" ? "trunc" : "ceil";
if (e.fn === "math.min" || e.fn === "math.max") {
// IEEE 754-2019 minimum/maximum: NaN propagates and -0 orders below +0,
// exactly Math.min/Math.max of two numbers.
const a = host.emitExpr(e.args[0]!);
const b = host.emitExpr(e.args[1]!);
const intr = e.fn === "math.min" ? "minimum" : "maximum";
host.declare(`declare double @llvm.${intr}.f64(double, double)`);
const t = B.tmp();
B.line(`${t} = call double @llvm.${intr}.f64(double ${a.name}, double ${b.name})`);
return { name: t, type: e.type };
}
if (e.fn === "math.trunc") {
const v = host.emitExpr(e.args[0]!);
return { name: emitTruncF64(host, v.name), type: e.type };
}
if (e.fn === "math.floor" || e.fn === "math.ceil") {
const intr = e.fn === "math.floor" ? "floor" : "ceil";
const v = host.emitExpr(e.args[0]!);
host.declare(`declare double @llvm.${intr}.f64(double)`);
const t = B.tmp();
Expand Down
35 changes: 35 additions & 0 deletions packages/compiler/src/backend/llvm/trunc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* ToIntegerOrInfinity-style truncation of a double toward zero. */
import type { LlvmEmitterContext } from "./expr-context.js";
import { f64Lit } from "./common.js";

/** `Math.trunc(value)`. Targets with a rounding instruction use
* `llvm.trunc`; on baseline x86-64 that intrinsic is a libm call, so the
* value instead takes an integer round trip. Every double of magnitude at
* least 2^52 is already an integer, and NaN and the infinities fail the
* magnitude test, so those keep their value; the round trip of a smaller
* value restores its sign so that `-0.5` and `-0` truncate to `-0`. */
export function emitTruncF64(host: LlvmEmitterContext, value: string): string {
const B = host.B;
if (!host.inlineTrunc) {
host.declare("declare double @llvm.trunc.f64(double)");
const out = B.tmp();
B.line(`${out} = call double @llvm.trunc.f64(double ${value})`);
return out;
}
host.declare("declare double @llvm.fabs.f64(double)");
host.declare("declare double @llvm.copysign.f64(double, double)");
const magnitude = B.tmp(),
small = B.tmp(),
integer = B.tmp(),
back = B.tmp(),
signed = B.tmp(),
out = B.tmp();
B.line(`${magnitude} = call double @llvm.fabs.f64(double ${value})`);
B.line(`${small} = fcmp olt double ${magnitude}, ${f64Lit(2 ** 52)}`);
// Poison for a large or non-finite value, which the select never picks.
B.line(`${integer} = fptosi double ${value} to i64`);
B.line(`${back} = sitofp i64 ${integer} to double`);
B.line(`${signed} = call double @llvm.copysign.f64(double ${back}, double ${value})`);
B.line(`${out} = select i1 ${small}, double ${signed}, double ${value}`);
return out;
}
37 changes: 37 additions & 0 deletions packages/compiler/src/frontend/lowering/array-values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
type IrStmt,
type IrType,
JSVAL,
isIntegerBytesElem,
type SrcLoc,
UNDEFINED_T,
typeEquals,
Expand Down Expand Up @@ -282,6 +283,42 @@ export function lowerSafeBytesRead(
type: F64,
loc: at,
};
if (isIntegerBytesElem(bytesT.elem)) {
// An integer element is never NaN, so the NaN-for-invalid read
// decides presence with one inline index check (no separate range
// and integrality tests).
const v = varRef("v.0", F64, at);
lowerer.liftedFns.push({
name,
params: [
{ localId: "b.0", name: "b", type: bytesT },
{ localId: "i.0", name: "i", type: F64 },
],
returnType: resultT,
locals: [
{ id: "b.0", name: "b", type: bytesT, mutable: false },
{ id: "i.0", name: "i", type: F64, mutable: false },
{ id: "v.0", name: "v", type: F64, mutable: false },
],
body: [
{ kind: "varDecl", localId: "v.0", init: { ...read, invalidNaN: true }, loc: at },
{
kind: "return",
value: {
kind: "ternary",
cond: { kind: "libCall", fn: "num.isNaN", args: [v], type: BOOL, loc: at },
then: lowerer.wrappedUndefined(resultT, at)!,
else_: lowerer.coerceToExpected(v, resultT),
type: resultT,
loc: at,
},
loc: at,
},
],
loc: at,
});
return { kind: "call", callee: name, args: [receiver, index], type: resultT, loc };
}
lowerer.liftedFns.push({
name,
params: [
Expand Down
102 changes: 100 additions & 2 deletions packages/compiler/src/frontend/lowering/lower-exprs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,14 @@ export function lowerExpr(lowerer: Lowerer, expr: ts.Expression): IrExpr {
}
lowerExprDepth++;
try {
const lowered = lowerExprInner(lowerer, expr);
let lowered = lowerExprInner(lowerer, expr);
const nanCoded = lowerer.nanCodedReads;
if (nanCoded) {
if (lowered.type.kind === "f64" && nanCoded.reboxes(expr))
lowered = reboxNanCoded(lowerer, lowered, locOf(expr));
else if (lowered.type.kind === "union" && nanCoded.convertsSource(expr))
lowered = lowerOptionalNumber(lowerer, lowered, locOf(expr));
}
if (lowered.type.kind === "void" && (lowerer.typeOf(expr).flags & ts.TypeFlags.Never) !== 0)
lowerer.neverValued.add(lowered);
return lowered;
Expand All @@ -371,6 +378,28 @@ export function lowerExpr(lowerer: Lowerer, expr: ts.Expression): IrExpr {
}
}

/** `number | undefined` from a NaN-coded double whose slot never holds a
* genuine NaN (nan-coded-reads.ts): NaN is exactly undefined. */
function reboxNanCoded(lowerer: Lowerer, value: IrExpr, loc: SrcLoc): IrExpr {
const optional = lowerer.withUndefinedArm(F64);
const stmts: IrStmt[] = [];
let stable = value;
if (value.kind !== "varRef") {
const tmp = lowerer.declareHiddenLocal("%nanCoded", F64);
stmts.push({ kind: "varDecl", localId: tmp.id, init: value, loc });
stable = varRef(tmp.id, F64, loc);
}
const result: IrExpr = {
kind: "ternary",
cond: { kind: "libCall", fn: "num.isNaN", args: [stable], type: BOOL, loc },
then: lowerer.wrappedUndefined(optional, loc)!,
else_: lowerer.coerceToExpected(stable, optional),
type: optional,
loc,
};
return stmts.length === 0 ? result : { kind: "seqExpr", stmts, result, type: optional, loc };
}

function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
const loc = locOf(expr);

Expand Down Expand Up @@ -6768,7 +6797,12 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
// `t[i]!` asserts presence: the numeric element read (NaN when
// invalid). Every other read answers undefined for an invalid index,
// like Node.
if (ts.isNonNullExpression(expr.parent) && expr.parent.expression === expr) {
// A read whose every consumer treats undefined like NaN (see
// nan-coded-reads.ts) is the same plain numeric read.
if (
(ts.isNonNullExpression(expr.parent) && expr.parent.expression === expr) ||
lowerer.nanCodedReads?.isNumericRead(expr) === true
) {
return {
kind: "bytesIntrinsic",
method: "get",
Expand Down Expand Up @@ -9670,13 +9704,77 @@ export function lowerCompoundValueToTarget(
lowerer.unsupported("SC1043", expr);
}

/** Equality of two NaN-coded values (nan-coded-reads.ts), each of which may
* stand for undefined and never holds a genuine NaN: Node's undefined equals
* undefined, so two NaNs compare equal here. Operands evaluate once, in
* order. */
function lowerNanCodedEquality(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr | null {
const loc = locOf(expr);
const left = lowerer.lowerExpr(expr.left);
const right = lowerer.lowerExpr(expr.right);
if (left.type.kind !== "f64" || right.type.kind !== "f64") {
throw new InternalCompilerError("NaN-coded equality operand is not a plain number");
}
const stmts: IrStmt[] = [];
const stable = (value: IrExpr, name: string): IrExpr => {
if (value.kind === "varRef" || value.kind === "numLit") return value;
const tmp = lowerer.declareHiddenLocal(name, F64);
stmts.push({ kind: "varDecl", localId: tmp.id, init: value, loc });
return varRef(tmp.id, F64, loc);
};
const a = stable(left, "%eqLeft");
const b = stable(right, "%eqRight");
const isNaN = (value: IrExpr): IrExpr => ({
kind: "libCall",
fn: "num.isNaN",
args: [value],
type: BOOL,
loc,
});
const bothUndefined: IrExpr = {
kind: "logical",
op: "&&",
left: isNaN(a),
right: isNaN(b),
type: BOOL,
loc,
};
const op = expr.operatorToken.kind;
const negated =
op === ts.SyntaxKind.ExclamationEqualsEqualsToken ||
op === ts.SyntaxKind.ExclamationEqualsToken;
const result: IrExpr = negated
? {
kind: "logical",
op: "&&",
left: { kind: "bin", op: "!==", left: a, right: b, type: BOOL, loc },
right: { kind: "unary", op: "!", operand: bothUndefined, type: BOOL, loc },
type: BOOL,
loc,
}
: {
kind: "logical",
op: "||",
left: { kind: "bin", op: "===", left: a, right: b, type: BOOL, loc },
right: bothUndefined,
type: BOOL,
loc,
};
return stmts.length === 0 ? result : { kind: "seqExpr", stmts, result, type: BOOL, loc };
}

export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr {
const loc = locOf(expr);
const op = expr.operatorToken.kind;

const cacheHas = lowerRequireCacheHas(lowerer, expr);
if (cacheHas) return cacheHas;

if (lowerer.nanCodedReads?.needsUndefinedEquality(expr)) {
const undefinedEquality = lowerNanCodedEquality(lowerer, expr);
if (undefinedEquality) return undefinedEquality;
}

if (
op === ts.SyntaxKind.EqualsToken ||
(op >= ts.SyntaxKind.FirstCompoundAssignment && op <= ts.SyntaxKind.LastCompoundAssignment)
Expand Down
Loading
Loading