Native performance
C# and Rust use native storage and operations. Selecting the JavaScript or Node APIs does not select a JavaScript engine or exact JavaScript behavior at any cost. Where the native contract differs, the difference is explicit below. Neither target reads uninitialized memory or discards aliasing guarantees to save work.
Strings
Ordinary C# strings use .NET UTF-16. Ordinary Rust strings use UTF-8. Rust lengths, search results, regular-expression indices and slice positions are byte offsets. Slices must start and end on character boundaries.
const text = "é😀";
const length = text.length;
const position = text.indexOf("😀");
const face = text.slice(position);
In C#, length is 3 and position is 1. In Rust, they are 6 and 2. face is the
same text in both. Rust rejects a slice that splits a UTF-8 character; it does not
silently convert the entire string to UTF-16 to make the slice possible. JSON and
Node APIs retain native strings too.
Rust String.fromCharCode and String.fromCodePoint construct Unicode scalar
values from finite integer arguments. Fractional values, surrogate code points
and values beyond 0x10FFFF reject instead of applying UTF-16 wrapping.
Use an explicit value when UTF-16 code-unit behavior is required:
import { jsstr } from "@tsonic/js/lang.js";
const text = jsstr("é😀");
const length = text.length;
length is 3 on both targets. The conversion and the exact UTF-16 operations
belong to this value, not every string in the program. A native Rust string cannot
contain lone surrogates. Parsing JSON containing one rejects; an explicit
JsString can retain it and JSON serialization escapes it.
Arrays
Ordinary arrays have contiguous initialized elements. They do not store a presence flag beside every element.
const values = new Array<number>(3);
values[1] = 7;
values.push(9);
const copy = Array.from(values);
The result is [0, 7, 0, 9], not an array containing holes. Array.from creates
independent storage and preserves the identity of reference-valued elements.
This also works when the source is a parameter; the native carrier itself is
dense. Reserving capacity in a native collection leaves its length zero and is
different from constructing an initialized length.
Omitted literal elements, deleting an existing array element, increasing
.length, and assignments that jump beyond the next element are unsupported.
Use push, splice, initialized length construction or a native indexed map.
There is no automatic switch to sparse storage. Native initialization must be
available for the selected element type; no arbitrary reference is fabricated.
Absence is an explicit element type when needed:
const values: (number | undefined)[] = [1, undefined, 3];
All three indices exist. Unmatched regular-expression captures likewise use optional values, not holes in the array representation.
Rust values that leave mutable shared storage still need a safe ownership transfer. For example, retaining an array’s string while a callback can replace that element requires an owned string. Tsonic does not hold a borrow across an arbitrary callback or silently change value semantics to avoid that copy.
Fixed-size storage
FixedArray<T, N> preserves an exact extent. It is not a universal promise of
stack allocation. C# currently uses a managed T[]; Rust uses native fixed-array
storage through its selected carrier. Changing C# to an inline value struct would
change assignment and alias behavior and needs a separate storage contract.
Use the target’s native storage APIs for allocation and lifetime controls that
are not expressed by ordinary arrays.
Requested results only
- Hash updates retain incremental hash state, not the complete message history.
- Buffer views share a backing range; explicit copies allocate independent bytes.
- Boolean regular-expression tests on proven native expressions do not build
discarded public match arrays. Unknown C# receivers retain virtual
execdispatch so an override’s observable behavior is not erased. - Number formatting writes text directly;
formatToPartscreates parts when asked. - File statistics retain a numeric snapshot; Date objects are created on access.
- Errors capture no stack until
Error.captureStackTraceis called. - Rust async filesystem operations complete on a bounded worker pool. Ready network and worker events wake the event loop rather than waiting for a fixed polling interval.
- C# HTTP clients expose headers before reading the whole body. Streaming has
bounded in-flight chunks;
readAllandreadAllBufferexplicitly request the complete body.
Costs that remain necessary
First-class callbacks retain captured state and identity. Shared mutable values retain shared ownership. Awaiting one shared promise more than once must preserve its result for each consumer; a uniquely owned result can move. These are source requirements, not optional emulation. Native direct functions, value types and explicit borrow APIs remain available where their contracts fit.
Closed C# unions use typed value storage, avoiding allocation and boxing of a primitive payload. A wide union occupies the combined inline field space, so passing a large union by value still has a native copy cost. This is not a claim that every union is free or that every program improves by the same amount.