Dense numeric arrays and linear algebra for Lua 5.4, implemented as a Rust crate. Scripts see a Lua library; hosts embed the crate and register it into a PUC Lua 5.4 state.
Architectural counterpart to NumPy for the Lua world: high-level language in
front, systems language in the core. Python → Lua 5.4, C → Rust, ndarray →
Apache Arrow, BLAS/LAPACK →
faer.
| You are… | You get… |
|---|---|
| Lua author (inside a host) | require "matlua", 1-based arrays, operators, matmul / matmul_at / matmul_bt / normal_eq / solve / factorizations |
| Host / embedder | Rust crate: owned f64 n-D arrays, faer LA, optional lua feature + matlua::lua::register |
| Rust-only consumer | Same arrays + LA without linking Lua |
Implementer rulings, scope, and architecture: DESIGN.md.
Process: AGENTS.md.
After the host registers MatLua:
local ml = require "matlua"
local A = ml.array({{3, 1}, {1, 2}})
local b = ml.array({9, 8})
local x = ml.solve(A, b) -- ≈ {2, 3}
-- one expression, one result (no forced temps for subexpressions)
local X = ml.array({{1, 0}, {1, 1}, {1, 2}, {1, 3}})
local y = ml.array({1, 3, 5, 7})
local beta = ml.normal_eq(X, y) -- short path: matmul_at + solve (see DESIGN)
-- or: ml.solve(ml.matmul_at(X, X), ml.matmul_at(X, y))Indexing is 1-based on the Lua face — element indices and reduction axes
(axis 1 = down rows, axis 2 = across columns); the Rust face is 0-based.
Elementwise + - * / and unary -
work on arrays (and array ↔ number). Matrix product is always explicit:
ml.matmul(a, b) — Lua has a single *, so it stays elementwise.
| Area | Surface |
|---|---|
| Masks / compare | where; eq/ne/lt/le/gt/ge (0/1); isnan / isfinite |
| Indexing | nonzero, compress, put, put_mask, take (i64 or f64 indices) |
| Host embed (Rust) | lua::push_view_f64 / push_view_i64 (zero-copy read-only), push_array_copy_* |
| Broadcast / views | elementwise broadcast; broadcast_to; concatenate/stack; slice/rows (1-based half-open on face), row/col (single 1-based index) |
| NaN reductions | nansum, nanmean, nanmin, nanmax, nanvar, nanstd |
| Tier-2 | cov/corrcoef, outer/diag/trace, argsort/take, axis on sum/mean/…, any/all |
| Random | seed, random, randn, uniform, normal, integers (i64), choice |
| Constructors | zeros, ones, full, arange (start, stop[, step], half-open), array (nested tables → dense f64), eye |
| Array methods | shape, rank, dtype, get / set, sum / mean / min / max, var / std, median/quantile, argmin / argmax, ufuncs (abs/sqrt/exp/log/log1p/sign/power/clip/isnan/isfinite/cumsum), copy, reshape (may share; write COWs), transpose, fill, #a |
| Elementwise | +, -, *, /, unary -; add_out/mul_out/… and matmul_out (preallocated) |
Linear algebra (f64) |
matmul, …, solve, lstsq, eigh, pinv, cholesky, cholesky_solve, qr, svd, det/slogdet/matrix_rank/cond/eig/eigvals (M7.b) |
i64-unique |
bitwise / rem / shift, unique / isin / bincount / searchsorted / sort, divmod / gcd / lcm, bit counts |
Linear algebra (i64) |
Integer path: matmul_i64 / … (wrapping). Same solve/eigh/… accept ArrayI64 and return f64 (NumPy-style). Also linalg::from_i64 / i64_ops in Rust |
| Rust core | Array (f64), ArrayI64 (i64), views over host f64/i64 buffers, Arrow Float64/Int64, LA matlua::linalg + linalg::i64_ops |
f64 remains the primary continuous/LA dtype; i64 (M7 Done) is first-class for
keys, indices, integer arithmetic/LA, and i64-unique ops. Solvers on integer inputs
promote to f64 results (NumPy-style). Lua: zeros_i64, array_i64, where_i64, …
Storage is a dense buffer, not nested Lua tables (tables are constructor sugar only).
| Feature | Purpose |
|---|---|
| (default) | Arrays + faer LA (Rust API only) |
lua |
Hand-rolled Lua 5.4 bindings; vendors PUC 5.4.7 for tests/tools |
Hosts keep their own lua_State and call unsafe { matlua::lua::register(L) }.
The vendored interpreter is for MatLua’s own tests and simple tools.
cargo test
cargo test --features lua
python3 tests/bench/compare_tables.py --write-readme tests/README.md # result tables
// L: *mut lua_State owned by the host (PUC Lua 5.4)
unsafe {
matlua::lua::register(L);
// Recommended: large f64 buffers live on the Rust heap; help Lua GC.
matlua::lua::enable_generational_gc(L);
}
// scripts: local ml = require "matlua"Rust-side desk math without Lua:
use matlua::{Array, linalg};
let a = Array::from_shape_slice(vec![2, 2], &[3., 1., 1., 2.])?;
let b = Array::from_shape_slice(vec![2], &[9., 8.])?;
let x = linalg::solve(&a, &b)?;- User product: Lua library. Ship form: Rust crate.
- Hosts fit to MatLua — engines adapt buffers and embed to MatLua’s contracts.
- Portable dense LA — faer in-tree; no system BLAS/LAPACK default.
- Arrow for buffer model and interchange; faer for dense LA (views in, owned results out).
- Curated dense surface first — not full NumPy/SciPy parity.
Closed decisions and milestones: DESIGN.md.
M0–M6 are on main (see DESIGN.md §7.1). The tree is a v0.1
candidate: a host can embed MatLua and scripts can do ordinary dense array and
linear-algebra work end-to-end. Crate version remains 0.0.1 until a formal
0.1.0 cut. M7 Done. M7.b Done. M7.c work complete on the optimization
branch — closure pending sign-off on the rulings in DESIGN.md
§7.1.2 (performance bar, accepted residuals). Exact wrapping i64 matmul (plan A)
held, at ~73–88% of the measured machine roofline. Embed/TallyDB letter work is
M8–M12 (§7.1.1).
Performance: three-way microbenches (NumPy · MatLua Rust · MatLua Lua) under
tests/README.md — summary tables first (Lua vs NumPy, ranges
across sizes), full per-n three-face tables in the appendix, plus an i64
machine-roofline yardstick. i64 matmul is timed against NumPy f64 BLAS on
integer-valued inputs (not int64@int64).
Known limits (tracked M7.c–M12 / issues): exact i64 GEMM runs ~6–9× NumPy f64
BLAS (integer-valued) at ~73–88% of the measured machine ceiling — the rest is
integer-multiply ISA physics (see tests/README Roofline) — and range-safe
integer data (intermediates ≤ 2⁵³) takes an exact f64-promote path at BLAS
speed; refused allocations raise catchable errors, no size ceiling (DESIGN
§3.27); small-buffer pool min size; embed error boundary; dtypes
beyond f64/i64; arrow-lite cutover. Full in-place out= is
#21 (partial *_out exists).
Host→Lua views: push_view_f64 / push_view_i64 (M7.b).
MIT © 2026 Andy Emerson