Design principles which are guiding the development of Bramble and Thorn.
This is the multi-page printable view of this section. Click here to print.
Concepts
- 1: Projects
- 2: Modules & Paths
- 3: Types
- 4: Thorn Protocol
- 5: Raw Pointers
1 - Projects
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
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 oneproject
module in any given project. Theproject
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 asmodules
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 theproject
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
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
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.
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.