Design and Trade-offs¶
A language is a set of decisions, and decisions have costs as well as benefits. This chapter argues for wick’s decisions honestly: it puts the wick and Lua builds of the same real game side by side, it says plainly where Lua still reads better, and it explains the roadmap philosophy that makes v0.3’s omissions deliberate rather than embarrassing.
The same game, twice¶
Lantern Night is the proof. It ships in both languages, as two deliberately separate projects — games/showcase in Lua and games/showcase_wick in wick — kept side by side so the two languages stay comparable on the same real game. Read the pair like a Rosetta stone. Three excerpts carry the argument.
The hi-score load¶
This is the bug from Chapter 4, in its natural habitat. The Lua build shipped with a frame-1 crash:
-- Lua: a 0-byte save makes load_save return "" (truthy!), the
-- fallback never fires, and tonumber("") = nil poisons everything.
local best = tonumber(lt.load_save("lanternnight_best") or "0")
The wick build cannot express that bug. The line below is the only version that compiles, because both ways the value can be absent — no file, empty file — are forced to be handled:
// wick: the ONLY version the compiler accepts.
let best = num(lt.load_save("lanternnight_wick_best") ?? "") ?? 0
Here wick wins outright. The safe code is shorter than the buggy Lua code, and the unsafe code does not compile.
Records earned their place¶
Wick 0.1 represented lamps and props with parallel scalar lists. That kept the compiler small but scattered each logical object across several lists. KORA’s kitchen made the cost concrete: five lists had to remain in step. Wick 0.2 therefore admitted flat records; Chapter 5 explains their checked fields and list storage. Lua still supports richer nested tables and methods; Wick intentionally does not claim that whole surface.
Wick 0.3 follows the same rule. The processor-building project needs masks, shifts, register wrapping, and readable bus values. Bit Lab exercises those operations directly; Chapter 9 documents the result. The new functions use the existing typed-native mechanism without changing expression precedence.
Multiple returns¶
A smaller loss. Lua returns the nearest lamp and its distance together:
local i, dist2 = nearest_unlit()
wick has no multiple returns, so the two queries are split, and “no lamp” becomes a sentinel index rather than an absent second value:
let i = nearest_unlit() // returns the index, or -1
let d = dist2_to(i) // second query, explicit
This is more verbose, but it is not worse in kind — the split is mechanical, and the sentinel is a well-understood idiom. Multiple returns are on the roadmap; their absence is an inconvenience, not a hazard.
What you stop writing¶
Set against those losses is a real reduction in the defensive code a dynamic language demands. Porting Lantern Night to wick, the following simply left the source:
-
The
if x ~= nilchains around every save and parse. The compiler tracks nullability, so you write??once at the boundary and the value is a plainTthereafter. -
The
math.prefixes.sin,floor,rand, and their kin are built-ins with no namespace. -
The
string.formatcalls that existed only to print an integer without a trailing decimal.str(n)prints integers cleanly. -
The argument-order paranoia on engine calls. Arity and types are checked at compile time with the call name and the argument number in the message, so a mistake is caught the moment it is written, not discovered by a wrong-looking frame.
What stays the same¶
It is worth being clear about what the language change did not disturb, because it bounds the cost of adopting wick. The engine API is call-for-call identical: lt.draw(...) is the same line in both builds. The frame contract — update(dt) and draw() — is the same. Hot reload and the error screen behave the same. And the two games render the same frames, which is not a figure of speech: the deterministic screenshot tests of Chapter 6 hold the wick and Lua builds to the same pixels. wick changed the language, not the engine and not the workflow.
The v0.3 limits¶
wick is deliberately small — Lua-1.0 small. Every omission below is a known gap with a workaround today and a place on the roadmap, not a surprise waiting to be discovered. The bar for v0.3 was fixed and narrow: run real lantern games, kill the Lua bug classes, and stay around two thousand lines.
| Not in v0.3 | Workaround today |
|---|---|
| Closures, nested and first-class functions | top-level fn plus module globals |
| Multiple return values | two functions, or out-of-band state |
Nested containers (list<list<num>>) |
index arithmetic on a flat list |
Modules / imports (one main.wick) |
it is a game script, not an app platform — yet |
for x in list iteration |
for i in 0..len(xs) |
| String indexing / slicing / methods | len, +, str() cover the HUD cases |
| Exhaustive return-path checking | a typed function that falls off the end returns nil — don’t |
Narrowing for globals / and-chains |
??, or copy to a local first |
| Exponent number literals | decimal; hex/binary are supported |
Two entries deserve to be pulled out, because they are not limits at all but choices, and they are choices the roadmap will not reverse:
No JIT.¶
This is iOS-legal by construction and gives deterministic performance with no warm-up. It is a feature, permanent.
No time-seeded randomness.¶
Determinism is the default so that replays and CI screenshots work; if you want variety between runs you seed srand() yourself, with a number you can log. Also permanent.
Roadmap philosophy¶
The rule that governs what happens next deserves stating as law, because it is the core thing this language follows: wick contains only what we truly need — nothing for the sake of having it. A feature is admitted when real game code demonstrably hurt without it, and not before. The paired Lantern Night builds are the standing evidence mechanism: missing-feature pain must show up in a real game or focused example before the language grows. Optionals are in this book because a shipped crash demanded them; closures are not, because no shipped program has yet paid a price for their absence. Feature parity with other languages is explicitly not a reason to add anything.
The organising idea is “Lua 1.0 small.” Lua did not begin with metatables, coroutines, and an ecosystem; it began as a small, clean, embeddable core and grew as real use justified each addition. wick is taking the same path deliberately. Every omission in the table above is addable behind the same typed front end that already exists — closures and multiple returns do not require rethinking the compiler, only extending it — and each will be added when a real game needs it enough to justify the size, not before.
This is why the limits are stated so plainly. A language that hid its gaps would be inviting you to discover them at the worst moment. wick lists them, gives each a workaround, and puts each on a roadmap, because the whole thesis of the language is that a program’s failure modes should be visible up front rather than discovered in production — and a language owes its users the same honesty it enforces in their code.