Polydera

LEARN THE LANGUAGE

Luna tutorial.

Sit down at the console and ask.

One Luna session, first prompt to last. Someone who has never seen the language asks it for help, tries a cell, reads the refusal, fixes it, and moves on — and by the end, every part of the language has come up. Every cell ran on the Luna client; every answer is the one it printed.

340 cells22 chaptersEvery answer real
Chapters
  1. 01Open the console
  2. 02First cells
  3. 03A literal’s spelling is its type
  4. 04Ask the language
  5. 05Functions, compiled once
  6. 06Conditionals are expressions
  7. 07Lists, ranges, and safe reads
  8. 08Controls instead of loops
  9. 09No recursion, by construction
  10. 10Records, names, and enums
  11. 11Unions, absence, and two kinds of failure
  12. 12Native multi-dimensional arrays
  13. 13Jagged blocks
  14. 14Metafunctions and kinds
  15. 15Modules and imports
  16. 16Touching the world
  17. 17Async, and chains of work
  18. 18The environment, and its live tier
  19. 19Watches instead of event handlers
  20. 20Features working together
  21. 21The compiler is the interpreter
  22. 22The rest is in the language

How to read it: a > line is a cell typed at the prompt, and the lines under it are what the client printed. Refusals and faults are shown in rose. Where a long answer was shortened, the cut says … trimmed. Cells run in order in one session, so a diagnostic’s first number is the cell it points into.

01

Open the console

I know nothing about Luna. I start the client with no file — that opens a session — and the banner says where to begin.

luna — the session

luna

Luna 0.1.0 — luna.help() to get started
mounted: luna.cli, luna.clock, luna.session — :catalog lists them, :help the session
… trimmed

So I ask.

luna — the session
luna.help()
luna.help() -> str [compile-time]
Start here: write a cell, discover names, inspect one signature or document, then run it.

`luna.onboarding()` is the one page beneath this one: the cell, the exact types that refuse most
first drafts, controls instead of loops, and the environment.

Luna is statically typed and immutable. Each cell compiles once against the current session, then
publishes its `let`, `alias`, `type`, and `fn` names. A terminal and an agent's submissions use that
same session. A later definition of `fn.name` replaces that
name. This live rebind moves every later cell and every name based consumer; existing values and
completed cells stay unchanged.
… trimmed
Use these compile-time queries before guessing:

1. `luna.catalog()` lists modules and session names.
2. `luna.catalog(search: "word", grouped: true)` finds rows by name, signature, prose, or tag.
3. `luna.catalog(module: luna.cli)` lists a module, by its id or by the head its rows are spelled
   under, so `module: io` finds `module luna.cli`; `luna.catalog(type: list<float>)` finds the
   exact producers and consumers of a type, then every generic construct a call handing it
   enters, printed as that call instantiates it.
4. `luna.signature(of: fn.total)` prints a declaration. `luna.doc(of: luna.fold)` adds behavior and
   examples. `luna.source(of: fn.total)` returns authored Luna source.
5. `luna.env()` returns the session's standing names as `{name, type_name, kind}` records, where
   `kind` separates a `value`, a `function`, and a `live value`.

Query arguments such as `of:`, `module:`, `type:`, `search:`, `full:`, `grouped:`, and `data:` are
compile-time selectors. `module:` may accompany `search:`; other primary selectors are alternatives.
`data: true` returns typed records instead of text. Search is case-insensitive, requires every term,
and accepts quoted phrases. No text match proves only that the indexed text did not match.
… trimmed
Read next, as needed: `luna.syntax()` for the complete grammar; then `luna.types()`,
`luna.operators()`, `luna.controls()`, `luna.ownership()`, `luna.effects()`, `luna.cells()`,
`luna.parallelism()`, `luna.patterns()`, and `luna.diagnostics()`.

Three things stand out. A cell compiles once, then publishes its names. The language answers questions about itself with ordinary calls. And there are topics to read when I need them — the first is the page beneath this one.

luna — the session
luna.onboarding()
luna.onboarding() -> str [compile-time]
Luna in one page: the cell, the exact types, controls instead of loops, and how to find a name.

Luna is the language of the running application. A submission is one source cell: `fn`, `type`, and
`alias` declarations first, then `let` bindings and expression statements. The last expression is
the cell's answer. A successful cell publishes its top-level names atomically and a refused cell
publishes none of them. A later cell calls a saved function as `fn.name`. A named function reads
its parameters and the session values its body names, captured when its cell ran and sealed into
… trimmed

Then I start typing.

02

First cells

A cell prints its value.

luna — the session
1 + 2
3

A let publishes a name instead. The console reports what the cell published.

luna — the session
let answer = 40 + 2
values: answer
answer * 2
84

Ending a cell with ; makes it a statement: it runs, and nothing prints.

luna — the session
answer * 2;

03

A literal’s spelling is its type

My first real mistake comes from habit.

luna — the session
answer + 0.5
terminal 7:1:1: LUNA3003: compiling failed: exact type conflict: the binary operands must share one exact type; the two sides are constrained to int and float; nothing converts on its own: write float(value: answer)
  1 | answer + 0.5
    | ^~~~~~~~~~~~
terminal 7:1:1: note: definition-time ABI seed
terminal 7:1:10: note: a float literal
terminal 4:1:1: note: "answer" was committed by terminal 4 (revision 4)

The refusal has a stable code, points at the exact span, says why — answer is an int, 0.5 is a float — and names the fix. Luna never converts a number on its own. A conversion that changes a value is a construction I write, so what I spelled is what runs.

luna — the session
float(value: answer) + 0.5
42.5

The same law applies at a call. A square root takes a float, so the integer literal 2 is refused, and the refusal says how to spell it.

luna — the session
luna.sqrt(value: 2)
terminal 9:1:1: LUNA3003: compiling failed: exact type conflict: language construct "luna.sqrt" derives what it publishes from its operands; luna.sqrt reads a float or an array, but its operand is int; nothing converts on its own: spell the literal 2.0
  1 | luna.sqrt(value: 2)
    | ^~~~~~~~~~~~~~~~~~~
luna.sqrt(value: 2.0)
1.4142135623730951

Some numbers have names. A constant is a value, not a function: its name alone is the value.

luna — the session
luna.pi()
terminal 11:1:1: LUNA3001: compiling failed: "luna.pi" is a constant, not a function: its name alone is its value, so write it without a call
  1 | luna.pi()
    | ^~~~~~~~~
luna.pi
3.141592653589793
luna.doc(of: luna.pi)
luna.pi -> float [constant]
the ratio of a circle's circumference to its diameter, as the nearest float
tags: pi, circle, half turn, radians, constant
… trimmed

Text does not add either, and the refusal names the function that joins texts.

luna — the session
"moon" + "light"
terminal 14:1:1: LUNA3003: compiling failed: exact type conflict: arithmetic requires one exact numeric type; str is not assignable to int or float; + never concatenates text: luna.concat(a:, b:) joins two texts, and str(value: n) spells a number as one
  1 | "moon" + "light"
    | ^~~~~~~~~~~~~~~~
terminal 14:1:1: note: a str literal
luna.concat(a: "moon", b: "light")
moonlight

Parsing text may have no answer, so it answers missing instead of failing.

luna — the session
int(text: "42")
42
int(text: "forty-two")
missing

Integer division truncates toward zero, and the remainder keeps the dividend’s sign. Floats follow IEEE, so dividing by zero is an answer.

luna — the session
-7 / 2
-3
-7 % 2
-1
1.0 / 0.0
inf

Integers never wrap. Overflow is a fault: the run ends with a report at the exact span.

luna — the session
9223372036854775807 + 1
terminal 21:1:1: LUNA4006: execution failed: checked integer arithmetic overflow
  1 | 9223372036854775807 + 1
    | ^~~~~~~~~~~~~~~~~~~~~~~

The refusal (LUNA3003) came before anything ran. The fault (LUNA4006) came while running. That distinction comes back.

04

Ask the language

Before I guess another name, I ask what exists. The answers come from the same contracts the compiler checks — they cannot go stale.

luna — the session
luna.catalog()
module luna — the language; luna.help() to get started [327 constructs, 12 topics]
module luna.cli — this client's own World: writing a value, asking a user, and putting text on disk or taking it back [4 callables, 0 types] (rows spelled io.*)
module luna.clock — the monotonic and system clocks' readings, beneath the `time` module [2 callables, 0 types] (rows spelled time.*)
module luna.session — the session's own door: what a run keeps when the rest of it evaporates, and when what stands was committed [7 callables, 9 types] (rows spelled env.*)
values
  answer: int

Four modules are mounted in this console. The search finds a row by its name, signature, prose, or tags.

luna — the session
luna.catalog(search: "square root", grouped: true)
[3 groups, 5 call forms]
expand callables: luna.signature(of: <name>); luna.doc(of: <name>)
  luna.sqrt [2 overloads] — the square root of a float, by IEEE; a negative…
    (matches tag "root", tag "square root")
  luna.sqrt2 [1 overload] — the square root of two, as the nearest float
    (matches tag "root two", summary)
  luna.norm [2 overloads] — the Euclidean length of every element of a list…
    (matches description)

luna.signature prints one declaration. A name can answer several signatures; this one takes a float or a whole array.

luna — the session
luna.signature(of: luna.sqrt)
luna.sqrt(value: float) -> float
luna.sqrt(T1: type, value: array<T1>[..any]) -> array<T1>[..any]

luna.doc adds what each parameter means, the behavior, and examples that compile.

luna — the session
luna.doc(of: luna.sqrt)
luna.sqrt(value: float) -> float [cost 1]
luna.sqrt(T1: type, value: array<T1>[..any]) -> array<T1>[..any] [cost 1]
  value — the float whose square root is taken, or an array of floats, element by element
the square root of a float, by IEEE; a negative operand answers nan; over an array, each float's square root
tags: square root, root, sqrt

The square root of a `float`, by IEEE binary64. It is total and quiet: a negative operand answers NaN and an infinite one answers infinity, and neither is a fault — an IEEE answer is an answer.

Over an array: Maps every element of an array of `float` or `float32` to one of the same scalar, under the operand's own shape and type. Floats follow IEEE; the integer forms follow the language's own int law.

example
```
fn hypotenuse(a: float, b: float) -> float {
  luna.sqrt(value: a * a + b * b)
}
```

example
```
fn f(xs: array<float>) -> array<float> {
  luna.sqrt(value: xs)
}
```

05

Functions, compiled once

A function is declared with fn. Its body answers its last line. A parameter can carry a default.

luna — the session
fn area(width: float, height: float = 1.0) -> float { width * height }
functions: fn.area

The cell published fn.area. Later cells call it by that name, with named arguments. The omitted default is inserted by the compiler.

luna — the session
fn.area(width: 3.0, height: 2.0)
6.0
fn.area(width: 3.0)
3.0

Arguments may be positional when they follow the parameter order.

luna — the session
fn.area(3.0, 2.0)
6.0

Each call is checked against the stored signature; the body is never compiled again. That is why a published function is cheap to call from anywhere in the session. A misspelled label is refused, and the refusal lists the inputs that exist.

luna — the session
fn.area(widht: 3.0)
terminal 30:1:9: LUNA3007: compiling failed: linked callable "fn.area" has no input named "widht"; its inputs are width, height
  1 | fn.area(widht: 3.0)
    |         ^~~~~~~~~~
fn.area(width: 3)
terminal 31:1:9: LUNA3003: compiling failed: exact type conflict: argument "width" must conform to linked callable "fn.area"; int is not assignable to float; nothing converts on its own: spell the literal 3.0
  1 | fn.area(width: 3)
    |         ^~~~~~~~
terminal 31:1:16: note: an int literal

The signature and the source I wrote are both retained.

luna — the session
luna.signature(of: fn.area)
fn.area(width: float, height: float = 1.0) -> float
luna.source(of: fn.area)
fn area(width: float, height: float = 1.0) -> float { width * height }

Out of habit, I end a body with return.

luna — the session
fn half(x: float) -> float {
  return x / 2.0
}
terminal 34:2:3: LUNA2020: compiling failed: a body answers its tail, and "return" only leaves early: drop the keyword
  2 |   return x / 2.0
    |   ^~~~~~~~~~~~~~

The last line already answers. return only leaves early.

luna — the session
fn half(x: float) -> float {
  x / 2.0
}
functions: fn.half

Next I use a session value inside a function. A function captures what it reads, as the value stood when the function was declared.

luna — the session
let rate = 1.5
values: rate
fn priced(units: float) -> float { units * rate }
functions: fn.priced
fn.priced(units: 4.0)
6.0

A lambda bound with let is the other spelling of the same thing, and it captures the same way.

luna — the session
let price = fn (units: float) => units * rate
values: price
price(units: 4.0)
6.0

Rebinding rate later reaches into neither. Both were sealed with the value they saw — the answer cannot change behind my back. For a newer rate, I declare again or pass it as a parameter.

luna — the session
let rate = 3.0
values: rate
fn.priced(units: 4.0)
6.0
price(units: 4.0)
6.0

Defining a function again with the same signature replaces it. Every later cell calls the new body.

luna — the session
fn greet(name: str = "agent") -> str { luna.concat(a: "hello, ", b: name) }
functions: fn.greet
fn.greet()
hello, agent
fn greet(name: str = "agent") -> str { luna.concat(a: "welcome, ", b: name) }
functions: fn.greet
fn.greet(name: "reader")
welcome, reader

Defining it with a different signature adds an overload instead. The name then answers a set, and each call selects one member while compiling.

luna — the session
fn twice(x: int) -> int { x * 2 }
functions: fn.twice
fn twice(x: float) -> float { x + x }
functions: fn.twice
luna.signature(of: fn.twice)
fn.twice(x: int) -> int
fn.twice(x: float) -> float
fn.twice(x: 3)
6
fn.twice(x: 1.5)
3.0

06

Conditionals are expressions

if is not a statement that runs a branch. It is an expression: it answers the value of the arm it takes, so I can print it, bind it, or return it.

luna — the session
if answer > 40 { "big" } else { "small" }
big
let size = if answer > 40 { "big" } else { "small" }
values: size

Because an if answers one value, every arm must answer one compatible type. An arm that answers a number beside one that answers text is refused.

luna — the session
if answer > 40 { "big" } else { 0 }
terminal 55:1:1: LUNA3003: compiling failed: exact type conflict: conditional result must match its false branch; int is not assignable to str
  1 | if answer > 40 { "big" } else { 0 }
    | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
terminal 55:1:33: note: an int literal
terminal 55:1:18: note: a str literal

Several tests chain with else if, and the first that holds decides.

luna — the session
fn grade(score: int) -> str { if score >= 90 { "A" } else if score >= 75 { "B" } else if score >= 50 { "C" } else { "F" } }
functions: fn.grade
fn.grade(score: 80)
B
fn.grade(score: 20)
F

A boolean if needs an else, because without one there would be no value when the test fails.

luna — the session
if answer > 40 { "big" }
terminal 59:1:1: LUNA3018: compiling failed: an if with no else must test a union member: only an is chain that exhausts the tested union needs none
  1 | if answer > 40 { "big" }
    | ^~~~~~~~~~~~~~~~~~~~~~~~

The exception is an arm that never answers: one that leaves early with return, or faults. Then the if is a guard, and the line after it is the program going on.

luna — the session
fn sign(x: int) -> int {
  if x < 0 { return -1 }
  if x == 0 { return 0 }
  1
}
functions: fn.sign
fn.sign(x: -5)
-1
fn.sign(x: 9)
1

The other place an if needs no else is a member test that covers every member of a union — that comes with unions.

07

Lists, ranges, and safe reads

A list literal knows its elements’ type and its length.

luna — the session
let xs = [3, 1, 4, 1, 5, 9, 2, 6]
values: xs
luna.length(array: xs)
terminal 64:1:13: LUNA3007: compiling failed: language construct "luna.length" has no input named "array"; its inputs are value
  1 | luna.length(array: xs)
    |             ^~~~~~~~~

I guessed the label. The refusal lists the real one.

luna — the session
luna.length(value: xs)
8
xs[2]
4

A negative position counts from the end.

luna — the session
xs[-1]
6

Because the compiler knows the length of xs, a position that can never fit is refused before anything runs, from either end.

luna — the session
xs[20]
terminal 68:1:4: LUNA3003: compiling failed: exact type conflict: index expression; list index 20 is outside every statically possible cardinality
  1 | xs[20]
    |    ^~
xs[-9]
terminal 69:1:4: LUNA3003: compiling failed: exact type conflict: index expression; list index -9 is outside every statically possible cardinality
  1 | xs[-9]
    |    ^~

Inside a function the length is not known, so xs[i] is an assertion. If the claim is false, the run faults.

luna — the session
fn pick(values: list<int>, i: int) -> int { values[i] }
functions: fn.pick
fn.pick(values: xs, i: 20)
terminal 70:1:52: LUNA4008: execution failed: list index is out of bounds
  1 | fn pick(values: list<int>, i: int) -> int { values[i] }
    |                                                    ^

When an element might not be there, luna.at asks instead of asserting. It answers the element or missing.

luna — the session
luna.at(value: xs, index: 2)
4
luna.at(value: xs, index: 20)
missing

Values are immutable. An edit returns a new value and leaves its input alone.

luna — the session
let more = luna.append(list: xs, value: 7)
values: more
xs
[3, 1, 4, 1, 5, 9, 2, 6]
more
[3, 1, 4, 1, 5, 9, 2, 6, 7]

A range is a counted domain of integers.

luna — the session
luna.range(start: 0, stop: 5)
luna.range(0, 5, 1)

08

Controls instead of loops

I look for a loop.

luna — the session
luna.catalog(search: "loop", grouped: true)
[11 groups, 19 call forms]
expand callables: luna.signature(of: <name>); luna.doc(of: <name>)
  luna.each [4 overloads] — run one step over every element of a domain, in…
    (matches tag "loop")
  luna.grid [1 overload] — walk every combination of two to four domains,…
    (matches tag "nested loops")
  luna.controls [topic] — Iteration is a construct over a finite domain;…
    (matches summary)
… trimmed

There is no loop statement. Iteration is a control over a finite domain — map, filter, fold and their siblings — and the step is a function, often a lambda written in place.

luna — the session
luna.map(step: fn (x: int) => x * x, domain: xs)
[9, 1, 16, 1, 25, 81, 4, 36]
luna.filter(keep: fn (x: int) => x % 2 == 0, domain: xs)
[4, 2, 6]
luna.fold(step: fn (sum: int, x: int) => sum + x, domain: xs, seed: 0)
31

A published function is a value too, and so is a construct of the language.

luna — the session
luna.map(step: fn.twice, domain: [1, 2, 3])
[2, 4, 6]
luna.map(step: luna.trim, domain: ["  fig ", " pear"])
["fig", "pear"]

Sorting is stable. A key comes from by:.

luna — the session
luna.sort(value: xs, descending: true)
[9, 6, 5, 4, 3, 2, 1, 1]
luna.sort(value: ["pear", "fig", "banana"], by: fn (w: str) => luna.length(value: w))
["fig", "pear", "banana"]

zip walks two lists together, as a list of pairs.

luna — the session
luna.zip(a: [1, 2, 3], b: ["a", "b", "c"])
[(1, "a"), (2, "b"), (3, "c")]

I guess that a step over it takes one parameter per list.

luna — the session
luna.map(step: fn (n: int, s: str) => luna.concat(a: s, b: str(value: n)), domain: luna.zip(a: [1, 2, 3], b: ["a", "b", "c"]))
terminal 87:1:1: LUNA3007: compiling failed: serial map callback target must have exactly 1 parameter: a zip of 2 domains hands each turn one positional product of their elements, the element its list holds, so the step takes that one product and reads its fields by position
  1 | luna.map(step: fn (n: int, s: str) => luna.concat(a: s, b: str(value: n)), domain: luna.zip(a: [1, 2, 3], b: ["a", "b", "c"]))
    | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
terminal 87:1:10: LUNA3003: compiling failed: exact type conflict: argument "step" must conform to language control "luna.map"; function arity 2 is outside the admitted range [1, 1]
  1 | luna.map(step: fn (n: int, s: str) => luna.concat(a: s, b: str(value: n)), domain: luna.zip(a: [1, 2, 3], b: ["a", "b", "c"]))
    |          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
terminal 87:1:39: note: function shape requires a function value
terminal 87:1:1: note: function shape requires a function value

The refusal says what each turn is: one pair, read by position.

luna — the session
luna.map(step: fn (p: (int, str)) => luna.concat(a: p.1, b: str(value: p.0)), domain: luna.zip(a: [1, 2, 3], b: ["a", "b", "c"]))
["a1", "b2", "c3"]

Controls nest. Here a fold runs inside a map, once per row.

luna — the session
luna.map(step: fn (row: list<int>) => luna.fold(step: fn (s: int, x: int) => s + x, domain: row, seed: 0), domain: [[1, 2], [3, 4, 5], []])
[3, 12, 0]

A fold can stop early. When the step answers {done, value}, the first done ends it. This one sums up to and including the first element above 4.

luna — the session
luna.fold(domain: xs, seed: 0, step: fn (acc: int, x: int) => {done: x > 4, value: acc + x})
14

tally counts values, in first-seen order.

luna — the session
luna.tally(domain: xs)
[{count: 1, key: 3}, {count: 2, key: 1}, {count: 1, key: 4}, {count: 1, key: 5}, {count: 1, key: 9}, {count: 1, key: 2}, {count: 1, key: 6}]

Reductions take a list directly.

luna — the session
luna.min(value: [3.0, 1.0, 2.0])
1.0

A list of records has no order of its own, so the smallest one is a question the refusal sends back to me: name a key with by:.

luna — the session
let fruit = [{name: "fig", size: 3}, {name: "pear", size: 1}, {name: "melon", size: 9}]
values: fruit
luna.min(value: fruit)
terminal 94:1:1: LUNA3003: compiling failed: exact type conflict: language construct "luna.min" derives what it publishes from its operands; luna.min reads a list of int, float, float32, int32, str, or bool, or of positional products of them, or an array of int, float, float32, or int32; a list of any other element chooses by a key it names with by:, but its operands are list<{name: str, size: int}>[3]
  1 | luna.min(value: fruit)
    | ^~~~~~~~~~~~~~~~~~~~~~
luna.min(value: fruit, by: fn (f: {name: str, size: int}) => f.size)
{name: "pear", size: 1}
luna.max(value: fruit, by: fn (f: {name: str, size: int}) => f.size).name
melon

One vocabulary, and parallel is a word on it: map and its siblings take parallel: true. The values are identical either way; only time changes. A step with some work in it:

luna — the session
fn heavy(x: int) -> int {
  luna.fold(step: fn (acc: int, i: int) => (acc * 31 + i + x) % 1000003, domain: luna.range(start: 0, stop: 200), seed: x)
}
functions: fn.heavy
let ns = luna.range(start: 0, stop: 100000)
values: ns
let serial = luna.map(step: fn.heavy, domain: ns)
values: serial
let threaded = luna.map(step: fn.heavy, domain: ns, parallel: true)
values: threaded
serial == threaded
true

A fold is a chain, so it runs serially. Given a merge: that joins two accumulators, it folds blocks on the engine’s threads and then merges them in order, to the same total.

luna — the session
luna.fold(domain: ns, seed: 0, step: fn (s: int, x: int) => s + fn.heavy(x: x))
50002871996
luna.fold(domain: ns, seed: 0, step: fn (s: int, x: int) => s + fn.heavy(x: x), merge: fn (a: int, b: int) => a + b)
50002871996

To see what parallel buys, the shipped time module measures: time.tick() takes a mark, time.tock(since:) answers the seconds since, as units.seconds. Marks are values, so timing two things takes two marks. The numbers depend on the machine, so I compare them instead of printing them. The benchmarks measure the same engine against C++, Lua and Python.

luna — the session
import time
types: units.degrees, units.radians, units.seconds, units.milliseconds, time.mark, time.instant
functions: units.degrees, units.radians, units.seconds, units.milliseconds, time.tick, time.tock, time.now
module: units (4 types, 4 functions; generation 88)
module: time (2 types, 3 functions; generation 88)
let start = time.tick()
values: start
let summed = luna.fold(domain: ns, seed: 0, step: fn (s: int, x: int) => s + fn.heavy(x: x), merge: fn (a: int, b: int) => a + b)
values: summed
let elapsed = time.tock(since: start)
values: elapsed
elapsed.value > 0.0
true

09

No recursion, by construction

A countdown is the first thing I would write with recursion.

luna — the session
fn countdown(n: int) -> int { if n == 0 { 0 } else { countdown(n: n - 1) } }
terminal 109:1:54: LUNA3008: compiling failed: recursion is refused by construction: the invocation graph is acyclic, so every program terminates and its activation depth is known before it runs — iteration is a control over a domain, see luna.controls()
  1 | fn countdown(n: int) -> int { if n == 0 { 0 } else { countdown(n: n - 1) } }
    |                                                      ^~~~~~~~~~~~~~~~~~~
terminal 109:1:29: note: the active invocation chain reaches this callable again

The call graph is acyclic, so every program terminates and its depth is known before it runs. Repetition is a fold over a range. A factorial of ten:

luna — the session
luna.fold(step: fn (acc: int, i: int) => acc * i, domain: luna.range(start: 1, stop: 11), seed: 1)
3628800

Hiding the cycle behind a second function changes nothing. Two declarations on one line form one cell, so each can name the other, and the cycle between them is refused the same way.

luna — the session
fn ping(n: int) -> int { if n == 0 { 0 } else { pong(n: n - 1) } } fn pong(n: int) -> int { ping(n: n) }
terminal 111:1:93: LUNA3008: compiling failed: recursion is refused by construction: the invocation graph is acyclic, so every program terminates and its activation depth is known before it runs — iteration is a control over a domain, see luna.controls()
  1 | fn ping(n: int) -> int { if n == 0 { 0 } else { pong(n: n - 1) } } fn pong(n: int) -> int { ping(n: n) }
    |                                                                                             ^~~~~~~~~~
terminal 111:1:24: note: the active invocation chain reaches this callable again

10

Records, names, and enums

A record is written with braces, and a tuple with parentheses.

luna — the session
let point = {x: 3.0, y: 4.0}
values: point
point.y
4.0
let pair = (1, "one")
values: pair
pair.1
one

An alias names a record shape without making a new type.

luna — the session
alias geo.point = {x: float, y: float}
types: geo.point
fn norm(p: geo.point) -> float { luna.sqrt(value: p.x * p.x + p.y * p.y) }
functions: fn.norm
fn.norm(p: point)
5.0

A type makes a distinct type over a representation. A metre is stored as a float, but it is not one: a function that takes metres cannot be handed a bare number by mistake, and the distinction costs nothing at run time.

luna — the session
type geo.metres = float
types: geo.metres
fn doubled(d: geo.metres) -> geo.metres { geo.metres(value: d.value * 2.0) }
functions: fn.doubled
fn.doubled(d: 2.5)
terminal 121:1:12: LUNA3003: compiling failed: exact type conflict: argument "d" must conform to linked callable "fn.doubled"; float is not assignable to geo.metres; construct it: geo.metres(value: 2.5)
  1 | fn.doubled(d: 2.5)
    |            ^~~~~~
terminal 121:1:15: note: a float literal
fn.doubled(d: geo.metres(value: 2.5))
geo.metres(value: 5.0)

A function can answer several named results. They form one named product. The reductions I met in the controls chapter work directly on the list.

luna — the session
fn bounds(values: list<float>) -> {low: float, high: float} {
  {low: luna.min(value: values), high: luna.max(value: values)}
}
functions: fn.bounds
fn.bounds(values: [2.0, 7.0, 1.0])
{high: 7.0, low: 1.0}
fn.bounds(values: [2.0, 7.0, 1.0]).high
7.0

The catalog can also be asked by type: what produces a list<float>, and what takes one. My new function is listed beside the language’s own rows.

luna — the session
luna.catalog(type: list<float>)
type list<float>
  producers
    list<float>(values: list<int>) -> list<float>
  parameters
    luna.sum(value: list<float>) -> float
    list<int>(values: list<float>) -> list<int>
    fn.bounds(values: list<float>) -> {high: float, low: float}
  producers by instantiation
    luna.filter(keep: (float) -> bool, domain: list<float>, parallel: boolean = false) -> list<float>
    luna.sort(T2: luna.orderable, value: list<float>, by: (float) -> T2 = <element>, descending: bool = false) -> list<float>
    luna.slice(value: list<float>, start: int, stop: int) -> list<float>
    luna.concat(a: list<float>, b: list<float>) -> list<float>
    luna.reverse(value: list<float>) -> list<float>
    luna.append(list: list<float>, value: float) -> list<float>[1..]
    luna.insert(list: list<float>, at: int, value: float) -> list<float>[1..]
    luna.remove_at(list: list<float>, at: int) -> list<float>
    luna.set_at(list: list<float>, at: int, value: float) -> list<float>
    luna.unique(value: list<float>) -> list<float>
  parameters by instantiation
    luna.fold(T1: type, step: (T1, float) -> T1, domain: list<float>, seed: T1) -> T1
    compiler.partials(T1: type, step: (T1, float) -> T1, domain: list<float>, seed: T1) -> list<T1>
    luna.each(step: (float) -> none, domain: list<float>, seed: none = none) -> none
    luna.map(T2: type, step: (float) -> T2, domain: list<float>, parallel: boolean = false) -> list<T2>
    luna.generate(T2: type, step: (float) -> list<T2>, domain: list<float>, parallel: boolean = false) -> jagged<T2>
    luna.filter(keep: (float) -> bool, domain: list<float>, parallel: boolean = false) -> list<float>
    luna.any(test: (float) -> bool, domain: list<float>, parallel: boolean = false) -> bool
    luna.all(test: (float) -> bool, domain: list<float>, parallel: boolean = false) -> bool
    luna.zip(T2: type, a: list<float>, b: list<T2>) -> list<(float, T2)>
    luna.zip(T1: type, a: list<T1>, b: list<float>) -> list<(T1, float)>
    luna.zip(K2: type, V2: type, a: list<float>, b: index<K2, V2>) -> list<(float, (K2, V2))>
    luna.zip(K1: type, V1: type, a: index<K1, V1>, b: list<float>) -> list<((K1, V1), float)>
    luna.sort(T2: luna.orderable, value: list<float>, by: (float) -> T2 = <element>, descending: bool = false) -> list<float>
    luna.group(T2: luna.orderable_type, step: (float) -> T2, domain: list<float>) -> list<{key: T2, items: list<float>}>
    luna.tally(domain: list<float>) -> list<{count: int, key: float}>
    luna.at(value: list<float>, index: int) -> float | missing
    luna.length(value: list<float>) -> int
    luna.mean(value: list<float>) -> float
    luna.norm(value: list<float>) -> float
    luna.min(value: list<float>) -> float
    luna.min(T2: luna.orderable, value: list<float>, by: (float) -> T2) -> float
    luna.max(value: list<float>) -> float
    luna.max(T2: luna.orderable, value: list<float>, by: (float) -> T2) -> float
    luna.argmin(value: list<float>) -> int
    luna.argmin(T2: luna.orderable, value: list<float>, by: (float) -> T2) -> int
    luna.argmax(value: list<float>) -> int
    luna.argmax(T2: luna.orderable, value: list<float>, by: (float) -> T2) -> int
    luna.slice(value: list<float>, start: int, stop: int) -> list<float>
    luna.concat(a: list<float>, b: list<float>) -> list<float>
    luna.reverse(value: list<float>) -> list<float>
    luna.append(list: list<float>, value: float) -> list<float>[1..]
    luna.insert(list: list<float>, at: int, value: float) -> list<float>[1..]
    luna.remove_at(list: list<float>, at: int) -> list<float>
    luna.set_at(list: list<float>, at: int, value: float) -> list<float>
    luna.index_of(value: list<float>, of: float) -> int | missing
    luna.contains(value: list<float>, of: float) -> bool
    luna.unique(value: list<float>) -> list<float>
    array(values: list<float>) -> array<float>

An enum is a union of named units, and is tells its members apart. A misspelled member is refused and the refusal lists the members.

luna — the session
enum demo.side = left | right
types: demo.side.left, demo.side.right, demo.side
fn flip(s: demo.side) -> demo.side { if s is demo.side.left l { demo.side.right } else { demo.side.left } }
functions: fn.flip
fn.flip(s: demo.side.left)
demo.side.right(value: none)
fn.flip(s: demo.side.middle)
terminal 130:1:12: LUNA3007: compiling failed: "demo.side.middle" is not a member of demo.side; its members are demo.side.left, demo.side.right
  1 | fn.flip(s: demo.side.middle)
    |            ^~~~~~~~~~~~~~~~

11

Unions, absence, and two kinds of failure

A union holds exactly one of its members. A function that can refuse says so in its result type — int | error, where error is the record {code, message}. A caller cannot forget the failure case; the type says it is there.

luna — the session
fn halved(x: int) -> int | error {
  if x % 2 == 0 { x / 2 } else { {code: 1, message: "not even"} }
}
functions: fn.halved
fn.halved(x: 8)
4
fn.halved(x: 3)
{code: 1, message: "not even"}

A union is not an int, so I cannot add to it directly.

luna — the session
fn.halved(x: 8) + 1
terminal 134:1:1: LUNA3003: compiling failed: exact type conflict: the binary operands must share one exact type; the two sides are constrained to a union and int
  1 | fn.halved(x: 8) + 1
    | ^~~~~~~~~~~~~~~~~~~
terminal 134:1:1: note: linked callable result type
terminal 134:1:19: note: an int literal

? means: continue with the value if it is not an error, or else answer with the error right here.

luna — the session
fn.halved(x: 8)? + 1
5
fn.halved(x: 3)? + 1
{code: 1, message: "not even"}

Inside a function, ? answers the error from that function. Halving twice composes without any branches.

luna — the session
fn quartered(x: int) -> int | error { fn.halved(x: fn.halved(x: x)?) }
functions: fn.quartered
fn.quartered(x: 12)
3
fn.quartered(x: 6)
{code: 1, message: "not even"}

To look inside, is tests a member and binds it. A plain else binds nothing, so the value there is still the union.

luna — the session
let result = fn.halved(x: 3)
values: result
if result is int n { n } else { result.code }
terminal 141:1:39: LUNA3003: compiling failed: exact type conflict: return value projection; field projection requires a named product, but the value is int | {code: int, message: str}; a union's member is read through a member test that names it
  1 | if result is int n { n } else { result.code }
    |                                       ^~~~~

An else arm can bind what the test leaves. Here that is the error alone.

luna — the session
if result is int n { n } else problem { problem.code }
1

A test whose arm reads nothing needs no binding name. Absence has two spellings. none is a value that holds nothing, such as an empty slot in a list. missing is the answer when nothing is there at all. A read of a list<int | none> can produce either, and they mean different things.

luna — the session
let slots = [1, none, 3]
values: slots
fn describe(v: int | none | missing) -> str {
  if v is int n { "a number" } else if v is none { "an empty slot" } else { "no slot at all" }
}
functions: fn.describe
fn.describe(v: luna.at(value: slots, index: 0))
a number
fn.describe(v: luna.at(value: slots, index: 1))
an empty slot
fn.describe(v: luna.at(value: slots, index: 9))
no slot at all

An is chain that tests every member of the union needs no final else, the exception promised with conditionals. Leaving a member out is refused, naming it.

luna — the session
fn kind(v: int | str) -> str { if v is int n { "a number" } else if v is str s { "some text" } }
functions: fn.kind
fn.kind(v: "x")
some text
fn kind_of(v: int | str) -> str { if v is int n { "a number" } }
terminal 150:1:35: LUNA3018: compiling failed: an if chain with no else must test every member of int | str: "str" is unhandled
  1 | fn kind_of(v: int | str) -> str { if v is int n { "a number" } }
    |                                   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~

That leaves faults. A refusal is a value the program handles; a fault ends the run because a claim was false. I state my own claims with luna.fault, and the report carries my sentence.

luna — the session
fn ratio(a: float, b: float) -> float {
  if b == 0.0 { luna.fault(message: "a ratio needs a nonzero denominator") }
  a / b
}
functions: fn.ratio
fn.ratio(a: 1.0, b: 4.0)
0.25
fn.ratio(a: 1.0, b: 0.0)
terminal 151:2:17: LUNA4014: execution failed: a ratio needs a nonzero denominator
  2 |   if b == 0.0 { luna.fault(message: "a ratio needs a nonzero denominator") }
    |                 ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

12

Native multi-dimensional arrays

For numeric work — points, meshes, images — Luna has native multi-dimensional arrays: one dense buffer of numbers and its shape.

luna — the session
let m = array(values: [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
values: m
luna.shape(value: m)
[2, 3]
luna.rank(value: m)
2

One position per axis reads an element. Fewer positions read a row: a view of the axes that remain, over the same buffer. A negative position counts from the end, as it does for lists.

luna — the session
m[1, 2]
6.0
m[1]
array(values: [4.0, 5.0, 6.0])
m[-1]
array(values: [4.0, 5.0, 6.0])
m[0, -1]
3.0

A slice takes a range of positions along one axis. Here, the first two columns:

luna — the session
luna.slice(value: m, axis: 1, start: 0, stop: 2)
array(values: [[1.0, 2.0], [4.0, 5.0]])

luna.take gathers positions in any order, repeats allowed. Here, the columns in reverse:

luna — the session
luna.take(value: m, indices: array(values: [2, 1, 0]), axis: 1)
array(values: [[3.0, 2.0, 1.0], [6.0, 5.0, 4.0]])

A reduction works over the whole block or along an axis:.

luna — the session
luna.sum(value: m)
21.0
luna.sum(value: m, axis: 0)
array(values: [5.0, 7.0, 9.0])
luna.sum(value: m, axis: 1)
array(values: [6.0, 15.0])

Arithmetic broadcasts. Subtracting the column means centers every column.

luna — the session
m * 2.0
array(values: [[2.0, 4.0, 6.0], [8.0, 10.0, 12.0]])
m - luna.mean(value: m, axis: 0)
array(values: [[-1.5, -1.5, -1.5], [1.5, 1.5, 1.5]])
luna.reshape(value: m, shape: [3, 2])
array(values: [[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])
luna.transpose(value: m)
array(values: [[1.0, 4.0], [2.0, 5.0], [3.0, 6.0]])

The controls read an array along its first axis, so a step over m receives each row as a rank-one view. The row’s type, array<float>[3], states its length: an array type can state the extent of each axis.

luna — the session
luna.map(domain: m, step: fn (row: array<float>[3]) => luna.sum(value: row))
array(values: [6.0, 15.0])
luna.filter(domain: m, keep: fn (row: array<float>[3]) => row[0] > 2.0)
array(values: [[4.0, 5.0, 6.0]])

A type that states its extents can also check a shape at run time.

luna — the session
array<float>[2, 3](value: m)
array(values: [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
array<float>[3, 3](value: m)
terminal 173:1:1: LUNA4010: execution failed: array extents are outside the rank or a window the destination states
  1 | array<float>[3, 3](value: m)
    | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~

Now I want the elements of an integer array above 3. An ordering over arrays answers element by element, as a mask.

luna — the session
let flat = array(values: [1, 2, 3, 4, 5, 6])
values: flat
flat > 3
array(values: [false, false, false, true, true, true])

A scalar can stand on either side, and arithmetic works the same way.

luna — the session
3.0 < m
array(values: [[false, false, false], [true, true, true]])
3.0 - m
array(values: [[2.0, 1.0, 0.0], [-1.0, -2.0, -3.0]])

Equality is different: == asks a whole-value question that has one answer, so it stays a single bool. An ordering over arrays has only the elementwise answer. Asking == of an array beside a single number is refused, and the refusal names the elementwise verb.

luna — the session
flat == flat
true
m == 0.0
terminal 179:1:1: LUNA3003: compiling failed: exact type conflict: the binary operands must share one exact type; float is not assignable to array<float>[2, 3]; == asks whether two values are the same whole, so an array beside a value is not its question; for the elementwise mask write luna.equal(a: m, b: 0.0) (luna.not_equal for !=)
  1 | m == 0.0
    | ^~~~~~~~
terminal 179:1:6: note: a float literal
terminal 179:1:1: note: definition-time ABI seed
terminal 154:1:1: note: "m" was committed by terminal 154 (revision 130)

luna.compress keeps what a mask selects, and its signature says it wants an axis for an array.

luna — the session
luna.signature(of: luna.compress)
luna.compress(T1: type, condition: array<bool>[..any], value: array<T1>[..any], axis: int) -> array<T1>[..any]
luna.compress(T1: type, condition: array<bool>, value: jagged<T1>) -> jagged<T1>
luna.compress(condition: flat > 3, value: flat, axis: 0)
array(values: [4, 5, 6])

A mask over one row selects columns. Here, the columns whose first entry is above 1:

luna — the session
luna.compress(condition: m[0] > 1.0, value: m, axis: 1)
array(values: [[2.0, 3.0], [5.0, 6.0]])

luna.where chooses element by element between two arrays of one shape, so values at or below 4 become zero while the rest stay.

luna — the session
luna.where(mask: m > 4.0, then: m, otherwise: m * 0.0)
array(values: [[0.0, 0.0, 0.0], [0.0, 5.0, 6.0]])

luna.nonzero answers the flat positions where a mask holds.

luna — the session
luna.nonzero(value: m > 4.0)
array(values: [4, 5])

list(array:) crosses back to a list.

luna — the session
list(array: flat)
[1, 2, 3, 4, 5, 6]

13

Jagged blocks

A jagged is a sequence of blocks of unequal length, with no allocation per block. A mesh of triangles and quads is one. Underneath: one flat array of data, and the offsets where each block starts. A triangle and a quad:

luna — the session
let faces = luna.jagged(offsets: array(values: [0, 3, 7]), data: array(values: [0, 1, 2, 2, 1, 3, 4]))
values: faces
luna.length(value: faces)
2
luna.at(value: faces, index: 1)
array(values: [2, 1, 3, 4])

The controls from lists and arrays work here unchanged. One vocabulary covers lists, arrays and jagged blocks; each step receives one block, as an array view of the data.

luna — the session
luna.map(domain: faces, step: fn (f: array<int>) => luna.shape(value: f)[0])
[3, 4]
luna.filter(domain: faces, keep: fn (f: array<int>) => luna.shape(value: f)[0] == 4)
luna.jagged(offsets: array(values: [0, 4]), data: array(values: [2, 1, 3, 4]))
luna.fold(domain: faces, seed: 0, step: fn (corners: int, f: array<int>) => corners + luna.shape(value: f)[0])
7

luna.generate builds one: each step answers a list, and the lists become the blocks, in order. Each face answers its edges, as many as it has corners:

luna — the session
luna.generate(domain: faces, step: fn (f: array<int>) => luna.map(domain: luna.range(start: 0, stop: luna.shape(value: f)[0]), step: fn (i: int) => (f[i], f[(i + 1) % luna.shape(value: f)[0]])))
luna.jagged(offsets: array(values: [0, 3, 7]), data: [(0, 1), (1, 2), (2, 0), (2, 1), (1, 3), (3, 4), (4, 2)])

14

Metafunctions and kinds

A parameter annotated type is known while compiling. A function with one is a metafunction — an instruction for making a function. I write one generic definition; each type it meets gets its own exact compiled function.

luna — the session
fn first(T: type, values: list<T>) -> T { values[0] }
functions: fn.first

A call usually infers T from its arguments, and can also spell it.

luna — the session
fn.first(values: [7, 8, 9])
7
fn.first(values: ["a", "b"])
a
fn.first(T: int | none, values: [1, none])
1

Each distinct T was compiled once, on first use, and published. The environment shows the instantiations next to the metafunction.

luna — the session
luna.filter(domain: luna.env(), keep: fn (e) => luna.starts_with(value: e.name, prefix: "fn.first"))
[{kind: "metafunction", name: "fn.first", type_name: ""}, {kind: "function", name: "fn.first<int | none>", type_name: "(list<int | none>) -> int | none"}, {kind: "function", name: "fn.first<int>", type_name: "(list<int>) -> int"}, {kind: "function", name: "fn.first<str>", type_name: "(list<str>) -> str"}]

A concept names the set of types a kind may range over. The compiler reads it only while compiling.

luna — the session
concept demo.number = int | float
types: demo.number
fn largest(T: demo.number, values: list<T>) -> T {
  luna.fold(step: fn (best: T, v: T) => if v > best { v } else { best }, domain: values, seed: values[0])
}
functions: fn.largest
fn.largest(values: [3, 9, 4])
9
fn.largest(values: [0.5, 2.5])
2.5
fn.largest(values: ["a", "b"])
terminal 202:1:1: LUNA3013: compiling failed: type parameter "T" of metafunction "fn.largest" is constrained by concept demo.number, and str is outside it
  1 | fn.largest(values: ["a", "b"])
    | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Every argument that determines T must agree. Mixing an int seed with float values is refused, naming both.

luna — the session
fn total(T: demo.number, values: list<T>, zero: T) -> T { luna.fold(step: fn (s: T, x: T) => s + x, domain: values, seed: zero) }
functions: fn.total
fn.total(values: [1.5, 2.5], zero: 0.0)
4.0
fn.total(values: [1.5, 2.5], zero: 0)
terminal 205:1:1: LUNA3013: compiling failed: call to metafunction "fn.total" has conflicting types for type parameter "T": float and int
  1 | fn.total(values: [1.5, 2.5], zero: 0)
    | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

15

Modules and imports

Luna ships modules, and none is loaded until I import it.

luna — the session
color.map(value: 0.5)
terminal 206:1:1: LUNA3007: compiling failed: no installed callable named "color.map" exists in this compilation snapshot; no module named "color" is loaded here — submit "import color" as a cell of its own
  1 | color.map(value: 0.5)
    | ^~~~~~~~~~~~~~~~~~~~~

One is here already. When I imported time, the console listed units too: a module’s own imports load with it.

luna — the session
luna.catalog(module: units)
module units — Units that change the meaning of a number without changing its storage. [4 callables, 4 types]
  type units.degrees [nominal]
  type units.milliseconds [nominal]
  type units.radians [nominal]
  type units.seconds [nominal]
  units.degrees(radians: units.radians) -> units.degrees [library, cost 4]
    The degree angle a radian angle measures.
  units.milliseconds(seconds: units.seconds) -> units.milliseconds [library, cost 2]
    The millisecond duration a second duration measures.
  units.radians(degrees: units.degrees) -> units.radians [library, cost 4]
    The radian angle a degree angle measures.
  units.seconds(milliseconds: units.milliseconds) -> units.seconds [library, cost 2]
    The second duration a millisecond duration measures.

A unit is a number with a meaning, and its type’s own name both makes and converts one. value: constructs a unit from a number; another unit’s name converts from that unit.

luna — the session
units.degrees(value: 90.0)
units.degrees(value: 90.0)
units.degrees(radians: units.radians(value: luna.pi))
units.degrees(value: 180.0)

So the argument name is the only thing that tells construction from conversion, and leaving it out is refused, listing both faces.

luna — the session
units.degrees(units.radians(value: luna.pi))
terminal 210:1:1: LUNA3017: compiling failed: this call of type "units.degrees" matches several of its signatures and selects none: (value: float) -> units.degrees, (radians: units.radians) -> units.degrees; only argument names tell its construction from its functions, so name the argument
  1 | units.degrees(units.radians(value: luna.pi))
    | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

A shipped module is source, and I can read it.

luna — the session
luna.source(of: units.radians)
type units.radians = float

fn radians(degrees: units.degrees) -> units.radians {
  units.radians(value: degrees.value * luna.pi / 180.0)
}

My own module is one file whose first line names it. The comments above a declaration become its documentation. I save this next to where I opened the console:

A module fileshapes.luna
module shapes
import units

// a circle, by its radius
alias shapes.circle = {radius: float}

// the area a circle encloses
fn area(of: shapes.circle) -> float {
  luna.pi * of.radius * of.radius
}

// the angle an arc of the given length turns through, around a circle
fn arc_angle(of: shapes.circle, length: float) -> units.radians {
  units.radians(value: length / of.radius)
}

// the circle every other is measured against
let unit = {radius: 1.0}
luna — the session
import shapes
types: shapes.circle
values: shapes.unit
functions: shapes.area, shapes.arc_angle
module: shapes (1 types, 2 functions; generation 182)
luna.catalog(module: shapes)
module shapes [2 callables, 1 types]
  type shapes.circle [alias]
  shapes.arc_angle(of: shapes.circle, length: float) -> units.radians [library, cost 1]
    the angle an arc of the given length turns through, around a circle
  shapes.area(of: shapes.circle) -> float [library, cost 3]
    the area a circle encloses
shapes.area(of: {radius: 2.0})
12.566370614359172
units.degrees(radians: shapes.arc_angle(of: {radius: 1.0}, length: luna.pi))
units.degrees(value: 180.0)

A module-level let is a constant: a value under the module’s name, like luna.pi, not a call.

luna — the session
shapes.unit
{radius: 1.0}
shapes.area(of: shapes.unit)
3.141592653589793

Shipped modules use the same form. The color module’s viridis color map is a constant array of colors.

luna — the session
import color
types: color.rgb
values: color.turbo, color.viridis
functions: color.range_refused, color.stops_refused, color.map
module: color (1 types, 3 functions; generation 188)
color.viridis()
terminal 219:1:1: LUNA3001: compiling failed: "color.viridis" is a value, not a function: its name alone is its value, so write it without a call
  1 | color.viridis()
    | ^~~~~~~~~~~~~~~
luna.shape(value: color.viridis)
[16, 3]

A module can also ship compiled. The client compiles a source file once into an image, outside any session; a session that finds the image loads it without compiling. --private leaves the source text out. I keep this module’s source in a subdirectory, so the only turns the session can find is the image:

A module filesrc/turns.luna
module turns
import units

// the fraction of a full turn an angle covers
fn of(angle: units.degrees) -> float {
  angle.value / 360.0
}
shell

luna --compile-module turns src/turns.luna -o turns.lmdi --private

The compile prints nothing when it succeeds. Back in the session, the import finds turns.lmdi.

luna — the session
import turns
functions: turns.of
module: turns (0 types, 1 functions; generation 190)
turns.of(angle: units.degrees(value: 90.0))
0.25

The image keeps the signatures and the documentation — the module is as discoverable as one shipped as source. Only the source text is withheld.

luna — the session
luna.doc(of: turns.of)
turns.of(angle: units.degrees) -> float [library, cost 2]
the fraction of a full turn an angle covers
luna.source(of: turns.of)
missing

16

Touching the world

A program touches the world only through world steps, in the order I wrote them. The console mounts its own module for that, and the catalog tags each row with its posture: world changes something outside the program, and read only observes.

luna — the session
luna.catalog(module: io)
module luna.cli — this client's own World: writing a value, asking a user, and putting text on disk or taking it back [4 callables, 0 types]
  io.ask(prompt: str) -> future<str> [world, cost 10000]
    ask this client's user one question
    tags: read, input, prompt, question
  io.print(value: T1) -> none [world, cost 100]
    write one value to this client's output
    tags: write, output, show
  io.read_text(path: str) -> str | {code: int, message: str} [read, cost 1000000]
    read a file's text, or say why it did not come
    tags: read, load, file, text, document, data
  io.write_text(path: str, text: str) -> none | {code: int, message: str} [world, cost 1000000]
    write text to a file, or say why it did not go
    tags: write, save, file, text, document, report
… trimmed
io.print(value: "hello from a cell");
hello from a cell
luna.each(step: fn (x: int) => io.print(value: x), domain: [1, 2, 3]);
1
2
3

Files follow the same two postures. Reading is not an effect: it changes nothing, and it answers what the file held at the moment I asked, as an ordinary value I keep. I have a small table next to where I opened the console:

A data filesizes.csv
width,depth
2.5,4.0
3.0,1.5
luna — the session
io.read_text(path: "sizes.csv")
width,depth
2.5,4.0
3.0,1.5

A file that is not there is not a fault. The read answers an error value, so the failure is in the type, as in the unions chapter.

luna — the session
io.read_text(path: "missing.csv")
{code: 3, message: "io.read_text: no file opens for reading at \"missing.csv\""}
let sizes = io.read_text(path: "sizes.csv")
values: sizes

Computing over the text is ordinary Luna: split it into lines, skip the header, and parse each row. A row that does not parse answers missing through ?.

luna — the session
fn area_of(line: str) -> float | missing { let cells = luna.split(value: line, by: ","); float(text: cells[0])? * float(text: cells[1])? }
functions: fn.area_of
fn areas(text: str) -> list<float | missing> { luna.map(domain: luna.slice(value: luna.lines(value: text), start: 1, stop: luna.length(value: luna.lines(value: text))), step: fn.area_of) }
functions: fn.areas
fn.areas(text: sizes?)
[10.0, 4.5]

Writing is a world step: it changes something outside the program, and it happens where it is reached, in order. A function that writes says so in its type, because a write can fail too.

luna — the session
fn save(text: str) -> none | error { io.write_text(path: "areas.txt", text: str(value: fn.areas(text: text))) }
functions: fn.save
fn.save(text: sizes?)
none
io.read_text(path: "areas.txt")
[10.0, 4.5]

The fourth row, io.ask, puts a question to the person at the prompt and answers a future. It needs someone typing, so it stays off this page.

A function that reaches a world step inherits that posture. The compiler follows it through every call.

luna — the session
fn say(x: int) -> none { io.print(value: x) }
functions: fn.say

A worker may not change the world. A parallel step that calls fn.say, which prints, is refused while compiling.

luna — the session
luna.map(step: fn (x: int) { fn.say(x: x); x }, domain: [1, 2], parallel: true)
terminal 238:1:1: LUNA3001: compiling failed: language control "luna.map" declares "parallel: true", but separately compiled function "fn.say" is not admissible on a worker; a worker performs no world step — no effect and no wait — and enters only natives their own host declared reentrant
  1 | luna.map(step: fn (x: int) { fn.say(x: x); x }, domain: [1, 2], parallel: true)
    | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

17

Async, and chains of work

Some work should run without the session waiting on it. luna.spawn starts a call and answers a future at once; luna.await answers the call’s value, or an error if it failed.

luna — the session
fn work(x: int) -> int { x + 7 }
functions: fn.work
let pending = luna.spawn(call: fn.work(x: 1))
values: pending
pending
<future<int>>
luna.await(value: pending)
8

What comes next can be written before the answer exists. luna.then takes a future and a step and answers a new future, without waiting.

luna — the session
luna.signature(of: luna.then)
luna.then(T1: type, T2: type, task: future<T1>, step: (T1) -> T2) -> future<T2>
let doubled = luna.then(task: luna.spawn(call: fn.work(x: 1)), step: fn (x: int) => x * 2)
values: doubled
luna.await(value: doubled)
16

A then of a then makes a chain, here three links long.

luna — the session
let chain = luna.then(task: luna.then(task: luna.spawn(call: fn.work(x: 1)), step: fn (x: int) => x * 2), step: fn (x: int) => x + 1)
values: chain
luna.await(value: chain)
17

If a link fails, the steps after it never run. The failure travels down the chain and arrives as an ordinary error value at the tail’s await. Here the head reads past the end of a list:

luna — the session
let broken = luna.then(task: luna.spawn(call: fn.pick(values: [1, 2], i: 5)), step: fn (x: int) => x * 2)
values: broken
luna.await(value: broken)
{code: 2, message: "list index is out of bounds"}

Where does a step run? A pure step runs as offshoot work on whichever worker settles the task before it. A step that writes the world cannot run on a worker, so it is delivered once on the live lane, when the console returns to the prompt. I let the head settle first, then chain a step that prints:

luna — the session
let head = luna.spawn(call: fn.pick(values: [1, 2], i: 1))
values: head
luna.await(value: head)
2
let shown = luna.then(task: head, step: fn (x: int) { io.print(value: x) })
values: shown
2

The print came after the cell published shown, at the prompt’s return. A world-writing step can also write a live entry a watch follows. Live entries and watches come next; the chain’s story completes in the watch chapter.

18

The environment, and its live tier

Everything I have published stands in one environment. luna.env() answers it as typed records, so I can filter it like any list.

luna — the session
luna.signature(of: luna.env)
luna.env() -> list<{kind: str, name: str, type_name: str}>
luna.filter(domain: luna.env(), keep: fn (e) => e.kind == "value")
[{kind: "value", name: "answer", type_name: "int"}, {kind: "value", name: "broken", type_name: "future<int>"}, {kind: "value", name: "chain", type_name: "future<int>"}, {kind: "value", name: "doubled", type_name: "future<int>"}, {kind: "value", name: "elapsed", type_name: "units.seconds"}, {kind: "value", name: "faces", type_name: "jagged<int>"}, {kind: "value", name: "flat", type_name: "array<int>[6]"}, {kind: "value", name: "fruit", type_name: "list<{name: str, size: int}>[3]"}, {kind: "value", name: "head", type_name: "future<int>"}, {kind: "value", name: "m", type_name: "array<float>[2, 3]"}, {kind: "value", name: "more", type_name: "list<int>[9]"}, {kind: "value", name: "ns", type_name: "range<int>"}, {kind: "value", name: "pair", type_name: "(int, str)"}, {kind: "value", name: "pending", type_name: "future<int>"}, {kind: "value", name: "point", type_name: "{x: float, y: float}"}, {kind: "value", name: "price", type_name: "(float) -> float"}, {kind: "value", name: "rate", type_name: "float"}, {kind: "value", name: "result", type_name: "int | {code: int, message: str}"}, {kind: "value", name: "serial", type_name: "list<int>"}, {kind: "value", name: "shown", type_name: "future<none>"}, {kind: "value", name: "size", type_name: "str"}, {kind: "value", name: "sizes", type_name: "str | {code: int, message: str}"}, {kind: "value", name: "slots", type_name: "list<int | none>[3]"}, {kind: "value", name: "start", type_name: "time.mark"}, {kind: "value", name: "summed", type_name: "int"}, {kind: "value", name: "threaded", type_name: "list<int>"}, {kind: "value", name: "xs", type_name: "list<int>[8]"}]

A later let rebinds a name. Earlier answers do not change. The environment keeps no history by default, so I ask it to keep the last three acts.

luna — the session
env.window(revisions: 3);
let total = 10
values: total
let total = 25
values: total
total
25
env.prior(name: "total")
10

A committed binding records an act. The environment has a second tier for what changes in the instant, like a pointer or a slider: a live entry. It has a declared type, it is written where it stands with :=, and it keeps no history.

luna — the session
live angle: int = 0
angle := angle + 15
angle
15

A function can write a live entry, and the next line reads the new value.

luna — the session
fn turn(by: int) -> none { angle := angle + by }
functions: fn.turn
fn.turn(by: 30);
angle
45
luna.filter(domain: luna.env(), keep: fn (e) => e.kind == "live value")
[{kind: "live value", name: "angle", type_name: "int"}]

The console’s :history command lists what this session committed: one numbered act each, with the cell that act recorded beneath it. Refused cells are not there — a refusal publishes nothing. Here is the stretch where I met metafunctions: each first use of fn.first with a new type published one instantiation, and the import published a module. The runtime page follows one such act through the application.

luna — the session
:history
… trimmed
167: person luna functions=fn.first
    fn first(T: type, values: list<T>) -> T { values[0] }
168: person luna functions=fn.first<int> presentation
    fn.first(values: [7, 8, 9])
169: person luna functions=fn.first<str> presentation
    fn.first(values: ["a", "b"])
170: person luna functions=fn.first<int | none> presentation
    fn.first(T: int | none, values: [1, none])
171: person luna presentation
    luna.filter(domain: luna.env(), keep: fn (e) => luna.starts_with(value: e.name, prefix: "fn.first"))
172: person luna types=demo.number
    concept demo.number = int | float
173: person luna functions=fn.largest
    fn largest(T: demo.number, values: list<T>) -> T {
      luna.fold(step: fn (best: T, v: T) => if v > best { v } else { best }, domain: values, seed: values[0])
    }
174: person luna functions=fn.largest<int> presentation
    fn.largest(values: [3, 9, 4])
175: person luna functions=fn.largest<float> presentation
    fn.largest(values: [0.5, 2.5])
176: person luna functions=fn.total
    fn total(T: demo.number, values: list<T>, zero: T) -> T { luna.fold(step: fn (s: T, x: T) => s + x, domain: values, seed: zero) }
177: person luna functions=fn.total<float> presentation
    fn.total(values: [1.5, 2.5], zero: 0.0)
178: person luna presentation
    luna.catalog(module: units)
179: person luna presentation
    units.degrees(value: 90.0)
180: person luna presentation
    units.degrees(radians: units.radians(value: luna.pi))
181: person luna presentation
    luna.source(of: units.radians)
182: person luna types=shapes.circle values=shapes.unit functions=shapes.area, shapes.arc_angle module=shapes types=1 functions=2
    import shapes
183: person luna presentation
    luna.catalog(module: shapes)
184: person luna presentation
    shapes.area(of: {radius: 2.0})
185: person luna presentation
    units.degrees(radians: shapes.arc_angle(of: {radius: 1.0}, length: luna.pi))
186: person luna presentation
    shapes.unit
187: person luna presentation
    shapes.area(of: shapes.unit)
188: person luna types=color.rgb values=color.turbo, color.viridis functions=color.range_refused, color.stops_refused, color.map module=color types=1 functions=3
… trimmed

19

Watches instead of event handlers

In most applications, something happening is an event: a click calls a listener someone registered on a button, and the listener updates whatever depends on it. Luna has no listeners. What changes is a live entry, and what depends on it says so. The shipped watch module is where that is declared.

luna — the session
import watch
types: scope.id, watch.id
functions: scope.recording, watch.detach, watch.attach, watch.attach, watch.attach
module: scope (1 types, 1 functions; generation 235)
module: watch (1 types, 4 functions; generation 235)
luna.signature(of: watch.attach)
watch.attach(Ts: ..type, ..binding to: Ts, body: (..Ts | missing) -> none, initialize: bool = false, system: bool = false) -> watch.id
watch.attach(Ts: ..type, ..binding to: Ts, body: (..Ts | missing, units.seconds) -> none, every: units.seconds | units.milliseconds, initialize: bool = false, system: bool = false) -> watch.id
watch.attach(Ts: ..type, ..binding to: Ts, body: (..Ts | missing) -> none, settle: units.seconds | units.milliseconds, system: bool = false) -> watch.id

The parameter that matters is ..binding to: Ts. A binding is a name standing in the environment, a live entry or a committed let, and a binding seat takes the name itself, not the value it holds right now. Naming it is the whole dependency declaration. The body gets one parameter per binding, receiving its newest value, or missing where the name stands nowhere. An expression is not a binding, so it is refused.

luna — the session
live selected: list<int> = []
watch.attach(to: luna.length(value: selected), body: fn (n: int | missing) { none })
terminal 270:1:18: LUNA3030: compiling failed: the binding seat "to" takes a binding itself, and "luna.length(value: selected)" is an expression; name the binding itself
  1 | watch.attach(to: luna.length(value: selected), body: fn (n: int | missing) { none })
    |                  ^~~~~~~~~~~~~~~~~~~~~~~~~~~~

So, a small application. Its model is two live entries — the selected ids and a zoom factor. Its view is a status line and a window title that follow them.

luna — the session
live zoom: float = 1.0
live status: str = ""
live title: str = ""

The status follows the selection. My first body forgets that the value can be missing.

luna — the session
watch.attach(to: selected, body: fn (ids: list<int> | missing) { status := luna.concat(a: str(value: luna.length(value: ids)), b: " selected") }, initialize: true)
terminal 274:1:102: LUNA3003: compiling failed: exact type conflict: language construct "luna.length" counts a list, a str, or a jagged; a union is not assignable to list, str, or jagged
  1 | watch.attach(to: selected, body: fn (ids: list<int> | missing) { status := luna.concat(a: str(value: luna.length(value: ids)), b: " selected") }, initialize: true)
    |                                                                                                      ^~~~~~~~~~~~~~~~~~~~~~~
terminal 274:1:43: note: declared as list<int> | missing
watch.attach(to: selected, body: fn (ids: list<int> | missing) { if ids is list<int> now { status := luna.concat(a: str(value: luna.length(value: now)), b: " selected") } else { none } }, initialize: true)
watch.id(value: 0)

The title follows two bindings at once, the status and the zoom, named as one product.

luna — the session
watch.attach(to: (status, zoom), body: fn (s: str | missing, z: float | missing) { if s is str text { if z is float factor { title := luna.concat(a: text, b: luna.concat(a: " at ", b: str(value: factor))) } else { none } } else { none } }, initialize: true)
watch.id(value: 1)
title
0 selected at 1.0

A click that selects something is one write. Everything that named the selection recomputes, and whatever named those results recomputes after them. The console runs due bodies when a cell finishes, so the whole cascade has settled before the prompt returns.

luna — the session
selected := luna.append(list: selected, value: 4)
title
1 selected at 1.0
zoom := 2.0
title
1 selected at 2.0

Each watch is one row of a live table the watch module declares. The row holds only what the attach said. Whether a delivery is pending is the schedule’s fact, kept beside the table, so delivering never rewrites the row.

luna — the session
luna.at(value: watch.rows, index: watch.id(value: 1))
{body: <fn>, every: none, initialize: true, scope: none, settle: none, system: false}

The async story completes here. A chain’s last step can write a live entry, and the watches that named it take over. Some work finds an id in the background; its tail adds the id to the selection; the status and the title follow on their own.

luna — the session
let found = luna.spawn(call: fn.pick(values: [7, 8, 9], i: 2))
values: found
luna.await(value: found)
9
let added = luna.then(task: found, step: fn (id: int) { selected := luna.append(list: selected, value: id) })
values: added
title
2 selected at 2.0

Formira, the application built on Luna, works this way at scale. Nowhere in it is an event listener, or a callback registered on an emitter. A gesture writes a live entry, and every standing computation that named it recomputes, transitively, delivered by the host’s step. Event handling becomes a declared dependency graph. The runtime page has the rest of the story.

An event system can loop: a handler updates something whose handler updates the first thing again. Here that loop is refused when it would be declared. Suppose a reset should clear the selection whenever the status changes:

luna — the session
watch.attach(to: status, body: fn (s: str | missing) { if s is str text { selected := [] } else { none } })
modules/watch.luna:69:3: LUNA4010: execution failed: the watch follows "status" and its body writes "selected", which a standing watch follows and writes "status", so the watch would close a cycle
  69 |   watch.rows := luna.set(index: watch.rows, key: id,
     |   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The report names the path: the selection feeds the status, so a watch that follows the status and writes the selection would close a loop. A body that writes what it follows, like a tidy-up that sorts the selection in place, is the same loop with one member.

luna — the session
watch.attach(to: selected, body: fn (ids: list<int> | missing) { if ids is list<int> now { selected := luna.sort(value: now) } else { none } })
modules/watch.luna:69:3: LUNA4010: execution failed: the watch follows "selected" and its body writes "selected", so the watch would close a cycle
  69 |   watch.rows := luna.set(index: watch.rows, key: id,
     |   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

This is the recursion refusal again, one level up. Calls form an acyclic graph, so every program ends. Watches form an acyclic graph too, so every change settles. Feedback over time is still possible: the second form of watch.attach takes every: and follows the clock, and time is no edge. In this console the clock is the prompt return: there is no timer thread, so a cadence is delivered at the first submission after its deadline, once, however many periods passed. The third form takes settle: and waits until its inputs have stopped moving for that long.

20

Features working together

So far the pieces came one at a time. Here they work together: a parallel loop does the expensive work, its answers land in a live list, and a watch prints the newest answer every time the list grows.

The list is the model, and the watch is the only thing that prints. It follows the list and reads its last element, from the end.

luna — the session
live results: list<int> = []
watch.attach(to: results, body: fn (rs: list<int> | missing) { if rs is list<int> now { if luna.at(value: now, index: -1) is int last { io.print(value: last) } else { none } } else { none } })
watch.id(value: 2)

The expensive part is a parallel fold over twenty thousand calls of fn.heavy from the controls chapter. Its blocks run on the engine’s threads and merge in order.

luna — the session
fn batch(start: int) -> int { luna.fold(domain: luna.range(start: start, stop: start + 20000), seed: 0, step: fn (s: int, x: int) => s + fn.heavy(x: x), merge: fn (a: int, b: int) => a + b) }
functions: fn.batch

The obvious shortcut is to append from inside the parallel loop. A worker never writes the world, and a live list is world, so the compiler refuses it.

luna — the session
luna.map(step: fn (x: int) { results := luna.append(list: results, value: x); x }, domain: [1, 2, 3], parallel: true)
terminal 292:1:1: LUNA3001: compiling failed: language control "luna.map" declares "parallel: true", but live_write "results" is a world step; a worker performs no world step — no effect and no wait — and enters only natives their own host declared reentrant
  1 | luna.map(step: fn (x: int) { results := luna.append(list: results, value: x); x }, domain: [1, 2, 3], parallel: true)
    | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

A spawned task is a worker too. A function that writes the list cannot be spawned.

luna — the session
fn record(computed: int) -> none { results := luna.append(list: results, value: computed) }
functions: fn.record
let attempt = luna.spawn(call: fn.record(computed: 1))
terminal 294:1:32: LUNA3001: compiling failed: spawned callee "fn.record" is not admissible on a worker: a spawned graph must perform no world step — no effect and no wait — and every installed native it reaches must declare itself reentrant
  1 | let attempt = luna.spawn(call: fn.record(computed: 1))
    |                                ^~~~~~~~~~~~~~~~~~~~~~

So the work splits the way the law says. The task computes; the cell that awaits it writes. The statement prints nothing itself — the line below it is the watch, printing the newest element at the prompt’s return.

luna — the session
let opening = luna.spawn(call: fn.batch(start: 0))
values: opening
if luna.await(value: opening) is int computed { results := luna.append(list: results, value: computed) } else { none };
10002327019

Three more batches start at once, each a task of its own. One serial pass awaits them in order and appends each answer.

luna — the session
let jobs = luna.map(domain: [1, 2, 3], step: fn (k: int) => luna.spawn(call: fn.batch(start: k * 20000)))
values: jobs
luna.each(domain: jobs, step: fn (job: future<int>) { if luna.await(value: job) is int computed { results := luna.append(list: results, value: computed) } else { none } });
9997598083

Three appends, one line printed. Moves between two deliveries are one delivery, so the watch saw the list once, with all three answers in it, and printed the last. The list holds everything:

luna — the session
results
[10002327019, 9995750692, 10005174413, 9997598083]

21

The compiler is the interpreter

Luna’s compiler is not a step that runs before the program. It lives in the running session — it is the interpreter. So the two times meet: a value the session already holds can feed the next compile, and what the compiler computes can be held at run time. This is how a program computes its own shapes — a form from a function’s parameters, a record from a dataset’s columns. Types have shown up as arguments, T: type and T: int | none. I try to hold one as a value.

luna — the session
int
terminal 300:1:1: LUNA3025: compiling failed: a value of type exists only while compiling; the cell's presented value needs a value at run time, so the type must be consumed by a compile-time position or an instantiation, or crossed as data with compiler.value(of:), whose answer is a type.value
  1 | int
    | ^~~

A type exists only while compiling. The refusal names the crossing: compiler.value(of:) turns it into data a run can hold.

luna — the session
compiler.value(of: [int, str, float])
[compiler.value(of: int), compiler.value(of: str), compiler.value(of: float)]

Inside a function, a list of types is an ordinary list, and the ordinary controls run over it. Since types exist only while compiling, this filter runs in the compiler, and the run sees only its answer.

luna — the session
fn numeric_columns() -> int { luna.length(value: luna.filter(domain: [int, str, float, bool], keep: fn (t) => t == int || t == float)) }
functions: fn.numeric_columns
fn.numeric_columns()
2

A function’s parameters are a list of fields, each with a name, a type, and a default. fn.area from earlier:

luna — the session
compiler.value(of: luna.parameters(of: fn.area))
[compiler.value(of: field(name: "width", of: float)), compiler.value(of: field(name: "height", of: float, default: 1.0))]

That list can declare another function’s parameters. ..size spreads fn.area’s fields into scaled, defaults included, and passes them on.

luna — the session
fn scaled(factor: float, ..size: luna.parameters(of: fn.area)) -> float { fn.area(..size) * factor }
functions: fn.scaled
fn.scaled(factor: 2.0, width: 3.0)
6.0
luna.signature(of: fn.scaled)
fn.scaled(factor: float, width: float, height: float = 1.0) -> float

That is enough to build something real: a timing wrapper for any function. timed takes a published function and spreads its parameters into a new one. The new one takes a mark, calls through with the same arguments, records the seconds in a live entry, and answers the original answer. The compiler read the wrapped function’s shape and minted its instrumented twin.

luna — the session
live last_elapsed: float = 0.0
fn timed(work: fn) {
  fn (..inputs: luna.parameters(of: work)) {
    let start = time.tick()
    let answer = work(..inputs)
    last_elapsed := time.tock(since: start).value
    answer
  }
}
functions: fn.timed

fn name = ... publishes the twin under a name of its own. Its signature is the original’s, default included, so a misspelled label is refused exactly as it would be for fn.area.

luna — the session
fn timed_area = fn.timed(work: fn.area)
functions: fn.timed_area, fn.timed<fn.area>
luna.signature(of: fn.timed_area)
fn.timed_area(width: float, height: float = 1.0) -> float
fn.timed_area(width: 3.0)
3.0
fn.timed_area(widht: 3.0)
terminal 313:1:15: LUNA3007: compiling failed: linked callable "fn.timed_area" has no input named "widht"; its inputs are width, height
  1 | fn.timed_area(widht: 3.0)
    |               ^~~~~~~~~~

The same metafunction wraps fn.heavy from the controls chapter. The answer is the original’s, and the seconds stand in last_elapsed; they depend on the machine, so I check them instead of printing them.

luna — the session
fn timed_heavy = fn.timed(work: fn.heavy)
functions: fn.timed_heavy, fn.timed<fn.heavy>
fn.timed_heavy(x: 5) == fn.heavy(x: 5)
true
last_elapsed > 0.0
true

A condition on a type is decided while compiling, so each instantiation keeps only its own arm.

luna — the session
fn describe(T: type) -> str { if T == int { "an integer" } else if T == float { "a float" } else { "something else" } }
functions: fn.describe
fn.describe(T: int)
an integer
fn.describe(T: str)
something else

type is one of four kinds. An integer, a symbol, and a boolean are values the compiler knows, and a parameter of one makes its function a metafunction just as type does.

luna — the session
fn repeat(n: integer, x: int) -> list<int> { luna.map(domain: luna.range(start: 0, stop: n), step: fn (i: int) => x) }
functions: fn.repeat
fn.repeat(n: 3, x: 7)
[7, 7, 7]

A committed value is known to the next cell’s compile, so it can stand in a kind seat. A value known only at run time cannot.

luna — the session
let copies = 4
values: copies
fn.repeat(n: copies, x: 1)
[1, 1, 1, 1]
fn later(n: int) -> list<int> { fn.repeat(n: n, x: 1) }
terminal 324:1:46: LUNA3013: compiling failed: argument "n" of metafunction "fn.repeat" is a compile-time value, and "n" is known only at run time
  1 | fn later(n: int) -> list<int> { fn.repeat(n: n, x: 1) }
    |                                              ^

A symbol names a field. A string is not one.

luna — the session
fn pick(name: symbol, of: {x: int, y: int}) -> int { compiler.member(of: of, name: name) }
functions: fn.pick
fn.pick(name: "y", of: {x: 1, y: 2})
terminal 326:1:15: LUNA3013: compiling failed: argument "name" of metafunction "fn.pick" requires symbol, and it is handed str
  1 | fn.pick(name: "y", of: {x: 1, y: 2})
    |               ^~~
fn.pick(name: symbol(value: "y"), of: {x: 1, y: 2})
2
fn.pick(name: symbol(value: "z"), of: {x: 1, y: 2})
terminal 328:1:1: LUNA3014: compiling failed: metafunction "fn.pick" does not compile for these kind arguments: exact type conflict: return value projection; named product has no field "z"
  1 | fn.pick(name: symbol(value: "z"), of: {x: 1, y: 2})
    | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

A kind pack, Ts: ..type, is a product of kinds: one seat that takes as many arguments as the call hands it, each with its own type. Here a function is applied to whatever arguments arrive:

luna — the session
fn applied(Ts: ..type, ..xs: Ts, f: (..Ts) -> int) -> int { f(..xs) }
functions: fn.applied
fn.applied(3, 4, f: fn (a: int, b: int) => a * 10 + b)
34

Now a record type built from a list of names. The function maps names to fields, makes the product type from them, and writes a value of it, all while compiling.

luna — the session
fn zeros(names: list<symbol>) -> str {
  let fields = luna.map(domain: names, step: fn (n) => field(name: n, of: float))
  alias row = luna.product(of: fields)
  let r: row = compiler.record(domain: fields, step: fn (f) => 0.0)
  str(value: r)
}
functions: fn.zeros
fn.zeros(names: [symbol(value: "width"), symbol(value: "depth")])
{depth: 0.0, width: 0.0}

The names can come from the session itself. luna.env() answers at run time, so the compiler cannot read it directly.

luna — the session
fn.zeros(names: luna.map(domain: luna.filter(domain: luna.env(), keep: fn (e) => e.type_name == "float"), step: fn (e) => symbol(value: e.name)))
terminal 333:1:17: LUNA3026: compiling failed: the call to "luna.env" cannot run while compiling, and "luna.map(domain: luna.filter(domain: luna.env(), keep: fn (e) => e.type_name == "float"), step: fn (e) => symbol(value: e.name))" is a compile-time value; its cone reaches the call to "luna.env" at 1:54
  1 | fn.zeros(names: luna.map(domain: luna.filter(domain: luna.env(), keep: fn (e) => e.type_name == "float"), step: fn (e) => symbol(value: e.name)))
    |                 ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
terminal 333:1:54: note: the cone reaches this

Committing the answer first is enough. The next cell’s compile reads the committed list.

luna — the session
let float_names = luna.map(domain: luna.filter(domain: luna.env(), keep: fn (e) => e.type_name == "float"), step: fn (e) => e.name)
values: float_names
float_names
["last_elapsed", "rate", "zoom"]
fn.zeros(names: luna.map(domain: float_names, step: fn (c) => symbol(value: c)))
{last_elapsed: 0.0, rate: 0.0, zoom: 0.0}

Every instantiation along the way was compiled once and published.

luna — the session
luna.filter(domain: luna.env(), keep: fn (e) => e.kind == "function" && luna.starts_with(value: e.name, prefix: "fn.repeat") || luna.starts_with(value: e.name, prefix: "fn.zeros"))
[{kind: "function", name: "fn.repeat<3>", type_name: "(int) -> list<int>"}, {kind: "function", name: "fn.repeat<4>", type_name: "(int) -> list<int>"}, {kind: "metafunction", name: "fn.zeros", type_name: ""}, {kind: "function", name: "fn.zeros<[symbol(value: \"last_elapsed\"), symbol(value: \"rate\"), symbol(value: \"zoom\")]>", type_name: "() -> str"}, {kind: "function", name: "fn.zeros<[symbol(value: \"width\"), symbol(value: \"depth\")]>", type_name: "() -> str"}]

22

The rest is in the language

Each topic the help page named is a call, and each reads as one page. luna.cells() explains what a cell stages and what it performs.

luna — the session
luna.cells()
luna.cells() -> str [compile-time]
A session is ordered cells; each cell compiles once against prior committed publications.

`---` separates a cell stream. A successful cell publishes its top-level bindings and declarations;
later cells use a function as `fn.name`. A named function reads its parameters and what it
captured: a session value its body names is taken when its cell runs and sealed into the published
function, exactly as `fn name = fn (...) => ...` seals it, so a later rebind or drop of that name
leaves `fn.name` as it was. A value the function should read at each call is a parameter.
… trimmed
The environment has a second tier. `live name: type = expression` declares one entry of it, written
where it stands with `name := expression` and read as a snapshot of the instant it was asked in. A
live entry has no history — a write mints no revision, no receipt and no archive row — so it is what
a pointer, a slider or a frame writes, and `luna.env()` reports it as `live value`. The committed
tier TAKES the name: a later `let` or `env.keep` under that name drops the live entry beneath it and
the name is an ordinary binding from then on. The declaration is top-level only (`LUNA1138`); a
`:=` write and a read of the name stand anywhere, a function body included.
… trimmed

luna.diagnostics() lists every code the language can refuse with, grouped by the phase that raises it. The codes I met today are all there.

luna — the session
luna.diagnostics()
luna.diagnostics() -> str [compile-time]
Every `LUNA####` code Luna can refuse with, with its one-line meaning, grouped by the phase that raises it: the three that refuse a program before it runs, and the fault tier a running program raises — a `4xxx` code is a fault, and it names its law beside the exact span that broke it.
… trimmed
2xxx — lowering: syntax to one total SSA body
… trimmed
LUNA2003 — a name no binding, function, or declaration in scope denotes, including a session value unavailable to a named function
… trimmed
LUNA2020 — a body answers its tail, and return only leaves early: a return in tail position is dropped
… trimmed
3xxx — compiler: typing, linking, and emission
… trimmed
LUNA3001 — the program types, but the law it needs to run does not exist: a missing copy or equality law, a shape bytecode cannot encode, a callback used as a value, a worker graph reaching a world step (including commit), or a provider that is not linked
… trimmed
LUNA3003 — two exact types the program requires to be one are not
… trimmed
LUNA3007 — a call names no construct, installed callable, or published function, spells a construct where it has no value, composes or parallelizes a settled domain, or combines incompatible reflection queries or result depths
LUNA3008 — the invocation graph is acyclic, so a callable cannot reach itself
… trimmed
LUNA3013 — a compile-time argument is not spelled as its position requires: a published name, an exact type annotation, a string literal, or a boolean literal; a metafunction type argument is missing, ambiguous, or conflicts with the call's solved value types
… trimmed
4xxx — execution: the fault tier, raised while a program runs
… trimmed
LUNA4006 — a checked integer operation left the signed 64-bit domain
… trimmed
LUNA4008 — an indexed access named a position the sequence it reads does not hold
… trimmed
LUNA4010 — a construct's own domain law refused the values it was given; the message names the law
… trimmed
LUNA4014 — an author's luna.fault stated that the program's own claim failed; the message is the author's own sentence
… trimmed

The other topics are luna.syntax(), luna.types(), luna.operators(), luna.controls(), luna.ownership(), luna.effects(), luna.parallelism() and luna.patterns(). I did not read a manual to get here. Every answer on this page came from the session.

LUNA

The language is its own reference.

Everything on this page came from asking the session. See what the compiler proves, how the runtime keeps it live — and the application built on it.