Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Recipes

Short, complete programs for the things you usually try first in a new language. Each one runs as it is: save it as a .bc file and run it with bcc run. The -- stdin: lines under a program are the input it was run with, and the -- output: lines are what it printed.

Sum the numbers on standard input

include textio

main -> 1 =
  let s = read_input();
  let sum = borrow imm s { t -> fold_i32s(t, !{ (x, acc) -> acc + x }, 0) };
  print_dec(sum);
  print_byte(b'\n')
-- stdin: 1 2
-- stdin: 3
-- stdin: -4 10
-- output: 12

read_input() reads all of standard input into a string, and borrow imm s { t -> … } lends it as a text view t. fold_i32s walks every integer written in the text, in order, and folds it into a running value: a run of digits is a number, a - just before it makes it negative, and every other byte separates numbers, so spaces, newlines and commas all work. The closure !{ (x, acc) -> acc + x } gets each number and the sum so far.

Parse numbers and report bad input

include textio

data Tally = { line: i32, sum: i32, bad: Vec[i32] }

-- One more line: added to the sum if it is a number, its line number kept if not.
fn add_line<'a>(l: text 'a, t: Tally) -> Tally =
  match t {
    Tally { line, sum, bad } ->
      let line = line + 1;
      match with to_result { parse_i32(trim(l)) } {
        inl x -> Tally { line, sum: sum + x, bad }
      | inr e -> push(&mut bad, line); Tally { line, sum, bad }
      }
  }

fn report<'a>(bad: array_imm 'a i32) -> 1 =
  for n in bad { print_lit("line "); print_dec(*n); print_lit(" is not a number\n") }

main -> 1 =
  let s = read_input();
  let start = Tally { line: 0, sum: 0, bad: box buf[i32](0) };
  let t = borrow imm s { v -> fold_lines(v, !{ (l, t) -> add_line(l, t) }, start) };
  match t {
    Tally { line, sum, bad } ->
      borrow imm bad { v -> report(v) };
      print_lit("sum ");
      print_dec(sum);
      print_byte(b'\n')
  }
-- stdin:  12
-- stdin: four
-- stdin: 30
-- stdin: 3x
-- output: line 2 is not a number
-- output: line 4 is not a number
-- output: sum 42

parse_i32 raises a ParseError when the text isn’t a number, and with to_result { … } turns that into a value: inl the number, or inr the error (Errors). fold_lines hands each line to add_line with the state so far, a Tally record that counts lines, keeps the sum and collects the numbers of the bad lines in a Vec. match t { Tally { line, sum, bad } -> … } takes a record apart into variables named after its fields, and Tally { line, sum, bad } builds one back from variables of the same names. The report is printed once the fold has finished.

Count things with a map

include textio
include map

-- One more sighting of `x`.
fn seen(x: i32, counts: Map[i32, i32]) -> Map[i32, i32] =
  let mut counts = counts;
  let old = map_lookup(&counts, x);
  let n = match old { Some(n) -> n + 1 | None -> 1 };
  map_insert(&mut counts, x, n);
  counts

main -> 1 =
  let s = read_input();
  let counts = borrow imm s { t -> fold_i32s(t, !{ (x, m) -> seen(x, m) }, map_new[i32, i32](16)) };
  let mut best = 0;
  let mut most = 0;
  borrow imm counts { 'x, r ->
    for e in map_iter(r) { let (x, v) = e; let n = *v; if n > most { best = x; most = n } } };
  let distinct = map_len(&counts);
  print_dec(distinct);
  print_lit(" different numbers; ");
  print_dec(best);
  print_lit(" appears most, ");
  print_dec(most);
  print_lit(" times\n")
-- stdin: 3 1 4 1 5 9 2 6 5 3 5
-- output: 7 different numbers; 5 appears most, 3 times

map_lookup answers a copy of the count stored for a key, as an Option, and map_insert stores the new one; both take the map lent, &counts to read it and &mut counts to change it. To visit every entry, borrow imm counts { 'x, r -> … } lends the map and map_iter(r) yields each key with a view of its value, read with *v. The iteration order comes from the hash, and it’s the same on every run. Maps and sets lists the rest of the map’s operations.

Build a string

include textio

-- `[a, b, c]`, written into a new string.
fn show_list<'a>(v: array_imm 'a i32) -> string =
  let mut s = box string(32);
  push_char(&mut s, '[');
  let mut first = true;
  for p in v {
    if not(first) { push_lit(&mut s, ", ") };
    push_dec(&mut s, *p);
    first = false
  };
  push_char(&mut s, ']');
  s

fn square(i: i32) -> i32 = (i + 1) * (i + 1)

main -> 1 =
  let a = array_new[i32](5, square);
  let s = borrow imm a { v -> show_list(v) };
  let n = len_of(&s);
  let s = print_string(s);
  print_lit(" (");
  print_dec(n);
  print_lit(" bytes)\n")
-- output: [1, 4, 9, 16, 25] (17 bytes)

box string(32) allocates an empty string with room for 32 bytes; it grows when it needs to. push_char, push_lit and push_dec append a character, a literal and a number. The function hands the finished string back to its caller, which owns it from then on. print_string(s) prints it and hands it back, and the string is freed when main ends.

Read the command-line arguments

include textio

-- Greets each name given on the command line, or the world if none is.
main -> 1 =
  let mut n = 0;
  let mut more = true;
  while more {
    let (name, got) = next_arg(box string(16));
    if got {
      n = n + 1;
      print_lit("Hello, ");
      let name = print_string(name);
      print_lit("!\n")
    } else { more = false }
  };
  if n == 0 { print_lit("Hello, world!\n") }
-- output: Hello, world!

next_arg appends the next argument to the string it’s given, and answers it with whether there was one. The arguments come after the file name:

$ bcc run hello.bc Ada Grace
Hello, Ada!
Hello, Grace!
()
live 0

In the interpreter they come after --: bcc interp hello.bc -- Ada Grace.

Update a record’s Vec through &mut

include textio

data Scores = { best: i32, history: Vec[i32] }

-- Records one score: appended to `history`, and kept as `best` if it is the highest yet.
fn record<'a>(s: mut 'a Scores, x: i32) -> 1 =
  modify s { sc ->
    match sc {
      Scores { best, history } ->
        push(&mut history, x);
        let best = if x > best { x } else { best };
        Scores { best, history }
    } }

fn print_all<'a>(v: array_imm 'a i32) -> 1 =
  for p in v { print_byte(b' '); print_dec(*p) }

main -> 1 =
  let mut s = Scores { best: 0, history: box buf[i32](4) };
  record(&mut s, 12);
  record(&mut s, 40);
  record(&mut s, 27);
  let best = s.best;
  print_lit("best ");
  print_dec(best);
  print_lit(", history:");
  match s { Scores { best, history } -> borrow imm history { v -> print_all(v) } };
  print_byte(b'\n')
-- output: best 40, history: 12 40 27

record gets the scores lent mut and changes them in place. modify s { sc -> … } takes the record out of the borrow, and the record the block ends with is put back: here the same Vec with one more element pushed, and the new best. A copyable field is read directly, as in let best = s.best;. To read the Vec, main takes the record apart and lends the vector to print_all as a view.

A recursive function over a tree

include textio
include rec

fn max(a: i32, b: i32) -> i32 = if a > b { a } else { b }

data Tree = Leaf | Node(left: Tree, val: i32, right: Tree)

-- `t` with `x` added, smaller values to the left.
fn insert(t: Tree, x: i32) -> Tree =
  match t {
    Leaf -> box Node(Leaf, x, Leaf)
  | box Node(l, v, r) ->
      if x < v { box Node(insert(l, x), v, r) } else { box Node(l, v, insert(r, x)) }
  }

-- The height, read through a borrow.
fn height<'a>(t: imm 'a Tree) -> i32 =
  fold_imm t { Leaf -> 0 | Node(l, v, r) -> 1 + max(l, r) }

-- Prints the values in order, consuming the tree.
fn print_sorted(t: Tree) -> 1 =
  match t {
    Leaf -> ()
  | box Node(l, v, r) -> print_sorted(l); print_byte(b' '); print_dec(v); print_sorted(r)
  }

fn build(xs: array i32) -> Tree =
  let mut t = Leaf;
  for x in xs { t = insert(t, x) };
  t

main -> 1 =
  let t = with on_exhausted(!{ u -> Leaf }) { build(array_new[i32](7, !{ i -> (i * 5) % 7 })) };
  let h = height(&t);
  print_lit("height ");
  print_dec(h);
  print_lit(", sorted:");
  with on_exhausted(!{ u -> () }) { print_sorted(t) };
  print_byte(b'\n')
-- output: height 5, sorted: 0 1 2 3 4 5 6

A binary search tree, built by inserting the values one at a time. insert takes the tree and gives back a new one: the pattern box Node(l, v, r) frees the old node, and box Node(…) allocates its replacement. height only reads the tree, so it takes a borrow and uses fold_imm, which hands each node its subtrees’ heights. print_sorted consumes the tree as it prints it, so nothing is left to free. A function that calls itself needs a handler for running out of depth; on_exhausted gives the value to use if it does (Recursion).

A tree whose children are a Vec

include textio
include rec

-- A directory: the bytes of its own files, and its subdirectories.
data Dir = Dir(size: i32, subdirs: Vec[Dir])

fn files(n: i32) -> Dir = Dir(n, box buf[Dir](0))

fn with_sub(d: Dir, sub: Dir) -> Dir =
  match d { Dir(n, subs) -> push(&mut subs, sub); Dir(n, subs) }

-- The size of `d` and everything under it, and the number of directories; consumes `d`.
fn du(d: Dir) -> i32 * i32 =
  match d {
    Dir(size, subs) ->
      let mut bytes = size;
      let mut dirs = 1;
      let n = len_of(&subs);
      for i in 0..n {
        match pop(&mut subs) {
          Some(sub) -> let (b, k) = du(sub); bytes = bytes + b; dirs = dirs + k
        | None -> ()
        }
      };
      (bytes, dirs)
  }

main -> 1 =
  let src = with_sub(with_sub(files(120), files(300)), files(45));
  let root = with_sub(with_sub(files(10), src), files(2048));
  let (bytes, dirs) = with on_exhausted(!{ u -> (0, 0) }) { du(root) };
  print_dec(bytes);
  print_lit(" bytes in ");
  print_dec(dirs);
  print_lit(" directories\n")
-- output: 2523 bytes in 5 directories

Each directory holds its subdirectories in a Vec[Dir]; the vector’s storage is on the heap already, so no box is needed. du takes a directory apart, pops each subdirectory off its vector, adds up what the recursive call returns, and answers a pair: the bytes and the number of directories. Each directory is freed once it has been counted.

Test I/O with a fake handler

include textio

-- The code under test: it writes to whatever serves `Stdout`.
fn fizzbuzz(n: i32) -> 1 with Stdout =
  for i in 1..n + 1 {
    if i % 15 == 0 { print_lit("FizzBuzz") }
    else if i % 3 == 0 { print_lit("Fizz") }
    else if i % 5 == 0 { print_lit("Buzz") }
    else { print_dec(i) };
    print_byte(b' ')
  }

-- A fake standard output that keeps what is written.
handler collect(mut out: string): Stdout = {
  print_byte(b) = { push_byte(&mut out, b) };
  print_bytes(v) = { push_str(&mut out, v) }
}

-- Runs `fizzbuzz(n)` with its output captured, and compares it with `want`.
fn check(name: lit, n: i32, want: lit) -> 1 =
  let c = alloc(box string(64));
  with collect(&mut c) { fizzbuzz(n) };
  let got = free(c);
  let ok = borrow imm got { t -> eq_lit(t, want) };
  if ok { print_lit("ok   ") } else { print_lit("FAIL ") };
  print_lit(name);
  print_byte(b'\n')

main -> 1 =
  check("first five", 5, "1 2 Fizz 4 Buzz ");
  check("fifteen", 15, "1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz ")
-- output: ok   first five
-- output: ok   fifteen

fizzbuzz writes to Stdout, and its signature says so. In a test, with collect(&mut c) { … } serves Stdout with a handler that appends each byte to a string, instead of the real standard output; fizzbuzz itself doesn’t change. Afterwards the cell c holds everything that was written, and eq_lit compares it with the expected text. A string literal’s type is lit, which is how check takes the name and the expected output. A fake clock or scripted input is written the same way (Serving an effect yourself).

Time something

include textio

fn collatz(n: i32) -> i32 =
  let mut x = n;
  let mut steps = 0;
  while x != 1 {
    if x % 2 == 0 { x = x / 2 } else { x = 3 * x + 1 };
    steps = steps + 1
  };
  steps

-- The number below `n` with the longest Collatz run.
fn longest(n: i32) -> i32 =
  let mut best = 1;
  let mut most = 0;
  let mut i = 1;
  while i < n {
    let s = collatz(i);
    if s > most { best = i; most = s };
    i = i + 1
  };
  best

main -> 1 =
  let t0 = now();
  let x = longest(100000);
  let t1 = now();
  print_dec(x);
  print_lit(" has the longest run; that took ");
  print_dec64((t1 - t0) / 1000000);
  print_lit(" ms\n")
$ bcc run --native collatz.bc
77031 has the longest run; that took 16 ms
()
live 0

now() is a monotonic clock in nanoseconds, as an i64, so the difference of two readings is the time between them, and dividing by 1000000 gives milliseconds. print_dec64 prints an i64. The time you see will differ from run to run.

A generator pipeline

include textio
include adapters

fn range(lo: i32, hi: i32) -> 1 with Yield[i32] =
  let mut i = lo;
  while i < hi { yield(i); i = i + 1 };
  ()

-- Whether `n` is prime, trying the divisors up to its square root.
fn is_prime(n: i32) -> bool =
  let mut ok = true;
  for d in 2..n {
    if d * d > n { break };
    match checked_rem(n, d) { inl r -> if r == 0 { ok = false; break } | inr e -> () }
  };
  ok

fn print_all<'a>(v: array_imm 'a i32) -> 1 =
  for p in v { print_byte(b' '); print_dec(*p) }

main -> 1 =
  let mut found: Vec[i32] = box buf[i32](8);
  for x in range(2, 1000) |> filter(is_prime) |> map(!{ p -> p * p }) |> take(6) {
    push(&mut found, x)
  };
  print_lit("squares of the first six primes:");
  borrow imm found { v -> print_all(v) };
  print_byte(b'\n')
-- output: squares of the first six primes: 4 9 25 49 121 169

range is a generator: it yields each number in turn (Generators). |> passes the value on its left as the first argument of the call on its right, so the pipeline reads in order: the numbers from 2, the primes among them, their squares, the first six. Nothing is computed ahead: take(6) stops the pipeline after the sixth value, so range never gets near 1000. The loop collects the values in a Vec, and they’re printed after it. is_prime uses checked_rem, which answers the remainder as a sum instead of raising on a zero divisor, so it’s a plain function that filter can take.