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 installs three things:
moonc— the command-line compiler (.moon→.lua)moon— runs.moonfiles directlymoonscript— the Lua module for loading MoonScript from Lua code
moonc my_script.moon # compile to my_script.lua
moon my_script.moon # run directly without compiling first
To load .moon files from a Lua script at runtime:
require("moonscript")
dofile("script.moon")
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
-- 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
To create a global, use export:
export GLOBAL_NAME = "I am global"
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
Functions: Thin and Fat Arrows
Functions use -> as the basic function arrow:
add = (x, y) -> x + y
print add 10, 20 -- 30
Parentheses are optional when calling functions. The arguments apply to the nearest function to the left:
print add 10, 20 -- print(add(10, 20))
If a function takes no arguments, call it with !:
greet = -> print "Hello!"
greet! -- no parentheses needed
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.
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!
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
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
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
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
Constructor arguments can also begin with @ to auto-assign:
class Widget
new: (@name, @size="medium") =>
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
Table Literals
Table literals use : instead of = for key-value pairs:
point =
x: 10
y: 20
label: "origin"
Single-line tables can omit the curly braces:
my_function x: 1, y: 2, z: 3
You can use newlines instead of commas to separate entries:
profile =
name: "Alice"
age: 30
hobbies: {"reading", "coding"}
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 }
Square brackets let you use expressions as keys:
key = "dynamic_key"
table = { [key]: "value" }
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.
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).
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).
Loading MoonScript 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.
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