Welcome to the BedRock language documentation. BedRock is a systems programming language that compiles to bare-metal MIPS-32 big-endian machine code. Every language construct maps directly and transparently to a fixed sequence of hardware instructions — no runtime, no OS abstraction, no magic.
This documentation covers the complete language specification, compiler internals, hardware interfacing primitives, and hard-won guidance on the compiler's known enforcement behaviors and edge cases.
TOC Documentation Sections
Core Philosophy
The three axioms that govern every design decision in the BedRock language and compiler.
1.1 "No Magic"
In BedRock, every operation maps to a known, fixed sequence of machine instructions. There are no hidden runtime allocations, no garbage collector, no implicit type coercions, and no virtual dispatch. What you write is exactly what executes.
This is enforced by design — the table below shows the exact 1-to-1 mapping for common constructs:
| BedRock Construct | Machine Code Behavior |
|---|---|
| let x = 5; | Exactly one sw instruction to a pre-assigned static address. No stack frame creation, no heap allocation. |
| let arr = [1, 2, 3]; | Compiler statically assigns a base address in the DATA segment, emits three sw instructions during initialization. All accesses are compile-time pointer arithmetic. |
| fn foo() { ... } | Exactly one prologue (addiu $sp, -32; sw $ra, 28($sp)) and one epilogue (lw $ra, 28($sp); addiu $sp, +32; jr $ra). Nothing else. |
.bin output to confirm.1.2 "Byte-Level Validation"
Numbers in BedRock are unsigned 64-bit integers at parse time, collapsed to 32-bit at code generation. All memory addresses are 32-bit values. There are no floating-point types, no signed arithmetic in the type system, and no implicit widening or truncation.
The compiler validates output at the byte level by:
- Encoding all instructions as big-endian 32-bit words via
to_be_bytes() - Writing a raw
.binfile with zero padding assumptions - Optionally writing a 1.44 MB floppy image (
bedrock_os.img) by embedding the binary at sector 0
1.3 "Direct Hardware Control"
BedRock provides five mechanisms for hardware access that map directly to bare-metal MIPS operations. There is no operating system abstraction layer.
| Construct | Hardware Operation | MIPS Instruction |
|---|---|---|
| poke(addr, val) | Write 32-bit word to physical address | sw $val, 0($addr) |
| peek(addr) | Read 32-bit word from physical address | lw $dest, 0($addr) |
| outb(port, val) | Memory-mapped I/O write | sw $val, 0($port) |
| inb(port) | Memory-mapped I/O read | lw $dest, 0($port) |
| asm("hex") | Emit raw machine instruction | Direct word emission |
Lexical Grammar
Reserved keywords, operator tokens, literal types, and preprocessor directives recognized by the BedRock lexer.
2.1 Keywords
The following identifiers are reserved and cannot be used as variable or function names.
| Keyword | Category | Description |
|---|---|---|
| fn | Declaration | Function definition |
| let | Declaration | Local/static variable declaration |
| root | Declaration | Hardware/environment constant declaration |
| return | Control Flow | Return from function |
| if | Control Flow | Conditional branch |
| else | Control Flow | Alternative branch |
| while | Control Flow | Conditional loop |
| loop | Control Flow | Infinite loop |
| break | Control Flow | Exit innermost loop |
| asm | Hardware | Emit raw machine instruction |
| outb | Hardware | Memory-mapped I/O write |
| inb | Hardware | Memory-mapped I/O read |
| poke | Hardware | Direct memory write |
| peek | Hardware | Direct memory read |
| in | Hardware | Wait for keyboard input (alias for WaitKey) |
| include | Preprocessor | Include another source file (textual substitution) |
2.2 Operators
Arithmetic Operators
| Symbol | Token | MIPS Instruction |
|---|---|---|
| + | Plus | addu |
| - | Minus | subu |
| * | Star | mult + mflo |
| / | Slash | div + mflo |
| ^ | Caret | xor |
Bitwise Operators
| Symbol | Token | MIPS Instruction |
|---|---|---|
| & | Ampersand | and |
| | | Pipe | or |
| ^ | Caret | xor |
Comparison Operators
| Symbol | Token | MIPS Sequence |
|---|---|---|
| == | EqEq | xor + sltiu $d, $d, 1 |
| != | NotEq | xor + sltu $d, $0, $d |
| > | Greater | slt (operands swapped) |
| < | Less | slt |
| >= | GreaterEq | slt + xori $d, $d, 1 |
| <= | LessEq | slt (swapped) + xori $d, $d, 1 |
Assignment
| Symbol | Token | Description |
|---|---|---|
| = | Equal | Assignment (not comparison — use == for equality testing) |
2.3 Literals
Integer Literals
BedRock supports decimal and hexadecimal integer literals. All integers are stored as u64 during lexing.
String Literals
Strings are delimited by double quotes, stored in the DATA segment as null-terminated byte arrays (C-style). Escape sequences are not processed — see §8.8.
Array Literals
Arrays are comma-separated lists of integer literals only enclosed in [ ]. Variables and expressions are not permitted as elements — see §8.2.
2.4 Comments & Include Directive
The include directive is a preprocessor feature handled before lexing. It performs textual substitution of the named file's content into the current file. Include paths are resolved relative to the source file's directory and processed recursively.
Memory Model
BedRock's flat 32-bit address space, segment layout, variable binding semantics, and stack frame structure.
3.1 Address Space
BedRock uses a flat 32-bit address space. The layout is determined by three root declarations:
| Root Symbol | Default Value | Description |
|---|---|---|
| BASE | 0x80000000 | Code segment start address |
| DATA | BASE + 0x10000 | Static data segment start address |
| STACK | BASE + 0x20000 | Initial stack pointer value |
3.2 Segment Layout
3.3 let — Static Variable
let declares a static variable. In global scope, the compiler allocates a fixed address in the DATA segment and emits sw (store word) instructions to initialize it. Variables do not occupy stack space in global scope.
let-declared variable's address is resolved entirely at compile time. There is no dynamic allocation.3.4 root — Hardware / Environment Constant
root declares a compile-time constant mapping a name to a fixed 32-bit address or value. Unlike let, a root symbol is never dereferenced. When used in an expression, it evaluates to the raw address value itself — not the value stored at that address.
| Declaration | Expression Behavior | Use Case |
|---|---|---|
| let x = 5 | Loads value 5 from memory address | Variables, counters, state |
| root ADDR = 0xB8000 | Loads the literal value 0xB8000 — no memory dereference | Hardware addresses, config constants |
root declaration must be a literal integer. Expressions are not evaluated. See §8.1.3.5 Stack Frame Structure
Every function call creates a 32-byte stack frame:
Prologue (emitted automatically)
Epilogue (emitted automatically)
3.6 Register Allocation
BedRock uses a pool of 8 temporary registers ($t0–$t7, MIPS registers 8–15) for all expression evaluation. Registers are allocated from the front of the pool and freed back to the rear (FIFO with free-list).
| Register | MIPS Number | Role |
|---|---|---|
| $sp | 29 | Stack pointer |
| $ra | 31 | Return address (saved by JAL) |
| $v0 | 2 | Function return value |
| $a0–$a3 | 4–7 | Function arguments (caller convention) |
| $t0–$t7 | 8–15 | Temporary expression registers (pool of 8) |
alloc_reg() finds the pool empty, it silently returns register 8 ($t0), potentially corrupting a live value. See §8.9 for mitigation.Detailed Syntax Guide
Complete syntax reference for variables, arrays, strings, functions, control flow, and inline assembly.
4.1 Variable Declarations
4.2 Arrays & Strings
Array Definition & Access
Arrays are statically allocated in the DATA segment. Only integer literals are permitted as element values. The compiler scales all indices by 4 (sll idx, idx, 2) — each element is a 32-bit word.
String Definition
Strings are stored as null-terminated byte arrays in the DATA segment, padded to the next 4-byte boundary.
4.3 Arithmetic & Expressions
Precedence rules: * and / bind tightly (handled in parse_term). All other operators — +, -, ==, !=, >, <, &, |, ^ — share the same lower precedence level (parse_expression). Use parentheses liberally — see §8.6.
4.4 Function Definitions
$a0–$a3. Return value is always in $v0 — see §8.4 and §8.5.4.5 Control Flow
if / else
while
loop / break
An infinite loop. Exits only via break or a return from the enclosing function.
4.6 Inline Assembly
Emits a single 32-bit MIPS instruction directly into the code stream. The argument must be the exact 32-bit encoding as a hex string — without the 0x prefix.
<<, >>) exist in BedRock's grammar. Use asm for all shift operations. See §8.10.Instruction Generation
The compiler's eight-phase pipeline, MIPS encoding patterns, control flow generation, and binary output format.
5.1 Compiler Pipeline
The compiler operates in a single pass with eight sequential phases:
| Phase | Name | Action |
|---|---|---|
| Phase 0 | Root Symbol Collection | Resolves BASE, DATA, STACK addresses |
| Phase 1 | Bootloader Emission | NOP, li $sp STACK, J <main_start> (patched later) |
| Phase 2 | Static Data Allocation | Assigns addresses to arrays and strings in DATA segment |
| Phase 3 | Function Code Generation | Emits function bodies with prologue/epilogue |
| Phase 4 | Jump Patch | Back-patches the Phase 1 J instruction to point past functions |
| Phase 5 | Static Data Initialization | Emits sw instructions to write array/string data into memory |
| Phase 6 | Main Code Generation | Emits all non-root, non-function, non-static-data statements |
| Phase 7 | Halt | Emits J <self> infinite loop |
5.2 Load Immediate (emit_li)
All constant loading uses the following pattern based on the value's bit width:
5.3 Binary Operation Code Generation
All binary operations follow the pattern: allocate regs → gen left → gen right → emit instruction → free regs.
Arithmetic Encodings
| BedRock | MIPS Encoding | Notes |
|---|---|---|
| a + b | addu $d, $s, $t | 0x00000021 | ... |
| a - b | subu $d, $s, $t | 0x00000023 | ... |
| a * b | mult $s, $t + mflo $d | Result from LO register |
| a / b | div $s, $t + mflo $d | Quotient from LO register |
| a ^ b | xor $d, $s, $t | 0x00000026 | ... |
| a & b | and $d, $s, $t | 0x00000024 | ... |
| a | b | or $d, $s, $t | 0x00000025 | ... |
Comparison Encodings
| BedRock | MIPS Sequence | Result |
|---|---|---|
| a == b | xor $d,$s,$t + sltiu $d,$d,1 | 1 if equal |
| a != b | xor $d,$s,$t + sltu $d,$0,$d | 1 if not equal |
| a < b | slt $d,$s,$t | 1 if s < t |
| a > b | slt $d,$t,$s | Operands swapped |
| a >= b | slt $d,$s,$t + xori $d,$d,1 | Invert lt |
| a <= b | slt $d,$t,$s + xori $d,$d,1 | Invert gt |
5.4 Control Flow Code Generation
if Statement
while Loop
loop (Infinite)
Function Call (jal)
5.5 Memory Operations
| BedRock | MIPS | Description |
|---|---|---|
| poke(addr, val) | sw $val, 0($addr) | 32-bit store to physical address |
| peek(addr) | lw $dest, 0($addr) | 32-bit load from physical address |
| outb(port, val) | sw $val, 0($port) | MMIO write (identical to sw) |
| inb(port) | lw $dest, 0($port) | MMIO read (identical to lw) |
5.6 Binary Output Format
The compiler emits a flat binary of 32-bit big-endian MIPS instructions:
| Output File | Description |
|---|---|
| <source>.bin | Raw binary of the compiled program (flat MIPS words) |
| bedrock_os.img | 1,474,560-byte floppy disk image (1.44 MB), binary at offset 0, first sector = 512 bytes |
Hardware Interfacing
Complete reference for direct memory access, MMIO I/O port operations, VGA text mode, keyboard input, and raw instruction injection.
6.1 Direct Memory Writes — poke
poke writes a 32-bit value to an absolute physical address. No bounds checking occurs.
Machine Code Generated
6.2 Direct Memory Reads — peek
peek reads a 32-bit value from an absolute physical address and returns it as an expression value (in $v0 / the allocated register).
6.3 I/O Port Access — outb / inb
BedRock implements I/O as MMIO. outb and inb are functionally identical to poke and peek at the machine code level — both emit sw and lw instructions respectively. The distinction is semantic only, signaling programmer intent.
6.4 VGA Text Mode — Practical Reference
VGA text mode buffer begins at physical address 0xB8000. Each character cell is 2 bytes.
Color Constants (Foreground)
| Color | Value | Color | Value |
|---|---|---|---|
| Black | 0x0 | Light Gray | 0x7 |
| Blue | 0x1 | Dark Gray | 0x8 |
| Green | 0x2 | Light Blue | 0x9 |
| Cyan | 0x3 | Light Green | 0xA |
| Red | 0x4 | Light Cyan | 0xB |
| Magenta | 0x5 | Light Red | 0xC |
| Brown | 0x6 | White | 0xF |
Cell Address Formula
Example — Print 'H' in White on Black at (0,0)
Example — Clear Entire Screen
6.5 Keyboard Input — in
The keyword in (used as an expression) reads from the keyboard input address 0x80020000 (hardcoded in the compiler). Use inb with an explicit port address to override.
6.6 Raw Instruction Injection — asm
Use asm for instructions not expressible in BedRock's grammar. Always document the encoding.
Best Practices
Proven patterns for writing maintainable, auditable, and correct bare-metal BedRock programs.
7.1 The Address Library Pattern
Declare all hardware addresses as root constants in a dedicated include file. This creates a zero-cost hardware abstraction layer and eliminates magic numbers from your code.
❌ Without Address Library (Avoid)
✅ With Address Library (Preferred)
7.2 Write Pure Functions
Functions that only operate on their parameters and local variables are predictable, testable, and cacheable. Avoid functions that modify hardware state without naming what they target.
7.3 Use Loops for Bulk Hardware Operations
7.4 Always Set BASE, DATA, STACK Explicitly
7.5 Use asm Sparingly and Document It
Inline assembly breaks the readability contract of BedRock. When you must use it, always document the instruction's human-readable name and effect.
let variables at global scope are static and persist for the program lifetime. Prefer passing values through function parameters and return values to keep program state explicit and auditable.Compiler Enforcement & Edge Cases
Twelve critical behaviors, limitations, and silent failure modes that every BedRock developer must understand before writing production code.
8.1 root Requires Literal Number Values
The right-hand side of a root declaration must be a literal integer. Expressions are not evaluated.
8.2 Array Literals Allow Only Integer Literals
Array literal values must be compile-time integer constants. Variables and expressions are not permitted — the parser will panic.
8.3 Unresolved Variable Panics at Codegen
Referencing an undeclared variable causes a panic! in the codegen phase. There is no graceful error recovery — the compiler process terminates.
8.4 Function Call Arguments Are Positional Only
BedRock has no named parameters at call sites. Arguments are matched by position. The number of arguments passed is not validated by the parser.
8.5 Return Value Is Always $v0
There is only one return register ($v0 = register 2). Multiple return values are not supported. Calling two functions in a complex expression may overwrite $v0 before it is consumed.
8.6 No Operator Precedence for Bitwise / Comparison
All of +, -, ==, !=, >, <, &, |, ^ share the same precedence level. Only * and / bind more tightly.
8.7 ! Without = Is a Lexer Error
The lexer only recognizes !=. A lone ! produces Token::EOF and terminates lexing silently — not an error message.
8.8 String Literals Do Not Support Escape Sequences
The lexer reads string content character-by-character until the closing ". Backslash sequences are stored literally as two characters.
8.9 Maximum 8 Concurrent Temporary Registers
The register pool contains 8 registers ($t0–$t7). If the pool is exhausted, alloc_reg() silently returns register 8 ($t0), corrupting a live value without any warning.
8.10 No Shift Operators (<< / >>)
These operators are absent from the lexer and parser. Use asm for all bit-shift operations.
8.11 Integer Overflow Is Silent
All arithmetic uses unsigned MIPS addu/subu. Overflow wraps silently at 2³² − 1. There are no overflow traps or checks at compile time or runtime.
0xFFFFFFFF + 1 produces 0x00000000 with no warning, no exception, and no way to detect it after the fact.8.12 Division by Zero Is Undefined
BedRock emits a raw MIPS div instruction. On MIPS, division by zero produces an unpredictable value in LO and does not raise an exception unless the hardware is explicitly configured to do so. There is no compile-time or runtime guard.
if guard or assert non-zero before any / expression where the denominator could be variable.