Values, references and memory
Salam runs without a garbage collector. Nothing is freed behind your back, and nothing is shared unless you can see why. This lesson shows when a value is copied, when it is shared, and who is responsible for giving memory back.
Structs are copied
When you put a struct into another variable, or pass it to a function, a copy is made, and changing the copy leaves the original alone. If a function has to change the caller's value, declare the parameter as a reference with &:. A reference also saves copying a large struct; the functions lesson has more on it.
struct Account:
pub balance: int = 0
end
func deposit(a &: Account, amount: int):
a.balance += amount
end
func main:
mut first := Account { balance = 100 }
mut second := first
second.balance = 500
println first.balance, second.balance
deposit(first, 25)
println first.balance
endArrays are copied, slices are views
A fixed array such as int[3] is a value too: assigning it or passing it as int[3] copies every element. A slice parameter, int[], is a view of the caller's array, so writing through it changes the original. Choose the parameter type by what the function is allowed to do.
func zero_first(scores: int[3]):
scores[0] = 0
end
func zero_first_in_place(scores: int[]):
scores[0] = 0
end
func main:
week := [7, 5, 9]
mut plan := week
plan[1] = 100
println week, plan
zero_first(week)
println week
mut live := [7, 5, 9]
zero_first_in_place(live)
println live
endA Vector shares its storage
A Vector or a HashMap is a small header (where the items are, how many there are, how much room is left) that points at storage on the heap. Assigning one copies only the header. Both names then see the same items, so cart.set(0, "rice") shows through view, but each name keeps its own length, so the "tea" pushed through cart is not counted by view. Do not rely on that half-shared state. When you need an independent value, ask for one with copy(), and free it like any other Vector.
func main:
cart := Vector {} as Vector<str>
defer cart.free()
cart.push("bread")
cart.push("milk")
view := cart
cart.set(0, "rice")
cart.push("tea")
println cart, view
saved := cart.copy()
defer saved.free()
cart.set(1, "eggs")
saved.push("salt")
println cart, saved
endA HashMap behaves the same way, and so does a Vector inside a struct: copying the struct copies the header of its Vector, not the items.
copy and deep_copy
copy() gives new storage only for the outer container. When the items are containers themselves, as in a Vector of rows, the copy still shares every row. deep_copy() goes all the way down and copies the rows too. Change one seat in the original below: the shallow copy sees it, the deep copy does not.
func free_grid(g: Vector<Vector<int>>):
each row in g:
row.free()
end
g.free()
end
func main:
seats := Vector {} as Vector<Vector<int>>
repeat 2 in r:
row := Vector {} as Vector<int>
repeat 3 in c:
row.push(r * 3 + c + 1)
end
seats.push(row)
end
shallow := seats.copy()
deep := seats.deep_copy()
seats.get(0).set(0, 0)
println seats
println shallow
println deep
shallow.free()
free_grid(deep)
free_grid(seats)
endEach copy has to be freed, and so does every container a deep copy made: that is why free_grid frees each row before the outer Vector, while the shallow copy, whose rows belong to the original, only frees its own storage. A struct gets copy() and deep_copy() too, built by the compiler from its fields.
Freeing: free and defer
Numbers, booleans, plain structs and fixed arrays need no freeing. Anything that holds heap storage does: a Vector, a HashMap, a dyn box, and the strings described in the next section. The habit worth forming is to write defer x.free() on the line right after x is created, so it is freed when the function ends, whatever path it takes.
A function that builds and returns a container hands it over: from then on the caller owns it and frees it. Freeing a container frees its own storage, not what the items own, so a map of Vectors frees each Vector first.
struct Student:
pub name: str = ""
pub class: str = ""
end
func by_class(all: Vector<Student>): HashMap<str, Vector<str>>:
groups := HashMap {} as HashMap<str, Vector<str>>
each s in all:
names := groups.has(s.class) ? groups.get(s.class) : Vector {} as Vector<str>
names.push(s.name)
groups.put(s.class, names)
end
ret groups
end
func main:
students := Vector {} as Vector<Student>
defer students.free()
students.push(Student { name = "Sara", class = "7A" })
students.push(Student { name = "Reza", class = "7B" })
students.push(Student { name = "Mina", class = "7A" })
groups := by_class(students)
defer groups.free()
each class, names in groups:
println class, names
end
each _, names in groups:
names.free()
end
endLook at how by_class adds a name. groups.get(c) returns a copy of the Vector's header, so pushing onto it and doing nothing else could leave the map holding the old length. Taking the Vector out, pushing, and putting it back with put keeps the map up to date.
Who frees a string
A string literal such as "hello" is part of the program and is never freed. A string that is built while the program runs lives on the heap. The rule in the standard library is simple: a function that returns a str returns a new string that the caller owns, on every path, so you free it with mem.Free(s as void*) when you are done. Follow the same rule in your own functions.
import str
import mem
func slug(title: str): str:
ret str.Replace(title, " ", "-")
end
func tag(title: str): str:
s := slug(title)
defer mem.Free(s as void*)
ret "#" + s
end
func main:
url := slug("my first post")
defer mem.Free(url as void*)
println url
t := tag("salam news")
defer mem.Free(t as void*)
println t
b := str.NewBuilder()
defer str.BufFree(b)
repeat 3 in i:
if i > 0:
str.BufAppend(b, ", ")
end
str.BufAppend(b, "row ")
str.BufAppendInt(b, (i + 1) as i64)
end
report := str.BufStr(b)
defer mem.Free(report as void*)
println report
endtagfrees the result ofslugafter using it. Writingret "#" + slug(title)instead would leave that string with no owner, and it would never be freed.- Every
+on strings makes a new heap string, so a chain likea + b + cleaves the middle result behind. Short-lived programs do not mind; a loop or a server that runs for days does. For text built in pieces, use astr.NewBuilder(), take the result withBufStrand release the builder withBufFree.
Pointers and mem
For raw memory and for C libraries, Salam has pointers: i64* points at an i64, void* is an untyped pointer and null is the empty one. mem.Allocate(bytes) returns a block of memory, p[i] reads and writes the i-th element, and mem.Free gives the block back. sizeof(T) tells you how many bytes one element takes.
import mem
func main:
n := 5
squares := mem.Allocate((n as u64) * sizeof(i64)) as i64*
defer mem.Free(squares as void*)
repeat n in i:
squares[i] = (i * i) as i64
end
mut total := 0 as i64
repeat n in i:
total += squares[i]
end
println squares[4], total
nothing := null as i64*
println nothing == null
endNothing checks a pointer for you: reading past the end of the block, or using it after mem.Free, is a bug that may not show up right away. Ordinary Salam code rarely needs pointers; reach for a Vector first. The C interop lesson shows where pointers are needed.
Finding leaks with memcheck
salam memcheck app.salam builds the program with debug information and runs it under a memory checker: Valgrind when it is installed, AddressSanitizer otherwise. When the program ends it reports memory that was never freed and any read or write outside a block, each with the line of Salam code that allocated it. salam build app.salam --asan builds the same AddressSanitizer check into an ordinary executable.
The string and pointer examples above run under salam memcheck with no errors. Run it now and then on your own programs, and always on anything meant to run for a long time.
Rules of thumb
- Structs and fixed arrays are values: assigning copies them. Use
&:when a function must change the caller's value. - Assigning a Vector or a HashMap shares its items. Use
copy()ordeep_copy()when you need an independent value. - Whoever creates heap memory frees it, or hands it to someone who will. Write
defer x.free()right after creatingx. - A
strreturned by a function belongs to the caller; free it withmem.Free. Never free a literal. - Free what is inside a container before the container itself.
- Check long-running programs with
salam memcheck.