i was bored, so i decided to add complex numbers to luau
This implementation may not be 100% correct as there’s a few functions in this library I vibecoded because I don’t really know them myself, but the basic operations (+, -, * and /) have been tested (as I do know how to test for these)
Complex Source Code
--!native
--!optimize 2
-- Complex Numbers
-- metatablecat 2026
-- This implementation may not be 100% complete, but feel free to contribute!
-- REMARK: Right now, real numbers will remain in complex notation for performance reasons
-- you can return them to reals by simply indexing the part of the complex number you want
export type Complex = typeof(setmetatable({}::{
Real: number,
Imaginary: number
}, {}::{
__add: (self: Complex, other: number | Complex) -> Complex,
__sub: (self: Complex, other: number | Complex) -> Complex,
__mul: (self: Complex, other: number | Complex) -> Complex,
__div: (self: Complex, other: number | Complex) -> Complex,
__tostring: (self: Complex) -> string,
}))
-- some methods are useful in the metamethod
local COMPLEX_NEW
local function COMPLEX_TEST(complex: Complex, allowReals: boolean): string
if type(complex) == "number" and allowReals then return "real" end
if type(complex) ~= "table" then
error("Attempt to call a complex number method with a non-complex value", 2)
end
return "complex"
end
local function COMPLEX_MUL(a: number, bi: number, c: number, di: number): (number, number)
-- lower level method for just returning the raw numbers
-- without expensively rehashing the result
-- (a + bi)(c + di) = ac - (bd)i, (ad + cb)i
return a*c - bi*di, a*di + bi*c
end
local COMPLEX_MT = {
__add = function(lhs: Complex, rhs: number | Complex): Complex
COMPLEX_TEST(lhs, false)
local rhsType = COMPLEX_TEST(rhs, true)
if rhsType == "real" then
return COMPLEX_NEW(lhs.Real + rhs, lhs.Imaginary)
end
return COMPLEX_NEW(lhs.Real + rhs.Real, lhs.Imaginary + rhs.Imaginary)
end,
__sub = function(lhs: Complex, rhs: number | Complex): Complex
COMPLEX_TEST(lhs, false)
local rhsType = COMPLEX_TEST(rhs, true)
if rhsType == "real" then
return COMPLEX_NEW(lhs.Real - rhs, lhs.Imaginary)
end
return COMPLEX_NEW(lhs.Real - rhs.Real, lhs.Imaginary - rhs.Imaginary)
end,
__mul = function(lhs: Complex, rhs: number | Complex): Complex
-- TIP: This is just the distributive property
-- z(a + bi) or (a + bi)(c + di) for complex numbers
COMPLEX_TEST(lhs, false)
local rhsType = COMPLEX_TEST(rhs, true)
if rhsType == "real" then
return COMPLEX_NEW(lhs.Real * rhs, lhs.Imaginary * rhs)
end
return COMPLEX_NEW(COMPLEX_MUL(lhs.Real, lhs.Imaginary, rhs.Real, rhs.Imaginary))
end,
__div = function(lhs: Complex, rhs: number | Complex): Complex
-- For this, if its not a number, we first need to figure out the conjugate of the RHS
-- then multiply both the numerator and denominator by the conjugate,
-- the denominator will then ALWAYS be real, so we just divide
COMPLEX_TEST(lhs, false)
local rhsType = COMPLEX_TEST(rhs, true)
local nz, ni = lhs.Real, lhs.Imaginary
local dz, di
if rhsType == "real" then
dz = rhs
else
nz, ni = COMPLEX_MUL(nz, ni, rhs.Real, -rhs.Imaginary)
-- shortuct: a conjugate is an expression that gives us a difference
-- of two squares, so we can just use a^2 + b^2 for dz
dz = rhs.Real^2 + rhs.Imaginary^2
end
return COMPLEX_NEW(nz / dz, ni / dz)
end,
__tostring = function(c: Complex): string
if c.Real == 0 then
if c.Imaginary == 0 then
return "(0 + 0i)"
else
return `{c.Imaginary}i`
end
else
if c.Imaginary == 0 then
return tostring(c.Real)
end
local complexSign = if math.sign(c.Imaginary) == -1 then "-" else "+"
local abs = math.abs(c.Imaginary)
if abs == 1 then
return `({c.Real} {complexSign} i)`
end
return `({c.Real} {complexSign} {abs}i)`
end
end,
}
local memoized: {[string]: Complex} = setmetatable({}, {__mode = "v"})
local function complex_hash(a: number, bi: number): string
return `{a} + {bi}i`
end
function COMPLEX_NEW(real: number?, imaginary: number?): Complex
local hash = complex_hash(real or 0, imaginary or 0)
local comp = memoized[hash]
if not comp then
comp = setmetatable({
Real = real or 0,
Imaginary = imaginary or 0,
}, COMPLEX_MT)
memoized[hash] = comp
end
return comp
end
local function COMPLEX_conjugate(comp: Complex): Complex
COMPLEX_TEST(comp, false)
return COMPLEX_NEW(comp.Real, -comp.Imaginary)
end
local function COMPLEX_normalize(comp: Complex): Complex
COMPLEX_TEST(comp, false)
return comp.Real ^ 2 + comp.Imaginary ^ 2
end
local function COMPLEX_abs(comp: Complex): number
COMPLEX_TEST(comp, false)
return math.sqrt(comp.Real^2 + comp.Imaginary^2)
end
local function COMPLEX_argument(comp: Complex): number
COMPLEX_TEST(comp, false)
return math.atan2(comp.Imaginary, comp.Real)
end
local function COMPLEX_polar(comp: Complex): (number, number)
-- TODO: should this return a Vector2 if we're in the Roblox namespace?
return COMPLEX_abs(comp), COMPLEX_argument(comp)
end
local function COMPLEX_unpack(comp: Complex): (number, number)
return comp.Real, comp.Imaginary
end
local function COMPLEX_isReal(comp: Complex): boolean
return comp.Imaginary == 0
end
local function COMPLEX_sqrt(comp: Complex): Complex
local r, theta = COMPLEX_polar(comp)
local sr = math.sqrt(r)
return COMPLEX_NEW(
sr * math.cos(theta / 2),
sr * math.sin(theta / 2)
)
end
local function COMPLEX_exp(comp: Complex): Complex
local er = math.exp(comp.Real)
return COMPLEX_NEW(
er * math.cos(comp.Imaginary),
er * math.sin(comp.Imaginary)
)
end
local function COMPLEX_log(comp: Complex): Complex
return COMPLEX_NEW(
math.log(COMPLEX_abs(comp)),
COMPLEX_argument(comp)
)
end
return {
new = COMPLEX_NEW,
zero = COMPLEX_NEW(0, 0),
i = COMPLEX_NEW(0, 1),
one = COMPLEX_NEW(1, 0),
abs = COMPLEX_abs,
argument = COMPLEX_argument,
conjugate = COMPLEX_conjugate,
normalize = COMPLEX_normalize,
polar = COMPLEX_polar,
unpack = COMPLEX_unpack,
isReal = COMPLEX_isReal,
exp = COMPLEX_exp,
log = COMPLEX_log,
sqrt = COMPLEX_sqrt
}
This is currently missing support for the ^ operator currently, but I’m sure some of you math nerds will tell me how to implement it lol.







