Borgo Programming Language

Intro

Borgo sits between Go and Rust

Borgo is a new programming language that compiles to Go.

For a high-level overview of the features and instructions on running the compiler locally, check the README.

This playground runs the compiler as a wasm binary and then sends the transpiled go output to the official Go playground for execution.

use fmt

enum NetworkState<T> {
    Loading,
    Failed(int),
    Success(T),
}

struct Response {
    title: string,
    duration: int,
}

fn main() {
    let res = Response {
        title: "Hello world",
        duration: 0,
    }

    let state = NetworkState.Success(res)

    let msg = match state {
        NetworkState.Loading => "still loading",
        NetworkState.Failed(code) => fmt.Sprintf("Got error code: %d", code),
        NetworkState.Success(res) => res.title,
    }

    fmt.Println(msg)
}

Primitive Types

Primitive types are the same as in Go.

Collections like slices and maps can be used without specifying the type of the values.

For example, a slice of int elements would be declared as []int{1,2,3} in Go, whereas Borgo relies on type inference to determine the type, so you can just write [1, 2, 3].

Functions like append() and len() are available as methods.

Maps are initialized with the Map.new() function, which under the hood compiles to a map[K]V{} expression, with the K and V types helpfully filled in for you.

Borgo also has tuples! They work exactly like in Rust.

Multiline strings are defined by prefixing each line with \\ like in Zig. This has the benefit that no character needs escaping and allows more control over whitespace.

use fmt

fn main() {
    let n = 1
    let s = "hello"
    let b = false

    fmt.Println("primitives: ", n, s, b)

    let mut xs = [1,2,3]
    fmt.Println("slice:", xs)

    xs = xs.Append(10)
    fmt.Println("len after append:", xs.Len())

    let mut m = Map.new()
    m.Insert(1, "alice")
    m.Insert(2, "bob")

    fmt.Println("map:", m)

    let pair = ("hey", true)
    fmt.Println("second element in tuple:", pair.1)

    let multi = \\a multi line
        \\  string with unescaped "quotes"
        \\ that ends here

    fmt.Println("multiline string:", multi)
}

Control flow

Like in Go, the only values that can be iterated over are slices, maps, channels and strings.

However, loops always iterate over a single value, which is the element in the slice (contrary to Go, where using a single iteration variable gives you the index of the element).

To iterate over (index, element) pairs call the .enumerate() method on slices. This has no runtime cost, it just aids the compiler in generating the correct code.

When iterating over maps, you should always destructure values with (key, value) pairs instead of a single value.

Like in Rust, infinite loops use the loop {} construct whereas loops with conditions use while {}.

Expressions like if, match and blocks return a value, so you can assign their result to a variable.

use fmt
use math.rand

fn main() {
    let xs = ["a", "b", "c"]

    fmt.Println("For loop over slices")
    for letter in xs {
        fmt.Println(letter)
    }

    fmt.Println("Indexed for loop")
    for (index, letter) in xs.Enumerate() {
        fmt.Println(index, letter)
    }

    let m = Map.new()
    m.Insert(1, "alice")
    m.Insert(2, "bob")

    fmt.Println("For loop over maps")
    for (key, value) in m {
        fmt.Println(key, value)
    }

    fmt.Println("Loop with no condition")
    loop {
        let n = rand.Float64()
        fmt.Println("looping...", n)

        if n > 0.75 {
            break
        }
    }

    fmt.Println("While loop")

    let mut count = 0
    while (count < 5) {
        fmt.Println(count)
        count = count + 1
    }

    fmt.Println("using if statements as expressions")
    fmt.Println(if 5 > 3 { "ok" } else { "nope" })

    let block_result = {
        let a = 1
        let b = 2
        a + b
    }

    fmt.Println("block result:", block_result)
}

Algebraic data types and pattern matching

You can define algebraic data types with the enum keyword (pretty much like Rust).

Pattern matches must be exhaustive, meaning the compiler will return an error when a case is missing (try removing any case statement from the example and see what happens!).

For now, variants can only be defined as tuples and not as structs.

use fmt
use strings


enum IpAddr {
    V4(uint8, uint8, uint8, uint8),
    V6(string),
}

fn isPrivate(ip: IpAddr) -> bool {
  match ip {
    IpAddr.V4(a, b, _, _) => {
        if a == 10 {
            return true
        }

        if a == 172 && b >= 16 && b <= 31 {
            return true
        }

        if a == 192 && b == 168 {
            return true
        }

        false
    }

    IpAddr.V6(s) => strings.HasPrefix(s, "fc00::")
  }
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn valueInCents(coin: Coin) -> int {
    match coin {
        Coin.Penny => 1,
        Coin.Nickel => 5,
        Coin.Dime => 10,
        Coin.Quarter => 25,
    }
}

fn main() {
    let home = IpAddr.V4(127, 0, 0, 1)
    let loopback = IpAddr.V6("::1")
    fmt.Println("home ip is private: ", home, isPrivate(home))
    fmt.Println("loopback: ", loopback)

    let cents = valueInCents(Coin.Nickel)
    fmt.Println("cents:", cents)

}

Structs

Defining and instantiating structs is similar to Rust.

Contrary to Go, all struct fields must be initialized. See the section on nil and zero values for more information.

use fmt

struct Person {
    name: string,
    hobbies: [Hobby],
}

enum Hobby {
    SkyDiving,
    StaringAtWall,
    Other(string),
}

fn main() {
    let mut p = Person {
        name: "bob",
        hobbies: [Hobby.StaringAtWall, Hobby.Other("sleep")],
    }

    fmt.Println("person:", p)

    p.hobbies = p.hobbies.Append(Hobby.SkyDiving)
    fmt.Println("with more hobbies:", p)
}

Result and Option

Sometimes it's helpful to deal with values that may or may not be there. This is the idea behind the Option<T> type.

For example, to get an element out of a slice or a map, you can use the .get(index) method that will force you to handle the case where the element isn't there.

Other times you may want to return a value or an error. In those cases use Result<T, E> to let the caller know that a function may return an error.

When you're sure that a value is definitely there, you can call .unwrap(). Like in Rust, this is an unsafe operation and will panic.

A lot of methods are missing from both Result and Option, contributions to the stdlib are welcome!

use fmt

struct Person {
    name: string,
    age: int
}

fn validate(name: string, age: int) -> Result<Person, string> {
    if (age < 18) {
        return Err("too young")
    }

    if (age > 98) {
        return Err("too old")
    }

    Ok(Person { name, age })
}

fn main() {
    let xs = ["a", "b", "c"]
    let element = xs.Get(2) // Option<string>

    match element {
        Some(s) => fmt.Println("ok, the element was found:", s),
        None => fmt.Println("element not found"),
    }

    let result = validate("alice", 33) // Result<Person, string>

    match result {
        Ok(p) => fmt.Println("got a person:", p),
        Err(e) => fmt.Println("couldn't validate:", e),
    }
}

Interoperability with Go

One ambitious goal of this project is to be fully compatible with the existing Go ecosystem.

You've already seen how the fmt package was used in previous examples, but how do we deal with functions that return multiple values?

This is where our trusty Option and Result types come in! The compiler will handle the conversion automatically for you :)

A good mental model is to think of return types in Go functions as:

when return type is    (T, bool)
it becomes             Option<T>

when return type is    (T, error)
it becomes             Result<T, E>

Let's take the os.LookupEnv function as an example:

Go definition:
  func LookupEnv(key string) (string, bool)

becomes:
  fn LookupEnv(key: string) -> Option<string>

Or the os.Stat function from the same package:

Go definition:
  func Stat(name string) (FileInfo, error)

becomes:
  fn Stat(name: string) -> Result<FileInfo>

Result<T> is short-hand for Result<T, error> where error is the standard Go interface.

With this simple convention, pretty much any Go package can be used in Borgo code! All is needed is a package declaration, which is discussed in the next section.

use fmt
use os

fn main() {
    let key = os.LookupEnv("HOME")

    match key {
        // Option<T>
        Some(s) => fmt.Println("home dir:", s),
        None => fmt.Println("Not found in env"),
    }

    let info = os.Stat("file-does-not-exist")

    match info {
        // Result<T, E>
        Ok(_) => fmt.Println("The file exists"),
        Err(err) => fmt.Println("Got error reading file", err),
    }
}

Package definitions

In order to use existing Go packages, Borgo needs to know what types and functions they contain. This is done in declaration files, which serve a similar purpose to what you might see in Typescript with d.ts files.

Only a small part of the Go stdlib is currently available for use in Borgo -- check the std/ folder for more information.

The example on the right uses the regexp package from the Go standard library. The relevant bindings are defined in std/regexp/regexp.brg (here's a snippet):

struct Regexp { }

fn Compile  (expr: string) -> Result<*Regexp> { EXT }

fn CompilePOSIX  (expr: string) -> Result<*Regexp> { EXT }

fn MustCompile  (str: string) -> *Regexp { EXT }

fn MustCompilePOSIX  (str: string) -> *Regexp { EXT }

fn Match  (pattern: string, b: [byte]) -> Result<bool> { EXT }

// ... other stuff

Writing such declarations by hand is a pain! There's no reason why this process couldn't be automated though. The compiler comes with an importer tool that parses a Go package and generates corresponding bindings to be used in Borgo.

use fmt
use regexp

fn main() {
    let validID = regexp.MustCompile("^[a-z]+[[0-9]+]$")

    fmt.Println(validID.MatchString("adam[23]"))
    fmt.Println(validID.MatchString("eve[7]"))
}

Pointers and References

Pointers and References work the same as in Go.

To dereference a pointer, use foo.* instead of *foo (like in Zig).

use fmt

struct Foo {
    bar: int
}

struct Bar {
    foo: *Foo
}

fn main() {
    let mut f = Foo { bar: 0 }
    let b = Bar { foo: &f }

    f.bar = 99

    fmt.Println(b.foo)

    // pointer dereference
    // In Go, this would be:   *b.foo = ...
    b.foo.* = Foo { bar: 23 }

    fmt.Println(b.foo)
}

Methods

To define methods on types, you can use impl {} blocks.

In Go, the method receiver must be specified at each function declaration. In Borgo, this is specified only once at the beginning of the impl block (p: *Person). All functions within the block will have that receiver.

It's also possible to declare static methods: functions can be declared with dots in their name, so you can define a Person.new function like in the example.

use fmt

struct Person {
    name: string,
    hours_slept: int,
}

fn Person.new(name: string) -> Person {
    Person {
        name,
        hours_slept: 0,
    }
}

impl (p: *Person) {
    fn sleep() {
        p.hours_slept = p.hours_slept + 1 
    }

    fn ready_for_work() -> bool {
        p.hours_slept > 5
    }

    fn ready_to_party() -> bool {
        p.hours_slept > 10
    }
}

fn main() {
    let mut p = Person.new("alice")

    p.sleep()
    p.sleep()

    fmt.Println("is ready:", p.ready_for_work())
}

Interfaces

Interfaces in Borgo work the same as in Go, it's all duck typing.

If a type implements the methods declared by the interface, then the type is an instance of that interface.

Embedded interfaces are also supported, just list out the other interfaces implied by the one being defined (prefixed by impl). For example, the ReadWriter interface from the io package can be defined as:

interface ReadWriter {
    impl Reader
    impl Writer
}

type sets are not supported.

use fmt
use math

interface geometry {
    fn area() -> float64
    fn perim() -> float64
}

struct rect {
    width: float64,
    height: float64,
}

impl (r: rect) {
    fn area() -> float64 {
        r.width * r.height
    }

    fn perim() -> float64 {
        2 * r.width + 2 * r.height
    }
}

struct circle {
    radius: float64,
}

impl (c: circle) {
    fn area() -> float64 {
        math.Pi * c.radius * c.radius
    }

    fn perim() -> float64 {
        2 * math.Pi * c.radius
    }
}

fn measure(g: geometry) {
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perim())
}

fn main() {
    let r = rect {
        width: 3,
        height: 4,
    }

    let c = circle { radius: 5 }

    measure(r)
    measure(c)
}

Error handling

In functions that return a Result, it's possible to propagate errors with the ? operator.

This is similar to what happens in Rust, refer to the section on Propagating errors in the Rust book .

Currently the ? operator only works with Result, but it will be extended to also work with Option.

use fmt
use io
use os

fn copy_file(src: string, dst: string) -> Result<(), error> {
    let stat = os.Stat(src)?

    if !stat.Mode().IsRegular() {
        return Err(fmt.Errorf("%s is not a regular file", src))
    }

    let source = os.Open(src)?
    defer source.Close()

    let destination = os.Create(dst)?
    defer destination.Close()

    // ignore number of bytes copied
    let _ = io.Copy(destination, source)?

    Ok(())
}

fn copy_all_files(folder: string) -> Result<int, error> {
    let mut n = 0

    for f in os.ReadDir(folder)? {
        if !f.IsDir() {
            let original = f.Name()
            let new_name = fmt.Sprintf("%s-copy", original)

            fmt.Println("copying", original, "to", new_name)

            copy_file(original, new_name)?
            n = n + 1
        }
    }

    Ok(n)
}

fn main() {
    match copy_all_files(".") {
        Ok(n) => fmt.Println(n, "files copied"),
        Err(err) => fmt.Println("Got error:", err),
    }
}

Zero values and nil

In Borgo, you can't create nil values.

The concept of null references (or nil in this case) is being referred to as "The billion dollar mistake" and modern languages are moving away from it with types like Option<T>. Borgo tries to do the same.

You can still end up with null pointers if you're calling into existing Go code, which is unfortunate. That should be solvable by writing better bindings, so that functions that could return a null pointer, will instead return an Option<*T>, forcing you to handle all cases.

In Go, it's common to see types not needing to be initialized, as their zero value is ready to be used (ie. sync.Mutex or sync.WaitGroup). Borgo goes in the opposite direction, requiring that all values are explicitely initialized.

You can use the built-in function zeroValue() whenever you need the zero value of a type. While you won't need to provide a type annotation in all cases (as the type can be inferred), it's probably clearer to annotate variables that are initialized with zeroValue().

As mentioned in a previous section, this also applies to struct fields, which always need to be initialized.

use sync
use bytes
use fmt

fn main() {
     // in Go:
     // var wg sync.WaitGroup
     let wg: sync.WaitGroup = zeroValue()

     // in Go:
     // var b bytes.Buffer
     let b: bytes.Buffer = zeroValue()

     fmt.Println("variables are initialized:", wg, b)
}

Concurrency (goroutines)

Borgo aims to support all concurrency primitives available in Go.

Use the spawn keyword (instead of go) to start a goroutine. The parameter needs to be a function call.

Channels and select {} statements are discussed next.

use sync
use fmt

struct Counter {
    count: int,
    mu: sync.Mutex,
}

fn Counter.new() -> Counter {
    Counter { count: 0, mu: zeroValue() }
}

impl (c: *Counter) {
    fn Inc() {
       c.mu.Lock() 
       c.count = c.count + 1
       c.mu.Unlock() 
    }
}

fn main() {
    let desired = 1000
    let counter = Counter.new()

    let wg: sync.WaitGroup = zeroValue()
    wg.Add(desired)

    let mut i = 0

    while (i < desired) {

        // equivalent to:   go func() { ... }()
        spawn (|| {
            counter.Inc()
            wg.Done()
        })()

        i = i + 1
    }

    wg.Wait()

    fmt.Println("Counter value:", counter.count)
}

Channels

Borgo doesn't provide any extra syntax to send/receive from channels.

You use Channel.new() to create a Sender<T> and Receiver<T>.These are roughly equivalent to send-only and receive-only channels in Go and will compile to raw channels in the final Go output.

With a Sender<T> you can call send(value: T) to send a value. With a Receiver<T> you can call recv() -> T to receive a value.

This design is somewhat inspired by the sync::mspc::channel module in the Rust standard library.

use fmt

fn main() {
    let (sender, receiver) = Channel.new()

    spawn (|| {
        sender.Send(1)
    })()

    spawn (|| {
        sender.Send(2)
    })()

    let msg = receiver.Recv()
    let msg2 = receiver.Recv()

    fmt.Println(msg + msg2)
}

Select statements

select {} works like in Go, however the syntax is slightly different.

Reading from a channel

Go:    case x := <- ch
Borgo: let x = ch.Recv() 


Sending to a channel

Go:    case ch <- x
Borgo: ch.Send(x) 

Default case

Go:    default
Borgo: _
use fmt
use time

fn main() {
    let (tx1, rx1) = Channel.new()
    let (tx2, rx2) = Channel.new()

    // dummy done channel
    let (_, done) = Channel.new()

    spawn (|| {
        tx1.Send("a")
    })()

    spawn (|| {
        loop {
            select {
                // in Go:
                //   case tx2 <- "b":
                tx2.Send("b") => {
                    fmt.Println("sending b")
                    time.Sleep(1 * time.Second)
                }

                let _ = done.Recv() => return
            }
        }
    })()

    select {
        // in Go:
        //   case a := <- rx1:
        let a = rx1.Recv() => {
            fmt.Println("got", a)
        },

        let b = rx2.Recv() => {
            fmt.Println("got", b)
        },
    }
}
Loading compiler & stdlib...