Examples

These 22 programs are both Singular's tutorial and its test suite. Each one must compile and print exactly the output shown under it. Read them in order: each builds on the ones before.

  1. 01 HelloThe smallest Singular program.
  2. 02 ValuesValues, types and conversions. Every name is written with its type.
  3. 03 FunctionsFunctions, single-expression bodies, and returning more than one value.
  4. 04 Control flowBranches and loops. Braces are always required.
  5. 05 StructsStructs hold data. Methods live in impl blocks.
  6. 06 Enums and matchEnums list every form a value can take. match must handle all of them.
  7. 07 OptionThere is no null. A value that might be missing is an Option.
  8. 08 ListsLists grow and shrink. A list declared with const can't.
  9. 09 MapsMaps store values by key and keep keys in the order they were added.
  10. 10 StringsStrings are UTF-8 text. Building one from pieces uses interpolation.
  11. 11 Access modesThe three ways a function can use what you pass it: view, edit and move.
  12. 12 Moves and copiesPlain values copy by themselves. Everything else is moved or copied on purpose.
  13. 13 SlicesOne function can read a whole list, a whole array, or just part of either.
  14. 14 ErrorsA function that can fail says so in its signature, and every call that can fail is marked.
  15. 15 ClosuresClosures use the same fn syntax as functions. Errors pass straight through them.
  16. 16 Traits and genericsTraits describe what a type can do. Generics let one piece of code work for many types.
  17. 17 Anyany Trait holds values of different types in one collection.
  18. 18 DropValues clean up after themselves when their owner goes away.
  19. 19 ArenaLinked data lives in an arena. Handles point into it safely.
  20. 20 Compile timeWork done while compiling: constants, sized arrays and compile-time switches.
  21. 21 Word countCounts the words in a piece of text and prints the most common ones.
  22. 22 ModulesA program split across two files.

01 Hello

01-hello.sing
//! The smallest Singular program.

fn main() {
    print("Hello, Singular!")
}
Output
Hello, Singular!

02 Values

02-values.sing
//! Values, types and conversions. Every name is written with its type.

fn main() {
    const String name = "Singular"
    const Int year = 2026
    const Float pi = 3.14159
    const Bool ready = true
    const Char letter = 'S'
    print("{name} {year} {pi:.2} {ready} {letter}")

    // Without `const`, a name can change. With `const`, it never does.
    Int lives = 3
    lives -= 1
    print("lives: {lives}")

    // There are no silent conversions: you call the type you want.
    const Int8 small = 100
    const Int bigger = Int(small) * 10
    print(bigger)

    const Float ratio = Float(7) / 2.0
    print("{ratio:.1}")
    print(Int(ratio))

    const Int big_number = 1_000_000
    const Int mask = 0xFF
    print(big_number + mask)
}
Output
Singular 2026 3.14 true S
lives: 2
1000
3.5
3
1000255

03 Functions

03-functions.sing
//! Functions, single-expression bodies, and returning more than one value.

/// A body that is one expression can be written after `=>`.
fn square(Int x) -> Int => x * x

/// A block body always says `return`.
fn average(Float a, Float b) -> Float {
    const Float total = a + b
    return total / 2.0
}

/// Returns two values at once as a tuple.
fn min_max(List[Int] values) -> (Int, Int) {
    Int low = values[0]
    Int high = values[0]
    for Int v in values {
        if v < low {
            low = v
        }
        if v > high {
            high = v
        }
    }
    return (low, high)
}

fn greet(String name) {
    print("hi, {name}")
}

fn main() {
    print(square(7))
    print("{average(3.0, 4.0):.1}")

    const (Int low, Int high) = min_max([5, 2, 9, 1])
    print("low {low}, high {high}")

    const (Int, Int) pair = min_max([10, 20])
    print(pair)
    print(pair.0)

    greet("Ada")
}
Output
49
3.5
low 1, high 9
(10, 20)
10
hi, Ada

04 Control flow

04-control-flow.sing
//! Branches and loops. Braces are always required.

fn classify(Int n) -> String {
    if n < 0 {
        return "negative"
    } else if n == 0 {
        return "zero"
    }
    return "positive"
}

fn main() {
    print(classify(-5))
    print(classify(0))
    print(classify(7))

    // `if` can produce a value. Each branch's last line is its value.
    const Int temperature = 25
    const String feeling = if temperature > 20 { "warm" } else { "cold" }
    print(feeling)

    Int total = 0
    for Int i in 1..=10 {
        if i % 2 == 0 {
            continue
        }
        total += i
    }
    print("sum of odd numbers up to 10: {total}")

    Int n = 1
    while n < 100 {
        n *= 3
    }
    print("first power of 3 over 100: {n}")

    for Int i in 0..10 {
        if i == 3 {
            break
        }
        print(i)
    }

    // Mixing `and` with `or` needs parentheses, so the grouping is never a guess.
    const Int x = 5
    if (x > 0 and x < 10) or x == 100 {
        print("in range")
    }
}
Output
negative
zero
positive
warm
sum of odd numbers up to 10: 25
first power of 3 over 100: 243
0
1
2
in range

05 Structs

05-structs.sing
//! Structs hold data. Methods live in `impl` blocks.

struct Counter {
    String label
    Int value
}

impl Counter {
    /// A static function: there is no `self`. Called as `Counter.new(...)`.
    fn new(move String label) -> Counter {
        return Counter { label: move label, value: 0 }
    }

    /// `edit self` means this method changes the counter.
    fn increment(edit self) {
        self.value += 1
    }

    fn add(edit self, Int amount) {
        self.value += amount
    }

    /// Plain `self` means this method only reads it.
    fn report(self) -> String => "{self.label}: {self.value}"
}

fn main() {
    Counter clicks = Counter.new("clicks")
    clicks.increment()
    clicks.increment()
    clicks.add(5)
    print(clicks.report())
    print(clicks)
}
Output
clicks: 7
Counter { label: "clicks", value: 7 }

06 Enums and match

06-enums-and-match.sing
//! Enums list every form a value can take. `match` must handle all of them.

enum Shape {
    Circle(Float radius)
    Rect(Float width, Float height)
    Dot
}

fn describe(Shape shape) -> String {
    return match shape {
        Circle(Float r) if r > 10.0 => "a big circle"
        Circle(_) => "a circle"
        Rect(Float w, Float h) if w == h => "a square"
        Rect(_, _) => "a rectangle"
        Dot => "a dot"
    }
}

enum Direction {
    North
    East
    South
    West
}

fn is_vertical(Direction direction) -> Bool {
    return match direction {
        North | South => true
        East | West => false
    }
}

fn main() {
    const List[Shape] shapes = [
        Shape.Circle(1.0),
        Shape.Circle(12.0),
        Shape.Rect(2.0, 2.0),
        Shape.Rect(1.0, 3.0),
        Shape.Dot,
    ]
    for Shape shape in shapes {
        print(describe(shape))
    }

    print(is_vertical(Direction.North))
    print(is_vertical(Direction.East))

    // `is` checks one pattern and can name its parts.
    if shapes[2] is Rect(Float w, _) {
        print("the third shape is {w:.1} wide")
    }

    // `match edit` lets an arm change the value in place.
    Shape ball = Shape.Circle(1.0)
    match edit ball {
        Circle(Float r) => {
            r *= 3.0
        }
        _ => {}
    }
    if ball is Circle(Float r) {
        print("the ball's radius is now {r:.1}")
    }
}
Output
a circle
a big circle
a square
a rectangle
a dot
true
false
the third shape is 2.0 wide
the ball's radius is now 3.0

07 Option

07-option.sing
//! There is no null. A value that might be missing is an `Option`.

fn find_index(List[String] names, String target) -> Option[Int] {
    for Int i in 0..names.len() {
        if names[i] == target {
            return Some(i)
        }
    }
    return None
}

fn main() {
    const List[String] names = ["ada", "grace", "linus"]

    const Option[Int] found = find_index(names, "grace")
    print(found)
    print(find_index(names, "bob"))

    if found is Some(Int i) {
        print("grace is at position {i}")
    }

    // Option[Int] is plain, so calling a `move self` method just copies it.
    const Option[Int] missing = find_index(names, "bob")
    print(missing.unwrap_or(-1))
    print("found something: {found.is_some()}")

    // `pop` returns a new value each time, so `job` owns what it gets.
    List[String] queue = ["first", "second", "third"]
    while queue.pop() is Some(String job) {
        print("doing {job}")
    }
}
Output
Some(1)
None
grace is at position 1
-1
found something: true
doing third
doing second
doing first

08 Lists

08-lists.sing
//! Lists grow and shrink. A list declared with `const` can't.

fn main() {
    List[String] names = ["ada", "grace"]
    names.push("linus")
    print(names)
    print("count: {names.len()}")
    print("second: {names[1]}")

    for String name in names {
        print("hello, {name}")
    }

    List[Int] numbers = [3, 1, 2]
    numbers.sort()
    print(numbers)

    // `edit` on the loop source lets each element be changed in place.
    for Int n in edit numbers {
        n *= 10
    }
    print(numbers)

    const Option[Int] last = numbers.pop()
    print("popped: {last}")
    print(numbers)
    print("has 10: {numbers.contains(10)}")
}
Output
["ada", "grace", "linus"]
count: 3
second: grace
hello, ada
hello, grace
hello, linus
[1, 2, 3]
[10, 20, 30]
popped: Some(30)
[10, 20]
has 10: true

09 Maps

09-maps.sing
//! Maps store values by key and keep keys in the order they were added.

fn main() {
    Map[String, Int] stock = ["apples": 3, "pears": 0]
    stock.insert("plums", 7)
    print(stock)

    // Indexing works for keys that exist. New keys are added with `insert`.
    stock["apples"] += 2
    const Int apples = stock["apples"]
    print("apples: {apples}")

    const String key = "pears"
    const Option[Int] removed = stock.remove(key)
    print("removed {key}: {removed}")
    print("still has {key}: {stock.contains(key)}")

    for (String fruit, Int count) in stock {
        print("{fruit}: {count}")
    }

    // With `edit`, values can change. Keys never can.
    for (_, Int count) in edit stock {
        count *= 2
    }
    print(stock)

    print(stock.get("kiwis"))
}
Output
["apples": 3, "pears": 0, "plums": 7]
apples: 5
removed pears: Some(0)
still has pears: false
apples: 5
plums: 7
["apples": 10, "plums": 14]
None

10 Strings

10-strings.sing
//! Strings are UTF-8 text. Building one from pieces uses interpolation.

fn main() {
    const String greeting = "Hello, Singular"
    print(greeting.len())
    print(greeting.to_upper())
    print(greeting.slice(7, 15))
    print(greeting.contains("Sing"))
    print(greeting.starts_with("Hell"))

    Int vowels = 0
    for Char c in greeting {
        if c == 'a' or c == 'e' or c == 'i' or c == 'o' or c == 'u' {
            vowels += 1
        }
    }
    print("vowels: {vowels}")

    const List[String] colors = "red,green,blue".split(",")
    print(colors)
    print(colors.join(" + "))

    String shout = "hey"
    shout.append("!!")
    shout.push('?')
    print(shout)

    // Raw strings have no escapes and no interpolation.
    const String path = r"C:\games\{save}.dat"
    print(path)
    print("braces: {{like this}}")
}
Output
15
HELLO, SINGULAR
Singular
true
true
vowels: 5
["red", "green", "blue"]
red + green + blue
hey!!?
C:\games\{save}.dat
braces: {like this}

11 Access modes

11-access-modes.sing
//! The three ways a function can use what you pass it: view, edit and move.

struct Item {
    String name
    Float price
}

/// Only reads the items. The caller's list is untouched.
fn total(List[Item] items) -> Float {
    Float sum = 0.0
    for Item item in items {
        sum += item.price
    }
    return sum
}

/// Changes the items in place. Callers must write `edit`.
fn discount(edit List[Item] items, Float percent) {
    for Item item in edit items {
        item.price *= 1.0 - percent / 100.0
    }
}

/// Takes the list for good. Callers must write `move`.
fn archive(move List[Item] items) -> Int {
    return items.len()
}

fn main() {
    List[Item] cart = [
        Item { name: "book", price: 20.0 },
        Item { name: "pen", price: 5.0 },
    ]
    print("total: {total(cart):.2}")

    discount(edit cart, 10.0)
    print("after discount: {total(cart):.2}")

    // Names for parts of a value, with no copying.
    // A `view` is read-only, so it's declared `const`.
    const Item first = view cart[0]
    print("first item: {first.name}")

    Item second = edit cart[1]
    second.price = 1.0
    print("pen now costs {cart[1].price:.2}")

    // After this line, `cart` is gone.
    const Int count = archive(move cart)
    print("archived {count} items")
}
Output
total: 25.00
after discount: 22.50
first item: book
pen now costs 1.00
archived 2 items

12 Moves and copies

12-moves-and-copies.sing
//! Plain values copy by themselves. Everything else is moved or copied on purpose.

struct Point {
    Int x
    Int y
}

struct Player {
    String name
    Point position
}

fn make_player(String name) -> Player {
    const Player player = Player { name: name.copy(), position: Point { x: 0, y: 0 } }
    // `return` hands the value to the caller. No `move` is needed.
    return player
}

fn main() {
    // Point holds only numbers, so it's plain: `Point b = a` copies it.
    const Point a = Point { x: 1, y: 2 }
    Point b = a
    b.x = 10
    print("a: {a}, b: {b}")

    // Player holds a String, so it isn't plain. You say what you want.
    const String name = "Ada"
    const Player hero = Player { name: move name, position: a }
    // Using `name` from here on would be error E0401: it was moved into `hero`.

    Player twin = hero.copy()
    twin.name = "Grace"
    print("{hero.name} and {twin.name}")

    // A `const` value can still be moved. Being moved isn't being changed.
    List[Player] team = []
    team.push(move hero)
    team.push(move twin)
    print("team size: {team.len()}")

    const Player guest = make_player("Linus")
    print("guest: {guest.name} at {guest.position}")
}
Output
a: Point { x: 1, y: 2 }, b: Point { x: 10, y: 2 }
Ada and Grace
team size: 2
guest: Linus at Point { x: 0, y: 0 }

13 Slices

13-slices.sing
//! One function can read a whole list, a whole array, or just part of either.

fn sum(Slice[Int] values) -> Int {
    Int total = 0
    for Int v in values {
        total += v
    }
    return total
}

/// An `edit` slice can change elements, but can't add or remove them.
fn zero_out(edit Slice[Int] values) {
    for Int v in edit values {
        v = 0
    }
}

fn main() {
    List[Int] scores = [4, 8, 15, 16, 23, 42]
    print("all: {sum(scores)}")
    print("first three: {sum(scores[0..3])}")
    print("last two: {sum(scores[4..6])}")

    const Array[Int, 3] fixed = [1, 1, 1]
    print("array: {sum(fixed)}")

    zero_out(edit scores[0..2])
    print(scores)
}
Output
all: 108
first three: 27
last two: 65
array: 3
[0, 0, 15, 16, 23, 42]

14 Errors

14-errors.sing
//! A function that can fail says so in its signature, and every call that can fail is marked.

enum ConfigError {
    Missing(String)
    BadNumber(ParseIntError)
}

/// Reads the port number from the settings.
///
/// # Errors
/// - `Missing` if there is no "port" entry.
/// - `BadNumber` if the entry isn't a whole number.
fn read_port(Map[String, String] settings) -> Int raises ConfigError {
    const String key = "port"
    if not settings.contains(key) {
        raise ConfigError.Missing("port")
    }
    // `catch` handles the error here, converting it to this function's error type.
    const Int port = Int.parse(settings[key]) catch ParseIntError e {
        raise ConfigError.BadNumber(e)
    }
    return port
}

/// `try` passes any error straight up to whoever called this function.
fn describe(Map[String, String] settings) -> String raises ConfigError {
    const Int port = try read_port(settings)
    return "listening on port {port}"
}

fn main() {
    const Map[String, String] good = ["host": "localhost", "port": "8080"]
    const Map[String, String] typo = ["host": "localhost", "port": "80x0"]
    const Map[String, String] empty = [:]

    for Map[String, String] settings in [move good, move typo, move empty] {
        const String message = describe(settings) catch ConfigError e {
            "config problem: {e}"
        }
        print(message)
    }
}
Output
listening on port 8080
config problem: BadNumber(InvalidDigit('x'))
config problem: Missing("port")

15 Closures

15-closures.sing
//! Closures use the same `fn` syntax as functions. Errors pass straight through them.

fn apply_twice(fn(Int) -> Int f, Int x) -> Int => f(f(x))

fn repeat(Int times, fn() action) {
    for _ in 0..times {
        action()
    }
}

/// `map` has no "fallible" twin. If the function you pass can fail, so does the call.
fn parse_all(List[String] texts) -> List[Int] raises ParseIntError {
    return try texts.map(Int.parse)
}

fn main() {
    const List[Int] numbers = [1, 2, 3, 4]
    const List[Int] doubled = numbers.map(fn(Int n) => n * 2)
    print(doubled)

    // A closure can read variables around it.
    const Int limit = 2
    const List[Int] big = numbers.filter(fn(Int n) => n > limit)
    print(big)

    print(apply_twice(fn(Int n) => n + 10, 1))

    // It can also change them while it runs. Here `count` is edited.
    Int count = 0
    repeat(3, fn() { count += 1 })
    print("count: {count}")

    const List[Int] good = parse_all(["4", "5", "6"]) catch _ { [] }
    print(good)

    const List[Int] bad = parse_all(["7", "x"]) catch ParseIntError e {
        print("could not parse: {e}")
        []
    }
    print(bad)
}
Output
[2, 4, 6, 8]
[3, 4]
21
count: 3
[4, 5, 6]
could not parse: InvalidDigit('x')
[]

16 Traits and generics

16-traits-and-generics.sing
//! Traits describe what a type can do. Generics let one piece of code work for many types.

trait Describe {
    fn label(self) -> String

    /// A default body. Implementations get it for free and can replace it.
    fn describe(self) -> String => "this is {self.label()}"
}

struct Dog {
    String name
}

impl Describe for Dog {
    fn label(self) -> String => self.name.copy()
}

/// Works for any type that can be ordered and copied.
fn largest[T: Ord + Copy](Slice[T] values) -> T {
    T best = values[0].copy()
    for T v in values {
        if v > best {
            best = v.copy()
        }
    }
    return best
}

struct Stack[T] {
    List[T] items
}

impl[T] Stack[T] {
    fn new() -> Stack[T] => Stack { items: [] }

    fn push(edit self, move T item) {
        self.items.push(move item)
    }

    fn pop(edit self) -> Option[T] => self.items.pop()

    fn size(self) -> Int => self.items.len()
}

fn main() {
    const Dog rex = Dog { name: "Rex" }
    print(rex.describe())

    print(largest([3, 9, 4]))
    const List[String] fruits = ["pear", "apple", "fig"]
    print(largest(fruits))

    Stack[String] stack = Stack.new()
    stack.push("first")
    stack.push("second")
    print("size: {stack.size()}")
    while stack.pop() is Some(String item) {
        print("popped {item}")
    }
}
Output
this is Rex
9
pear
size: 2
popped second
popped first

17 Any

17-any.sing
//! `any Trait` holds values of different types in one collection.

use std.math

trait Shape {
    fn area(self) -> Float
    fn label(self) -> String
}

struct Circle {
    Float radius
}

impl Shape for Circle {
    fn area(self) -> Float => math.pi * self.radius * self.radius
    fn label(self) -> String => "circle"
}

struct Square {
    Float side
}

impl Shape for Square {
    fn area(self) -> Float => self.side * self.side
    fn label(self) -> String => "square"
}

fn total_area(Slice[any Shape] shapes) -> Float {
    Float total = 0.0
    for any Shape s in shapes {
        total += s.area()
    }
    return total
}

fn main() {
    const List[any Shape] shapes = [
        Circle { radius: 1.0 },
        Square { side: 2.0 },
        Circle { radius: 0.5 },
    ]
    for any Shape s in shapes {
        print("{s.label()}: {s.area():.2}")
    }
    print("total: {total_area(shapes):.2}")
}
Output
circle: 3.14
square: 4.00
circle: 0.79
total: 7.93

18 Drop

18-drop.sing
//! Values clean up after themselves when their owner goes away.

struct Logger {
    String name
}

impl Drop for Logger {
    fn drop(edit self) {
        print("closing {self.name}")
    }
}

fn main() {
    const Logger a = Logger { name: "a" }
    const Logger b = Logger { name: "b" }
    const Logger c = Logger { name: "c" }
    print("working")

    drop(move a)
    print("a was closed early")

    // At the end of the block, the rest drop in reverse order: c, then b.
}
Output
working
closing a
a was closed early
closing c
closing b

19 Arena

19-arena.sing
//! Linked data lives in an arena. Handles point into it safely.

struct Monster {
    String name
    Int hp
}

fn main() {
    Arena[Monster] monsters = Arena.new()
    const Handle[Monster] slime = monsters.add(Monster { name: "slime", hp: 10 })
    const Handle[Monster] bat = monsters.add(Monster { name: "bat", hp: 4 })

    // A handle is plain, so it can be copied and stored anywhere.
    const Handle[Monster] target = bat
    monsters[target].hp -= 4

    if monsters[target].hp <= 0 {
        const Monster defeated = monsters.remove(target)
        print("{defeated.name} was defeated")
    }

    // The bat's handle is now stale. Checking it is safe; using it would panic.
    print("bat still here: {monsters.contains(bat)}")

    monsters[slime].hp += 5
    for (_, Monster monster) in monsters {
        print("{monster.name} has {monster.hp} hp")
    }
}
Output
bat was defeated
bat still here: false
slime has 15 hp

20 Compile time

20-compile-time.sing
//! Work done while compiling: constants, sized arrays and compile-time switches.

/// Turn this on to print extra detail. Code behind a false switch isn't compiled.
comptime Bool verbose = false

/// The squares of 0 through 7, computed by the compiler. The program only reads them.
comptime Array[Int, 8] squares = make_squares()

fn make_squares() -> Array[Int, 8] {
    Array[Int, 8] out = Array[Int, 8].filled(0)
    for Int i in 0..8 {
        out[i] = i * i
    }
    return out
}

/// Works for an array of any length. `N` is known at compile time.
fn sum[comptime Int N](Array[Int, N] values) -> Int {
    Int total = 0
    for Int v in values {
        total += v
    }
    return total
}

/// A value parameter can be used like any other value.
fn length[comptime Int N](Array[Int, N] values) -> Int => N

fn main() {
    print(squares)
    print("squares holds {length(squares)} numbers")
    print("sum of squares: {sum(squares)}")

    const Array[Int, 3] small = [1, 2, 3]
    print("sum of small: {sum(small)}")

    comptime if verbose {
        print("verbose mode is on")
    }
}
Output
[0, 1, 4, 9, 16, 25, 36, 49]
squares holds 8 numbers
sum of squares: 140
sum of small: 6

21 Word count

21-word-count.sing
//! Counts the words in a piece of text and prints the most common ones.

comptime String sample = "the cat and the hat and the bat"

/// One word and how many times it appeared.
pub struct WordCount {
    pub String word
    pub Int count
}

/// Splits `text` on spaces and counts how often each word appears.
///
/// Words are compared in lowercase, so "The" and "the" count together.
///
/// ```sing
/// const Map[String, Int] counts = count_words("a b A")
/// assert_eq(counts["a"], 2)
/// ```
pub fn count_words(String text) -> Map[String, Int] {
    Map[String, Int] counts = [:]
    for String word in text.split(" ") {
        if word.is_empty() {
            continue
        }
        const String key = word.to_lower()
        if counts.contains(key) {
            counts[key] += 1
        } else {
            counts.insert(move key, 1)
        }
    }
    return counts
}

/// Returns the `limit` most common words, most frequent first.
/// Words with the same count keep the order they first appeared in.
pub fn top_words(Map[String, Int] counts, Int limit) -> List[WordCount] {
    List[WordCount] ranked = []
    for (String word, Int count) in counts {
        ranked.push(WordCount { word: word.copy(), count: count })
    }
    ranked.sort_by(fn(WordCount a, WordCount b) => b.count.compare(a.count))
    ranked.truncate(limit)
    return ranked
}

fn main() {
    const Map[String, Int] counts = count_words(sample)
    print("{counts.len()} different words")
    for WordCount entry in top_words(counts, 3) {
        print("{entry.word}: {entry.count}")
    }
}

test "empty text has no words" {
    assert_eq(count_words("").len(), 0)
}

test "counting ignores case" {
    const Map[String, Int] counts = count_words("Go go GO")
    assert_eq(counts["go"], 3)
}
Output
5 different words
the: 3
and: 2
cat: 1

22 Modules

22-modules/geometry.sing
//! Shapes and measurements. Only items marked `pub` can be used from other files.

pub struct Rect {
    pub Int width
    pub Int height
}

pub fn area(Rect r) -> Int => multiply(r.width, r.height)

/// Private: only this file can call it.
fn multiply(Int a, Int b) -> Int => a * b
22-modules/main.sing
//! A program split across two files.

// `use geometry` makes the module name available: geometry.area(...)
use geometry
// `use geometry.{Rect}` brings in one item by its bare name.
use geometry.{Rect}

fn main() {
    const Rect r = Rect { width: 3, height: 4 }
    print(geometry.area(r))
}
Output
12