Quick Start

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.).
}