Skip to content
Merged
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
6089db5
Add engine-native FFI (bun:ffi) support under USE(BUN_JSC_ADDITIONS)
Jarred-Sumner Jul 23, 2026
a3a2455
Fix review findings in the DFG/FTL FFI hooks and readers
Jarred-Sumner Jul 23, 2026
ca52299
FTL: inline number and typed-array-view conversion for CallFFI pointe…
Jarred-Sumner Jul 23, 2026
48250cd
JSFFIFunction: make .ptr an intrinsic property, not a putDirect
Jarred-Sumner Jul 24, 2026
596f4fc
DFG: emit FFI tail calls as plain Calls so CallFFI conversion fires i…
Jarred-Sumner Jul 24, 2026
dd034da
JSFFIFunction: also serve .native intrinsically
Jarred-Sumner Jul 24, 2026
3e6b9a9
FTL: call the native FFI target directly instead of the invoke thunk
Jarred-Sumner Jul 24, 2026
2197550
FTL: template compileCallFFI on the direct-call decision
Jarred-Sumner Jul 24, 2026
6d6505b
FTL: keep the invoke thunk for FFI calls on Windows
Jarred-Sumner Jul 24, 2026
42face0
Remove the bun:ffi raw-memory readers (read.*) and the FFIRawRead node
Jarred-Sumner Jul 24, 2026
b248749
Restore case chains clobbered while removing FFIRawRead
Jarred-Sumner Jul 24, 2026
6d2fe0b
Review fixes: CallSiteIndex on FTL call paths, no NULL fn ptrs, gated…
Jarred-Sumner Jul 25, 2026
feea5e1
FFI: napi purge, owner cells + call hooks, single JSFFICallback with …
Jarred-Sumner Jul 25, 2026
87658cf
Remove the dead functionFFIReadObject declaration left by the reader …
Jarred-Sumner Jul 26, 2026
aa79ce5
FTL: reload UntypedUse int32 FFI args as Int32; drain-loop exception …
Jarred-Sumner Jul 26, 2026
b67425c
FFI: exact pointer boxing for .ptr; $vm reuses FFI::typeFromJS
Jarred-Sumner Jul 26, 2026
3da2d3c
JSFFIFunction: enumerate the intrinsic ptr/native own properties
Jarred-Sumner Jul 26, 2026
ac08a0a
$vm drain: retire un-run threadsafe records on the exception path
Jarred-Sumner Jul 26, 2026
7bd52e0
FFI review follow-ups: pointer bound, failing test can fail, stale co…
Jarred-Sumner Jul 26, 2026
2007dd8
FFI threadsafe: deliver queued invocations after close()
Jarred-Sumner Jul 26, 2026
30fb0a0
Threadsafe test + comments: assert post-close delivery and the pendin…
Jarred-Sumner Jul 26, 2026
3e2fa28
JSGlobalObject: expose the barrier'd String constructor
Jarred-Sumner Jul 26, 2026
aa47e4c
FFIConversions.h: align the pointer-cutoff doc comment to 2^53-1
Jarred-Sumner Jul 26, 2026
d15626a
FFI: add the buffer_length argument type
Jarred-Sumner Jul 26, 2026
02bc3fb
JSFFIFunction: honor ReadOnly/DontDelete on the intrinsic ptr/native
Jarred-Sumner Jul 26, 2026
2357e8a
FFI review follow-ups: IWYU, stale comments, redundant exception check
Jarred-Sumner Jul 26, 2026
8ae9533
JSFFIFunction: order intrinsic keys per OrdinaryOwnPropertyKeys
Jarred-Sumner Jul 26, 2026
3e8a9c4
JSFFIFunction: defineOwnProperty for intrinsics; include order; comment
Jarred-Sumner Jul 27, 2026
5a3c8aa
JSFFIFunction: make ptr/native real own properties on a pre-built str…
Jarred-Sumner Jul 27, 2026
54f9cc8
FFICallHost.cpp: move TopExceptionScope.h into the guarded include block
Jarred-Sumner Jul 27, 2026
ce8686f
Cleanup: duplicated word in FFIConversions.h; stray blank lines in JS…
Jarred-Sumner Jul 27, 2026
ea0b3e8
FFI: strip code comments; copyright holder is Anthropic PBC
Jarred-Sumner Jul 27, 2026
4877f22
Intrinsic.h: drop empty macro continuation lines
Jarred-Sumner Jul 27, 2026
4e555e2
FFI: specialize the callback dispatch by threadsafe-ness at thunk gen…
Jarred-Sumner Jul 27, 2026
b1c78f8
FFI: strip remaining comments; rename the jsvalue test; drop fossils
Jarred-Sumner Jul 27, 2026
f91318c
FFI: call-scoped arena; cstring decodes to a string; callback returns…
Jarred-Sumner Jul 27, 2026
bbf3d80
FFI: delete verified-dead symbols
Jarred-Sumner Jul 27, 2026
a60d157
FFI: review fixes -- arena exit on the exception path, cstring specul…
Jarred-Sumner Jul 27, 2026
a862d95
Intrinsic.h: restore the five list separators dropped by the comment …
Jarred-Sumner Jul 27, 2026
1bb3191
FFI review: upstream-clean shared files, dead stringConstructor(), st…
Jarred-Sumner Jul 27, 2026
de5b6c5
FFI review: narrow the arena bracket to cstring, clear upstream lefto…
Jarred-Sumner Jul 27, 2026
d67e864
FFI: remove three orphaned single-line comment fragments
Jarred-Sumner Jul 27, 2026
5adc9e3
FFI: check-webkit-style brace fixes
Jarred-Sumner Jul 27, 2026
a554d31
FFI: remaining check-webkit-style hits in compileCallFFIImpl and headers
Jarred-Sumner Jul 28, 2026
131d8a8
Build and ship testFFI in every artifact's bin/
Jarred-Sumner Jul 28, 2026
60dd66e
Preview releases: attach standalone testFFI-<platform> assets
Jarred-Sumner Jul 28, 2026
48d3b44
Revert standalone testFFI-<platform> release assets
Jarred-Sumner Jul 28, 2026
e553887
shell/CMakeLists: drop the stale --target jsc parenthetical
Jarred-Sumner Jul 28, 2026
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
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ RUN --mount=type=tmpfs,target=/webkitbuild \
-G Ninja \
/webkit && \
cd /webkitbuild && \
cmake --build /webkitbuild --config $WEBKIT_RELEASE_TYPE --target "jsc" && \
cmake --build /webkitbuild --config $WEBKIT_RELEASE_TYPE --target "jsc" --target "testFFI" && \
python3 /webkit/Tools/Scripts/check-classinfo-uniqueness.py $WEBKIT_OUT_DIR/bin/jsc && \
cp -r $WEBKIT_OUT_DIR/lib/*.a /output/lib && \
cp $WEBKIT_OUT_DIR/*.h /output/include && \
Expand Down
3 changes: 2 additions & 1 deletion Dockerfile.android
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,11 @@ RUN --mount=type=tmpfs,target=/webkitbuild \
-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=BOTH \
-G Ninja \
/webkit && \
cmake --build /webkitbuild --config ${WEBKIT_RELEASE_TYPE} --target jsc && \
cmake --build /webkitbuild --config ${WEBKIT_RELEASE_TYPE} --target jsc --target testFFI && \
python3 /webkit/Tools/Scripts/check-classinfo-uniqueness.py $WEBKIT_OUT_DIR/bin/jsc && \
cp -r $WEBKIT_OUT_DIR/lib/*.a /output/lib && \
cp $WEBKIT_OUT_DIR/*.h /output/include && \
cp -r $WEBKIT_OUT_DIR/bin /output/bin && \
cp $WEBKIT_OUT_DIR/*.json /output && \
find $WEBKIT_OUT_DIR/JavaScriptCore/DerivedSources/ -name "*.h" -exec sh -c 'cp "$1" "/output/include/JavaScriptCore/$(basename "$1")"' sh {} \; && \
find $WEBKIT_OUT_DIR/JavaScriptCore/DerivedSources/ -name "*.json" -exec sh -c 'cp "$1" "/output/$(basename "$1")"' sh {} \; && \
Expand Down
3 changes: 2 additions & 1 deletion Dockerfile.freebsd
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,11 @@ RUN --mount=type=tmpfs,target=/webkitbuild \
-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=BOTH \
-G Ninja \
/webkit && \
cmake --build /webkitbuild --config ${WEBKIT_RELEASE_TYPE} --target jsc && \
cmake --build /webkitbuild --config ${WEBKIT_RELEASE_TYPE} --target jsc --target testFFI && \
python3 /webkit/Tools/Scripts/check-classinfo-uniqueness.py $WEBKIT_OUT_DIR/bin/jsc && \
cp -r $WEBKIT_OUT_DIR/lib/*.a /output/lib && \
cp $WEBKIT_OUT_DIR/*.h /output/include && \
cp -r $WEBKIT_OUT_DIR/bin /output/bin && \
cp $WEBKIT_OUT_DIR/*.json /output && \
find $WEBKIT_OUT_DIR/JavaScriptCore/DerivedSources/ -name "*.h" -exec sh -c 'cp "$1" "/output/include/JavaScriptCore/$(basename "$1")"' sh {} \; && \
find $WEBKIT_OUT_DIR/JavaScriptCore/DerivedSources/ -name "*.json" -exec sh -c 'cp "$1" "/output/$(basename "$1")"' sh {} \; && \
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.macos
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ RUN --mount=type=tmpfs,target=/webkitbuild \
-DCMAKE_CXX_FLAGS="$CXXFLAGS" \
-G Ninja \
/webkit && \
cmake --build /webkitbuild --config ${WEBKIT_RELEASE_TYPE} --target jsc && \
cmake --build /webkitbuild --config ${WEBKIT_RELEASE_TYPE} --target jsc --target testFFI && \
python3 /webkit/Tools/Scripts/check-classinfo-uniqueness.py $WEBKIT_OUT_DIR/bin/jsc && \
# -L everywhere: WebKit's cmake stages the WTF/bmalloc framework headers
# as *symlinks* into the source tree when the host filesystem supports
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.musl
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ RUN --mount=type=tmpfs,target=/webkitbuild \
-G Ninja \
/webkit && \
cd /webkitbuild && \
cmake --build /webkitbuild --config ${WEBKIT_RELEASE_TYPE} --target "jsc" && \
cmake --build /webkitbuild --config ${WEBKIT_RELEASE_TYPE} --target "jsc" --target "testFFI" && \
python3 /webkit/Tools/Scripts/check-classinfo-uniqueness.py $WEBKIT_OUT_DIR/bin/jsc && \
cp -r $WEBKIT_OUT_DIR/lib/*.a /output/lib && \
cp $WEBKIT_OUT_DIR/*.h /output/include && \
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.windows
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ RUN --mount=type=tmpfs,target=/webkitbuild \
"-DCMAKE_CXX_FLAGS_DEBUG=/FS /O0 /Ob0" \
-G Ninja \
/webkit && \
cmake --build /webkitbuild --config ${WEBKIT_RELEASE_TYPE} --target jsc && \
cmake --build /webkitbuild --config ${WEBKIT_RELEASE_TYPE} --target jsc --target testFFI && \
\
mkdir -p /output/lib /output/include/JavaScriptCore /output/include/wtf /output/include/bmalloc && \
cp -rL $WEBKIT_OUT_DIR/lib/* /output/lib/ && \
Expand Down
69 changes: 69 additions & 0 deletions JSTests/stress/ffi-align.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//@ requireOptions("--useDollarVM=1")

// Stack-alignment probes (SPEC section 11.1): each fixture performs an
// aligned 16-byte vector access on a 16-byte-aligned local, which faults if
// the FFI caller (host path, IC stub, DFG/FTL CallFFI, or the callback thunk
// on the way back into native code) mis-aligned the stack. Both probes must
// return exactly 1.0 in every tier.

function main() {
const fixture = name => $vm.ffiFixture(name);
const probe0 = $vm.ffiFunction({ args: [], returns: "f64" }, fixture("ffi_align_probe_0"), "ffi_align_probe_0");
const probe9 = $vm.ffiFunction({ args: new Array(9).fill("i32"), returns: "f64" }, fixture("ffi_align_probe_9"), "ffi_align_probe_9");
const callCbVoid = $vm.ffiFunction({ args: ["function"], returns: "void" }, fixture("ffi_call_cb_void"), "ffi_call_cb_void");
const callCbI32 = $vm.ffiFunction({ args: ["function", "i32"], returns: "i32" }, fixture("ffi_call_cb_i32"), "ffi_call_cb_i32");

if (probe0() !== 1)
throw new Error("ffi_align_probe_0 cold: " + probe0());
if (probe9(1, 2, 3, 4, 5, 6, 7, 8, 9) !== 1)
throw new Error("ffi_align_probe_9 cold");
// Missing / extra JS arguments must not change the call frame layout.
if (probe9(1, 2, 3) !== 1)
throw new Error("ffi_align_probe_9 with missing arguments");
if (probe9(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11) !== 1)
throw new Error("ffi_align_probe_9 with extra arguments");
if (probe0(1) !== 1)
throw new Error("ffi_align_probe_0 with an extra argument");

// Hot: every tier's call path must keep 16-byte alignment.
for (let i = 0; i < 3e4; ++i) {
if (probe0() !== 1)
throw new Error("ffi_align_probe_0 hot iteration " + i);
}
for (let i = 0; i < 3e4; ++i) {
if (probe9(i, -i, i, -i, i, -i, i, -i, i) !== 1)
throw new Error("ffi_align_probe_9 hot iteration " + i);
}
// Mixed argument shapes (int32 and double) at the same call site.
for (let i = 0; i < 1e4; ++i) {
if (probe9(i + 0.5, 1, 2, 3, 4, 5, 6, 7, 8) !== 1)
throw new Error("ffi_align_probe_9 double first argument iteration " + i);
}

// Alignment on the way back out: a callback that runs the probes from
// inside the native -> JS -> native sandwich.
const cbProbe = $vm.ffiCallback({ args: [], returns: "void" }, () => {
if (probe0() !== 1)
throw new Error("probe0 inside callback");
if (probe9(9, 8, 7, 6, 5, 4, 3, 2, 1) !== 1)
throw new Error("probe9 inside callback");
});
for (let i = 0; i < 3000; ++i)
callCbVoid(cbProbe);

// Nested: FFI -> callback -> FFI -> callback -> probe, to depth 20.
const nestCb = $vm.ffiCallback({ args: ["i32"], returns: "i32" }, depth => {
if (probe0() !== 1)
throw new Error("probe0 at depth " + depth);
if (depth <= 0)
return 0;
return callCbI32(nestCb, depth - 1) + 1;
});
for (let i = 0; i < 200; ++i) {
if (callCbI32(nestCb, 20) !== 20)
throw new Error("nested alignment ladder iteration " + i);
}
}

if ($vm.useJIT())
main();
41 changes: 41 additions & 0 deletions JSTests/stress/ffi-arena-depth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//@ requireOptions("--useDollarVM=1")

function main() {
const fixture = name => $vm.ffiFixture(name);
const depth = () => $vm.ffiArenaDepth();

if (depth() !== 0)
throw new Error("arena depth should start at 0, got " + depth());

const strlen = $vm.ffiFunction({ args: ["cstring"], returns: "u64" }, fixture("ffi_strlen"), "ffi_strlen");
for (let i = 0; i < 2e4; ++i) {
strlen("call " + (i & 7));
if (depth() !== 0)
throw new Error("arena depth leaked after a normal call at iteration " + i + ": " + depth());
}

let calls = 0;
const callback = $vm.ffiCallback({ args: [], returns: "cstring" }, () => {
++calls;
throw new Error("thrown from callback");
});
const callThrough = $vm.ffiFunction({ args: ["ptr"], returns: "cstring" }, fixture("ffi_call_cb_ret_cstring"), "ffi_call_cb_ret_cstring");
for (let i = 0; i < 2e4; ++i) {
let threw = false;
try {
callThrough(callback.ptr);
} catch (e) {
threw = e instanceof Error && e.message === "thrown from callback";
}
if (!threw)
throw new Error("expected the callback exception to propagate at iteration " + i);
if (depth() !== 0)
throw new Error("arena depth leaked after a throwing cstring call at iteration " + i + ": " + depth());
}
callback.close();
if (calls !== 2e4)
throw new Error("callback ran " + calls + " times, expected 20000");
}

if ($vm.useJIT())
main();
208 changes: 208 additions & 0 deletions JSTests/stress/ffi-arity-ladders.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
//@ requireOptions("--useDollarVM=1")

// Arity ladders that straddle every register->stack boundary of the
// supported ABIs, plus the interleaved ffi_mix_* fixtures. Each mix returns
// the position-weighted checksum sum((k + 1) * arg_k), so any argument that
// lands in the wrong register or stack slot changes the result.

function check(actual, expected, message) {
if (!Object.is(actual, expected))
throw new Error(message + ": expected " + String(expected) + " but got " + String(actual));
}

function main() {
const fixture = name => $vm.ffiFixture(name);
const bind = (name, args, ret) => $vm.ffiFunction({ args, returns: ret }, fixture(name), name);

// Deterministic PRNG (mulberry32).
let seed = 0x1abe11ed;
function random() {
seed = (seed + 0x6D2B79F5) | 0;
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
}
const randomInt32 = () => (Math.floor(random() * 4294967296) | 0);
const randomSmall = () => Math.floor(random() * 2001) - 1000;

// ---- ffi_sum_i32_<n>: int64 sums returned as BigInt.
const sumI32Arities = [0, 1, 2, 4, 6, 7, 8, 9, 12, 16];
const sumI32 = new Map();
for (const n of sumI32Arities)
sumI32.set(n, bind("ffi_sum_i32_" + n, new Array(n).fill("i32"), "i64"));

function checkSumI32(n, values) {
let expected = 0n;
for (const v of values)
expected += BigInt(v | 0);
const actual = sumI32.get(n)(...values);
check(actual, expected, "ffi_sum_i32_" + n + "(" + values.join(",") + ")");
}
for (const n of sumI32Arities) {
checkSumI32(n, new Array(n).fill(0));
checkSumI32(n, new Array(n).fill(-1));
checkSumI32(n, new Array(n).fill(2147483647));
checkSumI32(n, new Array(n).fill(-2147483648));
// Distinct powers of two: catches any duplicated or swapped argument.
checkSumI32(n, new Array(n).fill(0).map((_, i) => (i % 2 ? -1 : 1) * (1 << (i + 5))));
for (let iteration = 0; iteration < 30; ++iteration)
checkSumI32(n, new Array(n).fill(0).map(() => randomInt32()));
}

// ---- ffi_sum_f64_<n>: double sums.
const sumF64Arities = [1, 2, 7, 8, 9, 12];
const sumF64 = new Map();
for (const n of sumF64Arities)
sumF64.set(n, bind("ffi_sum_f64_" + n, new Array(n).fill("f64"), "f64"));
function checkSumF64(n, values) {
let expected = 0;
for (const v of values)
expected += v; // same left-to-right order as the fixture
const actual = sumF64.get(n)(...values);
check(actual, expected, "ffi_sum_f64_" + n + "(" + values.join(",") + ")");
}
for (const n of sumF64Arities) {
checkSumF64(n, new Array(n).fill(0));
checkSumF64(n, new Array(n).fill(-0.5));
checkSumF64(n, new Array(n).fill(0).map((_, i) => 1 / (1 << i))); // exact binary fractions
checkSumF64(n, new Array(n).fill(0).map((_, i) => (i % 2 ? -1 : 1) * 2 ** (i * 4)));
for (let iteration = 0; iteration < 30; ++iteration)
checkSumF64(n, new Array(n).fill(0).map(() => randomSmall() * 2 ** (Math.floor(random() * 60) - 30)));
}

// ---- Sub-8-byte stack ladders (Apple arm64 packing).
const sumU8_10 = bind("ffi_sum_u8_10", new Array(10).fill("u8"), "i64");
const sumU8_12 = bind("ffi_sum_u8_12", new Array(12).fill("u8"), "i64");
const sumI16_10 = bind("ffi_sum_i16_10", new Array(10).fill("i16"), "i64");
const sumI16_12 = bind("ffi_sum_i16_12", new Array(12).fill("i16"), "i64");
function checkSubword(fn, name, values, widthMask, signed) {
let expected = 0n;
for (const v of values) {
let w = (v | 0) & widthMask;
if (signed && (w & ((widthMask + 1) >>> 1)))
w -= widthMask + 1;
expected += BigInt(w);
}
check(fn(...values), expected, name + "(" + values.join(",") + ")");
}
for (const [fn, name, n, mask, signed] of [
[sumU8_10, "ffi_sum_u8_10", 10, 0xff, false],
[sumU8_12, "ffi_sum_u8_12", 12, 0xff, false],
[sumI16_10, "ffi_sum_i16_10", 10, 0xffff, true],
[sumI16_12, "ffi_sum_i16_12", 12, 0xffff, true],
]) {
checkSubword(fn, name, new Array(n).fill(0), mask, signed);
checkSubword(fn, name, new Array(n).fill(-1), mask, signed); // 255 / -1
checkSubword(fn, name, new Array(n).fill(0).map((_, i) => 1 << i), mask, signed); // distinct powers of two
checkSubword(fn, name, new Array(n).fill(0).map((_, i) => i + 1), mask, signed);
checkSubword(fn, name, new Array(n).fill(mask), mask, signed);
checkSubword(fn, name, new Array(n).fill((mask + 1) >>> 1), mask, signed); // sign bit
for (let iteration = 0; iteration < 30; ++iteration)
checkSubword(fn, name, new Array(n).fill(0).map(() => randomInt32()), mask, signed);
}

// ---- Mixes. checksum = sum (k + 1) * cast(arg_k)
const mix1 = bind("ffi_mix_1", ["i32", "f64", "i64", "f32", "ptr", "u8", "f64", "i16", "f64", "i32"], "f64");
const mix2 = bind("ffi_mix_2", ["f32", "i32", "f32", "i32", "f32", "i32", "f32", "i32", "f32", "i32"], "f64");
const mix3 = bind("ffi_mix_3", ["f64", "f64", "f64", "f64", "f64", "f64", "f64", "f64", "i32"], "f64");
const mix4 = bind("ffi_mix_4", ["i64", "i64", "i64", "i64", "i64", "i64", "f64", "i64", "f64"], "f64");
const mix5 = bind("ffi_mix_5", ["u8", "i8", "u16", "i16", "u32", "i32", "u64", "i64"], "f64");
const mix6 = bind("ffi_mix_6", ["bool", "bool", "i32", "bool", "f64", "bool", "f32", "bool", "bool", "bool", "bool", "bool", "bool"], "f64");
const mix7 = bind("ffi_mix_7", ["ptr", "char", "ptr", "char", "ptr", "char", "ptr", "char", "ptr", "char"], "f64");
const mix8 = bind("ffi_mix_8", ["f32", "f64", "f32", "f64", "f32", "f64", "f32", "f64", "f32", "f64", "f32", "f64"], "f64");

// JS reference of the C casts used by the fixtures.
const castByType = {
"i32": v => v | 0,
"f64": v => +v,
"i64": v => Number(BigInt.asIntN(64, BigInt(Math.trunc(v)))),
"f32": v => Math.fround(v),
"ptr": v => Math.trunc(v), // small non-negative pointers only
"u8": v => (v | 0) & 0xff,
"i16": v => ((v | 0) << 16) >> 16,
"i8": v => ((v | 0) << 24) >> 24,
"char": v => ((v | 0) << 24) >> 24,
"u16": v => (v | 0) & 0xffff,
"u32": v => (v | 0) >>> 0,
"u64": v => Number(BigInt.asUintN(64, BigInt(Math.trunc(v)))),
"bool": v => (v ? 1 : 0),
};
function checksum(types, values) {
let sum = 0;
for (let k = 0; k < types.length; ++k)
sum += (k + 1) * castByType[types[k]](values[k]);
return sum;
}
const mixes = [
[mix1, "ffi_mix_1", ["i32", "f64", "i64", "f32", "ptr", "u8", "f64", "i16", "f64", "i32"]],
[mix2, "ffi_mix_2", ["f32", "i32", "f32", "i32", "f32", "i32", "f32", "i32", "f32", "i32"]],
[mix3, "ffi_mix_3", ["f64", "f64", "f64", "f64", "f64", "f64", "f64", "f64", "i32"]],
[mix4, "ffi_mix_4", ["i64", "i64", "i64", "i64", "i64", "i64", "f64", "i64", "f64"]],
[mix5, "ffi_mix_5", ["u8", "i8", "u16", "i16", "u32", "i32", "u64", "i64"]],
[mix6, "ffi_mix_6", ["bool", "bool", "i32", "bool", "f64", "bool", "f32", "bool", "bool", "bool", "bool", "bool", "bool"]],
[mix7, "ffi_mix_7", ["ptr", "char", "ptr", "char", "ptr", "char", "ptr", "char", "ptr", "char"]],
[mix8, "ffi_mix_8", ["f32", "f64", "f32", "f64", "f32", "f64", "f32", "f64", "f32", "f64", "f32", "f64"]],
];
// Value generators per type. All chosen so that the checksum arithmetic
// is exact in double (weights <= 13, magnitudes <= 2^40).
const generatorByType = {
"i32": () => randomSmall() * 65536 + Math.floor(random() * 65536),
"f64": () => randomSmall() / 8,
"i64": () => randomSmall() * 1048576,
"f32": () => Math.fround(randomSmall() / 16),
"ptr": () => Math.floor(random() * 65536) * 8,
"u8": () => Math.floor(random() * 512) - 128,
"i16": () => Math.floor(random() * 200000) - 100000,
"i8": () => Math.floor(random() * 512) - 256,
"char": () => Math.floor(random() * 512) - 256,
"u16": () => Math.floor(random() * 200000) - 100000,
"u32": () => Math.floor(random() * 4294967296) - 2147483648,
"u64": () => Math.floor(random() * 65536),
"bool": () => [0, 1, 2, -1, 0.5, 0, 1][Math.floor(random() * 7)],
};
for (const [fn, name, types] of mixes) {
// Distinct-position probe: 1 at each position in turn.
for (let k = 0; k < types.length; ++k) {
const values = types.map((_, i) => (i === k ? 1 : 0));
check(fn(...values), checksum(types, values), name + " unit vector at " + k);
}
// All-ones and per-type extremes.
check(fn(...types.map(() => 1)), checksum(types, types.map(() => 1)), name + " all ones");
for (let iteration = 0; iteration < 200; ++iteration) {
const values = types.map(t => generatorByType[t]());
check(fn(...values), checksum(types, values), name + " random iteration " + iteration + " (" + values.join(",") + ")");
}
}

// ---- Hot loops so the ladders are also driven through the JIT tiers.
// Every hot call site below is exact-arity, monomorphic and non-spread so
// it can become a typed CallFFI node (SPEC section 10.2); a spread call
// (CallVarargs) is never converted.
const nine = sumI32.get(9);
for (let i = 0; i < 4e4; ++i) {
const r = nine(1, -2, 3, -4, 5, -6, 7, -8, 100000);
if (r !== 99996n)
throw new Error("ffi_sum_i32_9 hot iteration " + i + " got " + r);
}
// 2^52 keeps every weighted product exactly representable, so FMA
// contraction inside the C fixture cannot change the result.
const mixValues = [7, 1.5, 4503599627370496, 2.5, 4096, 250, -3.25, -1234, 8.75, -99];
const mixTypes = mixes[0][2];
const mixExpected = checksum(mixTypes, mixValues);
for (let i = 0; i < 4e4; ++i) {
const r = mix1(7, 1.5, 4503599627370496, 2.5, 4096, 250, -3.25, -1234, 8.75, -99);
if (r !== mixExpected)
throw new Error("ffi_mix_1 hot iteration " + i + " got " + r + " expected " + mixExpected);
}
const mix8Values = mixes[7][2].map((t, i) => (t === "f32" ? Math.fround(i + 0.5) : -(i + 0.25)));
const mix8Expected = checksum(mixes[7][2], mix8Values);
const [m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11] = mix8Values;
for (let i = 0; i < 4e4; ++i) {
const r = mix8(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11);
if (r !== mix8Expected)
throw new Error("ffi_mix_8 hot iteration " + i + " got " + r + " expected " + mix8Expected);
}
}

if ($vm.useJIT())
main();
Loading
Loading