Lists & arrays#
> V3 has two collection surfaces: a typed List<T>, and an older untyped handle. Know which one you are holding.
This is the one part of V3 with genuine overlap, because List<T> was added on top of a pre-existing untyped array. Both still work, and they are not interchangeable everywhere.
List<T> | Legacy handle | |
|---|---|---|
| Type | List<int>, List<word>, List<Shape>, … | int |
| Created by | [a, b, c], List::new(), List::with_capacity(n), List::filled(v, n) | array_new() |
| Element type | Checked | Always word |
| Length | list.length() | array_len(h) |
| Read | list[i] | array_get(h, i) |
| Write | list[i] = v (needs pilot mut) | array_set(h, i, v) |
| Append | list.push(v) | array_push(h, v) |
| Remove | list.pop() | — |
Accepted by array_* builtins | yes, when List<word> | yes |
Accepted by std/algorithm.fk tasks | no | yes |
The List type#
The element type must be a scalar — int, num, word, bool — or the name of a shape. There is no nesting: List<List<int>> does not parse.
-- `List<T>` is V3's typed sequence. The element type must be a scalar
-- (`int`, `num`, `word`, `bool`) or the name of a shape. Nesting is not
-- allowed: there is no `List<List<int>>`.
shape Contact {
tag: word
}
-- A List may be a parameter and a return type. It may NOT be a shape
-- field: that reports "V3 owned shape fields do not yet support List
-- values".
task count_all(items: List<word>) -> int {
give back items.length()
}
task first_three() -> List<int> {
give back [1, 2, 3]
}
task main() -> void {
-- An array literal infers List<T> from its elements.
pilot names: List<word> = ["Takeru", "Meiya", "Sumika"]
say word_from_int(names.length())
say names[1]
-- Indexed assignment requires `pilot mut`.
pilot mut scores: List<int> = [10, 20, 30]
scores[0] = 99
say word_from_int(scores[0])
-- Every scalar element type works.
pilot ratios: List<num> = [1.5, 2.5]
say format_num(ratios[1])
pilot flags: List<bool> = [true, false]
say word_from_bool(flags[0])
-- Shapes too, and field access chains off the element.
pilot contacts: List<Contact> = [Contact { tag: "BETA-1" }]
say contacts[0].tag
-- List::filled(value, count) builds a pre-populated list.
pilot zeros: List<int> = List::filled(0, 4)
say word_from_int(zeros.length())
-- Passed to a task, and returned from one.
say word_from_int(count_all(names))
say word_from_int(first_three()[2])
-- Lists grow as of v0.14.2. Mutating methods need `pilot mut`.
pilot mut queue: List<int> = List::new()
queue.push(10)
queue.push(20)
queue.push(30)
say word_from_int(queue.length())
say word_from_int(queue.pop()) -- 30, removed and returned
say word_from_int(queue.length())
say word_from_int(queue.capacity()) -- starts at 8, doubles as needed
queue.reserve(64)
say word_from_int(queue.capacity())
queue.clear()
say word_from_int(queue.length())
-- Iterate with an index; there is no `for each`.
pilot mut i: int = 0
repeat names.length() times {
say names[i]
i += 1
}
}
3
Meiya
99
2.5
true
BETA-1
4
3
3
3
30
2
8
64
0
Takeru
Meiya
Sumika
Rules worth pinning down:
- **
[a, b, c]infersList<T>** from its elements, all of which must share one type. Numbers stay numbers; no conversion towordis needed. - **
.length()is a method**, not a field.list.lengthreports "non-shape value has no fields". - **Indexed assignment needs
pilot mut.** Otherwise you get "indexed assignment requires a mutable list binding; declare it with pilot mut". - **
List::filled(value, count)** builds a pre-populated list and takes its element type fromvalue. - Indexing a shape element chains:
contacts[0].tag.
Growing a list#
As of v0.14.2 lists grow. This is the biggest change to the type since it was introduced, and it removes most reasons to reach for the legacy handle.
| Method | Signature | Notes |
|---|---|---|
.push(value) | T -> void | Append |
.pop() | -> T | Remove and return the last element |
.length() | -> int | |
.capacity() | -> int | Allocated slots; starts at 8 and doubles |
.reserve(n) | int -> void | Grow the capacity up front |
.clear() | -> void | Length to zero, capacity kept |
pilot mut xs: List<int> = List::new()
xs.push(10)
xs.push(20)
say word_from_int(xs.pop()) -- 20
say word_from_int(xs.length()) -- 1Constructors: List::new(), List::with_capacity(n), List::filled(v, n), or a literal.
Mutating methods need pilot mut, same as indexed assignment.
Still missing: insert, remove, sort, and any iterator. Sorting a list means copying into a legacy handle, or writing the loop.
Where a List may appear#
| Position | Allowed |
|---|---|
pilot / fixed pilot binding | yes |
| Task parameter | yes |
| Task return type | yes |
| Shape field | no |
A shape field cannot hold a list: ``text type error: V3 owned shape fields do not yet support List values ` Store a legacy int` array handle in the field instead, or keep the list in a separate binding beside the shape.
With push/pop landed, List<T> is now the default choice. The legacy handle is worth keeping for two things only: reaching the std/algorithm.fk helpers, which are declared handle: int, and storing a collection in a shape field, which List<T> still cannot do.
The legacy handle#
array_new() returns an int handle to a runtime array whose elements are always word. Store other types by converting them in and out.
-- V3 arrays are runtime handles of `word` elements, addressed by an
-- `int` handle. Store other types by converting them to `word`.
task main() -> void {
pilot xs: int = array_new()
array_push(xs, "alpha")
array_push(xs, "bravo")
array_push(xs, "charlie")
say word_from_int(array_len(xs))
say array_get(xs, 0)
say array_get(xs, 2)
array_set(xs, 1, "BRAVO")
say array_get(xs, 1)
-- Iterate with an index and a counted loop.
pilot mut i: int = 0
repeat array_len(xs) times {
say array_get(xs, i)
i += 1
}
array_release(xs)
}
3
alpha
charlie
BRAVO
alpha
BRAVO
charlie
| Call | Signature |
|---|---|
array_new() | -> int |
array_push(h, value) | word-array, word -> void |
array_get(h, i) | word-array, int -> word |
array_set(h, i, value) | word-array, int, word -> void |
array_len(h) | word-array -> int |
array_release(h) | word-array -> void |
word_join(h) | int -> word |
word-array is a checker-internal marker meaning "either an int handle or a List<word>" — it is not a type you can write in source. That is why the array_* builtins accept both, while a user-declared task with handle: int accepts only the handle.
Mixing the two#
-- An array literal infers `List<T>` from its elements. Numbers stay
-- numbers -- there is no need to convert them to `word` any more.
--
-- The older untyped `array_*` handle still exists alongside it; see
-- arrays.html for when each one applies.
task main() -> void {
pilot names: List<word> = ["Takeru", "Meiya", "Sumika"]
say word_from_int(names.length())
say names[1]
pilot numbers: List<int> = [3, 1, 2]
say word_from_int(numbers.length())
say word_from_int(numbers[0])
-- The array_* builtins accept a List<word> as well as a raw handle.
say word_from_int(array_len(names))
say array_get(names, 2)
-- std/algorithm.fk tasks are declared with `handle: int`, so they take
-- only the legacy handle. Build one with array_new() to use them.
pilot h: int = array_new()
array_push(h, word_from_int(3))
array_push(h, word_from_int(1))
array_push(h, word_from_int(2))
array_sort_int(h)
say array_join(h, ",")
say word_from_int(array_sum_int(h))
say word_from_int(array_max_int(h))
say word_from_int(array_binary_search_int(h, 3))
array_reverse(h)
say array_join(h, "|")
say word_from_bool(array_contains(h, "2"))
array_release(h)
}
3
Meiya
3
3
3
Sumika
1,2,3
6
3
2
3|2|1
true
The rule that follows from the table above: builtins bridge, library tasks do not.
pilot names: List<word> = ["b", "a"]
say word_from_int(array_len(names)) -- fine: builtin, accepts word-array
say array_get(names, 0) -- fine
-- array_join is a task in std/algorithm.fk declared `handle: int`:
-- say array_join(names, ",")
-- type error: call to 'array_join' argument 1 expects int, got List<word>If you need the library helpers, build a legacy handle with array_new().
Iterating#
There is no for each in either surface. Keep an index and use a counted loop:
pilot mut i: int = 0
repeat items.length() times {
say items[i]
i += 1
}repeat N times evaluates its count once, so growing the collection inside the loop will not extend the iteration.
std/algorithm.fk helpers#
Plain FREAK source, linked by freak build and freak run (not freak check). Every helper takes a **legacy int handle**.
| Task | Signature | Notes |
|---|---|---|
array_sort_int(h) | int -> void | In-place insertion sort, numeric order |
array_binary_search_int(h, target) | int, int -> int | Index or -1; needs a sorted array |
array_reverse(h) | int -> void | In place |
array_find(h, target) | int, word -> int | Index or -1 |
array_contains(h, target) | int, word -> bool | |
array_count(h, target) | int, word -> int | |
array_copy(h) | int -> int | New handle, shallow copy |
array_join(h, sep) | int, word -> word | |
array_sum_int(h) | int -> int | |
array_max_int(h) / array_min_int(h) | int -> int |
Two of these are broken, re-confirmed against the compiler that verified this site: - array_sort_word(h) segfaults. It routes through extern task freak_word_compare(a: word, b: word) -> int, and that declaration does not bridge the LLVM backend's word handle representation. See extern & FFI. - array_unique(h) returns an empty array. To sort words, keep a parallel int key array and sort that, or write an insertion sort in your own code comparing with .char_at().
Memory#
array_release(h) frees a legacy array. V3 has no garbage collector and no automatic drop, so a long-running program that allocates in a loop must release. Do not use a handle after releasing it, and do not release twice.
List<T> values have no explicit release call.
Passing collections to tasks#
A legacy handle is just an int, so it passes freely and the callee mutates the caller's array — the standard way to return a collection:
task fill(out: int) -> void {
array_push(out, "alpha")
array_push(out, "bravo")
}
task main() -> void {
pilot items: int = array_new()
fill(items)
say word_from_int(array_len(items))
array_release(items)
}A List<T> can also be a parameter or return type, since parser_take_type accepts the annotation in both positions.
Maps and sets#
Still none. Map<K,V>, Set<T> and Lineup<T> do not exist, and the { "k": v } map literal does not parse. For small lookups, use two parallel collections and array_find.