Documentation
bedrock-lang reference
Introduction
Bedrock is an imperative, statically typed systems programming language designed
around simplicity, readability, and practicality.
The goal is to provide the control expected from a systems language while removing unnecessary complexity from the programming model.
Memory is managed automatically through a region-based memory model. For C
interoperability an unsafe block is required.
The language intentionally avoids object-oriented programming. Programs are built using procedures, functions, structs, and modules instead of classes or inheritance.
Getting Started
Start by setting up your local development environment by installing the necessary tools according to the installation instructions.
Now to test that the installed bok compiler works, copy this code sample in a file named hello.bok
extern proc printf(...);
proc main()
printf("Hello, World!\n");
end
Note: using libc printf until we have our own stdlib. :(
Then in your terminal run this command:
bok hello.bok --jit
or you can also compile it into a binary by doing this:
bok hello.bok -o hello
then run the binary hello that is created:
./hello
Voila! you have run your first program written in bedrock
Let's briefly breakdown what's happening in this program, then go into more detail throughout the remainder of the tutorial.
The first line declares the printf function from the C ABI using Bedrock's
foreign function interface (FFI), allowing the program to link against libc.
The program's entry point is main which executes after program environment is initialized.
The shape of this function is defined by bedrock's standard proc which returns no values,
it's like a void function.
The purpose of this function for this demonstration is just printing Hello, World! to the console.
Bedrock Basics
Bedrock's syntax might be of some familiarity to people who have used pascal or freebasic like languages.
The control flow in bedrock is divided into distinct blocks which starts with respective keyword
and ends with the keyword end.
This makes bedrock programs to be read procedurally like a documentation.
let's get started!
Comments
Single line comments in bedrock starts with //
// this is a comment
there are no block comments as of now in bedrock
refer to this discussion if you are curious why
but we are not rigid, so it could be there in future.
Reserved Words
A good way to get a bird eye view of a language's style is to see what words it reserves.
Here's what bedrock has:
import pub inline func proc extern
type struct enum static const var
defer unsafe if elif else for
in while match case return orelse
comptime try end true false break
continue nil where new ptr undefined
Identifiers
The naming of variables/identifiers are similiar to other programming languages. Identifiers start with a letter or underscore and may contain letters, digits, and underscores. Case is sensitive.
hi
openSource
NiklausWirth
bjarne_stroustrup
_dandalion
h2g2
In bedrock a new identifiers is assigned using var or const keyword. Here identifiers assigned using var
are mutable by default and variables assigned with const are non-mutable, bedrock overall has this property that
it's mutable by default.
var a = 5;
var b = 5.42;
here the type of
aandbwill be inferred, so it's not dynamic typing.
You can declare variables with explicit types.
Like this:
var a: i32 = 5;
var b: f32 = 5.42;
var c: str = "hello sir";
here the types are explicitly declared in syntax.
Type Declarations
All user defined types begin with the type keyword.
- Define a struct in bedrock
type Person = struct
name: str,
age: u32,
end
Structs are constructed using where keyword
func main() -> i32
var p1 = Point where name = "Alice", age = 20 end
return 0;
end
when using where initializing all values is necessary and if not then you will encounter a
semantic errorfor safety purposes.
- Define an enum in bedrock (
todo)
type Color = enum
red,
yellow,
green,
end
- Aliasing in bedrock
it's like #define or type aliasing in go.
type real = f32;
type vecstring = [_]str;
note: when type used for aliasing then
endkeyword is not used, instead;is used to end it. As it's not starting a block and just ending in one line, unlikestructandenum(special types). see discussion if you are more curious as to why.
Newlines
A bedrock program could completely be written in a single line, though new lines should be used for convinience and better readability
Blocks
Bedrock does not uses curly braces to define blocks, but uses explicit end keyword where a block ends.
A block is everywhere inside of which a statement is allowed, like in control flow statements, expressions, etc.
Ex:
if (eyes_open && morning)
rise.shine();
else
print("sleepy");
end
so here if-else is a block and you end it with the end keyword
If you forget to put end at the end of a block, then it's a hard error as block is not closed
and could lead to unwanted behaviour of your program.
proc foo()
// do some crazy work...
end
this is also a block (a procedure block (more about it soon)) that does something crazy.
It's not necessary to have these blocks on different lines like, literally a block, u can have it like this too:
if (is_even(n)) result = true; else result = false; end
Booleans
A boolean represent truth or falsehood, bedrock have two literals for boolean, true and false. Their type
is bool.
Numbers
Bedrock is a systems programming language so it has many built in strict types.
- numerical literals
0
42
-3.14
0xc1c1c1
10_21_201
- numerical types
u8
i8
u16
i16
u32
i32
u64
i64
usize (u64)
isize (i64)
f32 (float)
f64 (double)
Strings
A string is array of bytes, in bedrock strings are not null terminated and are represented like this { ptr: *T, len: usize },
a fat pointer and the length, and for use with c ABI, it could be converted to a null terminated string. (todo)
- String literals are surround in double quotes:
"bok bok"
-
Type of string is
strin bedrock -
Escaping
These escape characters are supported.
"\n" // Newline.
"\t" // Tab.
"\r" // Carriage return.
"\x00"
"\\" // A backslash.
"\"" // A double quote character.
"\a" // Alarm beep. (Who uses this?)
"\b" // Backspace.
"\f" // Formfeed.
"\v" // Vertical tab.
Char
A char is simply of 8 bit so a char is just u8, but bedrock explicitly also has a type char (u8 only) for chars,
for convinience and better semantics.
Ranges
A range is a little object that represents consecutive range of numbers. In bedrock you can represent a range
using .. for exclusive range and ..= for inclusive range.
So for example:
for i in 0..10
// print i
end
will print numbers from 0 to 9. Whereas,
for i in 0..=10
// print i
end
will print number from 0 to 10.
Nil
Bedrock has special keyword nil which allows many things like if optional variable is not containing
any value, then it returns nil.
Arrays
Array are data structures that stores homogenous type of elements in them.
In bedrock array are initialized like this:
var arr: [10]i32 = [];
this initializes an empty array of capacity 10.
You can use _ for inferencing an array's size, it could be used in such manner:
var arr: [_]i32 = [1,2,3,4,5];
Then array will be of capacity 5.
Having such inferencing power allows us for quick declaration of variables, for example you can initialize the above array in short hand like this too:
var arr = [1,2,3,4,5];
But this:
var arr = [];
or this:
var arr:[_]i32 = [];
will result into an obvious semantic error -> cannot infer type or size of empty array literal
What's inferabble is inferabble what's not inferabble cannot be inferred.
Accessing elements
elements of an array are accessed like most other languages, using a [].
proc main()
var arr = [1,2,3,4,5];
const c = arr[2];
printf("%d\n", c);
end
output: 3
Lists are also mutable
proc main()
var arr = [1,2,3,4,5];
arr[2] = 10;
const c = arr[2];
printf("%d\n", c);
end
output: 10
Slices
Just like array type bedrock has a slice type for passing arrays to functions or using it in some other way
like initializing in structs (todo).
func foo(arr: []i32) -> i32
return arr[2];
end
Here arr is passed to the function foo, and it does not requires any value or _ as it just represents
the type of an array.
Procedures and Functions
Bedrock has a clear seperation between a procedure (instantiated by keyword proc) and function (instantiated by keyword func)
proc
A procedure performs work but does not return a value.
proc main()
for i in 0..100
printf("%d\n", i);
end
end
If proc returns something then a strict semantic error is given by the compiler.
Such as this will result in error:
proc main()
for i in 0..100
printf("%d\n", i);
end
return 1;
end
error: proc cannot return a value
Whereas a bare return in proc works for early return in some branches.
proc main()
var a = 10;
if (a > 10)
return;
else
a = 20;
end
func
A function should always return a value
func add(x: i32, y: i32) -> i32
return x + y;
end
func main() -> i32
return add(10, 10);
end
A function not returning a value will be warned to the user by compiler.
func add(x: i32, y: i32) -> i32
var a = x + y;
end
warning: control reaches the end of the function
This was an intentional and a well thought distinction between proc and func it improves readablity as
now just by seeing proc you know they are not returning anything and also for func we do explicit return
type checking but for proc it's not at all necessary proved in obvious way that it does not return any value at all.
This also syncs well with the language's procedural roots.
Control Flow
In bedrock control flow constructs includes and are not limited to if, match, while, for.
--- If Statement ---
If is used for conditional statement control flow.
used solo:
if (is_valid) doit(); end
Use with else
if (is_valid) doit(); else dont(); end
if-elif ladder
proc main()
var a = 10;
if (a == 1)
printf("1\n");
elif (a == 2)
printf("2\n");
elif (a == 3)
printf("3\n");
end
if-elif-else ladder
proc main()
var a = 10;
if (a == 1)
printf("1\n");
elif (a == 2)
printf("2\n");
elif (a == 3)
printf("3\n");
else
printf("neo\n");
end
output: neo
--- Match-Case (todo) ---
The match-case statements are the switch-case of bedrock.
You can use it for char, number, enums. The especiality of match-case is that it evaluates at compile
time so using them makes runtime faster.
Like using match for enums.
match Color
case red
printf("red\n");
case yellow
printf("yellow\n");
case green
printf("green\n");
end
Here if enum Color have more elements in it then just red, yellow and green then semantically it could
give warning that not all variants were considered in the match-case.
For such situation to handle the default case we have the else in match:
match Color
case red
printf("red\n");
case yellow
printf("yellow\n");
case green
printf("green\n");
else
printf("no color\n");
end
--- While Loop ---
The While loop works in same style, by instatiating a while block ending with end.
proc main()
var i = 0;
while i < 5
printf("%d\n", i);
i+=1;
end
end
output:
0
1
2
3
4
--- For Loop ---
The For loop works in same style, by instatiating a for block ending with end.
You specify a binding variable and a range or an iterable structure.
proc main()
var arr = [10, 21, 42, 5];
for i in arr
printf("%d ", i);
end
end
output: 10 21 42 5
for ranges too:
proc main()
for i in 7..10
printf("%d ", i);
end
end
output: 7 8 9
Here in both example the binding variable is i and it iterates over arr or the range.
Optional Values (todo)
So some operations or some containers could have a value or not (maybe). These are optional values. And such operations contain the optinal type.
These are denoted by ? at suffix of them like ?user
Here's a function returning optional value:
func find_user(id: u32) -> ?User
Here the return User structure could have a User struct or nil.
Error Propogation (todo)
The language syntax provides a concise way to propogate errors.
An error returning value will have a ! at the end like result! unlike ? which is appended at starting.
func load_config() -> Config!
var file = try open("config.cfg")
return try parse(file)
end
- If the operation succeeds, execution continues using the returned value.
- If the operation returns an error, the current function immediately returns that error to its caller.
Modules (todo)
Each bok file is a module and modules are imported using import keyword.
import io;
import math;
import graphics.window;
any function, identifiers, user defined types, marked with pub could be now accessed in the file these modules are imported into.
like:
pub func add(a: i32, b: i32) -> i32
return a + b
end
So, by default all declarations are private in a module.
Unsafe (wip)
The language is memory safe by default. Memory for language objects is managed automatically through the region model (see: memory model), eliminating the need for manual allocation and deallocation in normal programs.
Certain low level operations, however, cannot be verified or protected by the compiler. These operations must be performed inside an unsafe block.
unsafe
// Unsafe operations
end
An unsafe block explicitly indicates that the programmer is taking responsibility for the correctness of the enclosed code.
The presence of an unsafe block serves two purposes:
- It enables low level operations that are otherwise prohibited.
- It clearly communicates to readers that this section of code requires additional care and review.
Operations permitted within an unsafe block include:
- Manual memory allocation and deallocation.
- Raw pointer dereferencing.
- Pointer arithmetic.
- Calling foreign (C) functions.
- Accessing memory mapped hardware.
- Low level memory manipulation.
- Inline assembly (near future).
Example:
extern func malloc(size: usize) -> *u8
extern proc free(ptr: *u8)
unsafe
var buffer = malloc(4096)
*(buffer + 8) = 42
free(buffer)
end
Memory Model (wip)
Region Based
Bedrock uses regions to manage memory, every objects is being allocated in a region and it is reclaimed from all of the region at once when the region goes out of scope intead of keep tracking each allocation.
Invariants about region model
- Every allocation belongs to one region, all allocations within an arena are reclaimed together when that region is destroyed.
- A reference cannot outlive its target, program cannot contain dangling references.
- Destruction happens exactly once when the region goes out of scope.
The compiler tracks the lifetime of references to make sure that a reference cannot outlive the region it currently lives in.
func ret_person() -> &Person
region R
var p = Person where id = 0, age = 24 end
return &p;
end
end
The compiler can determine that lifetime of return = caller and lifetime of p in region R ends before
caller uses the return reference, preventing dangling reference at compile time. User is not explicitly required
to annotate the region R in the above example the compiler can infer the lifetime in a function body.
Lifetime concepts
Lexical Region
A lexical region corresponds to a local scope.
proc lexical()
region @outer
var p1: &Person
region @inner
var p2 = Person where name = "Alice", age = 24 end
p1 = &p2
end
end
end
Here both p1 and p2 are in lexical region, lifetime(p2) < lifetime(p1) so p1 cannot
reference to p2. Its not necessary to type the region block manually the compiler can infer the
region annotations in this case.
Function params
Parameters are not a separate storage region. A reference parameter borrows an allocation from the caller.
proc set_friend(p: &Person, friend: &Person)
p.friend = friend
end
Because friend might have a shorter lifetime than p, we need some lifetime annotation hints here.
proc set_friend(p: &@p Person, friend: &@r Person)[@p <= @r]
p.friend = friend
end
Return
A function cannot return a reference to storage whose lifetime ends before the caller can use the return value. Local references cannot escape their lexical region. We cannot return a reference to local variable to the caller that will result in dangling pointer.
func foo() -> &Person
var p = Person where name = "Alice", age = 24 end
return &p
end
Static region
Static storage lives for the entire program.
var local = new(@static) Person where name = "Alice", age = 24 end
Foreign Function Interface
The language provides direct interoperability with C through the native ABI.
extern proc printf(fmt: *u8)
extern func malloc(size: usize) -> *u8
or using variadic
extern proc printf(...)
extern func malloc(...) -> *u8
This allows seamless integration with existing operating system APIs and C/C++ libraries.
See the example using raylib:
extern proc InitWindow(...);
extern func WindowShouldClose() -> bool;
extern proc BeginDrawing();
extern proc EndDrawing();
extern proc CloseWindow();
extern proc ClearBackground(...);
extern proc DrawRectangle(...);
func main() -> i32
var white: i64 = 245 | (245 << 8) | (245 << 16) | (255 << 24);
var skin: i64 = 224 | (180 << 8) | (135 << 16) | (255 << 24);
var nose: i64 = 188 | (142 << 8) | (106 << 16) | (255 << 24);
var robe: i64 = 89 | (122 << 8) | (66 << 16) | (255 << 24);
var robe_dark: i64 = 63 | (88 << 8) | (47 << 16) | (255 << 24);
var black: i64 = 10 | (10 << 8) | (10 << 16) | (255 << 24);
InitWindow(800, 600, "Minecraft Villager");
while !WindowShouldClose()
BeginDrawing();
ClearBackground(white);
// Arms
DrawRectangle(290, 210, 35, 150, robe);
DrawRectangle(475, 210, 35, 150, robe);
// Body / robe
DrawRectangle(320, 200, 160, 220, robe);
DrawRectangle(320, 340, 160, 30, robe_dark);
// Head
DrawRectangle(340, 90, 120, 110, skin);
// Eyebrows (villagers have a monobrow-style look per eye)
DrawRectangle(355, 130, 25, 8, black);
DrawRectangle(420, 130, 25, 8, black);
// Big nose
DrawRectangle(380, 138, 40, 55, nose);
EndDrawing();
end
CloseWindow();
return 0;
end
output:
Installation Instructions
For now the preferred way of installing bedrock until we have our own package manager is to go the
bedrock github repo and follow the instructions in the readme
and you will have a working binary of bedrock called bok.