There is no promotion ladder and no coercion at a call site. Implicit conversion only forgets type facts or adds a union tag; a conversion that changes a value is an explicit construction the author writes.
THE LANGUAGE
A dynamic language that's static.
Luna feels dynamic because the compiler is the interpreter: it lives inside the runtime, compiling and checking each definition the moment it is written. You get the freedom of a scripting language — define, call, and reshape while the application runs — and nothing unchecked ever executes.
Write source in the running application and it becomes a compiled program the moment it is defined: the whole body checked once — types, calls, effects, ownership — then retained. Every later call is checked against the retained signature and links that same body. Nothing is parsed or inferred again.
Interactive goes deeper than typing. The compiler is present at run time, so programs can compute with it: read a signature, build a type, declare a checked function. More on that below, where code works on its own structure.
fn worst_kept(
// one quality per face, where 1.0 is equilateral
qualities: list<float>,
// slivers below the floor are ignored
floor: float = 0.05) -> float {
let kept = luna.sort(value: luna.filter(keep: fn (q: float) => q >= floor,
domain: qualities),
by: fn (q: float) => q)
return if luna.at(value: kept, index: 0) is float worst { worst } else { 1.0 }
} Arguments are named at the call; omit floor and compilation supplies 0.05. The ascending sort puts the worst surviving face first. luna.at answers float | missing, so a mesh whose faces are all slivers answers 1.0 instead of faulting. The parameter comments remain discoverable documentation.
An agent's first act is asking. luna.help() answers with the installed language: every construct, tool, and type, each carrying its signature, its one-line documentation, and its tags. Nothing is read from a manual — the catalog is rendered from the same contracts the compiler checks, so what you discover is never stale. A fresh agent with no prior knowledge can be dropped into the console and learn the language in minutes — it asks, and the application answers.
It goes to any depth: luna.signature(of:) renders any callable honestly — liveness included — and luna.parameters(of:) answers a function's parameters with their documentation and defaults, as data a program can compute with. Twelve topics group the vocabulary; the language is its own reference.
module luna — the language; luna.help() to get started [189 constructs, 12 topics]
luna.fold(step: (T1, T2) -> T1, domain: list<T2> | range<T2> | <index>, seed: T1) -> T1 [control]
run one step over a domain from a seed, each step taking the accumulator and the element — or the accumulator alone, which repeats a counted number of times — and publish the final accumulator
tags: reduce, accumulate, aggregate, sum, iterate Actual output: the language's own catalog row for luna.fold. The listing is executable truth — the same test suite that gates the compiler pins this text.
03 / THE PROOFS
There is no promotion ladder and no coercion at a call site. Implicit conversion only forgets type facts or adds a union tag; a conversion that changes a value is an explicit construction the author writes.
A fault ends the invocation with its exact source position: an invalid index, an integer overflow. A refusal is an ordinary union value, such as int | error, handled with is. Nothing silently converts between the tiers.
Recursion is refused by construction, including through callbacks. Iteration uses bounded sequence controls, so the compiler proves every Luna computation finite. Only work that leaves Luna — a native call, an outside future — depends on the outside.
An immutable value can share storage safely; when the final owner ends, the storage is released immediately, without a tracing garbage collector. Cyclic ownership is impossible, so nothing needs a cycle collector. Success and fault paths both release their obligations. A capture, retained snapshot, or history entry can deliberately keep storage alive; release follows the final owner.
parallel: true changes time, nothing elseAn ordinary Boolean argument: same meaning, identical values, only time changes. The compiler follows the callback's entire call graph and refuses a world write on workers — "a worker performs no world step — no effect and no wait — and enters only natives their own host declared reentrant". The artifact's cost estimate is advice for scheduling that changes no value anywhere.
Every program that runs was issued by the compiler and passed the verifier. A structural lookalike is data, not executable authority.
A local list and a retained snapshot refer to the same immutable storage.
// the spelling is the type: this literal is list<float>[3]
let weights = [1.5, 2.0, 2.5]
// a map keeps its domain's count: list<float>[3] again
let scaled = luna.map(step: fn (w: float) => w * 2.0, domain: weights)
// a read that can miss answers float | missing; is decides the union
let floor = if luna.at(value: weights, index: 0) is float w { w } else { 0.0 }
// the domain is bounded, so the compiler proves this fold ends
let total = luna.fold(step: fn (a: float, w: float) => a + w,
domain: scaled, seed: floor)
// parallel is admission-checked; the values are identical either way
luna.map(step: fn (w: float) => w / total, domain: scaled, parallel: true)The same compile-time judgment that proves the big properties also settles the language's everyday questions.
A name can answer a set of signatures, and every call picks exactly one while compiling: by the argument names it spells, then by their solved types. Execution performs no overload search; an ambiguous call refuses, listing what exists.
The language has two callable citizens. A metafunction is an instruction for making a function — compiled once per exact shape, on first use — and it overloads under the same law: names first, then the kinds of what the call hands over. T: type and T: field are simply two different metafunctions.
newtype gives a value a distinct identity with no allocation and no tag. A color is not an int, and the compiler holds that line for free.
none is absence held as a value; missing is the absence of an answer. A list of optional values can distinguish an empty element from nothing there — most languages cannot say the difference.
enum demo.side = left | right declares a union of named units; the member test is the same is every union uses. No second enum machinery exists.
Everything a body receives is in its signature: defaults, unions, even liveness — watch.attach(..live to: T, …) says its seats are observed live entries. If the signature does not say it, it does not happen.
04 / PROGRAMMING AT TWO TIMES
Types and fields are values the compiler can compute with. At compile time, a product and its list of fields are two spellings of one structure. Those fields can declare a function's parameters, carrying their names, types, documentation and defaults.
Ordinary map, filter and fold can transform that structure. Feed the answer into a declaration and the compiler checks the resulting function exactly as it checks one written by hand. The same source language does both jobs, with no separate macro syntax.
Read a dataset's column names and commit them. The next submission can turn those names into fields and declare a function for that schema. The compiler pins the committed revision it reads. A function built this way can be published for later calls.
fn numeric_columns() -> int {
let columns = [int, str, float, bool]
let numeric = luna.filter(domain: columns, keep: fn (t) => t == int || t == float)
return luna.length(array: numeric)
} The same filter used for face qualities can select numeric types. This body becomes return 2; the type list and its traversal never exist at runtime.
Compile-time parameters make a function a metafunction. Each exact instantiation compiles once and is retained. A mixed computation can resolve its structure now and leave its numeric work for execution.
A field list retains declaration order, defaults and docs. Named product type identity uses only names and types. Declared product aliases retain their defaults for construction; positional products retain their order.
A live parameter names a binding. Its delivered value has the binding's type or missing. Liveness belongs to the parameter name.
watch.attach(..live to: T, body: (..T | missing) -> none) -> watch.id05 / NATIVE ARRAYS
Dense multidimensional array is a Luna type, with a window per axis. Broadcasting, reductions, selection, reshape and indexed gathers operate on native typed buffers. Shapes known at compile time travel with the type; what stays dynamic is checked at the operation.
luna.spawn hands a checked call to the host's dispatcher and returns future<T>. The caller can continue before asking for its answer. The task owns copies of its inputs; nothing borrows the caller's memory.
luna.await answers T | error. Failed or abandoned work settles as an error value, handled by the same is test as any union. With no dispatcher, the call runs inline and returns an already settled future.
A host can admit blocking waits, refuse an actual pending wait, or let a retained invocation yield and resume when ready. Yielding leaves the application free to process its next turn. Parallel sequence work is part of the correctness story: the compiler checks it like everything else.
Follow a retained invocationfn work(x: int) -> int { return x + 7 }
fn both(a: int, b: int) -> int {
let pending = luna.spawn(call: work(x: a))
let here = work(x: b)
return if luna.await(value: pending) is int there { here + there } else { here }
} The caller computes here, then consumes the spawned answer. This example uses here alone if the other call failed.
07 / MEASURED PERFORMANCE
Safety gives the compiler room to simplify. Exact types determine native layouts before execution: integers and floats occupy unboxed slots, products have fixed field offsets, and a fold inside a map becomes nested counted loops. Compile-time structure disappears once it has shaped the program.
The compiler derives the call graph, effects, waiting behavior and cleanup obligations. Execution needs no parsing, type inference or overload search. The measurements below show what remains.
Luna beat PUC-Lua in all six warm retained-program workloads shown here. LuaJIT remains a separate JIT target outside this comparison.
Processor cycles per element step, as a multiple of Luna’s — the same program authored in each language, one interleaved session on Apple M4 Max (PUC-Lua 5.5, CPython 3.13). Ratios derive from the recorded cycle columns, rounded to one decimal place in the source. The teal line is Luna.
Inputs of 1,000 and 5,000 elements, with equivalent authored work on every side; cold compilation was measured separately.
One installed string-length call through each language's native-function boundary — cycles per call, one interleaved session on Apple M4 Max. K = 1 versus K = 16 at one million calls, three repetitions. This measures Luna’s typed native door against PUC-Lua’s C function door. Lower is better.
Four retained array programs against CPython lists and PUC-Lua tables. Each workload transforms a whole block and is measured per input element, at 1,000 and 100,000 elements; the operations run as whole-block kernels over native typed buffers. Luna retires between 5 and 36 times fewer instructions per element across all eight rows — language and container comparisons, not native-library numbers. The per-element cost holds as blocks grow — both sizes are in the table.
Double each value into a fresh block.
Sum each 100-wide row.
Gather even positions into a fresh block, then sum.
Build input, double into a fresh block, then sum; allocation is timed.
Lower is better. Warm cycles per original input element, rounded to one decimal. Luna uses native dense arrays; PUC-Lua uses tables and CPython uses lists. Every resulting block was checked element by element outside timing. Bars share a scale within each workload. Languages and repetition counts were interleaved; the 1,000-element axis row uses a separate longer five-sample run.
Apple M4 Max, Release, Apple clang 17; CPython 3.13.11 and PUC-Lua 5.5.1. Native arrays are compared with ordinary lists and tables, not NumPy or LuaJIT. Each chart value is a median of counter differences between R and 4R executions, divided by the additional repetitions and original input count. Setup cancels from the difference; the composite pipeline builds its input each time.
Three samples per repetition count, five for the longer small-axis run. The largest repeat range was 12.1% of its median; all other rows were at most 9.3%. Counters were calibrated and all timing ran under an exclusive machine lock. Results recorded on 28 September 2026.
Median of 21 independent compilations of the exact Luna function in a resident kernel, after the first sample. Input construction and execution are absent.
Ratios are based on retired instructions; cycles are reported alongside. Compilation is measured separately in a resident kernel; methodology and raw samples are recorded with the benchmark harness.
LUNA RUNTIME
See how the running product holds live state, records deliberate changes, and delivers standing programs.