LLuce

Errors

An error is a failure a correct program can meet anyway, because the world decided. A -> T! says a call may raise one; try passes it up and catch handles it here.

main.luc
func parse_port(text: String) -> Int!:
    let n = parse_int(text) else error(f"not a number: {text}")
    if n < 1 or n > 65535:
        error(f"port out of range: {n}")
    return n

func main() -> !:
    print(str(try parse_port("8080")))
    print(str(parse_port("nope") catch -1))
    print(str(parse_port("99999") catch -1))
Output
8080
-1
-1

try propagates#

try releases what this frame owns and leaves — exactly what return does, with one terminator changed. It needs a caller that said !.

main.luc
func inner(n: Int) -> Int!:
    if n == 0:
        error("inner refuses zero")
    return 100 / n

func middle(n: Int) -> Int!:
    return try inner(n) + 1

func outer(n: Int) -> Int!:
    return try middle(n) * 2

func main() -> !:
    print(str(try outer(5)))
    print(str(outer(0) catch -1))
Output
42
-1

Forgetting to say which you meant is a compile error. There is no spelling that ignores the outcome.

main.luc
func risky() -> Int!:
    error("no")

func main():
    risky()
luce check — the program is refused
luce: compile failed
main.luc:5:5: risky can fail: write 'try risky(…)' to pass the error on, or 'risky(…) catch …' to handle it [luce.sema.fallible]
        risky()
        ^~~~~~~

catch has two forms#

catch EXPR supplies a value. catch: opens a handler block, and it guards exactly one call — there is never a question about which statement failed.

main.luc
import std.files

func main() -> !:
    let text = files.read("absent.txt") catch "(default contents)"
    print(text)

    files.write("/nowhere/x", "data") catch:
        print("the write did not land")

    var greeting = "unset"
    greeting = files.read("absent.txt") catch:
        greeting = "(new file)"
    print(greeting)
Output
(default contents)
the write did not land
(new file)

An uncaught error#

Out of main() -> ! it ends the run, and loom prints the words and the one place it was raised. One line, not a stack: a trap is a bug and the stack is its diagnosis, but an error is news and where it came from is the news.

main.luc
func check(value: Int) -> Int!:
    if value > 100:
        error(f"{value} is too large")
    return value

func main() -> !:
    print(str(try check(50)))
    print(str(try check(500)))
Output — the error reaches the top
50
loom: error: 500 is too large [user_error]
    raised in check (main.luc:3:9)

The worked example#

programs/calc.luc in the repository is a recursive-descent calculator. Every way it can be defeated is a way the user defeated it, 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.

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

Give it something it cannot parse and the same program says so, at the position it gave up:

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 — the error reaches the top
loom: error: expected a number at position 4 [user_error]
    raised in Scan.number (calc.luc:36:13)