Download

← Examples

mowitnow

Lawn mowers driven across a grid: enums, a trait, an exhaustive match, and an input file that @comptime reads during the build.

examples/mowitnow in the repository.

// MowItNow: drive a fleet of automatic lawn mowers across a rectangular grid.
//
// Showcases a broad slice of Vader's surface area:
//   - enums (`Direction`, `Command`) with dot-shorthand and exhaustive `match`
//   - user-defined trait (`Turnable`) implemented by an enum, with `Self` return
//   - trait impl on both struct and enum (`Mower`/`Direction` implements Display)
//   - tuple destructuring of a function result
//   - explicit error unions (`T | ParseError`) propagated through `?`
//   - a top-level `@comptime` block baking the canonical test input at compile time
//   - argv handling so the same binary accepts an external file at runtime
//   - UFCS chained calls (`s.trim().split_where(is_white_char)`, `m.dir.turn_left()`)

module "main"

import "std/abort"
import "std/io"
import "std/string"
import "std/char"
import "std/string_builder"

// Compile-time bake: the canonical test input is embedded next to the source.
DEFAULT_INPUT :: @comptime {
    read :: read_file_string("mowitnow_input.txt")
    if read is IOError {
        panic("cannot read `mowitnow_input.txt`: ${read.message()}")
    }
    return read
}

// --- Domain types ---------------------------------------------------------

Direction :: enum { North, East, South, West }

Direction implements Display {
    to_string :: fn(self) -> string {
        match self {
            .North -> "N"
            .East  -> "E"
            .South -> "S"
            .West  -> "W"
        }
    }
}

// Quarter-turn rotation, abstracted as a trait so any compass-like type can
// opt in. The methods return `Self`, which resolves to the implementer.
Turnable :: trait {
    turn_left  :: fn(self) -> Self
    turn_right :: fn(self) -> Self
}

Direction implements Turnable {
    turn_left :: fn(self) = match self {
        .North -> .West
        .West  -> .South
        .South -> .East
        .East  -> .North
    }

    turn_right :: fn(self) = match self {
        .North -> .East
        .East  -> .South
        .South -> .West
        .West  -> .North
    }
}

Command :: enum { Advance, Left, Right }

// Lawn upper-right corner: the lower-left corner is fixed at (0, 0).
Lawn :: struct {
    width:  i32
    height: i32
}

contains :: fn(self: Lawn, x: i32, y: i32) -> bool {
    return x >= 0 && y >= 0 && x <= self.width && y <= self.height
}

Mower :: struct {
    x:   i32
    y:   i32
    dir: Direction
}

Mower implements Display {
    to_string :: fn(self) = "${self.x} ${self.y} ${self.dir}"
}

// --- Movement -------------------------------------------------------------

// Hypothetical next position one cell ahead of `m` (no bounds checking).
step :: fn(m: Mower) -> [i32, i32] = match m.dir {
    .North -> [m.x,     m.y + 1]
    .East  -> [m.x + 1, m.y]
    .South -> [m.x,     m.y - 1]
    .West  -> [m.x - 1, m.y]
}

advance :: fn(m: Mower, lawn: Lawn) -> Mower {
    [nx, ny] :: step(m)
    if !lawn.contains(nx, ny) {
        return m
    }
    return Mower { .x = nx, .y = ny, .dir = m.dir }
}

apply :: fn(m: Mower, lawn: Lawn, cmd: Command) = match cmd {
    .Advance -> advance(m, lawn)
    .Left    -> Mower { .x = m.x, .y = m.y, .dir = m.dir.turn_left() }
    .Right   -> Mower { .x = m.x, .y = m.y, .dir = m.dir.turn_right() }
}

run_mower :: fn(start: Mower, lawn: Lawn, commands: Command[]) -> Mower {
    mower := start
    for cmd in commands {
        mower = apply(mower, lawn, cmd)
    }
    return mower
}

// --- Parsing --------------------------------------------------------------

parse_direction :: fn(c: char) -> Direction | ParseError {
    if c == 'N' { return .North }
    if c == 'E' { return .East }
    if c == 'S' { return .South }
    if c == 'W' { return .West }
    return ParseError { .msg = "unknown direction: '${c}'" }
}

parse_command :: fn(c: char) -> Command | ParseError {
    if c == 'A' { return .Advance }
    if c == 'G' { return .Left }
    if c == 'D' { return .Right }
    return ParseError { .msg = "unknown command: '${c}'" }
}

parse_lawn :: fn(line: string) -> Lawn | ParseError {
    parts :: line.trim().split_where(is_white_char)
    if parts.len() != 2 {
        return ParseError { .msg = "expected '<w> <h>', got '${line}'" }
    }
    w :: parse_int(parts[0])
    if w is ParseError { return w }
    h :: parse_int(parts[1])
    if h is ParseError { return h }
    if w < 0 || h < 0 {
        return ParseError { .msg = "lawn dimensions must be non-negative, got '${line}'" }
    }
    return Lawn { .width = w, .height = h }
}

parse_position :: fn(line: string, lawn: Lawn) -> Mower | ParseError {
    parts :: line.trim().split_where(is_white_char)
    if parts.len() != 3 {
        return ParseError { .msg = "expected '<x> <y> <dir>', got '${line}'" }
    }
    x :: parse_int(parts[0])
    if x is ParseError { return x }
    y :: parse_int(parts[1])
    if y is ParseError { return y }
    if parts[2].len() != 1 {
        return ParseError { .msg = "expected single direction char, got '${parts[2]}'" }
    }
    d :: parse_direction(parts[2][0])
    if d is ParseError { return d }
    if !lawn.contains(x, y) {
        return ParseError { .msg = "starting position '${line}' is outside the lawn" }
    }
    return Mower { .x = x, .y = y, .dir = d }
}

parse_commands :: fn(line: string) -> Command[] | ParseError {
    out: Command[]: []
    for c in line.trim().chars() {
        cmd :: parse_command(c)
        if cmd is ParseError { return cmd }
        out.push(cmd)
    }
    return out
}

// --- Driver ---------------------------------------------------------------

is_newline :: fn(c: char) = c == '\n' || c == '\r'

run :: fn(input: string) -> string | ParseError {
    lines :: input.split_where(is_newline)
    if lines.len() == 0 {
        return ParseError { .msg = "input is empty" }
    }
    lawn :: parse_lawn(lines[0])
    if lawn is ParseError { return lawn }

    out :: StringBuilder {}
    i: i64 = 1
    first := true
    for i < lines.len() {
        if i + 1 >= lines.len() {
            return ParseError { .msg = "missing command line for mower at '${lines[i]}'" }
        }
        mower    :: parse_position(lines[i], lawn)
        if mower is ParseError { return mower }
        command  :: parse_commands(lines[i + 1])
        if command is ParseError { return command }
        final    :: run_mower(mower, lawn, command)
        if !first {
            out.append("\n")
        }
        out.append(final.to_string())
        first = false
        i = i + 2
    }
    return out.to_string()
}

// --- Entrypoint -----------------------------------------------------------

load_input :: fn(argv: string[]) -> string | Error {
    if argv.len() > 1 {
        r :: read_file_string(argv[1])
        if r is Error { return r }
        return r
    }
    return DEFAULT_INPUT
}

main :: fn(argv: string[]) -> i32 {
    raw :: load_input(argv)
    match raw {
        is Error -> {
            // The IO error from `read_file` is currently host-tagged ; calling
            // `.message()` on it would round-trip through a vtable that the VM
            // bootstrap does not always populate, so we report a generic note.
            eprintln(if argv.len() > 1 {
                "io error: cannot read '${argv[1]}'"
            } else {
                "io error reading the embedded input"
            })
            return 1
        }
        is string -> {
            match out :: run(raw) {
                is string     -> println(out)
                is ParseError -> {
                    eprintln("parse error: ${out.message()}")
                    return 1
                }
            }
        }
    }
    return 0
}