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
}1 3 N 5 1 E