Introduction

Let's start with a simple "Hello World" program.

io.println("Hello, world!")

Save this to a file named hello.bs and run it as follows.

$ bs hello.bs
Hello, world!

Comments

// Single Line Comment

/*
    Multi
    Line
    Comments
*/

/*
    Nested

    /*
        Multi
        Line
    */

    Comments
*/

Basic Types

Nil

io.println(nil) // Output: nil

Numbers

BS supports 64-bit floating point numbers only.

io.println(69)     // Output: 69
io.println(420.69) // Output: 420.69
io.println(0xff)   // Output: 255

Standard arithmetic as well as bitwise operations are supported.

io.println(34 + 35)        // Output: 69
io.println(500 - 80)       // Output: 420
io.println(23 * 3)         // Output: 69
io.println(840 / 2)        // Output: 420
io.println(209 % 70)       // Output: 69
io.println(-420)           // Output: -420

io.println(17 * 2 + 35)    // Output: 69
io.println((900 - 60) / 2) // Output: 420

io.println(276 >> 2)       // Output: 69
io.println(105 << 2)       // Output: 420
io.println(~-70)           // Output: 69
io.println(160 | 260)      // Output: 420
io.println(77 & 103)       // Output: 69
io.println(~-419 + 10 & 3) // Output: 420
io.println(69 ^ 1404)      // Output: 1337

Booleans

io.println(true)  // Output: true
io.println(false) // Output: false

Standard logical operations are supported.

io.println(!true)          // Output: false
io.println(!false)         // Output: true
io.println(!nil)           // Output: true

// All numbers are considered "true"
io.println(!0)             // Output: false
io.println(!1)             // Output: false

// Ordering
io.println(69 > 420)       // Output: false
io.println(69 >= 420)      // Output: false
io.println(69 < 420)       // Output: true
io.println(69 <= 420)      // Output: true
io.println(69 == 420)      // Output: false
io.println(69 != 420)      // Output: true

// BS is strongly typed
io.println(nil == nil)     // Output: true
io.println(nil == true)    // Output: false
io.println(nil == 0)       // Output: false
io.println(false == 0)     // Output: false

// Logical AND, OR
io.println(true && true)   // Output: true
io.println(true && false)  // Output: false
io.println(true || false)  // Output: true
io.println(false || false) // Output: false

// Short circuiting
io.println(nil && true)    // Output: nil
io.println(0 || false)     // Output: 0

Strings

// Output: Hello!
io.println("Hello!")

// Output:
// Say "Hello"!
// Here, a	 tab!
io.println("Say \"Hello\"!\nHere, a\t tab!")

// Output: Interpolation! 69
io.println("Interpolation! {34 + 35}")

// Output: Nested interpolation! 420
io.println("Nested {"interpolation! {420}"}")

// Output: A literal '{'
io.println("A literal '\{'")

// Output: 6
io.println(len("Hello!"))

// Output: b
io.println("foobar"[3])

// Output: Let's go!
io.println('Let\'s go!')             // Single quotes can also be used

// Check if substring exists in a string
io.println("bar" in "foobar")        // Output: true
io.println("something" in "foobar")  // Output: false

// Check if substring doesn't exist in a string
io.println("bar" !in "foobar")       // Output: false
io.println("something" !in "foobar") // Output: true

($)

// Unary ($) converts any value to a string
io.println(len($(21 * 20)))            // Output: 3

// Binary ($) performs string concatenation
io.println("Hello, " $ "world! " $ 69) // Output: Hello, world! 69

Q. Why ($) of all things? Couldn't you have chosen a more sensible operator?
A. In my defense, I am just a silly goose.

Raw Strings

io.println({{
    This is a raw string literal.

    The indentation of the first non empty line is considered zero.

    Escape characters are NOT escaped: \n\t\r.

    The first and last newline surrounding the braces are trimmed off.
}})
$ bs string.bs
This is a raw string literal.

The indentation of the first non empty line is considered zero.

Escape characters are NOT escaped: \n\t\r.

The first and last newline surrounding the braces are trimmed off.

The number of starting { can be two or more, and the ending must contain the same number of }

io.println({{{
    io.println({{
        Yoho!!
    }})
}}})
$ bs string.bs
io.println({{
    Yoho!!
}})

Arrays

// Variables will be introduced later
var array = [69, 420]

// Pretty printing by default!
io.println(array)                 // Output: [69, 420]

// Array access
io.println(array[0])              // Output: 69

// Array access out of bounds is an error
io.println(array[2])              // Error!

// Array assignment
array[1] = "nice!"
io.println(array)                 // Output: [69, "nice!"]

// Array assignment out of bounds is NOT an error
array[3] = "Are you serious?"
io.println(array)                 // Output: [69, "nice!", nil, "Are you serious?"]

// Array length
io.println(len(array))            // Output: 4

// Due to the assignment semantics, appending to arrays is quite easy
array[len(array)] = 420
io.println(array)                 // Output: [69, "nice!", nil, "Are you serious?", 420]

// Of course, you can also use the push() method of arrays
array.push(1337)

// Output:
// [
//     69,
//     "nice!",
//     nil,
//     "Are you serious?",
//     420,
//     1337
// ]
io.println(array)

// Delete value at index
io.println(delete(array[2]))      // Output: nil
io.println(array)                 // Output: [69, "nice!", "Are you serious?", 420, 1337]

// Deletion of index out of range is an error
io.println(delete(array[69]))     // Error!

// Check if value exists in an array
io.println("nice!" in array)      // Output: true
io.println("something" in array)  // Output: false

// Check if value doesn't exist in an array
io.println("nice!" !in array)     // Output: false
io.println("something" !in array) // Output: true

// Arrays are compared by value
var xs = [1, 2, 3]
var ys = [1, 2, 3]
var zs = [1, 2, 3, 4]
io.println(xs == ys)              // Output: true
io.println(xs == zs)              // Output: false

// Clone an array using the (..) operator
var as = [1, 2, 3]
var bs = [..as]

bs[0] = 69
io.println(as)                    // Output: [1, 2, 3]
io.println(bs)                    // Output: [69, 2, 3]

Tables

var table = {
    foo = 69,
    [34 + 35] = 420
}

// Output:
// {
//     foo = 69,
//     [69] = 420
// }
io.println(table)

// Key access
io.println(table.foo)             // Output: 69
io.println(table[69])             // Output: 420

// Key assignment
table.key = "value"
table["bar" $ 69] = "eh"

// Output:
// {
//     bar69 = "eh",
//     foo = 69,
//     key = "value",
//     [69] = 420
// }
io.println(table)

// Undefined key access is an error
io.println(table.something)       // Error!

// Check if key exists in a table
io.println("foo" in table)        // Output: true
io.println("something" in table)  // Output: false

// Check if key doesn't exist in a table
io.println("foo" !in table)       // Output: false
io.println("something" !in table) // Output: true

// Table length
io.println(len(table))            // Output: 4

// Delete keys
io.println(delete(table[69]))     // Output: 420

// Output:
// {
//     bar69 = "eh",
//     foo = 69,
//     key = "value",
// }
io.println(table)

// Deletion of non existent key is an error
io.println(delete(table.wrong))   // Error!

// Tables are compared by value
var xs = {a = 1, b = 2}
var ys = {a = 1, b = 2}
var zs = {a = 1, b = 2, c = 3}
io.println(xs == ys)              // Output: true
io.println(xs == zs)              // Output: false

// Clone a table using the (..) operator
var as = {a = 1, b = 2}
var bs = {..as}

bs.a = 69

// Output:
// {
//     a = 1,
//     b = 2
// }
io.println(as)

// Output:
// {
//     a = 69,
//     b = 2
// }
io.println(bs)

Typeof

io.println(typeof(nil))      // Output: nil
io.println(typeof(true))     // Output: boolean
io.println(typeof(69))       // Output: number
io.println(typeof("deez"))   // Output: string
io.println(typeof([]))       // Output: array
io.println(typeof({}))       // Output: table

// Functions will be introduced later
io.println(typeof(fn () {})) // Output: function

Is

io.println(nil is "nil")           // Output: true
io.println(true is "boolean")      // Output: true
io.println(69 is "number")         // Output: true
io.println("deez" is "string")     // Output: true
io.println([] is "array")          // Output: true
io.println({} is "table")          // Output: true
io.println(fn () {} is "function") // Output: true

io.println(420 is "nil")           // Output: false
io.println(420 !is "nil")          // Output: true

Semicolons

Semicolons can be used to mark the end of an expression optionally. This is not necessary though, since BS performs automatic semicolon insertion based on newlines similarly to Go.

// Output:
// 69
// 420
io.println(69); io.println(420)

This does mean, however, that placement of binary operators matter.

// -> 100 - 31;
100 - 31

// -> 100 - 31;
100 -
31

// -> 100; -31;
100
- 31

Basically binary operators cannot start on a newline. If you wish to split an expression across multiple lines, the operators have to kept on the same line if you wish the next line to be part of that expression.

The field access operator (.) is the only exception to this rule.

// Considered part of the same expression, even though the binary operator (.)
// starts on a new line.
something
    .foo
    .bar(deez, nuts)
    .baz

// The above expression is equivalent to this.
something.foo.bar(deez, nuts).baz

Conditions

If Statement

var age = 18

if age >= 18 {
    io.println("You are an adult")
} else {
    io.println("Stay away from Drake")
}

if age == 18 {
    io.println("Get a life")
}

if age != 18 {
    io.println("Go to school")
}
$ bs conditions.bs
You are an adult
Get a life

If Expression

var age = 17
io.println(if age >= 18 then "Adult" else "Minor")
$ bs conditions.bs
Minor

Match Statement

// Output: A
match 69 {
    69 -> io.println("A")
    420 -> {
        io.println("B")
    }
}

// Output: B
match 420 {
    69 -> io.println("A")
    420 -> {
        io.println("B")
    }
}

match 1337 {
    69 -> io.println("A")
    420 -> {
        io.println("B")
    }

    // No output in this case, since none of the cases match
}

// Output: C
match 1337 {
    69 -> io.println("A")
    420 -> {
        io.println("B")
    }
} else {
    // Executed when none of the cases match
    io.println("C")
}

// Output: C
match 42 {
    // Multiple cases for the same branch
    0, 1 -> io.println("A")
    69, 420, 1337 -> {
        io.println("B")
    }
    42 -> io.println("C")
} else {
    io.println("D")
}

// Output: A
match 0 {} else {
    // Why would you do this though? :/
    io.println("A")
}

var x = "foo"
var y = "bar"

// Output:
// Side effect!
// x
match "foobar".slice(0, 3) { // All values can be matched
    y -> io.println("y")

    // Expressions are allowed
    22 + 10 -> io.println("Deez")

    // Arbritary runtime code in general is allowed
    os.clock() -> panic("What?")

    // And you thought JS was bad
    (fn () {
        // 1. Functions will be described later
        // 2. The order of operations matter. If a matching case was encountered
        //    before this, then this side effect would not have occured
        io.println("Side effect!")
    })() -> {
        io.println("Why?")
    }

    x -> io.println("x")
}

// Output: D
var z = "foobar"
match z {
    "nice" -> io.println("A")
    "hehe" -> io.println("B")

    // You can check arbitrary conditions
    if 69 == 420, if z.prefix("bar") -> io.println("C")
    if z.prefix("foo") -> io.println("D")
} else {
    io.println("E")
}

Loops

While Loop

var i = 0
while i < 5 {
    io.println(i)
    i = i + 1
}
$ bs loops.bs
0
1
2
3
4

For Loop

Iterate over range of numbers.

for i in 0..5 {
    io.println(i)
}
$ bs loops.bs
0
1
2
3
4

Iterate over range of numbers with custom step.

for i in 0..5, 2 {
    io.println(i)
}
$ bs loops.bs
0
2
4

Direction of the range iteration is automatically selected.

for i in 5..0 {
    io.println(i)
}
$ bs loops.bs
5
4
3
2
1

The end of the range can be made inclusive.

for i in 0..=5 {
    io.println(i)
}
$ bs loops.bs
0
1
2
3
4
5

Iterate over a string.

var str = "foobar"

for i, c in str {
    io.println("Index: {i} Char: {c}")
}
$ bs loops.bs
Index: 0 Char: f
Index: 1 Char: o
Index: 2 Char: o
Index: 3 Char: b
Index: 4 Char: a
Index: 5 Char: r

Iterate over an array.

var xs = [2, 4, 6, 8, 10]

for i, v in xs {
    io.println("Index: {i} Value: {v}")
}
$ bs loops.bs
Index: 0 Value: 2
Index: 1 Value: 4
Index: 2 Value: 6
Index: 3 Value: 8
Index: 4 Value: 10

Iterate over a table.

var xs = {
    foo = 69,
    bar = 420,
    [42] = 1337
}

for k, v in xs {
    io.println("Key: {k} Value: {v}")
}
$ bs loops.bs
Key: 42 Value: 1337
Key: bar Value: 420
Key: foo Value: 69

Break and Continue

var i = 0
while i < 10 {
    if i == 5 {
        break
    }

    io.println(i)
    i = i + 1
}

io.println()

for i in 0..5 {
    if i == 3 {
        continue
    }
    io.println(i)
}
$ bs loops.bs
0
1
2
3
4

0
1
2
4

Functions

fn greet(name) {
    io.println("Hello, {name}!")
}

greet("world")
$ bs functions.bs
Hello, world!

Return

fn factorial(n) {
    if n < 2 {
        return 1
    }

    return n * factorial(n - 1)
}

io.println(factorial(6))

fn f() {}
fn g() {
    return

    io.println("HERE!")
}

// Functions implicitly return 'nil'
io.println(f())
io.println(g())
$ bs functions.bs
720
nil
nil

First Class Functions

fn add(x, y) {
    return x + y
}

fn combine(f, x, y) {
    return f(x, y)
}

io.println(combine(add, 34, 35))
$ bs functions.bs
69

Anonymous Functions

fn combine(f, x, y) {
    return f(x, y)
}

io.println(combine(fn (x, y) {
    return x + y
}, 34, 35))
$ bs functions.bs
69

Single Expression Function

Functions support a shorthand syntax for a single expression body.

fn combine(f, x, y) -> f(x, y)

io.println(combine(fn (x, y) -> x + y, 34, 35))
$ bs functions.bs
69

Field Functions

var M = {}

fn M.add(x, y) -> x + y

io.println(M.add(34, 35)) // Output: 69

Closures

fn outer(x) {
    return fn () {
        return x * 2
    }
}

var a = outer(34.5)
var b = outer(210)

io.println(a())
io.println(b())
$ bs functions.bs
69
420

Looped Closures

The variables defined in the body of a loop, as well as the iterators, are captured as unique values in the nested closure on each iteration of the loop.

var closures = []

for i in 0..5 {
    var z = i * 2
    closures.push(fn () -> io.println(i, z))
}

for _, f in closures {
    f()
}
$ bs functions.bs
0 0
1 2
2 4
3 6
4 8

Variadics

Variadic arguments are collected into an array that is supplied to the variadic argument. All array operations work as expected.

fn sum(..numbers) {
    var total = 0
    for _, n in numbers {
        total += n
    }
    return total
}

io.println(sum())             // Output: 0
io.println(sum(69))           // Output: 69
io.println(sum(90, 110, 220)) // Output: 420

A mix of variadic and non-variadic arguments is also allowed, although the variadic argument must be the last one.

fn sum(x, y, ..numbers) {
    var total = x + y
    for _, n in numbers {
        total += n
    }
    return total
}

io.println(sum(34, 35))                 // Output: 69
io.println(sum(90, 110, 110, 110))      // Output: 420
io.println(sum(90, 110, 220, 400, 517)) // Output: 1337

Spread

The (..) operator can be used in function calls to spread the values of an array into the call.

fn sum(x, y, z) {
    return x + y + z
}

var xs = [16, 18, 35]
io.println(sum(..xs))       // Output: 69

var ys = [100]
var zs = [120, 200]
io.println(sum(..ys, ..zs)) // Output: 420

This can also be used in variadic functions.

fn sum(..numbers) {
    var total = 0
    for _, n in numbers {
        total += n
    }
    return total
}

var xs = [16, 18, 35]
io.println(sum(..xs))       // Output: 69

var ys = [100]
var zs = [120, 200]
io.println(sum(..ys, ..zs)) // Output: 420

Of course, it goes without saying that normal arguments can be mixed with spreaded ones.

fn sum(..numbers) {
    var total = 0
    for _, n in numbers {
        total += n
    }
    return total
}

var xs = [100, 125, 126]
io.println(sum(34, ..xs, 35)) // Output: 420

Defer

Defers are like in Go. They are executed at the end of the function in reverse order.

fn main(x, y) {
    io.print("Hello")
    defer {
        var f = io.println
        f("!")
    }

    io.print(", ")
    defer io.print("world")

    return x + y
}

io.println(main(34, 35))
io.println(main(200, 220))
$ bs defer.bs
Hello, world!
69
Hello, world!
420

Variables

var a = 34
var b = 35
io.println(a + b) // Output: 69

Assignment

var a = 17
var b             // Assigned to 'nil'

a = a * 2
b = 35

io.println(a + b) // Output: 69

Shorthand assignment operators also work.

var a = 17

// All arithmetic operators are supported
a *= 2
a += 35

var b = "Nice! "

// String concatenation is also supported
b $= a

io.println(b) // Output: Nice! 69

Scoped Variables

var x = 69
io.println(x)     // Output: 69

{
    var x = 420
    io.println(x) // Output: 420
}

io.println(x)     // Output: 69

Variable Shadowing

var x = 69
io.println(x) // Output: 69

var x = "x used to be {x}"
io.println(x) // Output: x used to be 69

Local Variables

var z = 420 // Local to the scope of the file

fn add(x, y) {
    var z = x + y // Local to the scope of the function
    io.println(z)
}

add(34, 35)
io.println(z)
$ bs variables.bs
69
420

File Local Variables

Here's a pitfall you might run into

var x = 69

fn f() {
    io.println(x) // Refers to the variable 'x'
    io.println(y) // Refers to the variable 'y'
}

var y = 420       // Variable 'y' now defined at the toplevel

f()               // "Should" print 69 and 420
$ bs variables.bs
69
variables.bs:5:16: error: undefined identifier 'y'

    5 |     io.println(y) // Refers to the variable 'y'
      |                ^

variables.bs:10:2: in f()

    10 | f()               // "Should" print 69 and 420
       |  ^

Variables defined at the toplevel are local to the scope of the file, which behaves as a function itself. To put simply, variables cannot be used before they are declared. This is where global variables come into play.

Global Variables

var x = 69

fn f() {
    io.println(x) // Refers to the variable 'x'
    io.println(y) // Refers to the variable 'y'
}

pub var y = 420   // Global variable 'y' now defined

f()
$ bs variables.bs
69
420

Global variables can be accessed from all modules. More on that later.

Constants

const a = 34
const b = 35
io.println(a + b) // Output: 69

Constants cannot be assigned to.

const a = 69
a = 420 // Compile Time Error!

Functions and classes are constant.

fn f() {}
f = 69    // Compile Time Error!

class Foo {}
Foo = 420 // Compile Time Error!

A variable function needs to be declared as a lambda bound to a var.

var g = fn () {}
g = 1337  // Not An Error!

Import

// one.bs
var M = {}

fn M.inc(n) -> n + 1
fn M.dec(n) -> n - 1

return M // Any arbitrary value can be returned, this is just the usual pattern
// main.bs
var one = import("one") // Note that the extension is omitted

io.println(one.inc(68))
io.println(one.dec(421))
$ bs main.bs
69
420

Singular Load

Modules are loaded only once.

// one.bs
var M = {}

io.println("Loading module 'one'")

fn M.inc(n) -> n + 1
fn M.dec(n) -> n - 1

return M
// main.bs
io.println(import("one").inc(68))
io.println(import("one").dec(421))
$ bs main.bs
Loading module 'one'
69
420

Main Module

// one.bs
var M = {}

if is_main_module {
    io.println("Loading module 'one'")
}

fn M.inc(n) -> n + 1
fn M.dec(n) -> n - 1

return M
// main.bs
io.println(import("one").inc(68))
io.println(import("one").dec(421))
$ bs main.bs
69
420

But if the module one is executed directly...

$ bs one.bs
Loading module 'one'

Global variables across modules

// one.bs
pub var x = 69
// main.bs
import("one")

io.println(x)
$ bs main.bs
69

OOP

I refuse to showcase the Animal > Dog, Cat nonsense, so here's a more applicable, albeit involved, example.

class Sprite {
    // Constructor
    init(name, health, attack_power) {
        this.name = name
        this.health = health
        this.attack_power = attack_power
    }

    attack(target) {
        io.println("{this.name} attacks {target.name} for {this.attack_power} damage!")
        target.take_damage(this.attack_power)
    }

    take_damage(amount) {
        this.health -= amount
        if this.is_alive() {
            io.println("{this.name} has {this.health} health remaining.")
        } else {
            io.println("{this.name} has been defeated!")
        }
    }

    is_alive() {
        return this.health > 0
    }
}

// Inheritance
class Hero < Sprite {
    init(name, health, attack_power) {
        super.init(name, health, attack_power)
        this.poisoned = false
        this.defending = false
    }

    // Special ability of hero: defending
    defend() {
        io.println("{this.name} is defending!")
        this.defending = true
    }

    take_damage(amount) {
        if this.defending {
            if this.poisoned {
                // Hero will not be poisoned if defending
                this.poisoned = false
            } else {
                // Hero will take half damage for a normal attack if defending
                amount /= 2
            }

            this.defending = false
        }

        super.take_damage(amount)
    }
}

class Enemy < Sprite {
    init(name, health, attack_power) {
        super.init(name, health, attack_power)
    }

    // Special ability of enemy: poisoning
    poison(target) {
        io.println("{this.name} poisons {target.name}!")
        target.poisoned = true
        target.take_damage(this.attack_power)
    }
}

// io.input() is a core library function which optionally prints a prompt and
// reads a line from standard input
var hero = Hero(io.input("Enter hero's name> "), 30, 10)
var enemy = Enemy(io.input("Enter enemy's name> "), 25, 8)

// Create a random number generator
var rng = math.Random()

while hero.is_alive() && enemy.is_alive() {
    io.println()

    // Hero cannot choose while poisoned
    if hero.poisoned {
        io.println("{hero.name} has been poisoned! skipping turn.")

        // Prevent consecutive poisoning
        hero.poisoned = false
        enemy.attack(hero)
    } else {
        var choice = io.input("Enter {hero.name}'s choice (A: attack, D: defend)> ").toupper()

        if choice == "A" {
            hero.attack(enemy)
            if !enemy.is_alive() {
                break
            }
        } else if choice == "D" {
            hero.defend()
        } else {
            io.println("ERROR: invalid choice! skipping {hero.name}'s turn.")
        }

        // Enemy will either attack or poison the hero based on a 50-50 chance
        if rng.number() >= 0.5 {
            enemy.poison(hero)
        } else {
            enemy.attack(hero)
        }
    }
}

if hero.is_alive() {
    io.println("{hero.name} won!")
} else {
    io.println("{hero.name} lost :(")
}
$ bs oop.bs
Enter hero's name> Luffy
Enter enemy's name> Magellan

Enter Luffy's choice (A: attack, D: defend)> a
Luffy attacks Magellan for 10 damage!
Magellan has 15 health remaining.
Magellan poisons Luffy!
Luffy has 22 health remaining.

Luffy has been poisoned! skipping turn.
Magellan attacks Luffy for 8 damage!
Luffy has 14 health remaining.

Enter Luffy's choice (A: attack, D: defend)> a
Luffy attacks Magellan for 10 damage!
Magellan has 5 health remaining.
Magellan poisons Luffy!
Luffy has 6 health remaining.

Luffy has been poisoned! skipping turn.
Magellan attacks Luffy for 8 damage!
Luffy has been defeated!
Luffy lost :(

Is

class Foo {}
class Bar {}

var foo = Foo()
var bar = Bar()

io.println(foo is Foo)              // Output: true
io.println(bar is Bar)              // Output: true

io.println(foo is Bar)              // Output: false
io.println(bar is Foo)              // Output: false

io.println(io.stdin is io.Reader)   // Output: true
io.println(io.stdout is io.Writer)  // Output: true

io.println(io.stdin is io.Writer)   // Output: false
io.println(io.stdout is io.Reader)  // Output: false

// '!is' works as well
io.println(foo !is Foo)             // Output: false
io.println(bar !is Bar)             // Output: false

io.println(foo !is Bar)             // Output: true
io.println(bar !is Foo)             // Output: true

Classof

class Foo {}
class Bar {}

var foo = Foo()
var bar = Bar()

// Output:
// class Foo {}
io.println(classof(foo))

// Output:
// class Bar {}
io.println(classof(bar))

// Output:
// class Reader {
//     // Can fail
//     eof = <fn>,
//     tell = <fn>,
//     close = <fn>,
//     seek = <fn>,
//     read = <fn>,
//     readln = <fn>
// }
io.println(classof(io.stdin))

// Output:
// class Writer {
//     // Can fail
//     flush = <fn>,
//     close = <fn>,
//     writeln = <fn>,
//     write = <fn>
// }
io.println(classof(io.stdout))

// Output: nil
io.println(classof(69))

Constructor Failure

Typical OOP languages mandate that constructors must return the instance under all circumstances, even at the cost of error handling. In BS however, constructors may return nil in order to indicate that the constructor failed. Any other return value is strictly forbidden, however.

class Logger {
    init(path) {
        // io.Writer is a core C class that handles writeable files. It can fail
        this.file = io.Writer(path)
        if !this.file {
            // Could not create logger file, return 'nil' to indicate failure
            return nil
        }
    }

    write(s) {
        // os.clock() returns the current monotonic time in seconds as a
        // floating point number
        this.file.writeln("{os.clock()}: {s}")
    }
}

var logger = Logger("log.txt")
if !logger {
    // io.eprintln() prints to standard error
    io.eprintln("Error: could not create logger")

    // os.exit() exits the program with the provided exit code
    os.exit(1)
}

logger.write("Hello, world!")

How to know if a class can fail?

Just print the class.

class Foo {
    init(x) {
        if x == 69 {
            return nil
        }

        this.x = x
    }
}

class Bar {
    init(x) {
        this.x = x
    }
}

io.println(Foo)
io.println(Bar)
$ bs oop.bs
class Foo {
    // Can fail
}
class Bar {}

How to know the methods in a class?

Just print the class.

class Lol {
    foo() {}
    bar() {}
    baz() {}
}

io.println(Lol)
$ bs oop.bs
class Lol {
    baz = <fn>,
    bar = <fn>,
    foo = <fn>
}

Panic

panic()
$ bs panic.bs
panic.bs:1:1: panic

An optional message can also be provided.

panic("TODO")
$ bs panic.bs
panic.bs:1:1: TODO

Assert

assert(true)
assert(false)
$ bs assert.bs
assert.bs:2:1: assertion failed

An optional message can also be provided.

assert(true, "Ligma")
assert(false, "Ligma")
$ bs assert.bs
assert.bs:2:1: Ligma

Assert returns the value being checked.

var x = assert(69, "Ligma")
io.println(x)
$ bs assert.bs
69

FFI

BS supports loading of native modules at runtime.

Compiling a native module

Create a file arithmetic.c with the following content

// arithmetic.c

#include <bs/object.h>

static Bs_Value arithmetic_add(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 2);
    bs_arg_check_value_type(bs, args, 0, BS_VALUE_NUM);
    bs_arg_check_value_type(bs, args, 1, BS_VALUE_NUM);
    return bs_value_num(args[0].as.number + args[1].as.number);
}

static Bs_Value arithmetic_sub(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 2);
    bs_arg_check_value_type(bs, args, 0, BS_VALUE_NUM);
    bs_arg_check_value_type(bs, args, 1, BS_VALUE_NUM);
    return bs_value_num(args[0].as.number - args[1].as.number);
}

// This is the "entry point" of the library. This function will be called when
// the native library is loaded into memory. Prepare the FFI here
BS_LIBRARY_INIT void bs_library_init(Bs *bs, Bs_C_Lib *library) {
    static const Bs_FFI ffi[] = {
        {"add", arithmetic_add},
        {"sub", arithmetic_sub},
    };
    bs_c_lib_ffi(bs, library, ffi, bs_c_array_size(ffi));
}

Compile it into a dynamic library, linking against bs

$ cc -o arithmetic.so -fPIC -shared arithmetic.c -lbs # On Linux
$ cc -o arithmetic.dylib -fPIC -shared arithmetic.c -lbs # On macOS
$ cl /LD /Fe:arithmetic.dll arithmetic.c bs.lib # On Windows

Make sure to provide the compiler and linker flags as required.

Finally, load it from BS.

// ffi.bs

var arith = import("arithmetic")
io.println(arith.add(34, 35))
io.println(arith.sub(500, 80))
$ bs ffi.bs
69
420

Wrapping an existing C library

As an example let's create a simple wrapper around the raylib library.

// raylib.c

#include <raylib.h>
#include <bs/object.h>

static Bs_Value rl_init_window(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 3);
    bs_arg_check_whole_number(bs, args, 0);
    bs_arg_check_whole_number(bs, args, 1);
    bs_arg_check_object_type(bs, args, 2, BS_OBJECT_STR);

    const int width = args[0].as.number;
    const int height = args[1].as.number;
    const Bs_Str *title = (const Bs_Str *)args[2].as.object;

    // BS strings are null terminated by default for FFI convenience
    InitWindow(width, height, title->data);
    return bs_value_nil;
}

static Bs_Value rl_close_window(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 0);
    CloseWindow();
    return bs_value_nil;
}

static Bs_Value rl_window_should_close(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 0);
    return bs_value_bool(WindowShouldClose());
}

static Bs_Value rl_begin_drawing(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 0);
    BeginDrawing();
    return bs_value_nil;
}

static Bs_Value rl_end_drawing(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 0);
    EndDrawing();
    return bs_value_nil;
}

static Bs_Value rl_clear_background(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 1);
    bs_arg_check_whole_number(bs, args, 0);
    ClearBackground(GetColor(args[0].as.number));
    return bs_value_nil;
}

static Bs_Value rl_draw_text(Bs *bs, Bs_Value *args, size_t arity) {
    bs_check_arity(bs, arity, 5);
    bs_arg_check_object_type(bs, args, 0, BS_OBJECT_STR);
    bs_arg_check_whole_number(bs, args, 1);
    bs_arg_check_whole_number(bs, args, 2);
    bs_arg_check_whole_number(bs, args, 3);
    bs_arg_check_whole_number(bs, args, 4);

    const Bs_Str *text = (const Bs_Str *)args[0].as.object;
    const int x = args[1].as.number;
    const int y = args[2].as.number;
    const int size = args[3].as.number;
    const Color color = GetColor(args[4].as.number);

    DrawText(text->data, x, y, size, color);
    return bs_value_nil;
}

BS_LIBRARY_INIT void bs_library_init(Bs *bs, Bs_C_Lib *library) {
    static const Bs_FFI ffi[] = {
        {"init_window", rl_init_window},
        {"close_window", rl_close_window},
        {"window_should_close", rl_window_should_close},
        {"begin_drawing", rl_begin_drawing},
        {"end_drawing", rl_end_drawing},
        {"clear_background", rl_clear_background},
        {"draw_text", rl_draw_text},
    };
    bs_c_lib_ffi(bs, library, ffi, bs_c_array_size(ffi));
}

Compile it into a dynamic library, linking against bs and raylib

$ cc -o raylib.so -fPIC -shared raylib.c -lbs -lraylib    # On Linux
$ cc -o raylib.dylib -fPIC -shared raylib.c -lbs -lraylib # On macOS
$ cl /LD /Fe:raylib.dll raylib.c bs.lib raylib.lib        # On Windows

Make sure to provide the compiler and linker flags as required.

Finally, load it from BS.

var rl = import("raylib")

rl.init_window(800, 600, "Hello from BS!")
while !rl.window_should_close() {
    rl.begin_drawing()
    rl.clear_background(0x282828FF)
    rl.draw_text("Hello world!", 50, 50, 50, 0xD4BE98FF)
    rl.end_drawing()
}
rl.close_window()