Like most interpreted languages, there are several ways to create mutable values in Minilang. These include variables, list and map elements, object fields, etc. Some interpreted languages treat each of these as different forms of assignment, so

  • Local := Value is treated as set_local(Local, Value),

  • List[Index] := Value is treated as set_index(List, Index, Value),

  • Object:field := Value is treated as set_field(Object, :field, Value),

  • etc.

Instead Minilang expects operations such as element or field access to return assignable references. This means that

  • Local := Value is treated as assign(Local, Value),

  • List[Index] := Value is treated as assign(List[Index], Value),

  • Object:field := Value is treated as assign(Object:field, Value),

  • etc.

Each type of reference has an internal method that defines assignment to the reference.

This approach has the advantage that any expression that returns a reference can be assigned to, including function calls, conditional or loops expressions, etc. The disadvantage (internally) is that when not used for assignment, the current value of a reference needs to be extracted as an extra step. This is handled automatically within Minilang.

Using the old value

Minilang does not provide augmented assignments such as X += Y, etc. Instead the keyword old can be used in the right hand expression of an assignment to refer to the current value of the target reference. Note that even if the target of an assignment is a complex expression, it is only evaluated once.

1var X := 10
2print('X = {X}\n')
3X := old + 5
4print('X = {X}\n')
X = 10
X = 15

old can be used multiple times, and in any position, allowing for more flexible updates to values.

1var L := [1, 2, 3]
2print('L = {L}\n')
3L[2] := old + (old * old)
4print('L = {L}\n')
L = [1, 2, 3]
L = [1, 6, 3]

References in closures and loops

Minilang captures references in closures without dereferencing. This means that variables visible in a closure can be assigned within the closure. When used within a loop, variables are allocated new instances for each iteration. Closures created in the loop will likewise capture the current instance of each variable.

 1let L := [], M := []
 2var Sum := 0
 3for I in 1 .. 10 do
 4   var J
 5   L:put(fun() J := I)
 6   M:put(fun() Sum := old + J)
 8for F in L do F() end
 9for F in M do F() end
10print('Sum = {Sum}\n')
Sum = 55

Passing arguments by reference

By default, arguments to function calls are derefenced before the function code is executed, and bound to immutable parameters (equivalent to a let declaration). If the original reference is required, the parameter can be declared using ref which will bind the argument without derefencing.

1fun incr(ref X) do
2   X := old + 1
5var Y := 10
6print('Y = {Y}\n')
8print('Y = {Y}\n')
Y = 10
Y = 11

Creating references

Since references are typically derefenced during function calls, they are not derefenced when returned from a function. This allows functions (and methods) to return assignable references when required. For example, the implementations of :"[]" for lists and maps return assignable references to the corresponding element.

1var Z := "original"
2fun test() Z
4print('Z = {Z}\n')
5test() := "updated!"
6print('Z = {Z}\n')
Z = original
Z = updated!