182. The cost model in Rust
The planner is one Rust function with a ladder of early returns, an Option for the size, and a list of reasons in plain English. Reading it closely turned up a sentence the Rust engine worded differently from the reference, in a case no fixture covered.
- Walk plan_one in plan.rs rung by rung, and say what each early return records.
- Explain how a measured size, an estimate, an upper bound and an unknown size reach the threshold.
- Describe a parity bug the threshold message had, and why the fixtures missed it.
- Implement the bottom of the ladder, reasons included, with tests.
Same collection, three verdicts
This program pushes inside an if:
const ps = [];
for (let i = 0; i < 100; i++)
if (i % 4 === 0) ps.push({ x: i });
for (let i = 0; i < ps.length; i++)
ps[i].x += 1;
Here is what the Rust CLI decides, by default, with a higher threshold,
and with a profile from viscosity profile (rustc 1.97 release build;
first two lines of each, wrapped):
$ viscosity explain l1.js
ps (line 1) {x} → struct of arrays
· loop at line 4 in <module>() …
$ viscosity explain l1.js --min-size 200
ps (line 1) {x} → kept as array of objects
· at most 100 elements: below the
200-element threshold, where …
$ viscosity explain l1.js --profile p.json
ps (line 1) {x} → kept as array of objects
· about 25 elements (measured): below
the 64-element threshold, where …
Chapter 101 built this ladder in TypeScript. In Rust it is one function,
plan_one in crates/core/src/plan.rs, and the details of how it is
written decide every word of those lines.
A loan officer works down a checklist. The first box that fails ends the interview, and the reason goes on the letter. An applicant who passes every box gets the loan, with a note of what was checked.
From continue to return
The TypeScript plan() is one loop over collections, and each refusal
is out.push(keep(...)); continue;. The port moves the body into its own
function, so each refusal is an early return:
fn plan_one(
a: &Analysis,
c: &Collection,
opts: &PlanOptions,
) -> CollectionPlan {
let keep = |reasons: Vec<String>| {
CollectionPlan {
collection: c.clone(),
layout: Layout::Aos,
block_size: 0,
reasons,
}
};
if !c.bailouts.is_empty() {
return keep(c.bailouts.iter()
.map(|b| format!("unsafe: {b}"))
.collect());
}
// ... the other rungs ...
}
keep is a closure that borrows c and builds the plan for “leave it
alone”. Every rung below is the same shape: test, then return keep(...)
with the reason. A collection with bailouts gets exactly one reason per
bailout. explain --frames relies on that: it pairs the k-th reason with
the k-th bailout’s source position.
The annotation rungs come next. Two refusals look inside an
aosoa(N) annotation (chapter 184 explains them):
let b = ann.block_size.unwrap_or(8);
if !(b >= 2 && b.is_power_of_two()) {
return keep(vec![format!(
"annotated aosoa({}), but …",
js_number(b as f64))]);
}
is_power_of_two is true exactly when the number is 2 to some power[2],
which includes 1, so b >= 2 rules that out separately. Then come opt-in
mode, Python without --python-auto, no numeric field, and no loop. Each
is three lines, and each reason string is meant to match the
reference’s word for word.
The size rung
Now the only rung with arithmetic. Step through it on the program above, under all three option sets:
if !c.bailouts.is_empty() {return keep(unsafe_reasons);}if let Some(ann) = ann { /* … */ }if opts.mode == Mode::OptIn {return keep("opt-in mode: …");}if py && !opts.python_auto { /* … */ }if c.numeric_fields.is_empty() { … }if !c.loops.iter().any(|l| l.depth > 0) {return keep("no loop reads …");}let measured = opts.profile.as_ref().and_then(|p| p.get(&c.name)).copied();let size = measured.or(c.size_estimate);if let Some(size) = size {if size < opts.min_size {return keep(below(size));}}soa(describe(c, size, measured))
The code, adapted (the long message is shortened):
let measured = opts.profile.as_ref()
.and_then(|p| p.get(&c.name))
.copied();
let size = measured.or(c.size_estimate);
if let Some(size) = size {
if size < opts.min_size {
let word = if measured.is_none()
&& c.size_is_upper_bound
{ "at most" } else { "about" };
let m = if measured.is_some()
{ " (measured)" } else { "" };
return keep(vec![format!(
"{word} {} elements{m}: …",
js_number(size))]);
}
}
Option::or keeps the first value if there is one[1],
so a measurement always wins over the estimate. if let Some(size) makes
the unknown case explicit: no size, no refusal. The planner bets that a
collection it can’t count may be large. A maximum is compared as if it
were the size, so “at most 100” passes a threshold of 64 even though the
program holds 25. A maximum below the threshold is a safe refusal,
since the real size can only be smaller.
The comparison is plain f64 <. That has one odd consequence, in
both engines: a min_size of NaN makes every comparison false, so
the threshold silently disappears. Both CLIs once let --min-size abc
through as Number("abc"), which is NaN. Now both refuse it with
--min-size must be a non-negative number; only a program calling the
library can still pass NaN. Try it:
// The size rung of plan_one, alone:
// true means "keep: too small".
fn too_small(
measured: Option<f64>,
estimate: Option<f64>,
min: f64,
) -> bool {
match measured.or(estimate) {
Some(s) => s < min,
// Unknown: it may be big.
None => false,
}
}
fn main() {
let cases = [
(None, Some(100.0), 64.0),
(None, Some(100.0), 200.0),
(Some(25.0), Some(100.0), 64.0),
(None, None, 64.0),
// NaN: the CLIs now refuse it
(None, Some(2.0), f64::NAN),
];
for (m, e, min) in cases {
let k = too_small(m, e, min);
let v = if k { "keep" } else {
"soa"
};
println!(
"{m:?} {e:?} {min}: {v}"
);
}
}
The cost model in Rust is plan_one in plan.rs: a ladder of
early returns. Safety first (one unsafe: reason per bailout), then
annotations, opt-in mode and Python, then the cost rules: some field
numeric, some loop touching a field, and a size not below
min_size. The size is the measured one if a profile has it, else
the estimate (Option::or). An unknown size passes. A collection
that passes every rung becomes SoA, and describe lists its reasons.
A sentence the fixtures never saw
When I first wrote this chapter, the profiled line in Rust read
about 25 (measured) elements: below. The format string was
"{} {}{} elements: below …", so the third placeholder,
” (measured)”, landed before the word “elements”. The TypeScript
reference writes ${size} elements${...}: same decision, different
text. Why did nothing catch it? The 186 parity fixtures then compiled
every program with default options and with --bounds assume, never
with --profile, and the fuzzer passed no profile either. Parity holds
only for what is tested. The fix moved the placeholder and added the
missing tests: four profile fixtures beside today’s 190 parity pairs,
and random profiles in the fuzzer.
The options are typed. Mode and Bounds are enums with
#[derive(Default)] and a #[default] variant, and PlanOptions
implements Default by hand: auto mode, min_size: 64.0, bounds
checks on, no annotations, no profile, no python_auto. Where the
reference’s compile() ignores an option passed as undefined, Rust
has no such value: the README lists that as one of the few things the
port doesn’t reproduce.
Your turn
Write plan, a cut-down plan_one, in this order. Bailouts: Aos, with one reason unsafe: {b} per bailout. No numeric field: Aos, "no numeric field". No loop: Aos, "no loop". Then the size is the measured one if there is one, else the estimate. The word is "at most" only when nothing was measured and the estimate is an upper bound, else "about". If the size is known and below min_size: Aos, "{word} {s} elements: below {min}", with (measured) right after elements when measured (the reference’s order). Otherwise Soa, with one reason: "size unknown", or "{word} {s} elements", plus (measured by profiling) when measured.
#[derive(Debug, PartialEq)]
enum Layout {
Aos,
Soa,
}
struct Coll {
bailouts: Vec<String>,
// fields proven numeric
numeric: usize,
// loops of depth > 0 touching it
loops: usize,
estimate: Option<f64>,
// the estimate is a maximum
upper: bool,
}
struct Opts {
min_size: f64,
// from a profile, if any
measured: Option<f64>,
}
const M: &str = " (measured)";
const P: &str =
" (measured by profiling)";
fn plan(
c: &Coll,
o: &Opts,
) -> (Layout, Vec<String>) {
todo!()
}What you learned
plan_oneis a ladder of early returns, each through akeepclosure that records the reason.- One reason per bailout lets
--framespair reasons with positions. - The size is
measured.or(estimate). An unknown size passes, a maximum is treated as the size, andNaNdisables the threshold. - Byte parity covers what the fixtures exercise. The threshold message with a profile differs between the engines.
Check your understanding
0/6- 1.
A collection has a bailout and a
// @viscosity soaannotation. What doesplan_onedo? - 2.
// @viscosity aosoa(1)on a numeric collection. What does explain say? - 3.
A collection’s size is unknown statically, and there is no profile. Which is true?
- 4.
The estimate is “at most 100”, a profile measured 25, and
min_sizeis 64. What is the outcome? - 5.
What does
--min-size abcdo today? - 6.
The Rust and TypeScript engines once worded the measured threshold message differently. Why did 186/186 fixtures still pass?
References
- [1] The Rust Project Developers. Option::or (Rust standard library). Rust standard library; source in library/core/src/option.rs of github.com/rust-lang/rust. doc.rust-lang.org/std/option/enum.Option.html#method.or (checked 2026-10-31)
- [2] The Rust Project Developers. is_power_of_two and trailing_zeros on unsigned integers (Rust standard library). Rust standard library; source in library/core/src/num/uint_macros.rs of github.com/rust-lang/rust. doc.rust-lang.org/std/primitive.u64.html#method.is_power_of_two (checked 2026-10-31)