Viscosity
Review
Part IX · Rust for TypeScript developers · Day 33

130. Vec and HashMap

The two collections that hold almost everything in drip-rs and the Rust port of Viscosity: a growable array that owns its elements, and a hash table that owns its keys. The borrow checker finally pays rent here.

About 17 minutes, including the exercises and quiz
By the end you will be able to
  • Create, grow, index and loop over a Vec, and say what its length, capacity and heap buffer are.
  • Explain why the compiler rejects a push while a reference into the Vec is alive.
  • Count things with HashMap, using get and the entry API, and say why its order can't be relied on.
  • Compare std's HashMap with drip-rs's own hash table from chapter 51.

A column that grows

In chapter 105 you taught Viscosity to grow a Float64Array column by hand: keep a length and a capacity, and when they meet, allocate a bigger array and copy. In Rust that bookkeeping comes built in:

let mut xs: Vec<f64> = Vec::new();
xs.push(1.5);
xs.push(2.5);
println!("{} {}", xs.len(), xs[0]);
// 2 1.5

Vec<f64> reads “a vector of f64”. Unlike a JavaScript array, every element has the same type, so the elements sit side by side in memory with no boxes: a Vec<f64> is laid out like a Float64Array that can grow. vec![1.5, 2.5] is shorthand for building one with those elements.

Intuition

A Vec is a ticket stub for a row of seats in a theatre. The stub (on the stack) says where the row starts, how many seats are taken and how many are reserved. The seats themselves are in the hall (the heap). When your group outgrows the row, the usher finds a longer row elsewhere and moves everyone. Your stub gets updated; anyone holding the old seat numbers is now pointing at empty chairs.

Length, capacity, buffer

The standard library documents the picture exactly: a Vec value is a pointer to a heap buffer, a length and a capacity, and the buffer is one contiguous block[2]. Step through some pushes:

Step throughA Vec grows
let mut v: Vec<f64> = Vec::new();
A Vec's pointer, length and capacity on the stack, and its buffer on the heapstack: vptr–len0cap0
An empty Vec allocates nothing yet. The value on the stack is three words: a pointer, a length (0) and a capacity (0).
1/6

The capacities in that diagram are real. On this machine, with Rust 1.97, pushing nine f64s one at a time printed capacities 4, 4, 4, 4, 8, 8, 8, 8, 16. Don’t build on those numbers: the docs say Vec guarantees no particular growth strategy, only that push is amortized O(1), which is the doubling argument from chapter 105. What it does guarantee is that Vec::with_capacity(n) allocates room for exactly n, and that a Vec never shrinks by itself[2].

Definition

A Vec<T> is Rust’s growable array. It owns a contiguous heap buffer of Ts and records its length (elements in use) and capacity (room allocated). push appends, reallocating and moving the elements when the length would pass the capacity. When the Vec is dropped, its elements are dropped and the buffer is freed.

Indexing, safely or not

xs[5] on a two-element Vec doesn’t return undefined. It panics: index out of bounds: the len is 2 but the index is 5. If an index might be out of range, ask with get, which returns the Option from the last chapter: xs.get(5) is None, xs.get(0) is Some(&1.5).

Now the rule that surprises everyone once. This doesn’t compile:

let mut v = vec![1.0, 2.0];
let first = &v[0];
v.push(3.0);
println!("{}", first);

Rust 1.97 says (trimmed):

error[E0502]: cannot borrow `v` as
  mutable because it is also borrowed
  as immutable
3 |     let first = &v[0];
  |                  - immutable borrow
4 |     v.push(3.0);
  |     ^^^^^^^^^^^ mutable borrow
5 |     println!("{}", first);
  |              immutable borrow later

It is the shared-or-mutable rule from chapter 125, and the diagram shows why it matters here. If this push hit the capacity, the elements would move to a new buffer and first would point into freed memory. JavaScript never hands you a pointer into an array’s storage, so it can’t have this bug. Rust can hand you one, so it checks.

Looping uses borrows too. for x in &v lends you each element; for x in &mut v lends each one mutably, so *x += 1.0 changes it in place; for x in v moves the elements out and uses the Vec up. A function that only reads should take a slice, &[f64]: a &Vec<f64> turns into one automatically.

Try itA Vec grows, and lends its elements
fn sum(xs: &[f64]) -> f64 {
    let mut s = 0.0;
    for x in xs {
        s += x;
    }
    s
}

fn main() {
    let mut v: Vec<f64> = Vec::new();
    for i in 0..6 {
        v.push(i as f64);
        println!("len {} cap {}",
            v.len(), v.capacity());
    }
    for x in &mut v {
        *x *= 10.0;
    }
    println!("{:?}", v);
    println!("{:?}", v.get(9));
    println!("{}", sum(&v));
}
Open in Rust Playground ↗

HashMap: counting with keys

HashMap<K, V> maps keys of type K to values of type V. It isn’t imported automatically, so you start with use std::collections::HashMap;. Here is a count of which fields a loop touches, the kind of fact Viscosity’s analysis collects:

let used = ["x", "vx", "x", "y", "x"];
let mut n: HashMap<&str, i32> =
    HashMap::new();
for f in used {
    *n.entry(f).or_insert(0) += 1;
}
println!("{:?}", n.get("x"));
// Some(3)

get returns Option<&V>, since the key may be missing. insert(k, v) adds or replaces. The entry line is the counting idiom: entry(f) finds the slot for f, or_insert(0) fills it with 0 if it’s empty, and either way hands back a mutable reference to the value, which *... += 1 increments. And ownership applies: inserting a String key moves it into the map, which owns it from then on.

Try itCounting field uses
use std::collections::HashMap;

fn main() {
    let fs = ["x", "vx", "x", "y", "x"];
    let mut n: HashMap<&str, i32> =
        HashMap::new();
    for f in fs {
        *n.entry(f).or_insert(0) += 1;
    }
    println!("{:?}", n.get("x"));
    println!("{:?}", n.get("mass"));
    n.insert("mass", 0);
    println!("{}", n.len());
    // Run it twice: the order changes.
    println!("{:?}", n);
}
Open in Rust Playground ↗
Definition

A HashMap<K, V> is Rust’s hash table. It owns its keys and values. insert adds or overwrites, get returns Option<&V>, and entry(k).or_insert(v) gives a mutable reference to the value for k, inserting v first if there was none. Its iteration order is arbitrary and can change from run to run.

That last sentence is not a technicality. Running the example four times in a row and printing the whole map gave three different orders, among them {"vx": 1, "y": 1, "x": 3} and {"y": 1, "vx": 1, "x": 3}. A JavaScript Map iterates in insertion order[4]. The Rust port of Viscosity must make the same decisions as the TypeScript one, message for message, so where order matters it uses IndexMap from the indexmap crate, a hash map that remembers insertion order. One comment in engine/rust/crates/core/src/builder.rs explains why: the incomplete phis of SSA construction are filled in insertion order, which “decides value numbering and the order of instructions, exactly as in the TS reference”.

Under the hood

std’s HashMap is a Rust port of Google’s SwissTable, using quadratic probing and SIMD lookup, and by default hashes keys with SipHash 1-3, randomly seeded, to resist attackers who choose keys that collide[3]. drip-rs doesn’t use it for Drip’s globals and fields. Its table.rs is chapter 51’s table ported: linear probing over a power-of-two capacity, tombstones, and storage as a struct of arrays, keys: Vec<Option<StrRef>> beside values: Vec<Value>. Keys are interned strings whose handle carries the hash, so the table never runs a hash function at all. And Vec is everywhere in drip-rs: the value stack is a Vec<Value> created with Vec::with_capacity, and each kind of heap object lives in its own Vec<Option<T>>, addressed by index, with mark bits in a Vec<bool>. Chapter 138 turns that into a pattern.

Your turn

ExerciseCount field accesses

Viscosity’s analysis records which fields a loop reads. Write count_fields(used), which takes the field names in the order they were read and returns a HashMap<String, usize> from each name to how many times it was read.

use std::collections::HashMap;

fn count_fields(used: &[&str])
    -> HashMap<String, usize> {
    todo!()
}
Open in Rust Playground ↗
Extra practiceThe hot fields, in a fixed order

Write hot_fields(n, min), which returns the names whose count is at least min, sorted alphabetically so the answer doesn’t depend on the map’s order. Loop with for (k, c) in n, push into a Vec<String>, then call sort() on it.

use std::collections::HashMap;

fn hot_fields(
    n: &HashMap<String, usize>,
    min: usize,
) -> Vec<String> {
    todo!()
}
Open in Rust Playground ↗

What you learned

  • A Vec<T> owns a contiguous heap buffer of Ts, with a length and a capacity. push grows it; only amortized O(1) growth is promised.
  • v[i] panics when out of range; v.get(i) returns an Option.
  • You can’t push while a reference into the Vec is alive, because the buffer might move.
  • A HashMap<K, V> owns its keys and values; get returns Option<&V>, and entry(k).or_insert(v) is the counting idiom.
  • Its iteration order is arbitrary. The Rust Viscosity uses IndexMap where order reaches the output; drip-rs keeps its own SoA hash table.

Check your understanding

0/6
  1. 1.

    You push a fifth f64 into a Vec whose length and capacity are both 4. What happens?

  2. 2.

    What does the standard library promise about how much a Vec grows?

  3. 3.

    Which code does the borrow checker reject?

  4. 4.

    What is m.get("x") for a HashMap<String, usize> that holds "x" with count 2?

  5. 5.

    You print a HashMap in two runs of the same program and get different orders. Why?

  6. 6.

    How does drip-rs’s Table differ from std’s HashMap?

References

  1. [1] Steve Klabnik, Carol Nichols, Chris Krycho (2026). The Rust Programming Language (3rd edition). No Starch Press; free online at doc.rust-lang.org/book. doc.rust-lang.org/book/ (checked 2026-10-31)
  2. [2] The Rust Project Developers. Struct std::vec::Vec (Rust standard library documentation). Rust standard library API documentation; source in library/alloc/src/vec/mod.rs of github.com/rust-lang/rust. doc.rust-lang.org/std/vec/struct.Vec.html (checked 2026-10-31)
  3. [3] The Rust Project Developers. Struct std::collections::HashMap (Rust standard library documentation). Rust standard library API documentation; source in library/std/src/collections/hash/map.rs of github.com/rust-lang/rust. doc.rust-lang.org/std/collections/struct.HashMap.html (checked 2026-10-31)
  4. [4] MDN contributors. Map. MDN Web Docs. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map (checked 2026-10-31)