Skip to content

The basics

This section covers Blombly commands for writing basic sequential code. It includes concepts that may already be familiar, such as comments, variable handling, builtin datatypes, and flow control. But rarer features are added to the mix, like immutable variables, currying, and deferred execution.

Comments

Blombly only has line comments that start with //. However, it supports multi-line strings and these can be used for documentation as in the snippet below. Do not worry about bloating the size of intermediate representation files, as the compiler optimizes away unused code segments that cannot produce side-effects.

// main.bb
"This is multiline documentation 
that describes the current file.";
print("Hello world!"); // This is a line comment.
> ./blombly main.bb
Hello world!

Scopes

Scopes refer to isolated execution contexts. Each program starts from a global scope, and new ones are entered when creating structs or calling functions. Assign values to variables per @var = @value;. This also creates the variables if they do not already exist. Subsequent code can normally overwrite those values, but prepend the final keyword to their last assignment to make them immutable. This prevents overwrites by subsequent code and exposes the variables to running functions. For now, consider immutability as a code safety feature. Below is what invalidation looks like, but keep in mind that the same symbols can be written anew in new scopes.

// main.bb
final x = 0;
xinc() = {  //callable code block 
  print(x); // get x from parent scope
  x = x+1;  // this is a new scope, so any assignment creates new variables
  return x;
}

y = xinc();
print(y);
x = x+1; // CREATES AN ERROR
> ./blombly main.bb --strip
0
1
 (ERROR) Cannot overwrite final value: x
      add x x _bb2                                      main.bbvm line 4

Builtins

There are several builtin data types that are directly incorporated in the language. Exhaustively, these are int, float, bool, str, list, vector, map, iter, code, struct, file, server, sqlite, graphics, error. Here we start with the first four, and split the rest to dedicated pages. Some well-known operations are implemented, computed as one might have come to learn from other programming languages. Only differences to usual practices are the existence of as assignments (more details later), and that element access is overloaded by some data types.


For example, format a float to a string of three decimal digits per x[".3f"]. Time can be obtained as a float number of second from the start of the system's clock epoch with the time() command. This is computed by counting the time elapsed from the start of the program and adding that to the wall clock time retrieved at the start of the program. If system time changes while a program is running, you need to restart it; this ensures that time differences can be obtained and are monotonically increasing. Below is a list of numeric formats (applicable to both ints and floats by implicitly converting between the two)

Operations
Category Operation Description
Assignment (expression) Compute the expression first. Also used in method calls later.
Assignment y=x, y as x The return values of assignments are covered below.
Conversion typename(x) Everything can be converted to str, numbers can be converted from str.
Elements a[i], a[i]=x Element get and set, for example for lists and maps.
Arithmetic +, -, *, /
^
%
Basic arithmetic (division is floating-point).
Exponentiation.
Modulo for integers.
Self-assignment op= Replace op with any arithmetic, string, or boolean operation.
Iterable operations +
a<<i
pop(a)
next(a)
String or list concatenation.
Push value `i` to `a` (often to the end, returns either the iterable or a result).
Extract the last value.
Extract the first value.
Comparisons <, >, <=, >=
==, !=
Inequality comparisons.
Equality comparisons.
Boolean operations and, or
not
Logical operations for booleans.
Negation of any boolean value it prepends.
Number to string formatting (through examples)
Visual category Operation Example DEscription
Integer "d" 42["d"] → "42" Decimal (default integer)
"x" 255["x"] → "ff" Hexadecimal (lowercase)
"X" 255["X"] → "FF" Hexadecimal (uppercase)
"b" 5["b"] → "101" Binary format
Float ".2f" 3.14159[".2f"] → "3.14" Fixed-point with 2 decimals
"2.1f" 2.718["2.1f"] → "2.7" Fixed-point with 1 decimal
".2F" 3.14159[".2F"] → "3.14" Same as f
".2e" 1234.5678[".2e"] → "1.23e+03" Scientific notation (lowercase e)
".2E" 1234.5678[".2E"] → "1.23E+03" Scientific notation (uppercase E)
".6g" 3.1415926535[".6g"] → "3.14159" General format
Date/time "%Y-%m-%d" number["%Y-%m-%d"] → "2025-03-25" ISO date format
"%d %b %Y" number["%d %b %Y"] → "25 Mar 2025" Day, abbreviated month, year
"%I:%M %p" number["%I:%M %p"] → "09:15 AM" 12-hour clock with AM/PM
"%Y-%m-%d %H:%M:%S" number["%Y-%m-%d %H:%M:%S"] → "2025-03-25 09:15:00" Full datetime format (24-hour)

Blombly also supports string interpolation as syntactic sugar: expressions enclosed in the pattern !{...} within string literals are replaced at compile time with their evaluation and converted to str. For safety, symbols that terminate expressions (brackets, colons, or semicolons) are not allowed within linterpolation. Here is an example that contains several operations.

// main.bb
x = int("1");
y = float("0.5");
x += 1;
print("Sum is !{x+y}");
print("Is it positive? !{x+y>0}")
> ./blombly main.bb
Sum is 2.500000
Is it positive? true

Currying

Blombly offers the currying notation @value|@func as the simpler equivalent of calling a function of one argument @func(@value) while avoiding excessive parentheses. This has lower priority than all other symbols and operations other than member or item access; the goal is to convert specific values. In the example below, declare your own string formatting function and apply it after converting a string to a float number.

// main.bb
// `=>` is a shorthand for functions that directly return
fmt(x) => x[".3f"];
print("1.2345"|float|fmt);
> ./blombly main.bb
1.234

Similarly to numeric operations, the expression variable |= func; reassigns to a variable. In this case, however, the leftwise function is applied first, enabling the pattern variable |= func1|func2|...;. The modelled property is considered to semantically hold true for subsequent code, bringing strong typing conventions.

Info

This notation is intentionally similar to double turnstile and may be thought of as a way to indicate that a variable being able to model some property through a series of transformations. For example a statement x|=float; indicates that x can be converted to a float and will be treated thusly from thereon.

Tip

For easily readable code, use the dash (|) notation for all possible function calls of one argument while keeping at least one pair of parentheses like so: value = float("number"|read); This lets the outcome's type and the starting variable appear side-by-side, with intermediate transformation steps following.

Control flow

Control flow alters which code segments are executed next. Blombly offers similar options to most programming languages in terms of conditional branching, loops, deferring execution for later, function calling, and error handling. The first three is described in this and the next section, but functions are described elsewhere because they offer more options than what is common.


Conditionals take the form if(@condition){@accept}else{@reject} where @ denotes code segments or expressions and is the nnotation also used by macros. The condition must yield a boolean and makes the respective branch execute, where the else branch is optional. You may omit brackets for single-command segments, but put semicolons only at the end of commands.


Similarly, loops take the form while (condition) {@code} and keep executing the code while the condition is true. To avoid ambiguity, there are no other ways in the language's core to declare a loop, albeit the in macro is a shorthand for looping through iterables, like lists. Again, omit brackets if only one command runs. Here is an example with both control flow options.

// main.bb
i = 0;
while (i<5) {
    // no semicolon before `else`
    if(i%2==0) print("!{i} is even")
    else print("!{i} is odd");
    i += 1;
}
> ./blombly main.bb
0 is even 
1 is odd 
2 is even 
3 is odd 
4 is even 

Tip

Emulate switch, continue, or break by intercepting return signals. This is described later.

Defer

Deferring sets code to run later. The deferred code is always executed, even if errors occur in the interim. Normally, defer occurs just before return statements, including returning from new scopes that create structs. It is also applied upon entering and exiting do blocks. In advanced settings, you can clear resources, remove cyclic struct references, or completely clear struct contents. Here is a usage example, where we ignore the brackets of the deferred code block for simplicity.

// main.bb
// create a struct creation scope
A = new {
    x=0;
    defer x=1;
    print(x);
}
print(A.x);
> ./blombly main.bb
0
1

Warning

Defer cannot contain return statements, as it may already be executed in response to return statements.