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.
No account needed. Exercises run in your browser. Your drafts stay on this device.
First steps
Your first green test
A test compares an actual value with an expected value. Run the starter, read the failure, then change the exported answer. Lua chunks can expose functions and values through the platform’s exports table.
koan · 8 minBuild a greeting
Lua joins strings with two dots. Keep the separator inside a string so the greeting has a space before the name.
koan · 8 minKeep state local
A local variable belongs to its lexical scope. Update a local accumulator before exporting its value. This prevents accidental global names and makes the calculation easy to follow.
koan · 8 minFind complete batches
The remainder operator % reports what is left after division. A batch is complete when its quantity divides evenly by the batch size.
Thinking in Lua
Zero is true
Only false and nil are falsy in Lua. Zero and the empty string are truthy. Predict the result before changing the expression.
koan · 8 minChoose a shipping band
Conditions choose between behaviours. A boundary deserves its own test: free shipping starts at exactly 100, not above it.
koan · 8 minCount inclusively
A numeric for loop includes its end value. Compute the sum from 1 through n, including n. For n = 0 the loop performs no iterations.
koan · 8 minDouble until ready
A while loop tests its condition before each iteration. Starting at 1, double the capacity until it is at least the requested size. Ensure each iteration moves towards termination.
koan · 8 minReject invalid quantities
An early return handles an exceptional case before the main calculation. Return zero for negative quantities; otherwise multiply by the price.
koan · 8 minCombine conditions
Use and when both conditions must hold. An order may proceed only when it is paid and not cancelled. Lua’s logical operators short-circuit.
types · 8 minDefault only when missing
The or operator is useful for defaults, but it replaces false as well as nil. Preserve an explicit false preference and default only nil to true.
Working with data
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 minStart 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 minKeep 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 minTransform 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 minRead 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 minTotal named stock
pairs visits key/value entries in a table, including string keys. Sum values without depending on the iteration order.
koan · 8 minCopies 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 minMeasure 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 minNormalise a code
Normalisation reduces accidental differences in input. Convert an ASCII product code to uppercase using the string library.
koan · 8 minFormat 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
Return a range
Lua functions can return more than one value. The caller assigns those results to separate locals. Implement a function that returns the smaller number first.
koan · 8 minExtract a calculation
A local helper can describe one reusable calculation. Export a function that adds the areas of two identical rectangles.
types · 8 minCapture a multiplier
A closure retains access to variables in its surrounding scope. Return a function that multiplies its argument by the captured factor.
types · 8 minAccept behaviour as an argument
A function type describes a callback. Apply the provided number-to-number function twice. This lets callers choose the transformation.
koan · 8 minUse a supplied module
require loads a named module. In the academy, modules are explicit in-memory files; missing modules are never fetched from the network. Use the supplied units helper to convert metres to millimetres.
Typed Lua
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 minHandle 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 minModel 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 minMake the return promise true
A return annotation describes every path through the function. Repair the string returned from a number-producing calculation.
types · 10 minKeep array elements consistent
An array annotation applies to every element. Replace the incompatible string with the required number.
types · 10 minRespect named fields
Record fields carry their own types. A quantity should remain numeric even when it comes from a form or document.
types · 10 minNarrow 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 minRead an optional field
Optional fields may be absent. Return a label using the nickname when supplied and the required name otherwise.
types · 10 minMake array absence explicit
Strict nil mode makes an indexed lookup optional. Handle an empty array rather than assuming the first entry exists.
types · 10 minDescribe map values
A map annotation constrains each value while allowing different string keys. Build a map of numeric stock levels.
types · 10 minKeep callback contracts
A callback annotation describes both input and output. Return a function that formats a number as text.
types · 10 minDescribe 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 minReuse 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
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 minProtect 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 minCompare 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 minCompare 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 minTest 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 minCover 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 minShare 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
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 minHandle 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 minVerify 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 minAvoid 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 minUse 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 minMock 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 minKeep 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 minHandle 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 minMove 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 minExpect 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.
Read configuration with a fallback
Use ctx.config.get to read a setting, and simulate success and failure without accessing a real account.
koan · 12 minSimulate a cache with a function fake
Use an argument-aware mock for ctx.cache.get and handle both missing values and host failures.
koan · 12 minVerify writes and expiration
Mock ctx.cache.set, assert the full request record, and propagate a failed write.
koan · 12 minHandle GraphQL response envelopes
Mock ctx.graphql.query and distinguish host failure, GraphQL errors and successful data.
koan · 12 minAssert structured logging
Use spies to check ctx.log.info while keeping private payloads out of log attributes.
Practical scripting
Handle a catchable failure
pcall returns a success flag and a result or error. Keep a recoverable error from escaping the function and return a deliberate fallback.
koan · 10 minSeparate validation and calculation
Keep validation at the boundary and calculation inside it. Reject negative width or height; otherwise compute area, including a zero-area shape.
koan · 10 minPreserve source records
A normalisation pipeline should not unexpectedly mutate its input. Build a new record with an uppercase code and retain the quantity.
koan · 10 minMake a workflow decision
A workflow decision is easier to test as a pure function. Route paid orders with stock to production, paid orders without stock to waiting, and unpaid orders to payment.
koan · 10 minKeep unique identifiers
Use a set-like map to remember identifiers already seen. Return each identifier once, preserving the order of its first appearance.
Capstones
Build an order summary
Combine records, arrays, loops and branches. Export summarize(lines), returning quantity and total. Each line has quantity and unitPrice. An empty order has zero quantity and zero total. Keep this calculation pure: it needs no platform calls.
capstone · 10 minBuild an inventory decision
Build a pure allocation function. For each line, allocate the smaller of requested and available, never a negative amount. Return allocations in input order and the total allocated quantity. Keep the input unchanged.
capstone · 10 minBuild a resilient cached lookup
Implement lookup(key, fallback). Try the cache once. On success return its value. On failure call fallback(key), write that result to the cache, and return it. Test the behaviour without accessing any real platform services.