In Scopes, the let
keyword is an immutable binding to a name. Sometimes we call them register variables, which is an LLVM-ism. Because it's just a binding with no location in memory and a completely abstract construct, you can alias things that aren't necessarily values, like types or keywords. You can bind anything to a valid symbol; you could even shadow let
itself.
You can mutate 3 different types of values in Scopes: stack variables, global state variables and mutable pointers.
- Stack variables are declared with the
local
keyword. They expire when you exit the function frame (ie. return). - Global state variables are declared with the
global
keyword. Their memory location is static and always available. It's always safe to take a pointer to global data (although it might not be to do so from multiple threads). - Pointers are addresses to a memory location. You can take the address from a local, global or through memory allocation (
malloc
, or using data types like Array, which implies topointer
). Taking the address of a variable will yield a mutable pointer by default, but you can cast it to an immutable one like we'll explain in the next section.
A bit of nomenclature: local
s and global
s are references. When you pass a reference as a function argument, it is still mutable inside the function body. You can't return a reference from a function - it is automatically dereferenced. To explicitly dereference a reference, use the deref
keyword, which copies the value.
To create a reference type:
let T = (& i32)
The pointer
supertype can be used to create other pointer types. The mutable
keyword makes it so the value pointed to can be modified. For example:
let pT = (pointer i32) # alternatively, (@ i32)
# memory leak for educational purposes only, don't try this at home
let iptrT = (typeof (malloc i32))
print (iptrT == (mutable pointer i32) and iptrT == (mutable@ i32))
One takes the address of a mutable variable with the &
prefix, and dereferences it with @
:
local a : i32 = 10
let ptr = &a # or (& a)
print @a # 10; alternatively (@ a)
# get a function pointer:
fn A (val)
print val
val + 1
let fptr = (static-typify A i32) # return type is inferred based on argument types, so you just supply the arg types
print ((typeof fptr) == (pointer (function i32 i32))) # note that when constructing the type from scratch the return type is required
# pointers always cast to each other. Do this with care.
local a : i32
let v* = (&a as voidstar) # equivalent to C's void*
print (@ (v* as (mutable@ u32)))
# coerce an Array to access its raw memory:
using import Array
local arr : (Array i32)
let arrptr = (imply arr pointer) # becomes (@ i32)