feat: support static Float64Array (#391)

- Add Float64Array construction, storage, and access across compiler backends and runtime.

- Cover precision, views, methods, and dynamic boundaries with differential fixtures.
This commit is contained in:
Chris Tate
2026-09-23 19:13:18 -05:00
committed by GitHub
parent 7482bc773b
commit edc95464b8
15 changed files with 151 additions and 42 deletions
+9 -7
View File
@@ -1498,7 +1498,7 @@ export class CEmitter {
`}`,
);
}
if ([...this.bytesElementHelpers].some((key) => key.startsWith("set:") && key.split(":")[1] !== "f32")) {
if ([...this.bytesElementHelpers].some((key) => key.startsWith("set:") && !key.startsWith("set:f32:") && !key.startsWith("set:f64:"))) {
out.push(
`static inline uint32_t sc_bytes_coerce_u32(double v) {`,
` if (v >= -9007199254740992.0 && v <= 9007199254740992.0) return (uint32_t)(int64_t)v;`,
@@ -1509,7 +1509,7 @@ export class CEmitter {
`}`,
);
}
for (const elem of ["u8", "u32", "i32", "f32"] as const) {
for (const elem of ["u8", "u32", "i32", "f32", "f64"] as const) {
for (const mode of ["f64", "u64"] as const) {
const suffix = mode === "u64" ? "_u64" : "";
const indexType = mode === "u64" ? "uint64_t" : "double";
@@ -1522,11 +1522,12 @@ export class CEmitter {
`}`,
);
} else {
const valueType = elem === "f32" ? "float" : elem === "i32" ? "int32_t" : "uint32_t";
const valueType = elem === "f32" ? "float" : elem === "f64" ? "double" : elem === "i32" ? "int32_t" : "uint32_t";
const size = elem === "f64" ? 8 : 4;
out.push(
`static inline double sc_bytes_get_${elem}${suffix}(const ScrBytes *b, ${indexType} i) {`,
` ${valueType} v;`,
` memcpy(&v, b->data + ${checked}(b, i) * 4, 4);`,
` memcpy(&v, b->data + ${checked}(b, i) * ${size}, ${size});`,
` return (double)v;`,
`}`,
);
@@ -1540,13 +1541,14 @@ export class CEmitter {
`}`,
);
} else {
const valueType = elem === "f32" ? "float" : "uint32_t";
const init = elem === "f32" ? `(float)v` : `sc_bytes_coerce_u32(v)`;
const valueType = elem === "f32" ? "float" : elem === "f64" ? "double" : "uint32_t";
const init = elem === "f32" ? `(float)v` : elem === "f64" ? "v" : `sc_bytes_coerce_u32(v)`;
const size = elem === "f64" ? 8 : 4;
out.push(
`static inline void sc_bytes_set_${elem}${suffix}(ScrBytes *b, ${indexType} i, double v) {`,
` size_t idx = ${checked}(b, i);`,
` ${valueType} stored = ${init};`,
` memcpy(b->data + idx * 4, &stored, 4);`,
` memcpy(b->data + idx * ${size}, &stored, ${size});`,
`}`,
);
}
+1 -1
View File
@@ -1941,7 +1941,7 @@ function emitContainerExpr(
}
return emitter.newTemp(
e.type,
`(double)(${r.name}->len * ${e.receiver.type.elem === "u8" ? "1" : "4"})`,
`(double)(${r.name}->len * ${e.receiver.type.elem === "u8" ? "1" : e.receiver.type.elem === "f64" ? "8" : "4"})`,
);
case "get":
// Any invalid index traps (the array runtime's discipline).
+11 -3
View File
@@ -4,7 +4,7 @@ import { InternalCompilerError } from "../../errors.js";
* array element kinds, map key/value kinds), plus C literal spelling. Pure
* functions of IrType/values — every emission module leans on these, so they
* live in ONE place with no emitter state. */
import type { IrType } from "../../ir/ir.js";
import type { IrBytesElem, IrType } from "../../ir/ir.js";
import { POINTER_KINDS, type PointerKind, runtimeRcStem, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES } from "../../ir/ir.js";
import {
mangleClassRelease,
@@ -329,8 +329,16 @@ export function elemKindC(elem: IrType): string {
}
/** The runtime's element-kind tag for a bytes (typed array) type. */
export function bytesElemKindC(elem: "u8" | "u32" | "i32" | "f32"): string {
return elem === "u8" ? "SCR_BYTES_U8" : elem === "u32" ? "SCR_BYTES_U32" : elem === "i32" ? "SCR_BYTES_I32" : "SCR_BYTES_F32";
const BYTES_ELEM_KIND_C: Record<IrBytesElem, string> = {
u8: "SCR_BYTES_U8",
u32: "SCR_BYTES_U32",
i32: "SCR_BYTES_I32",
f32: "SCR_BYTES_F32",
f64: "SCR_BYTES_F64",
};
export function bytesElemKindC(elem: IrBytesElem): string {
return BYTES_ELEM_KIND_C[elem];
}
/** The runtime's ScrBytesNumKind tag + littleEndian flag per readNum/
@@ -145,7 +145,7 @@ export function emitBytesLength(host: LlvmEmitterContext, elem: IrBytesElem, rec
B.line(`${p} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 1`);
B.line(`${len} = load ${host.sizeType}, ptr ${p}`);
const count = bytes && elem !== "u8" ? B.tmp() : len;
if (count !== len) B.line(`${count} = shl ${host.sizeType} ${len}, 2`);
if (count !== len) B.line(`${count} = shl ${host.sizeType} ${len}, ${elem === "f64" ? 3 : 2}`);
const out = B.tmp();
B.line(`${out} = uitofp ${host.sizeType} ${count} to double`);
return { name: out, type: F64 };
@@ -174,6 +174,12 @@ export function emitBytesGet(host: LlvmEmitterContext, elem: IrBytesElem, receiv
B.line(`${out} = fpext float ${raw} to double`);
return { name: out, type: F64 };
}
if (elem === "f64") {
const out = B.tmp();
B.line(`${p} = getelementptr inbounds double, ptr ${data}, ${host.sizeType} ${idx}`);
B.line(`${out} = load double, ptr ${p}, align 1`);
return { name: out, type: F64 };
}
const raw = B.tmp();
const out = B.tmp();
B.line(`${p} = getelementptr inbounds i32, ptr ${data}, ${host.sizeType} ${idx}`);
@@ -252,7 +258,7 @@ export function emitToUint32(host: LlvmEmitterContext, value: string): string {
export function emitBytesSet(host: LlvmEmitterContext, elem: IrBytesElem, receiver: string, index: string, value: string, integerIndex = false): void {
const B = host.B;
const idx = host.emitBytesIndex(receiver, index, integerIndex);
const stored = elem === "f32" ? null : host.emitToUint32(value);
const stored = elem === "f32" || elem === "f64" ? null : host.emitToUint32(value);
const data = host.emitBytesData(receiver);
const p = B.tmp();
if (elem === "u8") {
@@ -269,6 +275,11 @@ export function emitBytesSet(host: LlvmEmitterContext, elem: IrBytesElem, receiv
B.line(`store float ${narrowed}, ptr ${p}, align 1`);
return;
}
if (elem === "f64") {
B.line(`${p} = getelementptr inbounds double, ptr ${data}, ${host.sizeType} ${idx}`);
B.line(`store double ${value}, ptr ${p}, align 1`);
return;
}
B.line(`${p} = getelementptr inbounds i32, ptr ${data}, ${host.sizeType} ${idx}`);
B.line(`store i32 ${stored!}, ptr ${p}, align 1`);
}
@@ -1,18 +1,19 @@
/* Focused LLVM expression emission extracted from emitter.ts. */
import { InternalCompilerError } from "../../errors.js";
import { matchStringSelfConcat, undefinedArmTag } from "../../ir/analysis.js";
import { isRefCounted } from "../../ir/ir.js";
import { isRefCounted, type IrBytesElem } from "../../ir/ir.js";
import { mangleRecordClone, mangleRecordNew } from "../mangle.js";
import { arrNewCall, elemAccess } from "./shapes.js";
import { LlvmUnsupportedError } from "./unsupported.js";
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
/** ScrBytesElem (scr_runtime.h): U8, U32, F32, I32. */
const BYTES_ELEM_NUM: Record<"u8" | "u32" | "f32" | "i32", number> = {
/** ScrBytesElem (scr_runtime.h): U8, U32, F32, I32, F64. */
const BYTES_ELEM_NUM: Record<IrBytesElem, number> = {
u8: 0,
u32: 1,
f32: 2,
i32: 3,
f64: 4,
};
import { f64Lit } from "./common.js";
@@ -96,16 +96,17 @@ function lowerBytesToSortedCall(
/* ── typed arrays / Buffer ─────────────────────────────────────────────── */
/** The typed-array constructors with a runtime representation, by lib
* interface name. The other TypedArray flavors (Int8Array, Float64Array,
* DataView, ...) fall through to the generic stdlib-constructor fence. */
* interface name. Other flavors (Int8Array, Float16Array, ...) fall through
* to the generic stdlib-constructor fence. DataView is handled separately. */
const BYTES_CTORS: Record<string, IrBytesElem | undefined> = {
Uint8Array: "u8",
Uint32Array: "u32",
Int32Array: "i32",
Float32Array: "f32",
Float64Array: "f64",
};
/** `new Uint8Array(...)` / `new Uint32Array(...)` / `new Float32Array(...)`
/** `new Uint8Array(...)` / `new Uint32Array(...)` / `new Float32Array(...)` / `new Float64Array(...)`
* (stdlib provenance — a user's own class with the name resolves through
* classBySymbol). Lowered argument shapes: none (empty), a length
* (zero-filled; ToIndex at runtime — invalid lengths throw Node's
@@ -163,7 +164,7 @@ export function lowerBytesNew(lowerer: Lowerer, expr: ts.NewExpression, symbol:
"erases into the view): drop the options bag",
);
}
const elemSize = elem === "u8" ? 1 : 4;
const elemSize = elem === "u8" ? 1 : elem === "f64" ? 8 : 4;
const lenArg = argNode.arguments?.length === 1 ? argNode.arguments[0] : undefined;
const lenT = lenArg ? lowerer.typeOf(lenArg) : null;
const byteLen = lenT?.isNumberLiteralType() ? lenT.value : null;
@@ -284,7 +285,7 @@ function lowerDataViewNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
}
const hint =
"views compile over a typed array's own storage — new DataView(x.buffer, byteOffset?, byteLength?) " +
"where x is a Uint8Array/Uint32Array/Float32Array/Buffer value — or a fresh buffer erased into " +
"where x is a Uint8Array/Uint32Array/Float32Array/Float64Array/Buffer value — or a fresh buffer erased into " +
"the view: new DataView(new ArrayBuffer(n), ...); free-standing ArrayBuffers have no representation";
if (!ts.isPropertyAccessExpression(bufNode) || bufNode.name.text !== "buffer" || bufNode.questionDotToken) {
lowerer.noLowering("new DataView over this buffer expression", bufNode, hint);
@@ -5180,8 +5180,8 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
);
}
// `new Uint8Array(...)` / `new Uint32Array(...)` / `new
// Float32Array(...)`: the typed-array constructors with a runtime
// representation (stdlib provenance — see lowerBytesNew for the
// Float32Array(...)` / `new Float64Array(...)`: the typed-array
// constructors with a runtime representation (stdlib provenance — see lowerBytesNew for the
// lowered argument shapes; a user's own class with one of the names
// resolves through classBySymbol below).
const bytesNew = lowerer.lowerBytesNew(expr, symbol);
@@ -430,7 +430,7 @@ export function formatIrType(t: IrType, shapes: ShapeRegistry, unions: UnionRegi
case "bytes":
// The u8 kind reads as Uint8Array (Buffer maps here too — one
// runtime representation; the message stays honest either way).
return t.elem === "u8" ? "Uint8Array" : t.elem === "u32" ? "Uint32Array" : t.elem === "i32" ? "Int32Array" : "Float32Array";
return t.elem === "u8" ? "Uint8Array" : t.elem === "u32" ? "Uint32Array" : t.elem === "i32" ? "Int32Array" : t.elem === "f32" ? "Float32Array" : "Float64Array";
case "map":
return `Map<${formatIrType(t.key, shapes, unions, seen)}, ${formatIrType(t.value, shapes, unions, seen)}>`;
case "set":
@@ -1702,10 +1702,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
return { kind: "regex" };
}
// Typed arrays: references to the lib's Uint8Array/Uint32Array/
// Float32Array interfaces (provenance, not names). The es2022+ lib
// Float32Array/Float64Array interfaces (provenance, not names). The es2022+ lib
// declares them generic over the backing buffer (`Uint8Array<ArrayBuffer>`
// in error text) — the type argument is irrelevant here: no views exist,
// every value owns its storage. The other TypedArray flavors stay
// in error text) — the type argument is irrelevant here: no free-standing
// ArrayBuffer value exists. The other TypedArray flavors stay
// unmapped (the record path's index-signature check rejects them).
// RegExpMatchArray (s.match's result) IS a string[] here — the honest
// slice: [whole match, ...captures] (a nonparticipating capture reads
@@ -1741,6 +1741,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
// SharedArrayBuffer, and the i32 semantics hold for every other use.
if (isStdlibInterface("Int32Array")) return bytesOf("i32");
if (isStdlibInterface("Float32Array")) return bytesOf("f32");
if (isStdlibInterface("Float64Array")) return bytesOf("f64");
// DataView: the ONE view kind — a u8 bytes value whose runtime
// representation borrows (aliases) its owner's storage, so reads through
// it see writes to the source exactly like JS. The checker keeps the
+10 -11
View File
@@ -24,11 +24,10 @@ export interface SrcLoc {
/* ── types ─────────────────────────────────────────────────────────────── */
/** The typed-array element kinds with a runtime representation: exactly
* the constructors real CLI code reaches (Uint8Array/Buffer, Uint32Array,
* Int32Array — the Atomics.wait sleep idiom's array — Float32Array). The
* other TypedArray flavors stay frontend-fenced. */
export type IrBytesElem = "u8" | "u32" | "i32" | "f32";
/** Typed-array element kinds with a runtime representation: Uint8Array/Buffer,
* Uint32Array, Int32Array, Float32Array, and Float64Array. Other flavors stay
* frontend-fenced. */
export type IrBytesElem = "u8" | "u32" | "i32" | "f32" | "f64";
export type IrType =
| { kind: "f64" }
@@ -72,14 +71,14 @@ export type IrType =
* elements, no map keys/values, no union arms (a regex arm would have no
* narrowing test), not JSON-safe. */
| { kind: "regex" }
/** A typed array / Node Buffer (Uint8Array, Uint32Array, Float32Array;
/** A typed array / Node Buffer (Uint8Array, Uint32Array, Float32Array,
* Float64Array;
* Buffer IS a Uint8Array subclass and shares the u8 kind) — heap,
* refcounted, MUTABLE, fixed-length, with ONE runtime representation
* (ScrBytes) that OWNS its storage: no views exist — subarray()/slice()
* both COPY (documented divergence for subarray), `.buffer`/
* `.byteOffset`/DataView are frontend-fenced. Element reads widen to
* f64; writes coerce JS-exactly (ToUint8/ToUint32 modular truncation,
* double→float rounding). OOB element access traps like arrays. Allowed
* (ScrBytes) that owns storage or borrows it for subarray and DataView
* views. Typed-array slice copies. Element reads widen to f64; writes
* coerce JS-exactly (ToUint8/ToUint32 modular truncation and f32
* rounding). OOB element access traps like arrays. Allowed
* as array elements and union arms (tag-based narrowing, like url);
* fenced out of map keys/values, set elements, and JSON. Holds only raw
* bytes — never part of a cycle, no trace. */
+12 -1
View File
@@ -26,7 +26,7 @@ static void scr_bytes_oom(void) {
}
size_t scr_bytes_elem_size(ScrBytesElem elem) {
return elem == SCR_BYTES_U8 ? 1 : 4;
return elem == SCR_BYTES_U8 ? 1 : elem == SCR_BYTES_F64 ? 8 : 4;
}
/* ── lifecycle ─────────────────────────────────────────────────────────── */
@@ -149,6 +149,11 @@ double scr_bytes_get(const ScrBytes *b, double i) {
memcpy(&v, b->data + idx * 4, 4);
return (double)v;
}
case SCR_BYTES_F64: {
double v;
memcpy(&v, b->data + idx * 8, 8);
return v;
}
case SCR_BYTES_I32: {
int32_t v;
memcpy(&v, b->data + idx * 4, 4);
@@ -174,6 +179,9 @@ void scr_bytes_set(ScrBytes *b, double i, double v) {
memcpy(b->data + idx * 4, &f, 4);
break;
}
case SCR_BYTES_F64:
memcpy(b->data + idx * 8, &v, 8);
break;
case SCR_BYTES_I32: {
/* ToInt32 is ToUint32 reinterpreted signed (same 2^32 residue). */
uint32_t u = scr_bytes_to_u32(v);
@@ -1605,6 +1613,9 @@ ScrBytes *scr_bytes_from_arr(ScrBytesElem elem, const ScrArr *arr) {
memcpy(b->data + i * 4, &f, 4);
break;
}
case SCR_BYTES_F64:
memcpy(b->data + i * 8, &v, 8);
break;
case SCR_BYTES_I32: {
uint32_t u = scr_bytes_to_u32(v);
memcpy(b->data + i * 4, &u, 4); /* same residue reinterpreted */
+1
View File
@@ -9717,6 +9717,7 @@ ScrJsval *scr_jsval_from_bytes(const ScrBytes *b) {
JSValue v = JS_NewTypedArray(isl_ctx, 3, argv,
b->elem == SCR_BYTES_U32 ? JS_TYPED_ARRAY_UINT32
: b->elem == SCR_BYTES_I32 ? JS_TYPED_ARRAY_INT32
: b->elem == SCR_BYTES_F64 ? JS_TYPED_ARRAY_FLOAT64
: JS_TYPED_ARRAY_FLOAT32);
JS_FreeValue(isl_ctx, buf);
if (JS_IsException(v)) {
+3 -2
View File
@@ -5093,7 +5093,7 @@ double scr_bit_ushr(double a, double b);
double scr_bit_not(double a);
/* ── typed arrays / Buffer (scr_bytes.c) ──────────────────────────────
* ONE runtime representation for Uint8Array/Uint32Array/Float32Array,
* ONE runtime representation for Uint8Array/Uint32Array/Float32Array/Float64Array,
* Node's Buffer (a Uint8Array subclass), and DataView: a refcounted,
* MUTABLE, fixed-length element buffer. An ScrBytes either OWNS its
* storage (backing == NULL, byteOffset 0) or is a VIEW: its `data` points
@@ -5119,6 +5119,7 @@ typedef enum ScrBytesElem {
SCR_BYTES_U32, /* Uint32Array */
SCR_BYTES_F32, /* Float32Array */
SCR_BYTES_I32, /* Int32Array (reads sign-extend; writes ToInt32-wrap) */
SCR_BYTES_F64, /* Float64Array */
} ScrBytesElem;
typedef struct ScrBytes {
@@ -5133,7 +5134,7 @@ typedef struct ScrBytes {
struct ScrBytes *backing;
} ScrBytes;
size_t scr_bytes_elem_size(ScrBytesElem elem); /* 1, 4, 4 */
size_t scr_bytes_elem_size(ScrBytesElem elem); /* 1, 4, or 8 */
/* node:string_decoder's StringDecoder (scr_bytes.c, beside the decoders
* it shares): the decoder value is a record holding the CANONICAL
+70
View File
@@ -0,0 +1,70 @@
// Float64Array keeps double precision across construction, element access,
// copies, views, DataView aliases, and typed-array methods.
const a = new Float64Array(4);
console.log("empty", a.length, a.byteLength, a.byteOffset, a[0], a[3]);
a[0] = 0.1;
a[1] = -0.25;
a[2] = 1e100;
a[3] = -0;
console.log("elements", a[0], a[1], a[2], 1 / a[3]);
const seeded = new Float64Array([Math.PI, 0.1, -1.5]);
console.log("seeded", seeded.length, seeded.byteLength, seeded[0], seeded[1], seeded[2]);
const values = [0.2, -3.25];
const fromArray = new Float64Array(values);
console.log("from-array", fromArray[0], fromArray[1]);
const copy = new Float64Array(seeded);
copy[0] = 12.5;
console.log("copy", seeded[0], copy[0]);
const bufferBacked = new Float64Array(new ArrayBuffer(16));
bufferBacked[1] = 3.25;
console.log("buffer", bufferBacked.length, bufferBacked.byteLength, bufferBacked[0], bufferBacked[1]);
const view = new DataView(bufferBacked.buffer);
view.setFloat64(0, -7.5, true);
console.log("dataview", bufferBacked[0], view.getFloat64(8, true));
const alias = Buffer.from(bufferBacked.buffer);
alias.writeDoubleLE(4.5, 8);
console.log("buffer-alias", alias.length, bufferBacked[1]);
const sub = a.subarray(1, 3);
sub[0] = 6.75;
console.log("subarray", a[1], sub[0], sub.length, sub.byteLength, sub.byteOffset);
const slice = a.slice(1, 3);
slice[0] = 9.5;
console.log("slice", a[1], slice[0], slice.byteLength);
const filled = new Float64Array(4);
filled.fill(0.1, 1, 3);
filled.set(new Float64Array([5.5, 6.25]), 2);
console.log("fill-set", filled[0], filled[1], filled[2], filled[3]);
filled.subarray(0, 2).fill(-2.5);
console.log("fill-view", filled[0], filled[1]);
const odd = new Float64Array([0 / 0, 1 / 0, -1 / 0, -0]);
console.log("special", odd[0] !== odd[0], odd[1], odd[2], 1 / odd[3]);
console.log("lengths", new Float64Array().length, new Float64Array(3.5).length, new Float64Array(0 / 0).length);
function doubleFirst(xs: Float64Array): Float64Array {
const result = new Float64Array(xs);
result[0] = xs[0] * 2;
return result;
}
const doubled = doubleFirst(seeded);
console.log("typed", doubled[0], seeded[0]);
const looped = new Float64Array(4);
let sum = 0;
for (let i = 0; i < looped.length; i++) {
looped[i] = i + 0.125;
sum += looped[i];
}
console.log("loop", sum, looped[0], looped[3]);
try {
new Float64Array(-1);
console.log("bad-length", "ok");
} catch (e) {
if (e instanceof RangeError) console.log("bad-length", e.name, e.message);
else console.log("bad-length", "unexpected");
}
+3
View File
@@ -38,6 +38,9 @@ console.log(`${hw.length}`, `${hw[1]}`, `${hw.constructor.name}`);
const floats = new Float32Array([1.5, -0.25]);
const hf: any = floats;
console.log(`${hf.length}`, `${hf[1]}`, `${hf.constructor.name}`);
const doubles = new Float64Array([Math.PI, -0.1]);
const hd: any = doubles;
console.log(`${hd.length}`, `${hd[0]}`, `${hd[1]}`, `${hd.constructor.name}`);
// A bytes-armed union, both arms.
function payload(flag: boolean): Uint8Array | string {
+1 -1
View File
@@ -39,4 +39,4 @@ dataview.ts:13:29 - error SC2020: 'new DataView over this buffer expression' is
| ^~
14 | void view;
hint: views compile over a typed array's own storage — new DataView(x.buffer, byteOffset?, byteLength?) where x is a Uint8Array/Uint32Array/Float32Array/Buffer value — or a fresh buffer erased into the view: new DataView(new ArrayBuffer(n), ...); free-standing ArrayBuffers have no representation
hint: views compile over a typed array's own storage — new DataView(x.buffer, byteOffset?, byteLength?) where x is a Uint8Array/Uint32Array/Float32Array/Float64Array/Buffer value — or a fresh buffer erased into the view: new DataView(new ArrayBuffer(n), ...); free-standing ArrayBuffers have no representation