Luau vs Lua 5.1 — Complete Difference Reference

Luau version covered: 0.730 (latest tagged release, 17 July 2026) Baseline: Lua 5.1.5 Sources: luau.org docs, luau-lang/luau release notes (0.720–0.730), luau-lang/rfcs

Luau is syntactically backwards compatible with Lua 5.1 — any valid Lua 5.1 program is a valid Luau program, with the caveat that some 5.1 library functions have been removed for sandboxing. Luau is not a superset of later Lua versions; it cherry-picks from 5.2–5.5 and adds a large amount of its own.

In the AntOS wiki: this is the exhaustive upstream reference. For the AntOS-oriented introduction start at the Luau hub and The Luau language; for hands-on porting see the migration tutorials. Note that the sandboxing removals in §6 (io, most of os) are re-supplied by AntOS through its own io / os / fs libraries.

Status markers used below:

Marker Meaning
✅ Shipped and stable
🚧 RFC accepted, implementation landing now (0.72x) — verify against your build
⚠️ Behavioural difference that can silently break ported Lua code

Table of contents

  1. The 30-second summary
  2. Syntax additions
  3. The type system
  4. New value types
  5. Standard library differences
  6. Removed from Lua 5.1 (sandboxing)
  7. Behavioural / semantic differences
  8. Implementation limits
  9. Lua 5.2 / 5.3 / 5.4 / 5.5 feature status
  10. C API and embedding differences
  11. Modules, require, and .luaurc
  12. Tooling
  13. Recent changes: 0.720 → 0.730
  14. Porting checklist

1. The 30-second summary

What you gain over Lua 5.1:

  • A full gradual type system with inference, generics, unions/intersections, refinements and user-defined type functions.
  • continue, compound assignment, //, if..then..else expressions, string interpolation, generalized for..in iteration, const bindings, export, function attributes.
  • New value types: vector (native, SIMD-friendly), buffer (fixed-size mutable memory), and 🚧 integer (true 64-bit).
  • A much faster VM, native code generation (@native / --!native), and a bytecode inliner.
  • Real sandboxing (table.freeze, read-only globals, luaL_sandbox).

What you lose:

  • io, package, most of os and debug, loadstring/loadfile/dofile, string.dump.
  • Tail calls (⚠️ deep recursion will overflow).
  • __gc, goto, <close>, <const> attribute syntax, bitwise operators, 64-bit integer operators.

2. Syntax additions

2.1 String literal escapes ✅

Luau adopts the Lua 5.3 escape set on top of 5.1:

local hex  = "\x41"            -- "A"
local uni  = "\u{1F600}"       -- UTF-8 bytes for U+1F600 (braces are mandatory)
local long = "this is a very \z
              long literal"    -- \z eats the newline + following whitespace

2.2 Number literals ✅

local dec  = 1048576
local hex  = 0xFFFF            -- also in Lua 5.1
local bin  = 0b0101_0101       -- Luau only
local sep  = 1_048_576         -- underscore separators, Luau only
local both = 0xFFFF_FFFF

Numbers are still a single type: 64-bit IEEE-754 doubles (exact integers to 2^53). Literals larger than that lose precision — unless you use the new integer suffix (§4.3).

2.3 continue ✅

Lua 5.1 has no continue (and Luau deliberately rejects goto, so the ::continue:: workaround doesn't apply either).

for i = 1, 10 do
    if i % 2 == 0 then
        continue                     -- must be the last statement in its block
    end
    print(i)
end

continue is a contextual keyword, not a reserved word, so continue() still parses as a function call and old code using continue as an identifier keeps working.

Restriction: inside repeat..until, continue may not skip the declaration of a local that the until condition reads — that's a compile error.

repeat
    continue
    local x = false     -- error: continue would skip this declaration
until x

2.4 Compound assignment ✅

local n = 1
n += 1          -- also -=  *=  /=  //=  %=  ^=  ..=
local t = { count = 0 }
t.count += 5

They are statements, not expressions (no local a = (b += 1)). The left-hand side is evaluated once, so getTable().field += 1 calls getTable() a single time — unlike the manual expansion. Arithmetic and index metamethods (__add, __index, __newindex, …) are honoured, so custom types work with no extra effort.

2.5 Floor division // ✅

Taken from Lua 5.3, with //= and an __idiv metamethod.

print(7 // 2)     --> 3
print(-7 // 2)    --> -4       (rounds toward negative infinity)
print(7 // 0)     --> inf
print(0 // 0)     --> nan

For the vector type, v // n applies math.floor component-wise.

2.6 if..then..else expressions ✅

local maxValue = if a > b then a else b

local size =
    if n < 10 then "small"
    elseif n < 100 then "medium"
    else "large"

else is mandatory. This is the recommended replacement for the a and b or c idiom, which misbehaves when b is false or nil:

local wrong = cond and false or true    -- always true!
local right = if cond then false else true

2.7 String interpolation ✅

local name, count = "world", 3
print(`hello {name}, you have {count} item{if count == 1 then "" else "s"}`)
print(`sum = {1 + 2}, nested = {("x"):rep(3)}`)

Rules and gotchas:

  • Inside backticks, \ escapes `, {, \ and newline.
  • {{ is a parse error (deliberately, to stop people writing JS/C#-style escapes).
  • Backtick strings are not allowed in type annotations: type Foo = `Foo` is invalid.
  • Unlike "..." and '...', a backtick string cannot be used as a bare call argument — fx`is invalid, writef(x)`.

2.8 Generalized iteration ✅

In Lua 5.1 you must call pairs/ipairs. In Luau you can iterate a table directly:

local t = { a = 1, b = 2 }
for k, v in t do            -- no pairs() needed
    print(k, v)
end

Extendable to tables and userdata via the __iter metamethod, which is called once before iteration begins and returns an iterator function:

local Range = {}
Range.__index = Range
function Range.__iter(self)
    local i = self.from - 1
    return function()
        i += 1
        if i <= self.to then return i end
    end
end

local r = setmetatable({ from = 1, to = 3 }, Range)
for n in r do print(n) end          --> 1 2 3

Ordering guarantees: elements 1..#t are visited consecutively, the rest in unspecified order. Mutating keys other than the current one is unspecified — with one exception: in a dense array, non-nil elements appended at #t+1 during the loop are guaranteed to be visited.

__pairs / __ipairs from Lua 5.2 are not supported and never will be; __iter supersedes them.

New in 0.722: the iterator function may now yield. (Metamethods — including __iter itself — still may not yield.)

-- the generator can now block on IO and yield
for request in net.serve(8080) do
    request.respondWith("Echo: " .. request.body)
end

2.9 const bindings ✅

Luau's answer to Lua 5.4's <const>, with syntax it considers less ugly. const is a contextual keyword valid anywhere local is.

const MAX = 100
MAX = 200                 -- error: cannot reassign a const binding
MAX += 1                  -- also an error

const x: number = 5       -- type annotations work
const a, b = 1, 2         -- multi-assignment works
const function f() end    -- function declarations work

⚠️ const freezes the binding, not the value:

const t = { count = 0 }
t.count += 1              -- fine, the table is still mutable
t = {}                    -- error

const frozen = table.freeze({ count = 0 })
frozen.count = 1          -- now this errors too

Backwards compatible: local const = 5 still works, because const is only a keyword where local would be valid.

2.10 export for values ✅ (0.723)

Historically Luau only had export type. RFC 179 added value exports at module top level.

-- mylib.luau
export local version = "5.1"
export const TAU = math.pi * 2
export function init()          -- exported functions are always const
    -- ...
end

export type Point = { x: number, y: number }

if foo then
    export local bar = 1        -- syntax error: top level only
end

A module with exports returns a frozen table of them. Exported bindings count toward the local variable limit, since the compiler can lower them to locals. This is what makes cross-module inlining and constant folding possible — the reason const was introduced alongside it.

2.11 Function attributes ✅

Not in Lua at all. @name annotations before a function declaration, built into the language (users cannot define new ones).

@native
local function hot(a: number, b: number)     -- request native code generation
    return a * b + a
end

@deprecated
function oldApi() end

@[deprecated { use = "newApi()", reason = "oldApi leaks handles." }]
function oldApi2() end

@[native, deprecated]                        -- group multiple in one @[ ]
function both() end
  • @native is the per-function equivalent of the --!native file directive. It does not apply recursively to inner functions — mark those separately.
  • @deprecated makes the linter warn at call sites and the LSP style the entry differently in autocomplete. As of 0.730 the deprecation is propagated into autocomplete entries.
  • @attr, @[attr] and @[attr()] are all equivalent. For a single string/table parameter the parens are optional.

2.12 Classes 🚧 (RFC accepted, implementation started in 0.721)

The biggest language addition in flight. class and public are new contextual keywords.

class Point
    public x: number
    public y                       -- un-annotated fields are typed `any`

    function length(self)
        return math.sqrt(self.x * self.x + self.y * self.y)
    end

    function __add(self, other: Point)
        return Point { x = self.x + other.x, y = self.y + other.y }
    end

    function __tostring(self)
        return `Point \{ x = {self.x}, y = {self.y} \}`
    end

    function new(x, y)             -- no `self` ⇒ static, call as Point.new(...)
        return Point { x = x, y = y }
    end
end

local p = Point.new(3, 4)
print(`{p} length = {p:length()}`)

Key semantics:

  • Top-level declarations only; export class X is allowed; duplicate names in a module are an error.
  • Instantiate by calling the class object with a table of initial field values: Point { x = 1, y = 2 }. MyClass(original) shallow-clones.
  • The class binding is hoisted (so mutually-recursive classes/functions work), but the class object is only allocated when the statement executes.
  • Class objects are always const and frozen.
  • Instances are a new VM value type, not tables: no array part, no pairs/ipairs, no getmetatable/setmetatable, and reading or writing a nonexistent property raises.
  • type()/typeof() return "class" for class objects and "object" for instances.
  • Class types are nominal, unlike Luau's structural table types.
  • Only a fixed metamethod set is allowed (__add, __sub, __mul, __div, __mod, __pow, __idiv, __unm, __concat, __len, __eq, __lt, __le, __call, __iter, __tostring). Any other __-prefixed method is a syntax error.
  • New global library:
if class.isinstance(p, Point) then      -- participates in type refinement
    print(p.x, p.y)
end
local C = class.classof(p)              -- class? 

Out of scope for now: inheritance, generic classes, private/const fields.

Related cleanup: the old declare class definition-file syntax is being removed in favour of declare extern type Foo with ... end. extern class was disallowed in 0.727.


3. The type system

Lua 5.1 has none of this. Types are erased at compile time and cost nothing at runtime.

3.1 Modes

--!strict        -- full inference, errors on mismatch
--!nonstrict     -- default; unknown types become `any`
--!nocheck       -- inference disabled for this file

Extra file directives: --!native (native codegen for the whole script), --!optimize N, --!nolint.

3.2 Annotations and casts

local n: number = 1
local s: string? = nil                       -- shorthand for `string | nil`

local function add(a: number, b: number): number
    return a + b
end

local function none(): () end                -- zero returns: parenthesise
local function two(): (number, string) end
local cb: (value: number) -> string          -- argument names are documentation only

local names = {} :: { string }               -- type cast with ::
local x = value :: any

Casts are themselves checked: one side must be a subtype of the other, or any. Casting a multiple-return expression keeps only the first value.

3.3 Type aliases, typeof, export type

type Point = { x: number, y: number }
type Vector = typeof(vector.create(0, 0, 0))     -- `typeof` never evaluates its argument
type Handler = (Point) -> ()

export type Config = { retries: number }         -- visible to other modules
-- consumer.luau
local lib = require("./lib")
local c: lib.Config = { retries = 3 }

3.4 Tables: arrays, indexers, read/write properties

type Names   = { string }                    -- array-like shorthand
type Lookup  = { [string]: number }          -- indexer
type Mixed   = { n: number, [string]: any }

type ReadOnly  = { read x: number }          -- property can be read but not written
type WriteOnly = { write y: number }

Read-only indexers ✅ (new in 0.721) — {read T} / {read [K]: V}. This matters because mutable arrays must be checked invariantly, whereas read-only ones can be checked covariantly:

local function printThemOld(a: { Instance }) end
local function printThemNew(a: { read Instance }) end

local players: { Player } = ...
printThemOld(players)   -- rejected: the function could insert a non-Player
printThemNew(players)   -- accepted: the function cannot write

3.5 Unions, intersections, singletons

type Result = { ok: true, value: string } | { ok: false, err: string }
type Overloaded = ((number) -> string) & ((boolean) -> string)
type Direction = "north" | "south" | "east" | "west"     -- string singletons

3.6 Generics and variadics

local function identity<T>(x: T): T
    return x
end

type Map<K, V> = { [K]: V }

local function pack<T...>(...: T...): () -> T...
    local args = table.pack(...)
    return function(): T...
        return table.unpack(args, 1, args.n)
    end
end

3.7 Refinements

Control flow narrows types automatically:

local function f(x: string | number)
    if type(x) == "string" then
        print(x:upper())          -- x : string here
    else
        print(x + 1)              -- x : number here
    end
end

local function g(v: Part?)
    if not v then return end
    print(v.Name)                 -- v : Part
end

typeof(), type(), assert, truthiness tests and 🚧 class.isinstance all participate.

3.8 User-defined type functions

Type-level computation written in Luau itself:

type function MakeOptional(t)
    local props = {}
    for k, v in t:properties() do
        props[k] = types.optional(v)
    end
    return types.newtable(props)
end

type PartialPoint = MakeOptional<{ x: number, y: number }>

The types library gained type:issubtypeof in 0.724. types.integer is added by the integer RFC.

3.9 The new solver

Luau has been migrating to a rewritten constraint-based inference engine ("new type solver", DCR). Since 0.721 you can pick explicitly:

luau-analyze --solver=new  file.luau     # default
luau-analyze --solver=old  file.luau

Most 0.72x release notes are new-solver bug fixes; if you're on an older Luau, inference quality differs noticeably.


4. New value types

type() in Luau can return: "nil", "boolean", "number", "vector", "string", "table", "function", "userdata", "thread", "buffer" — and 🚧 "integer", "class", "object".

Lua 5.1 returns only the first-listed subset (no vector, buffer, integer, class, object).

typeof() is a Luau-only global: for host-defined userdata with a __type metafield it returns that name; for newproxy userdata it always returns "userdata" so host types cannot be spoofed.

4.1 vector ✅

A native 3-component (or 4 with LUA_VECTOR_SIZE=4) float vector stored inline in the value — no allocation, no GC pressure. Full operator support.

local a = vector.create(1, 2, 3)
local b = vector.one
local c = a + b * 2
print(c.x, c.y, c.z)              -- also .X/.Y/.Z; components are immutable

print(vector.magnitude(a), vector.dot(a, b))
local n = vector.normalize(a)
local x = vector.cross(a, b)
local m = vector.min(a, b, c)

Also: vector.zero, vector.angle, vector.floor, vector.ceil, vector.abs, vector.sign, vector.clamp, vector.max. Recent builds can be compiled with double-precision vectors (heap-allocated in that mode).

4.2 buffer ✅

Fixed-size mutable byte block — the thing Lua 5.1 completely lacks. Little-endian, bounds-checked (out-of-range access throws), max 1 GB.

local b = buffer.create(16)
buffer.writeu32(b, 0, 0xDEADBEEF)
buffer.writef32(b, 4, 1.5)
buffer.writestring(b, 8, "abcd")

print(buffer.readu32(b, 0), buffer.len(b))
print(buffer.readstring(b, 8, 4))

buffer.writebits(b, 96, 3, 5)     -- bit-level access, 0..32 bits
print(buffer.readbits(b, 96, 3))

buffer.fill(b, 0, 0, 4)
buffer.copy(dst, 0, b, 0, 8)      -- overlap-safe
local s = buffer.tostring(b)
local c = buffer.fromstring("hi")

Read/write functions: readi8/u8/i16/u16/i32/u32/f32/f64 and matching write*.

4.3 integer — real 64-bit integers 🚧

RFC merged; the library is referenced in 0.728 release notes. This is Luau's answer to Lua 5.3's integer subtype, done differently: a separate type with no arithmetic operators.

local a = 123i
local b = 1_000i
local c = 0xABABi
local d = 0b1000_1000i
local e = 0xFFFF_FFFF_FFFF_FFFFi      -- == -1i
local f = -123i                       -- unary minus on the literal is fine; -(123i) is not
  • Overflowing integer literals are a parse error.
  • Only == is defined. No +, -, <, no metamethods.
  • No implicit conversion either way. Passing an integer where a number is expected is a type error, and mixing them in an operation is not supported.
local x = integer.create(42)              -- number -> integer?, nil if not exact
local y = integer.fromstring("ff", 16)    -- -> integer?
local n = integer.tonumber(x)             -- may lose precision

local sum  = integer.add(a, b)            -- wraps (two's complement)
local quot = integer.div(a, b)            -- truncated; errors on /0 and on minsigned/-1
local fl   = integer.idiv(a, b)           -- floored
local rem  = integer.rem(a, b)
local mod  = integer.mod(a, b)
local ud   = integer.udiv(a, b)           -- 'u' prefix = unsigned interpretation

if integer.lt(a, b) then end              -- signed compare
if integer.ult(a, b) then end             -- unsigned compare

local m = integer.band(a, b)              -- bnot/band/bor/bxor/btest
local s = integer.lshift(a, 4i)           -- lshift/rshift/arshift/lrotate/rrotate
local w = integer.extract(a, 0i, 8i)      -- extract/replace
local z = integer.countlz(a)              -- countlz/countrz/bswap
print(integer.maxsigned, integer.minsigned)

Companion additions: buffer.readinteger / buffer.writeinteger; string.format accepts integers for d/i/* (signed) and o/u/x/X (unsigned) but errors for e/f/g; tostring prints without the i suffix; rawequal compares integers; types.integer in type functions; C API lua_pushinteger64 / lua_tointeger64 / lua_isinteger64 / luaL_checkinteger64 / luaL_optinteger64.

string.pack/unpack are deliberately not extended.


5. Standard library differences

5.1 Additions over Lua 5.1

Library Luau-only additions
globals typeof, newproxy (restricted), gcinfo
math clamp, sign, round, noise, isnan, isinf, isfinite, lerp, map; log(n, base) (5.2)
table create, find, clear, freeze, isfrozen, clone; move (5.3)
string split; pack/unpack/packsize (5.3, fixed sizes)
coroutine close (5.4), isyieldable (5.3)
bit32 whole library (5.2), plus Luau-only countlz, countrz, byteswap
utf8 whole library (5.3)
buffer whole library (Luau-only)
vector whole library (Luau-only)
integer whole library 🚧 (Luau-only)
debug info, traceback (see below — this is a replacement, not an addition)

Worth calling out:

-- table.create: preallocate, huge win in hot loops
local t = table.create(1000)          -- capacity only, #t == 0
local z = table.create(1000, 0)       -- 1000 zeros

-- table.freeze / isfrozen: real immutability, the basis of sandboxing
local cfg = table.freeze({ debug = false })
cfg.debug = true                       -- error
print(table.isfrozen(cfg))             --> true

-- table.clone: shallow copy preserving the metatable, never frozen
local copy = table.clone(cfg)

-- math.lerp guarantees lerp(a, b, 1) == b exactly
print(math.lerp(0, 10, 0.5))           --> 5
print(math.map(5, 0, 10, 0, 100))      --> 50

-- string.split
print(table.concat(string.split("a,b,c", ","), "|"))   --> a|b|c

math.random uses PCG (Lua 5.4 uses Xoroshiro); math.randomseed(seed) gives a deterministic sequence.

5.2 debug is a different library

Lua 5.1's introspection API is gone. Luau provides two safe functions instead:

local src, line, name = debug.info(2, "sln")   -- s=source l=line n=name f=function a=arity
print(debug.traceback("context", 1))

No debug.getinfo, getlocal, setlocal, getupvalue, setupvalue, sethook, getregistry.

5.3 os is trimmed

Kept: os.clock, os.date, os.difftime, os.time. Removed: os.execute, os.exit, os.getenv, os.remove, os.rename, os.tmpname, os.setlocale.

⚠️ os.time(table) returns a UTC timestamp in Luau; Lua 5.1 interprets the table as local time.


6. Removed from Lua 5.1 (sandboxing)

In AntOS, the filesystem/system removals below are replaced by AntOS's own io / os / fs libraries (DBFS-backed, drive-letter paths) — so AntOS scripts do have files and storage; they use AntOS's versions, not stock Lua's.

Removed Why
io library filesystem access
package library, module() filesystem / global namespace pollution
os (most of it) process and filesystem access
debug (most of it) breaks the isolation barrier
loadfile, dofile filesystem access
loadstring with bytecode, string.dump loading untrusted bytecode is a memory-safety hole
newproxy(table) only newproxy() / newproxy(bool) remain
__gc sandboxing + GC cost; use lua_newuserdatadtor on the host side

getfenv / setfenv are kept (deprecated in 5.2, removed in 5.3) purely for backwards compatibility.

Embedder-side sandboxing, which has no Lua 5.1 equivalent:

  • luaL_sandbox(L) marks all builtin library tables read-only — no monkey-patching, not even via rawset or setmetatable.
  • luaL_sandboxthread(L) gives each script its own global table chained to the builtin globals via __index, so scripts are isolated from each other.
  • This "safeenv" state is what unlocks many of Luau's builtin-call optimizations.

7. Behavioural / semantic differences

These are the ones that bite when porting.

⚠️ No tail calls. return f() grows the stack. Dropped to simplify the implementation, keep stack traces predictable, and allow deep validation of caller identity. Rewrite deep tail recursion as a loop.

⚠️ __eq is called even when the operands are rawequal. Lua 5.1 short-circuits on pointer equality; Luau does not (this matches __lt/__le and makes NaN-style checks expressible).

⚠️ Table literal assignment order. In mixed tables Luau assigns in program order; Lua 5.x sometimes does array elements first.

⚠️ Closure identity. function() ... end may return a previously created closure when all captured upvalues are identical. Call semantics are unchanged, but object identity is not stable — don't use closures as table keys expecting freshness. (Same as Lua 5.2/5.3, different from 5.1.)

⚠️ os.time(table) is UTC (see §5.3).

No integer/float distinction — one number type, so no math.type, no 1//1 producing an integer, no %d overflow surprises from 5.3.

No yielding in metamethods (__index, __newindex, __iter, …), and no yielding from most C callbacks. pcall/xpcall can yield (5.2 feature, supported). As of 0.722 generalized-iteration generator functions can yield.

Errors preserve non-string error objects through pcall, as in later Lua versions.


8. Implementation limits

Limit Lua 5.1 Luau
Local variables / function 200 200
Upvalues / function 60 200
Registers / function 255 255
Constants / function 2^18 2^23
Instructions / function 2^17 2^23
Nested functions / function 2^18 2^15 (lower)
Lua call depth tunable 20000 per thread
C call depth tunable 200 (so pcall/coroutine.resume nesting is capped at 200)

9. Lua 5.2 / 5.3 / 5.4 / 5.5 feature status

Legend: ✔️ in Luau · ❌ rejected · 😞 wanted but breaks compatibility · 🔜 planned · 🤷 no strong opinion

Lua 5.2

Feature Status Note
yieldable pcall/xpcall ✔️
yieldable xpcall handler ❌ VM complexity
yieldable metamethods ❌ significant perf cost
ephemeron tables ❌ GC cost; maybe after GC rework
emergency GC 🤷
goto ❌ unstructured control flow, compiler complexity
__gc for tables ❌ sandboxing
removal of fenv 😞 breaks compatibility
__len on tables ✔️
hex and \z escapes ✔️
hexadecimal floats 🤷
__lt/__le across unrelated metatables ❌
empty statement ;; 🤷
break mid-block 🤷
args for xpcall'd function ✔️
math.log(x, base) ✔️
separator in string.rep 🤷
__pairs / __ipairs ❌ superseded by __iter
frontier patterns, %g, \0 in patterns ✔️
bit32 ✔️
stricter gsub % handling ✔️
light C functions 😞
NaN keys with __newindex ✔️

Lua 5.3

Feature Status Note
\u escapes ✔️
64-bit integer subtype ❌ but see the separate integer type in §4.3
bitwise operators ❌ bit32 (and integer.*) instead
utf8 library ✔️
string.pack/unpack/packsize ✔️ fixed sizes: short 16, int 32, long 64, size_t 32
floor division ✔️
ipairs/table respecting metamethods ❌ perf
table.move ✔️
collectgarbage("count") single result ✔️
coroutine.isyieldable ✔️
stricter table.insert/remove checks 😞
__eq across unrelated metatables ❌

Lua 5.4

Feature Status Note
generational GC 🤷 other GC optimizations done instead
<close> ❌ no file/socket APIs to close; high complexity
<const> ❌ as syntax equivalent available as const x = v (§2.9)
new math.random ✔️ PCG rather than Xoroshiro
init arg to string.gmatch 🤷
coroutine.close / lua_resetthread ✔️
string→number coercion moved to string lib 😞
%p in string.format 🤷
utf8 codepoints up to 2^31 🤷
__lt emulating __le removed ❌
non-function __gc ❌
print calls __tostring ✔️
utf8 decoders reject surrogates ✔️

Lua 5.5

Feature Status Note
global variable declarations ❌ typechecker catches this
named vararg tables 🤷
read-only for-loop variables ❌ breaks compatibility
shortest round-trip float printing ✔️
table.create(arraysize, recordsize) ❌ conflicts with Luau's existing table.create
utf8.offset returning final position 🤷
external strings ❌ incompatible object layout
luaL_openselectedlibs, luaL_makeseed 🤷

10. C API and embedding differences

The API "mostly preserves" Lua 5.1, so existing bindings largely port, with these caveats:

  • Compilation is separate from loading. You call luau_compile() to produce bytecode and luau_load() to load it — this lets you ship a VM with no compiler. There is no luaL_loadstring equivalent that takes source in the runtime.
  • No __gc. Use lua_newuserdatadtor for userdata destructors.
  • Sandboxing entry points: luaL_sandbox, luaL_sandboxthread.
  • Tagged userdata with fast type checks and lua_setuserdatametatable. (0.725 made the GC visit cached tagged-userdata metatables so they are not collected prematurely.)
  • 0.725: automatic stack reservation. The C API now auto-reserves required stack slots, so manual lua_checkstack bookkeeping is largely unnecessary.
  • 0.725: luaL_pcallyieldable — yieldable protected C calls for custom libraries.
  • Recently promoted to the public API: lua_atbreakpoint, lua_allocationrate, LUA_GCISPAUSED for lua_gc, lua_memorydump, lua_setpointerencodekey, lua_callhook.
  • Build options: LUA_VECTOR_SIZE=4 for 4-wide vectors; double-precision vector mode (heap-allocated).
  • Native code generation is a separate Luau.CodeGen module; 0.727 added a bytecode-to-bytecode JIT inliner (Luau::JitInliner::setup(L), --jit-inliner in the CLI).
  • Integer C API additions per §4.3.

11. Modules, require, and .luaurc

Lua 5.1's package/require machinery is gone. Luau uses string-based requires with a resolution scheme defined by RFC:

local sibling = require("./sibling")
local parent  = require("../shared/util")
local aliased = require("@modules/foo")
local own     = require("@self/foo")     -- only meaningful inside init.luau
  • Paths must start with ./, ../ or @. A bare require("foo") is an error. (In AntOS, built-in libraries are the exception — require("fs"), require("terminal") are bare, host-registered names; see AntOS libraries.)
  • A directory containing init.luau is itself a module.
  • ⚠️ Inside init.luau, ./foo resolves relative to the parent directory, not the init file. Use @self/foo to reach siblings inside the package. @self is reserved and cannot be overridden.
  • Aliases come from .luaurc:
{
    "languageMode": "strict",
    "lint": { "*": true },
    "aliases": {
        "modules": "./some/long/path/to/modules"
    }
}

.luaurc files are found by walking up the directory tree; closer files override further ones. There is also an RFC for .config.luau (executable Luau config, evaluated in an isolated VM with a 2-second default timeout); .luaurc and .config.luau in the same directory is an error.


12. Tooling

Lua 5.1 ships lua and luac. Luau ships:

  • luau — REPL / script runner, sandboxed (no filesystem access beyond require). Since 0.726 it will try .luau then .lua extensions before erroring.
  • luau-analyze — type checker and linter. --solver=new|old, --output for diagnostics to a file (0.727).
  • luau-compile — bytecode dumper.
  • luau-lsp — community language server front-end.
  • A rich linter with its own warning set (unused variables, shadowing, deprecated calls, suspicious globals, etc.), configurable per-file via --! comments or per-tree via .luaurc.
  • Lute — a separate standalone general-purpose Luau runtime (luau-lang/lute), tracking Luau releases closely.

13. Recent changes: 0.720 → 0.730

Weekly releases. Highlights that affect the language rather than internals:

0.730 (17 Jul 2026)

  • @deprecated propagated into autocomplete entries.
  • Frontend::clearModules() for selective module removal.
  • Fixed a regression inferring { unknown } for array-tables built with table.insert.
  • Fixed loops "cancelling out" refinements established earlier in the block.
  • math.round on negative zero in SSE 4.1 mode.
  • NCG: better register-spill handling and diagnostics.

(0.729 is not present in the release list; lute nightlies referenced it, so treat 0.728 → 0.730 as contiguous for language purposes.)

0.728 (3 Jul 2026)

  • Improved inference around table-literal function arguments.
  • NCG: fixed rare data corruption in the integer library's mod/div/rem family — the first release note confirming integer in the tree.

0.727 (26 Jun 2026)

  • JIT bytecode-to-bytecode inliner (Luau::JitInliner::setup, --jit-inliner).
  • Variadic type annotations are now pushed into arguments — a lambda assigned to a field typed (...string) -> () gets string params instead of unknown.
  • extern class declaration syntax disallowed (cleanup toward declare extern type).
  • Bidirectional return-type inference disabled when the target function type is ambiguous.
  • Generic-bound fix for nil <: T?.

0.726 (19 Jun 2026)

  • Union subtyping failures now report one abbreviated message rather than one per union member.
  • Cyclic type alias assertion fix; requirers of a broken module see an error type instead of cascading type-function errors.
  • CST support for attribute lists and parameterized attributes.

0.725 (12 Jun 2026)

  • C API auto-reserves stack slots (no more manual lua_checkstack).
  • luaL_pcallyieldable.
  • GC visits cached tagged userdata metatables.
  • export crash fixes; Library.table.unpack no longer inferred as ...unknown.

0.724 (8 Jun 2026)

  • ConstraintGraph abstraction in the solver.
  • type:issubtypeof type function implemented.
  • Compiler: constant table folding, table function expression inlining, bytecode call inlining.

0.723 (29 May 2026)

  • export value semantics implemented (RFC 179) — §2.10.

0.722 (22 May 2026)

  • Yielding iterators — generalized iteration generators may yield. Metamethods including __iter still may not.
  • CST preserves expression and type groups (parentheses/whitespace).
  • New CMPPROTO bytecode instruction for JIT inlining.

0.721 (15 May 2026)

  • Read-only indexers {read T} / {read [K]: V} — §3.4.
  • First implementation steps for classes — §2.12.
  • --solver=new|old in luau-analyze (new is default).
  • CALLFB instruction + feedback vectors for call-site inlining decisions.

0.720 (8 May 2026)

  • const appears in autocomplete.
  • Fixed false OptionalValueAccess when iterating a table with an optional indexer type.
  • Better bidirectional inference for unions of tables and functions.
  • Deprecation notice: declare class is being removed; migrate definition files to declare extern type Foo with ... end.

14. Porting checklist

Moving Lua 5.1 code to Luau:

  1. Search for loadstring, dofile, loadfile, require "name", io., os.execute, os.getenv, package., module(, debug.getinfo, setmetatable(..., {__gc=...}). All of these need rework.
  2. Check for deep tail recursion — no tail calls.
  3. Check __eq implementations for assumptions about rawequal short-circuiting.
  4. Check os.time(t) — now UTC.
  5. Anything relying on closure identity (closures as table keys, == on freshly created functions) may behave differently.
  6. Bytecode caching — string.dump is gone; precompile with luau_compile on the host.
  7. Then start opting in: add --!strict per file, replace pairs(t) with for k, v in t, replace a and b or c with if..then..else, table.insert hot loops with table.create preallocation, byte-packing code with buffer.

Generated 28 August 2026. Luau moves weekly — check github.com/luau-lang/luau/releases and rfcs.luau.org for anything after 0.730.

Important: The Ant64 family of home computers are at early design/prototype stage, everything you see here is subject to change.