Examples

Start with the first language examples and work down for a tour of Jik, or jump directly to a topic. For detailed explanations, see the documentation.

hello.jik

// Example: Hello, world

func main():
    println("Hello, world!")
end

jik run examples/hello.jik

values.jik

// Example: values, inferred and explicit types, and assignment.

func main():
    item := "notebook"
    aisle: char = 'B'
    quantity := 3
    unit_price: double = 2.5
    in_stock: bool = true
    reserved: int // Without an initializer, a type's default value is used.

    quantity += 1
    reserved += 1
    total := quantity * unit_price
    eligible := in_stock and quantity >= 4
    discount := 1.0 if eligible else 0.0
    total = total - discount

    println(item, " in aisle ", aisle)
    println("quantity: ", quantity)
    println("available: ", quantity - reserved)
    println("discount applied: ", eligible)
    println("total: ", total)
end

jik run examples/values.jik

functions.jik

// Example: functions and type inference through temperature conversion.

// Calls and arithmetic determine the types of this local helper.
func fahrenheit(celsius):
    return celsius * 9.0 / 5.0 + 32.0
end

// An explicit signature describes the contract without inspecting callers.
func celsius(fahrenheit_value: double) -> double:
    return (fahrenheit_value - 32.0) * 5.0 / 9.0
end

func show_temperature(degrees):
    println(degrees, " C = ", fahrenheit(degrees), " F")
end

func main():
    show_temperature(0.0)
    show_temperature(20.0)
    println("boiling point in C: ", celsius(212.0))
    println("round trip: ", celsius(fahrenheit(20.0)))
end

jik run examples/functions.jik

control_flow.jik

// Example: filling a delivery van, then counting down to departure.

func main():
    capacity := 10
    loaded := 0

    // Numeric ranges exclude the upper bound: weights are 1 through 6.
    for weight = 1, 7:
        if weight == 2:
            println("skip parcel awaiting an address")
            continue
        elif loaded + weight > capacity:
            println("next parcel will not fit")
            break
        else:
            loaded += weight
            println("loaded weight: ", loaded)
        end
    end

    remaining := 3
    while remaining > 0:
        println("departing in ", remaining)
        remaining -= 1
    end
    println("departed with ", loaded, " of ", capacity)
end

jik run examples/control_flow.jik

cl_args.jik

// Example: Command-line arguments

func main(args):
    for i, arg in args:
        println("Value of arg ", i, " is: ", arg)
    end
end

jik run examples/cl_args.jik

Build this example and pass arguments to the executable to see them printed.

strings.jik

// Example: cleaning a label and inspecting its text.

use "jik/string"

func main():
    raw := "  Jik language  "
    label := string::trim(raw)

    // Strings store UTF-8 bytes. Length, indexing, and slicing count bytes;
    // ASCII input makes each byte here a complete character.
    println("label: ", label)
    println("bytes: ", len(label))
    println("first character: ", label[0])
    println("prefix: ", label[:3])
    println("description starts at: ", string::find(label, "language"))
    println("starts with Jik: ", string::starts_with(label, "Jik"))
end

jik run examples/strings.jik

vectors.jik

// Example: correcting scores and collecting the passing results.

func main():
    scores := [42, 68, 91]
    scores[0] = 55
    push(scores, 73)
    push(scores, 0)
    println("removed accidental entry: ", pop(scores))

    passed: Vec[int]
    for score in scores:
        if score >= 60:
            push(passed, score)
        end
    end

    for index, score in scores:
        println("score ", index + 1, ": ", score)
    end
    println("passing count: ", len(passed))
    println("first two passing scores: ", passed[:2])
end

jik run examples/vectors.jik

structs.jik

// Example: struct construction, fields, and a uniform function call.

struct Task:
    title: String
    done: bool
end

func complete(task: Task):
    task.done = true
end

func main():
    draft := Task{}
    draft.title = "Draft a proposal"
    review := Task{title = "Review the proposal"}
    title := "Publish the proposal"
    publish := Task{title}

    // This is shorthand for the ordinary function call complete(draft).
    draft.complete()
    println(draft.title, ": done = ", draft.done)
    println(review.title, ": done = ", review.done)
    println(publish.title, ": done = ", publish.done)
end

jik run examples/structs.jik

options.jik

// Example: a search result that may be absent.

func find_index(values: Vec[int], target: int) -> Option[int]:
    for index, value in values:
        if value == target:
            // The returned option is allocated in the input vector's region.
            return Some{index}
        end
    end
    return None
end

func main():
    values := [12, 25, 38]
    found := find_index(values, 25)
    if found is Some:
        // ? extracts the payload; check presence before using it.
        println("25 found at index ", found?)
    end

    missing := find_index(values, 99)
    if missing is None:
        println("99 was not found")
    end
end

jik run examples/options.jik

dictionaries.jik

// Example: tracking stock with string keys. See options.jik for option basics.

func main():
    stock := {"notebooks": 4, "pens": 12}
    stock["notebooks"] = 6
    stock["folders"] = 3

    // Lookup returns Option[int]; a missing key has no payload to extract.
    notebooks := stock["notebooks"]
    if notebooks is Some:
        println("notebooks available: ", notebooks?)
    end
    if stock["erasers"] is None:
        println("erasers are not listed")
    end

    // Aggregate without making the output depend on dictionary iteration order.
    total := 0
    for name, count in stock:
        total += count
    end
    println("product kinds: ", len(stock), ", total items: ", total)
end

jik run examples/dictionaries.jik

enum_match.jik

// Exhaustive matching over an enum.

enum TrafficLight:
    RED
    YELLOW
    GREEN
end

func wait_seconds(light: TrafficLight) -> int:
    match light:
        case RED:
            return 30
        case YELLOW:
            return 3
        case GREEN:
            return 0
    end
end

func main():
    light := TrafficLight.YELLOW
    println("wait seconds: ", wait_seconds(light))
end

jik run examples/enum_match.jik

variants.jik

// Example: input events with enums, variants, and match.

enum Key:
    ENTER
    ESCAPE
end

variant Event:
    KEY: Key
    TEXT: String
    QUIT
end


func show(event: Event):
    // An exhaustive match handles every tag and can bind its payload.
    match event:
        case KEY{key}:
            println("key: ", key)
        case TEXT{text}:
            println("text: ", text)
        case QUIT:
            println("quit requested")
    end
end


func is_input(event: Event) -> bool:
    // Omit payload bindings when only the tag matters.
    // other handles every tag not listed explicitly.
    match event:
        case KEY:
            return true
        case TEXT:
            return true
        other:
            return false
    end
end


func main():
    events := [
        Event.KEY{Key.ENTER},
        Event.TEXT{"hello"},
        Event.QUIT{}
    ]

    for event in events:
        show(event)
        // UFCS calls the ordinary function is_input(event).
        println("input event: ", event.is_input())

        // Member access syntax is used to extract the active variant tag.
        // Accessing .TEXT on another tag results in a runtime error.
        if event is TEXT:
            println("text length: ", len(event.TEXT))
        end
    end
end

jik run examples/variants.jik

tables.jik

// Example: exhaustive, immutable lookup tables

enum Signal:
    RED
    GREEN
    YELLOW
end

table SignalNames[Signal] -> String:
    RED: "red"
    GREEN: "green"
    YELLOW: "yellow"
end

table Durations[Signal] -> int:
    RED: 30
    GREEN: 25
    YELLOW: 5
end

table NextSignal[Signal] -> Signal:
    RED: Signal.GREEN
    GREEN: Signal.YELLOW
    YELLOW: Signal.RED
end

func main():
    signal := Signal.RED

    for cycle = 0, 6:
        println(SignalNames[signal], " for ", Durations[signal], " seconds")
        signal = NextSignal[signal]
    end
end

jik run examples/tables.jik

region_ergonomics.jik

// Example: region ergonomics in Jik.

struct User:
    name: String
    labels: Vec[String]
end


func new_user(foreign name: String, r: Region) -> User:
    // The returned struct and its composite field `labels` are automatically
    // allocated in r, since this is the only possible valid allocation destination.
    // Since `name` is marked as a foreign parameter, we still need to copy it to `r`.
    return User{
        name = copy(name, r),
        labels = ["new", "active"]
    }
end


func display_name(user: User) -> String:
    if user.name == "":
        // The returned string literal is allocated in user's region, since
        // this is the only valid destination.
        return "Anonymous"
    end
    return user.name
end


func add_default_label(user: User):
    // The string literal is automatically allocated in user's region.
    // This is also valid for other store operations involving composite values.
    push(user.labels, "member")
end

func default_labels(r: Region) -> Vec[String]:
    // @ selects the implicit region determined by the same-region rule.
    // Here it is equivalent to [r]. With multiple participating composite arguments,
    // @ selects their shared region without naming a specific argument.
    // Without @, labels would be local and could not be returned.
    labels := ["new", "active"]@
    push(labels, "member")
    return labels
end


func main():
    // An omitted final Region argument automatically passes the current
    // function's local region `_`.
    user := new_user("Ada")

    add_default_label(user)
    println(display_name(user), ": ", user.labels)
    println("default labels: ", default_labels())
end

jik run examples/region_ergonomics.jik

regions_copy.jik

// Example: copy composite values into the caller's region before returning them.

struct Note:
    text: String
    pinned: bool
end


func make_note(foreign source: String, r: Region) -> Note:
    // The source may belong to another region, so make an owned copy for the
    // returned struct. source.copy(r) is shorthand for copy(source, r).
    // The caller chooses the destination region through r.
    return Note{text = source.copy(r), pinned = true}[r]
end


func default_names(r: Region) -> Vec[String]:
    names := ["Ada", "Grace"]
    // names is local to this helper; copying makes it safe to return.
    return names.copy(r)
end


func main():
    note := make_note("remember who owns this value", _)
    names := default_names(_)

    println(note.text, " (pinned: ", note.pinned, ")")
    println("names: ", names)
end

jik run examples/regions_copy.jik

error_handling.jik

// Example: throwing functions, recovery, propagation, must, and postfix !

throws func validate_count(count):
    if count < 0:
        fail("count must not be negative")
    end
    if count > 100:
        fail("count exceeds the batch limit", 17)
    end
    return count
end

func show_count(count):
    try value := validate_count(count):
        println("accepted count: ", value)
    except:
        println("rejected count: ", count)
        println("  msg:  ", error_msg())
        println("  code: ", error_code())
    end
end

throws func half_count(count):
    // A declaration with try propagates a failure to half_count's caller.
    valid := try validate_count(count)
    return valid / 2
end

func main():
    show_count(42)
    show_count(-1)
    show_count(101)

    value := must validate_count(42)
    println("must succeeded with value ", value)
    println("postfix ! succeeded with value ", validate_count(42)!)

    try half := half_count(-1):
        println("half: ", half)
    except:
        println("propagated failure: ", error_msg())
    end
end

jik run examples/error_handling.jik

// Example: Local modules and imports

use "stats" as st


func main():
    xs := [3, 1, 4, 1, 5]

    total := st::sum(xs)
    best := st::max(xs)

    println("sum is: ", total, ", max value is: ", best)
end

jik run examples/modules/main.jik

Keep main.jik and stats.jik together in the modules directory.

testing_demo.jik

// Example: Basic use of jik/testing

use "jik/testing" as test

func square(x):
    return x * x
end

func main():
    ts := test::suite_new()

    test::suite_assert(ts, square(0) == 0, site())
    test::suite_assert(ts, square(3) == 9, site())
    test::suite_assert(ts, square(-4) == 16, site())

    test::suite_finish(ts)
end

jik run examples/testing_demo.jik

ffi_demo.jik

// Example: Foreign function interface (C interop)

@embed{C_END}

int32_t
impl_adder(int32_t x, int32_t y)
{
    return x + y;
}

typedef struct impl_Point {
    double x;
    double y;
} impl_Point;

impl_Point *
impl_point_new(double x, double y, JikRegion *r)
{
    impl_Point *p = jik_region_alloc(r, sizeof(impl_Point));
    p->x = x;
    p->y = y;
    return p;
}

double
impl_point_x(impl_Point *p)
{
    return p->x;
}

C_END

extern func impl_adder as adder(x: int, y: int) -> int
extern func toupper as toupper(ch: char) -> char
extern struct impl_Point as Point
extern func impl_point_new as point_new(x: double, y: double, r: Region) -> Point
extern func impl_point_x as point_x(p: Point) -> double

func main():
    println("adder(0, 1) = ", adder(0, 1))
    println("adder(5, -3) = ", adder(5, -3))
    println(toupper('a'))

    point := point_new(3.5, 4.25, _)
    // Extern structs are opaque; UFCS calls the exported accessor function.
    println("point x = ", point.point_x())
end

jik run examples/ffi_demo.jik

filesystem.jik

// Example: inspect a file using filesystem, path, and I/O utilities.

use "jik/fs"
use "jik/io"
use "jik/path"
use "jik/sys"


func main():
    examples_dir := path::join([sys::cwd(), "examples"])
    readme := path::join([examples_dir, "README.md"])

    entries := must fs::read_dir(examples_dir)
    contents := must io::read_file(readme)

    println("directory exists: ", fs::is_dir(examples_dir))
    println("README is a file: ", fs::is_file(readme))
    println("name: ", path::basename(readme))
    println("extension: ", path::extname(readme))
    println("directory entries: ", len(entries))
    println("README bytes: ", len(contents))
end

jik run examples/filesystem.jik

binary_data.jik

// Example: build and inspect data that can contain any byte value.

use "jik/bytes"


func main():
    packet := bytes::buf_from_string("JIK")
    must bytes::buf_push_int(packet, 0)
    bytes::buf_push(packet, '!')
    must bytes::buf_push_int(packet, 255)

    data := bytes::to_bytes(packet)
    header := bytes::slice(data, 0, 3)

    println("header: ", must bytes::to_string_ascii(header))
    println("packet bytes: ", bytes::len(data))
    println("contains NUL: ", bytes::get(data, 3) == '\0')
    println("hex: ", bytes::to_hex(data))
end

jik run examples/binary_data.jik

strbuf_demo.jik

// Example: efficiently build a string from many small pieces.

use "jik/strbuf"
use "jik/string"


func main():
    scores := [7, 9, 10]
    line := strbuf::new("scores: ")

    for index, score in scores:
        if index > 0:
            strbuf::append(line, ", ")
        end
        strbuf::append(line, string::from_int(score))
    end
    strbuf::append_char(line, '.')

    result := strbuf::to_string(line)
    println(result)
    println("bytes: ", strbuf::len(line))
end

jik run examples/strbuf_demo.jik

text_processing.jik

// Example: string and vector slices, indexed iteration, and string comparison.

use "jik/string"


func main():
    text := "alpha\nbeta\ngamma"
    lines := string::split(text, "\n", _)

    prefix := lines[:2]
    middle := lines[1:3]
    suffix := lines[2:]
    word := text[6:10]

    println("first two lines: ", prefix)
    println("middle lines:    ", middle)
    println("last line:       ", suffix[0])
    println("sliced word:     ", word)

    for line_no, line in lines:
        println(line_no + 1, ": ", line)
    end

    if string::compare(lines[0], lines[2]) < 0:
        println(lines[0], " comes before ", lines[2])
    end
end

jik run examples/text_processing.jik

argparse_demo.jik

// Example: Command-line parsing, generated help, and path normalization.

use "jik/argparse"
use "jik/path"


func main(args):
    parser := argparse::new("copy-plan", _)
    parser.add_positional("source", "File to copy.")
    parser.add_positional("destination", "Planned destination.")
    parser.add_option("--verbose", "-v", "Print the normalized paths.")

    if len(args) == 1 or args[1] == "--help":
        println(parser.format_help())
    else:
        try result := parser.parse(args[1:]):
            source := path::normalize(result.positionals["source"]?, _)
            destination := path::normalize(result.positionals["destination"]?, _)

            if result.options["--verbose"] is Some:
                println("source:      ", source)
                println("destination: ", destination)
            end
            println("would copy ", source, " to ", destination)
        except:
            println(error_msg())
            println("\n", parser.format_help())
        end
    end
end

jik run examples/argparse_demo.jik

Run without arguments for help. To supply arguments, build it and run the executable.

process_capture.jik

// Example: Run a child process and inspect captured stdout/stderr bytes.

use "jik/bytes"
use "jik/process"
use "jik/sys"


COMPILER := ".\\jik.exe" if sys::platform() == "windows" else "./jik"

func main():
    res := must process::capture(COMPILER, ["version"], _)

    println("exit code: ", res.code)
    println("stdout bytes: ", bytes::len(res.out))
    println("stderr bytes: ", bytes::len(res.err))

    if bytes::len(res.out) > 0:
        println("stdout text: ", must bytes::to_string_ascii(res.out, _))
    end
end

jik run examples/process_capture.jik

Run from the compiler repository root; this example starts the local Jik executable.

fib.jik

// Example: Fibonacci, recursive and iterative

func fib(n):
    if n < 2:
        return n
    end
    return fib(n - 1) + fib(n - 2)
end


func fib_iter(n, r):
    // Allocate result in region "r"
    res := [n of 0][r]
    if n < 2:
        return res
    end
    res[0] = 0
    res[1] = 1
    for i = 2, n:
        res[i] = res[i - 1] + res[i - 2]
    end
    return res
end


func main():
    for i = 0, 10:
        print("fib (", i, ") = ", fib(i), "\n")
    end

    nums := fib_iter(10, _)
    print("fib_iter: ", nums)
end

jik run examples/fib.jik

primes.jik

// Example: Sieve of Eratosthenes
func sieve(n, r):
    is_prime := [n + 1 of true][r]
    is_prime[0] = false
    is_prime[1] = false
    p := 2
    while p * p <= n:
        if is_prime[p]:
            i := p * p
            while i <= n:
                is_prime[i] = false
                i = i + p
            end

        end

        p = p + 1
    end
    return is_prime
end


func main():
    n := 100
    res := sieve(n, _)
    println("Primes up to ", n, ":")
    for i = 2, n + 1:
        if res[i]:
            print(i, ", ")
        end
    end
end

jik run examples/primes.jik

word_count.jik

// Example: Count lines, words, and bytes in a text file.

use "jik/io"
use "jik/char"


struct Counts:
    lines: int
    words: int
    bytes: int
end


func count_text(s: String, r: Region) -> Counts:
    lines := 0
    words := 0
    bytes := len(s)

    in_word := false

    for i = 0, len(s):
        c := s[i]

        if c == '\n':
            lines += 1
        end

        if char::isspace(c):
            in_word = false
        elif not in_word:
            words += 1
            in_word = true
        end
    end

    return Counts{lines, words, bytes}[r]
end


func main(args):
    if len(args) < 2:
        println("usage: word_count <file>")
    else:
        path := args[1]
        text := must io::read_file(path, _)
        counts := count_text(text, _)

        println("file:  ", path)
        println("lines: ", counts.lines)
        println("words: ", counts.words)
        println("bytes: ", counts.bytes)
    end
end

jik run examples/word_count.jik

To read a file, build this example and pass a file path to the executable.

newton.jik

use "jik/math" as math


struct NewtonResult:
    root: double
    steps: int
    converged: bool
    xs: Vec[double]
end


func f(x: double) -> double:
    // We want f(x) = 0  <=>  cos(x) - x = 0  <=>  cos(x) = x
    return math::cos(x) - x
end


func df(x: double) -> double:
    // f'(x) = -sin(x) - 1
    return -math::sin(x) - 1.0
end


func newton_cos_minus_x(x0: double, tol: double, max_steps: int, r: Region) -> NewtonResult:
    xs: Vec[double][r]
    x := x0
    push(xs, x)

    step := 0
    while step < max_steps:
        fx := f(x)
        if math::abs(fx) <= tol:
            break
        end

        dfx := df(x)
        if math::abs(dfx) <= 1e-14:
            // Derivative too small: would amplify error or divide by ~0.
            break
        end

        x = x - fx / dfx
        push(xs, x)
        step = step + 1
    end

    // Check the returned estimate, including the final permitted update.
    converged := math::abs(f(x)) <= tol
    return NewtonResult{
        root = x,
        steps = len(xs) - 1,
        converged,
        xs
    }[r]
end


func main():
    res := newton_cos_minus_x(1.0, 1e-12, 40, _)

    print("converged: ", res.converged, "\n")
    print("steps:     ", res.steps, "\n")
    print("root:      ", res.root, "\n")
    print("f(root):   ", f(res.root), "\n")
    print("\n")
    print("iteration history:", "\n")

    for i, x in res.xs:
        println("  ", i, ": x = ", x, ", f(x) = ", f(x))
    end
end

jik run examples/newton.jik

dijkstra.jik

// Example: Dijkstra's algorithm

INF := 1_000_000_000


func dijkstra_calc(w: Vec[Vec[int]], src: int):
    n := len(w)

    // The returned distances live in w's region (@ is equivalent to [.w]).
    dist := [n of INF]@
    // Scratch storage stays local and is reclaimed when this function returns.
    used := [n of false]

    dist[src] = 0

    for i = 0, n:
        best_v := -1
        best_d := INF

        for v = 0, n:
            if not used[v] and dist[v] < best_d:
                best_d = dist[v]
                best_v = v
            end
        end

        if best_v == -1:
            break
        end

        used[best_v] = true

        for to = 0, n:
            wt := w[best_v][to]
            if wt < INF:
                if dist[best_v] + wt < dist[to]:
                    dist[to] = dist[best_v] + wt
                end
            end
        end
    end

    return dist
end


func main():
    w := [
        [0,   10,  3,   INF, INF],
        [INF, 0,   1,   2,   INF],
        [INF, 4,   0,   8,   2],
        [INF, INF, INF, 0,   7],
        [INF, INF, INF, 9,   0]
    ]
    d := dijkstra_calc(w, 0)
    print(d)
end

jik run examples/dijkstra.jik

game_of_life.jik

// Example: Conway's Game of Life in the terminal.

// Note (terminal rendering):
// This program clears the screen once with `cls`/`clear`, then uses ANSI cursor-home
// sequences to redraw in-place. This works in most Unix-like terminals and in ANSI-capable Windows terminals
// (e.g., Windows Terminal). If your terminal prints the escape codes literally, it does not support
// ANSI sequences.

use "jik/sys" as sys
use "jik/rand" as rand
use "jik/strbuf"


W := 80
H := 40
STEPS := 400
DELAY_MS := 60
DENSITY_PERCENT := 15    // initial probability of life: 0 - 100


func clear_screen():
    p := sys::platform(_)
    if p == "windows":
        sys::system("cls")
    else:
        sys::system("clear")
    end
end


func wrap(i: int, max: int) -> int:
    if i < 0:
        return i + max
    elif i >= max:
        return i - max
    else:
        return i
    end
end


func count_neighbors(g: Vec[Vec[bool]], x: int, y: int, w: int, h: int) -> int:
    c := 0
    for dy = -1, 2:
        for dx = -1, 2:
            if dx == 0 and dy == 0:
                continue
            end

            nx := wrap(x + dx, w)
            ny := wrap(y + dy, h)

            if g[ny][nx]:
                c += 1
            end
        end
    end
    return c
end


func render(g: Vec[Vec[bool]], step: int, w: int, h: int):
    print("\x1b[HGame of Life (step ", step, ")\n")

    // Reuse the buffer for each row; clear retains its allocated capacity.
    line := strbuf::new("")
    for y = 0, h:
        row := g[y]
        line.clear()
        for x = 0, w:
            line.append_char('#' if row[x] else '.')
        end
        line.append_char('\n')
        line.print()
    end

    print("\x1b[?25h")
end


func randomize(g: Vec[Vec[bool]], rng: rand::Rng, w: int, h: int):
    for y = 0, h:
        for x = 0, w:
            r := rand::next_int(rng) % 100
            g[y][x] = (r < DENSITY_PERCENT)
        end
    end
end


func step_life(curr: Vec[Vec[bool]], next: Vec[Vec[bool]], w: int, h: int):
    for y = 0, h:
        for x = 0, w:
            n := count_neighbors(curr, x, y, w, h)
            alive := curr[y][x]

            if alive:
                next[y][x] = (n == 2) or (n == 3)
            else:
                next[y][x] = (n == 3)
            end
        end
    end
end


func main():
    clear_screen()
    rng := rand::new_time(_)

    curr := [H of [W of false]]
    next := [H of [W of false]]

    randomize(curr, rng, W, H)

    step := 0
    while step < STEPS:
        render(curr, step, W, H)
        step_life(curr, next, W, H)

        tmp := curr
        curr = next
        next = tmp

        sys::sleep(DELAY_MS)
        step += 1
    end
end

jik run examples/game_of_life.jik

Runs an animation in your terminal. Press Ctrl+C to stop.

forth.jik

// Example: Forth interpreter REPL
use "jik/std"
use "jik/string"
use "jik/char"


HELP_TEXT := """
REPL Commands:
    quit - exit REPL
    help - show help

Interpreter commands:
    . - show the stack
    ? - show defined words

"""


struct ForthMachine:
    stack: Vec[int]
    words: Dict[Vec[String]]
end

throws func require_stack(fm: ForthMachine, count: int):
    if len(fm.stack) < count:
        fail("Stack underflow")
    end
end


throws func eval_tokens(fm, tokens):
    n := len(tokens)
    ip := 0
    compile_mode := false
    word := [0 of ""][.fm]
    word_name := ""[.fm]
    while ip < n:
        tok := tokens[ip]
        if compile_mode and tok == ";":
            compile_mode = false
            fm.words[word_name] = word
            ip = ip + 1
            continue
        elif compile_mode and tok == ":":
            fail("Cannot re-enter compile mode")
        elif compile_mode:
            push(word, tok)
            ip = ip + 1
            continue
        elif tok == "":
            ip = ip + 1
            continue
        elif tok == "\n":
            ip = ip + 1
            continue
        elif char::isdigit(tok[0]):
            res := try string::to_int(tok)
            push(fm.stack, res)
        elif tok == ".":
            print(fm.stack)
        elif tok == "?":
            print(fm.words)
        elif tok == "+":
            try require_stack(fm, 2)
            rhs := pop(fm.stack)
            lhs := pop(fm.stack)
            push(fm.stack, lhs + rhs)
        elif tok == "*":
            try require_stack(fm, 2)
            rhs := pop(fm.stack)
            lhs := pop(fm.stack)
            push(fm.stack, lhs * rhs)
        elif tok == "dup":
            try require_stack(fm, 1)
            push(fm.stack, fm.stack[len(fm.stack) - 1])
        elif tok == "drop":
            try require_stack(fm, 1)
            pop(fm.stack)
        elif tok == ":":
            compile_mode = true
            word = [0 of ""][.fm]
            ip = ip + 1
            // split preserves empty fields, so allow spaces before the name.
            while ip < n and tokens[ip] == "":
                ip += 1
            end
            if ip >= n:
                fail("Expected a word name after ':'")
            end
            word_name = tokens[ip]
            if word_name == ":":
                fail("Cannot re-enter compile mode")
            elif word_name == ";":
                fail("Expected a word name after ':'")
            end
        else:
            w := fm.words[tok]
            if w is None:
                fail("Unknown word")
            else:
                // Propagate failures from user-defined words to the REPL.
                try eval_tokens(fm, w?)
            end
        end
        ip = ip + 1
    end
end


throws func run(fm, code):
    tokens := string::split(code, " ", .fm)
    try eval_tokens(fm, tokens)
end


func main():
    fm := ForthMachine{}
    print("Welcome! Type quit to quit, help for help.")
    while true:
        print("\n> ")
        code := std::input(_)
        if code == "quit":
            break
        elif code == "help":
            print(HELP_TEXT)
            continue
        end
        try run(fm, code):
        except:
            println("Error: ", error_msg())
            break
        end
    end
end

jik run examples/forth.jik

Starts an interactive Forth interpreter in your terminal.