Goose

04_errors.goose

// Error handling. Goose has no exceptions and no Result type; it has three
// idioms, and this program uses each where it fits: a trailing bool (or
// code) for a two-outcome call, an optional for "found or not", and
// `return ... from` for a failure deep inside nested calls that should land
// in one place. Plus the ones that end the program: assert and abort.
//
// Build and run (from this directory):
//   goose -o errors.c 04_errors.goose && cl errors.c && errors

import std;

struct User { name: u8[..16], age: i32 }

// 1. A trailing bool: the caller writes `let v, ok = ...` and tests ok.
fn parse_age(s: u8[:]) -> i32, bool {
    let v, ok = parse_int(trim(s));
    if !ok || v < 0 || v > 150 { return 0, false; }
    return v as i32, true;
}

// 2. An optional: a reference to the element, or null. The caller must
//    narrow it with if/guard before touching it.
fn find_user(users: User[:], name: u8[:]) -> User? {
    for &u in users { if u.name == name { return u; } }
    return null;
}

// 3. return-from: any failure inside the nested parsing below returns from
//    `load` directly, unwinding parse_record and parse_field with it. Those
//    two return plain values and carry no error type, and the happy path
//    pays nothing for the possibility. The error is the last result, by
//    convention; "" means none.
fn load(text: u8[:]) -> User[>..], u8[] {
    var users: User[>..] = [];
    var lineno = 0;
    each_split(text, '\n') {
        lineno++;
        if it.len > 0 { users.push(parse_record(it, lineno)); }   // built straight into the element
    };
    return users, "";
}

fn parse_record(line: u8[:], lineno: i64) -> User {
    let comma = find(line, ",");
    guard comma >= 0 else { return [], str("line ", lineno, ": expected name,age") from load; }
    let name = parse_field(line[..comma], lineno);
    let age, ok = parse_age(line[comma + 1..]);
    guard ok else { return [], str("line ", lineno, ": bad age '", trim(line[comma + 1..]), "'") from load; }
    return User { name: name, age: age };
}

fn parse_field(s: u8[:], lineno: i64) -> u8[..16] {
    let t = trim(s);
    guard t.len > 0 else { return [], str("line ", lineno, ": empty name") from load; }
    guard t.len <= 16 else { return [], str("line ", lineno, ": name too long") from load; }
    return t;
}

fn report(text: u8[:]) {
    let users, err = load(text);
    if err.len > 0 { print("error: ", err); return; }
    print("loaded ", users.len, " users");
    for u in users { print("  ", u.name, " ", u.age); }
}

fn main() {
    // A two-outcome call, tested inline.
    for s in ["42", " 7 ", "-3", "abc", "200"] {
        let age, ok = parse_age(s);
        if ok { print("'", s, "' -> ", age); } else { print("'", s, "' is not an age"); }
    }

    // An optional, narrowed by `if` and by `guard`.
    var users: User[>..] = [];
    users.push(User { name: "ada", age: 36 });
    users.push(User { name: "linus", age: 20 });
    let found = find_user(users, "linus");
    if found { print("found ", found.name, ", age ", found.age); }
    let missing = find_user(users, "nobody");
    if missing == null { print("nobody is not a user"); }
    block {
        let u = find_user(users, "ada");
        guard u else { print("no ada"); break; }
        u.age += 1;                      // narrowed: a plain reference here
        print("ada is now ", u.age);
    }

    // Deep failures landing in one place.
    report("ada, 36\nlinus, 20\n");
    report("ada, 36\nlinus\n");
    report("ada, 36\n, 20\n");
    report("ada, x\n");
    report("averyveryverylongname, 1\n");

    // The ones that stop the program: assert for invariants, abort for a
    // fatal condition with a message, exit for a deliberate end with a code.
    // None of them fire here.
    assert(users.len == 2);
    if users.len > 100 { abort("too many users"); }
    if users.len == 0 { exit(1); }
    print("done");
}