Functions and Control Flow

Lux keeps Lua's imperative execution model, but removes the ceremony that makes large GLua files hard to scan. The compiler lowers the extra syntax into direct Lua 5.1 shapes, so the runtime remains predictable.

This page focuses on syntax that changes statement and expression flow.

fn Functions

A fn declaration creates a local function binding. If the body is a single expression, Lux returns that expression.

Single-expression function
fn clamp01(value) value < 0 ? 0 : value > 1 ? 1 : value

Block functions may also end with a tail expression. The tail expression becomes the return value unless it is suppressed with a semicolon.

Block function with implicit return
fn tierForExp(exp) {
  stopif exp == nil, "guest"
  stopif exp < 25, "regular"
  "veteran"
}

Top-level function declarations are hoisted across the whole module. That means module parts can call each other's top-level functions without depending on part order.

Top-level function hoisting
fn renderLabel(player) formatName(player)

fn formatName(player) {
  player:Nick()
}

Only simple top-level fn bindings are hoisted. Top-level non-function locals are initialized in part order and cannot be used before initialization.

Default Parameters

Default parameters are applied only when the argument is nil. This matches Lua conventions and avoids treating false as missing.

Defaults are nil-only
fn alpha(color, amount = 255) {
  Color(color.r, color.g, color.b, amount)
}

Defaults are evaluated from left to right, so later defaults may depend on earlier parameters after their defaults have been applied.

Defaults evaluate in parameter order
fn rect(x = 0, y = x, w = 100, h = w) {
  { x = x, y = y, w = w, h = h }
}

GLua Method Declarations

Lux keeps Lua's colon method semantics. A colon method receives self exactly as Lua would.

Colon method
fn PANEL:Paint(w, h) {
  self:DrawBackground(w, h)
}

When you need a function field without implicit self, use a dot path.

Dot function field
fn API.draw(panel, w, h) {
  panel:PaintManual()
}

Exported declarations must create module-scope bindings. Method path declarations are not exportable as module bindings; wrap them in a named function when you need public API.

Implicit Return and Semicolons

The final expression in a block is returned. This is intentionally limited to tail position; Lux does not turn every expression statement into a return.

Tail expression return
fn describe(value) {
  local kind = type(value)
  `value is ${kind}`
}

Add a semicolon when the final expression is meant only for side effects.

Semicolon suppresses implicit return
fn paint(panel, w, h) {
  draw.RoundedBox(8, 0, 0, w, h, Color(20, 20, 20));
}

Use explicit return when you need Lua's multiple-return forwarding.

Explicit return preserves multiple results
fn readPair() {
  return next(cache)
}

Conditional Expressions

if can be used as an expression. It lowers to a temporary and ordinary Lua branching, so each branch runs only when selected.

if as an expression
local label = if ply:IsAdmin() {
  "admin"
} else {
  "player"
}

The short conditional expression is useful in compact value positions. Parenthesize nested forms when readability would otherwise suffer.

Short conditional expression
local color = health <= 0 ? deadColor : health < 25 ? dangerColor : okColor

Guard Statements

stopif returns early when a condition is true. It is meant for validation and early exits at the top of functions or loops.

Basic stopif
fn displayName(ply) {
  stopif not IsValid(ply), "<invalid>"
  ply:Nick()
}

stopifn is the inverted form: stop when the expression is falsey.

stopif and stopifn
fn requireInventory(ply) {
  stopif not IsValid(ply), nil
  stopifn ply.inventory, nil
  ply.inventory
}

Inside loops, stopif may target break or continue.

Loop guards
for _, ply in ipairs(player.GetAll()) {
  stopif not IsValid(ply), continue
  stopif ply:IsBot(), continue
  sendHudUpdate(ply)
}

The exact continue lowering depends on the surrounding loop shape, but the observable behavior is the same: skip the rest of the current iteration.

Arrow Functions

Arrow functions are expression-friendly anonymous functions. They are useful with array helpers, hooks, and small callbacks.

Plain arrow function
local names = arr.map(players, (ply) => ply:Nick())

Use self => when the generated function should receive an explicit self parameter.

Implicit self arrow
button.DoClick = self => openPanel(self.owner)

For long callbacks, prefer a named fn; diagnostics and stack traces are easier to read.

Optional Access

Optional access stops evaluating the chain when the left side is nil. Lux lowers it through temporaries so complex receivers are evaluated once.

Safe colon call
local nick = ply?.GetActiveWeapon?.():GetPrintName()

Indexes and call arguments after a failed optional step are not evaluated.

Safe index delays evaluation
local item = inventory?.items?[expensiveSlot()]

Optional access only protects the steps marked with ?. or ?[]. Ordinary access after the optional result behaves like ordinary Lua.

?? Nil Coalescing

?? selects the right side only when the left side is nil. Unlike or, it preserves false.

nil coalescing is not or
local enabled = config.enabled ?? true

When mixing ?? with comparisons, group the expression you mean. The compiler keeps the precedence explicit rather than guessing.

Group ?? with comparisons
local visible = (settings.alpha ?? 255) > 0

Optional chains compose naturally with ??.

Optional chain in a relational expression
local isLoaded = (weapon?.Clip1() ?? 0) > 0

Destructuring Bindings

Object destructuring reads named fields into locals.

Object destructuring
local { x, y, width = w, height = h } = rect

Array destructuring reads one-based positions, matching Lua arrays.

Array destructuring
local [first, second] = values

Destructuring does not automatically protect against nil receivers. Use ?? or a guard when the source may be absent.

Fallback before destructuring
local { title = name } = item ?? {}

Table Spread

Table spread copies fields from source tables into a new literal. Later entries overwrite earlier entries.

Table spread overwrites in order
local style = {
  ...baseStyle,
  color = Color(255, 255, 255),
  ...stateStyle,
}

Lux does not deep-clone nested tables. If a nested table must be independent, clone it explicitly with a helper.

Compound Assignment

Compound assignment is syntax for reading, computing, and writing the same place.

Simple compound assignment
count += 1
label ..= "!"

Complex left-hand sides are evaluated once. This matters for expressions with function calls or metamethod-heavy indexing.

Complex left-hand side evaluates once
inventory:getSlot(slot).count += amount

Pipelines

The pipeline operator passes the left value into an explicit _ placeholder in the right expression.

Explicit-placeholder pipeline
local label = player
  |> _.GetActiveWeapon(_)
  |> formatWeapon(_)
  |> string.upper(_)

The _ placeholder belongs to the immediate pipeline expression. It does not leak into nested function literals.

Pipeline placeholder does not enter nested functions
local out = values |> arr.map(_, (x) => x + _)

If a nested callback needs the piped value, bind it before the callback or pass it as an explicit argument.

do Expressions

do expressions let you compute a value with local statements while staying in expression position.

do expression
local label = do {
  local nick = ply:Nick()
  `[${team.GetName(ply:Team())}] ${nick}`
}

Use do when a value needs temporary locals but extracting a named function would hide nearby context.

Template Strings

Template strings concatenate strings and tostring conversions.

Template string
local line = `${ply:Nick()} has ${score} points`

Template strings are not format strings. Use string.format when you need number formatting or padding.

Multiple Returns and Varargs

Lux preserves Lua's multiple-return rules where they matter: explicit return, tail calls, and final expression positions.

Multiple-return forwarding
fn forward(...) {
  return hook.Run("LuxEvent", ...)
}

Local multiple assignment still follows Lua rules. Only the last expression in an expression list can expand to multiple values.

Local multi-bindings keep Lua rules
local key, value = next(tbl)
local first, second, fallback = maybeMany(), "fallback"

When generated Lua would need extra temporaries, Lux preserves the same observable ordering as the source expression.