LLuce

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

sort.luc
# 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)
Output
-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.

mathx.luc
# 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)))
stats.luc
# 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)))
Output
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.

calc.luc
# 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))
Output
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.

bf.luc
# 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")
Output
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.luc
# 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")
Output
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.

input.txt
the quick brown fox
jumps over the lazy dog
the fox and the dog and the fox
wordcount.luc
# 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 + 1
Output
9 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.