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<<ipop(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, ornot |
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.