luaguides

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 .moon files directly
  • moonscript — 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