The bundled programs
programs/ in the repository is Luce's userland. These pages show the real files, compiled and run from their own source when this site is built — so if one of them changes, this page changes with it, or the build stops.
sort#
Sorting, searching and joining a real List(Int).
# Sorting, searching, and joining a real List(Int).
#
# loom luce programs/sort.luc
import std.strings
func main():
var values = [42, 7, -3, 99, 0, 13, -40, 8, 77, 1]
values.sort()
var pieces: List(String) = []
for value in values:
pieces.append(str(value))
print(pieces.join(" "))
assert(values.find(13) == 6)
assert(values.contains(99))
assert(values[0] == -40)
assert(values[9] == 99)-40 -3 0 1 7 8 13 42 77 99
stats — two files#
stats.luc imports mathx.luc as a sibling module. Both compile into one program and one .lc.
# A tiny math module: imported by stats.luc (`import mathx`).
func mean(values: List(Float)) -> Float:
var total = 0.0
for value in values:
total = total + value
return total / Float(len(values))
# A sorted copy; whoever calls owns it.
func sorted(values: List(Float)) -> List(Float):
var copied = values[0:]
copied.sort()
return copied
func median(values: List(Float)) -> Float:
var ordered = sorted(values)
let count = len(ordered)
var middle = ordered[count / 2]
if count % 2 == 0:
middle = (ordered[count / 2 - 1] + ordered[count / 2]) / 2.0
return middle
func deviation(values: List(Float)) -> Float:
let center = mean(values)
var spread = 0.0
for value in values:
spread = spread + (value - center) * (value - center)
return sqrt(spread / Float(len(values)))# Summary statistics over the arguments — a two-file program:
# mathx.luc is imported as a module and compiles into the same .lc.
#
# loom run stats.lc 3 1 4 1 5 9 2.6
import mathx
func main():
if arg_count() == 0:
print("usage: stats NUMBER [NUMBER ...]")
return
var values: List(Float) = []
for index in range(0, arg_count()):
let written = arg(index)
let number = parse_float(written)
if number == none:
print("stats: not a number: " + written)
return
values.append(number)
var ordered = mathx.sorted(values)
print("count " + str(len(values)))
print("lowest " + str(ordered[0]))
print("highest " + str(ordered[len(ordered) - 1]))
print("mean " + str(mathx.mean(values)))
print("median " + str(mathx.median(values)))
print("stddev " + str(mathx.deviation(values)))count 7 lowest 1 highest 9 mean 3.6571428571428575 median 3 stddev 2.567298270198316
calc — a recursive-descent parser#
Structs as parser state, recursion, checked integer arithmetic, and the worked example for errors: every way this parser can be defeated is a way the user defeated it, so it raises rather than traps.
# A recursive-descent expression calculator: structs as parser state,
# recursion, checked integer arithmetic — and the worked example for
# errors (docs/FAILURE.md).
#
# loom run calc.lc "2 + 3 * (10 - 4)" one expression, then exit
# loom run calc.lc a REPL: type, blank line to quit
#
# Every way this parser can be defeated is a way the *user* defeated
# it, not a way the program is wrong: a correct calculator given
# "2 + )" still has nothing to compute. So the parser says `-> Step!`
# and raises with `error(...)`; `try` carries the failure up through
# four frames of recursion without a single `if` written for it, and
# `main() -> !` hands what is left to loom, which prints the words and
# the line they were raised on.
#
# Compare what this file said before: `trap(...)`, four times, for
# conditions no caller could handle and every one of which ended the
# program with "trap:" in front of a message about the user's typing.
struct Step:
value: Int
at: Int
struct Scan:
func skip_spaces(text: String, at: Int) -> Int:
var here = at
while here < len(text) and text.byte_at(here) == 32:
here = here + 1
return here
func number(text: String, at: Int) -> Step!:
var stop = at
while stop < len(text) and text.byte_at(stop) >= 48 and text.byte_at(stop) <= 57:
stop = stop + 1
if stop == at:
error("expected a number at position " + str(at))
let digits = text[at:stop]
return Step(value = parse_int(digits) else error("not a number: " + digits), at = stop)
struct Parse:
# expression = term (("+" | "-") term)*
func expression(text: String, at: Int) -> Step!:
var left = try Parse.term(text, at)
var here = Scan.skip_spaces(text, left.at)
while here < len(text) and (text.byte_at(here) == 43 or text.byte_at(here) == 45):
let operator = text.byte_at(here)
let right = try Parse.term(text, here + 1)
if operator == 43:
left = Step(value = left.value + right.value, at = right.at)
else:
left = Step(value = left.value - right.value, at = right.at)
here = Scan.skip_spaces(text, left.at)
return left
# term = factor (("*" | "/" | "%") factor)*
func term(text: String, at: Int) -> Step!:
var left = try Parse.factor(text, at)
var here = Scan.skip_spaces(text, left.at)
while here < len(text) and (text.byte_at(here) == 42 or text.byte_at(here) == 47 or text.byte_at(here) == 37):
let operator = text.byte_at(here)
let right = try Parse.factor(text, here + 1)
if operator == 42:
left = Step(value = left.value * right.value, at = right.at)
elif operator == 47:
left = Step(value = left.value / right.value, at = right.at)
else:
left = Step(value = left.value % right.value, at = right.at)
here = Scan.skip_spaces(text, left.at)
return left
# factor = number | "(" expression ")" | "-" factor
func factor(text: String, at: Int) -> Step!:
let here = Scan.skip_spaces(text, at)
if here < len(text) and text.byte_at(here) == 45:
let inner = try Parse.factor(text, here + 1)
return Step(value = 0 - inner.value, at = inner.at)
if here < len(text) and text.byte_at(here) == 40:
let inner = try Parse.expression(text, here + 1)
let close = Scan.skip_spaces(text, inner.at)
if close >= len(text) or text.byte_at(close) != 41:
error("expected ) at position " + str(close))
return Step(value = inner.value, at = close + 1)
return try Scan.number(text, here)
# Evaluate one line and print what it came to. Fallible, so every
# `error(...)` four frames down arrives here whole.
func evaluate(text: String) -> !:
let result = try Parse.expression(text, 0)
let rest = Scan.skip_spaces(text, result.at)
if rest != len(text):
error("unexpected character at position " + str(rest))
print(text + " = " + str(result.value))
# The interactive loop. `read_line` answers `String?`, so end of input
# — a pipe running dry, Ctrl-D at a terminal — is `none` and ends the
# session; an empty line ends it too, because a person pressing return
# at a prompt has said "nothing more".
#
# A bad expression must not end the loop: `evaluate(...) catch:` says
# so on stderr and asks again, which is the whole difference between a
# calculator and a program that exits when you mistype. What it
# cannot yet do is repeat the reason — `catch` has no binding form, so
# the words the parser raised are discarded here and only the one-shot
# path below ever prints them.
func repl():
print("calc — an expression a line, blank line to quit")
var running = true
while running:
let typed = read_line("calc> ")
if typed == none or typed == "":
running = false
else:
evaluate(typed) catch:
print_error("cannot compute: " + typed)
func main() -> !:
if arg_count() == 0:
repl()
return
try evaluate(arg(0))2 + 3 * (10 - 4) = 20
bf — a Brainfuck interpreter#
The byte tape is a real Array, output accumulates in a Builder, and chr() maps cell values to text.
# A Brainfuck interpreter in Luce: the byte tape is a real Array,
# output accumulates in a Builder, and chr() maps cell values to text.
#
# loom luce programs/bf.luc
func matching_forward(code: String, pc: Int) -> Int:
var depth = 1
var at = pc
while depth > 0:
at = at + 1
if code.byte_at(at) == 91:
depth = depth + 1
elif code.byte_at(at) == 93:
depth = depth - 1
return at
func matching_backward(code: String, pc: Int) -> Int:
var depth = 1
var at = pc
while depth > 0:
at = at - 1
if code.byte_at(at) == 93:
depth = depth + 1
elif code.byte_at(at) == 91:
depth = depth - 1
return at
func interpret(code: String, cells: Int) -> String:
var tape = new Array(Int, cells)
var pointer = 0
var pc = 0
var out = new Builder()
while pc < len(code):
let op = code.byte_at(pc)
if op == 62:
pointer = pointer + 1
elif op == 60:
pointer = pointer - 1
elif op == 43:
tape[pointer] = (tape[pointer] + 1) % 256
elif op == 45:
tape[pointer] = (tape[pointer] + 255) % 256
elif op == 46:
out.append(chr(tape[pointer]))
elif op == 91:
if tape[pointer] == 0:
pc = matching_forward(code, pc)
elif op == 93:
if tape[pointer] != 0:
pc = matching_backward(code, pc)
pc = pc + 1
return str(out)
func main():
let hello = "++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++."
let out = interpret(hello, 30)
print(out)
assert(out == "Hello World!\n")Hello World!
dice — the standard library at work#
Deterministic randomness from a seed with no globals, a histogram, and a file written at the end. main() -> ! hands what the disk said to loom.
# dice — roll dice with the standard library.
#
# loom run dice.lc [SEED [COUNT]]
#
# Rolls COUNT six-sided dice (default 20) from SEED (default 2026),
# prints the rolls and a histogram, and writes the rolls to
# dice_rolls.txt — a small showcase of `import std.math` (deterministic
# randomness, no globals) and `import std.files`.
import std.math
import std.files
import std.strings
func bar(count: Int) -> String:
return "#".repeat(count)
func main() -> !:
var seed = 2026
var count = 20
if arg_count() > 0:
seed = parse_int(arg(0)) else seed
if arg_count() > 1:
count = parse_int(arg(1)) else count
var rng = math.seed(seed)
var rolls: List(String) = []
var histogram = new Array(Int, 7)
var total = 0
for i in range(0, count):
let roll = math.random_int(rng, 1, 7)
rolls.append(str(roll))
histogram[roll] += 1
total += roll
print(f"{count} rolls from seed {seed}: " + rolls.join(" "))
let mean = strings.format_float(Float(total) / Float(count), 2)
print(f"total {total}, mean {mean}")
for face in range(1, 7):
print(f"{face}: " + bar(histogram[face]))
try files.write_lines("dice_rolls.txt", rolls)
print("rolls written to dice_rolls.txt")12 rolls from seed 7: 3 1 3 2 2 4 1 2 3 6 3 1 total 31, mean 2.58 1: ### 2: ### 3: #### 4: # 5: 6: # rolls written to dice_rolls.txt
wordcount#
Map, List, Builder, file reading and arguments together.
the quick brown fox
jumps over the lazy dog
the fox and the dog and the fox# Word frequencies: Map, List, Builder, file reading, arguments.
#
# loom run wordcount.lc FILE [TOP_N]
import std.files
func is_word_byte(byte: Int) -> Bool:
if byte >= 97 and byte <= 122:
return true
if byte >= 65 and byte <= 90:
return true
if byte >= 48 and byte <= 57:
return true
return byte == 95 or byte >= 128
# Collect every word of `content` into the counts map.
func count_words(content: String, counts: Map(String, Int)):
var at = 0
while at < len(content):
if is_word_byte(content.byte_at(at)):
var stop = at + 1
while stop < len(content) and is_word_byte(content.byte_at(stop)):
stop = stop + 1
let word = content[at:stop]
if counts.has(word):
counts[word] = counts[word] + 1
else:
counts[word] = 1
at = stop
else:
at = at + 1
# The most frequent remaining word (first seen wins ties).
func heaviest(counts: Map(String, Int)) -> String:
var best = ""
var best_count = 0
for word in counts:
if counts[word] > best_count:
best = word
best_count = counts[word]
return best
func main() -> !:
if arg_count() == 0:
print("usage: wordcount FILE [TOP_N]")
return
let path = arg(0)
var top = 5
if arg_count() > 1:
top = parse_int(arg(1)) else top
# One read, and its answer is the whole story. The exists-then-
# read this used to write had a window between the two calls that
# nothing could close, and it could not tell "not there" from
# "would not open" either (docs/FAILURE.md).
var counts = new Map(String, Int)
count_words(try files.read(path), counts)
print(str(len(counts)) + " distinct words in " + path)
var shown = 0
while shown < top and len(counts) > 0:
let word = heaviest(counts)
var line = new Builder()
line.append(" ")
line.append(str(counts[word]))
line.append(" ")
line.append(word)
print(str(line))
counts.remove(word)
shown = shown + 19 distinct words in input.txt 5 the 3 fox 2 dog 2 and
The ones that need a terminal#
Two programs cannot be shown here, because they draw on a real screen.
programs/life.luc is Conway's Life on the terminal grid.
programs/editor.luc is the flagship: a full-screen editor with movement, editing, scrolling, line numbers, a status bar and per-line Luce syntax highlighting — 445 lines, written entirely in Luce. Its source ships embedded in the loom binary, so loom edit always works, and a test in the repository compiles the embedded copy so it can never rot.
build/loom edit notes.txt # Ctrl-S saves, Ctrl-Q quits
build/loom run programs/life.lc
The editor is also the honest example of what Luce still lacks: its key handling is seventeen string comparisons with no else, and its keyword tables are forty-six word == "…" comparisons — a hash set written as a truth table, because there are no sets and no tagged unions. The status page counts them.