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
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.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.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.
1 + 23
A let publishes a name instead. The console reports what the cell published.
let answer = 40 + 2values: answer
answer * 284
Ending a cell with ; makes it a statement: it runs, and nothing prints.
answer * 2;03
A literal’s spelling is its type
My first real mistake comes from habit.
answer + 0.5terminal 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.
float(value: answer) + 0.542.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.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.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.pi3.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.
"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.
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.
-7 / 2-3
-7 % 2-1
1.0 / 0.0inf
Integers never wrap. Overflow is a fault: the run ends with a report at the exact span.
9223372036854775807 + 1terminal 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.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.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.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.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.
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.
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.
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.
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.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.
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.
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.
let rate = 1.5values: 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.
let price = fn (units: float) => units * ratevalues: 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.
let rate = 3.0values: 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.
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.
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.
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.
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.
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.
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.
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.
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.length(value: xs)8
xs[2]4
A negative position counts from the end.
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.
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.
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.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.
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.range(start: 0, stop: 5)luna.range(0, 5, 1)
08
Controls instead of loops
I look for a loop.
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.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.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.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.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.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.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.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.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.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.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:.
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).namemelon
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:
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 == threadedtrue
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.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.
import timetypes: 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.0true
09
No recursion, by construction
A countdown is the first thing I would write with recursion.
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.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.
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.
let point = {x: 3.0, y: 4.0}values: point
point.y4.0
let pair = (1, "one")values: pair
pair.1one
An alias names a record shape without making a new type.
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.
type geo.metres = floattypes: 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.
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]).high7.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.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.
enum demo.side = left | righttypes: 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.
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.
fn.halved(x: 8) + 1terminal 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.
fn.halved(x: 8)? + 15
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.
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.
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.
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.
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.
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.
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.
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.
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.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.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.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.
m * 2.0array(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.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.
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.
let flat = array(values: [1, 2, 3, 4, 5, 6])values: flat
flat > 3array(values: [false, false, false, true, true, true])
A scalar can stand on either side, and arithmetic works the same way.
3.0 < marray(values: [[false, false, false], [true, true, true]])
3.0 - marray(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.
flat == flattrue
m == 0.0terminal 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.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.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.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.nonzero(value: m > 4.0)array(values: [4, 5])
list(array:) crosses back to a list.
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:
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.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.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.
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.
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.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.
concept demo.number = int | floattypes: 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.
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.
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.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.
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.
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.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:
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}
import shapestypes: 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.
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.
import colortypes: 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:
module turns
import units
// the fraction of a full turn an angle covers
fn of(angle: units.degrees) -> float {
angle.value / 360.0
}
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.
import turnsfunctions: 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.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.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:
width,depth
2.5,4.0
3.0,1.5
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.
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 ?.
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.
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.
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.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.
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.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.
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:
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:
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.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.
env.window(revisions: 3);let total = 10values: total
let total = 25values: total
total25
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.
live angle: int = 0angle := angle + 15angle15
A function can write a live entry, and the next line reads the new value.
fn turn(by: int) -> none { angle := angle + by }functions: fn.turn
fn.turn(by: 30);angle45
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.
: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.
import watchtypes: 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.
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.
live zoom: float = 1.0live status: str = ""live title: str = ""The status follows the selection. My first body forgets that the value can be missing.
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.
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)
title0 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.
selected := luna.append(list: selected, value: 4)title1 selected at 1.0
zoom := 2.0title1 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.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.
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
title2 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:
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.
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.
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.
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.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.
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.
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.
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:
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.
intterminal 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.
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.
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:
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.
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.
live last_elapsed: float = 0.0fn 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.
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.
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.0true
A condition on a type is decided while compiling, so each instantiation keeps only its own arm.
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.
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.
let copies = 4values: 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.
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:
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.
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.
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.
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.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.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.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.