PONYλM2Modula-2
for Lua programmers

You already know Lua.Now explore other languages.

Side-by-side, interactive cheatsheets for Lua programmers
comparing Lua to other languages. Every example runs live in your browser — no setup, no installation.

▶ Start with RubyBrowse comparisons ↓

Choose your own path by reordering languages

Ruby⚡ Works Offline⚡ Offline

The closest language to Lua on this whole site. Two small dynamic languages that grew objects out of a lookup fallback rather than a class declaration — and, uniquely among the targets here, Ruby agrees with Lua about what is true.

  • Only nil and false are falsy — in both. 0 and "" stay true, so every truthiness guard you have written means the same thing
  • ✅ Coroutines and Fibers are near-identical, down to two-way value passing and the same names
  • ✅ Both overload operators, both intercept a missing member (__index vs method_missing), and Ruby 4.0 froze string literals so strings are immutable in both
  • 🚨 Blocks are the one big idea with no Lua counterpart — an invisible extra parameter with its own syntax and yield to call it
  • 🚨 Assigning nil to a Hash key does NOT delete it, which is the one place nil parts company with Lua
  • Everything is an object, so numbers and nil have methods, and Enumerable gives you the map/select/reduce Lua leaves you to write
  • A def opens a fresh scope and cannot see surrounding locals — blocks and lambdas still close over them
JavaScriptAlpha⚡ Works Offline⚡ Offline

Your closest living cousin. Both are small dynamic languages built to be embedded in a host, both grew objects out of a lookup fallback rather than declaring classes, and both have real closures. The family resemblance is why the handful of differences catch people.

  • 🚨 Two falsy values become six — 0 and "" change sides, so every if value then guard needs rereading
  • The one table type splits into objects and arrays, with different methods and no mixing
  • nil becomes two values, null and undefined, and assigning one no longer deletes a key
  • self is a parameter the colon fills in; this is decided by the call site, so a detached method loses it
  • Metatables become prototypes — __index maps over almost exactly, but there is no operator overloading
  • Coroutines become generators, which is a close match — but await is not coroutine.yield: there is an event loop underneath
  • A far larger standard library, starting with the map/filter/reduce Lua leaves you to write
PythonBeta⚡ Works Offline⚡ Offline

What Lua would look like if it had stopped saying no. Both are dynamic, garbage-collected and interpreted with first-class functions and closures — then Lua draws a hard line at 250 KB of interpreter, and Python says yes to everything.

  • The pattern you hand-roll in Lua is a language feature here — classes, exceptions, comprehensions, decorators, a standard library of hundreds of modules
  • 🚨 0, "", [] and {} are all falsy, so a zero check means the opposite of what it does in Lua
  • 🚨 A missing dict key raises instead of giving you nil, and assigning None no longer deletes it
  • One table type splits four ways — list, dict, tuple and set — and indexing is 0-based with half-open slices
  • Metatables map onto dunder methods almost one for one: __index, __newindex, __add, __call, __tostring all have counterparts
  • self is an explicit first parameter, exactly what Lua's colon has been inserting for you all along
  • Division agrees exactly — Lua 5.3 took // from Python — but assigning to a captured variable needs nonlocal
CPre-Alpha

The language your interpreter is written in. Lua exists to be embedded in a C program, so C is not a foreign country — it is the host you already live inside. This is the trip across that boundary, from the guest's side.

  • No garbage collector — malloc and free, with leaks, double frees, and use-after-free all compiling silently
  • 🚨 0 is false in C, the exact inverse of the rule you know — and if (strlen(name)) means the opposite of what its Lua shape suggests
  • No tables: fixed-size, zero-based, same-typed arrays that do not know their own length, plus structs whose fields are fixed at compile time
  • Strings are mutable bytes ending in a zero, so #text becomes an O(n) strlen and == compares addresses instead of contents
  • Function pointers capture nothing — closures, and the iterators and callbacks built on them, have no equivalent
  • No pcall: errors come back as return codes you must check at every level, with no stack unwinding
  • And the payoff — writing a C function require can load, with lua_State, the virtual stack, and luaL_check*
RustPre-Alpha

The language people embed Lua from today. mlua is what a Neovim plugin, a Redis module or a game's mod host reaches for, so this is the modern counterpart of the C page — same boundary, a host language that replaced C for new work. One idea explains every strange thing about it: there is no garbage collector, so the compiler tracks who owns each value.

  • Ownership is the whole story — passing a value can give it away, where passing a table has always just shared it
  • Rc<RefCell<T>> is how you spell a Lua table: one value, several names, all able to mutate it — and writing it out shows exactly what that convenience costs
  • nil becomes Option<T>, so a missing key is a value you must open rather than one that travels three functions before failing
  • Truthiness stops existing — if 0 is a type error, so the falsy-set question goes away instead of changing
  • Metatables become traits, and the orphan rule is the exact opposite of patching the shared string metatable: two libraries can never collide
  • The honest gap: stable Rust has no generators, so coroutine.wrap becomes an iterator and two-way resume becomes a struct you write by hand
TypeScriptAlpha⚡ Works Offline⚡ Offline

A type layer over the cousin you already know. Read /lua/javascript first — this page is not that page with types bolted on. What is left is the checking: describing what Lua leaves implicit, and having it erased before anything runs.

  • 🚨 strictNullChecks is the answer to the nil that travelled three modules before it crashed — absence becomes part of the return type
  • Union and literal types replace the table of allowed strings you validate by hand
  • Discriminated unions understand your kind field, and never makes the compiler find every unhandled case
  • Structural typing, so shape is the type — duck typing made static, which should feel familiar
  • Generics carry a type through a call; Lua has no way to say a list of strings gives back a string
  • 🚨 The types are ERASED — no reflection, no run-time check, and as is a promise rather than a conversion
  • TypeScriptToLua compiles it back down to Lua, so the runtime you already ship stays put
C#Pre-Alpha

The language your host program is most likely to become — Unity scripts are C#. It is also the furthest thing from Lua on this anchor: static nominal types, a compile step, and a standard library larger than Lua's whole ecosystem. Coroutines are the bridge, because Unity resumes an IEnumerator once per frame exactly as an update loop resumes yours.

  • Truthiness stops existing — a condition must be a bool, so if (0) is a compile error rather than a surprise
  • LINQ replaces the accumulate-into-a-fresh-table loop that Lua makes you write for every filter, map and group
  • One table becomes many types — array, List<T>, Dictionary<K,V>, tuple, record, and a struct that copies on assignment
  • Metatables become declarations: __index is an indexer or a property, __add is operator +, and __eq is what record gives you free
  • coroutine.wrap is yield return and coroutine.yield is await — the same machinery, with a scheduler you no longer write
  • The one real loss: nothing can send a value into a suspended yield return, where coroutine.resume always could
GDScriptPre-Alpha

Where a Lua game developer looks next. LÖVE, Defold and Roblox all script in Lua, and Godot is the obvious step across. The syntax is a short trip; the architecture is not — you stop owning the game loop.

  • 🚨 You own the loop in LÖVE; the engine owns it here — every node has its own _process(delta), and nothing iterates your entity list
  • 🚨 0, "" and empty collections are all falsy, so a zero-health check means the opposite of what it does in Lua
  • 🚨 7 / 2 is 3 — two integers divide as integers, which bites hardest when centering something
  • One table type splits into Array and Dictionary, and reading a missing key raises instead of giving you nil
  • Blocks come from indentation, not end; and there is no accidental global, because every name must be declared
  • Optional static types, a real match that destructures, and signals instead of polling a flag every frame
  • No pcall and no exceptions at all — you check before acting, or return an error code
Drag cards to reorder · your order is saved locally