Goose

05_shapes.goose

// Algebraic data types, the only dynamic polymorphism in the language: no
// inheritance, no vtables. The classic shapes example, with the two things
// that make Goose's version different -- an enum value can be stored
// *fixed* (every value the size of the largest variant, indexable and
// overwritable) or *variable* (each value exactly its variant's size, text
// included, walked sequentially or reached through an index kept beside the
// array) -- and case functions, which are `match` written as an overload
// set: the virtual-call idiom.
//
// Build and run (from this directory):
//   goose -o shapes.c 05_shapes.goose && cl shapes.c && shapes

import math;

enum Shape {
    Circle { r: f64 },
    Rect { w: f64, h: f64 },
    Tri { a: f64, b: f64, c: f64 },
    Dot,
}

// Case functions: one overload per variant, called with the enum value.
// The call dispatches on the tag; the checker requires every variant to
// have exactly one applicable overload, so a new variant is a compile error
// here until it gets its case.
fn area(s: Shape.Circle) -> f64 { PI * s.r * s.r }
fn area(s: Shape.Rect) -> f64 { s.w * s.h }
fn area(s: Shape.Tri) -> f64 {
    let p = (s.a + s.b + s.c) / 2.0;     // Heron's formula
    sqrt(p * (p - s.a) * (p - s.b) * (p - s.c))
}
fn area(s: Shape.Dot) -> f64 { 0.0 }

fn sides(s: Shape.Circle) -> i64 { 0 }
fn sides(s: Shape.Rect) -> i64 { 4 }
fn sides(s: Shape.Tri) -> i64 { 3 }
fn sides(s: Shape.Dot) -> i64 { 0 }

// `match` is the inline form; `_` covers the rest.
fn name(s: Shape) -> u8[:] {
    match s {
        Circle c => "circle",
        Rect r => if r.w == r.h { "square" } else { "rectangle" },
        _ => "other",
    }
}

// Scaling a fixed-mode value in place: the whole value is replaced, which
// is allowed exactly because nothing can point inside a fixed-mode payload.
fn scaled(s: Shape, k: f64) -> Shape {
    match s {
        Circle c => Shape.Circle { r: c.r * k },
        Rect r => Shape.Rect { w: r.w * k, h: r.h * k },
        Tri t => Shape.Tri { a: t.a * k, b: t.b * k, c: t.c * k },
        Dot => Shape.Dot,
    }
}

// A drawing's marks: shapes, and labels with text. A Label holds its text
// itself, a varint length and then the bytes, right after the tag, so the
// marks in a variable-mode array are one flat run of bytes, strings
// included, with no pointer anywhere. (An enum in Rust or C++ has one fixed
// size: its string variant would be a fixed-size string object, and every
// element would be as big as the largest variant.) The text makes Mark a
// variable-size type, so it is only ever stored in variable mode.
enum Mark {
    Circle { r: f64 },
    Rect { w: f64, h: f64 },
    Label { text: u8[varint] },
    Dot,
}

// Packed marks have no fixed stride, so `marks` cannot be indexed, but an
// index beside it can be: each entry is one mark's offset from the base of
// `marks` (`in marks`, 4 bytes), and loading one is an add. The array such
// offsets are measured from is a global; a local array can be indexed the
// same way with plain 8-byte references, `(Mark..&)[>..]`.
var marks: Mark..[>..] = [];
var mark_at: (Mark..&<u32 in marks>)[>..] = [];

fn main() {
    // --- fixed mode: an ordinary array of enum values ----------------------
    // Each element is 1 tag byte + the largest payload (three f64s): 25 bytes,
    // the layout a Rust enum or a C++ variant also has; `match` reads the
    // payload in place, with no boxing and no virtual call.
    var shapes: Shape[>..] = [];
    shapes.push(Shape.Circle { r: 1.0 });
    shapes.push(Shape.Rect { w: 2.0, h: 3.0 });
    shapes.push(Shape.Tri { a: 3.0, b: 4.0, c: 5.0 });
    shapes.push(Shape.Dot);
    shapes.push(Shape.Rect { w: 2.0, h: 2.0 });
    var total = 0.0;
    for s in shapes {
        print(name(s), " ", s, " area ", area(s), " sides ", sides(s));
        total += area(s);
    }
    print("total area ", total);
    // Indexable and replaceable: element 3 becomes a different variant.
    shapes[3] = Shape.Circle { r: 0.5 };
    for &s in shapes { s = scaled(s, 2.0); }
    print(shapes);

    // --- variable mode: each element as big as its own variant -------------
    // A Dot is 1 byte, a Circle 9, a Rect 17: half the memory of the fixed
    // array for this mix. The price is sequential access: no indexing (short
    // of keeping an index, below), and an element can never change variant,
    // since its neighbors are packed right behind it. In exchange, references
    // into a payload are allowed.
    var packed: Shape..[>..] = [];
    packed.push(Shape.Circle { r: 1.0 });
    packed.push(Shape.Dot);
    packed.push(Shape.Rect { w: 2.0, h: 3.0 });
    packed.push(Shape.Dot);
    packed.push(Shape.Tri { a: 3.0, b: 4.0, c: 5.0 });
    var dots = 0;
    var widest = 0.0;
    for s in packed {                    // variable-size elements bind by reference
        match s {
            Dot => block { dots++; },
            Rect &r => block { r.w += 1.0; widest = r.w; },   // edits the payload in place, inside the array
            _ => block {},
        }
        print(name(s), " ", area(s));
    }
    print(dots, " dots, widest rect now ", widest);
    print(packed);

    // --- variable mode with an index: random access ------------------------
    // push builds each mark in place at the end of `marks` and returns a
    // reference to it, which the index keeps as that mark's offset.
    mark_at.push(marks.push(Mark.Label { text: "hello, goose" }));
    mark_at.push(marks.push(Mark.Circle { r: 1.5 }));
    mark_at.push(marks.push(Mark.Dot));
    mark_at.push(marks.push(Mark.Rect { w: 2.0, h: 3.0 }));
    mark_at.push(marks.push(Mark.Label { text: "x" }));
    for i in [3, 0, 4, 1, 2] {           // any order; each lookup is one add
        match mark_at[i] {
            Circle c => print(i, ": circle ", c.r),
            Rect r => print(i, ": rect ", r.w, " by ", r.h),
            Label l => print(i, ": label \"", l.text, "\""),
            Dot => print(i, ": dot"),
        }
    }
    // A Label is its tag, a 1-byte length and its text: 14 + 9 + 1 + 17 + 3.
    print(marks.len, " marks in ", bytes_of(marks).len, " bytes");
}