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 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
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