Simple fast value semantics

Summary

Semantics means behavior. Value semantics is about value independence, meaning that changing one variable won't change any other.
In contrast, reference semantics allows multiple variables to share the same underlying memory, thus changing one variable changes the other.
In Boomla, variables holding temporary values have value semantics, while variables referencing stored values (eg. files) have reference semantics.

Example

a := []string{"A"} b := a b[0] = "B" print(a) print(b)
Given the above example, a programming language where arrays have reference semantics would print [B][B], a language where arrays have value semantics would print [A][B]. (See the above image.) Boomla belongs to the latter group.

How reference semantics was born

Reference semantics is typically implemented via pointers. Some languages like C make them explicit, others like JavaScript make them implicit. Either way, both languages provide reference semantics for certain types and they both use pointers under the hood.
So why were pointers introduced?
  1. Variable sized memory. We know that a 64bit number will use 64bits of memory. For other types, like strings, we don't know their size in advance, hence we are forced to dynamically allocate their memory at runtime and reference them using a pointer. For this reason, language designers were forced to use pointers.
  2. Speed, memory usage. When passing around large objects within a program, making a new copy every time would be prohibitively slow and one would quickly run out of memory. Instead, we pass around a pointer to the underlying data, which solves both problems.
Unfortunately, while introducing pointers solves the above problems, it introduces a new one: shared mutable state. Now developers can't tell if a piece of code owns a particular object. This means that modifying (mutating) it may have unexpected side effects, alas bugs. These bugs tend to be hard to reproduce, find and debug.
As a result, modern languages set out to solve the problem of shared mutable state. We believe Boomla has a unique solution that is simple to learn, simple to use and also has great performance.

Immutable values

The trivial approach to avoid shared mutable state is by getting rid of the mutable part by never changing (mutating) anything. In other words, everything is immutable. That way we can not have unexpected side-effects, since we never have any side effects in the first place. This approach has been popular among purely functional programming languages. The downside is that all this copying is slow.
Here is a visualization of how this plays out in practice. The code statements on the left are executed one after the other. On the right, you can see how the memory allocation of the program evoles.

Mutable value semantics

A more complex approach with better performance is to only require making a copy when there are multiple references to that object. In other words, when the object is shared. When an object is owned (not shared), in-place mutations are allowed as there can be no side effects. Given that mutations are often allowed, it is called mutable value semantics.
Boomla uses this approach. It is the fast in simple fast value semantics.
Here is the same visualization as above for mutable value semantics. Notice how much less garbage is created.

Reference counting

Under the hood, this solution uses reference counting. When a new reference is created, we increment this counter, when the reference is no longer used, we decrement the counter. When the reference count equals 1, we are allowed to mutate the object, otherwise we have to make a copy first.
Here is the same visualization as above with the reference counts revealed.

Implicit copying

Boomla analyses the code before execution. Where one variable is assigned to another, Boomla implicitly makes a copy or adjusts the reference count. You don't need to do anything about this. Two variables (that are not stored objects like files) always behave as if they were completely independent copies.
This is the simple in simple fast value semantics.
In the following example, a goes out of scope so b can be mutated in place.
a := "A" b := a // `a` goes out of scope b += "A" // `b` is the only reference, thus it can be mutated in place println(b)
While in the following example, aand b share the same underlying memory with reference count 2, so b needs to be copied before it can be mutated in place.
a := "A" b := a // `a` is used later b += "A" // `b` is copied before it can be mutated println(a) // prints "A" println(b) // prints "AA"

Only copy what is necessary

When mutating a large shared data structure, only those parts are copied that are affected by the write. Unchanged parts will remain shared. This greatly improves performance.
a := [][]string{[]string{"A1", "A2"}, []string{"B1", "B2"}} b := a b[1][1] = "MOD" println(a) println(b)
The resulting memory layout looks like the illustration below. In a large object, only the modified nodes and their ancestors need to be owned, thus potentially copied.

Consistent behavior across all data types

Data types have inconsistent behavior in mainstream programming languages. Numbers and boolean values are small and thus tend to have value semantics. The rest of the types have either value or reference semantics based on ad-hoc decisions. One example is string types where some languages decided giving them reference semantics would be too buggy, so they made them have value semantics. But this makes concatenating strings really slow, so they tend to also provide a StringBuilder type, which is essentially another string type with reference semantics, except it clumsy to use.
Mutable value semantics enables a language to have consistent behavior across all types. No hacks like a StringBuilder type are needed.
It offers the safety benefits of value semantics while also providing the performance characteristics of reference semantics. Plus a clean, consistent behavior across all types that makes learning the language easier.

No const

Boomla doesn't have a const keyword. While supporting it would be trivial, it doesn't add any value. Const is a language hack introduced by some languages to give developers some protection against unexpected mutations. Given that unexpected mutations can not happen in Boomla, there is no point in supporting this feature.

Function inputs

Function inputs are independent variables, so changing them has no external effect.
The following example uses an int type to demonstrate value independence, but it could be any type, like an array or a struct, as they behave identically for value semantics purposes.
fn increment(n int) { n++ } n := 1 increment(n) print(n) // prints 1

Changing input parameters

Consequently, to change an input variable at the call site, you have to return it.
fn increment(n int) (int) { n++ return n } n := 1 n = increment(n) print(n) // prints 2

Syntactic sugar

Mental models matter and developers are used to modifying variables in place. To support this mental model, Boomla provides a syntactic sugar for the above. It's only purpose is to provide a more ergonomic developer experience, but still works exactly as in the above example.
You can use the mutation symbol ~ (also called inout) before the variable name both in the function definition and in the function call. That way, the variable will be passed in at the start of the call, and will be implicitely passed back and reassigned at the end of the call.
fn increment(~n int) { n++ } n := 1 increment(~n) print(n) // prints 2
This works with methods as well.
type Num int fn (~n Num) increment() { n++ } n := Num(1) ~n.increment() print(n) // prints 2

Operators

In a way, ~n.increment() and n++ behave very much the same way, yet operators like n++ and n += 1 do not require the mutation sign before the variable names.
That's because Boomla optimizes for ergonomics, and it is totally obvious from n++ that the variable n will be modified. Having the mutation sign everywhere adds no value, it just makes the code harder to read. The mutation symbol is only used in function signatures and function calls.

Closures

Closures are functions that capture the environment around them. In Boomla, because of value semantics, those variables are captured by value. Modifying those values inside the closure will have no external effect. In fact, Boomla won't allow you to change them before you make a copy, to prevent the case where you mistakenly believe those mutations would affect the outer variable.
n := 1 inc := fn() { n += 1 // <-- error: can not write captured variable, copy it first } inc()
To have an external effect, you need to pass in the variable instead:
n := 1 inc := fn(~n int) { n += 1 } inc(~n) print(n) // prints 2