This provides reference documentation for the Bramble language and for the compiler.
Within this section will be clear and explicit documentation covering the features of the Bramble language, including syntax rules, semantic rules, and so on.
This is the multi-page printable view of this section. Click here to print.
This provides reference documentation for the Bramble language and for the compiler.
Within this section will be clear and explicit documentation covering the features of the Bramble language, including syntax rules, semantic rules, and so on.
Name | Description |
---|---|
null |
Represents a null reference or address. A reference that does not point to any location in memory |
null
null
represents a raw pointer value that does not point to any location in memory.
This is used to initialize a raw pointer with a placeholder value,
when it is not yet known where the pointer should point.
null
is also used to check if a pointer points to a valid location
in memory or not. For managing heap allocated memory, null
can
be used to check if memory has been allocated on the heap or not.
null
can be used for initialization, mutation, or comparison
operators, but not for any other operation.
Operator | Description |
---|---|
^ |
Dereference raw pointer |
== , != |
Equal to and Not Equal to |
< , <= , > , >= |
Comparison operators |
@const |
Address Of immutable addressable expression |
@mut |
Address Of mutable addressable expression |
@ |
Raw pointer offset |
==
& !=
==
checks if the value on the left and the value on the right are equal.
!=
checks if the value on the left and the value on the right are not equal.
For raw pointers, this compares the memory addresses stored in the pointers rather than the values stored at the memory address.
EQUAL := EXPRESSION == EXPRESSION
NOT_EQUAL := EXPRESSION != EXPRESSION
x, y: T, T: Primitive :- x == y -> bool
x, y: T, T: Primitive :- x != y -> bool
<
, <=
, >
, & >=
These are the ordering operations and compare two values to see what relative order they are too each other.
For raw pointers, these compare the memory addresses stored in the pointer rather than the values stored at the memory address.
LT := EXPRESSION < EXPRESSION
LTE := EXPRESSION <= EXPRESSION
GT := EXPRESSION > EXPRESSION
GTE := EXPRESSION >= EXPRESSION
x, y: T, T: Primitive :- x < y -> bool
x, y: T, T: Primitive :- x <= y -> bool
x, y: T, T: Primitive :- x > y -> bool
x, y: T, T: Primitive :- x >= y -> bool
@const
& @mut
The Address Of operators return a raw pointer to the location in memory
where the value of the operand is stored. The operand for @const
and @mut
must be an addressable expression.
An addressable expression is any expression which is logically tied to a location in memory and is not a temporary variable used for transferring data between calls. An example of an addressable expression is a named variable, or the element of an array which is bound to a variable. An addressable expression may be stored in the stack or in the heap.
An example of a non-addressable expression, which may have a location
in memory is the temporary value of a structure expression:
MyStruct{ x: 1, y: 2, z: 3}
.
This may be allocated to the stack to because it is too big to fit in
a register, but that location is considered strictly temporary and may get
used for later structure expressions, or, for small structures, be optimized
into a physical register or integer literal. Therefore, there can be no
guarantee that it can be safely addressed and it is considered non-addressable.
It’s important to remember that there are two different concepts of
mutability for a value of type pointer. The first, and most important,
is the mut
in *mut T
: this means that the location the pointer points
to can be mutated through the pointer. The second is the mut
that
refers to a variable of type *(const|mut) T
(let mut p: *const i64 ...
),
this means that the pointer variable can be mutated (not the location it points to);
in other words, that the pointer can be changed to point do a different address.
let i: i64 := 10;
let mut pi: *const i64 := @const i;
let mut j: i64 := 15;
let pj: *mut i64 := @mut j;
mut pi := @const j; // Here, `pi` is mutated to point to `j`.
// Note, that it still _cannot_ be used to mutate `j`
ADDRESS_OF := @(const|mut) EXPRESSION
The @
operator must be followed by either const
or mut
. This indicates
whether to pointer can be used to write to the memory location or only
to read from the memory location.
// x is an addressable value, T is a type
x: T :- @const x -> *const T
mut x: T :- @const x -> *const T, @mut x -> *mut T
@const
can take any addressable value, whether it is mutable or immutable
and will always return a value of *const T
, where T
is the type of the
operand.
@mut
can only be applied to an addressable value that is declared as mutable.
And will return a value of type *mut T
, where T
is the type of the operand.
This pointer can be used to write to the memory location as well as read from
the memory location.
^
Taking a *const T
or *mut T
value as an operand, the ^
operator accesses
the value stored in the location pointed to by the operand.
If the location is mutable (operand is of type *mut T
), then the
dereference expression can be used on the left of a mutation expression.
If T
is an array or a structure, then the result of the dereference
operation can be used as the left of element access or field access operators.
// Dereference a primitive value
let i: i64 := 5;
let p: *const i64 := @const i;
project::std::io::writei64ln(^p); // Prints 5
// Using the dereference in a mutation expression
let mut j: i64 := 10;
let pj: *mut i64 := @mut j;
mut ^pj := ^pj + 1; // Adds 1 to j
// Using with an array
let arr: [i32; 2] := [1i32, 2i32];
let p: *const [i32; 2] := @const arr;
^p[0]; // Will resolve to 1i32
DEREFERENCE := ^EXPRESSION
x: *const T :- ^x -> T
x: *mut T :- ^x -> mut T
@
The @
operator is a binary operator and is used for computing address
offsets from a raw pointer value. Given a pointer to a memory location,
you can use @
to move to the left or the right of that memory location.
The offsets are computed in increments of the size of the underlying type.
So, if p
is pointer to an i64
, then p@1
would increment the address
by 1*size_of(i64)
or 8
bytes and p@-3
would decrement the address by
-3*size_of(i64)
or -24
bytes.
The left operand must be of type *(const|mut) T
and the resulting type
will be the same as the left operand. The right operand must be an integer
type (either signed or unsigned).
let mut arr: [i64; 3] := [1, 2, 3];
let mut p: *const i64 := @const arr[1];
project::std::io::writei64ln(^(p@-1)); // Will print 1
let p2: *const i64 := p@1;
project::std::io::writei64ln(^p2); // Will print 3
RAW_POINTER_OFFSET := EXPRESSION @ EXPRESSION
x: *const T, o: i8|i16|i32|i64|u8|u16|u32|u64 :- x@o -> *const T
x: *mut T, o: i8|i16|i32|i64|u8|u16|u32|u64 :- x@o -> *mut T
Name | Description |
---|---|
as |
Allows primitive types to be cast between each other. |
The as
operator converts a value of one type to a value of another type.
As much as possible, the actual value is preserved, but there are certain
value ranges for which that cannot be done.
This can also be used to change the target type of a Raw Pointer
value. For example, *const MyStruct
can be changed to *const u8
.
Expression | Target Type | Description |
---|---|---|
Numerical | Numerical | Signed integers, unsigned integers, and floating point numbers can be cast between each other. |
*mut T |
*const Y or *mut Y |
Mutable raw pointers can be cast to constant or mutable raw pointers |
*mut T or *const T |
*const Y |
Any raw pointer can be cast to a constant raw pointer |
*mut T or *const T |
integer | Any raw pointer can be cast to an integer |
Integer | *const T |
An integer can be cast to a constant raw pointer |
Numerical types: bool i8 i16 i32 i64 u8 u16 u32 u64 f64
Integer types: bool i8 i16 i32 i64 u8 u16 u32 u64
Floating point types: f64
T
and Y
can be any type.
For raw pointer casts, the goal is to make it as difficult as possible to bypass the declared mutability of a location in memory.
When an integer is cast to an integer with higher bit width the additional bits need to be filled. If the source expression operand is signed, then the upcast will extend the sign bit. If the source expression operand is unsigned, then the upcast will do a zero extend.
Expression | Result | Description |
---|---|---|
1i8 as i32 |
1 |
Upcasting from signed to signed will result in the same value. |
-1i8 as i32 |
-1 |
The negative sign is extended resulting in the same value. |
-2i8 as u64 |
18446744073709551614 |
When upcasting a signed integer, the sign is extended. |
65535u16 as i64 |
65535 |
Unsigned integers are upcast by extending zeroes. |
When an integer is cast to a type with a smaller bit width, then the extra bits in the source are truncated.
Expression | Result | Description |
---|---|---|
-2i16 as i8 |
-2 |
These values are small enough that truncation has no effect. |
-5i16 as u8 |
254 |
u8 is formed by truncating all but the least significant byte from -5 . |
-500i16 as i8 |
12 |
Truncation converts the negative i16 to a positive i8 . |
FP to Int conversion takes the FP value and converts it to an integer value. If the FP value is outside the possible bounds of the target Integer type, then the result is a poison value. Conversion is always done by rounding towards zero.
For more details on poison values, see the LLVM Documentation.
EXPR as TYPE
S, T: Primitive Type, e: S :- e as T -> T