This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Concepts

Guiding ideas for Bramble and Thorn

Design principles which are guiding the development of Bramble and Thorn.

1 - Projects

How projects work in Bramble.

How projects work in Bramble.

Projects

A project can be either a single Bramble source code file or a directory containing one or more Bramble files.

To compile a single file, pass the pass to the file itself to the compiler. To compile a multi-file project, pass the path to the projects root directory to the compiler.

Importing a Project

To import an external Bramble project into your project you will need to manifest and the binary of the import target project.

Referring to Imported Items

To use an imported item in your project, you will have to use the canonical path to the item. This path is specified with

project::<project name>[::<sub directory>]*::<file>[::<module>]*::<item>

Allowing use statements to create aliases and remove the need for full paths to imported items is a QOL feature that has not yet been added to Bramble.

Manifests

If you want to use a project as a library in another project, you need to generate a project manifest. This manifest is a file which includes information about exported functions and types.

To generate a manifest, pass the --manifest flag to the compiler.

Importing

The manifest is then passed to the compiler so that the compiler will add the items from the other project to your current project as external imports.

Linking

Bramble currently uses gcc to perform linking. This is the final step for handling imports. You will need to pass the binary for your project and the binary for the imported project to gcc and it will output the final executable file.

Note: that only one binary can include a main function.

Examples

There are two examples of multi-file projects in the Bramble integration test suite: ./tests/src/projects/basic and ./tests/src/projects/nested.

All integration tests import the std::io project and the ./tests/make.sh script shows how to build a project with imports.

2 - Modules & Paths

Using Modules to Organize Code

Using Modules to Organize Code

Modules

A module is a purely organizational structure in Bramble. They allow you to group related functions and types together under a shared name.

Accessibility

The concepts of public and private and accessibility are not yet implemented in Bramble.

Default Modules

In Bramble, there are a few default modules that always exist and help to organize your code.

  • The project module. This is the parent module and corresponds to your project as a whole. There is only one project module in any given project. The project module is also what you import when you import another Bramble project into your code.
  • The file module. The file module is a module that represents a source code file and it has a name that is the same as the file name.
  • directory modules. If there are subdirectories in your project, those will be added as modules which contain all the times in those subdirectories.

Custom Modules

Within your own code, you can use mod to create modules that fall under the file module.

Using Paths

There are two types of paths in Bramble: relative and canonical. The relative path is a path that gives directions to an item starting from the current module, this path is dependent upon where it is used. The canonical path starts from the project root. This path will always point to the same item no matter where in the source code it is used.

Path Keywords

There are several path keywords.

  • project: all canonical paths begin with the project keyword.
  • self: self is an alias for the current module.
  • super: move to the current modules parent module.

Example

Here is an example of a module with another module in it:

mod my_mod {
    fn is_3(i: i64) -> bool {
        let x: i64 := self::inner::add(1, 2);
        return i == x;
    }
        
    fn plus(a: i64, b: i64) -> i64 {
        return a + b;
    }
    
    mod inner {
        fn add(a: i64, b: i64) -> i64 {
            return super::plus(a, b);
        }
    }
}

If this source code were contained in the project: example in the file foo.br, then the project module is example and the file module is foo. The canonical path to add is project::example::foo::my_mod::inner::add.

In this example, self::inner::add and super::plus represent relative paths.

3 - Types

Basics of Bramble Types

Basics of Bramble Types

Primitives

Keyword Description
bool boolean
i8 signed 8 bit integer
i16 signed 16 bit integer
i32 signed 32 bit integer
i64 signed 64 bit integer
u8 unsigned 8 bit integer
u16 unsigned 16 bit integer
u32 unsigned 32 bit integer
u64 unsigned 32 bit integer
f64 64 bit float
string C Style String
null A special type that corresponds to the null keyword. It represents a pointer that is not pointing to any address.

Arrays

Bramble supports staticly sized arrays of any type. The syntax for an array type is [<type>; <size>]. For example, [i32; 4] creates an array of 4 i32 values. Getting an element in array is done via the [] operators. For example, arr[0] will return the 0th element of the array arr.

Example:

    let arr: [i64; 2] := [0, 1];
    let x: i32 := arr[0];

Structures

A structure is a named aggregate type consisting of 0 or more named fields, where each field as a type. Fields can be primtive, arrays, or structures.

struct MyStruct {
    field: i32,
    foo: AnotherStruct,
    barr: [i8; 5],
}

To access a field in the structure, use the access operator: foo.bar. For an array of structures you would use arr[0].bar. For a structure in a structure: foo.bar.fizz.

Raw Pointers

Like most modern languages, Bramble will have a safe system for handling references, but to build that system and to support FFI a form of raw pointers is necessary. Raw pointers simply represent and address to a location in memory and offer no safe guards.

There are two types of raw pointers:

Keyword Description
*const T Raw pointer to an immutable location in memory
*mut T Raw pointer to a mutable location in memory

A raw pointer can be created for any type in the langauge, including structures and arrays.

Creating a Raw Pointer

@const x will create a raw pointer to any variable x. If x has type T then @const x will have type *const T. x can be immutable or mutable.

@mut x will create a raw pointer to a mutable location x. x must be mutable.

    let mut x: i32 := 5;

    let cp: *const i32 := @const x; // Create a raw pointer to an immutable location

    let p: *mut i32 := @mut x; // Create a raw pointer to a mutable location

The following would fail, because x is immutable:

    let x: i32 := 5;

    let p: *mut i32 := @mut x; // Error: cannot make a mutable pointer to immutable variable

Dereferencing a Raw Pointer

The ^ operator dereferences raw pointers.

Mutable raw pointers can be used on the left side of mutations:

    let mut x: i32 := 5;
    let p: *mut i32 := @mut x;
    mut ^p := ^p * 2;  // This will change the value stored in the location `x`

Unsafe Blocks

In the future, Bramble will require the use of raw pointers to be contained within unsafe blocks, as is standard for many languages.

4 - Thorn Protocol

The interface between Bramble and Thorn.

How Bramble communicates insight information to the Thorn insights platform.

Insights

The Thorn insights platform provides a first-class feature to give developers complete insight into everything Bramble does during compilation and why.

In order for Bramble to communicate with Thorn there is a communication protocol that defines how Bramble records every decision it makes. This is a set of JSON files that Bramble will generate when insight tracing is turned on.

Generating Insights

To generate the Thorn insights data, use the --json-trace flag on the Bramble compiler.

Thorn Protocol

Spans

The core value in the Thorn protocol is the span. This is a tuple that uniquely identifies a span of code in your project. Spans are unique across all the files in your project.

Source Map

In order to make sure that every file in your project can be assigned unique spans, each file must be assigned a span range from the global span space. Every span within a specific file will be contained within it’s span range in the Source Map file.

In theory, spans can cross files, but there are currently no cases where that would happen.

Example Source Map:

[
    {
        "source": "./main.br",
        "span": [0, 264]
    },
    {
        "source": "./second.br",
        "span": [264, 401]
    }
]

Trace File

The events themselves are recorded to trace.json. This file stores the trace of your source code through every stage of the compiler. It is consists of an array of compiler events. The compiler events represent decisions the compiler makes about different spans of your code. Each event consists of the span that caused the decision, the compiler stage, and information about the decision that was made.

Every event is given a unique integer ID. Some events have a parent ID, this means that the parent event triggered this event.

Event Results

Currently, an event can have one of two results: ok or err. Both of which have string values. ok means that the compiler successfully made a decision. err means that the compiler failed to make a decision and includes the error message detailing why the compiler failed.

Ref Spans

Some stages of compilation require context (for example, type resolution has to look up the declarations of functions). When a compiler decision needs to refer to another part of your code to make a decision, that reference is recorded with a ref span, which records the secondary span of your code that the compiler referred to.

Example Trace File

[{"id": 428, "parent_id": 427, "stage": "parser", "source": [393, 398], "ok": "Boolean Negate"},
{"id": 427, "parent_id": 426, "stage": "parser", "source": [393, 398]},
{"id": 426, "parent_id": 425, "stage": "parser", "source": [393, 398]},
{"id": 425, "parent_id": 424, "stage": "parser", "source": [393, 398]},
{"id": 424, "parent_id": 423, "stage": "parser", "source": [393, 398]},
{"id": 423, "parent_id": 422, "stage": "parser", "source": [393, 398]},
{"id": 422, "parent_id": 401, "stage": "parser", "source": [386, 398], "ok": "Return"},
{"id": 401, "parent_id": 321, "stage": "parser", "source": [353, 401], "ok": "Funtion Definition"},
{"id": 321, "stage": "parser", "source": [264, 401], "ok": "File Module"},
{"id": 437, "stage": "canonize-item-path", "source": [0, 401], "ok": "$basic::basic"},
{"id": 438, "stage": "canonize-item-path", "source": [0, 264], "ok": "$basic::main::main"},
{"id": 439, "stage": "canonize-item-path", "source": [0, 231], "ok": "$basic::main::my_main"},
{"id": 440, "stage": "canonize-item-path", "source": [233, 264], "ok": "$basic::main::MyStruct"},
{"id": 441, "stage": "canonize-item-path", "source": [255, 261], "ok": "$basic::main::f"},
{"id": 442, "stage": "canonize-item-path", "source": [264, 401], "ok": "$basic::second::second"},
{"id": 443, "stage": "canonize-item-path", "source": [264, 351], "ok": "$basic::second::my_bool"},
{"id": 444, "stage": "canonize-item-path", "source": [353, 401], "ok": "$basic::second::notter"},
{"id": 445, "stage": "canonize-item-path", "source": [363, 370], "ok": "$basic::second::b"},
{"id": 446, "stage": "canonize-type-ref", "source": [75, 89], "ok": "$basic::main::MyStruct"},
{"id": 447, "stage": "canonize-type-ref", "source": [153, 176], "ok": "$basic::second::my_bool"},
{"id": 448, "stage": "canonize-type-ref", "source": [305, 317], "ok": "$basic::second::notter"},
{"id": 449, "stage": "canonize-type-ref", "source": [321, 334], "ok": "$basic::second::notter"},
{"id": 453, "parent_id": 452, "stage": "type-resolver", "source": [39, 40], "ok": "i64"},
{"id": 455, "parent_id": 454, "stage": "type-resolver", "source": [44, 45], "ok": "i64"},
{"id": 454, "parent_id": 452, "stage": "type-resolver", "source": [43, 45], "ok": "i64"},
{"id": 452, "parent_id": 451, "stage": "type-resolver", "source": [39, 45], "ok": "i64"},
{"id": 451, "parent_id": 450, "stage": "type-resolver", "source": [26, 46], "ok": "i64"},
{"id": 459, "parent_id": 458, "stage": "type-resolver", "source": [87, 88], "ok": "i64"},
{"id": 458, "parent_id": 457, "stage": "type-resolver", "source": [75, 89], "ok": "$basic::main::MyStruct", "ref": [233, 264]}]

5 - Raw Pointers

How Bramble represents memory addresses, pointers to locations in memory, and the manipulation of memory directly.

How Bramble represents memory addresses, pointers to locations in memory, and the manipulation of memory directly.

Overview

In order to support low level memory operations and full FFI, Bramble must have the ability to operate on memory directly through pointers and pointer manipulation. As with many modern languages, Bramble wants users to avoid using these tools, except in the cases where they are the only or clearly correct option. To this end, the operations to get raw pointers are tedious and typing heavy and will, eventually, have to happen within an unsafe expression block.

Pointer Types

There are two raw pointer types: *const T and *mut T. *const T is a pointer to an immutable location in memory: dereferencing this pointer will let you read from this location in memory but it will not let you write to this location. *mut T is a pointer to a mutable location in memory: dereferencing this pointer will let you read from this location and write to this location.

Creating a Pointer

To create a pointer, use the address of operators: @const or @mut. @const will return a *const pointer to the operand. @mut will return a *const pointer to the operand, but it can only be applied to mutable values.

The operand of @const and @mut must be an addressable expression. This is an expression that resolves to a value that has a location in memory, generally this is a variable.

Dereferencing a Pointer

To read from or write to the location through a raw pointer, you need to use the dereference operator: ^. To read from a memory location you can use the ^pointer in any expression where the underyling type would be valid. To write to a memory location, use mut ^p := ....

Pointer Arithmetic

To get addresses that are offsets from a given pointer, use the @ in its binary form: p@-2 will return a new address which is equivalent to p - 2 * (size_of(T)) where T is the type of the value that p points to.