Skip to content
Prismio docs
ImplementedPrismio 0.1.0

Vec

Vec<T>, Prismio's growable vector — building one, its methods, what removing an element does to views of it, and the sorting and higher-order algorithms in std.vec.

Last verified

Implemented. Available in the audited Prismio 0.1.0 compiler. Pre-1.0 syntax may still change.

Vec<T> is the growable, contiguous, owned sequence — the vector you reach for by default. import std.vec for literals and methods.

prismio
import std.io
import std.vec

fn main() -> Int {
    let scores: Vec<Int> = [70, 85]
    scores.push(92)
    scores.insert(0, 60)

    println(scores.length)      // 4
    println(scores.first)       // 60
    println(scores.last)        // 92
    println(scores.contains(85))
    return 0
}

The type was called List<T> before 0.1's collections work. That spelling is now an error that names Vec<T>; nothing else about the type changed.

Build one

FormGives
let v: Vec<Int> = []an empty Vec
let v: Vec<Int> = [1, 2, 3]a Vec holding those elements, up to twelve of them
Vec<Int>.withCapacity(n)an empty Vec with room for n elements before it reallocates

The element type comes from what the literal is assigned to — an annotation, a struct field, a parameter. [1, 2] written as a Vec<I64> holds I64s.

A literal needs its elements to implement Copy (every scalar, String, and any struct that writes the impl), because each element is duplicated into the new Vec. Past twelve elements, push them.

Methods

v.name(args) is the call; properties are read without parentheses.

Size

PropertyAnswers
v.lengththe number of elements
v.isEmpty / v.isNotEmptywhether that number is zero
v.capacityhow many elements fit before the next reallocation

Read

FormAnswers
v[i]the element at i
v.first / v.lastthe element at either end
v.get(i)Option.Some(element), or Option.None when i is outside [0, length)
for x in veach element in order
v[a..b]a slice of the range

An index outside the Vec answers the element type's zero, not an error0 for an Int, "" for a String. Check length first, or use get, which says whether the element was there. last reads its receiver twice, so it has to be a name or a field of one; bind a call's result first.

Requires T: Eq (from std.eq, which covers every scalar and String).

MethodAnswers
v.contains(x)whether any element equals x
v.indexOf(x) / v.lastIndexOf(x)the first or last position holding x, or -1
v.countOf(x)how many elements equal x
v.binarySearch(x)the position of x in a sorted Vec, or -1; requires T: Ord
v.isSorted()whether the elements are in order; requires T: Ord

Change

MethodDoes
v.push(x)appends x
v.insert(i, x)puts x at i and moves the rest up; i may be length
v[i] = x / v.set(i, x)replaces the element at i
v.swap(i, j)exchanges two elements
v.pop()removes the last element: Option.Some(it), or Option.None when empty
v.removeAt(i)removes the element at i and returns it; everything after moves down
v.truncate(n)keeps the first n elements
v.clear()removes every element
v.reserve(n)makes room for n elements in total
v.extend(other)appends a copy of every element of other
v.reverse()reverses the order in place
v.sort() / v.sortBy(f)sorts in place — see Sorting

push, insert and set move the value in: the name you pushed is no longer usable. pop, removeAt, get and extend hand out copies, so they need T: Copy.

prismio
import std.io
import std.vec
import std.option

fn main() -> Int {
    let queue: Vec<String> = ["build", "test"]
    queue.insert(0, "fetch")
    queue.push("ship")

    let next = queue.removeAt(0)
    println(next)                  // fetch

    let finished = queue.pop()
    match (finished) {
        Option.Some(task) => { println(task) }   // ship
        Option.None => { println("nothing left") }
    }

    queue.truncate(1)
    println(queue.length)          // 1
    return 0
}

An insert or removeAt index outside the Vec stops the program with a message naming the index and the length. A removal that did nothing would leave you holding a copy of an element you did not take.

Copy

MethodGives
v.clone()a new Vec with a copy of every element; requires T: Copy
v.filter(keep)a new Vec of the elements keep accepts; requires T: Copy

Removing an element another name still reads

v[i] is a view of the element, not a copy, and nothing stops the Vec losing that element while the view is still in use:

prismio
import std.io
import std.vec

fn main() -> Int {
    let words: Vec<String> = ["a long enough string to own its own memory", "b"]
    let first = words[0]
    words.clear()
    println(first)       // still the string
    return 0
}

This is safe. A removed element that owns memory is released with the Vec, not at the removal, which releases exactly what the Vec would have released had the element stayed. So a removal can never free something a view still reads. Scalars and structs with no owned fields own nothing, so removing them holds nothing back.

The cost is that a long-lived Vec which keeps removing Strings keeps their memory until the Vec itself is released. Releasing at the removal when the compiler can prove no view exists is planned; see COLLECTIONS.md in the compiler repository.

Ownership

The Vec owns its elements and is itself move-only. Pushing moves the element in, so the pushed name cannot be used again:

prismio
import std.vec

struct Item { id: Int }

fn main() -> Int {
    let items: Vec<Item> = []
    let item = Item { id: 7 }
    items.push(item)
    return item.id
}

A Vec<T> parameter borrows the Vec, so the caller still owns it afterwards. inout borrows it mutably, and sink hands the whole Vec to the callee.

prismio
import std.vec

fn total(values: Vec<Int>) -> Int {
    let mut sum = 0
    for v in values {
        sum = sum + v
    }
    return sum
}

fn main() -> Int {
    let values: Vec<Int> = [1, 2, 3]
    return total(values) - 6
}

Storage follows the element type. Scalars, Strings and structs with no pointer-bearing fields live directly in the Vec's block; everything else is stored as one pointer per element. That choice is made when the program is compiled and changes nothing about how the Vec is used, and neither layout is a stable foreign ABI.

Sorting

Sorting needs an order, so it needs T: Ord — from std.ord, which covers every integer type, Char, Float and String.

prismio
import std.io
import std.vec

fn main() -> Int {
    let xs: Vec<Int> = [5, 1, 4]
    xs.sort()
    println(xs.first)              // 1
    println(xs.binarySearch(4))    // 1
    return 0
}

sort is pattern-defeating quicksort (pdqsort) and is not stable. Ranges under 24 elements go to insertion sort. The pivot is a median of three, or of nine above 128 elements. A range that keeps splitting badly is finished by heapsort, so the worst case is O(n log n) and recursion stays O(log n) deep whatever the input.

Ordered and repetitive input are fast, not just correct. A Vec already in order costs about one pass, and 10 000 equal values sort in linear time.

A struct sorts once it has an impl Ord:

prismio
import std.io
import std.vec
import std.ord

struct Version {
    major: Int,
    minor: Int
}

impl Ord for Version {
    fn cmp(self, other: Self) -> Int {
        if (self.major != other.major) { return cmp(self.major, other.major) }
        return cmp(self.minor, other.minor)
    }
}

fn main() -> Int {
    let older = Version { major: 1, minor: 9 }
    let newer = Version { major: 2, minor: 0 }
    println(cmp(older, newer))
    return 0
}

Float's order is not total. NaN compares as neither less nor greater, so cmp answers 0 for it. A sort over data containing NaN has an unspecified order rather than a corrupted one.

Higher-order

With closures:

MethodDoes
v.sortBy(order)sorts under a comparator answering negative, zero or positive; no Ord needed
v.filter(keep)a new Vec of the elements keep accepts; requires T: Copy
v.mapInto(out, f)appends f(x) for every element to out
v.countWhere(keep) / v.anyOf(keep) / v.allOf(keep)predicate queries
prismio
import std.io
import std.vec

fn main() -> Int {
    let xs: Vec<Int> = [3, 9, 1]
    xs.sortBy(|a: Int, b: Int| b - a)
    println(xs.first)                              // 9

    let big = xs.filter(|v: Int| v > 2)
    println(big.length)                            // 2
    println(xs.countWhere(|v: Int| v % 2 == 1))    // 3
    return 0
}

mapInto takes its destination rather than returning one. A map that returned a Vec<U> would have to learn U from the closure's return type, and Prismio solves a type parameter from argument types only. With the destination as an argument, U is solvable.

The runtime layer

The list_* functions (list_new, list_push, list_len, …) are what the methods compile to, as the str_* functions are under String. They still work, and they are not the interface this page describes.