At the bottom of every numerical program are bits in memory. A type tells the runtime how to interpret them:
bits in memory type value
1111 1111 + uint8 = 255
1111 1111 + int8 = -1Fixed-width integers use those bits for exact whole numbers within a fixed range. They are useful for identifiers, counters, image channels, packed data, file formats, and hardware interfaces.
uint8 8 bits 0 ───────────────────────── 255
int16 16 bits -32,768 ────────────────────── 32,767
uint64 64 bits 0 ───────────── 18,446,744,073,709,551,615A 64-bit floating-point double represents fractions and a much wider range of magnitudes. The tradeoff is precision: it cannot represent every 64-bit integer. Above 2^53, distinct whole numbers can produce the same stored value.
exact uint64 values stored as double
9,007,199,254,740,992 ───────────────┐
├──▶ 9,007,199,254,740,992
9,007,199,254,740,993 ───────────────┘Converting that double back to uint64 cannot recover the missing distinction.
This comes up often in MATLAB-syntax code. Ordinary numeric literals and arrays are double by default, but the language also defines eight fixed-width integer classes. Supporting those classes requires the runtime to preserve the stored value and its class everywhere the value travels.
Extending integer support beyond MATLAB
MATLAB defines eight integer classes, but integer values still cannot be used across large parts of its documented builtin surface. Support is decided separately for each function and argument. Some builtins accept all eight classes as data; others accept integers only for a dimension, index, or option; many numerical functions continue to document their data inputs as floating-point only.
RunMat now supports int8, int16, int32, int64, uint8, uint16, uint32, and uint64 throughout the runtime. MATLAB-syntax programs can construct and compute with integer arrays, use them in indexing and assignment, compile code that contains them, save and load them, and run supported operations on a GPU. RunMat also extends integer support to hundreds of builtin forms and operations that MATLAB does not yet support integer inputs for.
The integer capability audit covered:
1,476 integer-related forms audited across 787 builtins
632 forms extended beyond MATLAB
379 builtins with at least one extended integer form
43% of the audited surface includes a RunMat extensionIn these forms, a program does not need double(...) solely to satisfy an input contract. Sensor readings can enter a statistical function directly, and dimensions or indices can be decoded from their integer class without first becoming floating point.
For example, integer observations can go directly into additional statistical functions.
In MATLAB, the integer array must first be converted explicitly:
samples = uint16(readmatrix("sensor-readings.csv"));
R = corr(double(samples));This is because the corr function in MATLAB does not support uint16 inputs. In RunMat, typed integer classes are now preserved through the compilation pipeline:
samples = uint16(readmatrix("sensor-readings.csv"));
R = corr(samples); % floating-point result, as expectedThe input remains a uint16 array until corr begins the floating-point calculation. Correlation still returns double; the conversion happens where the calculation requires it. We added 327 forms of this kind across statistics, probability, signal processing, transforms, interpolation, optimization, and other numerical areas.
Another 222 forms use integers as exact controls: dimensions, counts, indices, groups, flags, weights, and options. These values are decoded directly from their integer type and range-checked. They do not take a detour through double, which matters for wide identifiers and values above flintmax.
The remaining 83 forms preserve integers through exact operations or add behavior specific to a builtin. RunMat can store integer payloads in sparse matrices, accept integer coordinates in graphics, provide additional type selection in randperm, support "like" and complex-axis forms in meshgrid, and persist typed values through the transactional data.* APIs.
To use RunMat's extended integer forms, keep the default mode in runmat.toml:
[runtime.language]
compat = "runmat"This enables all 632 extended forms, including integer data in additional numerical functions, exact integer controls, and RunMat-only typed storage features. The per-form capability catalog records which classes each form accepts and what it does with them.
To restrict a project to the documented MATLAB subset, change the same setting:
[runtime.language]
compat = "matlab"In matlab mode, RunMat rejects extension-only calls and uses MATLAB-oriented error identifiers where supported. The setting does not change the array representation or disable transparent runtime optimizations; it controls which language and builtin forms the program may use.
That arrangement keeps compatibility useful without making MATLAB's current builtin coverage the ceiling for RunMat. Existing scripts have a conservative mode, and new programs get the larger integer API automatically.

Numerical classes stay explicit in RunMat
A numerical class defines how a value is represented and how operations behave: its precision and range, whether arithmetic saturates or wraps, which inputs a function accepts, and what class the function returns. Erasing that class inside the runtime makes those rules impossible to apply consistently.
RunMat keeps the class of every numeric value explicit. Reshaping, indexing, assignment, concatenation, compilation, persistence, and supported device transfers preserve it. A class changes only when the program requests a conversion or when a function reaches a documented mathematical boundary with a different output class.
uint64 array ── reshape / index / save / GPU ──▶ uint64 array
uint64 array ── double(...) ──────────────────▶ double array
uint16 data ── corr(...) ────────────────────▶ double resultWide integers make the cost of losing the class easy to see. These two uint64 values cannot pass through double exactly:
ids = uint64([0x0020000000000001u64, 0xFFFFFFFFFFFFFFFFu64]);
ids = reshape(ids, 1, 2);
reversed = ids(:, [2, 1]);
class(reversed) % uint64RunMat keeps one native u64 payload for this array, so reversing it changes only the element order. The same principle applies to double, single, and all eight fixed-width integer classes, including supported sparse and complex representations.
The mathematical boundary is part of each builtin's contract. Integer multiplication follows MATLAB's saturating fixed-width rules and retains its integer class. corr(uint16_data) accepts exact integer observations in RunMat mode, converts when the floating-point statistical calculation begins, and returns double. An explicit double(values) converts where it appears in the source. Unsupported combinations return an error instead of triggering an implicit fallback conversion.
How other runtimes and languages do it
Overflow behavior differs enough between languages to change program results. MATLAB and RunMat use saturating fixed-width arithmetic: a value that exceeds its class stops at the class boundary. Julia's fixed-width integers and unsigned C and C++ integers use modular arithmetic instead, so the value wraps. Julia also provides BigInt when a calculation needs arbitrary precision.
Python makes arbitrary precision the default for scalar int, while NumPy arrays use fixed-width dtypes with their own promotion and overflow rules. These models are all reasonable, but code that depends on one of them cannot assume another runtime will make the same choice. RunMat keeps MATLAB's fixed-width classes and saturating behavior while extending the set of builtin forms that accept them.
The relevant rules are described in MATLAB's integer arithmetic, Julia's integer documentation, Python's numeric types, NumPy's overflow guide, and the C++ working draft's fundamental integer rules.
One representation for every numeric array
An integer array should not become approximate because it was compiled, sent to a GPU, written to a file, or passed through a builtin. RunMat enforces that rule with one typed storage model. A real numeric array owns one variant of an exhaustive tagged union:
enum NumericStorage {
F64(Vec<f64>),
F32(Vec<f32>),
I8(Vec<i8>),
I16(Vec<i16>),
I32(Vec<i32>),
I64(Vec<i64>),
U8(Vec<u8>),
U16(Vec<u16>),
U32(Vec<u32>),
U64(Vec<u64>),
}Dense tensors keep this payload private. Sparse tensors retain their CSC indices and store one native value class or a logical pattern. Complex tensors use native double, native single, or matching integer types for their real and imaginary components. The array's dtype comes from its storage variant, which prevents a type label from disagreeing with the underlying bytes. The authoritative-storage migration removed the old public payload and dtype fields once their consumers had moved to typed access.
During the migration, we kept the old floating copy beside the typed payload as temporary scaffolding. This let us move existing consumers over gradually across the integration history instead of changing the whole runtime at once.
Every conversion now appears at the operation that needs it. A builtin can work directly with native storage, apply a documented class conversion, convert at the start of a floating-point algorithm, decode a range-checked structural value, use a backend encoding that retains all bits, or reject the class. That makes conversions reviewable and testable.
Using Rust to cover the runtime
Typed storage changed code throughout the workspace. Builtins were only one part of the affected surface; integer values also pass through the VM, indexing, compiled execution, persistence, plotting, acceleration providers, WGPU, WebAssembly, tests, and fixtures.
We used Rust's compiler to enumerate the direct dependencies on the previous representation. After adding typed storage and views, we made the old tensor fields private. Every remaining access became a compiler error. This was instrumental because the representation touched virtually the whole runtime. We fixed those sites for dense, sparse, and complex values, then kept source-level checks to prevent the old access patterns from returning.
Numeric dispatch uses exhaustive match expressions at the core storage points. RunMat does not allow wildcard arms there, so a new storage variant forces the compiler to identify every match that needs another case. Rust's match documentation describes this exhaustiveness check.
Compiler coverage and semantic coverage solve different problems. The compiler verifies that code handles I64; a test must verify that I64 multiplication saturates correctly, that an empty row keeps its dimensions, or that a provider returns a buffer owned by the expected device. Compiler errors gave us a complete map of representation-dependent code, and conformance tests checked the behavior at those sites.
Integers on the GPU and in files
RunMat's acceleration interface uses the same element types as host storage. Transfers distinguish double, single, and all eight integer classes, including supported complex layouts. Each WGPU buffer records its physical element type.
On WGPU, each int64 or uint64 element occupies two u32 words, low word followed by high word. The integer shaders implement wide addition, subtraction, division, remainder, and saturating multiplication on those word pairs. They do not route the values through floating point. Supported kernels can therefore keep wide integers resident even when the shader language does not provide the required native 64-bit arithmetic. The shader implementation contains the carry, borrow, overflow, and saturation paths.
GPU handles carry their type, layout, provider owner, device, and placement origin. Automatically offloaded work can request a structured gather and retry when a compatible operation needs the host. An array created explicitly with gpuArray retains that device intent and does not fall back without the program asking for a gather. GPU coverage remains operation-specific; the generated capability catalog records host-and-GPU forms separately from gather fallbacks, host-only forms, and GPU restrictions.
The same typed values reach persistence APIs. Level 5 MAT files preserve supported integer classes and shapes. Binary fread and fwrite support integer precisions and endian modes without passing the payload through binary64. RunMat DataArrays retain typed values through full, chunked, sliced, transactional, and replay operations; the public DataArray persistence tests exercise wide values and storage boundaries.
Format limits still apply. HDF5 real integer datasets round-trip in their native classes, while unsupported compound-complex encodings return an error. Text, image, audio, and plotting formats convert according to the format and builtin contract.
Verifying the result
We based the compatibility work on public documentation and independent engineering. The work did not use MATLAB binaries, generated MATLAB output, disassembly, or reverse engineering.
Integer support touches too many paths for a handful of arithmetic examples to establish confidence. A cast can return the right value while indexing widens it, compiled code changes its class, a GPU transfer rounds it, or a file round trip changes its shape. We used several independent coverage methods so that one blind spot would not define the result.
| Coverage method | Scale | What it established |
|---|---|---|
| Public semantic research | Public documentation and an implementation ledger | The expected class, value, shape, overflow, output, and error behavior was written down before closure |
| Full builtin census | 964 candidates classified; 787 builtin names and 1,476 forms recorded | Every likely public integer entry point was reviewed, including rejected and inapplicable APIs; none remained untriaged |
| Compiler-directed migration | Dense, sparse, and complex payloads made private; core storage matches made exhaustive | Every direct consumer of the old representation had to move to typed storage or declare a conversion |
| Cross-runtime conformance | All eight classes across host, compiled VM, deterministic providers, and actual WGPU | The same program retains its class, shape, value, and error behavior across execution paths |
| Persistence matrix | Binary I/O, MAT, HDF5, DataArrays, mapped files, transactions, and replay | Supported values survive storage boundaries, including wide uint64, empty shapes, and sparse arrays |
| Architecture guards | Source checks, catalog invariants, all-target builds, and hardware tests | The checks detect attempts to restore the floating mirror or move type authority out of the buffer descriptor |
The conformance matrix deliberately stresses the cases most likely to expose an accidental conversion: signed minima, unsigned maxima, values above flintmax, saturation endpoints, mixed operands, empty arrays, vectors, matrices, N-D shapes, indexing, assignment, reductions, sparse storage, and supported complex layouts. It includes both compatibility policies and repeats representative all-class cases across the interpreter, compiled VM, providers, GPU, and persistence paths.
We tested providers at two levels. A deterministic in-process provider makes ownership, class, layout, and fallback behavior easy to inspect. Independently executed WGPU tests then run the final code on actual GPU hardware across all eight integer classes, including wide 64-bit values and resident operations. This catches failures that a mock backend or successful shader compilation cannot reveal.
During the storage migration, targeted tests also filled the old floating copy with conflicting values. Any path that still read the compatibility copy instead of the typed payload produced an obviously wrong result. Once those tests and the compiler frontier were clean, we removed the duplicate storage entirely.
The public suites cover values and classes, shapes, broadcasting, indexing, reductions, sparse and complex behavior, I/O, compiled execution, providers, and GPU transfers. Many tests run the same case across all eight classes or several execution modes. New integer cases were added to the existing builtin suites as each path moved to typed storage, so the work checks both the new exactness behavior and the behavior that was already there.
Final closure ran host semantics, compiled execution, persistence, provider behavior, WGPU execution, compatibility policy, static source checks, and generated-catalog synchronization. The merged integration history includes those tests and guards alongside the implementation changes.
Try it
x = uint16([1; 2; 3; 4]);
y = int32([2; 1; 4; 3]);
R = corr(x, y)The runnable block opens the example in the browser sandbox. In the default runmat compatibility mode, corr accepts both integer classes and returns 0.6000 without an explicit cast. To see the compatibility check, add compat = "matlab" under [runtime.language] in the workspace's runmat.toml and run the same code again. RunMat then rejects the extension-only corr call.
To run the same code locally, save it as mixed-integers.m and use the CLI:
runmat mixed-integers.mRunMat is an independent, modern runtime for MATLAB-syntax source code. We are not affiliated with MathWorks.
Related posts
Compiling MATLAB Code to Native Executables with RunMat
How RunMat compiles MATLAB-syntax projects into standalone host-native executables using one semantic pipeline, verified Native IR, and a matching compiler-free runtime.
RunMat Turns One: What We Built and What Comes Next
RunMat's first year: how v0.0.1 grew from a CLI with 87 built-in functions into a GPU-accelerated runtime for the browser, RunMat Cloud, and Desktop.
Switching from MATLAB to Julia
A practical guide to switching from MATLAB to Julia, covering syntax, arrays, performance, packages, Simulink, team training, deployment, and validation.
Enjoyed this post? Join the newsletter
Monthly updates on RunMat internals, development, and performance tips.
Download RunMat
Download RunMat for full performance, or use RunMat in your browser for zero setup.