Optimize binary numeric pipelines

- Specialize standard Buffer and DataView numeric reads and writes while preserving checked errors, byte order, and unaligned access.
- Carry integer values through binary-processing loops and remove proven field bounds and receiver ownership overhead.
- Strengthen numeric IR validation and cover binary pipelines, edge cases, and native compiler parity.
This commit is contained in:
Chris Tate
2026-10-05 10:35:06 -05:00
committed by GitHub
parent d2cdd203fe
commit 5de340f933
21 changed files with 645 additions and 32 deletions
@@ -0,0 +1,231 @@
import { byteNumberRange, type ByteNumberAccess } from "../../ir/byte-numbers.js";
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";
/** Supported LLVM targets are little-endian. All memory accesses use align
* 1: Buffer fields and DataView windows can start at arbitrary byte offsets. */
function endianBits(host: LlvmEmitterContext, raw: string, width: number, little: string): string {
if (width === 1 || little === "true") return raw;
const B = host.B, bits = width * 8;
const swapBits = bits <= 16 ? 16 : bits <= 32 ? 32 : 64;
let operand = raw;
if (bits !== swapBits) {
operand = B.tmp();
B.line(`${operand} = zext i${bits} ${raw} to i${swapBits}`);
}
host.declare(`declare i${swapBits} @llvm.bswap.i${swapBits}(i${swapBits})`);
let swapped = B.tmp();
B.line(`${swapped} = call i${swapBits} @llvm.bswap.i${swapBits}(i${swapBits} ${operand})`);
if (bits !== swapBits) {
const shifted = B.tmp(), narrowed = B.tmp();
B.line(`${shifted} = lshr i${swapBits} ${swapped}, ${swapBits - bits}`);
B.line(`${narrowed} = trunc i${swapBits} ${shifted} to i${bits}`);
swapped = narrowed;
}
if (little === "false") return swapped;
const selected = B.tmp();
B.line(`${selected} = select i1 ${little}, i${bits} ${raw}, i${bits} ${swapped}`);
return selected;
}
/** Emit the successful numeric access directly. The original runtime call
* remains the error path, preserving Buffer's value-before-offset errors,
* DataView's ToIndex behavior, and exception cleanup through the host. All
* operands have already been evaluated, exactly once and in source order. */
export function emitByteNumber(
host: LlvmEmitterContext,
e: IrExpr & { kind: "bytesIntrinsic" },
spec: ByteNumberAccess,
receiver: LlValue,
offset: LlValue,
value: LlValue | null,
little: string,
fallback: () => LlValue,
): LlValue {
const B = host.B, size = host.sizeType;
const slow = B.newLabel("bytes.number.slow"), access = B.newLabel("bytes.number.access");
const fastDone = B.newLabel("bytes.number.fast.done"), slowDone = B.newLabel("bytes.number.slow.done");
const done = B.newLabel("bytes.number.done");
const bits = spec.width * 8;
const integer = exactInteger(host, offset, e.args[spec.offsetArg]);
let index: string, fits: string, room = "true";
if (integer && host.bytesBounds.has(e)) {
const wide = widenInteger(host, integer);
index = size === "i64" ? wide : B.tmp();
if (size !== "i64") B.line(`${index} = trunc i64 ${wide} to ${size}`);
fits = "true";
} else {
const lengthPtr = B.tmp(), length = B.tmp(), capacity = B.tmp();
room = B.tmp();
B.line(`${lengthPtr} = getelementptr inbounds %ScrBytes, ptr ${receiver.name}, i64 0, i32 1`);
B.line(`${length} = load ${size}, ptr ${lengthPtr}`);
B.line(`${room} = icmp uge ${size} ${length}, ${spec.width}`);
B.line(`${capacity} = sub ${size} ${length}, ${spec.width}`);
if (integer) {
const wide = widenInteger(host, integer);
const cap = size === "i64" ? capacity : B.tmp();
if (size !== "i64") B.line(`${cap} = zext ${size} ${capacity} to i64`);
fits = B.tmp();
B.line(`${fits} = icmp ule i64 ${wide}, ${cap}`);
if (spec.dataView && integer.range.max > Number.MAX_SAFE_INTEGER) {
const safe = B.tmp(), both = B.tmp();
B.line(`${safe} = icmp ule i64 ${wide}, ${Number.MAX_SAFE_INTEGER}`);
B.line(`${both} = and i1 ${fits}, ${safe}`);
fits = both;
}
index = size === "i64" ? wide : B.tmp();
if (size !== "i64") B.line(`${index} = trunc i64 ${wide} to ${size}`);
} else {
let normalized = offset.name;
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})`);
}
const cap = B.tmp(), positive = B.tmp(), bounded = B.tmp(), representable = B.tmp(), range = B.tmp();
const addressRange = B.tmp(), initial = B.tmp(), convert = B.newLabel("bytes.number.index");
B.line(`${cap} = uitofp ${size} ${capacity} to double`);
B.line(`${positive} = fcmp oge double ${normalized}, ${f64Lit(0)}`);
B.line(`${bounded} = fcmp ole double ${normalized}, ${cap}`);
// The separate address limit makes fptoui safe even if converting a
// maximum size_t length to double rounds upward on a 64-bit target.
B.line(`${representable} = fcmp olt double ${normalized}, ${f64Lit(size === "i64" ? 2 ** 64 : 2 ** 32)}`);
B.line(`${range} = and i1 ${positive}, ${bounded}`);
B.line(`${addressRange} = and i1 ${range}, ${representable}`);
B.line(`${initial} = and i1 ${room}, ${addressRange}`);
B.condBr(initial, convert, slow);
B.startBlock(convert);
index = B.tmp();
B.line(`${index} = fptoui double ${normalized} to ${size}`);
fits = B.tmp();
const roundTrip = B.tmp();
B.line(`${roundTrip} = uitofp ${size} ${index} to double`);
B.line(`${fits} = fcmp oeq double ${roundTrip}, ${normalized}`);
const withinCapacity = B.tmp(), exactBounds = B.tmp();
B.line(`${withinCapacity} = icmp ule ${size} ${index}, ${capacity}`);
B.line(`${exactBounds} = and i1 ${fits}, ${withinCapacity}`);
fits = exactBounds;
if (spec.dataView) {
const safe = B.tmp(), both = B.tmp();
B.line(`${safe} = fcmp ole double ${normalized}, ${f64Lit(Number.MAX_SAFE_INTEGER)}`);
B.line(`${both} = and i1 ${fits}, ${safe}`);
fits = both;
}
}
}
let valid = B.tmp();
B.line(`${valid} = and i1 ${room}, ${fits}`);
if (spec.write && !spec.dataView && !spec.floating) {
const range = byteNumberRange(spec)!;
const known = host.integerRanges.get(e.args[spec.valueArg]!);
if (!known || known.min < range.min || known.max > range.max) {
const above = B.tmp(), below = B.tmp(), valueFits = B.tmp(), both = B.tmp();
// Buffer integer writes accept NaN (stored as zero) and fractions
// within the declared range. Unordered comparisons preserve that.
B.line(`${above} = fcmp uge double ${value!.name}, ${f64Lit(range.min)}`);
B.line(`${below} = fcmp ule double ${value!.name}, ${f64Lit(range.max)}`);
B.line(`${valueFits} = and i1 ${above}, ${below}`);
B.line(`${both} = and i1 ${valid}, ${valueFits}`);
valid = both;
}
}
B.condBr(valid, access, slow);
B.startBlock(access);
const data = host.emitBytesData(receiver.name), pointer = B.tmp();
B.line(`${pointer} = getelementptr inbounds i8, ptr ${data}, ${size} ${index}`);
let fastResult = "", fastInteger = "";
const readRange = !spec.write ? byteNumberRange(spec) : null;
const integerType = bits <= 32 ? "i32" : "i64";
if (spec.write) {
let raw: string;
if (spec.floating) {
let stored = value!.name;
if (bits === 32) {
stored = B.tmp();
B.line(`${stored} = fptrunc double ${value!.name} to float`);
}
raw = B.tmp();
B.line(`${raw} = bitcast ${bits === 32 ? "float" : "double"} ${stored} to i${bits}`);
} else if (spec.dataView) {
const stored = host.emitToUint32(value!.name, e.args[spec.valueArg], value!.uint32);
raw = stored;
if (bits < 32) {
raw = B.tmp();
B.line(`${raw} = trunc i32 ${stored} to i${bits}`);
}
} else {
const integerValue = exactInteger(host, value!, e.args[spec.valueArg]);
if (integerValue && (integerValue.type === "i64" || bits <= 32)) {
raw = integerValue.name;
if (integerValue.type !== `i${bits}`) {
raw = B.tmp();
B.line(`${raw} = trunc ${integerValue.type} ${integerValue.name} to i${bits}`);
}
} else {
const nan = B.tmp(), normalized = B.tmp(), whole = B.tmp();
B.line(`${nan} = fcmp uno double ${value!.name}, ${value!.name}`);
B.line(`${normalized} = select i1 ${nan}, double ${f64Lit(0)}, double ${value!.name}`);
B.line(`${whole} = fptosi double ${normalized} to i64`);
raw = B.tmp();
B.line(`${raw} = trunc i64 ${whole} to i${bits}`);
}
}
const stored = endianBits(host, raw, spec.width, little);
B.line(`store i${bits} ${stored}, ptr ${pointer}, align 1`);
if (!spec.dataView) {
fastResult = B.tmp();
B.line(`${fastResult} = fadd double ${offset.name}, ${f64Lit(spec.width)}`);
}
} else {
const loaded = B.tmp();
B.line(`${loaded} = load i${bits}, ptr ${pointer}, align 1`);
const raw = endianBits(host, loaded, spec.width, little);
fastResult = B.tmp();
if (spec.floating) {
if (bits === 32) {
const float = B.tmp();
B.line(`${float} = bitcast i32 ${raw} to float`);
B.line(`${fastResult} = fpext float ${float} to double`);
} else B.line(`${fastResult} = bitcast i64 ${raw} to double`);
} else {
B.line(`${fastResult} = ${spec.signed ? "sitofp" : "uitofp"} i${bits} ${raw} to double`);
if (readRange) {
fastInteger = raw;
if (`i${bits}` !== integerType) {
fastInteger = B.tmp();
B.line(`${fastInteger} = ${spec.signed ? "sext" : "zext"} i${bits} ${raw} to ${integerType}`);
}
}
}
}
B.br(fastDone);
B.startBlock(fastDone);
B.br(done);
B.startBlock(slow);
const failure = fallback();
let slowInteger = "";
if (readRange) {
slowInteger = B.tmp();
B.line(`${slowInteger} = ${spec.signed ? "fptosi" : "fptoui"} double ${failure.name} to ${integerType}`);
}
B.br(slowDone);
B.startBlock(slowDone);
B.br(done);
B.startBlock(done);
if (e.type.kind === "void") return { name: "", type: e.type };
const result = B.tmp();
B.line(`${result} = phi double [ ${fastResult}, %${fastDone} ], [ ${failure.name}, %${slowDone} ]`);
if (readRange) {
const integer = B.tmp();
B.line(`${integer} = phi ${integerType} [ ${fastInteger}, %${fastDone} ], [ ${slowInteger}, %${slowDone} ]`);
const uint32 = integerType === "i32" ? integer : B.tmp();
if (integerType !== "i32") B.line(`${uint32} = trunc i64 ${integer} to i32`);
return { name: result, type: e.type, uint32, integer: { name: integer, type: integerType, signed: spec.signed, range: readRange } };
}
return { name: result, type: e.type };
}
@@ -3,6 +3,7 @@ import type { IntegerRanges } from "../../ir/integer-ranges.js";
import { integerArithmeticRange, type IntegerRange } from "../../ir/integer-ranges.js";
import { everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js";
import { isStableReceiverOperand } from "../../ir/analysis.js";
import { byteNumberAccess } from "../../ir/byte-numbers.js";
interface Extent { receiver: string; offset: number }
interface Bound extends Extent { counter: string; minimum: number }
@@ -34,7 +35,8 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly
const cached = new Map<string, Extent | null>();
const resolving = new Set<string>();
function extent(e: IrExpr, cache: boolean): Extent | null {
if (e.kind === "bytesIntrinsic" && e.method === "length" && e.receiver.kind === "varRef" &&
if (e.kind === "bytesIntrinsic" && (e.method === "length" || e.method === "byteLength" &&
e.receiver.type.kind === "bytes" && (e.receiver.type.elem === "u8" || e.receiver.type.elem === "u8c")) && e.receiver.kind === "varRef" &&
eligible(e.receiver.localId) && (!cache || !changed.has(e.receiver.localId))) return { receiver: e.receiver.localId, offset: 0 };
if (e.kind === "varRef" && eligible(e.localId) && !changed.has(e.localId)) {
const previous = cached.get(e.localId);
@@ -112,7 +114,7 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly
}
return result && stable(s.body, result.receiver, counter) ? result : null;
}
function access(node: Node, array: IrExpr, index: IrExpr, active: Bound[]): void {
function access(node: Node, array: IrExpr, index: IrExpr, active: Bound[], width: number): void {
const integer = ranges.get(index);
let safe = false;
if (array.kind === "varRef" && eligible(array.localId)) {
@@ -120,17 +122,19 @@ export function findBytesBounds(fn: IrFunction, ranges: IntegerRanges): Readonly
if (array.localId !== bound.receiver) continue;
if (!integer && !affineRange(index, bound)) continue;
const offset = affine(index, bound.counter);
if (offset !== null && bound.minimum + offset >= 0 && bound.offset + offset <= 0) { safe = true; break; }
if (offset !== null && bound.minimum + offset >= 0 && bound.offset + offset + width - 1 <= 0) { safe = true; break; }
}
}
proved.set(node, (proved.get(node) ?? true) && safe);
}
function expr(e: IrExpr, active: Bound[]): boolean {
if (e.kind === "bytesIntrinsic" && e.method === "get" && e.args[0]) access(e, e.receiver, e.args[0], active);
if (e.kind === "bytesIntrinsic" && e.method === "get" && e.args[0]) access(e, e.receiver, e.args[0], active, 1);
const numeric = byteNumberAccess(e);
if (numeric && e.kind === "bytesIntrinsic") access(e, e.receiver, e.args[numeric.offsetArg]!, active, numeric.width);
return everyExprChild(e, (child) => expr(child, active), (child) => stmt(child, active));
}
function stmt(s: IrStmt, active: Bound[]): boolean {
if (s.kind === "bytesSet") access(s, s.arr, s.index, active);
if (s.kind === "bytesSet") access(s, s.arr, s.index, active, 1);
if (s.kind === "for") {
if (s.init) stmt(s.init, active);
if (s.cond) expr(s.cond, active);
@@ -5,6 +5,8 @@ import { BYTES_ELEMENT_SIZE, F64, type IrBytesElem, type IrExpr } from "../../ir
import type { LlvmEmitterContext, LlValue } from "./expr-context.js";
import { F64_INF, f64Lit } from "./common.js";
import { exactInteger, widenInteger } from "./integer-values.js";
import { byteNumberAccess } from "../../ir/byte-numbers.js";
import { emitByteNumber } from "./byte-numbers.js";
const BYTES_NUM_KIND: Record<string, { kind: number; le: boolean } | undefined> = {
u8: { kind: 0, le: false },
@@ -333,27 +335,31 @@ export function emitBytesIntrinsic(host: LlvmEmitterContext, e: IrExpr & { kind:
if (e.method === "readNum" || e.method === "writeNum" || e.method === "readNumVar" || e.method === "writeNumVar") {
const tok = e.args[0]!;
if (tok.kind !== "strLit") throw new InternalCompilerError(`llvm emitter bug: bytesIntrinsic ${e.method} kind must be a strLit`);
const r0 = emitBorrowedInput(host, e.receiver);
const r0 = host.emitStableReceiver(e.receiver, e.args.slice(1));
const rest = e.args.slice(1).map((a) => host.emitExpr(a));
const access = byteNumberAccess(e);
const numeric = (fallback: () => LlValue): LlValue => access
? emitByteNumber(host, e, access, r0, rest[access.offsetArg - 1]!, access.write ? rest[access.valueArg - 1]! : null, String(access.littleEndian), fallback)
: fallback();
if (e.method === "readNum" || e.method === "writeNum") {
const spec = BYTES_NUM_KIND[tok.value];
if (!spec) throw new InternalCompilerError(`llvm emitter bug: bytes numeric kind '${tok.value}'`);
return e.method === "readNum"
return numeric(() => e.method === "readNum"
? call("scr_bytes_read_num", "double (ptr, double, i32, i1 zeroext)",
`ptr ${r0.name}, double ${rest[0]!.name}, i32 ${spec.kind}, i1 ${spec.le}`, false, true)
: call("scr_bytes_write_num", "double (ptr, double, double, i32, i1 zeroext)",
`ptr ${r0.name}, double ${rest[0]!.name}, double ${rest[1]!.name}, i32 ${spec.kind}, i1 ${spec.le}`, false, true);
`ptr ${r0.name}, double ${rest[0]!.name}, double ${rest[1]!.name}, i32 ${spec.kind}, i1 ${spec.le}`, false, true));
}
const spec = BYTES_NUM_VAR[tok.value];
if (!spec) throw new InternalCompilerError(`llvm emitter bug: bytes variable-width kind '${tok.value}'`);
return e.method === "readNumVar"
return numeric(() => e.method === "readNumVar"
? call("scr_bytes_read_var", "double (ptr, double, double, i1 zeroext, i1 zeroext)",
`ptr ${r0.name}, double ${rest[0]!.name}, double ${rest[1]!.name}, i1 ${spec.sign}, i1 ${spec.le}`, false, true)
: call("scr_bytes_write_var", "double (ptr, double, double, double, i1 zeroext, i1 zeroext)",
`ptr ${r0.name}, double ${rest[0]!.name}, double ${rest[1]!.name}, double ${rest[2]!.name}, i1 ${spec.sign}, i1 ${spec.le}`, false, true);
`ptr ${r0.name}, double ${rest[0]!.name}, double ${rest[1]!.name}, double ${rest[2]!.name}, i1 ${spec.sign}, i1 ${spec.le}`, false, true));
}
const method = e.method;
const directElementAccess = method === "length" || method === "byteLength" || method === "get";
const directElementAccess = method === "length" || method === "byteLength" || method === "get" || byteNumberAccess(e) !== null;
const r = directElementAccess
? host.emitStableReceiver(e.receiver, e.args)
: emitBorrowedInput(host, e.receiver);
@@ -615,16 +621,17 @@ export function emitBytesIntrinsic(host: LlvmEmitterContext, e: IrExpr & { kind:
case "dvGetFloat32":
case "dvGetFloat64":
case "dvGetBigUint64Number":
case "dvGetBigInt64Number":
case "dvGetBigInt64Number": {
// DataView getters: an omitted littleEndian is big-endian (the JS
// default). Throw Node's constant RangeError on a bad offset.
return call(
return emitByteNumber(host, e, byteNumberAccess(e)!, r, args[0]!, null, args[1]?.name ?? "false", () => call(
"scr_dataview_get",
"double (ptr, double, i32, i1 zeroext)",
`ptr ${r.name}, double ${args[0]!.name}, i32 ${DV_GET_KIND[method]}, i1 ${args[1]?.name ?? "false"}`,
false,
true,
);
));
}
case "dvSetUint8":
case "dvSetInt8":
case "dvSetUint16":
@@ -632,17 +639,18 @@ export function emitBytesIntrinsic(host: LlvmEmitterContext, e: IrExpr & { kind:
case "dvSetUint32":
case "dvSetInt32":
case "dvSetFloat32":
case "dvSetFloat64":
case "dvSetFloat64": {
// DataView setters: [offset, value, littleEndian?] — void; throw
// the getters' constant RangeError on a bad offset.
return call(
return emitByteNumber(host, e, byteNumberAccess(e)!, r, args[0]!, args[1]!, args[2]?.name ?? "false", () => call(
"scr_dataview_set",
"void (ptr, double, double, i32, i1 zeroext)",
`ptr ${r.name}, double ${args[0]!.name}, double ${args[1]!.name}, ` +
`i32 ${DV_SET_KIND[method]}, i1 ${args[2]?.name ?? "false"}`,
false,
true,
);
));
}
default: {
const _exhaustive: never = method;
void _exhaustive;
@@ -1,5 +1,6 @@
import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js";
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
import { byteNumberAccess } from "../../ir/byte-numbers.js";
/** Keep a computed ToUint32 view beside an ordinary number when integer
* consumers in loops can reuse it. The number remains authoritative for floating
@@ -23,6 +24,9 @@ export function findIntegerViews(fn: IrFunction): Set<string> {
} else if (node.kind === "unary" && node.op === "~") operand(node.operand);
else if (node.kind === "libCall" && (node.fn === "math.imul" || node.fn === "math.clz32")) {
for (const arg of node.args) operand(arg);
} else if (node.kind === "bytesIntrinsic") {
const numeric = byteNumberAccess(node);
if (numeric?.write && !numeric.floating && numeric.width <= 4) operand(node.args[numeric.valueArg]!);
}
return everyExprChild(node, expr, stmt);
};
@@ -42,6 +42,18 @@ test("suspending bodies and environments cannot inherit synchronous guarantees",
expect(preservesRegexInputs("matchAllInto")).toBe(false);
});
test("numeric byte operations preserve owners but argument replacement and callbacks do not", () => {
const bytes: IrExpr = { kind: "varRef", localId: "bytes", type: { kind: "bytes", elem: "u8" }, loc };
const read: IrExpr = { kind: "bytesIntrinsic", method: "dvGetUint32", receiver: bytes, args: [number], type: F64, loc };
const write: IrExpr = { kind: "bytesIntrinsic", method: "dvSetUint32", receiver: bytes, args: [number, read], type: VOID, loc };
const summary = effects([fn("read", [read]), fn("write", [write])]);
expect(summary.functions).toEqual(new Set(["read", "write"]));
expect(summary.preserves(write)).toBe(true);
const replace: IrExpr = { kind: "assignExpr", localId: "bytes", value: bytes, type: bytes.type, loc };
expect(summary.preserves({ ...read, receiver: replace })).toBe(false);
expect(summary.preserves({ ...read, args: [call("unknown")] })).toBe(false);
});
test("long call graphs use a worklist and facts are rebuilt for changed bodies", () => {
const functions = Array.from({ length: 2000 }, (_, i) => fn(`f${i}`, i === 1999 ? [] : [call(`f${i + 1}`)]));
expect(effects(functions).functions.size).toBe(functions.length);
@@ -4,6 +4,7 @@ import { isRefCounted, type IrExpr, type IrFunction, type IrStmt } from "../../i
import { everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js";
import { borrowsStringInputs } from "./string-lifetimes.js";
import { borrowsMapReadInputs } from "./map-read-lifetimes.js";
import { byteNumberAccess } from "../../ir/byte-numbers.js";
type Call = IrExpr & { kind: "call" };
@@ -36,6 +37,8 @@ function expressionPreservesEdges(e: IrExpr, call: (value: Call) => boolean): bo
case "libCall": return e.fn === "error.nodeThrow" || isStableReceiverOperand(e, "");
case "call": return call(e);
case "arrIntrinsic": return e.method === "length";
case "bytesIntrinsic": return e.method === "get" || e.method === "length" || e.method === "byteLength" ||
e.method === "byteOffset" || byteNumberAccess(e) !== null;
default: return false;
}
}
@@ -43,7 +46,7 @@ function expressionPreservesEdges(e: IrExpr, call: (value: Call) => boolean): bo
function statementPreservesEdges(s: IrStmt): boolean {
switch (s.kind) {
case "varDecl": case "exprStmt": case "return": case "if": case "for":
case "while": case "doWhile": case "block": case "break": case "continue": return true;
case "while": case "doWhile": case "block": case "break": case "continue": case "bytesSet": return true;
case "assign": case "fieldSet": case "recordSet": return !isRefCounted(s.value.type);
default: return false;
}
+3 -1
View File
@@ -1,4 +1,5 @@
import { InternalCompilerError } from "../errors.js";
import { byteNumberAccess } from "./byte-numbers.js";
import {
BYTES_ELEMENT_NAME,
DYN_HANDLE_KINDS,
@@ -119,6 +120,7 @@ export function isStableReceiverOperand(e: IrExpr, receiverLocalId: string): boo
switch (e.kind) {
case "numLit":
case "boolLit":
case "strLit":
case "varRef":
case "incDec":
return true;
@@ -136,7 +138,7 @@ export function isStableReceiverOperand(e: IrExpr, receiverLocalId: string): boo
isStableReceiverOperand(e.then, receiverLocalId) &&
isStableReceiverOperand(e.else_, receiverLocalId);
case "bytesIntrinsic":
return (e.method === "get" || e.method === "length" || e.method === "byteLength") &&
return (e.method === "get" || e.method === "length" || e.method === "byteLength" || byteNumberAccess(e) !== null) &&
e.receiver.kind === "varRef" &&
e.args.every((arg) => isStableReceiverOperand(arg, receiverLocalId));
case "libCall":
@@ -0,0 +1,42 @@
import { expect, test } from "vitest";
import { F64, STRING, VOID, bytesOf, type IrBytesIntrinsicMethod, type IrExpr, type IrFunction } from "./ir.js";
import { byteNumberAccess, byteNumberRange, validByteNumberToken } from "./byte-numbers.js";
import { analyzeIntegerRanges } from "./integer-ranges.js";
const loc = { file: "byte-numbers.ts", start: 0, end: 0 };
const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
const token = (value: string): IrExpr => ({ kind: "strLit", value, type: STRING, loc });
const receiver: IrExpr = { kind: "varRef", localId: "bytes", type: bytesOf("u8"), loc };
function access(method: IrBytesIntrinsicMethod, args: IrExpr[]): IrExpr {
return { kind: "bytesIntrinsic", method, receiver, args, type: method.startsWith("dvSet") ? VOID : F64, loc };
}
test("standard numeric reads carry exact integer ranges without classifying floats or rounded 64-bit reads", () => {
const cases: [IrExpr, { min: number; max: number } | null][] = [
[access("readNum", [token("u32le"), num(0)]), { min: 0, max: 4294967295 }],
[access("readNum", [token("i16be"), num(0)]), { min: -32768, max: 32767 }],
[access("readNumVar", [token("ibe"), num(0), num(5)]), { min: -(2 ** 39), max: 2 ** 39 - 1 }],
[access("readNumVar", [token("ule"), num(0), num(6)]), { min: 0, max: 2 ** 48 - 1 }],
[access("dvGetUint8", [num(0)]), { min: 0, max: 255 }],
[access("dvGetInt32", [num(0)]), { min: -(2 ** 31), max: 2 ** 31 - 1 }],
[access("dvGetBigUint64Number", [num(0)]), null],
[access("dvGetFloat32", [num(0)]), null],
[access("readNum", [token("f64le"), num(0)]), null],
[access("writeNum", [token("u32le"), num(1), num(0)]), null],
];
const fn: IrFunction = { name: "f", params: [], locals: [], returnType: VOID, loc,
body: cases.map(([expr]) => ({ kind: "exprStmt", expr, loc })) };
const ranges = analyzeIntegerRanges(fn);
for (const [expr, expected] of cases) expect(ranges.get(expr)).toEqual(expected);
});
test("variable widths require a valid literal and fixed tokens cannot inherit prototype entries", () => {
for (const width of [0, 7, 1.5, NaN, Infinity]) expect(byteNumberAccess(access("readNumVar", [token("ule"), num(0), num(width)]))).toBeNull();
for (const value of ["toString", "constructor", "__proto__", "u8le", "u64le", "f32", "u32xx"]) {
expect(validByteNumberToken("readNum", value)).toBe(false);
expect(byteNumberAccess(access("readNum", [token(value), num(0)]))).toBeNull();
}
const dynamic: IrExpr = { kind: "varRef", localId: "width", type: F64, loc };
expect(byteNumberAccess(access("readNumVar", [token("ule"), num(0), dynamic]))).toBeNull();
expect(byteNumberRange({ width: 8, signed: true, floating: false })).toBeNull();
});
+82
View File
@@ -0,0 +1,82 @@
import type { IrExpr } from "./ir.js";
import type { IntegerRange } from "./integer-ranges.js";
/** Numeric widths are properties of the standard byte operation, not of
* ordinary number values. Offsets and values retain their JS contracts. */
export interface ByteNumberFormat {
width: number;
signed: boolean;
floating: boolean;
}
export interface ByteNumberAccess extends ByteNumberFormat {
dataView: boolean;
write: boolean;
offsetArg: number;
valueArg: number;
littleEndian: boolean;
endianArg: number;
}
const FORMATS: Readonly<Record<string, ByteNumberFormat | undefined>> = {
u8: { width: 1, signed: false, floating: false },
i8: { width: 1, signed: true, floating: false },
u16: { width: 2, signed: false, floating: false },
i16: { width: 2, signed: true, floating: false },
u32: { width: 4, signed: false, floating: false },
i32: { width: 4, signed: true, floating: false },
f32: { width: 4, signed: false, floating: true },
f64: { width: 8, signed: false, floating: true },
u64: { width: 8, signed: false, floating: false },
i64: { width: 8, signed: true, floating: false },
};
const VIEW_FORMATS: Readonly<Record<string, string | undefined>> = {
dvGetUint8: "u8", dvGetInt8: "i8", dvGetUint16: "u16", dvGetInt16: "i16",
dvGetUint32: "u32", dvGetInt32: "i32", dvGetFloat32: "f32", dvGetFloat64: "f64",
dvGetBigUint64Number: "u64", dvGetBigInt64Number: "i64",
dvSetUint8: "u8", dvSetInt8: "i8", dvSetUint16: "u16", dvSetInt16: "i16",
dvSetUint32: "u32", dvSetInt32: "i32", dvSetFloat32: "f32", dvSetFloat64: "f64",
};
function bufferFormat(token: string): ByteNumberFormat | null {
const kind = token.length === 2 ? token : token.slice(0, -2);
if (!Object.hasOwn(FORMATS, kind) || kind === "u64" || kind === "i64") return null;
const format = FORMATS[kind]!;
return format.width === 1 ? token === kind ? format : null
: token === kind + "le" || token === kind + "be" ? format : null;
}
export function validByteNumberToken(method: string, token: string): boolean {
return method === "readNum" || method === "writeNum" ? bufferFormat(token) !== null
: (method === "readNumVar" || method === "writeNumVar") && ["ule", "ube", "ile", "ibe"].includes(token);
}
export function byteNumberAccess(e: IrExpr): ByteNumberAccess | null {
if (e.kind !== "bytesIntrinsic") return null;
if (e.method === "readNum" || e.method === "writeNum") {
const token = e.args[0];
if (token?.kind !== "strLit") return null;
const format = bufferFormat(token.value);
if (!format) return null;
const write = e.method === "writeNum";
return { ...format, dataView: false, write, offsetArg: write ? 2 : 1, valueArg: 1, littleEndian: token.value.endsWith("le"), endianArg: -1 };
}
if (e.method === "readNumVar" || e.method === "writeNumVar") {
const token = e.args[0], write = e.method === "writeNumVar";
const width = e.args[write ? 3 : 2];
if (token?.kind !== "strLit" || !validByteNumberToken(e.method, token.value) ||
width?.kind !== "numLit" || !Number.isInteger(width.value) || width.value < 1 || width.value > 6) return null;
return { width: width.value, signed: token.value.startsWith("i"), floating: false, dataView: false,
write, offsetArg: write ? 2 : 1, valueArg: 1, littleEndian: token.value.endsWith("le"), endianArg: -1 };
}
if (!Object.hasOwn(VIEW_FORMATS, e.method)) return null;
const format = FORMATS[VIEW_FORMATS[e.method]!]!, write = e.method.startsWith("dvSet");
return { ...format, dataView: true, write, offsetArg: 0, valueArg: 1, littleEndian: false, endianArg: format.width === 1 ? -1 : write ? 2 : 1 };
}
export function byteNumberRange(format: ByteNumberFormat): IntegerRange | null {
if (format.floating || format.width > 6) return null;
const bits = format.width * 8;
return format.signed ? { min: -(2 ** (bits - 1)), max: 2 ** (bits - 1) - 1 } : { min: 0, max: 2 ** bits - 1 };
}
+14 -7
View File
@@ -2,6 +2,7 @@ import type { IrExpr, IrLocal, IrStmt } from "./ir.js";
import type { IntegerRange, IntegerRanges } from "./integer-ranges.js";
import { everyExprChild, everyStmtChild, everyStmtList } from "./traverse.js";
import { byteNumberAccess } from "./byte-numbers.js";
/** A canonical byte loop whose induction variable is mathematically an
* unsigned integer for every body entry. Backends may keep this binding in
@@ -47,7 +48,8 @@ function stableLoopValue(value: IrExpr, locals: ReadonlyMap<string, IrLocal>, bo
return (value.op === "+" || value.op === "-" || value.op === "&" || value.op === ">>>" || value.op === "|") &&
stableLoopValue(value.left, locals, body, invariant, counter) && stableLoopValue(value.right, locals, body, invariant, counter);
case "bytesIntrinsic": case "arrIntrinsic":
return value.method === "length" && (value.kind === "bytesIntrinsic" || !invariant) && value.receiver.kind === "varRef" &&
return (value.method === "length" || value.kind === "bytesIntrinsic" && value.method === "byteLength") &&
(value.kind === "bytesIntrinsic" || !invariant) && value.receiver.kind === "varRef" &&
stableLoopValue(value.receiver, locals, body, invariant, counter);
default: return false;
}
@@ -55,8 +57,9 @@ function stableLoopValue(value: IrExpr, locals: ReadonlyMap<string, IrLocal>, bo
/** Normalize finite loop progress into signed i64 storage. Unknown limits
* are guarded before versioning small innermost loops; proven bounds need
* no duplicate body. A bounded step keeps the final update exactly
* representable, preserving the floating fallback at both 2^53 fixed points. */
* no duplicate body. Every body entry must remain exactly representable.
* A final update may round beyond the limit: the scoped counter cannot be
* observed after that update, and both representations exit the loop. */
export function matchIntegerCountedForLoop(
stmt: IrStmt & { kind: "for" }, locals: ReadonlyMap<string, IrLocal>, ranges: IntegerRanges = new Map(),
): IntegerCountedForLoop | null {
@@ -82,7 +85,7 @@ export function matchIntegerCountedForLoop(
if (step > 0 ? op !== "<" && op !== "<=" : op !== ">" && op !== ">=") return null;
if (!stableLoopValue(limit, locals, stmt.body, true, local.id)) return null;
const inclusive = op === "<=" || op === ">=";
const guardLimit = step > 0 ? 2 ** 53 - step + (inclusive ? 0 : 1) : -(2 ** 53) - step - (inclusive ? 0 : 1);
const guardLimit = step > 0 ? 2 ** 53 - (inclusive ? 1 : 0) : -(2 ** 53) + (inclusive ? 1 : 0);
const boundRange = limit.kind === "numLit" ? { min: limit.value, max: limit.value } : ranges.get(limit);
const guarded = !boundRange || (step > 0 ? !(boundRange.max <= guardLimit) : !(boundRange.min >= guardLimit));
if (guarded && limit.kind === "numLit") return null;
@@ -93,7 +96,11 @@ export function matchIntegerCountedForLoop(
const expr = (e: IrExpr): boolean => {
if (e.kind === "bin" && ((e.op === "&" || e.op === "|" || e.op === "^" || e.op === "<<" || e.op === ">>" || e.op === ">>>") && (counter(e.left) || counter(e.right)) ||
e.op === "%" && counter(e.left))) useful = true;
if (e.kind === "bytesIntrinsic" && e.method === "get" && e.args[0] && counter(e.args[0])) useful = true;
if (e.kind === "bytesIntrinsic") {
const numeric = byteNumberAccess(e);
const offset = e.args[numeric ? numeric.offsetArg : 0];
if ((numeric || e.method === "get") && offset && counter(offset)) useful = true;
}
if (e.kind === "libCall" && (e.fn === "math.imul" || e.fn === "math.clz32") && e.args.some(counter)) useful = true;
return (!guarded || ++cost <= 80) && everyExprChild(e, expr, bodyStmt);
};
@@ -104,8 +111,8 @@ export function matchIntegerCountedForLoop(
};
if (!stmt.body.every(bodyStmt) || !useful) return null;
const range = step > 0
? { min: startRange.min, max: Math.max(startRange.min, Math.min(2 ** 53 - step, boundRange ? Math.ceil(boundRange.max) - (inclusive ? 0 : 1) : Number.MAX_SAFE_INTEGER)) }
: { min: Math.min(startRange.max, Math.max(-(2 ** 53) - step, boundRange ? Math.floor(boundRange.min) + (inclusive ? 0 : 1) : -Number.MAX_SAFE_INTEGER)), max: startRange.max };
? { min: startRange.min, max: Math.max(startRange.min, Math.min(Number.MAX_SAFE_INTEGER, boundRange ? Math.ceil(boundRange.max) - (inclusive ? 0 : 1) : Number.MAX_SAFE_INTEGER)) }
: { min: Math.min(startRange.max, Math.max(-Number.MAX_SAFE_INTEGER, boundRange ? Math.floor(boundRange.min) + (inclusive ? 0 : 1) : -Number.MAX_SAFE_INTEGER)), max: startRange.max };
return { localId: local.id, start, limit, step, inclusive, guarded, guardLimit, range };
}
+6 -2
View File
@@ -1,5 +1,6 @@
import type { IrExpr, IrFunction, IrStmt } from "./ir.js";
import { everyExprChild, everyStmtChild, everyStmtList } from "./traverse.js";
import { byteNumberAccess, byteNumberRange } from "./byte-numbers.js";
/** Exactly representable integers, excluding negative zero. Facts describe
* the evaluated value, never a later read of the same local binding. */
@@ -170,9 +171,11 @@ export function analyzeIntegerRanges(fn: IrFunction): IntegerRanges {
}
break;
}
case "bytesIntrinsic":
case "bytesIntrinsic": {
expr(e.receiver, facts); for (const arg of e.args) expr(arg, facts);
if (e.method === "length") range = { min: 0, max: Number.MAX_SAFE_INTEGER };
const numeric = byteNumberAccess(e);
if (numeric && !numeric.write) range = byteNumberRange(numeric);
else if (e.method === "length" || e.method === "byteLength" || e.method === "byteOffset") range = { min: 0, max: Number.MAX_SAFE_INTEGER };
else if (e.method === "get" && e.receiver.type.kind === "bytes") {
const elem = e.receiver.type.elem;
if (elem === "u8" || elem === "u8c") range = { min: 0, max: 255 };
@@ -183,6 +186,7 @@ export function analyzeIntegerRanges(fn: IrFunction): IntegerRanges {
else if (elem === "i32") range = SIGNED;
}
break;
}
case "arrIntrinsic":
// Callbacks and arguments can have writes hidden in lazy lowering.
invalidate(facts, [e]);
+21
View File
@@ -183,6 +183,27 @@ function expressionModule(expr: IrExpr, unions: IrUnionDef[]): IrModule {
};
}
test("numeric byte tokens and DataView stores validate their storage before emission", () => {
const bytes: IrExpr = { kind: "varRef", localId: "bytes", type: { kind: "bytes", elem: "u8" }, loc };
const number: IrExpr = { kind: "numLit", value: 0, type: F64, loc };
const kind: IrExpr = { kind: "strLit", value: "u32le", type: STRING, loc };
const read: IrExpr = { kind: "bytesIntrinsic", method: "readNum", receiver: bytes, args: [kind, number], type: F64, loc };
const mod = expressionModule(read, []);
mod.functions[0]!.locals.push({ id: "bytes", name: "bytes", type: bytes.type, mutable: false });
expect(validateModule(deserializeModule(serializeModule(mod)))).toEqual([]);
for (const value of ["toString", "u64le", "u8be"]) {
kind.value = value;
expect(validateModule(mod).some((e) => e.message.includes("invalid kind token"))).toBe(true);
}
kind.value = "u32le";
const store: IrExpr = { kind: "bytesIntrinsic", method: "dvSetUint32", receiver: bytes, args: [number, number], type: VOID, loc };
mod.functions[0]!.body = [{ kind: "exprStmt", expr: store, loc }];
expect(validateModule(mod)).toEqual([]);
bytes.type = { kind: "bytes", elem: "u32" };
mod.functions[0]!.locals[0]!.type = bytes.type;
expect(validateModule(mod).some((e) => e.message.includes("u8 only"))).toBe(true);
});
test("record declarations accept forward, mutual, and self references and refresh missing IDs", () => {
const mod = expressionModule({ kind: "numLit", value: 0, type: F64, loc }, []);
mod.records = [
+6 -3
View File
@@ -3,6 +3,7 @@
* produce invalid IR), but it still carries the user's source location:
* an ICE that points at source is a gift to whoever debugs it.
*/
import { validByteNumberToken } from "./byte-numbers.js";
import type {
IrClassDef,
IrExpr,
@@ -3326,10 +3327,10 @@ function validateFunction(
break;
}
const recv = e.receiver.type;
// The dvGet* getters read through a DataView — always a bytes<u8>
// The DataView getters and setters use a bytes<u8>
// view; dataViewNew and byteOffset take ANY elem kind (views form
// over any typed array's storage, owners answer byteOffset 0).
const isDvGet = e.method.startsWith("dvGet");
const isDv = e.method.startsWith("dvGet") || e.method.startsWith("dvSet");
const isNum =
e.method === "readNum" || e.method === "writeNum" || e.method === "readNumVar" || e.method === "writeNumVar";
const U8_ONLY_EXTRA: ReadonlySet<string> = new Set([
@@ -3337,7 +3338,7 @@ function validateFunction(
"indexOfNum", "lastIndexOfNum", "includesNum", "fill", "fillNum", "fillStr",
"copy", "swap16", "swap32", "swap64", "writeStr",
]);
const u8Only = U8_ONLY_EXTRA.has(e.method) || isNum || isDvGet;
const u8Only = U8_ONLY_EXTRA.has(e.method) || isNum || isDv;
if (u8Only && recv.elem !== "u8") {
err(`bytesIntrinsic ${e.method} on a ${recv.elem} receiver (u8 only)`, e.loc);
}
@@ -3346,6 +3347,8 @@ function validateFunction(
// time; a runtime-valued kind has no meaning).
if (isNum && e.args[0]?.kind !== "strLit") {
err(`bytesIntrinsic ${e.method} args[0] must be a strLit kind token`, e.loc);
} else if (isNum && e.args[0]?.kind === "strLit" && !validByteNumberToken(e.method, e.args[0].value)) {
err(`bytesIntrinsic ${e.method} invalid kind token '${e.args[0].value}'`, e.loc);
}
// fillStr/writeStr carry their NORMALIZED encoding as args[1],
// always a strLit (the frontend folds the aliases).
@@ -0,0 +1,46 @@
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test } from "vitest";
import { compile, deserializeModule } from "../src/index.js";
import { emitLlvmModule } from "../src/backend/llvm/emitter.js";
test("numeric byte pipelines retain checked fallbacks and use field widths on both pointer sizes", async () => {
const dir = await mkdtemp(join(tmpdir(), "scriptc-byte-numbers-"));
try {
const entry = join(import.meta.dirname, "../../../tests/corpus/byte-number-pipeline.ts");
const outPath = join(dir, "main.ir.json");
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
if (!result.ok) throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
const mod = deserializeModule(await readFile(outPath, "utf8"));
const body = (ll: string, name: string): string => ll.match(new RegExp(`define internal [^\\n]+ @sc_(?:b)?f_${name}\\([^\\n]*\\) #0 \\{([\\s\\S]*?)\\n\\}`))![1]!;
for (const pointerBits of [32, 64] as const) {
const ll = emitLlvmModule(mod, { pointerBits, wasi: pointerBits === 32 });
for (const name of ["bufferRecords", "viewRecords", "backwards"]) {
const code = body(ll, name);
expect(code).toContain("integer induction offset");
expect(code).not.toContain("counted.slow");
expect(code).not.toContain("@scr_bytes_retain");
expect(code).toMatch(/load i32, ptr %\w+, align 1/);
expect(code).toMatch(/store i32 %\w+, ptr %\w+, align 1/);
expect(code).toContain("phi i32");
}
// All fields lie inside the extent established by these loop heads.
for (const name of ["bufferRecords", "viewRecords"]) {
expect(body(ll, name)).not.toContain("bytes.number.index");
expect(body(ll, name)).not.toMatch(/icmp uge i(?:32|64)/);
}
for (const name of ["shortTarget", "replacedBound"]) expect(body(ll, name)).toMatch(/icmp uge i(?:32|64)/);
const wide = body(ll, "wideFields");
expect(wide).toMatch(/load i40, ptr %\w+, align 1/);
expect(wide).toMatch(/load i48, ptr %\w+, align 1/);
expect(wide).toContain("phi i64");
expect(wide).toContain("@llvm.bswap.i64");
const offsets = body(ll, "offsets");
expect(offsets).toContain("@llvm.trunc.f64");
expect(offsets).toContain("@scr_dataview_set");
expect(offsets).toContain("@scr_dataview_get");
expect(offsets).toMatch(/icmp ule i(?:32|64)/);
}
} finally { await rm(dir, { recursive: true, force: true }); }
});
@@ -14228,6 +14228,12 @@
"<repo>/tests/corpus/collection-storage-copies.ts"
],
"diags": []
},
"<repo>/tests/corpus/byte-number-pipeline.ts": {
"order": [
"<repo>/tests/corpus/byte-number-pipeline.ts"
],
"diags": []
}
}
}
+104
View File
@@ -0,0 +1,104 @@
function bufferRecords(bytes: Buffer): number {
let checksum = 0;
for (let offset = 0; offset <= bytes.length - 12; offset += 12) {
const id = bytes.readUInt32LE(offset);
const delta = bytes.readIntLE(offset + 4, 3);
const flags = bytes.readUInt8(offset + 7);
const value = bytes.readFloatLE(offset + 8);
bytes.writeUInt32BE((id + flags) >>> 0, offset);
bytes.writeIntBE(delta, offset + 4, 3);
bytes.writeFloatBE(value * 2, offset + 8);
checksum = Math.imul(checksum ^ id, 17) + delta;
}
return checksum;
}
function viewRecords(view: DataView, little: boolean): number {
let checksum = 0;
for (let offset = 0; offset <= view.byteLength - 12; offset += 12) {
const id = view.getUint32(offset, little);
const delta = view.getInt16(offset + 4, little);
const flags = view.getUint16(offset + 6, little);
view.setUint32(offset, (id + flags) >>> 0, !little);
view.setInt16(offset + 4, delta, !little);
view.setFloat32(offset + 8, view.getFloat32(offset + 8, little) * 2, !little);
checksum = Math.imul(checksum ^ id, 17) + delta;
}
return checksum;
}
function backwards(view: DataView): void {
for (let offset = view.byteLength - 4; offset >= 0; offset -= 4) {
view.setUint32(offset, view.getUint32(offset, true) ^ 0x80000000);
}
}
const storage = Buffer.alloc(51, 0x5a);
const window = storage.subarray(1, 49);
for (let i = 0; i < 4; i++) {
const offset = i * 12;
window.writeUInt32LE(0xffffffff - i, offset);
window.writeIntLE(-8388608 + i, offset + 4, 3);
window.writeUInt8(i, offset + 7);
window.writeFloatLE(i === 0 ? -0 : i / 4, offset + 8);
}
console.log("buffer", bufferRecords(window), storage.toString("hex"));
const view = new DataView(storage.buffer, storage.byteOffset + 1, 48);
console.log("view", viewRecords(view, false), viewRecords(view, true));
backwards(view);
console.log("reverse", storage.toString("hex"));
console.log("empty", bufferRecords(Buffer.alloc(0)), viewRecords(new DataView(new ArrayBuffer(0)), true));
function wideFields(bytes: Buffer): number {
bytes.writeIntBE(-549755813887, 1, 5);
const signed = bytes.readIntBE(1, 5);
bytes.writeUIntLE(281474976710655, 6, 6);
const unsigned = bytes.readUIntLE(6, 6);
bytes.writeIntLE(signed, 12, 5);
bytes.writeUIntBE(unsigned, 17, 6);
return signed + unsigned;
}
const wide = Buffer.alloc(24, 0x33);
console.log("wide", wideFields(wide), wide.toString("hex"));
// A valid bound for one receiver does not prove a different view's extent.
function shortTarget(source: Buffer, target: Buffer): void {
for (let offset = 0; offset <= source.length - 4; offset += 4) {
target.writeUInt32BE(source.readUInt32LE(offset), offset);
}
}
try { shortTarget(Buffer.alloc(8, 1), Buffer.alloc(5)); }
catch (error) { if (error instanceof Error) console.log("short", error.name, error.message); }
function replacedBound(input: Buffer): number {
const length = input.length;
input = Buffer.alloc(2);
let total = 0;
for (let offset = 0; offset <= length - 4; offset += 4) total += input.readUInt32LE(offset);
return total;
}
try { console.log(replacedBound(Buffer.alloc(8))); }
catch (error) { if (error instanceof Error) console.log("replaced", error.name, error.message); }
let current = Buffer.alloc(4, 0x11);
function replaceBuffer(): number { current = Buffer.alloc(4, 0x22); return 0; }
console.log("snapshot", current.readUInt32LE(replaceBuffer()), current.readUInt32LE(0));
let currentView = new DataView(new ArrayBuffer(4));
const oldView = currentView;
function replaceView(): number { currentView = new DataView(new ArrayBuffer(4)); return 0x12345678; }
oldView.setUint32(0, 1);
currentView.setUint32(0, replaceView());
console.log("view-snapshot", oldView.getUint32(0), currentView.getUint32(0));
function offsets(view: DataView, offset: number, value: number): void {
view.setInt16(offset, value, true);
console.log("offset", offset, view.getInt16(offset, true));
}
for (const offset of [NaN, -0, -0.75, 1.75, 2, 2.75, -1, Infinity, 4294967296, 9007199254740992]) {
try { offsets(currentView, offset, 4294967295.75); }
catch (error) { if (error instanceof Error) console.log("offset-error", error.name, error.message); }
}
// The value error still precedes the offset error on the checked path.
try { current.writeUInt16LE(-1, NaN); }
catch (error) { if (error instanceof Error) console.log("order", error.name, error.message); }
+21
View File
@@ -62,3 +62,24 @@ function remainders(limit: number): void {
for (let i = 0; i < limit; i++) console.log("remainders", i % 0, i % -3, i % 1.5, i % Infinity, (i % 17) % 3);
}
remainders(4);
// The last update may round outside the exact range, but it must leave the
// loop before that rounded counter can be observed by the body.
function stridedEdges(limit: number): void {
let visits = 0;
for (let i = 9007199254740987; i < limit; i += 3) {
console.log("stride-up", i, i >>> 0);
if (++visits === 5) break;
}
visits = 0;
for (let i = -9007199254740987; i > -limit; i -= 3) {
console.log("stride-down", i, i >>> 0);
if (++visits === 5) break;
}
visits = 0;
for (let i = 9007199254740987; i <= limit; i += 3) {
console.log("stride-inclusive", i, i >>> 0);
if (++visits === 5) break;
}
}
for (const limit of [9007199254740990, 9007199254740991, 9007199254740992, 9007199254740994, Infinity, NaN]) stridedEdges(limit);
@@ -59,7 +59,7 @@ for (const backend of ["llvm"] as const) {
// Real frontend IR exercises the exact representation the two emitters
// supply to analyzeIntegerRanges, including lowered helper functions.
for (const source of ["1409-typedarray-integer-loops.ts", "3103-scalar-record-nested-control-flow.ts", "3109-identity-union-collections.ts"]) {
for (const source of ["1409-typedarray-integer-loops.ts", "3103-scalar-record-nested-control-flow.ts", "3109-identity-union-collections.ts", "byte-number-pipeline.ts"]) {
const path = join(dir, "frontend.json");
const result = await compile(join(root, "tests/corpus", source), { outDir: dir, outPath: path, outputKind: "ir", dynamic: false });
if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n"));
@@ -26,6 +26,15 @@ export function validatorCases(): ValidatorCase[] {
cases.push({ name, module, ...(diagnostic ? { diagnostic } : {}) });
};
add("empty module", () => {});
for (const variant of ["valid", "token", "storage"]) {
add(`numeric byte ${variant}`, (m) => {
const bytes: IrType = { kind: "bytes", elem: variant === "storage" ? "u32" : "u8" };
m.functions[0]!.locals = [{ id: "bytes", name: "bytes", type: bytes, mutable: false }];
m.functions[0]!.body = [expression(variant === "storage"
? { kind: "bytesIntrinsic", method: "dvSetUint32", receiver: varRef("bytes", bytes, loc), args: [numLit(0, loc), numLit(1, loc)], type: VOID, loc }
: { kind: "bytesIntrinsic", method: "readNum", receiver: varRef("bytes", bytes, loc), args: [strLit(variant === "token" ? "toString" : "u32le", loc), numLit(0, loc)], type: F64, loc })];
}, variant === "token" ? "invalid kind token" : variant === "storage" ? "u8 only" : undefined);
}
for (const direction of ["left", "right", "alternating"]) {
for (const invalid of [false, true]) {
add(`deep logical ${direction} ${invalid ? "invalid" : "valid"}`, (m) => {
@@ -51,6 +51,7 @@ for (const backend of ["llvm"] as const) {
"tests/corpus/3109-identity-union-collections.ts",
"tests/corpus/collection-value-keys.ts",
"tests/corpus/collection-storage-copies.ts",
"tests/corpus/byte-number-pipeline.ts",
"tests/corpus/nullish-long-chain.ts",
]) {
const irPath = join(dir, "emitted.json");
+3
View File
@@ -162,6 +162,9 @@ describe.skipIf(!zigOnPath())("wasm32-wasi differential", () => {
"1475-readline-closed-stdin.ts",
"1400-typedarray-basics.ts",
"1409-typedarray-integer-loops.ts",
"1660-buffer-read-write-num.ts",
"byte-number-pipeline.ts",
"counted-loop-numbers.ts",
"1551-dyn-receiver-methods.ts",
"1558-any-joins-and-dyn-validation.ts",
"1634-inspect-classes.ts",