Reference

Everything Midnight writes becomes ordinary Lua 5.1 (with goto), so the runtime half of the language (values, metatables, closures, coroutines, the standard library) is Lua's, and the Lua manual still applies. This page is the syntax half. Markup and the CLI have their own pages, and Coming from Lua lists everything that behaves differently.

Most of this syntax comes from Erde, and its reference is worth a read too.

The shape of it

Blocks are braces. Statements end at the end of a line (or at a ;, if you like them). There's no then, do, or end anywhere.

local function lights_out(rooms) {
  for _, room in ipairs(rooms) {
    if room.lamp { room.lamp = false }
  }
}

Comments are the same as Lua's, including block comments with = levels for text that contains ]]:

-- until the end of the line
--[[ until the brackets ]]
--[==[ even if there's a ]] in here ]==]

Lists inside (), [], and {} can always end with a comma, which keeps diffs quiet when the list grows a line.

Values

nil, true, false, numbers, strings, tables, and functions, as in Lua. Numbers are doubles, written any way Lua 5.4 allows, plus 0b binary:

print(23, 0.5, 2e3)    -- 23 0.5 2000
print(0x17, 0b10111)   -- 23 23
print(0x1.8p1)         -- 3 (a hex float: 1.5 * 2^1)

Strings

Quotes, escapes, and [[block strings]] work like Lua's. What's new is that double quoted and block strings fill in {expressions}:

local hour, owl = 3, 'olive'

print("{owl} is up at {hour}am")    -- olive is up at 3am
print("in an hour: {hour + 1}am")   -- in an hour: 4am
print('{owl} stays literal')        -- {owl} stays literal
print("\{owl}")                     -- {owl}

Single quotes never interpolate, which makes them the easy way to write text full of braces. In the other two, \{ is a literal brace (escaping the closing } too is allowed, not required). Values go through tostring, so a nil shows up as nil rather than an error.

Add :%spec to an interpolation to run it through string.format:

local moon = 0.734
print("moon {moon * 100:%.0f}% full")  -- moon 73% full
print("color #{0x1d2b53:%06x}")        -- color #1d2b53

The spec is a single string.format directive (flags, width, precision, and one of c d i o x X e E f g G q s). The % has to come right after the :, which is how {owl:upper()} stays a method call.

A block string interpolates too, and skips a newline right after [[, as in Lua:

local bedtime = 23
io.write([[
  lights out at {bedtime}:00
  alarm at {(bedtime + 8) % 24}:00
]])

A string literal can be the start of a method call with no extra parentheses: '%02d:%02d':format(h, m).

Tables

One structure for everything, as in Lua:

local sky = { moon = 'waning', 'orion', 'lyra' }
sky.clouds = false
print(sky.moon, sky[2], #sky) -- waning lyra 2

Fields are separated by commas only (Lua's ; separator isn't allowed).

Destructuring

Declare names from a table's fields with braces, or from its array part with brackets:

local { moon, clouds } = sky
local [ brightest, second ] = sky

Each name can carry a fallback for when the value is nil, and a field can be renamed with field: name (the rename goes first, then the fallback):

local { stars = 0 } = sky                   -- sky.stars or 0
local { moon: phase } = sky                 -- phase = sky.moon
local { fog: visibility = 'clear' } = sky   -- sky.fog, or 'clear'

Parameters destructure the same way, which is handy for options tables and component props:

local function alarm({ hour = 7, snooze = true }) {
  return "alarm at {hour}, snooze {if snooze { 'on' } else { 'off' }}"
}
print(alarm({ hour = 6 })) -- alarm at 6, snooze on

Only one level deep: { a: { b } } isn't valid. Reach for a second statement instead.

Functions

Declarations look like Lua's, with braces. function T.f and function T:m work as usual:

local Lamp = {}
Lamp.__index = Lamp

function Lamp.new(room) {
  return setmetatable({ room = room, on = true }, Lamp)
}

function Lamp:off() {
  self.on = false
  print("{self.room} is dark")
}

Lamp.new('hallway'):off() -- hallway is dark

Every call needs parentheses, even with a lone string or table argument.

Defaults

A parameter can declare what it is when it arrives as nil. Any parameter can, in any order:

local function nap(minutes = 20, where, alarm = true) {
  return "{minutes}m on the {where}, alarm {alarm}"
}
print(nap(nil, 'couch')) -- 20m on the couch, alarm true

Varargs

... is Lua's. Give it a name and the extra arguments arrive as a table:

local function dreams(who, ...scenes) {
  return "{who} dreamt {#scenes} things: {table.concat(scenes, ', ')}"
}
print(dreams('cat', 'mice', 'yarn')) -- cat dreamt 2 things: mice, yarn

Arrow functions

Anonymous functions are arrows. The body is a block, or a single expression that's returned:

local is_late = (hour) -> hour >= 23 || hour < 5
local tuck_in = (kid) -> {
  kid.blanket = true
  return kid
}

A few details:

local clock = {
  hour = 2,
  show = () => print("it's {self.hour}am"),
}
clock:show() -- it's 2am

Returning several values

Lua returns several values with commas. Midnight also lets you wrap them in parentheses, which reads better once they span lines:

local function bounds(list) {
  return (
    math.min(unpack(list)),
    math.max(unpack(list)),
  )
}

Scopes

local is Lua's local. Midnight adds two more ways to declare a name.

global

global x = ... writes _G.x, and inside that block, x means the global even if a local x exists outside it:

local hour = 1

do {
  global hour = 23
  print(hour)          -- 23
}

print(hour, _G.hour)   -- 1 23

module

module marks an export. A file with module declarations returns them, collected in a table named _MODULE:

-- lamp.mn
module brightness = 0.2
module function dim(by = 0.1) {
  brightness = math.max(0, brightness - by)
}
local lamp = require('lamp')
lamp.dim()

local { dim } = require('lamp')  -- or import just what you need

_MODULE is a plain table you can modify (give it a metatable, add fields by hand). Two rules: such a file can't also return at the top level, because then it's unclear which value is the module, and the word module is taken, so Lua 5.1's module() function isn't callable.

Operators

KindOperators
Arithmetic+ - * / // % ^, unary -
Comparison== != < > <= >=
Logic&& || !
Strings and length.. #
Bits& | ~ (xor) << >>, unary ~

!= is "not equal" (Lua's ~= means xor assignment here, see Coming from Lua). && and || behave exactly like Lua's and and or: they short circuit, and give back one of their operands. // is floor division.

Bits

The VM is Lua 5.1, which has no bit operators, so these become calls into the bit32 library that Midnight bundles. They work on unsigned 32 bit values, which matters for negatives and ~:

print(0xf0 & 0x3c, 0xf0 | 0x0f, 0xf0 ~ 0xff) -- 48 255 15
print(1 << 4, 256 >> 4)                       -- 16 16
print(~0)                                     -- 4294967295

Assignment operators

Put = after any operator that isn't a comparison to update in place: +=, -=, ..=, //=, &&=, |=, and the rest. Several targets work, as with plain assignment:

local sheep, yawns = 0, 0
sheep, yawns += 10, 1
sheep *= 2

local status = 'awake'
status ..= ', barely'
print(sheep, yawns, status) -- 20 1 awake, barely

Index expressions on the left are evaluated once: in t[next_id()] += 1, next_id runs one time.

Control flow

The usual suspects, all with braces:

if hour >= 23 {
  print('late')
} elseif hour < 5 {
  print('very late')
} else {
  print('reasonable')
}

for countdown = 10, 1, -1 { print(countdown) }

for i, star in ipairs({ 'vega', 'deneb', 'altair' }) {
  print(i, star)
}

while !asleep { count_sheep() }

repeat {
  count_sheep()
} until asleep

do {
  local scratch = 'only in here'
}

break, continue, goto

break leaves the innermost loop. continue jumps to its next turn, in any kind of loop:

for hour = 20, 26 {
  if hour % 2 == 1 { continue }
  print("check on the baby at {hour % 24}:00")
}

goto label and ::label:: are Lua 5.2's, available everywhere:

for _, line in ipairs(lines) {
  for _, word in ipairs(line) {
    if word == 'goodnight' { goto done }
  }
}
::done::

match

match compares one value against a list of arms, top to bottom, and runs the first that fits:

match signal {
  'doorbell' => { wake_up() }
  'rain' | 'wind' => { roll_over() }      -- either one
  _ if snoring => { nudge() }             -- a guard
  _ => { keep_sleeping() }                -- everything else
}

Under the hood it's an if / elseif chain on a local copy of the subject:

do
  local subject = signal
  if subject == "doorbell" then wake_up()
  elseif subject == "rain" or subject == "wind" then roll_over()
  elseif snoring then nudge()
  else keep_sleeping() end
end

Which leads to the rules:

if and match expressions

Put if or match where a value goes and it produces one:

local lamp = if dark { 'on' } else { 'off' }

local sound = match animal {
  'owl' => { 'hoo' }
  'cricket' | 'frog' => { 'chirp' }
  _ => { '...' }
}

These fix the classic a and b or c bug: an if expression gives you the chosen branch's value even when that value is nil or false. With dark && nil || 'off' you'd get 'off', and midnight lint flags that pattern when it can tell.

Markup

Elements are part of the grammar. Each one compiles to a call, h(tag, props, ...children), and the midnight.markup runtime turns those into HTML:

local Star = ({ name, bright }) -> <li class={if bright { 'bright' } else { nil }}>
  {name}
</li>

local html = require('midnight.markup').render(<ul>
  <Star name="sirius" bright/>
  <Star name="polaris"/>
</ul>)
print(html)

A < only starts an element where a value can begin, so a < b is still a comparison. The Markup page covers the rest: components, state, scoped styles, and pages that update live.

Reserved words

break, continue, do, else, elseif, for, function, global, if, in, local, module, repeat, return, until, while, true, false, and nil.

goto and match only act as keywords in their own constructs. Lua's and, or, not, then, and end aren't reserved at all, so they're free to use as names. One nice side effect: a half asleep if not ready fails to compile instead of doing something odd.