The following example code demonstrates various language features. We do not go into detail here, but it provides a quick overview of the language feature set. For more information, please refer to the manual.
blc -run demo.bl
#import "std/fs"
#import "std/string"
#import "std/print"
#import "std/io"
#import "std/array"
main :: fn () s32 {
variables();
if_statements();
loops();
defer_statements();
arrays();
array_slices();
dynamic_arrays();
string_views();
dynamic_strings();
type_info();
structs();
function_taking_any();
function_returning_multiple_values();
error_handling();
polymorph_functions();
comptime_functions();
temporary_storage();
return 0;
}
variables :: fn () {
print_log("=== variables ===");
// Mutable variable declaration.
n1 := 10;
print_log("n1 = %", n1);
// Mutable variable declaration with explicit type.
n2 : s32 = 20;
print_log("n2 = %", n2);
// Immutable variable declaration.
N :: 30;
print_log("N = %", N);
// BL variables are by default zero initialized unless you
// use #noinit directive.
n3: s32;
print_log("n3 = %", n3);
n4: s32 #noinit; // Not initialized to 0.
print_log("n4 = %", n4);
}
if_statements :: fn () {
print_log("=== if_statements ===");
v :: 10;
// Inline if statement.
if v == 10 then print_log("v is 10");
// Inline if statement with else branch.
if v == 10 then print_log("v is 10") else print_log("v is not 10");
// If statement using blocks (then is optional).
if v == 10 {
print_log("v is 10");
} else {
print_log("v is not 10");
}
// We can use if statement as expresion.
n := if v == 10 then 20 else 40;
print_log("n = %", n);
}
loops :: fn () {
print_log("=== loops ===");
i := 0;
loop {
print_log("i = %", i);
i += 1;
// Use break statement to stop the looping.
if i == 10 then break;
}
j := 0;
loop j < 10 {
print_log("j = %", j);
j += 1;
}
loop k := 0; k < 10; k += 1 {
print_log("k = %", k);
}
}
defer_statements :: fn () {
print_log("=== defer_statements ===");
// We have defer as any other "cool" language these days. It can
// be used to postpone statement execution at the end of scope. So
// we can pair init/terminate functions together.
foo :: fn () {
print_log("A");
defer print_log("C");
defer print_log("B");
};
foo();
}
arrays :: fn () {
print_log("=== arrays ===");
arr: [10]s32; // Zero initialized array of 10 integers.
// Initialize the array.
arr = .{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
// Each array has builtin 'len' as s64 count of elements in
// the array, and 'ptr' as pointer to the array memory.
print_log("len = %", arr.len);
print_log("ptr = %", arr.ptr);
// Print each element.
loop i := 0; i < arr.len; i += 1 {
// Use [index] to access individual array elements.
print_log("arr[%] = %", i, arr[i]);
}
// We can print whole array directly like this.
print_log("arr = %", arr);
}
array_slices :: fn () {
print_log("=== array_slices ===");
// Slices are widely used across the language, in general,
// slice is just a structure with pointer to first array
// element and count of elements.
// Note that in case we have fixed number of elements, we
// can use "blank identifier" '_' to automatically set the array
// length bases on element count of the initializer.
arr :: [_]s32.{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
// Arrays can be implicitly converted to slices.
s : []s32 = arr;
print_log("s = %", s);
// This feature might be useful in case we for example want pass
// array of arbitrary sizes into function.
sum :: fn (arr: []s32) s32 {
sum := 0;
loop i := 0; i < arr.len; i += 1 {
sum += arr[i];
}
return sum;
};
print_log("sum(arr) = %", sum(arr));
print_log("sum(arr) = %", sum([3]s32.{ 10, 20, 30 }));
}
dynamic_arrays :: fn () {
print_log("=== dynamic_arrays ===");
// BL provides builtin dynamic arrays (implemented in Array module).
arr: [..]s32;
// Don't forget to release memory allocated by the array. We use defer, so
// termination is called at teh end of the scope or when funcition returns.
defer array_terminate(&arr);
loop i := 0; i < 10; i += 1 {
// Append at the array end.
array_push(&arr, i);
}
// Print count of elements.
print_log("arr.len = %", arr.len);
// Print whole array.
print_log("arr = %", arr);
// Dynamic arrays can be converted to slices too.
sum :: fn (arr: []s32) s32 {
sum := 0;
loop i := 0; i < arr.len; i += 1 {
sum += arr[i];
}
return sum;
};
print_log("sum(arr) = %", sum(arr));
}
string_views :: fn () {
print_log("=== string_views ===");
// Comptime constant string literal.
name :: "Martin";
// String views are just slices pointing to actual string data.
// Print the string directly.
print_log("My name is %.", name);
print_log("len = %", name.len);
}
dynamic_strings :: fn () {
print_log("=== dynamic_strings ===");
// Dynamic string (similar to dynamic arrays) are again provided by
// default API in 'std' namespace.
// Allocate new dynamic string.
str :: str_make("Hello");
defer str_terminate(&str);
// Append the string.
str_append(&str, " World!");
// Print the string.
print_log("str = %", str);
}
type_info :: fn () {
print_log("=== type_info ===");
// Language provides detailed runtime type information via 'typeinfo()'
// builtin function.
// Print the type info for 'print' function.
print_log("%", typeinfo(print));
}
structs :: fn () {
print_log("=== structs ===");
{
// Struct type declaration.
Entity :: struct {
position: [3]s32;
id: u32;
};
// To initialize variable of this type you can use compound initializer.
entity := Entity.{
position = .{ 10, 20, 30 },
id = 666,
};
// You can againg directly print the structure. Like this:
print_log("entity = %", entity);
// What is maybe more cool, you can print even the structure type:
print_log("T = %", Entity);
}
{
// Structure members can be tagged by u64 value known in compile-time.
NO_SERIALIZE : u64 : 1;
Entity :: struct {
position: [3]s32;
render_data: *u8 #tag NO_SERIALIZE;
id: u32;
};
// Tag is later accessible in structure member related type info, and
// can be used in runtime.
// Here we exclude automatically all members tagged as NO_SERIALIZE from
// serialization.
info_struct :: cast(*TypeInfoStruct) typeinfo(Entity);
loop i := 0; i < info_struct.members.len; i += 1 {
member :: info_struct.members[i];
if member.tag == NO_SERIALIZE then continue;
print_log("Serialize member '%'!", member.name);
}
}
}
function_taking_any :: fn () {
print_log("=== function_taking_any ===");
// The value of Any type contains pointer to type information and pointer
// to data. This way we can implement reusable "generic" function (like for example
// print/print_log we already used a lot).
print_number_or_bool :: fn (v: Any) {
if v.type_info.kind == TypeKind.INT {
// v.data is pointer to u8 type.
v_int :: @(cast(*s32)v.data); // @ is dereference of pointer
print_log("Number: %", v_int);
} else if v.type_info.kind == TypeKind.BOOL {
v_bool :: @(cast(*bool)v.data);
print_log("Boolean: %", if v_bool then "TRUE" else "FALSE");
}
};
// Notice we do not need to do explicit conversion here, anything can by converted
// to Any implicitly. (Even types)
print_number_or_bool(10);
print_number_or_bool(false);
}
function_returning_multiple_values :: fn () {
print_log("=== function_returning_multiple_values ===");
// In some cases you might need to return more than just one value from function
{
get_two_numbers :: fn () (s32, s32) {
return 10, 20;
};
a, b :: get_two_numbers();
print_log("a = %", a);
print_log("b = %", b);
}
{
// You can name these things...
get_two_numbers :: fn () (first: s32, second: s32) {
return 10, 20;
};
a, b :: get_two_numbers();
print_log("a = %", a);
print_log("b = %", b);
// You can use just one of the results.
first :: get_two_numbers();
print_log("first = %", first);
// Use blanbk identifier to ignore first value.
_, second :: get_two_numbers();
print_log("second = %", second);
}
}
error_handling :: fn () {
print_log("=== error_handling ===");
// There is really nothing special about error handling in BL; we just return an error,
// but multiple return values makes it a bit easier.
div :: fn (a: s32, b: s32) (result: s32, err: Error) {
if b == 0 then return 0, error("You can't divide by 0!");
return a/b, OK;
};
{
result, err :: div(10, 2);
if err then print_err(err) else print_log("result = %", result);
}
{
result, err :: div(10, 0);
if err then print_err(err) else print_log("result = %", result);
}
}
polymorph_functions :: fn () {
print_log("=== polymorph_functions ===");
// We also have generic/polymorph/templated/whatever functions in the language. You'll
// write thing we call "function recipe" and actual implementation is generated automatically
// when function is used.
// ? before type name indicates polymorph master type which is inferred from call
// side.
add :: fn (first: ?T, second: T) T {
return first + second;
};
// Use with integers.
print_log("add(10, 20) = %", add(10, 20));
// Use with floats.
print_log("add(10.f, 20.f) = %", add(10.f, 20.f));
}
comptime_functions :: fn () {
print_log("=== comptime_functions ===");
// Compile-time known expressions in the language are evaluated under the hood and
// replaced by constant results. We use integrated interpretter for this (the same thing
// used when you use 'blc -run something.bl').
// So we can have compile-time functions (executed during the compilation) and not
// existing in result executable produced by the compiler.
load_file_to_stack :: fn () []u8 #comptime { // Marked as comptime.
// Load current source file.
stream, open_err :: open_file(#file);
defer close_file(&stream);
if open_err then panic(open_err);
data: [..]u8;
read_err :: read_whole(&stream, &data);
if read_err then panic(read_err);
return data;
};
// Here the comptime function is executed during the compilation and its return value
// (content of this file in our case) becomes static string literal stored in DATA constant.
DATA :: load_file_to_stack();
print_log("Static data: %...", slice_range(DATA, 0, 32));
}
comptime_function_arguments :: fn () {
print_log("=== comptime_function_arguments ===");
// You can specify function arguments as comptime, this way we actually implement
// generic containers. You can pass types as comptime arguments.
MyArray :: fn (TValue: type #comptime, SIZE: s32 #comptime) type #comptime {
return struct {
data: [SIZE]TValue;
len: s32;
};
};
{ // Use for integer
arr: MyArray(s32, 10);
loop i := 0; i < 4; i += 1 {
arr.data[i] = i;
arr.len += 1;
}
print_log("arr = %", arr);
}
{ // Use for strings
arr: MyArray(string_view, 10);
loop i := 0; i < 4; i += 1 {
arr.data[i] = "Hello";
arr.len += 1;
}
print_log("arr = %", arr);
}
}
temporary_storage :: fn () {
print_log("=== temporary_storage ===");
// Temporary storage is preallocated memory in memory pool provided to every
// application implicitly in 'application_context' global variable.
// This might be useful in frame based applications where you need some scratch
// memory used each frame.
ROOT_DIR :: "/my/dir";
loop i := 0; i < 10; i += 1 {
// Our frame.
// tprint internally uses temporary storage to hold the resulting path here.
path :: tprint("%/%.txt", ROOT_DIR, i);
print_log("path = %", path);
// Resetting the temporary storage will keep memory preallocated, but 'path'
// becomes invalid after reset.
temporary_reset();
}
// Release the temporary storage (free preallocated pool). (this is done automatically
// at main function exit, but you might want to be explicit sometimes).
temporary_release();
// Note that the 'application_context' is muttable, and you can change various default
// things there (such as abort handler, log handler, global allocator, temporary allocator
// etc.).
}