MoonScript: CoffeeScript for Lua
Lua is fast and lightweight, but its syntax can feel spartan if you are coming from Python, Ruby, or JavaScript. MoonScript addresses this by providing a cleaner, more expressive syntax that compiles down to standard Lua. The result is code that is easier to read and write, while remaining fully compatible with every Lua environment including LuaJIT.
MoonScript describes itself as “CoffeeScript for Lua” — the influence is obvious in its significant whitespace, arrow functions, and class system.
Installation
You install MoonScript via LuaRocks:
luarocks install moonscript
This pulls MoonScript from the LuaRocks repository and places the compiler and loader module in your Lua path. The installation is self-contained and adds no C dependencies — everything is pure Lua, so it works on LuaJIT as well as standard Lua 5.1 through 5.4.
This installs three things:
moonc— the command-line compiler (.moon→.lua)moon— runs.moonfiles directlymoonscript— the Lua module for loading MoonScript from Lua code
Each tool serves a different stage of development. During active coding you can run .moon files directly, and for deployment you compile to .lua and ship standard Lua files that need no MoonScript runtime.
moonc my_script.moon # compile to my_script.lua
moon my_script.moon # run directly without compiling first
The moonc command produces a plain .lua file you can distribute and require normally. The moon command is the faster feedback loop — it compiles and runs in one step, so you never leave the MoonScript syntax. Both commands understand the same .moon input, so the output is identical either way.
To load .moon files from a Lua script at runtime:
require("moonscript")
dofile("script.moon")
Registering the moonscript module tells Lua’s require machinery how to locate and compile .moon files. After the require call, dofile and require both work on .moon files as if they were ordinary Lua — the compiler runs transparently in the background.
Whitespace-based syntax
The most visible difference from Lua is that MoonScript uses indentation instead of do/end keywords to delimit blocks:
-- Lua
if x > 0 then
for i = 1, 10 do
print(i)
end
end
This is the familiar Lua pattern: every control structure introduces a block, and every block must be explicitly closed. It is unambiguous and easy to parse, but the closing keywords accumulate quickly in nested logic. In a deeply nested function you can end up with three or four end statements stacked at the bottom, which makes reformatting fragile and indentation mistakes hard to spot.
-- MoonScript
if x > 0
for i = 1, 10
print i
No then, no do, no end. The indentation does all the work. MoonScript does not care whether you use spaces or tabs, only that you are consistent; two spaces is the convention.
Variables and scope
All variables are local by default in MoonScript. If you try to assign to an undeclared name, MoonScript automatically declares it as a local:
x = 100 -- local x (auto-declared)
name = "Alice" -- also local
This is a deliberate inversion of Lua’s default. In Lua, variables are global unless you write local. In MoonScript, the opposite is true: everything is local, and you opt into globals explicitly. This eliminates one of the most common sources of accidental bugs — forgetting a local keyword and silently polluting the global environment, which can cause mysterious collisions between unrelated modules.
To create a global, use export:
export GLOBAL_NAME = "I am global"
The export keyword makes the variable visible outside the current scope. Under the hood, MoonScript compiles this to a standard Lua global assignment. The key advantage is that globals are now a conscious choice, not an accident of omission. When another developer reads your code, export signals that this value is intentionally shared across modules.
To import specific fields from a table into local scope, use import:
import concat, insert from table
concat {"a", "b", "c"} -- table.concat
insert list, "new" -- table.insert
The import statement works much like destructuring imports in JavaScript or ES6. It pulls named fields from a module table into your local scope, which reduces typing and makes frequent calls to standard library functions much more concise. Under the hood, MoonScript generates explicit local declarations — local concat = table.concat — so the compiled output is clear and predictable.
Functions: thin and fat arrows
Functions use -> as the basic function arrow:
add = (x, y) -> x + y
print add 10, 20 -- 30
The -> arrow is MoonScript’s most iconic piece of syntax. It replaces both the function keyword and the return statement: the expression after the arrow is implicitly returned. This makes single-expression functions — mapping, filtering, simple calculations — a single line instead of three. In Lua you would write function(x, y) return x + y end, which stretches the same logic across more tokens.
Parentheses are optional when calling functions. The arguments apply to the nearest function to the left:
print add 10, 20 -- print(add(10, 20))
This is unlike Lua, where you must always use parentheses. MoonScript’s optional parentheses make function composition read more like a pipeline, but they also introduce ambiguity if you are not careful. The rule is simple: arguments bind to the nearest callable to the left, so print add 10, 20 is always print(add(10, 20)), never print(add(10), 20). When in doubt, add explicit parentheses to make your intent clear.
If a function takes no arguments, call it with !:
greet = -> print "Hello!"
greet! -- no parentheses needed
The ! operator is the zero-argument shorthand — it is equivalent to () but reads more naturally as “invoke this” rather than “call with empty args.” It pairs especially well with functions that act as commands or side-effect triggers, which often take no arguments.
The fat arrow: methods with self
The fat arrow => creates a function that automatically binds self to the first argument, exactly what you need for methods:
counter =
count: 0
increment: (n=1) => @count += n
counter\increment 5
print counter.count -- 5
Without =>, @count would be nil because the function would not have a self reference. The fat arrow is what makes MoonScript’s OOP features work: it ensures that every method body has access to the instance table through self, which the @ shorthand then references directly.
Default arguments
Function arguments can have default values. Any argument that is nil at call time gets replaced with its default:
greet = (name="world") -> print "Hello, #{name}!"
greet! -- Hello, world!
greet "MoonScript" -- Hello, MoonScript!
MoonScript uses Lua’s own nil semantics to decide when to apply a default. If you pass nil explicitly — greet nil — the default still kicks in, which is consistent with Lua’s “absent argument is nil” model. This differs from languages like Python, where defaults only activate when the argument is truly omitted from the call site, not when None (the closest Python equivalent to nil) is passed intentionally.
Default values are evaluated in order, so later defaults can reference earlier arguments:
scale = (x=10, y=x*2) -> print x, y
scale! -- 10, 20
scale 5 -- 5, 10
This sequential evaluation is a compile-time transformation: MoonScript generates a series of if arg == nil then checks, and each one occurs after the previous argument has been resolved. Because the defaults are evaluated left to right at function entry, y’s default can safely depend on x — MoonScript compiles it into a temporary local that holds the resolved value of the first argument before computing the second.
Implicit return
MoonScript coerces the last statement in a function body into a return. This means you often omit the return keyword:
sum = (x, y) -> x + y -- implicitly returns x + y
first = (a, b) -> a -- implicitly returns a
The implicit return rule is one of MoonScript’s most opinionated departures from Lua. Because every function body ends with an expression, the compiler knows the final value and wraps it in a return statement automatically. This eliminates one of the most common Lua boilerplate patterns — typing return before the value at the end of every function — but it does require you to think differently about function design. A function that ends with an assignment like x = 10 returns the assigned value, not nil, because the assignment itself evaluates to the right-hand side in Lua semantics.
If the last statement is an assignment with no expression (like x = 10), nothing is returned.
Classes
MoonScript has a built-in class system. You declare a class with class, and use extends for inheritance:
class Animal
new: (@name, @age=0) =>
speak: => print "#{@name} makes a noise"
class Dog extends Animal
speak: =>
print "#{@name} barks"
rex = Dog "Rex", 3
rex\speak -- Rex barks
Under the hood, MoonScript’s class compiles to a Lua metatable-based inheritance chain. The extends keyword creates a parent-child prototype relationship: Dog has Animal as its __index fallback. Method calls follow the prototype chain naturally, so rex\speak finds the method on Dog first. This is the same object model that Lua developers build by hand, but MoonScript writes the boilerplate for you — no manual metatable setup, no self.__index = self incantation.
The new method (or any method named new) is the constructor. Prefixing a parameter with @ assigns it directly as an instance property:
class Point
new: (@x, @y) =>
p = Point 10, 20
print p.x -- 10
print p.y -- 20
The @ prefix on a constructor parameter is a syntactic shortcut that expands to self.x = x inside the function body. It works exclusively in method definitions — specifically those defined with => (the fat arrow) — because the fat arrow provides the self reference that @ depends on. Without =>, the compiler does not know what table @x should be assigned to.
Constructor arguments can also begin with @ to auto-assign:
class Widget
new: (@name, @size="medium") =>
The @ shortcut inside a constructor parameter list is one of MoonScript’s most efficient patterns. Each @-prefixed parameter automatically generates an assignment statement that stores the constructor argument as an instance field, which lets you define a class with several fields in a single line of parameter declarations rather than writing out self.field = field for each.
Calling the parent class
The special super function calls the parent method of the same name:
class BufferedWriter
new: (@buffer_size=1024) =>
@buffer = {}
write: (data) =>
table.insert @buffer, data
class CountingWriter extends BufferedWriter
write: (data) =>
super data
@line_count or= 0
@line_count += 1
The super keyword behaves like an alias for the parent class’s method: super data compiles to BufferedWriter.write(self, data). You can call super from any method in the child class, and MoonScript resolves the target method automatically by walking the prototype chain. This is especially useful when you want to augment parent behavior — run the parent’s logic first, then add extra work — rather than replace it entirely. The or= operator in the child constructor also deserves attention: it is MoonScript’s shorthand for “assign if nil,” which initialises counters and caches without cluttering the constructor with if not self.field then checks.
Table literals
Table literals use : instead of = for key-value pairs:
point =
x: 10
y: 20
label: "origin"
The colon syntax is one of the first things Lua developers notice about MoonScript. In Lua, { x = 10 } uses = inside tables, which clashes visually with assignment statements outside tables. MoonScript unifies the syntax: : pairs keys and values everywhere — in tables, in function calls, and in property definitions. The compiled output is standard { x = 10 } Lua, so there is no runtime difference; the change is purely aesthetic.
Single-line tables can omit the curly braces:
my_function x: 1, y: 2, z: 3
When you pass key-value pairs as function arguments, MoonScript infers the table wrapper automatically. The call my_function x: 1, y: 2, z: 3 compiles to my_function({ x = 1, y = 2, z = 3 }). This makes functions that accept a single configuration table — a common Lua pattern — read like named-argument calls in languages that support them natively. It is one of MoonScript’s most ergonomic features for library authors who design APIs around option tables.
You can use newlines instead of commas to separate entries:
profile =
name: "Alice"
age: 30
hobbies: {"reading", "coding"}
Newline-separated entries remove visual noise from multiline tables. The compiler treats each line break as an implicit comma, so you never need to remember whether the last entry needs a trailing comma. This is safe because the compiler only inserts commas between complete key-value pairs — it will not insert one mid-expression.
Table shorthand
When a variable name and a table key are the same, use the : prefix:
name = "Alice"
age = 30
person = { :name, :age }
-- compiles to { name = "Alice", age = 30 }
This is much like JavaScript’s shorthand property notation ({ name, age }), and it serves the same purpose: cutting repetitive name = name pairs that clutter constructors and data-aggregation functions. Under the hood, :name compiles to name = name, so the variable must exist in scope at the point of use.
Square brackets let you use expressions as keys:
key = "dynamic_key"
table = { [key]: "value" }
This bracket-key syntax maps to Lua’s [expression] = value table constructor syntax, which is how Lua handles computed keys natively. MoonScript keeps this identical to the Lua form so there is no new concept to learn — any expression that evaluates to a hashable type (string or number) can serve as a key, which makes this useful for building lookup tables from runtime data like configuration keys or user input.
Table comprehensions
MoonScript supports list comprehensions over iterators:
-- Double every number
nums = [x * 2 for x in *{1, 2, 3, 4, 5}]
-- {2, 4, 6, 8, 10}
-- Filter even numbers
evens = [x for x in values when x % 2 == 0]
-- Key-value pairs from a table
pairs_list = [{k, v} for k, v in pairs my_table]
The *{...} syntax expands a literal array argument so it can be used with for ... in syntax.
These comprehensions compile to standard Lua for loops that build a table one element at a time. The when clause translates to an if guard inside the loop body, so nothing is added to the result unless the condition holds. This is functionally identical to what you would write by hand, but with far less ceremony — a one-liner replaces several lines of accumulator setup, iteration, conditional logic, and returning the result table.
The with statement
with gives you a shorthand for calling multiple methods on the same object:
file = with io.open "data.txt", "r"
.close! -- io.close(file)
obj =
init: => @value = 100
double: => @value *= 2
with obj
\init!
\double!
print .value -- 200
Inside the with block, .method() is shorthand for method(obj).
The with statement is inspired by Visual Basic and Pascal — it temporarily re-binds the “current object” so every dot-prefixed call implicitly targets that object. The .close! call above compiles to file:close(), and print .value compiles to print(file.value). This is particularly useful for resource management patterns where a file handle, socket, or database connection needs several setup calls before use, because it keeps the variable name out of each call and makes the sequence read like a checklist.
Update operators
MoonScript adds compound assignment operators that Lua lacks:
x = 10
x += 5 -- x = 15
x -= 3 -- x = 12
s = "hello"
s ..= " world" -- s = "hello world"
enabled = true
enabled and= false -- enabled = false
!= is also available as an alias for ~= (Lua’s not-equal operator).
Lua does not have +=, -=, or any compound assignment operators. MoonScript adds them at compile time by expanding x += 5 into x = x + 5. The ..= operator for string concatenation is particularly convenient because Lua’s .. operator is verbose and easy to mistype when combined with assignment. The logical assignment operators and= and or= follow the same expansion rule, though they are used less frequently in practice.
Loading .moon files from Lua
The moonscript module registers itself as a loader for .moon files, so you can require MoonScript files directly from Lua:
-- main.lua
package.preload["my_script"] = function(...)
local Moonscript = require("moonscript")
local ok, result = Moonscript.loadfile("my_script.moon")
if not ok then error(result) end
return result
end
-- Or simpler with moonscript registered:
require("moonscript")
local script = require("my_script") -- loads my_script.moon
The moonscript module also gives you moonscript.loadstring(code) and moonscript.compile(code) for working with code as strings.
The preload approach shown above is the most explicit integration pattern: you define a Lua chunk that compiles the .moon file on demand and register it in package.preload so require can find it. The simpler approach with require("moonscript") installs a global loader that intercepts all require calls — when Lua’s module system cannot find a .lua file, it tries .moon instead and the MoonScript loader compiles it transparently.
Compiling to file for distribution
If you want to distribute Lua code without requiring MoonScript at runtime, compile ahead of time:
moonc --output output.lua input.moon
This gives you a .lua file you can distribute and require normally. Many projects use this approach: write in MoonScript during development, ship the compiled Lua.
When to use MoonScript
MoonScript is a good fit when you want:
- Cleaner syntax for complex Lua projects without runtime dependencies
- A class system without pulling in additional libraries
- Iterators and comprehensions that make data transformation readable
It is less ideal when:
- Your team does not want a build step
- You need to share code with people who do not know MoonScript
- You are writing Roblox scripts (Roblox uses Luau, a different dialect)
See Also
- lua-metatables — Lua’s object system that MoonScript’s classes build on through metatables
- lua-closures — closures and scope, which MoonScript uses extensively for its class and function syntax
- lua-penlight-utilities — another Lua utility library that works well alongside MoonScript-compiled code