Polydera

LEARN THE LANGUAGE

Luna tutorial.

Sit down at the console and ask.

This page is one Luna session, from the first prompt to the last. Someone who has never seen the language asks it for help, tries a cell, reads the refusal, fixes it, and moves on. By the end, every part of the language has come up. Every cell was run on the Luna client and every answer is the one it printed.

256 cells20 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. 06Lists, ranges, and safe reads
  7. 07Controls instead of loops
  8. 08No recursion, by construction
  9. 09Records, names, and enums
  10. 10Unions, absence, and two kinds of failure
  11. 11Arrays, with a window per axis
  12. 12Jagged blocks
  13. 13Metafunctions and kinds
  14. 14Modules and imports
  15. 15The environment, and its live tier
  16. 16Standing computations, and an acyclic graph
  17. 17Touching the world
  18. 18Parallel, with identical values
  19. 19Programs at two times
  20. 20The 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, which opens a session, and the banner tells me where to begin.

luna — the session

luna

Luna 0.1.0 — luna.help() to get started
mounted: luna.cli, luna.pose, 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
   exact producers and consumers of a type.
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 and then publishes its names. The language answers questions about itself with ordinary calls. And there is a list of topics to read when I need them. The first one it names 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 captures
nothing from the session, so pass session values as parameters; `LUNA2003` says so by name.
… trimmed

I will start by typing.

02

First cells

A cell prints its value.

luna — the session
1 + 2
3

A let publishes a name instead of printing a value. 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
  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, and its notes say why: answer is an int and 0.5 is a float. Luna never converts a number on its own. A conversion that changes a value is a construction I write.

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.

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

Some numbers have names. A constant is a value, not a function, so 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
… 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; luna.concat(a:, b:) joins two texts
  1 | "moon" + "light"
    | ^~~~~~~~~~~~~~~~
terminal 14:1:1: note: a str literal
luna.concat(a: "moon", b: "light")
moonlight

Parsing text is a question that 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
    | ^~~~~~~~~~~~~~~~~~~~~~~

A refusal (LUNA3003) happened before anything ran. The fault (LUNA4006) happened while running. I will meet that distinction again.

04

Ask the language

Before guessing any more names, I ask what exists.

luna — the session
luna.catalog()
module luna — the language; luna.help() to get started [318 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.pose — native inversion of row-major affine poses [1 callables, 0 types] (rows spelled pose.*)
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, 5 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. 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
  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 }

I try to write return at the end of a body, as I would elsewhere.

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 try to use a session value inside a function.

luna — the session
let rate = 1.5
values: rate
fn priced(units: float) -> float { units * rate }
terminal 37:1:44: LUNA2003: compiling failed: unknown value "rate"; named functions cannot capture session values; pass "rate" as a parameter
  1 | fn priced(units: float) -> float { units * rate }
    |                                            ^~~~

A named function captures nothing from the session, and the refusal says what to do.

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

A lambda bound with let is different: it captures what it reads, as the value stood when the lambda was made. Rebinding rate later does not reach into it.

luna — the session
let price = fn (units: float) => units * rate
values: price
price(units: 4.0)
6.0
let rate = 3.0
values: rate
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

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 54:1:13: LUNA3007: compiling failed: language control "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 58: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 59: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 60: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)

07

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. 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. I guess that the step takes one pair.

luna — the session
luna.zip(a: [1, 2, 3], b: ["a", "b", "c"])
[(1, "a"), (2, "b"), (3, "c")]
luna.map(step: fn (p: (int, str)) => p.1, domain: luna.zip(a: [1, 2, 3], b: ["a", "b", "c"]))
terminal 77:1:1: LUNA3007: compiling failed: serial map callback target must have exactly 2 parameters over 2 composed domains
  1 | luna.map(step: fn (p: (int, str)) => p.1, domain: luna.zip(a: [1, 2, 3], b: ["a", "b", "c"]))
    | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
terminal 77:1:10: LUNA3003: compiling failed: exact type conflict: argument "step" must conform to language control "luna.map"; function arity 1 is outside the admitted range [2, 2]
  1 | luna.map(step: fn (p: (int, str)) => p.1, domain: luna.zip(a: [1, 2, 3], b: ["a", "b", "c"]))
    |          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
terminal 77:1:38: note: function shape requires a function value
terminal 77:1:1: note: function shape requires a function value

Over a zipped domain the step 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"]))
["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 84:1:1: LUNA3003: compiling failed: exact type conflict: language control "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

08

No recursion, by construction

A countdown is the first thing I would write recursively.

luna — the session
fn countdown(n: int) -> int { if n == 0 { 0 } else { countdown(n: n - 1) } }
terminal 87: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 87: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 89: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 89:1:24: note: the active invocation chain reaches this callable again

09

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.

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 99:1:12: LUNA3003: compiling failed: exact type conflict: argument "d" must conform to linked callable "fn.doubled"; float is not assignable to geo.metres
  1 | fn.doubled(d: 2.5)
    |            ^~~~~~
terminal 99: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}

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 108: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)
    |            ^~~~~~~~~~~~~~~~

10

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

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 112: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 112:1:1: note: linked callable result type
terminal 112: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 119: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

That leaves faults. A refusal is a value the program handles. A fault ends the run because a claim was false. I can state my own claim 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 126: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") }
    |                 ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

11

Arrays, with a window per axis

An array is one dense buffer and the extents that read it. Its type states a window per axis.

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 view of the axes that remain, over the same buffer.

luna — the session
m[1, 2]
6.0
m[1]
array(values: [4.0, 5.0, 6.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.

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 with stated windows can 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 144: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])

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.

luna — the session
flat == flat
true

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])

list(array:) crosses back to a list.

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

12

Jagged blocks

A jagged holds blocks of different lengths in one flat array, bounded by offsets. 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])

luna.generate builds one: each step answers a list, and the lists become the blocks, in order.

luna — the session
luna.generate(domain: [1, 2, 3], step: fn (n: int) => luna.map(domain: luna.range(start: 0, stop: n), step: fn (i: int) => n))
luna.jagged(offsets: array(values: [0, 1, 3, 6]), data: array(values: [1, 2, 2, 3, 3, 3]))

13

Metafunctions and kinds

A parameter annotated type is known while compiling. A function with one is a metafunction: an instruction for making a 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(text: e.name, prefix: "fn.first"))
terminal 159:1:66: LUNA3007: compiling failed: language control "luna.starts_with" has no input named "text"; its inputs are value, prefix
  1 | luna.filter(domain: luna.env(), keep: fn (e) => luna.starts_with(text: e.name, prefix: "fn.first"))
    |                                                                  ^~~~~~~~~~~~

I guessed a label. The refusal lists the real ones.

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 165: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 168: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)
    | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

14

Modules and imports

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

luna — the session
units.degrees(value: 90.0)
terminal 169:1:1: LUNA3007: compiling failed: no installed callable named "units.degrees" exists in this compilation snapshot; no module named "units" is loaded here — submit "import units" as a cell of its own
  1 | units.degrees(value: 90.0)
    | ^~~~~~~~~~~~~~~~~~~~~~~~~~
import units
types: units.degrees, units.radians, units.seconds, units.milliseconds
functions: units.to_radians, units.to_degrees, units.to_seconds, units.to_milliseconds
module: units (4 types, 4 functions; generation 143)
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.to_degrees(of: units.radians) -> units.degrees [library, cost 4]
    Convert a radian angle to degrees.
  units.to_milliseconds(of: units.seconds) -> units.milliseconds [library, cost 2]
    Convert a second duration to milliseconds.
  units.to_radians(of: units.degrees) -> units.radians [library, cost 4]
    Convert a degree angle to radians.
  units.to_seconds(of: units.milliseconds) -> units.seconds [library, cost 2]
    Convert a millisecond duration to seconds.
units.to_radians(of: units.degrees(value: 90.0))
units.radians(value: 1.5707963267948966)

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

luna — the session
luna.source(of: units.to_radians)
fn to_radians(of: units.degrees) -> units.radians {
  units.radians(value: of.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)
}
luna — the session
import shapes
types: shapes.circle
functions: shapes.area, shapes.arc_angle
module: shapes (1 types, 2 functions; generation 147)
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.to_degrees(of: shapes.arc_angle(of: {radius: 1.0}, length: luna.pi))
units.degrees(value: 180.0)

15

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: "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: "m", type_name: "array<float>[2, 3]"}, {kind: "value", name: "more", type_name: "list<int>[9]"}, {kind: "value", name: "pair", type_name: "(int, str)"}, {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: "slots", type_name: "list<int | none>[3]"}, {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 per line. Refused cells are not there, because 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.

luna — the session
:history
… trimmed
132: functions=fn.first
133: functions=fn.first<int> presentation
134: functions=fn.first<str> presentation
135: functions=fn.first<int | none> presentation
136: presentation
137: types=demo.number
138: functions=fn.largest
139: functions=fn.largest<int> presentation
140: functions=fn.largest<float> presentation
141: functions=fn.total
142: functions=fn.total<float> presentation
143: types=units.degrees, units.radians, units.seconds, units.milliseconds functions=units.to_radians, units.to_degrees, units.to_seconds, units.to_milliseconds module=units types=4 functions=4
… trimmed

16

Standing computations, and an acyclic graph

A live entry changes in the instant. Something that should follow it, like a derived value, is a standing computation: a body that names its inputs and runs again when one moves. The shipped watch module stands them.

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 165)
module: watch (1 types, 4 functions; generation 165)
live celsius: float = 20.0
live fahrenheit: float = 0.0

The body receives each input’s newest value, or missing where the entry stands nowhere. My first body forgets the second case.

luna — the session
watch.attach(to: celsius, body: fn (c: float | missing) { if c is float t { fahrenheit := t * 9.0 / 5.0 + 32.0 } }, initialize: true)
terminal 195:1:57: LUNA3014: compiling failed: this lambda escapes as a value, so it is a function of its own, and it does not compile as one: an if chain with no else must test every member of float | missing: "missing" is unhandled
  1 | watch.attach(to: celsius, body: fn (c: float | missing) { if c is float t { fahrenheit := t * 9.0 / 5.0 + 32.0 } }, initialize: true)
    |                                                         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
watch.attach(to: celsius, body: fn (c: float | missing) { if c is float t { fahrenheit := t * 9.0 / 5.0 + 32.0 } else { none } }, initialize: true)
watch.id(value: 0)

The console runs due standing bodies each time a cell finishes, before the prompt returns. initialize: true asked for one delivery at once, so fahrenheit already follows.

luna — the session
fahrenheit
68.0
celsius := 100.0
fahrenheit
212.0

The watch is one row of a live table the watch module declares, and I can read it like any entry. 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: 0))
{body: <fn>, every: none, initialize: true, scope: none, settle: none}

Standing computations chain. A label that follows fahrenheit moves when celsius does, and the whole cascade finishes before the prompt returns.

luna — the session
live label: str = ""
watch.attach(to: fahrenheit, body: fn (f: float | missing) { if f is float t { label := luna.concat(a: str(value: t), b: " F") } else { none } }, initialize: true)
watch.id(value: 1)
celsius := -40.0
label
-40.0 F

What keeps a cascade finite is the rule I meet next. I add the reverse conversion, so that fahrenheit would feed celsius.

luna — the session
watch.attach(to: fahrenheit, body: fn (f: float | missing) { if f is float t { celsius := (t - 32.0) * 5.0 / 9.0 } else { none } })
modules/watch.luna:60:3: LUNA4010: execution failed: the watch follows "fahrenheit" and its body writes "celsius", which a standing watch follows and writes "fahrenheit", so the watch would close a cycle
  60 |   watch.rows := luna.set(index: watch.rows, key: id,
     |   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Refused, and the report names the path. The first watch follows celsius and writes fahrenheit, so a second watch that follows fahrenheit and writes celsius would close a loop. A body that writes what it follows is the same loop with one member.

luna — the session
watch.attach(to: celsius, body: fn (c: float | missing) { if c is float t { celsius := t + 1.0 } else { none } })
modules/watch.luna:60:3: LUNA4010: execution failed: the watch follows "celsius" and its body writes "celsius", so the watch would close a cycle
  60 |   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. Standing computations form an acyclic graph too, so a change ripples through them and stops. Feedback over time is still possible: a watch with every: follows the clock, and time is no edge.

luna — the session
luna.signature(of: watch.attach)
watch.attach(Ts: ..type, ..binding to: Ts, body: (..Ts | missing) -> none, initialize: 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) -> watch.id
watch.attach(Ts: ..type, ..binding to: Ts, body: (..Ts | missing) -> none, settle: units.seconds | units.milliseconds) -> watch.id

The second form takes every: and hands the body the elapsed time as its last argument. 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. An idle session runs nothing. The third form takes settle: and waits until its inputs have stopped moving for that long.

17

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
… trimmed
  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

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

18

Parallel, with identical values

Parallelism is something I write: parallel: true on a control. The values are identical either way. Only time changes.

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.

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

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 219: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)
    | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

A single call can also run on its own: luna.spawn starts it and answers a future, and luna.await answers its value or an error.

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

19

Programs at two times

Types have shown up as arguments: T: type, T: int | none. I try to hold one as a value.

luna — the session
int
terminal 223: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

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 238: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 240: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 242: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.

luna — the session
fn tupled(Ts: ..type, ..xs: Ts) -> (..Ts) { xs }
functions: fn.tupled
fn.tupled(1, "a", 2.5)
(1, "a", 2.5)
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 249: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 249: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
["celsius", "fahrenheit", "rate"]
fn.zeros(names: luna.map(domain: float_names, step: fn (c) => symbol(value: c)))
{celsius: 0.0, fahrenheit: 0.0, rate: 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: \"celsius\"), symbol(value: \"fahrenheit\"), symbol(value: \"rate\")]>", type_name: "() -> str"}, {kind: "function", name: "fn.zeros<[symbol(value: \"width\"), symbol(value: \"depth\")]>", type_name: "() -> str"}]

20

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`. Named functions do not capture session bindings, so pass
session values as parameters.
… 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 with it, and how the runtime keeps it live.