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
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.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.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.
1 + 23
A let publishes a name instead of printing a value. 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 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.
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.
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.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 … 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; 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.
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 | ^~~~~~~~~~~~~~~~~~~~~~~
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.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.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. 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 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 }
I try to write return at the end of a body, as I would elsewhere.
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 try to use a session value inside a function.
let rate = 1.5values: 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.
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.
let price = fn (units: float) => units * ratevalues: price
price(units: 4.0)6.0
let rate = 3.0values: rate
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
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 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.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 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.
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.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)
07
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. 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. I guess that the step takes one pair.
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.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.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 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).namemelon
08
No recursion, by construction
A countdown is the first thing I would write recursively.
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.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 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.
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.
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 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.
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}
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 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}.
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 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.
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 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.
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
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.
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.
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.
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.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.
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.
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.
let flat = array(values: [1, 2, 3, 4, 5, 6])values: flat
flat > 3array(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.
flat == flattrue
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])
list(array:) crosses back to a list.
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:
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.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.
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(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.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 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.
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.
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 unitstypes: 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.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:
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)
}
import shapestypes: 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.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.
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 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.
: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.
import watchtypes: 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.0live fahrenheit: float = 0.0The body receives each input’s newest value, or missing where the entry stands nowhere. My first body forgets the second case.
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.
fahrenheit68.0
celsius := 100.0fahrenheit212.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.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.
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.0label-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.
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.
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.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.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.
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.
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.
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.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.
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.
intterminal 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.
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
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 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.
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.
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.
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 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.
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.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.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.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.