Lists
[T] is a list: any number of values of type T, growing and shrinking as
the program runs. Lists work in hosted and native programs, with the same
meaning; freestanding programs, which have no allocator, use arrays or
alloc.Vec(T) (Standard library).
let xs: [i64] = [3, 1, 2] // a list, because a list is expectedpush(xs, 4) // [3, 1, 2, 4]print(len(xs)) // 4print(pop(xs)) // 4; xs is [3, 1, 2] againxs[0] = 10let tail = xs[1:] // [1, 2], a new listlet ys = xs + [5, 6] // [10, 1, 2, 5, 6]let zeros: [u8] = [0; 1024] // 1024 zeros; the length may be any i64Making lists
Section titled “Making lists”- A list literal is written like an array literal,
[a, b, c]or[v; n], where a list is expected: a typedlet, a parameter, a field, a return value, an element of another list. Elsewhere[1, 2]is an array[i64; 2]. []is the empty list; it always needs a list type from where it is used.- In
[v; n],nis anyi64expression, worked out when the program runs. a + bis a new list with the elements ofaand then those ofb.
Using lists
Section titled “Using lists”| operation | result |
|---|---|
len(xs) |
the number of elements, an i64 |
xs[i], xs[i] = v |
element i, bounds-checked as for arrays |
xs[lo:hi], xs[lo:], xs[:hi] |
a new list of elements lo up to hi, as for strings |
push(xs, v) |
appends v |
pop(xs) |
removes the last element and gives it; the program stops if the list is empty |
a == b, a != b |
the same length and equal elements (Equality) |
push and pop change the list they are given, which must be something that
can be assigned: a variable, a part of one (s.items, grid[i]), or what a
cell holds (*c, c.items). An index past the end is an error, as for
arrays: a list grows only by push.
Lists are values
Section titled “Lists are values”Like arrays and structs, lists are values: assigning a list, passing it to a function or storing it in a field makes a copy, and changing one copy never changes another.
fn add_one(xs: [i64]) -> [i64] { push(xs, 1) // changes the function's own copy return xs}
let a: [i64] = [0]let b = add_one(a)print(len(a)) // 1print(len(b)) // 2To have several parts of a program change the same list, keep it in a
cell: push(*c, v) changes what every holder of the cell sees.
Performance
Section titled “Performance”A copy costs nothing until one of the copies is changed: the VM shares the elements and copies them once, at the first change after the list was copied somewhere (VM and GC). So:
push,popand element writes on a list that nothing else holds take constant time (pushamortized): a loop building a list ofnelements is O(n). This holds for a list in a variable, in a field of a struct in a variable, in another list, or in a cell.- Passing a list to a function and changing it there copies it once, for that call. A function that only reads its list never copies it.
xs[lo:hi]anda + bmake new lists, so they take time in proportion to their length.
Strings and bytes
Section titled “Strings and bytes”A str is not a list, but converts to and from one: from_bytes(b) makes a
string of the bytes in b: [u8], and strings.bytes(s) gives the bytes of a
string (Standard library).
Pushing bytes to a [u8] and converting it once at the end is how to build a
long string in time proportional to its length, where s = s + piece in a loop
copies the text so far each time.
Library
Section titled “Library”lib/lists.jh has generic functions on lists: sort, map, filter,
fold, reverse, index_of, contains and range
(Standard library).
Lists in native programs
Section titled “Lists in native programs”Natively there is no GC. Since a list is a value, the compiler can tell from the program alone where each list is last used, and frees it there; nothing is collected while the program runs.
- Assigning or passing a list that is not used again afterwards moves it: no copy. A copy is made only where the original is still used, as the meaning of the program requires.
- A copy shares the elements: it counts one more holder of the list’s memory, whatever its size. The first write to a list whose memory is shared copies that list (its elements, which are counted once more in turn, so a list of lists copies one level), and later writes do not. So keeping old versions of a value, or putting one list in many records, costs what it does on the VM, not a copy of everything each time.
- Reading a part, as
len(g.nodes[i].edges), copies nothing, also while another part of the same value changes (let t = s.textass.at += 1). - A value matched and not used again gives its parts away: in
match parse(s) { Done(v, rest) => ... }each arm takesvandrestout of the result instead of copying them. - A function that only reads a list parameter never copies it. One that
changes it copies it once, as on the VM, unless the caller’s list was at
its last use: then the function takes it over, so
xs = f(xs)changesxsin place. The same holds for a part:rows[i] = f(rows[i])andb.items = f(b.items)move the part out and back. xs = lists.map(xs, f)to the same type, andlists.filter, write intoxs’s memory: the library’s rules for them allocate nothing when the list is handed over.- A thread started with
go f(xs)gets its own list, and frees it.
jihoo build --cost lists every place a program copies (shares) a list, and why
(Command line), and a function declared
nocopy is checked to copy nothing at all.
A pointer cannot reach a list, or a value holding one: &xs is an error, as
is reading such a value through a pointer, since only its variable may free
its memory. To refer to a part of a value, use a path.
Layout
Section titled “Layout”Lists have no fixed layout, so size_of([T]) is an error, and they cannot be
passed to C. On the VM they live on the GC heap; natively a list is a
pointer to its elements, from malloc, with its length and capacity. In JIR
a list is [T], built with array or repeat and changed with push and
pop (JIR).