Brink is a domain specific language for linking and composing of an output file. Brink simplifies construction of complex files by managing sizes, offsets and ordering in a readable declarative style. Brink tries to be especially useful when creating FLASH, ROM or other non-volatile memory images.
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/steveking-gh/brink/releases/download/7.0.2/brink-installer.sh | sh
Start a command prompt and execute the following:
powershell -ExecutionPolicy Bypass -c "irm https://github.com/steveking-gh/brink/releases/download/7.0.2/brink-installer.ps1 | iex"
Brink is written in rust, which works on all major operating systems. Installing rust is simple and documented in the Rust Getting Started guide.
From a command prompt, clone Brink and change directory to your clone. For example:
git clone https://github.com/steveking-gh/brink.git
cd brink
cargo test --release --all
All tests should pass, 0 tests should fail.
The previous build step created the Brink binary as ./target/release/brink.
You can install the Brink binary anywhere on your system. As a convenience,
cargo provides a per-user installation as $HOME/.cargo/bin/brink.
cargo install --path ./
Brink can assemble any number of input files into a unified output.
Brink can calculate relative or absolute offsets, allowing the output to contain pointer tables, cross-references and so on.
Brink can add pad bytes to force parts of the file to be a certain size.
Brink can add pad bytes to force parts of the file to start at an aligned boundary or at an absolute location.
Brink can write your own strings and data defined within your Brink source file.
Brink provides full featured assert and print statement support to help with debugging complex output files.
For a source file called hello.brink:
/*
* A section defines part of an output.
*/
section foo {
// Print a quoted string to the console
print "Hello World!\n";
}
// An output statement outputs the section to a file
output foo;
Running Brink on the file produces the expected message:
$ brink hello.brink
Hello World!
$
Brink also produced an empty file called output.bin. This file is the default
output when you don't specify some other name on the command line with the -o
option. Why is the file empty? Because nothing in our program produced output
file content -- we just printed the console message.
Let's fix that. We can replace the print command with the wrs command,
which is shorthand for 'write string':
/*
* A section defines part of an output.
*/
section foo {
// Write a quoted string to the output
wrs "Hello World!\n";
}
// An output statement outputs the section to a file
output foo;
Now, running the command again:
$ brink hello.brink
$
Produces output.bin containing the string Hello World!\n.
A Brink source file consists of one or more section definitions and exactly one output statement. The output statement specifies the top-level section that defines the output file. Starting from this top section, Brink recursively evaluates each nested section and command to produce the output file. For example, we can define a section with a write-string (wrs) command:
section foo { // Start a new section named 'foo'
wrs "I'm foo"; // wrs writes a string into the section.
}
output foo; // Final output
Produces a default output named output.bin.
$ cat output.bin
I'm foo
Using the wr command, sections can embed other sections:
section foo {
wrs "I'm foo\n";
}
section bar {
wrs "I'm bar\n";
wr foo; // nested section
}
output bar;
Produces output.bin:
$ cat output.bin
I'm bar
I'm foo
Users can extend Brink with custom data processing using Brink
extensions. Users write the output of their extension call
into a section with a wr.
section foo {
wrs "I'm foo\n";
}
section bar {
wrs "I'm bar\n";
wr foo; // nested section
}
section final {
wr bar;
wr my_stuff::crc(bar); // Write a 4 byte CRC hash for section 'bar'.
}
assert(sizeof(final) == 20);
output final;
To aid in debug, Brink supports assert and print statements in your
programs.
Assert expressions automate error checking. This example verifies our expectation that section 'bar' is 13 bytes long.
section bar {
wrs "Hello World!\n";
assert sizeof(bar) == 13;
}
output bar;
You can print this length information to the console during generation of your output:
section bar {
print "Output size is ", sizeof(bar), " bytes\n";
wrs "Hello World!\n";
assert sizeof(bar) == 13;
}
output bar;
Prints the console message:
Output size is 13 bytes
Unlike the GNU linker 'ld' concept of a location counter, Brink uses scoped addresses and scoped offsets to track locations. Addresses and offsets are 64-bit unsigned values that mark the position of the next byte of output. Brink allows users to reference and manipulate these values, adding pad bytes as necessary.
Importantly, addresses and offsets are scoped to their enclosing section.
When entering a nested (child) section, Brink saves the outer (parent) section's
inflight address and offset values. When exiting a child section, Brink
restores and updates the parent's address and offset values. From the
perspective of the parent section, a child section is a wr with the parent's
addresses and offsets updated per the size of the child.
For the specific case of the address and address offset, a child section
inherits these values by default from the parent section. If the child section
does not use set_addr, then the address and address offset simply continue
growing in step with the parent.
The only global (non-scoped) offset is the file_offset. Starting from 0, this
value monotonically increases to the end of the output file.
The following table provides a summary of the addresses and offsets used in Brink.
| Variable | Section Entry | Section Exit | set_addr |
pad_sec_offset |
pad_addr_offset |
pad_file_offset |
|---|---|---|---|---|---|---|
| Address | No Change | Restore & Update | Set | Pad Forward | Pad Forward | Pad Forward |
| Address Offset | No Change | Restore & Update | Set to 0 | Pad Forward | Pad Forward | Pad Forward |
| Section Offset | Set to 0 | Restore & Update | No change | Pad Forward | Pad Forward | Pad Forward |
| File Offset | No Change | No Change | No Change | Pad Forward | Pad Forward | Pad Forward |
The following diagram shows several address and offset concepts. Users specify
the starting logical address of the output section D using a region.
Alternatively, users can change the address within D using
set_addr at the top of the output section.
addr(D) used in the context of section D returns 0x8C00, not the starting address value 0xF000 nested within section C.
By address, Brink tracks all bytes written to the output. Brink reports an error if a program's offset or address manipulations cause more than one write to the same address.
Brink enforces that set offset commands must specify an offset change greater or equal to 0. Brink emits pad bytes into the output for any offset change greater than 0.
Brink emits an error if an address or offset change causes 64-bit unsigned overflow. In other words, programs cannot use unsigned overflow wrapping back to 0.
As a mental model, user's can think of program execution as occurring in output order. Output order means the sequence of operations that produce bytes in-order starting with the initial byte of the output file. In other words, an operation producing the first byte of the output will execute before an operation producing the second byte.
Within a section definition, output order and source code order are the same. However, outside of a section definition, output order and program order may differ. For example, source code may define whole sections in a different order than instantiated into in the output.
This section provides an overview of Brink's internal output creation phases.
constEvaluation Phase: First, Brink evaluates all const expressions. This phase includes evaluation of allif/elsestatements and the dependent const-time operations such asincludestatements in the taken path.- Layout Phase: Next, Brink iteratively evaluates all expressions that
affect output size and layout. For example, Brink evaluates
alignexpressions and extensionsize()calls during this phase. Brink skips data generation, since knowing the size of operations suffices to determine the precise output structure. This phase completes when successive layout iterations produce identical results. - Generate Phase 1: Next, Brink begins populating data values into the
output. In this first generation phase, Brink first evaluates
wrstatements that do NOT call extensions. Brink evaluates wr calls in output order. - Generate Phase 2: Next, Brink evaluates
wrstatement that call an extension. Like before, brink evaluates extension calls in output order. Brink executes all extension calls serially on the engine thread. - Validation Phase: Finally, Brink evaluates
assertstatements, including those that call extensions. Note that Brink may take an early exit in any phase if anassertstatement will unambiguously fail.
brink [OPTIONS] <input>
The required input file contains the brink source code to compile and build the output file. Brink source files typically have a .brink file extension.
| Option | Description |
|---|---|
-D<name>[=value] |
Defines a const value from the command line.See Command-Line Const Defines below. |
--list-extensions |
List all available extensions compiled into brink as controlled by Cargo feature flags. |
--max-output-size=<size> |
Reject the output if its size exceeds <size> bytes before writing data.Accepts a plain integer or a K/M/G suffix (e.g. 64M, 512K, 1G). Default is 256M. |
--map-csv |
Writes a CSV format map file <stem>.map.csv to the current directory.For example: firmware.brink → firmware.map.csv. |
--map-csv=<file> |
Writes a CSV map file to the specified file. |
--map-csv=- |
Writes a CSV map file to stdout. |
--map-c99 |
Writes a C99 header file <stem>.map.h to the current directory.For example: firmware.brink → firmware.map.h. |
--map-c99=<file> |
Writes a C99 header to the specified file. |
--map-c99=- |
Writes a C99 header to stdout. |
--map-json |
Writes a JSON format map file <stem>.map.json to the current directory.For example: firmware.brink → firmware.map.json. |
--map-json=<file> |
Writes a JSON map to the specified file. |
--map-json=- |
Writes a JSON map to stdout. |
--map-rs |
Writes a Rust module file <stem>.map.rs to the current directory.For example: firmware.brink → firmware.map.rs. |
--map-rs=<file> |
Writes a Rust module map to the specified file. |
--map-rs=- |
Writes a Rust module map to stdout. |
--noprint |
Suppress print statement output from the source program. |
-o <file> |
Output file name. Defaults to output.bin. |
-q, --quiet |
Suppress all console output, including errors. Overrides -v. Useful for fuzz testing. |
-v |
Increase verbosity. Repeat up to four times (-v -v -v -v). |
When the user does not specify a path, Brink writes map file(s) and the output to the current working directory.
The -D option injects a const definition into the
program from the command line. This option is modelled after the GCC -D
preprocessor syntax. You can specify -D multiple times, once per each
definition. For example:
brink -DBASE=0x8000 -DCOUNT=16 firmware.brink
The name must be a valid Brink identifier. The value is
optional; without a value, Brink sets the const to 1, with type Integer,
following the GCC boolean-flag convention.
-D overrides any same-named const definition in the source.
Map output lists all const definitions including -D consts.
Brink knows or infers the type from the value string using the same rules as source code for type inference.
| Example | Value | Type | Description |
|---|---|---|---|
-DFLAG |
1 | Integer |
Defaults to true (1). |
-DCOUNT=16 |
16 | Integer |
Plain decimal → Integer |
-DBASE=0x1000 |
0x1000 | U64 |
Hex/binary without suffix → implicit U64 |
-DBASE=0x1000u |
0x1000 | U64 |
u suffix → explicit U64 |
-DOFFSET=0x40i |
0x40 | I64 |
i suffix → explicit I64 |
-DDELTA=-4 |
-4 | I64 |
Negative decimal → implicit I64 |
Define a base address and section count at the command line:
brink -DBASE=0x0800_0000 firmware.brink -o firmware.bin
The source can reference BASE as an ordinary const:
section entry { wr8 0x01; }
section top { set_addr BASE; wr entry; }
output top;
Brink supports C language line and block comments.
Brink supports lenient C language style whitespace rules.
Like C language, statements must be terminated with a trailing semicolon character.
Brink supports the following data types:
- U64: 64-bit unsigned values
- I64: 64-bit signed values
- Integer: 64-bit integers with flexible sign treatment
- String: UTF-8 string in double quotes
Brink reports an error for under/overflow on arithmetic operations on U64, I64 and Integer types as described in Arithmetic Operators.
An identifier begins with a letter (A–Z, a–z) or an underscore (_), followed by zero or more letters, digits (0–9), or underscores. Identifiers are case-sensitive.
Brink reserves certain identifiers and rejects their use as section names, const names, or label names at compile time.
Brink also reserves two identifier prefixes. Any user defined identifier beginning with a reserved prefix triggers an error.
| Reserved Prefix | Reason |
|---|---|
wr + digit |
Numeric write instructions (wr8, wr16, wr32, and future width variants) |
__ |
Leading double underscore names refer to builtin identifiers. |
Brink also reserves the following exact keywords:
| Reserved Keyword | Reason / possible future use |
|---|---|
import |
Module inclusion |
if |
Conditional section inclusion |
else |
Conditional section inclusion |
true |
Boolean literal |
false |
Boolean literal |
extern |
External section references |
let |
Variable declarations |
fill |
Fill / pad byte ranges |
Keyword reservation is case-sensitive. Fill and FILL are valid identifiers; fill is not.
Brink supports number literals in decimal, hex (0x) and binary (0b) forms. After the first digit, you can use '_' within number literals to help with readability.
assert 42 == 42;
assert -42 == -42;
assert 0x42 == 0x42;
assert 0x42 == 66;
assert 0x4_2 == 66;
assert 0x42 == 6_6;
assert 0b0 == 0;
assert 0b01000010 == 0x42;
assert 0b0100_0010 == 0x42;
assert 0b101000010 == 0x142;
assert 0b0000_0000_0100_0010 == 0x42;
The following table summarizes how Brink determines the type of number literals.
| Example | Type | Description |
|---|---|---|
| 4 | Integer | Simple decimal numbers are Integer type with flexible signedness |
| 4u | U64 | Explicitly U64 |
| 4i | I64 | Explicitly I64 |
| -4 | I64 | Negative numbers are I64 |
| 0x4 | U64 | Hex numbers are U64 by default |
| 0x4i | I64 | Explicitly I64 hex number |
| 0b100 | U64 | Binary numbers are U64 by default |
Brink does not support negative hex or binary literals.
For convenience, the compiler casts the flexible Integer type to U64 or
I64 as needed.
assert 42u == 42; // U64 operates with Integer
assert 42i == 42; // I64 operates with Integer
Otherwise the types used in an expression must match. For example:
assert 42u == 42i; // mix unsigned and signed
Produces an error message:
[EXEC_13] Error: Input operand types do not match. Left is 'U64', right is 'I64'
╭─[tests/integers_5.brink:2:12]
│
2 │ assert 42u == 42i; // mix unsigned and signed
· ^^^ ^^^
───╯
Users can explicitly cast a number literal or expression to the required
signedness using the built-in to_u64 to to_i64 functions. For example:
assert -42 != to_i64(42); // comparing signed to unsigned
The to_u64 and to_i64 functions DO NOT report an error if the runtime
value under/overflows the destination type.
assert 0xFFFF_FFFF_FFFF_FFFF == to_u64(-1); // OK
assert to_i64(0xFFFF_FFFF_FFFF_FFFF) == -1; // OK
Decimal number literals accept an optional K/M/G magnitude suffix (case sensitive) before the type suffix.
| Suffix | Multiplier | Example | Value |
|---|---|---|---|
K |
1024 | 64K |
65536 |
M |
1024 × 1024 | 1M |
1048576 |
G |
1024 × 1024 × 1024 | 2G |
2147483648 |
Magnitude and type suffixes combine: 4Ku is 4096 as a U64, -1Ki is -1024 as
an I64.
Brink considers a zero value false and all non-zero values true.
Brink allows utf-8 quoted strings with the following escape characters:
| Escape Character | UTF-8 Value | Name |
|---|---|---|
| \0 | 0x00 | Null |
| \t | 0x09 | Horizontal Tab |
| \n | 0x0A | Linefeed |
| \" | 0x22 | Quotation Mark |
Newlines are Linux style, so "A\n" is a two byte string on all platforms.
Brink supports the following arithmetic operators with same relative precedence as the Rust language. Where applicable, Brink checks for arithmetic under/overflow.
| Precedence | Operator | Under/Overflow Check? |
Description |
|---|---|---|---|
| Highest | ( ) | n/a | Paren grouping |
| * / | yes | Multiply and divide | |
| + - | yes | Add and subtract | |
| & | n/a | Bitwise-AND | |
| | | n/a | Bitwise-OR | |
| << >> | no | Bitwise shift up and down | |
| == != | n/a | Equal and non-equal | |
| >= <= | n/a | Greater-than-or-equal and less-than-or-equal | |
| && | n/a | Logical-AND | |
| Lowest | || | n/a | Logical-OR |
addr( [identifier] ) -> U64
When called with an identifier, returns the address of the identifier as a U64. When called without an identifier, returns the current address. See Addresses and Offsets for more information.
The following table shows the scoping rules for addr. To summarize, Brink
tracks exactly one address value per name. An addr(<name>) command
retrieves that one value regardless of the scope of the caller.
| Command Form | Scope used to determine address |
|---|---|
addr() |
Scope of current section |
addr(<section name>) |
Scope of parent section that contains the child section |
addr(<output section name>) |
Scope of the output section |
addr(<label name>) |
Scope of the section that contains the label |
Example:
const BASE = 0x1000u;
section fiz {
assert addr() == BASE + 6;
wrs "fiz";
assert addr() == BASE + 9;
assert addr(foo) == BASE;
}
section bar {
assert addr() == BASE + 3;
wrs "bar";
assert addr() == BASE + 6;
wr fiz;
assert addr() == BASE + 9;
}
// top level section
section foo {
set_addr BASE;
assert addr() == BASE;
wrs "foo";
assert addr() == BASE + 3;
assert addr(fiz) == BASE + 6;
wr bar;
assert addr() == BASE + 9;
assert addr(bar) == BASE + 3;
}
output foo;
addr_offset( [identifier] ) -> U64
Returns the offset from the output or most recent set_addr anchor as a U64.
When called without an identifier, returns the current address offset. When
called with an identifier, returns the address offset at the start of the named
section or label.
The offset resets to zero on each set_addr call.
The following table shows the scoping rules for addr_offset. To summarize,
Brink tracks exactly one address offset value per name. An
addr_offset(<name>) command retrieves that one value regardless of the scope
of the caller.
| Command Form | Scope used to determine address |
|---|---|
addr_offset() |
Scope of current section |
addr_offset(<section name>) |
Scope of parent section that contains the child section |
addr_offset(<output section name>) |
Scope of the output section |
addr_offset(<label name>) |
Scope of the section that contains the label |
Example:
const BASE = 0x1000u;
section fiz {
assert addr_offset() == 6;
wrs "fiz";
assert addr_offset() == 9;
assert addr_offset(foo) == 0;
}
section bar {
assert addr_offset() == 3;
wrs "bar";
assert addr_offset() == 6;
wr fiz;
assert addr_offset() == 9;
}
// top level section
section foo {
set_addr BASE;
assert addr_offset() == 0;
wrs "foo";
assert addr_offset() == 3;
assert addr_offset(fiz) == 6;
wr bar;
assert addr_offset() == 9;
assert addr_offset(bar) == 3;
}
output foo;
align <expression> [, <pad byte value>];
The align statement writes pad bytes into the current section until the absolute location counter reaches the specified alignment. Align writes 0 as the default pad byte value, but the user may optionally specify a different value.
Example:
section foo {
wrs "Hello";
align 32;
assert sizeof(foo) == 32;
assert addr() == 32;
}
output foo;
assert <expression>;
The assert statement reports an error if the specified expression does not evaluate to a true (non-zero) value. Assert expressions provide a means of error checking and do not affect the output file.
Example:
section foo {
assert 1; // OK, non-zero is true
assert -1; // OK, non-zero is true
assert 1 + 1 == 2;
}
output foo;
const <identifier> = <expr>;
A const expression creates an immutable user defined identifier for a value. The value can consist of a number or string literal, or an expression composed of other constants and literals. Const identifier names have global scope and must be globally unique. Const identifiers cannot conflict with any other global identifiers such as section names.
Example:
const RAM_BASE = 0x8000_0000u; // User defined unsigned constant.
section foo {
set_addr RAM_BASE;
wr64 RAM_BASE;
print "RAM base address is ", RAM_BASE, "\n";
}
output foo;
Const expressions support the full set of arithmetic, bitwise and comparison operators. Comparison operators evaluate to 1 (true) or 0 (false) and are useful for expressing relationships between constants:
const FLASH_BASE = 0x0800_0000;
const FLASH_SIZE = 0x0008_0000;
const RAM_BASE = 0x2000_0000;
// Verify flash and RAM regions do not overlap
const NO_OVERLAP = (FLASH_BASE + FLASH_SIZE) <= RAM_BASE;
assert NO_OVERLAP;
A const value expression cannot depend on addresses, sizes, offsets or any other dynamic aspect of the output file. Brink resolves all const values before starting layout of the output. For example:
const RAM_BASE = 0x8000_0000; // OK, just a 64b unsigned literal.
const RAM_SIZE = 32768; // OK, just a 64b integer literal.
const RAM_END = RAM_BASE + RAM_SIZE; // OK, const composed of other consts.
section foo {
wrs "Hello\n";
}
const RAM_USED = sizeof(foo); // ERROR! Const cannot depend on section properties.
output foo;
const variables support deferred assignment. This allows the user to declare
a const variable, then assign a value to the variable exactly once in later
code. For example:
const IO_START;
...
IO_START = 0xF000_0000_0000_0000;
Deferred assignment is primarily useful in
if/else statements, which allow users to
conditionally determine the value to assign.
To provide errors and warnings, Brink tracks the defined/undefined and used/unused state of each variable.
if <expression> { ... } else { ... }
Allows conditional execution of other statements. As described in Output
Creation, Brink evaluates all if/else statements
before starting layout of the output. Therefore, an if/else expression must
only depend on const variables and literal values. In other words, if/else
statements must not depend on dynamic addresses, sizes, offsets or any other
layout dependent aspect of the output file.
Users must pre-declare const variables before conditionally assigning values
to them. For example:
// Assume the user specified -DMEM_CONFIG="BIG" on the command line.
// Pre-declare variables prior to conditional assignment in an if/else.
// Brink strictly tracks variable definitions to prevent use of
// uninitialized variables.
const FLASH_SIZE;
const RAM_SIZE;
print "Memory configuration is ", MEM_CONFIG, "\n";
if MEM_CONFIG == "BIG" {
FLASH_SIZE = 0x8_0000;
RAM_SIZE = 0x80_0000;
include "big_config.brink";
} else {
if MEM_CONFIG == "MEDIUM" {
FLASH_SIZE = 0x4_0000;
RAM_SIZE = 0x40_0000;
include "medium_config.brink";
} else {
if MEM_CONFIG == "SMALL" {
FLASH_SIZE = 0x2_0000;
RAM_SIZE = 0x20_0000;
include "small_config.brink";
} else {
print "Invalid configuration. MEM_CONFIG must be BIG, MEDIUM, or SMALL.\n";
assert(0); // Halt execution
}
}
}
If the taken path in an if/else statement does not assign a value to a
predeclared const variable, then Brink reports an error if any later program
statement uses that variable.
For compactness, user's may omit braces around an else/if block. For example:
if MEM_CONFIG == "BIG" { include "big_config.brink"; }
else if MEM_CONFIG == "MEDIUM" { include "medium_config.brink"; }
else if MEM_CONFIG == "SMALL" { include "small_config.brink"; }
else { assert(0); }
include "<file>";
Includes another Brink source file. Brink processes the included file as if it were part of the current file. For example, the included file can define sections, labels, constants and nested include files.
An included file may contain an output statement. Brink will enforce that the
entire program after include file resolution contains only one output statement.
See the output
statement for more
information.
The default path for an included file is the directory of the source file that
contains the include statement. For example, if main.brink is in
/home/user/project/ and contains include "sections.brink", then Brink will
look for /home/user/project/sections.brink.
Include files starting with a / are absolute paths. Likewise, Brink supports
relative paths such as ../.
All paths use Linux style forward slashes.
Example:
// file: main.brink
include "../constants.brink";
include "sections.brink";
output main_rom;
// file: ../constants.brink
const RAM_BASE = 0x8000_0000u;
// file: sections.brink
section main_rom {
set_addr 0x1000;
wrs "Hello\n";
}
<identifier>:
Labels assign an identifier to a specific location in the output file. Programs can then refer to the location of the label by name. Labels names have global scope and label names must be globally unique. Multiple different labels can refer to the same location.
Labels have the form <label identifier>: and can prefix most statement types.
For example:
section foo {
set_addr 0x1000;
// assign the label 'lab1' to the current location
lab1: wrs "Wow!";
// assign the label 'lab2' to the current location
lab2:
assert addr(lab1) == 0x1000;
assert addr(lab2) == 0x1004;
assert addr(lab3) == 0x1004;
// yet another label, same location as 'lab2'
lab3:
}
output foo;
output <section identifier>;
An output statement specifies the top section to write to the output file. Use
set_addr inside the section to control the absolute starting address, or place
the top section in region with a start address.
A Brink program must have exactly one output statement.
An include file may contain an output statement. Brink will enforce that the
entire program after include file resolution contains only one output statement.
print <expression> [, <expression>, ...];
The print statement evaluates the comma separated list of expressions and prints
them to the console. For expressions, print displays unsigned values in hex and
signed values in decimal. If needed, the to_u64 and to_i64 functions can
control the output style.
Brink executes a given print statement for each instance found in the output file. In other words, a print statement in a section written multiple times will execute multiple times in the order found.
Example:
const BASE = 0x1000;
section bar {
print "Section 'bar' starts at ", addr(), "\n";
wrs "bar";
}
// top level section
section foo {
set_addr BASE;
print "Output spans address range ", BASE, "-", BASE + sizeof(foo),
" (", to_i64(sizeof(foo)), " bytes)\n";
wrs "foo";
wr bar;
wr bar;
wr bar;
}
output foo;
Will result in the following console output:
Output spans address range 0x1000-0x100C (12 bytes)
Section 'bar' starts at 0x1003
Section 'bar' starts at 0x1006
Section 'bar' starts at 0x1009
region <identifier> { ... }
A region defines the name and static properties of an address range. Regions provide a way to decouple memory placement and top-down layout control from the section content being placed. Unlike sections, regions are stateless and do not track dynamic information during layout.
Users place exactly one section in a region. We refer to this section as
the bound section of the region. The bound section is a normal
section with the following extra behaviors:
- The region sets the starting address of the bound section.
- The region caps the size of the bound section.
For example:
// Define the properties of the FLASH memory region
region FLASH {
addr = 0xF000_0000;
size = 1M;
}
// Define the properties of the EEPROM memory region
region EEPROM {
addr = 0xFF00_0000;
size = 64K;
}
// Flash sections
section boot { ... }
section flash_code { ... }
section flash_data { ... }
// EEPROM sections
section eeprom_data1 { ... }
section eeprom_data2 { ... }
// FLASH_TOP is the bound section in the FLASH region
section FLASH_TOP in FLASH {
// Starts at address 0xF000_0000
wr boot;
wr flash_code;
wr flash_data;
}
section EEPROM_TOP in EEPROM {
assert addr() == 0xFF00_0000;
wr runtime_code;
wr runtime_data;
}
// The output file contains the image for FLASH and EEPROM regions.
// This section is not a bound section of a region and behaves
// like any other section.
section FIRMWARE_UPDATE_FILE {
wr file_offset(FLASH_TOP); // Offset to the new FLASH image
wr file_offset(EEPROM_TOP); // Offset to the EEPROM image
wr FLASH_TOP; // FLASH image
wr EEPROM_TOP; // EEPROM image
}
output FIRMWARE_UPDATE_FILE; // Write the output
Regions support the following properties:
addrStarting address (required)sizeSize in bytes (required)
The addr property defines the region's absolute starting address. The
region's bound section starts at this address. Users can query the addr
property of a region with addr(<region name>).
Specifies the size of the region in bytes. Brink reports an error if the size of the bound section exceeds this value.
Users can query the size property of a region with sizeof(<region name>).
The size value accepts a K/M/G magnitude suffix.
Regions provide automatic size and boundary checking for all operations in the region. In practical terms this means:
- Write commands in a region cannot extend outside the region
- Address manipulation in a region cannot result in an address outside the region
- Offset manipulations in a region cannot result in a offset outside the region.
For example, the following wr32 command would extend outside the region by one
byte, resulting in an error:
region LITTLE_ROM {
addr = 0;
size = 7;
}
section data in LITTLE_ROM {
// occupies bytes 0-3
wr32 0x12345678;
// Occupies bytes 4-7, which extends 1 byte outside the region
wr32 0x87654321; // ERROR!
}
Of course, region enforcement occurs not just in the region's bound section, but in any reachable section. For example:
region LITTLE_ROM {
addr = 0;
size = 7;
}
section nested_stuff {
pad_sec_offset 6; // pad to last byte of region
wr more_nested;
}
section more_nested {
wr std::crc32c(more_nested); // 4 bytes of output
}
section data in LITTLE_ROM {
wr nested_stuff; // ERROR! Data written outside of region
}
The set_addr command and any offset manipulation commands are also constrained to fit in the region. For example:
region FLASH {
addr = 0xF000_0000;
size = 1M;
}
section foo in FLASH {
assert addr() == 0xF000_0000; // Start of FLASH region
set_addr 0xF000_1000; // OK, inside the region
wrs "Inside region!";
set_addr 0xA000_0000; // ERROR, outside the region
wrs "Outside region!";
}
output foo;
Users can freely nest sections in different regions into each other. However, Brink allows write operations only in the address range intersection permitted by all the parent regions. For example:
region READ_ONLY {
addr = 0xF000_0000;
size = 0x1000_0000;
}
region FLASH {
addr = 0xF100_0000;
size = 64K;
}
section flash_data in FLASH {
assert addr() == 0xF100_0000;
...
}
section ro_data in READ_ONLY {
assert addr() == 0xF000_0000;
// OK, region FLASH is a subset of READ_ONLY.
// Note that the FLASH region anchors the starting address
// at 0xF100_0000. This creates a logical (unpadded) address gap
// in the ro_data section between 0xF000_0000 and 0xF100_0000.
wr flash_data;
}
output ro_data;
For completeness, the region of a nested section need not be a proper subset of the parent region. Brink still enforces the constraints of all parent sections as follows:
- Any address written by the child section must lie in the intersection of all parent regions.
- The starting address of a nested section must fit the address range allowed by all parent regions.
Placing a section in a region forces the starting address of the section to the
region's addr value. Writing this section more than once results in an
address address conflict with the previous instance of the section.
sec_offset( [identifier] ) -> U64
When called with an identifier, returns the unsigned 64-bit offset of the identifier from the start of the section that contains the identifier. When called without an identifier, returns the offset from the start of the current section.
Example:
section fiz {
assert sec_offset() == 0;
wrs "fiz";
assert sec_offset() == 3;
}
section bar {
assert sec_offset() == 0;
wrs "bar";
assert sec_offset() == 3;
wr fiz;
assert sec_offset() == 6;
assert sec_offset(fiz) == 3;
}
// top level section
section foo {
assert sec_offset() == 0;
wrs "foo";
assert sec_offset() == 3;
wr bar;
assert sec_offset() == 9;
}
output foo;
When a section offset specifies an identifier, the identifier must be in the scope of the current section. For example:
section fiz {
wrs "fiz";
}
section bar {
wr fiz;
assert sec_offset(fiz) == 0; // OK fiz in scope in section bar
}
section foo {
wr bar;
assert sec_offset(bar) == 0; // OK, bar is local in this section
assert sec_offset(fiz) == 0; // ERROR, fiz is out of scope in section foo
}
output foo;
section <name> [in <region>] { ... }
A section is a named, reusable block of content. Sections are the primary
building block of a Brink program. Each section defines a sequence of bytes,
built up from write statements and padding operations such as align.
Sections may also contain labels, assertions, print statements and so on.
Sections may write other sections into themselves so long as the nesting does
not create a cycle.
Section names must be valid identifiers, must be globally unique, and must not conflict with const names, label names, region name, or reserved identifiers.
Sections have their own section-relative location counter which resets to zero
at the start of each section. Sections can read and advance the section
location counter with sec_offset() and
pad_sec_offset() statements
respectively.
The root section named in the output statement is the only section
Brink writes to the output file. Other sections can be directly or indirectly
included via wr statements from the output section. Unreachable
sections produce a warning.
To help guide layout, users can place a exactly one section in a
region with in <region name> after the section name. We call a
section placed in a region as the bound section of the region.
Example:
section magic {
wrs "FIRM"; // 4-byte magic number
wr8 0x01; // version
assert sec_offset() == 5; // Section location counter should be 5
}
section body {
wr8 0xAA, 16; // 16 bytes of payload
}
section image {
wr magic;
align 256; // Body should start on 256 byte boundary
wr body;
assert sizeof(image) == 272; // 256 + 16
}
output image;
set_addr <expression>;
The set_addr command forces the current address to the specified value and
resets the current addr_offset to zero. These changes happen within the scope
of the containing section. Child sections inherit the parent section's addr
and addr_offset values.
Using set_addr does not change the value of the section offset nor file
offset. A set_addr command does not add pad bytes to the output.
The set_addr command may move the address forward or backwards. However,
Brink tracks every output byte by address and reports an error if a program
tries to write to the same address more than once.
Example:
section foo {
wr8 1;
wr8 2;
wr8 3;
wr8 4;
wr8 5;
set_addr 16;
assert addr() == 16;
assert addr_offset() == 0; // set_addr resets addr_offset
assert file_offset() == 5; // set_addr does not pad
assert sec_offset() == 5;
wr8 0xAA, 3;
assert addr_offset() == 3;
assert file_offset() == 8;
assert sec_offset() == 8;
pad_sec_offset 24, 0xFF; // Adds 24 - 8 = 16 pad bytes
assert addr() == 35; // 19 + 16 = 35
assert addr_offset() == 19; // 3 + 16 = 19
assert file_offset() == 24; // 8 + 16 = 24
assert sec_offset() == 24; // 8 + 16 = 24
}
output foo;
When used in a section in a region, Brink reports an error if the set_addr command sets the address outside of a region.
pad_addr_offset <expression> [, <pad byte value>];
Pads the output until addr_offset reaches the specified value. Users may
specify an optional pad byte value or use the default value of 0.
If the specified value is less than the current addr_offset, Brink reports an
error.
pad_addr_offset is most useful after a set_addr call, because set_addr
resets addr_offset to zero. This lets users pad to a size relative to their
chosen address anchor without knowing what the surrounding section's
sec_offset happens to be.
Example:
const BASE = 0x1000;
section header {
wrs "FIRM"; // 4-byte magic number
wr8 0x01; // version byte
} // addr_offset == 5 on exit
section body {
set_addr BASE;
wr header;
// Relocate body to its target load address.
// addr_offset resets to 0.
set_addr 0xF000;
wr8 0xAA, 3; // 3 bytes of payload
// Pad to 0x20 bytes from the 0xF000 anchor.
pad_addr_offset 0x20;
assert addr() == 0xF020;
assert addr_offset() == 0x20;
assert sec_offset() == 0x25; // 5 (header) + 3 (payload) + 29 (pad) = 0x25
}
output body;
pad_file_offset <expression> [, <pad byte value>];
The pad_file_offset command pads the output file until the file offset reaches the specified value. Users may specify an optional pad byte value or use the default value of 0.
If the specified offset is less the current offset, Brink reports an error.
pad_file_offset is most useful when a section is written inside a parent
section, because sec_offset resets to zero at the start of each child section
while file_offset continues from the parent's position. This lets a child
section pad to an absolute file position regardless of where the parent places
it.
Example:
// A firmware container: an 8-byte header at file offset 0, followed by a
// payload that must start at file offset 512 for bootloader compatibility.
section header {
wrs "FIRM"; // 4-byte magic
wr32 0x00000001; // version
}
section payload {
// firmware writes header first (8 bytes), so payload opens at
// file_offset 8. Pad to the protocol-required file position 512.
pad_file_offset 512, 0xFF;
assert file_offset() == 512; // absolute position in the output file
assert sec_offset() == 504; // sec_offset starts from 0 inside payload
wrs "PAYLOAD"; // 7 bytes of payload data
assert file_offset() == 519;
assert sec_offset() == 511;
}
section firmware {
wr header;
wr payload;
}
output firmware;
pad_sec_offset <expression> [, <pad byte value>];
The pad_sec_offset command pads the current section until the section offset reaches the specified value. Users may specify an optional pad byte value or use the default value of 0.
If the specified offset is less the current offset, Brink reports an error.
Example:
section foo {
wr8 1;
wr8 2;
wr8 3;
wr8 4;
wr8 5;
pad_sec_offset 16;
assert addr() == 16;
assert file_offset() == 16;
assert sec_offset() == 16;
wr8 0xAA, 3;
pad_sec_offset 24, 0xFF;
assert addr() == 24;
assert file_offset() == 24;
assert sec_offset() == 24;
pad_sec_offset 24, 0xEE; // should do Nothing
wr8 0xAA, 3;
pad_sec_offset 27, 0x33; // should do nothing
pad_sec_offset 28, 0x77; // should pad to 28
assert sizeof(foo) == 28;
}
output foo;
sizeof( <identifier> ) -> U64
Returns the size in bytes of the specified identifier.
Example:
section empty_one {}
section foo {
wrs "Wow!";
wr empty_one;
assert sizeof(empty_one) == 0;
assert sizeof(foo) == 4;
}
output foo;
When called with an extension identifier, sizeof returns the size of the
extension's output. For example:
print "CRC size=", sizeof(std::crc32c); // returns "CRC size=4"
When called with a region identifier, sizeof returns the fixed size of the region regardless of whether the user's program writes any data in the region.
region FLASH { ...; size = 8K; ... }
...
print "FLASH size=", sizeof(FLASH); // returns "FLASH size=8192"
When called with a section identifier, sizeof returns the size of the section in the file. Therefore, this size does not take into account operations that do not write data nor pad bytes. For example, address jumps, e.g. by using set_addr do not change the sizeof() result for a section.
section foo {
set_addr 0;
wrs "Hello\n";
// Address jumps by 0x1000, but no data nor pads written, so
// no effect on sizeof(foo).
set_addr 0x1000;
wrs "World\n";
assert sizeof(foo) == 12;
}
to_i64( <expression> ) -> I64
Converts the specified expression to the I64 type without regard to under/overflow.
Example:
section foo {
assert to_i64(0xFFFF_FFFF_FFFF_FFFF) == -1;
assert to_i64(42u) == 42;
assert to_i64(42u) == 42i;
assert to_i64(42) == 42i;
}
output foo;
to_u64( <expression> ) -> U64
Converts the specified expression to the U64 type without regard to under/overflow.
Example:
section foo {
assert 0xFFFF_FFFF_FFFF_FFFF == to_u64(-1);
assert to_u64(42i) == 42;
assert to_u64(42i) == 42u;
assert to_u64(42) == 42u;
}
output foo;
The wr command has two forms. The first form writes the contents of another
section into the current section. The second wr form invokes an extension and
writes the output into the current section.
wr <section identifier>;
Brink adds the specified in section to the current section at the current section offset.
wr <namespace>::<extension_name>(<arg1>, <arg2>, ...);
Evaluates the specified extension call and writes the result to the output. The
extension's .size() method specifies the size of the result. See Brink
Extensions for more information.
Using wr, you can build complex outputs by composing smaller, modular sections
together.
Example:
section header {
wrs "FILE"; // Write a string.
wr8 0x01; // Write a byte.
}
section data {
wrs "DATA";
wr8 0xFF, 4;
}
// Compose the top-level section
section my_firmware {
wr header;
wr data;
// Use an extension to append a CRC to a section.
// Extensions can refer to their containing section.
wr std::crc32c(my_firmware);
}
output my_firmware;
wr8 <expression> [, <expression>];
wr16 <expression> [, <expression>];
wr24 <expression> [, <expression>];
wr32 <expression> [, <expression>];
wr40 <expression> [, <expression>];
wr48 <expression> [, <expression>];
wr56 <expression> [, <expression>];
wr64 <expression> [, <expression>];
Evaluates the first expression and writes the result as a little-endian binary value to the output file. The optional second expression specifies the repetition count.
[!IMPORTANT] Brink silently truncates the upper bits of the expression to fit the specified width.
Example:
// Test expressions in wrx; addr(foo) == 10 as set by region TOP
region TOP { addr = 10; size = 64K; }
section foo in TOP {
wr8 (1 + 2) + file_offset() + addr(foo) + sizeof(foo); // 3 + 0 + 10 + 36 = 49
wr16 (1 + 2) + file_offset() + addr(foo) + sizeof(foo); // 3 + 1 + 10 + 36 = 50 00
wr24 (1 + 2) + file_offset() + addr(foo) + sizeof(foo); // 3 + 3 + 10 + 36 = 52 00 00
wr32 (1 + 2) + file_offset() + addr(foo) + sizeof(foo); // 3 + 6 + 10 + 36 = 55 00 00 00
wr40 (1 + 2) + file_offset() + addr(foo) + sizeof(foo); // 3 + 10 + 10 + 36 = 59 00 00 00 00
wr48 (1 + 2) + file_offset() + addr(foo) + sizeof(foo); // 3 + 15 + 10 + 36 = 64 00 00 00 00 00
wr56 (1 + 2) + file_offset() + addr(foo) + sizeof(foo); // 3 + 21 + 10 + 36 = 70 00 00 00 00 00 00
wr64 (1 + 2) + file_offset() + addr(foo) + sizeof(foo); // 3 + 28 + 10 + 36 = 77 00 00 00 00 00 00 00
assert sizeof(foo) == 36;
}
output foo;
Another example using the optional repetition expression.
section foo {
wr32 0x12345678, 10; // write 0x12345678 10 times to the output file.
wr8 0, addr() % 4096; // write zero enough times to align to 4KB boundary.
}
Write the file at the specified path into the output file. Brink treats all input files as binary files. Paths can be relative to the current directory or absolute.
For example, given the file test_source_1.txt containing:
Hello!
The following program simply copies these 6 UTF-8 characters to the output file.
section foo {
wrf "test_source_1.txt"; // Hello!
assert sizeof(foo) == 6;
}
output foo;
Evaluates the comma separated list of expressions and writes the resulting string to the output file. Wrs accepts the same expressions and operates similarly to the print statement. For more information, see print.
The wrs statement does not implicitly write a terminating 0 byte after the string. Users creating null terminated (C style) strings in an output file should add an explicit \0.
wrs "my null terminated string\0";
Brink pre-defines built-in identifiers that begin with __ (double underscore).
They can appear in any expression context that accepts the corresponding type.
As shown in the table below, some builtins cannot be used in const expressions
because their values depend on dynamic layout values.
| Variable | Type | OK in const? |
Description |
|---|---|---|---|
__OUTPUT_SIZE |
U64 |
No | Total output size in bytes. Equivalent to sizeof(<output-section>). |
__OUTPUT_ADDR |
U64 |
No | Address of output section at SectionStart. Equivalent to addr(<output-section>). |
__BRINK_VERSION_STRING |
String |
Yes | Brink version as a string, e.g. "4.3.2". |
__BRINK_VERSION_MAJOR |
U64 |
Yes | Major version component, e.g. "4" in "4.3.2" |
__BRINK_VERSION_MINOR |
U64 |
Yes | Minor version component, e.g. "3" in "4.3.2" |
__BRINK_VERSION_PATCH |
U64 |
Yes | Patch version component, e.g. "2" in "4.3.2" |
Returns the total size of the output file in bytes.
Example — write a 4-byte header field containing the total output size:
section payload {
wrs "Hello";
}
section hdr {
wr32 __OUTPUT_SIZE; // filled with total image size at link time
}
section image {
wr hdr;
wr payload;
assert __OUTPUT_SIZE == sizeof(image); // equivalent forms
}
output image;
Returns the absolute starting address of the output section. Equivalent to
addr(<output-section>). Without placing the section in region,
__OUTPUT_ADDR is zero regardless of set_addr command internal to the output
section. This occurs because set_addr is a scoped operation. A set_addr
within a section affects address calculations for subsequent writes internal to
the section, not the logical start of the section itself.
If user places the output section in a region, then __OUTPUT_ADDR
is the starting address of the region.
Example — embed the output base address in a table:
region TOP { addr = 0x0800_0000; size = 64K; }
section vtable {
wr32 __OUTPUT_ADDR; // base address of the output image
}
section code {
wrs "code";
}
section image in TOP {
wr vtable;
wr code;
assert __OUTPUT_ADDR == addr(image); // equivalent expressions
}
output image;
Returns the Brink tool version as a string (e.g. "4.0.0"). The value is fixed
at compile time and may be used in const expressions, wrs, and print.
Example — stamp the tool version into a firmware header:
section hdr {
wrs __BRINK_VERSION_STRING;
}
section image {
wr hdr;
wrs "payload";
}
output image;
Return the individual numeric components of the Brink version as U64 values.
All three are fixed at compile time and may be used in const expressions and
arithmetic.
Example — pack the version into a 3-byte field and assert the tool is new enough:
const MIN_MAJOR = 4u;
section hdr {
assert __BRINK_VERSION_MAJOR >= MIN_MAJOR;
wr8 __BRINK_VERSION_MAJOR;
wr8 __BRINK_VERSION_MINOR;
wr8 __BRINK_VERSION_PATCH;
}
section image {
wr hdr;
wrs "payload";
}
output image;
Brink supports compile time extensions to simplify the addition of new functionality. This extension capability enables user defined hashing, compression, validation and other binary data processing tasks. The following sections describe how extensions work and how to create them.
The command line option --list-extensions outputs the names of all available
extensions as enabled by Cargo feature flags.
Extensions build and link to Brink at compile time as controlled by Cargo feature flags. Because Rust does not guarantee a stable ABI between versions, Brink requires compile time construction to eliminate ABI incompatibilities and enable the use of safe Rust. The following bullets provide an overview of how extensions work:
-
Extensions interact with Brink through the
BrinkExtensiontrait. -
Extensions can read directly from the output buffer for a specified section via zero-copy and safe-memory slices (
&[u8]). -
In addition to output buffer access, extensions can have their own input parameters like a normal function call.
-
Extensions are identified by a name in a namespace. Brink reserves the namespaces
stdandbrink. -
Extensions report their fixed length binary footprint by implementing the
.size()trait method. Brink calls each extension's.size()method exactly once during output layout calculations and caches the result. Brink always passes a mutable output slice (&mut [u8]) of the reported size to the extension's.generate()method. -
Extensions register themselves at compile time in Brink's internal extension registry.
-
The
BrinkExtensiontrait interface allows extensions to return logging and error diagnostics integrated with Brink's own diagnostic output. See []
Users invoke extensions using function-style syntax. Users creating their own extension can take any number of parameters of any Brink support type:
turbo::boost("Big", 1, -42, 0x12345678);
Fixed-size write commands like wr32 are invalid for extensions. If the
designer needs to pad the extension's output to a specific size, they must
follow the wr command with a pad_sec_offset or align statement.
Users can pass the data in a section to an extension by passing the section name as a parameter. Extensions take section data as an immutable zero-copy slice parameter of Rust type &[u8]. Section data passed to the extension at the time of the call includes all data generated by non-extension write commands. Furthermore, the data includes the output of extensions executed before the current extension.
As an example, consider the std::crc32c extension. This extension generates a CRC hash over the data provided by the specified section. The extension produces a 4-byte output.
section foo_binary {
wrf "foo.bin"; // Write the file foo.bin in this section.
}
section bar {
wr foo_binary;
// Write the CRC hash of everything in foo_binary
wr std::crc32c(foo_binary);
}
Users can also pass the section containing the extensions own output to the extension. The extension receives a slice of the full size of the section, including the size of the extension's own output. On input, the slice contains zero bytes at the location of the extension's future output. For example:
section foo_binary {
wrf "foo.bin";
// Warning, the CRC input data is the full length of the foo_binary
// section and includes 4 trailing zero bytes in place of the
// extension's output.
wr std::crc32c(foo_binary);
}
section bar {
wr foo_binary;
}
To help eliminate bugs, Brink extensions support named parameters. Extensions
define their parameter names when registered. In the example below, we call the
extension custom::my_extension passing it the required parameters
data_section and code_section. The compiler passes the values by name in
the order expected by the extension, regardless of the order given at the call
site.
//
// extension example
//
section my_data {
wrf "cool_data.bin";
};
section my_code {
wrf "cool_code.bin";
};
section stuff {
wr my_data;
wr my_code;
// Use named arguments to avoid positional and semantic bugs!
custom::my_extension(data_section=my_data, code_section=my_code);
};
Users can query the size of an extension's output using the sizeof command.
For example:
assert sizeof(std::crc32c) == 4;
Extensions register through the extensions crate (extensions/src/lib.rs).
process.rs calls extensions::register_all once at startup; adding an
extension requires no changes outside extensions/.
Place new extensions under std/ for proposed standard library extensions, or
under a workspace path matching your namespace for third-party extensions.
Implement the BrinkExtension trait from the brink_extension crate.
// my_extension/src/lib.rs
use brink_extension::BrinkExtension;
use extension_registry::ExtensionRegistry;
pub struct MyExtension;
impl BrinkExtension for MyExtension {
fn name(&self) -> &str { "my_ns::my_ext" }
fn size(&self) -> usize { 4 }
fn execute(&self, _args: &[u64], img: &[u8], out: &mut [u8]) -> Result<(), String> {
// write 4 bytes into out
Ok(())
}
}
pub fn register(registry: &mut ExtensionRegistry) {
registry.register_ranged(Box::new(MyExtension));
}
In the root Cargo.toml, add the crate path to [workspace] members.
In extensions/Cargo.toml, add the new crate as a dependency:
my_extension = { path = "../my_extension" }
In extensions/src/lib.rs, call its register function inside register_all:
pub fn register_all(registry: &mut ExtensionRegistry) {
std_crc32c::register(registry);
my_extension::register(registry); // add this line
}
Create a tests/ directory in your extension crate with .brink scripts
and an integration.rs test file. Use CARGO_MANIFEST_DIR to locate
.brink files relative to the workspace root — see
std/crc32c/tests/integration.rs for a complete example.
Run the extension's tests with:
cargo test -p my_extension
This section provides notes for developers interested in contributing to Brink.
Brink relies on 100's of unit tests to catch bugs. You can run these with:
cargo test --all
Brink supports fuzz tests for several of its internal libraries. Fuzz testing
starts from a corpus of random inputs and then further randomizes those inputs
to try to cause crashes and hangs. At the time of writing, fuzz testing
requires the nightly build. See fuzz_help.md in the source repo for more
information.
If you're using Windows as a development platform, then this worked for me to install the llvm-cov tool. I have the free version of Microsoft Visual Studio installed.
rustup component add llvm-tools
cargo install cargo-llvm-cov --locked
To generate an ASCII table of coverage stats to the terminal:
cargo llvm-cov --all-features --workspace
To update the coverage table in this README from Windows, run
.\update_coverage.ps1.
Filename Regions Missed Regions Cover Functions Missed Functions Executed Lines Missed Lines Cover Branches Missed Branches Cover
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
ast\ast.rs 2318 452 80.50% 70 8 88.57% 1265 208 83.56% 0 0 -
ast\lexer.rs 376 12 96.81% 16 0 100.00% 234 9 96.15% 0 0 -
brink_extension\lib.rs 3 0 100.00% 1 0 100.00% 3 0 100.00% 0 0 -
const_eval\const_eval.rs 1081 174 83.90% 26 5 80.77% 701 150 78.60% 0 0 -
diags\diags.rs 209 25 88.04% 10 1 90.00% 106 19 82.08% 0 0 -
engine\engine.rs 2292 483 78.93% 66 6 90.91% 1480 271 81.69% 0 0 -
extension_registry\extension_registry.rs 248 9 96.37% 17 3 82.35% 121 9 92.56% 0 0 -
extension_registry\test_mocks.rs 259 34 86.87% 38 6 84.21% 204 33 83.82% 0 0 -
extensions\src\lib.rs 8 0 100.00% 1 0 100.00% 5 0 100.00% 0 0 -
ir\ir.rs 226 28 87.61% 22 1 95.45% 172 18 89.53% 0 0 -
irdb\irdb.rs 818 97 88.14% 17 1 94.12% 482 60 87.55% 0 0 -
layoutdb\layoutdb.rs 772 160 79.27% 18 0 100.00% 434 72 83.41% 0 0 -
linearizer\linearizer.rs 568 87 84.68% 19 1 94.74% 332 38 88.55% 0 0 -
map\map.rs 860 13 98.49% 58 0 100.00% 579 9 98.45% 0 0 -
process\process.rs 360 25 93.06% 22 5 77.27% 195 9 95.38% 0 0 -
prune\prune.rs 150 9 94.00% 12 2 83.33% 95 7 92.63% 0 0 -
src\main.rs 123 6 95.12% 8 1 87.50% 84 5 94.05% 0 0 -
std\crc32c\src\crc32c.rs 31 2 93.55% 5 0 100.00% 26 3 88.46% 0 0 -
std\md5\src\md5.rs 31 2 93.55% 5 0 100.00% 26 3 88.46% 0 0 -
std\sha256\src\sha256.rs 31 2 93.55% 5 0 100.00% 26 3 88.46% 0 0 -
symtable\symtable.rs 114 5 95.61% 14 2 85.71% 80 5 93.75% 0 0 -
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
TOTAL 10878 1625 85.06% 450 42 90.67% 6650 931 86.00% 0 0 -
| File | Stage | Summary |
|---|---|---|
| ast/ast.rs | Stage 1 | Hand-rolled lexer -> token stream -> arena AST -> AstDb validation |
| const_eval/const_eval.rs | Stage 2 | Lowers const and region AST statements to LinIR, returns SymbolTable and RegionBindings |
| prune/prune.rs | Stage 3 | Eliminates if/else nodes from the AST; promotes sections from the taken branch |
| layoutdb/layoutdb.rs | Stage 4 | AST flattening into linear IR and operand vectors; values are still strings |
| irdb/irdb.rs | Stage 5 | String to typed value conversion, operand and file validation |
| layout_phase/layout_phase.rs | Stage 6 | Iterative address resolution and section footprint calculation |
| validation_phase/validation_phase.rs | Stage 7 | Evaluates all assert instructions after layout and before binary output |
| exec_phase/exec_phase.rs | Stage 8 | Writes inline data, padding, file contents, and extension output to binary |
| symtable/symtable.rs | Shared types | SymbolTable tracking every compile-time const from declaration through use |
| linearizer/linearizer.rs | Shared types | LinIR and LinOperand types; shared lowering infrastructure for stages 2 and 4 |
| ir/ir.rs | Shared types | IRKind, ParameterValue, IROperand, IR — the data flowing between stages 4–8 |
| locationdb/locationdb.rs | Shared types | LocationDb and Location produced by stage 6 and consumed by stages 7 and 8 |
| map_phase/map_phase.rs | Map output | Builds MapDb from LocationDb and IRDb; renders map to CSV, JSON, C99, and RS |
| process/process.rs | Orchestrator | Orchestration of all stages, parses -D defines, opens the output file |
| diags/diags.rs | Cross-cutting | Ariadne-backed diagnostic output channel used by every stage |
| extensions/src/lib.rs | Extensions | Single registration point for all extensions |
| brink_extension/lib.rs | Extensions | Public API for extension authors |
| extension_registry/extension_registry.rs | Extensions | Runtime extension registry and dispatch wrapper |
| std/crc32c/src/lib.rs | std extension | CRC-32C (Castagnoli) hash over caller-specified output region |
| std/sha256/src/lib.rs | std extension | SHA256 hash over caller-specified output region |
Rebuilding the extension require Node.js. After you install Node.js, you may need to restart your command prompt.
Building the extension requires
vsce. One time, you'll need to use
npm to install vsce
npm install -g @vscode/vsce
Now you're ready to rebuild the extension.
cd vscode-brink
vsce package
To install the extension into vscode locally:
code --install-extension vscode-brink-0.1.0.vsix