Polydera

THE LANGUAGE

Luna 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.

Compile onceExact typesDeterministic lifetimes
01 / WRITE IT. CHECK IT. KEEP IT.

Interactive source. Retained compiled programs.

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.

The worst face that mattersLanguage vocabulary
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.

02 / SELF-DESCRIBING

The language explains itself.

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.

Asked in the running applicationCatalog output, pinned by the test suite
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

How the compiler reasons about correctness.

EXACT TYPESA literal's spelling is its type

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.

TWO FAILURE TIERSA fault is not a refusal

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.

COMPUTATION ENDSFinite computation is proven, not hoped

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.

MEMORY HAS AN ENDINGDeterministic release, without a garbage collector

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.

CONCURRENCY IS CHECKEDparallel: true changes time, nothing else

An 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.

ONLY VERIFIED PROGRAMS RUNOnly compiler-issued artifacts execute

Every program that runs was issued by the compiler and passed the verifier. A structural lookalike is data, not executable authority.

A VALUE'S STORAGE LIFETIME
Two holders, one buffer.

A local list and a retained snapshot refer to the same immutable storage.

What each line establishesLanguage vocabulary
// 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 reasoning reaches everything.

The same compile-time judgment that proves the big properties also settles the language's everyday questions.

ONE NAME, MANY SIGNATURES

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.

FUNCTIONS AND METAFUNCTIONS

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.

IDENTITY WITHOUT COST

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.

TWO ABSENCES

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.

ENUMS ARE UNIONS

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.

SIGNATURES NEVER LIE

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

Code can work on its own structure.

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.

READTypes and fields

The signature is data.

COMPUTEMap · filter · fold

Ordinary Luna transforms it.

DECLAREA checked function

The answer shapes its parameters.

Reuse a function's parameter fieldsSyntax guide
fn area(
    // across
    width: int,
    // down
    height: int = 1) -> int {
  return width * height
}
---
fn scaled(factor: int, ..size: luna.parameters(of: fn.area)) -> int {
  return fn.area(..size) * factor
}
fn.scaled(factor: 2, width: 3, height: 4)

A signature you can reuse.

  1. Read the fields. luna.parameters returns area's width and height, including their docs and the height default.
  2. Declare the parameters. ..size introduces those fields into scaled. Inside its body, size holds their values as a product.
  3. Make the checked call. The argument spread passes those values to area. The final call answers 24; omitting height would use the inherited default.

At compile time, field lists describe structure. At runtime, a product holds its own exactly typed fields. No list of boxed values carries the call.

Call a function from partial answersExecutable test corpus
fn resize(width: int, label: str = "north", scale: int = 2) -> str {
  return luna.str.concat(a: label, b: str(value: width * scale))
}

fn paired(V: type, values: V) -> str {
  let wanted = luna.sort(value: luna.parameters(of: fn.resize), by: fn (p) => p.name)
  let given  = luna.sort(value: compiler.members(of: V),      by: fn (f) => f.name)
  let usable = luna.filter(domain: luna.zip(a: wanted, b: given),
                           keep: fn (f) => f.type != (int, missing) && f.type != (str, missing))
  return fn.resize(..compiler.arguments(
    domain: usable,
    step: fn (f) => compiler.member(of: values, name: f.name)))
}

fn.paired(values: {width: 7, label: "east", scale: missing})

The press.

  1. Read both structures. The function's parameters and the given values' members are both field lists — the compiler's working posture of a named product.
  2. Align and pair. Sort both by name; zip pairs each parameter with the member of its name. Mismatched name sets refuse, naming both sides.
  3. Keep what was given. The pair's second type says whether a value is present, so the filter is a compile-time decision: the shape of the call is settled before the program runs.
  4. Make the checked call. The argument spread writes one named argument per kept pair; the missing label takes its declared default. The answer is "east14".

This is the shape of every form, tool, and workflow door in a Luna application: a function called from whatever answers a person has given so far. Everything above the final call runs in the compiler; what executes is the call a hand would write. This function is an executable test in Luna's repository and answers east14 on every run of the suite.

ONE FACT, TWO POSTURES

A list of fields and a named product are the same fact in two postures: the list is what compile-time code manipulates, the product is what programs hold. Two doors cross between them.

THE TWINS

Every compile-time fact has a runtime twin: type(of: v) reads a declared type as a value the stratum computes with; T.value carries a type down as data; f.parameters and f.result read a function's signature.

TYPES ARE COMPUTED WHERE THEY ARE USED

A body can compute a type and stand it in its own annotations — type row = luna.product(of: …) then let copy: row = values — compiled byte-identical to the literal spelling.

The next compilation can learn from this run.

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.

See a computation that runs entirely in the compiler
Filter a list of typesLanguage report
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.

Fields, products and live parameters

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.

An entry-watch signatureInstalled signature
watch.attach(..live to: T, body: (..T | missing) -> none) -> watch.id

05 / NATIVE ARRAYS

The shape is in the language.

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.

array<float>[2, 3]2 axes · 6 contiguous values
3.01.04.01.05.09.0
mean_alongaxis: 1 →
2.675.0
One typed array operation reduces each row. The result is another native array.
Reduce, then normalize by broadcastArray vocabulary
fn normalized(m: array<float>[.., ..]) -> array<float> {
  let low = luna.array.min_along(value: m, axis: 0)
  let span = luna.array.subtract(a: luna.array.max_along(value: m, axis: 0), b: low)
  return luna.array.divide(a: luna.array.subtract(a: m, b: low), b: span)
}

The input declares two axes. Each reduction answers one value per column; broadcasting aligns shapes at the last axis, so those values combine with every row. Every step runs on native typed buffers.

06 / ASYNCHRONOUS CALLS

Start a call. Continue with other work.

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 invocation
Continue, then consume the futureParallelism topic
fn 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

Warm programs, measured openly.

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.

Luna = 1× PUC-Lua 5.5 CPython 3.13
Higher is slower
Sort by key
15.36×
3.31×
Map over records
12.49×
14.03×
Fold with captures
5.22×
16.34×
First match (any)
4.49×
10.19×
Split, filter, sum
3.06×
1.35×
Three chained passes
2.17×
9.99×

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.

Native calls stay cheap, too.

Lower is better
Luna — typed native call21.79 cyc
PUC-Lua — lua_CFunction49.14 cyc

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.

Native arrays, measured the same way.

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.

NATIVE ARRAY BENCHMARKS

Whole-block work, measured per input.

Luna native arrayPUC-Lua tableCPython list

Elementwise

Double each value into a fresh block.

2.1
36.1
36.0

Axis reduction

Sum each 100-wide row.

1.8
30.9
11.3

Even-index gather

Gather even positions into a fresh block, then sum.

5.8
31.5
25.4

Build → map → sum

Build input, double into a fresh block, then sum; allocation is timed.

3.8
72.4
87.1

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.

Measurement details

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.

FUNCTION COMPILATION

Median of 21 independent compilations of the exact Luna function in a resident kernel, after the first sample. Input construction and execution are absent.

Elementwise164 µs
Axis reduction70 µs
Even-index gather103 µs
Build → map → sum133 µs

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

The language stays inside the application.

See how the running product holds live state, records deliberate changes, and delivers standing programs.