Loomreference

Luau globals

The Luau standard library a previewed tree runs against, and what is missing on purpose.

A roblox-ts source file compiles against Luau’s global environment, not JavaScript’s: it calls table.insert with no import, indexes strings from 1, and expects math.clamp. None of that is in a browser.

Loom installs it. installGlobals(), injected ahead of your entry on both the dev server and a static build, defines the whole standard library on globalThis before a single component evaluates.

What is installed

GlobalMembers
tableinsert, remove, find, concat, sort, create, clear, clone, freeze, isfrozen, pack, unpack, move, and the deprecated getn, maxn, foreach, foreachi
stringlower, upper, len, reverse, char, byte, sub, rep, split, find, gsub, match, format
mathabs, floor, ceil, sqrt, max, min, pow, exp, sin, cos, tan, asin, acos, atan, atan2, sinh, cosh, tanh, log, log10, ldexp, frexp, modf, sign, clamp, round, fmod, deg, rad, noise, random, randomseed, huge, pi
osclock, time, difftime, date
bit32band, bor, bxor, bnot, btest, lshift, rshift, arshift, lrotate, rrotate, extract, replace, countlz, countrz, byteswap
utf8char, codepoint, len, offset, charpattern, nfcnormalize, nfdnormalize
debugtraceback, profilebegin, profileend, setmemorycategory, resetmemorycategory, info
buffercreate, fromstring, tostring, len, the readi8…readf64 / writei8…writef64 pairs, readstring, writestring, copy, fill
taskspawn, defer, delay, cancel, wait — mapped onto browser timers
coroutinecreate, resume, yield, wrap, status, running, isyieldable, close — real since 0.12.0
RandomA seeded generator with NextInteger / NextNumber / NextUnitVector / Clone. Added in 0.12.0
DateTimenow, fromUnixTimestamp, the format and component readers. Added in 0.12.0
Free functionsselect, unpack, rawget, rawset, rawequal, rawlen, typeOf, typeIs, pcall, xpcall, pairs, ipairs, next, tostring, tonumber, error, warn, print, assert, tick

table counts from 1

table is not a roblox-ts macro. The compiler passes its arguments straight through to Luau, so the number written in the source is already a Luau index. Loom reads it as one.

The array methods roblox-ts does compile as macros keep their 0-based TS indices. Both spellings appear in the same file all the time, and they drop the same element:

list.remove(0); // the macro — 0-based, like the TypeScript it is written in
table.remove(list, 1); // the library — 1-based, like the Luau it compiles to

string.find has always behaved this way here for the same reason.

The tables themselves are ordinary JavaScript values: an array, a Map, a Set or a plain object. So #list is list.length, and a hole does not end it.

sort takes a predicate

Luau’s comparator answers “does a come first”, not “which is bigger”:

table.sort(rows, (a, b) => a.order < b.order); // boolean, not -1 / 0 / 1

A JS-style comparator returning a number is truthy for -1 and 1, so it sorts nothing in particular. The default order, with no comparator, is < as in Luau. Since 0.12.0 Array.prototype.sort accepts the same boolean predicate, so a .sort() written in roblox-ts style orders correctly too.

Where loom is forgiving

The engine raises an error in each of these. Loom leans the other way, so a preview renders rather than dying over an off-by-one:

CaseRobloxLoom
insert at an out-of-range positionerrorclamps into range
remove at an out-of-range positionerrorreturns nil, mutating nothing
concat over a non-string, non-number elementerrorruns it through tostring

freeze is Object.freeze: it stops writes to an array or a plain object, and cannot stop Map.set or Set.add.

string patterns and tuple returns

find, match, gmatch and gsub take Luau patterns, translated to a regular expression — character classes (%a, %d, %s, %w, …), anchors, quantifiers and captures. Indices are 1-based and find accepts a negative init counting back from the end, as the engine does.

find, match and gmatch return their captures as the array roblox-ts reads a LuaTuple as, and an empty one when nothing matched:

const [start, finish] = string.find(label, "%d+");
const [name] = string.match(path, "([^/]+)$");
for (const [word] of string.gmatch(sentence, "%a+")) { /* … */ }

gsub takes a function or a table as its replacement, not only a string. format understands the Lua specifiers a UI reaches for: width, padding and precision.

string.gsub("a b", "%a", (c) => c.upper()); // "A B", 2
string.gsub("a b", "%a", { a: "X", b: "Y" }); // "X Y", 2
string.format("%02d:%05.1f | %-10s|", 7, 3.14159, "ab");

roblox-ts also calls these off a string receiver. String.prototype carries .lower(), .upper(), .sub(), .rep(), .find(), .gsub(), .format(), .gmatch(), .byte(), .len(), .reverse() and .size(). Each delegates to the library above, with the same 1-based indices and tuple returns. .sub() deliberately replaces the Annex B HTML wrapper JavaScript ships under that name.

Array macros are patched the same way: .size(), .isEmpty(), .remove(i), .unorderedRemove(i) and .clear() on Array.prototype, 0-based as roblox-ts compiles them. .size() and .isEmpty() resolve on Map and Set through a symbol the preview rewrites previewed source to. Every other Map on the page keeps plain JS semantics.

math.randomseed really seeds

Math.random cannot be seeded, so randomseed does not pretend to: it switches math.random over to a deterministic generator. Code that seeds for reproducibility gets it, rather than being silently ignored. A scene wanting the same “random” layout every reload works.

math.log takes an optional base, and frexp / modf return their pair as a destructurable array.

os.date

A strftime subset: %a %A %b %B %c %d %H %I %j %m %M %p %S %x %X %y %Y %%. An unrecognized specifier is left as written. The *t and !*t table forms and the ! UTC prefix work on any format. os.time accepts a date table as well as no argument, and os.difftime subtracts two of its results.

debug profiling shows up in devtools

debug.profilebegin and profileend are wired to performance.mark and performance.measure. Roblox instrumentation a component already carries appears as real entries in the browser’s Performance panel. debug.traceback returns the JavaScript stack, which is the real one here.

bit32 and buffer details worth knowing

bit32 implements Luau’s shift semantics, not JavaScript’s. A shift of 32 or more saturates to zero, where JS’s << masks the count to five bits and shifts by count % 32. Every result is an unsigned 32-bit number.

buffer is little-endian and bounds-checked, so an out-of-range access throws as it does in the engine. typeOf answers "buffer" for one, since Luau’s buffer is a distinct primitive type rather than an ArrayBuffer with a different name.

The stubs

Installed so the code path exists, and doing nothing meaningful:

GlobalWhat it actually does
math.noiseReturns 0 for every input. A UI that jitters with noise renders still rather than crashing.
debug.infoThe empty tuple. The engine reads its answers out of the Luau VM, which does not exist here, so callers destructure nils instead of crashing on a missing function.
debug.setmemorycategory resetmemorycategoryNothing. A browser has no Roblox memory categories to attribute to.
task.waitReturns a Promise, so await task.wait(n) works. A bare synchronous task.wait() cannot block in a browser and does not.

What is deliberately absent

setmetatable, getmetatable, newproxy. Loom runs the author’s TypeScript, whose classes are JavaScript classes. There is no faithful way to give a plain object a metatable’s __index behaviour without proxying every table in the program. A half-faithful one would be worse than the honest gap: a scene would render right up until the moment the metatable mattered.

print is installed but not declared. The runtime overwrites the value, so print works. The ambient globals declaration omits it because lib.dom already declares one, and redeclaring is a compile error. See TypeScript setup.

No filtering, no networking. Anything that would need a Roblox server throws by name rather than quietly doing nothing. That covers HttpService.GetAsync and TextService.FilterStringAsync. See Services.