← All academies

LEARN BY DOING

Lua Academy

Small discoveries. Useful scripts. Start with a failing test, learn the language, and build confidence one green result at a time.

Begin your first koan →

No account needed. Exercises run in your browser. Your drafts stay on this device.

First steps

Thinking in Lua

Working with data

koan · 8 min

Add every quantity

Lua arrays begin at index 1. ipairs visits the sequential entries. Start an accumulator at zero; this also gives an empty array a useful result.

koan · 8 min

Start at one

The first element of a Lua array is at index 1. This exercise uses a guaranteed nonempty array; later lessons handle missing elements with optional types.

koan · 8 min

Keep positive quantities

Build a new array instead of changing the input. table.insert appends one value. Preserve the order of all positive quantities.

koan · 8 min

Transform every entry

A transformation produces one output for every input. Convert centimetres to millimetres while preserving array order and leaving the input untouched.

types · 8 min

Read a named price

A map type describes keys and values. Bracket lookup allows a key to come from a variable, unlike dot lookup which names a literal field.

koan · 8 min

Total named stock

pairs visits key/value entries in a table, including string keys. Sum values without depending on the iteration order.

koan · 8 min

Copies and aliases

Assigning a table to another variable shares the same table. Create a fresh record when an update must leave the original unchanged.

koan · 8 min

Measure text

Use the supported string library to measure a simple ASCII identifier. This example deliberately avoids Unicode boundary assumptions: byte length and user-perceived characters are different concepts.

koan · 8 min

Normalise a code

Normalisation reduces accidental differences in input. Convert an ASCII product code to uppercase using the string library.

koan · 8 min

Format a label

string.format combines a format string and typed arguments. Produce a label with a name, a colon and an integer count.

Functions and modules

Typed Lua

types · 8 min

Name your data

A record type describes fields and their types. An annotation is checked before execution; it documents the shape a function accepts. Implement a subtotal from quantity and unitPrice.

types · 8 min

Handle a missing name

The type string? admits a string or nil. Narrow the value with a nil check before concatenating it. Missing and empty are different cases.

types · 8 min

Model a status

A literal union restricts a parameter to a small set of values. Use the declared states to decide whether an order may ship.

types · 10 min

Make the return promise true

A return annotation describes every path through the function. Repair the string returned from a number-producing calculation.

types · 10 min

Keep array elements consistent

An array annotation applies to every element. Replace the incompatible string with the required number.

types · 10 min

Respect named fields

Record fields carry their own types. A quantity should remain numeric even when it comes from a form or document.

types · 10 min

Narrow a union

A union admits more than one shape. Use type(value) to distinguish text from numbers, then return a number on both paths.

types · 10 min

Read an optional field

Optional fields may be absent. Return a label using the nickname when supplied and the required name otherwise.

types · 10 min

Make array absence explicit

Strict nil mode makes an indexed lookup optional. Handle an empty array rather than assuming the first entry exists.

types · 10 min

Describe map values

A map annotation constrains each value while allowing different string keys. Build a map of numeric stock levels.

types · 10 min

Keep callback contracts

A callback annotation describes both input and output. Return a function that formats a number as text.

types · 10 min

Describe nested data

Types can name nested records and arrays. Read the address from a typed customer record rather than duplicating its fields in every function.

types · 10 min

Reuse a domain type

A type alias gives a meaningful name to a record. Use the same dimensions contract for each argument and compute whether their sizes match.

Test-driven development

tdd · 8 min

Test the boundary first

Write a test that exposes the faulty free-shipping boundary. Then repair the implementation. Your tests will also run against a correct implementation and a deliberately broken one; an always-passing test is not enough.

tdd · 8 min

Protect the empty case

A useful test suite covers ordinary input and a boundary. Write tests for summing quantities, including the empty list. Then implement the function and refactor it while keeping tests green.

koan · 10 min

Compare table contents

Tables have identity as well as contents. Test helpers use deep_eq to compare nested data. Repair the function so its nested result matches the expected structure.

koan · 10 min

Compare calculated decimals

Floating-point arithmetic can introduce tiny rounding differences. near expresses a deliberate numeric tolerance; it does not excuse a substantially wrong calculation.

koan · 10 min

Test a rejected input

A function may reject invalid input with error. The raises matcher calls a supplied function and expects a catchable error. Resource exhaustion is not a catchable validation error.

koan · 10 min

Cover a table of examples

t.each creates a named case for each row of data. Read the input/output table before implementing clamp: values below zero become zero, values above ten become ten, and the middle stays unchanged.

koan · 10 min

Share deterministic setup

before_each prepares a case without coupling it to another case’s state. The cache mock is reinstalled for every test. Implement a reader that forwards its key and respects the success flag.

Testing the platform

koan · 10 min

Read a mocked cache

The exported contract describes ctx.cache.get as an (ok, value) call. A strict mock supplies both results. No real cache or network request is involved.

koan · 10 min

Handle a failed host call

A failed async-style host call returns false and an error message. Do not return that message as successful business data.

koan · 10 min

Verify the requested key

A spy records how a mocked host function was called. Forward the caller’s key unchanged instead of hard-coding a cache key.

koan · 10 min

Avoid duplicate calls

Repeated host calls can duplicate work or side effects. The called_once matcher asserts that a single request causes one cache read.

koan · 10 min

Use an argument-aware fake

A function fake computes responses from its arguments. The production function should use the requested key and handle the success flag.

koan · 10 min

Mock a side effect

Host writes are mocked too. ctx.cache.set accepts a record with key, value and optional ttl. Preserve the requested key and value.

koan · 10 min

Keep invalid input local

Validate before calling the host. An empty key should return false without attempting a cache delete. Strict mocks help expose unintended calls.

koan · 10 min

Handle changing responses

A sequence mock returns a different tuple on each call. This exposes code that ignores later responses or caches a result incorrectly.

koan · 10 min

Move time without waiting

The testing clock is controlled by the test. Advance milliseconds instantly to test elapsed time. There is no real wait, and the result is repeatable. Implement elapsed(start) using the granted time capability.

diagnostic · 10 min

Expect a type error

Some type exercises succeed when invalid code is rejected. Change the valid cache call into one that passes a number instead of a string. The academy checks for the precise type.bad-argument diagnostic and never executes this snippet.

Using the ctx

Guided platform examples with contract-checked mocks. Read configuration, simulate cache calls, handle GraphQL responses and verify logs entirely in your browser.

Practical scripting

Capstones