Crossing the Rust and WebAssembly boundary

Every value this calculator passes between JavaScript and Rust is a 64-bit float. There is no integer type, no string, and no BigInt anywhere in the exported surface: the generated type declarations give every parameter and every return the TypeScript type number, which is IEEE 754 double precision. Objects do not cross at all. The Calculator is allocated inside the WebAssembly module and JavaScript holds an integer handle to it, passing that handle back as the first argument of every method call. This page describes what that costs and why the design is the way it is.

Everything is a 64-bit float

The generated declarations show the whole exported surface as numbers. Addition, subtraction, multiplication, division, square root, squaring, percentage, compound interest and factorial all take numbers and return numbers. That is not a simplification in the type file; it reflects the actual WebAssembly value type, which for all of these is f64. A JavaScript number and a Rust f64 are the same 64 bits, so nothing is converted, rounded or reinterpreted on the way across.

Factorial is the interesting case, because the underlying Rust function takes a u32 and returns a u64. The exported wrapper deliberately does not. It takes an f64, checks the value itself, and converts only once the value is known to be sane. Had it exposed the u32 directly, the conversion from a JavaScript number would have happened in the generated glue, where a negative or fractional argument is silently coerced rather than reported. The wrapper trades a free conversion for the ability to say what was wrong.

What a fallible call costs

The cost of returning a Rust Result rather than a bare value is visible in the raw module interface. Every infallible export returns a single number. Every fallible one returns three.

Those three slots are the result value, a pointer to the error, and a discriminant saying which of the two is meaningful. The generated JavaScript reads the discriminant, and if it says the call failed it takes the error pointer, turns it back into a string and throws it. That is why the page catches an exception rather than checking a return code: the Rust Result has already been unpacked into the idiom JavaScript expects by the time the calculator's own code sees it.

Three of the exported functions carry that cost and the rest do not, which lines up exactly with the four error variants documented on the errors page. An operation that cannot fail does not pay for the machinery to report a failure.

The Calculator is a handle, not an object

The constructor appears in the raw interface as a function taking nothing and returning a number. That number is a pointer into the WebAssembly module's linear memory, where the calculator's working value, memory register and history actually live. Every method takes that pointer as its first argument. The JavaScript class you import is a thin wrapper holding the pointer in a field and forwarding calls.

Because the memory belongs to the module rather than to the JavaScript heap, the garbage collector cannot reclaim it. The generated class therefore exposes a free method and implements the disposal protocol, and the module exports a matching deallocation function. The calculator page constructs exactly one Calculator for the lifetime of the page and never frees it, which is correct for a page whose entire job is to hold one calculator until the tab closes.

The one value that is not a number

There is a single exception to the everything-is-a-number rule. The history getter is typed as returning any, because it hands back a structured object rather than a scalar. That object is produced by serde-wasm-bindgen, which walks the Rust history vector and builds a plain JavaScript array of objects from it. The value travels through an external reference table that the module exports alongside its functions, rather than through the numeric stack.

This is why the crate depends on serde and serde-wasm-bindgen at all, and it is the only place either is used. Everything else is numbers, which need no serialisation. The history page covers what that structure contains and why the on-screen list differs from it.

Questions and answers

Does the calculator use BigInt for large factorials?

No. The exported factorial takes a number and returns a number, both 64-bit floats. The Rust core computes in 64-bit unsigned integers, but the value converts to a float on the way out. Every result up to 20 factorial is exactly representable as a float, so nothing is lost.

Why does the WebAssembly module return three values for division?

Because division returns a Rust Result. The three slots are the result, a pointer to the error, and a flag saying which one is valid. The generated JavaScript reads the flag and throws the error as an exception. Operations that cannot fail return a single value.

Is there a performance cost to crossing the boundary?

For this calculator the crossing is a numeric function call with no allocation and no copying, because floats pass directly. The only value that needs building is the calculation history, which is serialised into a JavaScript object on demand rather than on every operation.

Does JavaScript need to free the calculator?

The generated class provides a free method and supports the disposal protocol, because the calculator's memory lives inside the WebAssembly module and the JavaScript garbage collector cannot reclaim it. The page holds one calculator for the lifetime of the tab and does not free it.