Skip to content

Brio Lang Tutorial

Brio Lang is an easy to learn programming language. This tutorial introduces the reader to the basic concepts and features of the Brio Lang programming lanugage and execution engine.

Using the Interpreter

The Brio Lang interpreter can be started by running ./bin/brio from the directory where the project was built. You may opt to introduce a symbolic link to your binary which will allow you to simply type brio to launch the interpreter.

$ ln -s /path/to/brio-lang/bin/brio /usr/local/bin/brio

Execute a Program

Brio Lang programs have the file extension .brio and can be run by invoking the interpreter and providing the path to the program.

$ brio ./my_app.brio

Interactive Mode

Running brio without any arguments, or brio -i, will launch the read-eval-print loop so you can experiment with expressions and statements.

Brio Lang 0.6.0 (Feb 20 2021, 11:33:11)
[g++ 7.4.0] on 4.19.128-microsoft-standard
Type "help", "license", or "copyright" for more information.
>>>

You can cycle through the history of statements using your keyboard arrows Up and Down.

To exit the interpreter, run the exit() method, press Ctrl+D, or press Ctrl+C.

Optional Arguments

If you supply the -gv argument when running a program, a DOT graph will be generated that allows you to visualize the abstract syntax tree constructed from the input source code. The graph will be created alongside the program, with the .gv file extension.

For example, the program below would produce the following AST.

method main(){
    let x = 1
    print(x)
}
$ brio -gv ./my_app.brio
1

DOT Graph

Similarly, if you supply the -ast argument, a text-based representation of the AST will be printed to the console.

$ brio -ast ./my_app.brio
AST Nodes:
(ProgramNode)  children=1
--(MethodDeclarationNode)  children=3
----(IdentifierNode)  children=0
----(ParamsListNode)  children=0
----(BlockNode)  children=1
------(VarDeclarationNode)  children=2
--------(IdentifierNode)  children=0
--------(LiteralIntNode)  children=0

To see all available arguments, run brio --help.

$ brio --help       
Brio 0.6.0
usage: ./brio [-h] [-v] [-i] [-t] [-gv] [-sym] [-ast] FILE.brio

optional arguments:
  -h, --help                  Prints the help information
  -v, --version               Prints the version
  -i                          Runs in interactive mode (read-eval-print-loop)
  -t                          Prints the tokens from the lexer
  -gv                         Outputs a DOT file for visualizing the AST
  -sym                        Prints the symbol table globals
  -ast                        Prints each node type in the AST
  -fcgi                       Starts FastCGI listener, must be called from spawn-fcgi

Using Brio Lang as a Calculator

Numbers

The Brio Lang interpreter can be used as a simple calculator that supports the common operators +, -, *, / for addition, subtraction, multiplication, and division respectivley. Expressions can be grouped in parenthesis ( ) to specify order of operations. For example:

>>> 5 + 10 / 5
7
>>> (5 + 10) / 5
3

Division will produce a Decimal or Integer based on the divisibility of the numerator and denominator:

>>> 100 / 50  # Integer
2
>>> 50 / 100  # Decimal
0.5
>>> 22 / 7  # Decimal
3.142857

Brio Lang offers the ** operator to calculate powers:

>>> 4 ** 2  # 4 squared
16
>>> 2 ** 5  # 2 to the power of 5
32
>>> 2 ** 4 + (20 - 5)
31

Additionally, Brio Lang offers the % modulus operator to calculate the remainder of division:

>>> 10 % 3
1
>>> 50 % 25
0

Strings

Brio Lang offers the ability to manipulate strings, which can defined with single quotes 'hello' as well as double quotes "hello".

>>> 'hello, world!'  # single quotes
hello, world!

>>> "hello, world!"  # double quotes
hello, world!

While in interactive mode, the interpreter will output special characters:

>>> "line one\nline two"
line one
line two

>>> 'one\ttwo'
one    two

String literals can be concatenated using the + operator:

>>> "Brio" + " " + "Lang"
Brio Lang

>>> let space = " "
>>> "First" + space + "Second"
First Second

Strings can be repeated using the * operator:

>>> "Hello" * 5
HelloHelloHelloHelloHello

>>> let count = 3
>>> print("Repeat" * count)
RepeatRepeatRepeat

String literals can be indexed using [] with the first index being 0. For example:

>>> let foo = 'bar'
>>> foo[0]
b
>>> foo[1]
a
>>> foo[2]
r

Attempting to use an index that is too large or less than zero will result in an error.

>>> foo[-1]
cannot access element -1, max index is 2

>>> foo[99]
cannot access element 99, max index is 2

You can determine the size of a string using the built-in size() method:

>>> let x = "my string"
>>> size(x)
9
>>> x.size()
9

Positional string formatting is supported as well:

>>> let x = "Hello, {0} {1}!"
>>> print(x.format("Brio", "Lang"))
Hello, Brio Lang!

Arrays

The Array data type allows you to define comma-separated values between square brackets. Values can be any object type within Brio Lang including additional arrays.

>>> let values = ['first', 'second', 'third']
>>> values
["first", "second", "third"]

Similar to String objects, arrays can be indexed with the initial index being 0. For example:

>>> let values = ["first", "second", 3]
>>> values[0]
first
>>> values[1]
second
>>> values[2]
3

You can determine the size of an array using the built-in size() method:

>>> let values = [0, 1, 2, 3, 4]
>>> size(values)
5
>>> values.size()
5

Additional values can be added to an array using the push() method:

>>> let values = ["Brio"]
>>> values.push("Lang")
>>> values
["Brio", "Lang"]

To remove values from an array, the pop() method can be used. If an integer is supplied to the method, that index will be removed from the array. If no argument is supplied, the trailing value will be removed:

>>> let values = ["a", "b", "c", "d"]
>>> values.pop()  # removes the last value
>>> print(values)
["a", "b", "c"]

>>> let x = values.pop(0)  # removes value at index 0
>>> print(x)
"a"

>>> print(values)
["b", "c"]

Dictionaries

The Dictionary data type allows you to define key-value pairs within braces { }. Values can be any object type within Brio Lang including nested dictionaries.

>>> let values = {
    "id": 100, 
    "data": {
        "attribute_1": "foo", 
        "attribute_2": "bar", 
        "attribute_3": 50
    }
}

Dictionaries can be indexed using an existing key. For example:

>>> let values = {"a": "A", "b": {}}
>>> values["a"]
"A"
>>> values["b"]
{}

You can determine the number of key-value pairs in a dictionary using the built-in size() method:

>>> let values = {"a": 1, "b": 2, "c": 3}
>>> size(values)
3
>>> values.size()
3

Key-value pairs can be removed from a dictionary with the pop() method, which takes a single argument to specify a key:

>>> let values = {"color": "blue", "size": 100}
>>> let x = values.pop("color")
>>> print(x)
"blue"

>>> print(values)
{"size": 100}

Control Flow

Brio Lang supports many flow control statements known from other programming languages.

if Statements

The if statement can be used to test a condition, and execute a block of code if the condition evaluates to true.

let x = integer(input('Please enter an integer: '))

if (x < 0){
    print('x is less than 0')
}
elseif (x == 0){
    print('x is equal to 0')
}
elseif (x == 1){
    print('x is equal to 1')
}
else{
    print('x is greater than 1')
}
You can optionally specify any number of subsequent elseif blocks, or a generic else block that will be executed if none of the prior conditions evaluate to true.

each Statements

The each statement can be used to iterate through a collection of values.

let values = ['hello', 'world']

each (let i : values){
    print(i)
}
The above snippet results in the following output:
"hello"
"world"

for Statements

The for statement can be used to loop through a set of values until the defined condition no longer evaulates to true. The first component of a for statement defines the index, the second component defines the halting condition, and the third component defines the increment/decrement to your index.

let values = ['hello', 'world']

for (let i = 0; i < size(values); i += 1){
    print(values[i])
}
The above snippet results in the following output:
"hello"
"world"

while Statements

The while statement allows you to loop while the specified condition evaluates to true.

let counter = 0

while (counter < 5){
    print(counter)
    counter += 1
}
The above snippet produces the output:
0
1
2
3
4

skip Statements

The skip statement can be used within a while loop, for loop, or each loop to continue the loop at the next iteration. For example:

let x = 0

while (x < 5){
    x += 1
    if (x > 3){
        skip
    }
    print(x)  # this line will not execute if the skip condition was met.
}

Input and Output

As you've seen earlier in the tutorial, data can be outputted from a Brio Lang program using the built-in print() method. This method takes two arguments: the first is the value to be outputted, and the second argument is optional, and allows you to define the end character to print alongside the value. By default, the newline \n character will be used.

>>> let values = ['hello', 'world']

>>> each (let value : values){
    print(value)
}

hello
world
>>> each (let value : values){
    print(value, '\t')
}

hello    world
>>> each (let value : values){
    print(value, ',')
}

hello,world,

input Method

The input() method can be used to capture user input during program execution. This method accepts a single String argument that will be displayed as part of the prompt for user input.

>>> let x = input('Enter a value: ')
Enter a value: Brio Lang

>>> print(x)
Brio Lang

The input() method returns a string value. If you require an Integer for example, you may cast the data type as follows:

>>> let x = integer(input('Enter an integer: '))

Reading and Writing Files

Files can be created or read using the built-in File object. Typically, the open() method will be used to acquire an instance for file manipulation.

let file = open("./my_file.txt")
let data = file.read()
print(data)

The open() method takes an optional second argument, which is the mode. By default, files are opened in read-only mode. Supported file modes include 'r' (read-only), 'r+' (read and write), 'w' (write-only), 'w+' (write and read), 'a' (append-only), or 'a+' (append and read).

let file = open("./my_file.txt", "w")
file.write("Hello!\n")
file.close()

To see the full list of File methods and attributes, please reference the File data type.

Data Types

Brio Lang provides several built-in, high level data types that can be used.

Integer

The Integer data type is used to define signed integer values such as -1 or 5000.

let x = 100
let y = -42

Decimal

The Decimal data type is used to define signed floating point numbers such as -1.5 or 3.14.

let x = 4.223
let y = -10.321

String

The String data type is used to define a sequence of characters and can be enclosed with single or double quotes.

let x = 'single quotes'
let y = "double quotes"
Method Description
size Returns an integer specifying the number of characters in the string.
format Returns a formatted string, replacing "{0}", "{1}", and so on with provided values.
isdigit Returns true or false to indicate if numeric.
isalpha Returns true or false to indicate if alphabetic.
split Returns an array of strings, split by the specified character.

Boolean

The Boolean data type is used to define true or false values.

let x = true
let y = false

Array

The Array data type is used to define a sequence of values, which can be any number of Seed Lang data types including strings, decimals, booleans, arrays, and dictionaries.

let x = ["one", 2, ["three"]]
let y = [[0, 1], [1, 0]]
Method Description
size Returns an integer specifying the number of key-value pairs in the dictionary.
push Appends a value to the end of an array.
pop Removes a value from the array based on provided index. If none provided, removes the last value.

Dictionary

The Dictionary data type is used to define key-value pairs.

let x = {"name": "Brio Lang", "version": 0.8}
Method Description
size Returns an integer specifying the number of key-value pairs in the dictionary.
pop Removes the specified key-value pair from the dictionary.

File

The File data type is used to work with files and is typically created with the open() built-in method, which takes a first argument for the path to the file, and an optional second argument to specify the mode.

Method Description
read Reads the all characters from the file, or the defined number if provided.
readline Reads and returns one line from the file.
readlines Reads and returns an array of lines from the file.
size Returns the number of characters in the file.
write Writes a string to the file.
writeline Writes a line to the file, with a terminating newline character.
writelines Writes an array of lines to the file.
tell Returns the current file cursor position.
flush Flushes the write buffer for the file stream.
seek Change the file cursor position to the specified index.
close Closes and opened file and frees up associated resources.
Attribute Description
closed Boolean that indicates if a File is closed.
encoding String that specifies the File encoding.
mode String that specifies the mode which the File was opened.
name String that specifies the file name.

Modules

The Brio Lang interpreter can be used to test simple expressions and statements, however if you wish to write a script, it's recommended to create a .brio source file using your prefered text editor. As the script increases in size, you may decide to split the program into several files for easier maintenance. In other cases, you may have written a snippet of code you would like to use across multiple Brio Lang programs. Brio Lang supports this with a simple module import system.

import Statements

The import statement can be used to import Brio Lang definitions and constructs that exist in separate .brio files. For example, we may have a module calc.brio:

method add(a, b){
    return a + b
}

To use this in another source file:

import calc

method main(){
    print(calc.add(1, 2))  # will print 3
}

You may notice that when importing a file, the .brio extension is omitted and only the name of the file is supplied. The Brio Lang interpreter looks for files and modules in the interpreter ./library/ directory where the standard libraries exist, the relative path to the executing program, as well as a BRIOPATH environment variable that you define.

Using dot notation, you may import files or modules that have a nested structure. For example, if we have the following project structure:

project/
- libs/
--- helpers/
------ calc.brio
- main.brio
we may import calc.brio in the main.brio source file with:
import libs.helpers.calc

Errors & Exceptions

While executing a Brio Lang program, you may encounter syntax errors, built-in exceptions, or even user-defined exceptions. Syntax errors will be more common while you are still learning Brio Lang, and the interpreter will help specify the offending line number.

method main{  # missing () after the method name
    print("error: missing parenthesis")
}
$ brio my_app.brio
terminate called after throwing an instance of 'SyntaxError'
  what():  line 1: unsupported statement

If an expression or statement is syntactically correct, you may encounter exceptions at runtime for a number of reasons.

method main(){
    let x = 10 / 0;  # cannot divide by zero
}
terminate called after throwing an instance of 'ZeroDivisionError'
  what():  line 2: division by zero

Attempting to divide by zero will result in a ZeroDivisionError, and as shown below, attempting to reference an undefined variable will result in a RuntimeError.

method main(){
    let x = undefined + 10
}
terminate called after throwing an instance of 'RuntimeError'
  what():  line 2: unable to resolve symbol 'undefined'

Within Brio Lang, it's possible to handle errors and exceptions using a try/catch statement.

try Statements

The try/catch statement allows you to handle defined exceptions.

try{
    let x = 1 / 0  # cannot divide by zero
}
catch(ZeroDivisionError as e){
    print("caught error!")
}
$ brio my_app.brio
caught error!

Brio Lang will process the try/catch statement as follows:

  • First, the try clause will be executed by the interpreter.
  • If no exceptions are raised, the subsequent catch clause(s) will be skipped.
  • If an exception was raised, the corresponding named catch clause will be executed.
  • If there is no named catch clause for the raised exception type, the generic catch will be called.
  • If there is no generic catch clause defined, the exception will be raised by the interpreter.

Try statements can have any number of subsequent named catch clauses, and only one generic catch clause specified at the end.

class A { }
class B : A {}

method main(){
    try{
        raise A  # the second named catch clause will be executed
    }
    catch(B as e){
        print("named catch for 'B' called")
    }
    catch(A as e){
        print("named catch for 'A' called")
    }
    catch{
        print("generic catch called")
    }
}
$ brio my_app.brio
named catch for 'A' called

It's important to be mindful when using the generic catch clause, it's possible to supress a real programming error this way. Working with named catch clauses is recommended.

Classes

Classes provide a way to group data and functionality together into meaningful constructs. Defining a class introduces a new data type, where instantiating a class with the new keyword introduces a class instance which can maintain its own state.

class Calculator {
    method add(a, b){
        return a + b
    }
    method subtract(a, b){
        return a - b
    }
    method multiply(a, b){
        return a * b
    }
    method divide(a, b){
        return a / b
    }
}

method main(){
    let calc = new Calculator()
    print(calc.multiply(5, 2))
}
$ brio my_app.brio
10

Instance variables can be accessed or defined using the @ keyword, for example:

class Hello {
    method init(first_name, last_name){
        @name = first_name + " " + last_name
    }
    method sayHello(){
        print(@name)
    }
}

method main(){
    let hi = new Hello("Brio", "Lang")
    hi.sayHello()
}
$ brio my_app.brio
Brio Lang

As seen in the above example, classes support an init() method which will be invoked when the class is instantiated, and can take a user-defined number of arguments. This can be used to set the instance's initial state

Class inheritence is supported in Brio Lang, for example:

class A {
    method hello(){
        print("hello!")
    }
}

class B : A {}

method main(){
    let b = new B()
    b.hello()
}
$ brio my_app.brio
hello!

Sub-classes may override a parent class method as well:

class A {
    method hello(){
        print("hello from A!")
    }
}

class B : A {
    method hello(){
        print("hello from B!")
    }
}

method main(){
    let a = new A()
    let b = new B()

    a.hello()
    b.hello()
}
$ brio my_app.brio
hello from A!
hello from B!

Standard Library

The Brio Lang standard library is in its infancy and is missing key utilities around handling dates, serializing XML, working with regular expressions, etc. If you would like to contribute to the standard library, please visit us on GitHub.

csv

The csv standard library makes it easy to read and write CSV files.

import csv

method main(){
    # write a CSV file
    let file = open("./examples/helloworld40.csv", "w")
    let x = new csv.Writer(file)
    let data = ["hello", "world"]
    x.writerow(data)
    file.close()

    # read a CSV file
    file = open("./examples/helloworld40.csv")
    x = new csv.Reader(file)
    print(x.rows())
    file.close()
}

json

The json standard library makes it easy to parse and stringify JSON.

import json

method main(){
    # parse JSON
    let data = json.parse('{"foo": "bar", "valid": true}')
    print(data["foo"])  # "bar"
    print(data["valid"])  # true

    # stringify JSON
    data["valid"] = false
    let output = json.dump(data)
    print(output)  # {"foo": "bar", "valid": false}
}

What Now?

Congratulations for making it through the Brio Lang tutorial! To learn more, please visit the language reference or check out a few examples of Brio Lang in action.