← In Class CS 326 · Week 04 · Code and output Space/j/k sections · click to pin · n/p example · i edit · l copy link

Week 04 · Collections, Traits, Errors, and Bytes

Every program in examples/src/bin/ beside the output it printed, and every file in examples/broken/ beside what rustc said about it. Each colored, numbered section of code sits in the same row as the output it produced. Click an output line to light up the println! that printed it, or click a println! to find its output. Click a section's number to copy a link straight to that section, to paste where students will follow it. Press i on a program to edit it and run it. On a phone the two columns do not fit, so a Code/Output switch shows one at a time.

The programs

ProgramThe one ideaThe line to point atSections
01array_slice_vecone function, three containerscount_free(&arr) and count_free(&v): the same &6
02iter_mut_and_enumerate*c = None writes into the arraythe *6
03slot_searchan index, or nothing — never −1position(..) returning Option<usize>6
04adapters_and_closuresthree words, three types.flatten().copied().collect::<Vec<u16>>()9
05trait_required_defaultthe default was never written by any implementerputs has a body in the trait; no impl mentions it6
06generics_and_boundsone body, three namestype_name::<U>() printed from inside the generic6
07static_vs_dyna fat pointer is two wordssize_of::<&dyn Uart>() = 167
08option_vs_resultabsence is a fact, failure is a decisionfirst_free(free).ok_or(AllocError::OutOfFrames)6
09error_enum_and_question_markeach ? is a different exitthe three ?s in load4
10errno_boundarythe collapse happens in exactly one placethe match in sys_read6
11bytes_vs_stringsa string is bytes plus a checked promisestr::from_utf8(..) returns a Result7
12argv_and_write_allthe slice is re-pointed, not copiedbuf = &buf[n..]6
13dispatch_at_run_timewho names the type: the call site, or the inputfn open(&mut self, k: Kind) -> &mut dyn Uart6
14generic_struct_and_guardone definition, a type for every Timpl<T> Lock<T> beside impl<T: Uart> Lock<T>6

Programs that must not compile

ErrorWhat rustc saysThe fix
E0038the trait Sink is not dyn compatiblewhere Self: Sized, or take a slice, or go generic
E0046not all trait items implemented, missing: putsupply put; delete the override
E0107missing generics for struct FdTablename it, go generic too, &'a mut dyn Sink, or a tag
E0277the ? operator can only be used in a function that returns Result or Option (or another type that implements FromResidual)match at the boundary; ? only below it
E0308mismatched types expected Result<Device, DevError>, found Option<Device>.ok_or(e), or the two-arm match
E0392type parameter T is never usedhold one in a field, PhantomData<T>, or drop the parameter
E0506cannot assign to table[_] because it is borrowediter_mut and *slot = …, or an index loop
E0596cannot borrow *table as mutable, as it is behind a & referencechange the parameter, not the loop
E0599no method named put found for mutable reference &mut S in the current scopeadd S: Sink

Keys

Space j ↓ →next section (dims the rest)
Shift+Space k ↑ ←previous section
0–9jump to that section
clickpin an output line and the code that printed it (click again to unpin)
click a numbercopy a link to that section, or to the whole example from the number beside its title
lcopy a link to the pinned line, else the section in focus, else this example
Escunpin, then leave section focus, and drop it from the link so a reload does not bring it back
n pnext or previous example
rnext run, for programs run more than once
eopen or close the explanation (broken files) or the build warnings
+ -larger or smaller text (remembered)
iedit this program and run it
⌘/Ctrl+↵run what you have edited
ccompare: what you changed in the code, and what changed in the output (press Esc first if you are typing)
oon a narrow screen, switch between the code and the output column

01 One function, three containers.

one function, three containers · point at count_free(&arr) and count_free(&v): the same &

$ cargo run --bin 01_array_slice_vec
//! 01 — One function, three containers.▸ 17 more lines
//!
//! Last week ended on this picture. This week starts by using it: the same
//! `count_free` walks an array, a slice of that array, and a Vec, and the
//! reason is the `&` in front of each argument.
//!
//! [Option<u32>; 8] the table itself: 64 bytes, no header, no pointer
//! &[Option<u32>] a borrowed VIEW: pointer + length, 16 bytes
//! Vec<Option<u32>> a heap buffer: pointer + capacity + length, 24 bytes
//!
//! array |slot|slot|slot|slot|slot|slot|slot|slot| lives where it was declared
//! slice [ptr -----> into somebody's table ][len] never owns anything
//! vec [ptr -----> the heap ][cap][len] grows, and asks an allocator
//!
//! `&arr`, `&arr[..4]`, and `&v` all become a `&[Option<u32>]` at the call.
//! That coercion is why one signature serves every kind of table.
//!
//! Run: cargo run --bin 01_array_slice_vec

use std::mem::size_of;

/// The most open files one process may hold. xv6 says 16; 8 fits on a slide.
pub const NOFILE: usize = 8;

/// How many slots are free. Takes a SLICE, so it takes anything.
fn count_free(table: &[Option<u32>]) -> usize {
table.iter().filter(|slot| slot.is_none()).count()
}

fn main() {
1 println!("== three sizes ==");
println!("size_of::<[Option<u32>; {NOFILE}]>() = {} the table: 8 x 8 bytes",
size_of::<[Option<u32>; NOFILE]>());
println!("size_of::<&[Option<u32>]>() = {} a slice: pointer + length",
size_of::<&[Option<u32>]>());
println!("size_of::<Vec<Option<u32>>>() = {} a Vec: pointer + capacity + length",
size_of::<Vec<Option<u32>>>());
println!("the array IS the storage; the other two point at storage");

1== three sizes ==
size_of::<[Option<u32>; 8]>() = 64 the table: 8 x 8 bytes
size_of::<&[Option<u32>]>() = 16 a slice: pointer + length
size_of::<Vec<Option<u32>>>() = 24 a Vec: pointer + capacity + length
the array IS the storage; the other two point at storage
2 println!("\n== the array: fixed, and exactly where it was declared ==");
let mut arr: [Option<u32>; NOFILE] = [None; NOFILE];
arr[0] = Some(1); // stdin
arr[1] = Some(1); // stdout
arr[2] = Some(1); // stderr -- all three the console
println!("arr = {arr:?}");
println!("count_free(&arr) = {}", count_free(&arr));
println!("arr.len() = {} fixed by the type, known at compile time",
arr.len());

2== the array: fixed, and exactly where it was declared ==
arr = [Some(1), Some(1), Some(1), None, None, None, None, None]
count_free(&arr) = 5
arr.len() = 8 fixed by the type, known at compile time
3 println!("\n== a slice: a view into the same bytes ==");
let first_half = &arr[..4];
println!("&arr[..4] = {first_half:?}");
println!("count_free(first_half) = {}", count_free(first_half));
println!("the slice starts at {:p}; the array starts at {:p} -- same address",
first_half.as_ptr(), arr.as_ptr());
println!("nothing was copied. A slice is where to look and how far.");

3== a slice: a view into the same bytes ==
&arr[..4] = [Some(1), Some(1), Some(1), None]
count_free(first_half) = 1
the slice starts at 0x16f945ab0; the array starts at 0x16f945ab0 -- same address
nothing was copied. A slice is where to look and how far.
4 println!("\n== a Vec: a copy that lives on the heap and can grow ==");
let mut v = arr.to_vec();
println!("arr.to_vec() = {v:?}");
println!("count_free(&v) = {}", count_free(&v));
println!("v.as_ptr() = {:p} NOT the array's address: this is the heap",
v.as_ptr());
println!("v.capacity() = {} room for {}; the next push must reallocate",
v.capacity(), v.capacity() - v.len());
for _ in 0..3 {
v.push(None);
}
println!("after 3 pushes: len {} capacity {} it asked the allocator",
v.len(), v.capacity());
println!("arr.len() is still {}. An array cannot be pushed into.", arr.len());

4== a Vec: a copy that lives on the heap and can grow ==
arr.to_vec() = [Some(1), Some(1), Some(1), None, None, None, None, None]
count_free(&v) = 5
v.as_ptr() = 0x100da5c90 NOT the array's address: this is the heap
v.capacity() = 8 room for 0; the next push must reallocate
after 3 pushes: len 11 capacity 16 it asked the allocator
arr.len() is still 8. An array cannot be pushed into.
5 println!("\n== indexing is checked, and there is a checked spelling ==");
println!("arr.get(2) = {:?}", arr.get(2));
println!("arr.get(8) = {:?} one past the end, and no panic", arr.get(8));
println!("arr[8] would panic: `index out of bounds: the len is 8 but the index is 8`");
println!("the kernel never indexes with a number a user program chose --");
println!("it checks first, or it uses get()");

5== indexing is checked, and there is a checked spelling ==
arr.get(2) = Some(Some(1))
arr.get(8) = None one past the end, and no panic
arr[8] would panic: `index out of bounds: the len is 8 but the index is 8`
the kernel never indexes with a number a user program chose --
it checks first, or it uses get()
6 println!("\n== the one idea ==");
println!("count_free(&arr), count_free(&arr[..4]), count_free(&v):");
println!("the SAME function, because each argument became a &[Option<u32>]");
println!("write functions over slices, and every kind of table can call them");
6== the one idea ==
count_free(&arr), count_free(&arr[..4]), count_free(&v):
the SAME function, because each argument became a &[Option<u32>]
write functions over slices, and every kind of table can call them
}
exit status 0

02 iter() lends, iter_mut() lends exclusively.

*c = None writes into the array · point at the *

$ cargo run --bin 02_iter_mut_and_enumerate
//! 02 — `iter()` lends, `iter_mut()` lends exclusively.▸ 14 more lines
//!
//! xv6's `wakeup(chan)` walks the whole process table and moves every process
//! sleeping on `chan` to runnable. That loop has to WRITE into the table, and
//! it has to know WHICH slot it is writing. Two things, two tools:
//!
//! for c in chan.iter() c: &Option<usize> read only
//! for c in chan.iter_mut() c: &mut Option<usize> one exclusive borrow at a time
//! .enumerate() (index, item) the slot number comes along
//!
//! The write is `*c = None`. The star is the whole lesson: `c` is a reference
//! INTO the array, and assigning through it changes the array. Without the
//! star you would be trying to overwrite the reference itself.
//!
//! Run: cargo run --bin 02_iter_mut_and_enumerate

/// Which channel each slot is sleeping on, or `None` if it is not asleep.
/// (In the kernel the channel is an address: the lock, the buffer, the pipe.)
const NPROC: usize = 8;

fn main() {
let mut chan: [Option<usize>; NPROC] =
[None, Some(0x10), None, Some(0x20), Some(0x10), None, None, Some(0x10)];
let target = 0x10;

1 println!("== the table ==");
show(&chan);

1== the table ==
[0] -
[1] sleeping on 0x10
[2] -
[3] sleeping on 0x20
[4] sleeping on 0x10
[5] -
[6] -
[7] sleeping on 0x10
2 println!("\n== iter(): each item is a &Option<usize>; the table is untouched ==");
let mut sleepers = 0;
for c in chan.iter() {
if *c == Some(target) {
sleepers += 1;
}
}
println!("{sleepers} slots sleep on {target:#x}");
println!("`c` is a &Option<usize>, so the comparison needs `*c` -- or a & on the other side: `c == &Some(target)`");

2== iter(): each item is a &Option<usize>; the table is untouched ==
3 slots sleep on 0x10
`c` is a &Option<usize>, so the comparison needs `*c` -- or a & on the other side: `c == &Some(target)`
3 println!("\n== enumerate(): the index rides along ==");
let mut which = Vec::new();
for (i, c) in chan.iter().enumerate() {
if *c == Some(target) {
which.push(i);
}
}
println!("they are slots {which:?}");
println!("a kernel almost always needs the INDEX: it is the pid, the fd, the frame");

3== enumerate(): the index rides along ==
they are slots [1, 4, 7]
a kernel almost always needs the INDEX: it is the pid, the fd, the frame
4 println!("\n== iter_mut(): each item is a &mut Option<usize>; the write goes in ==");
for (i, c) in chan.iter_mut().enumerate() {
if *c == Some(target) {
*c = None; // <- the star: assign THROUGH the reference, into the array
println!(" woke slot {i}");
}
}
println!("after wakeup({target:#x}):");
show(&chan);
println!("`c` is a &mut Option<usize>. `*c = None` writes into `chan`.");
println!("`c = None` would not compile: c is a reference, not an Option.");

4== iter_mut(): each item is a &mut Option<usize>; the write goes in ==
woke slot 1
woke slot 4
woke slot 7
after wakeup(0x10):
[0] -
[1] -
[2] -
[3] sleeping on 0x20
[4] -
[5] -
[6] -
[7] -
`c` is a &mut Option<usize>. `*c = None` writes into `chan`.
`c = None` would not compile: c is a reference, not an Option.
5 println!("\n== what you may NOT do inside that loop ==");
println!(" for (i, c) in chan.iter().enumerate() {{");
println!(" if *c == Some(target) {{ chan[i] = None; }} // error[E0506]");
println!(" }}");
println!("`iter()` holds a shared borrow of the whole table for the whole loop;");
println!("`chan[i] = ..` writes into it while that borrow is alive.");
println!("RUN ./show-errors.sh e0506");

5== what you may NOT do inside that loop ==
for (i, c) in chan.iter().enumerate() {
if *c == Some(target) { chan[i] = None; } // error[E0506]
}
`iter()` holds a shared borrow of the whole table for the whole loop;
`chan[i] = ..` writes into it while that borrow is alive.
6 println!("\n== the off-by-three ==");
let tail = &chan[3..];
for (i, c) in tail.iter().enumerate() {
if c.is_some() {
println!(" tail slot {i} is asleep -- that is chan[{}], not chan[{i}]", i + 3);
}
}
println!("enumerate counts the thing you handed it, from 0. Slice first,");
println!("and the numbers are the slice's, not the table's.");
6== the off-by-three ==
tail slot 0 is asleep -- that is chan[3], not chan[0]
enumerate counts the thing you handed it, from 0. Slice first,
and the numbers are the slice's, not the table's.
}

fn show(chan: &[Option<usize>]) {
for (i, c) in chan.iter().enumerate() {
match c {
Some(ch) => println!(" [{i}] sleeping on {ch:#x}"),
None => println!(" [{i}] -"),
}
}
}
exit status 0

04 Adapters are a pipeline; every stage has a type.

three words, three types · point at .flatten().copied().collect::<Vec<u16>>()

$ cargo run --bin 04_adapters_and_closures
//! 04 — Adapters are a pipeline; every stage has a type.▸ 17 more lines
//!
//! The global file table keeps a reference count per open file. Questions the
//! kernel asks it -- how many are live, which slots, the total -- are all one
//! walk with a different question at each step. Iterator ADAPTERS let you
//! write the walk as a chain, and the thing to watch is the ITEM TYPE
//! changing at each link:
//!
//! refs.iter() &Option<u16> a reference into the table
//! .flatten() &u16 an Option is an iterator of 0 or 1 items
//! .copied() u16 dereference each one
//! .map(f) whatever f returns
//! .collect() Vec<_> the ONE adapter that allocates
//!
//! The closures in between capture their environment, which is what lets a
//! walk refer to a `page_size` or a counter declared outside it.
//!
//! Run: cargo run --bin 04_adapters_and_closures

const NFILE: usize = 8;

fn main() {
0 // How many descriptors point at each open file; None = slot unused.
let refs: [Option<u16>; NFILE] = [Some(3), None, Some(1), Some(2), None, None, Some(1), None];
println!("refs = {refs:?}");

0refs = [Some(3), None, Some(1), Some(2), None, None, Some(1), None]
1 println!("\n== filter + count ==");
let live = refs.iter().filter(|r| r.is_some()).count();
println!("refs.iter().filter(|r| r.is_some()).count() = {live}");

1== filter + count ==
refs.iter().filter(|r| r.is_some()).count() = 4
2 println!("\n== flatten: an Option is an iterator of zero or one ==");
let inner: Vec<&u16> = refs.iter().flatten().collect();
println!("refs.iter().flatten().collect::<Vec<&u16>>() = {inner:?}");
println!("None contributed nothing; each Some contributed its &u16");

2== flatten: an Option is an iterator of zero or one ==
refs.iter().flatten().collect::<Vec<&u16>>() = [3, 1, 2, 1]
None contributed nothing; each Some contributed its &u16
3 println!("\n== copied: &u16 becomes u16 ==");
let counts: Vec<u16> = refs.iter().flatten().copied().collect();
println!("refs.iter().flatten().copied().collect() = {counts:?}");
println!("the Vec now OWNS numbers instead of borrowing the table");

3== copied: &u16 becomes u16 ==
refs.iter().flatten().copied().collect() = [3, 1, 2, 1]
the Vec now OWNS numbers instead of borrowing the table
4 println!("\n== map with a captured value ==");
let page = 4096usize;
let bytes: Vec<usize> = refs.iter().flatten().map(|&r| r as usize * page).collect();
println!("map(|&r| r as usize * page) = {bytes:?}");
println!("the closure reached `page` from the enclosing scope -- that is a capture");

4== map with a captured value ==
map(|&r| r as usize * page) = [12288, 4096, 8192, 4096]
the closure reached `page` from the enclosing scope -- that is a capture
5 println!("\n== collect needs to know the container ==");
let a: Vec<u16> = refs.iter().flatten().copied().collect();
let b = refs.iter().flatten().copied().collect::<Vec<u16>>();
println!("let a: Vec<u16> = ...collect() -> {a:?}");
println!("...collect::<Vec<u16>>() -> {b:?}");
println!("with neither annotation: error[E0282]: type annotations needed");
println!("`collect` can build a Vec, a String, a HashMap -- it has to be told");

5== collect needs to know the container ==
let a: Vec<u16> = ...collect() -> [3, 1, 2, 1]
...collect::<Vec<u16>>() -> [3, 1, 2, 1]
with neither annotation: error[E0282]: type annotations needed
`collect` can build a Vec, a String, a HashMap -- it has to be told
6 println!("\n== sum ==");
println!("refs.iter().flatten().sum::<u16>() = {}", refs.iter().flatten().sum::<u16>());

6== sum ==
refs.iter().flatten().sum::<u16>() = 7
7 println!("\n== enumerate + filter_map: the indices of the live slots ==");
let slots: Vec<usize> = refs
.iter()
.enumerate()
.filter_map(|(i, r)| r.map(|_| i))
.collect();
println!("enumerate().filter_map(|(i, r)| r.map(|_| i)) = {slots:?}");
println!("`r.map(|_| i)` turns Some(count) into Some(index) and leaves None alone");

7== enumerate + filter_map: the indices of the live slots ==
enumerate().filter_map(|(i, r)| r.map(|_| i)) = [0, 2, 3, 6]
`r.map(|_| i)` turns Some(count) into Some(index) and leaves None alone
8 println!("\n== lazy: the closure runs only as far as needed ==");
let mut looked = 0;
let pair = refs
.iter()
.map(|r| {
looked += 1;
*r
})
.find(|r| *r == Some(2));
println!("first slot with refcount 2: {:?}", pair.flatten());
println!("the map closure ran {looked} times, not {NFILE}: find stopped it");

8== lazy: the closure runs only as far as needed ==
first slot with refcount 2: Some(2)
the map closure ran 4 times, not 8: find stopped it
9 println!("\n== the same thing as a loop ==");
let mut by_hand = Vec::new();
#[allow(clippy::manual_flatten)] // the loop IS the point here
for r in refs.iter() {
if let Some(n) = r {
by_hand.push(*n);
}
}
println!("for / if let / push -> {by_hand:?}");
println!("flatten / copied / collect -> {counts:?}");
println!("every chain is a loop you could have written. The chain has no");
println!("mutable temporaries and no index to get wrong -- and `collect` is");
println!("the one link that allocates, so it is the one the kernel leaves out.");
9== the same thing as a loop ==
for / if let / push -> [3, 1, 2, 1]
flatten / copied / collect -> [3, 1, 2, 1]
every chain is a loop you could have written. The chain has no
mutable temporaries and no index to get wrong -- and `collect` is
the one link that allocates, so it is the one the kernel leaves out.
}
exit status 0

05 A trait: one required method, and defaults written once.

the default was never written by any implementer · point at puts has a body in the trait; no impl mentions it

$ cargo run --bin 05_trait_required_default
//! 05 — A trait: one required method, and defaults written once.▸ 19 more lines
//!
//! The kernel writes bytes to a UART. The tests write the same bytes to a
//! buffer they can inspect. A trait is the name for what those two have in
//! common:
//!
//! trait Uart {
//! fn putc(&mut self, b: u8); REQUIRED: no body
//! fn puts(&mut self, s: &[u8]) { ... } DEFAULT: body in the trait
//! }
//!
//! Every implementer writes `putc`. Nobody writes `puts`; it was written once,
//! here, against a `self` whose type does not exist yet -- and it works for
//! every type that ever implements the trait, including the ones written next
//! year.
//!
//! impl Console methods of Console (last week's impl)
//! impl Uart for Console the SECOND kind of impl: Console promises Uart
//!
//! Run: cargo run --bin 05_trait_required_default

use std::fmt::Write as _; // for the last section; the `as _` imports only the methods
use std::io::Write;

/// The promise. Holds no data. You never build one of these.
pub trait Uart {
/// Send one byte. Every implementer must write this.
fn putc(&mut self, b: u8);

/// Send a run of bytes. Default: one `putc` per byte.
fn puts(&mut self, s: &[u8]) {
for &b in s {
self.putc(b);
}
}

/// Send bytes, then a newline. Default, built on the default.
fn putln(&mut self, s: &[u8]) {
self.puts(s);
self.putc(b'\n');
}
}

/// The real thing: bytes go to the terminal. A UNIT struct -- no fields,
/// zero bytes -- exists only so there is a type to hang the impl on.
pub struct Console;

impl Uart for Console {
fn putc(&mut self, b: u8) {
let _ = std::io::stdout().write_all(&[b]);
}
}

/// The test's version: keep every byte so the test can read it back.
pub struct Recorder {
pub buf: Vec<u8>,
}

impl Uart for Recorder {
fn putc(&mut self, b: u8) {
self.buf.push(b);
}
}

/// Measure, and throw the bytes away.
#[derive(Debug)] // <- this line is `impl Debug for Tally`, written by the compiler
pub struct Tally {
pub bytes: usize,
}

impl Uart for Tally {
fn putc(&mut self, _b: u8) {
self.bytes += 1;
}
}

/// core::fmt::Write has one required method with 07r's exact signature, plus
/// a Result. Implement it and `write!` works on your type.
impl std::fmt::Write for Recorder {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
self.buf.extend_from_slice(s.as_bytes());
Ok(())
}
}

/// An implementer with a faster way to do `puts`, so it overrides the default.
pub struct Fast {
pub bytes: usize,
pub calls: usize,
}

impl Uart for Fast {
fn putc(&mut self, _b: u8) {
self.bytes += 1;
self.calls += 1;
}
// Overriding is allowed. The contract -- "all the bytes arrive" -- is the
// same; only the cost changed: one call instead of s.len() calls.
fn puts(&mut self, s: &[u8]) {
self.bytes += s.len();
self.calls += 1;
}
}

fn main() {
1 println!("== three implementers, each wrote ONLY putc ==");
let mut console = Console;
let mut rec = Recorder { buf: Vec::new() };
let mut tally = Tally { bytes: 0 };

print!("Console.putln(b\"rv6 boot\") -> ");
console.putln(b"rv6 boot");
let _ = std::io::stdout().flush();
rec.putln(b"rv6 boot");
tally.putln(b"rv6 boot");
println!("Recorder.putln(b\"rv6 boot\") -> buf = {:?}", String::from_utf8_lossy(&rec.buf));
println!("Tally.putln(b\"rv6 boot\") -> bytes = {} 8 + the newline", tally.bytes);
println!("putln and puts have no impl block anywhere. They are the defaults.");

1== three implementers, each wrote ONLY putc ==
Console.putln(b"rv6 boot") -> rv6 boot
Recorder.putln(b"rv6 boot") -> buf = "rv6 boot\n"
Tally.putln(b"rv6 boot") -> bytes = 9 8 + the newline
putln and puts have no impl block anywhere. They are the defaults.
2 println!("\n== a default is written once, against a self that does not exist yet ==");
println!(" fn putln(&mut self, s: &[u8]) {{ self.puts(s); self.putc(b'\\n'); }}");
println!("when that line was compiled, Console/Recorder/Tally were not in scope.");
println!("it only assumed what the trait promises: that self has putc and puts.");

2== a default is written once, against a self that does not exist yet ==
fn putln(&mut self, s: &[u8]) { self.puts(s); self.putc(b'\n'); }
when that line was compiled, Console/Recorder/Tally were not in scope.
it only assumed what the trait promises: that self has putc and puts.
3 println!("\n== overriding a default ==");
let mut fast = Fast { bytes: 0, calls: 0 };
fast.putln(b"rv6 boot");
println!("Fast.putln(b\"rv6 boot\") -> bytes = {}, calls = {} (puts was one call)",
fast.bytes, fast.calls);
println!("Tally would have made 9 calls. Same bytes, same contract, cheaper.");

3== overriding a default ==
Fast.putln(b"rv6 boot") -> bytes = 9, calls = 2 (puts was one call)
Tally would have made 9 calls. Same bytes, same contract, cheaper.
4 println!("\n== a unit struct is zero bytes ==");
println!("size_of::<Console>() = {}", std::mem::size_of::<Console>());
println!("it exists to be a TYPE, so that `impl Uart for Console` has a home");

4== a unit struct is zero bytes ==
size_of::<Console>() = 0
it exists to be a TYPE, so that `impl Uart for Console` has a home
5 println!("\n== you have been using this since week 03 ==");
println!("#[derive(Debug)] on Tally is `impl Debug for Tally`: {tally:?}");
println!("`impl Uart for Recorder` is the same shape, written by hand");

5== you have been using this since week 03 ==
#[derive(Debug)] on Tally is `impl Debug for Tally`: Tally { bytes: 9 }
`impl Uart for Recorder` is the same shape, written by hand
6 println!("\n== and the standard library's version ==");
let mut log = Recorder { buf: Vec::new() };
let _ = write!(log, "pid {} exited {}", 3, -1);
println!("write!(log, \"pid {{}} exited {{}}\", 3, -1) -> {:?}",
String::from_utf8_lossy(&log.buf));
println!(" trait Write {{ fn write_str(&mut self, s: &str) -> fmt::Result; ... }}");
println!("07r's `Out::write_str` is this signature without the Result.");
println!("the kernel's println! is this impl on the UART. That is the whole trick.");
6== and the standard library's version ==
write!(log, "pid {} exited {}", 3, -1) -> "pid 3 exited -1"
trait Write { fn write_str(&mut self, s: &str) -> fmt::Result; ... }
07r's `Out::write_str` is this signature without the Result.
the kernel's println! is this impl on the UART. That is the whole trick.
}
exit status 0

06 Generics: one body, compiled once per type.

one body, three names · point at type_name::<U>() printed from inside the generic

$ cargo run --bin 06_generics_and_bounds
//! 06 — Generics: one body, compiled once per type.▸ 17 more lines
//!
//! A generic function is written against a type parameter, and the BOUND on
//! that parameter is everything the body may assume:
//!
//! fn banner<U: Uart>(u: &mut U) "any U at all, provided U: Uart"
//! fn banner<U>(u: &mut U) where U: Uart the same, spelled for long lists
//! fn banner(u: &mut impl Uart) the same again, shorthand
//!
//! Without the bound, `U` is SOME type and nothing more; calling `u.putc(..)`
//! is error[E0599] -- and rustc's help text names the fix.
//!
//! What the compiler emits is not one clever function. It is one COPY per
//! type the function is called with, each with the method calls resolved and
//! inlined. That is MONOMORPHIZATION, and the program below proves it by
//! printing the type name from inside the body.
//!
//! Run: cargo run --bin 06_generics_and_bounds

use std::any::type_name;

pub trait Uart {
fn putc(&mut self, b: u8);
fn puts(&mut self, s: &[u8]) {
for &b in s {
self.putc(b);
}
}
}

pub struct Console;
impl Uart for Console {
fn putc(&mut self, b: u8) {
print!("{}", b as char);
}
}

pub struct Recorder(pub Vec<u8>);
impl Uart for Recorder {
fn putc(&mut self, b: u8) {
self.0.push(b);
}
}

pub struct Tally(pub usize);
impl Uart for Tally {
fn putc(&mut self, _: u8) {
self.0 += 1;
}
}

/// Spelling 1: the bound inline.
fn banner<U: Uart>(u: &mut U) {
u.puts(b"rv6 ready\n");
println!(" [banner compiled for {}]", type_name::<U>());
}

/// Spelling 2: a `where` clause. Identical meaning; reads better when long.
fn banner_where<U>(u: &mut U)
where
U: Uart,
{
u.puts(b"rv6 ready\n");
println!(" [banner_where compiled for {}]", type_name::<U>());
}

/// Spelling 3: `impl Trait` in argument position. Still one copy per type;
/// you just cannot name the parameter.
fn banner_impl(u: &mut impl Uart) {
u.puts(b"rv6 ready\n");
println!(" [banner_impl compiled for some impl Uart]");
}

/// Two bounds joined with `+`: the body compares AND copies.
fn largest<T: PartialOrd + Copy>(xs: &[T]) -> Option<T> {
let mut best = *xs.first()?;
for &x in xs {
if x > best {
best = x;
}
}
Some(best)
}

/// `impl Trait` in RETURN position: "an iterator, whose type I am not naming".
/// This is how to return a list without a Vec -- nothing is allocated.
fn live(table: &[Option<u32>]) -> impl Iterator<Item = u32> + '_ {
table.iter().flatten().copied()
}

fn main() {
let mut console = Console;
let mut rec = Recorder(Vec::new());
let mut tally = Tally(0);

1 println!("== one body, three names ==");
banner(&mut console);
banner(&mut rec);
banner(&mut tally);
println!("three different type names printed by ONE function body.");
println!("there are three copies of `banner` in this binary, one per U.");

1== one body, three names ==
rv6 ready
[banner compiled for 06_generics_and_bounds::Console]
[banner compiled for 06_generics_and_bounds::Recorder]
[banner compiled for 06_generics_and_bounds::Tally]
three different type names printed by ONE function body.
there are three copies of `banner` in this binary, one per U.
2 println!("\n== three spellings, one meaning ==");
banner_where(&mut rec);
banner_impl(&mut rec);
println!("rec now holds {} bytes: every spelling really called Recorder::putc",
rec.0.len());

2== three spellings, one meaning ==
[banner_where compiled for 06_generics_and_bounds::Recorder]
[banner_impl compiled for some impl Uart]
rec now holds 30 bytes: every spelling really called Recorder::putc
3 println!("\n== what the bound buys ==");
println!(" fn shout<T>(v: T) {{ v.putc(1) }} // no bound");
println!(" error[E0599]: no method named `putc` found for type parameter `T`");
println!(" help: perhaps you need to restrict type parameter `T` with it: `T: Uart`");
println!("inside a generic body you may use exactly what the bound promises.");
println!("RUN ./show-errors.sh e0599");

3== what the bound buys ==
fn shout<T>(v: T) { v.putc(1) } // no bound
error[E0599]: no method named `putc` found for type parameter `T`
help: perhaps you need to restrict type parameter `T` with it: `T: Uart`
inside a generic body you may use exactly what the bound promises.
4 println!("\n== bounds join with + ==");
println!("largest(&[3u32, 9, 4]) = {:?}", largest(&[3u32, 9, 4]));
println!("largest(&[7u8, 2]) = {:?}", largest(&[7u8, 2]));
println!("largest::<u32> and largest::<u8> are two functions. `PartialOrd + Copy`");
println!("is the whole of what the body was allowed to do to a T.");

4== bounds join with + ==
largest(&[3u32, 9, 4]) = Some(9)
largest(&[7u8, 2]) = Some(7)
largest::<u32> and largest::<u8> are two functions. `PartialOrd + Copy`
is the whole of what the body was allowed to do to a T.
5 println!("\n== returning an iterator instead of a Vec ==");
let table = [Some(7), None, Some(9), Some(2), None];
print!("live(&table) yields:");
for pid in live(&table) {
print!(" {pid}");
}
println!();
println!("no Vec was built. Where a heap is not allowed, return an iterator.");

5== returning an iterator instead of a Vec ==
live(&table) yields: 7 9 2
no Vec was built. Where a heap is not allowed, return an iterator.
6 println!("\n== the price ==");
println!("one copy per type: fast calls, inlining, and a bigger binary.");
println!("program 07 makes the other trade.");
6== the price ==
one copy per type: fast calls, inlining, and a bigger binary.
program 07 makes the other trade.
}
exit status 0

07 dyn: one copy of the code, and a fat pointer to find the method.

a fat pointer is two words · point at size_of::<&dyn Uart>() = 16

$ cargo run --bin 07_static_vs_dyn
//! 07 — `dyn`: one copy of the code, and a fat pointer to find the method.▸ 19 more lines
//!
//! Program 06 compiled one `banner` per type. The alternative is ONE banner
//! that takes a `&mut dyn Uart` -- a reference that carries, alongside the
//! data pointer, a pointer to a table of that type's methods:
//!
//! &Console [ptr] 8 bytes: thin
//! &dyn Uart [ptr][vtable] 16 bytes: fat
//! &[u8] [ptr][len] 16 bytes: fat, the other way
//!
//! vtable for Console as Uart:
//! +--------+--------+--------+--------+--------+--------+
//! | drop | size | align | putc | puts | putln |
//! +--------+--------+--------+--------+--------+--------+
//!
//! A call through `dyn` is one load from the vtable and one indirect jump.
//! Nothing inlines across it. What it buys: a `&mut dyn Uart` is ONE type, so
//! three different implementers fit in the same array, struct field, or slot.
//!
//! Run: cargo run --bin 07_static_vs_dyn

use std::any::type_name;
use std::mem::size_of;

pub trait Uart {
fn putc(&mut self, b: u8);
fn puts(&mut self, s: &[u8]) {
for &b in s {
self.putc(b);
}
}
fn putln(&mut self, s: &[u8]) {
self.puts(s);
self.putc(b'\n');
}
}

pub struct Console;
impl Uart for Console {
fn putc(&mut self, b: u8) {
print!("{}", b as char);
}
}

pub struct Recorder(pub Vec<u8>);
impl Uart for Recorder {
fn putc(&mut self, b: u8) {
self.0.push(b);
}
}

pub struct Tally(pub usize);
impl Uart for Tally {
fn putc(&mut self, _: u8) {
self.0 += 1;
}
}

/// Static dispatch: one copy per U.
fn banner_generic<U: Uart>(u: &mut U) {
u.putln(b"rv6 ready");
println!(" [generic: compiled for {}]", type_name::<U>());
}

/// Dynamic dispatch: one copy, ever. The method is found at run time.
fn banner_dyn(u: &mut dyn Uart) {
u.putln(b"rv6 ready");
println!(" [dyn: this body knows only `dyn Uart`]");
}

fn main() {
1 println!("== two fat pointers, one thin ==");
println!("size_of::<&Console>() = {}", size_of::<&Console>());
println!("size_of::<&dyn Uart>() = {} pointer + vtable pointer", size_of::<&dyn Uart>());
println!("size_of::<&[u8]>() = {} pointer + length", size_of::<&[u8]>());
println!("a slice and a trait object are both `pointer + one more word`.");
println!("the slice's word is a length; the trait object's is a method table.");

let mut console = Console;
let mut rec = Recorder(Vec::new());
let mut tally = Tally(0);

1== two fat pointers, one thin ==
size_of::<&Console>() = 8
size_of::<&dyn Uart>() = 16 pointer + vtable pointer
size_of::<&[u8]>() = 16 pointer + length
a slice and a trait object are both `pointer + one more word`.
the slice's word is a length; the trait object's is a method table.
2 println!("\n== the same three, through one body ==");
banner_dyn(&mut console);
banner_dyn(&mut rec);
banner_dyn(&mut tally);
println!("one function. Three targets. The vtable chose putc each time.");

2== the same three, through one body ==
rv6 ready
[dyn: this body knows only `dyn Uart`]
[dyn: this body knows only `dyn Uart`]
[dyn: this body knows only `dyn Uart`]
one function. Three targets. The vtable chose putc each time.
3 println!("\n== versus the generic ==");
banner_generic(&mut console);
banner_generic(&mut tally);

3== versus the generic ==
rv6 ready
[generic: compiled for 07_static_vs_dyn::Console]
[generic: compiled for 07_static_vs_dyn::Tally]
4 println!("\n== the coercion point ==");
let u: &mut dyn Uart = &mut rec; // <- here the concrete type is forgotten
u.puts(b"!");
println!("`let u: &mut dyn Uart = &mut rec;` -- from this line on, u is a fat");
println!("pointer and Recorder is gone. rec.0.len() = {} proves it still wrote.",
rec.0.len());

4== the coercion point ==
`let u: &mut dyn Uart = &mut rec;` -- from this line on, u is a fat
pointer and Recorder is gone. rec.0.len() = 11 proves it still wrote.
5 println!("\n== what dyn buys: heterogeneous storage ==");
let mut sinks: [&mut dyn Uart; 3] = [&mut console, &mut rec, &mut tally];
for s in sinks.iter_mut() {
s.putln(b"tick");
}
println!("[&mut dyn Uart; 3] held a Console, a Recorder, and a Tally.");
println!("[&mut U; 3] cannot: U is ONE type, chosen once.");
println!("Recorder now has {} bytes; Tally counted {}", rec.0.len(), tally.0);

5== what dyn buys: heterogeneous storage ==
tick
[&mut dyn Uart; 3] held a Console, a Recorder, and a Tally.
[&mut U; 3] cannot: U is ONE type, chosen once.
Recorder now has 16 bytes; Tally counted 25
6 println!("\n== the cost ==");
println!("each call: load the method pointer from the vtable, jump through it.");
println!("no inlining across the call. The shell uses dyn at 115200 baud, where");
println!("that is nothing. The scheduler uses generics, because it runs every tick.");

6== the cost ==
each call: load the method pointer from the vtable, jump through it.
no inlining across the call. The shell uses dyn at 115200 baud, where
that is nothing. The scheduler uses generics, because it runs every tick.
7 println!("\n== what a vtable cannot hold ==");
println!("a generic method would need one entry per type it is ever called");
println!("with -- not a number the compiler knows. Such a trait is not");
println!("`dyn compatible`, and using it as `dyn` is error[E0038].");
println!("RUN ./show-errors.sh e0038");
7== what a vtable cannot hold ==
a generic method would need one entry per type it is ever called
with -- not a number the compiler knows. Such a trait is not
`dyn compatible`, and using it as `dyn` is error[E0038].
}
exit status 0

08 Absence is a fact. Failure is a decision.

absence is a fact, failure is a decision · point at first_free(free).ok_or(AllocError::OutOfFrames)

$ cargo run --bin 08_option_vs_result
cargo build: 1 warning e
warning: unused `Result` that must be used
|
104 | alloc_frame(&mut free);
| ^^^^^^^^^^^^^^^^^^^^^^
|
= note: this `Result` may be an `Err` variant, which should be handled
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
104 | let _ = alloc_frame(&mut free);
| +++++++
//! 08 — Absence is a fact. Failure is a decision.▸ 20 more lines
//!
//! The page allocator keeps one bit per frame. Two questions look alike and
//! are not:
//!
//! first_free(&free) -> Option<usize> is there a free frame?
//! alloc_frame(&mut free) -> Result<usize, AllocError> give me one.
//!
//! A search that finds nothing has not FAILED; it has answered. `Option` is the
//! type for that. `alloc_frame` promised a frame and could not deliver, and
//! must say why. `Result` is the type for that. The line where one becomes the
//! other is
//!
//! first_free(free).ok_or(AllocError::OutOfFrames)
//!
//! and that line is where the POLICY lives: "no free frame" means "allocation
//! failed" HERE, at this call, and nowhere else.
//!
//! This program builds with ONE WARNING, on purpose. Read it.
//!
//! Run: cargo run --bin 08_option_vs_result

/// Every way frame allocation can go wrong. One variant per way.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AllocError {
/// Every frame is in use.
OutOfFrames,
/// The caller tried to free a frame that was already free: a double free.
AlreadyFree,
}

/// The lowest free frame, if there is one. A question, not a request.
fn first_free(free: &[bool]) -> Option<usize> {
free.iter().position(|&f| f)
}

/// Take a frame. This is a request, and it can be refused.
fn alloc_frame(free: &mut [bool]) -> Result<usize, AllocError> {
let i = first_free(free).ok_or(AllocError::OutOfFrames)?;
free[i] = false;
Ok(i)
}

/// Give a frame back. `Ok(())`: success with nothing to say.
fn free_frame(free: &mut [bool], i: usize) -> Result<(), AllocError> {
if free[i] {
return Err(AllocError::AlreadyFree);
}
free[i] = true;
Ok(())
}

fn main() {
0 let mut free = [false, false, true, true];
println!("free = {free:?} (true = available)");

0free = [false, false, true, true] (true = available)
1 println!("\n== Option: the honest answer may be `nothing` ==");
println!("first_free(&free) = {:?}", first_free(&free));
println!("first_free(&[false, false]) = {:?} not an error. There are none.",
first_free(&[false, false]));

1== Option: the honest answer may be `nothing` ==
first_free(&free) = Some(2)
first_free(&[false, false]) = None not an error. There are none.
2 println!("\n== Result: a request, and a reason when it is refused ==");
for _ in 0..3 {
let r = alloc_frame(&mut free);
println!("alloc_frame(..) = {r:?} free = {free:?}");
}
println!("the third call did not get a frame, and the value says why.");

2== Result: a request, and a reason when it is refused ==
alloc_frame(..) = Ok(2) free = [false, false, false, true]
alloc_frame(..) = Ok(3) free = [false, false, false, false]
alloc_frame(..) = Err(OutOfFrames) free = [false, false, false, false]
the third call did not get a frame, and the value says why.
3 println!("\n== the line between them ==");
println!(" first_free(free).ok_or(AllocError::OutOfFrames)");
println!("is exactly:");
println!(" match first_free(free) {{");
println!(" Some(i) => Ok(i),");
println!(" None => Err(AllocError::OutOfFrames),");
println!(" }}");
println!("Some -> Ok, None -> Err(the one you chose). That choice IS the policy.");

3== the line between them ==
first_free(free).ok_or(AllocError::OutOfFrames)
is exactly:
match first_free(free) {
Some(i) => Ok(i),
None => Err(AllocError::OutOfFrames),
}
Some -> Ok, None -> Err(the one you chose). That choice IS the policy.
4 println!("\n== Ok(()) : success with nothing to report ==");
println!("free_frame(&mut free, 2) = {:?}", free_frame(&mut free, 2));
println!("free_frame(&mut free, 2) = {:?} the double free, caught",
free_frame(&mut free, 2));
println!("free = {free:?}");

4== Ok(()) : success with nothing to report ==
free_frame(&mut free, 2) = Ok(())
free_frame(&mut free, 2) = Err(AlreadyFree) the double free, caught
free = [false, false, true, false]
5 println!("\n== opening a Result ==");
match alloc_frame(&mut free) {
Ok(i) => println!("match: got frame {i}"),
Err(AllocError::OutOfFrames) => println!("match: out of frames"),
Err(AllocError::AlreadyFree) => println!("match: cannot happen here, but the arm must exist"),
}
if let Ok(i) = alloc_frame(&mut free) {
println!("if let: got frame {i}");
} else {
println!("if let: refused");
}
println!("`match` lists every variant. Add one to AllocError and this match");
println!("stops compiling until you decide what it means here.");

5== opening a Result ==
match: got frame 2
if let: refused
`match` lists every variant. Add one to AllocError and this match
stops compiling until you decide what it means here.
6 println!("\n== the warning, on purpose ==");
println!("the next line calls alloc_frame and drops the Result on the floor.");
println!("rustc noticed when this program was built. Scroll up: it said");
println!(" warning: unused `Result` that must be used");
println!("that is #[must_use] on Result, and it is the -1 nobody checked in C.");
// THIS LINE WARNS ON PURPOSE. Leave it. The warning is the demo.
alloc_frame(&mut free);
println!("the honest way to discard one, when there is truly nothing to do:");
println!(" let _ = alloc_frame(&mut free);");
let _ = alloc_frame(&mut free);
println!("`let _ =` says `I looked, and I chose not to care` -- in the source.");
6== the warning, on purpose ==
the next line calls alloc_frame and drops the Result on the floor.
rustc noticed when this program was built. Scroll up: it said
warning: unused `Result` that must be used
that is #[must_use] on Result, and it is the -1 nobody checked in C.
the honest way to discard one, when there is truly nothing to do:
let _ = alloc_frame(&mut free);
`let _ =` says `I looked, and I chose not to care` -- in the source.
}
exit status 0

09 Your own error type, and the ? that carries it.

each ? is a different exit · point at the three ?s in load

$ cargo run --bin 09_error_enum_and_question_mark
//! 09 — Your own error type, and the `?` that carries it.▸ 25 more lines
//!
//! `exec` opens a file, checks the ELF magic, checks the size fits in memory,
//! and only then loads. Three fallible steps, three different reasons to stop:
//!
//! enum ExecError { NotFound, BadMagic, TooBig }
//!
//! The `?` operator is what makes the chain readable:
//!
//! let bytes = open(name)?; if open failed, return its Err NOW
//! check_magic(bytes)?; same
//! let size = check_size(bytes)?; same
//!
//! `expr?` is exactly
//!
//! match expr {
//! Ok(v) => v,
//! Err(e) => return Err(From::from(e)),
//! }
//!
//! so it can only appear in a function that returns a Result (or Option): it
//! needs somewhere to `return Err` TO. The `From::from` is the quiet part: it
//! converts the error type on the way out, so an inner error can become an
//! outer one with no visible code. The last section shows that.
//!
//! Run: cargo run --bin 09_error_enum_and_question_mark

/// One variant per way `load` can fail. Nothing else.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExecError {
NotFound,
BadMagic,
TooBig,
}

/// A loaded program: for now, just how big it was.
#[derive(Debug, PartialEq, Eq)]
pub struct Image {
pub size: usize,
}

const ELF_MAGIC: [u8; 4] = [0x7f, b'E', b'L', b'F'];
const MAX_IMAGE: usize = 64 * 1024;

// Three "files". A real filesystem arrives in 40k; today they are statics.
static INIT: [u8; 128] = {
let mut a = [0u8; 128];
a[0] = 0x7f;
a[1] = b'E';
a[2] = b'L';
a[3] = b'F';
a
};
static NOTES: &[u8] = b"remember to fix the scheduler\n";
static HUGE: [u8; 70_000] = {
let mut a = [0u8; 70_000];
a[0] = 0x7f;
a[1] = b'E';
a[2] = b'L';
a[3] = b'F';
a
};

/// A directory lookup: absence, so Option.
fn find_file(name: &str) -> Option<&'static [u8]> {
match name {
"init" => Some(&INIT),
"notes.txt" => Some(NOTES),
"huge" => Some(&HUGE),
_ => None,
}
}

/// The first fallible step. `.ok_or` decides what absence means here.
fn open(name: &str) -> Result<&'static [u8], ExecError> {
find_file(name).ok_or(ExecError::NotFound)
}

/// The second. Nothing to return on success, so `Ok(())`.
fn check_magic(bytes: &[u8]) -> Result<(), ExecError> {
if bytes.len() < 4 || bytes[..4] != ELF_MAGIC {
return Err(ExecError::BadMagic);
}
Ok(())
}

/// The third.
fn check_size(bytes: &[u8]) -> Result<usize, ExecError> {
if bytes.len() > MAX_IMAGE {
return Err(ExecError::TooBig);
}
Ok(bytes.len())
}

/// The chain. Four lines; three exits.
fn load(name: &str) -> Result<Image, ExecError> {
let bytes = open(name)?;
check_magic(bytes)?;
let size = check_size(bytes)?;
Ok(Image { size })
}

/// The same function with every `?` written out.
#[allow(clippy::question_mark)] // clippy knows. That is the demo.
fn load_longhand(name: &str) -> Result<Image, ExecError> {
let bytes = match open(name) {
Ok(b) => b,
Err(e) => return Err(e),
};
match check_magic(bytes) {
Ok(()) => {}
Err(e) => return Err(e),
}
let size = match check_size(bytes) {
Ok(n) => n,
Err(e) => return Err(e),
};
Ok(Image { size })
}

// ---- the last section: `?` converts on the way out ----------------------

/// The shell's errors: its own, plus anything exec can say.
#[derive(Debug, PartialEq, Eq)]
pub enum ShellError {
Exec(ExecError),
BadArgs,
}

/// Written once per pair of types. After this, `?` does the wrapping.
impl From<ExecError> for ShellError {
fn from(e: ExecError) -> ShellError {
ShellError::Exec(e)
}
}

/// The shell runs a command line. Two error types meet here.
fn run_cmd(argv: &[&str]) -> Result<Image, ShellError> {
let name = argv.first().ok_or(ShellError::BadArgs)?;
let image = load(name)?; // ExecError -> ShellError, via From, silently
Ok(image)
}

fn main() {
1 println!("== each ? is a different exit ==");
for name in ["init", "nope", "notes.txt", "huge"] {
println!("load({name:>10}) -> {:?}", load(name));
}
println!("init passed all three checks; the other three each stopped at a");
println!("different `?`, and the Err says which one.");

1== each ? is a different exit ==
load( init) -> Ok(Image { size: 128 })
load( nope) -> Err(NotFound)
load( notes.txt) -> Err(BadMagic)
load( huge) -> Err(TooBig)
init passed all three checks; the other three each stopped at a
different `?`, and the Err says which one.
2 println!("\n== longhand: what ? expands to ==");
for name in ["init", "nope", "notes.txt", "huge"] {
assert_eq!(load(name), load_longhand(name));
}
println!("load_longhand agrees on all four. It is 15 lines where load is 4,");
println!("and every one of those `Err(e) => return Err(e)` is the same line.");

2== longhand: what ? expands to ==
load_longhand agrees on all four. It is 15 lines where load is 4,
and every one of those `Err(e) => return Err(e)` is the same line.
3 println!("\n== ? needs somewhere to return to ==");
println!(" fn sys_exec(name: &str) -> i64 {{ let img = load(name)?; ... }}");
println!(" error[E0277]: the `?` operator can only be used in a function that");
println!(" returns `Result` or `Option`");
println!("a function returning an integer has no Err to return. That is the");
println!("boundary, and the boundary is where ? stops and match begins.");
println!("RUN ./show-errors.sh e0277");

3== ? needs somewhere to return to ==
fn sys_exec(name: &str) -> i64 { let img = load(name)?; ... }
error[E0277]: the `?` operator can only be used in a function that
returns `Result` or `Option`
a function returning an integer has no Err to return. That is the
boundary, and the boundary is where ? stops and match begins.
4 println!("\n== From: ? converts the error on the way out ==");
println!("run_cmd(&[]) -> {:?}", run_cmd(&[]));
println!("run_cmd(&[\"nope\"]) -> {:?}", run_cmd(&["nope"]));
println!("run_cmd(&[\"init\"]) -> {:?}", run_cmd(&["init"]));
println!("load returned Err(ExecError::NotFound). run_cmd returned");
println!("Err(ShellError::Exec(NotFound)). Nobody wrote the wrapping at the");
println!("call: `?` called From::from, and `impl From<ExecError> for ShellError`");
println!("was written once.");
4== From: ? converts the error on the way out ==
run_cmd(&[]) -> Err(BadArgs)
run_cmd(&["nope"]) -> Err(Exec(NotFound))
run_cmd(&["init"]) -> Ok(Image { size: 128 })
load returned Err(ExecError::NotFound). run_cmd returned
Err(ShellError::Exec(NotFound)). Nobody wrote the wrapping at the
call: `?` called From::from, and `impl From<ExecError> for ShellError`
was written once.
}
exit status 0

10 Where a Result becomes a number.

the collapse happens in exactly one place · point at the match in sys_read

$ cargo run --bin 10_errno_boundary
//! 10 — Where a `Result` becomes a number.▸ 16 more lines
//!
//! A user program does not receive a Result. It receives one register, `a0`.
//! So somewhere at the very edge of the kernel a rich error has to collapse
//! into an integer, and the Unix convention is:
//!
//! >= 0 success, and the value
//! < 0 minus an error number: -9 EBADF, -21 EISDIR, ...
//!
//! The rule this program is about: that collapse happens in EXACTLY ONE
//! PLACE. Everything behind it speaks `FdError`; everything in front of it
//! speaks integers; the `match` in `sys_read` is the wall between them.
//!
//! read_fd -> Result<usize, FdError> the kernel talking to itself
//! sys_read -> i64 the kernel talking to a user
//!
//! Run: cargo run --bin 10_errno_boundary

/// One open file, as the per-process table sees it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct File {
pub inum: u32,
pub readable: bool,
pub is_dir: bool,
}

/// Every way a read through a descriptor can fail.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FdError {
BadFd,
NotReadable,
IsADirectory,
}

// The real numbers. Linux's, xv6-adjacent, and ours from 50k on.
pub const EBADF: i64 = 9;
pub const EISDIR: i64 = 21;

/// Read from descriptor `fd`. Returns how many bytes: the file's size, here.
fn read_fd(fds: &[Option<File>], fd: usize) -> Result<usize, FdError> {
// .get(fd) is the bounds check as an Option; .copied().flatten() turns
// Option<&Option<File>> into Option<File>; .ok_or names what None means.
let file = fds.get(fd).copied().flatten().ok_or(FdError::BadFd)?;
if file.is_dir {
return Err(FdError::IsADirectory);
}
if !file.readable {
return Err(FdError::NotReadable);
}
Ok(512 * file.inum as usize) // pretend: file size is 512 x inum
}

/// THE BOUNDARY. One match. The only place these numbers are ever spelled.
fn sys_read(fds: &[Option<File>], fd: i64) -> i64 {
// The raw register first: a negative fd was never an index at all.
if fd < 0 {
return -EBADF;
}
match read_fd(fds, fd as usize) {
Ok(n) => n as i64,
Err(FdError::BadFd) | Err(FdError::NotReadable) => -EBADF,
Err(FdError::IsADirectory) => -EISDIR,
}
}

/// What rv6 does today. The cause is lost at the wall.
fn sys_read_lossy(fds: &[Option<File>], fd: i64) -> i64 {
if fd < 0 {
return -1;
}
match read_fd(fds, fd as usize) {
Ok(n) => n as i64,
Err(_) => -1,
}
}

/// ulib's side of the wall: turn the number back into a Result. Eight lines
/// in ulib/src/lib.rs, and this is all of them.
#[derive(Debug, PartialEq, Eq)]
pub struct UlibError(pub i32);
fn ulib_read(fds: &[Option<File>], fd: i64) -> Result<usize, UlibError> {
let rc = sys_read(fds, fd);
if rc < 0 {
Err(UlibError(rc as i32))
} else {
Ok(rc as usize)
}
}

fn main() {
let fds: [Option<File>; 5] = [
Some(File { inum: 1, readable: true, is_dir: false }), // 0 stdin
Some(File { inum: 1, readable: false, is_dir: false }), // 1 stdout: write only
Some(File { inum: 1, readable: false, is_dir: false }), // 2 stderr
Some(File { inum: 3, readable: true, is_dir: false }), // 3 a file
Some(File { inum: 5, readable: true, is_dir: true }), // 4 a directory
];

1 println!("== behind the wall: read_fd speaks FdError ==");
for fd in [0usize, 1, 3, 4, 9] {
println!("read_fd(fd={fd}) -> {:?}", read_fd(&fds, fd));
}

1== behind the wall: read_fd speaks FdError ==
read_fd(fd=0) -> Ok(512)
read_fd(fd=1) -> Err(NotReadable)
read_fd(fd=3) -> Ok(1536)
read_fd(fd=4) -> Err(IsADirectory)
read_fd(fd=9) -> Err(BadFd)
2 println!("\n== at the wall: sys_read speaks i64 ==");
println!("{:>6} {:>10} {:>12}", "fd", "sys_read", "meaning");
for fd in [0i64, 1, 3, 4, 9, -1] {
let rc = sys_read(&fds, fd);
let meaning = match rc {
n if n >= 0 => "bytes".to_string(),
n if n == -EBADF => "-EBADF".to_string(),
n if n == -EISDIR => "-EISDIR".to_string(),
_ => "?".to_string(),
};
println!("{fd:>6} {rc:>10} {meaning:>12}");
}
println!("one match. Add a variant to FdError and that match stops compiling");
println!("until it has a number. Nothing else in the kernel ever spells 21.");

2== at the wall: sys_read speaks i64 ==
fd sys_read meaning
0 512 bytes
1 -9 -EBADF
3 1536 bytes
4 -21 -EISDIR
9 -9 -EBADF
-1 -9 -EBADF
one match. Add a variant to FdError and that match stops compiling
until it has a number. Nothing else in the kernel ever spells 21.
3 println!("\n== the lossy wall: rv6 today ==");
for fd in [3i64, 4, 9] {
println!("sys_read_lossy(fd={fd}) -> {}", sys_read_lossy(&fds, fd));
}
println!("`Err(_) => -1` throws the cause away. That is why ulib::Error is");
println!("a thin i32 and not an enum: the kernel genuinely does not say more.");

3== the lossy wall: rv6 today ==
sys_read_lossy(fd=3) -> 1536
sys_read_lossy(fd=4) -> -1
sys_read_lossy(fd=9) -> -1
`Err(_) => -1` throws the cause away. That is why ulib::Error is
a thin i32 and not an enum: the kernel genuinely does not say more.
4 println!("\n== in front of the wall: ulib rebuilds a Result from the number ==");
for fd in [3i64, 4] {
println!("ulib_read(fd={fd}) -> {:?}", ulib_read(&fds, fd));
}
println!("Result -> i64 -> Result. Types do not cross the trap; numbers do.");

4== in front of the wall: ulib rebuilds a Result from the number ==
ulib_read(fd=3) -> Ok(1536)
ulib_read(fd=4) -> Err(UlibError(-21))
Result -> i64 -> Result. Types do not cross the trap; numbers do.
5 println!("\n== unwrap, expect, and the dead machine ==");
let n = read_fd(&fds, 3).expect("fd 3 was just opened");
println!("read_fd(3).expect(..) = {n} honest: the table above shows it is open");
println!("read_fd(9).unwrap() would panic:");
println!(" called `Result::unwrap()` on an `Err` value: BadFd");
println!("on your laptop that is one red test. On rv6 it prints `panic` and");
println!("the machine stops. Rule: Result for what the world did to you,");
println!("panic for what you did to yourself.");

5== unwrap, expect, and the dead machine ==
read_fd(3).expect(..) = 1536 honest: the table above shows it is open
read_fd(9).unwrap() would panic:
called `Result::unwrap()` on an `Err` value: BadFd
on your laptop that is one red test. On rv6 it prints `panic` and
the machine stops. Rule: Result for what the world did to you,
panic for what you did to yourself.
6 println!("\n== the siblings that do not panic ==");
println!("read_fd(9).unwrap_or(0) = {}", read_fd(&fds, 9).unwrap_or(0));
if let Ok(n) = read_fd(&fds, 0) {
println!("if let Ok(n) = read_fd(0) -> {n}");
}
println!("read_fd(4).is_err() = {}", read_fd(&fds, 4).is_err());

// UNCOMMENT to watch it die:
// let _ = read_fd(&fds, 99).unwrap();
6== the siblings that do not panic ==
read_fd(9).unwrap_or(0) = 0
if let Ok(n) = read_fd(0) -> 512
read_fd(4).is_err() = true
}
exit status 0

11 A string is bytes plus a promise the kernel cannot afford to check.

a string is bytes plus a checked promise · point at str::from_utf8(..) returns a Result

$ cargo run --bin 11_bytes_vs_strings
//! 11 — A string is bytes plus a promise the kernel cannot afford to check.▸ 15 more lines
//!
//! Four types that all look like "text" and are not the same thing:
//!
//! u8 one byte 1 byte
//! char one Unicode scalar value 4 bytes
//! &str pointer + length, UTF-8 GUARANTEED 16 bytes
//! &[u8] pointer + length, no promise at all 16 bytes
//!
//! The kernel boundary is defined in bytes: a UART sends bytes, a disk block
//! is bytes, and the argv `exec` builds is NUL-terminated bytes. Turning bytes
//! into a `&str` is a CHECK, it costs a validation table, and it can fail --
//! so `str::from_utf8` returns a Result, and `ulib::Args::get` hands you the
//! bytes and lets you decide.
//!
//! Run: cargo run --bin 11_bytes_vs_strings

use std::mem::size_of;
use std::str;

fn main() {
1 println!("== four sizes ==");
println!("size_of::<u8>() = {}", size_of::<u8>());
println!("size_of::<char>() = {} a Unicode scalar, not a byte", size_of::<char>());
println!("size_of::<&str>() = {} pointer + length, plus a promise", size_of::<&str>());
println!("size_of::<&[u8]>() = {} pointer + length, no promise", size_of::<&[u8]>());

1== four sizes ==
size_of::<u8>() = 1
size_of::<char>() = 4 a Unicode scalar, not a byte
size_of::<&str>() = 16 pointer + length, plus a promise
size_of::<&[u8]>() = 16 pointer + length, no promise
2 println!("\n== a byte-string literal ==");
let flag: &[u8; 2] = b"-n";
let s: &[u8] = flag; // an array reference coerces to a slice
println!("b\"-n\" is a &[u8; 2] = {flag:?}, and coerces to &[u8] = {s:?}");
let arg: &[u8] = b"-n";
println!("arg == b\"-n\" -> {} slices compare byte by byte", arg == b"-n");
println!("arg == \"-n\" -> error[E0277]: can't compare `[u8]` with `str`");

2== a byte-string literal ==
b"-n" is a &[u8; 2] = [45, 110], and coerces to &[u8] = [45, 110]
arg == b"-n" -> true slices compare byte by byte
arg == "-n" -> error[E0277]: can't compare `[u8]` with `str`
3 println!("\n== a &str is bytes underneath, and len() counts the bytes ==");
let word = "café";
println!("\"café\".len() = {} bytes", word.len());
println!("\"café\".chars().count() = {} characters", word.chars().count());
print!("as bytes:");
for b in word.as_bytes() {
print!(" {b:#04x}");
}
println!();
println!("é is two bytes, 0xc3 0xa9, and both have the high bit set.");
println!("every byte of a multi-byte character is >= 0x80, so a byte-wise");
println!("search for b'\\n' (0x0a) can never land inside one. Lines are free.");

3== a &str is bytes underneath, and len() counts the bytes ==
"café".len() = 5 bytes
"café".chars().count() = 4 characters
as bytes: 0x63 0x61 0x66 0xc3 0xa9
é is two bytes, 0xc3 0xa9, and both have the high bit set.
every byte of a multi-byte character is >= 0x80, so a byte-wise
search for b'\n' (0x0a) can never land inside one. Lines are free.
4 println!("\n== from_utf8 is a check, and it can fail ==");
println!("from_utf8(b\"hello\") = {:?}", str::from_utf8(b"hello"));
// Built at run time: rustc can see through a literal here and warns,
// which is itself the point -- the check is real, and it is not free.
let bad: Vec<u8> = [0xffu8, 0x41].to_vec();
println!("from_utf8(&[0xff, 0x41]) = {:?}", str::from_utf8(&bad));
let cut = &word.as_bytes()[..4]; // "caf" + the first byte of é
println!("from_utf8(&\"café\".as_bytes()[..4]) = {:?}", str::from_utf8(cut));
println!("a read() that stops mid-character hands you exactly that last case.");
println!("Args::get gives bytes. Args::str runs this check for you and says None.");

4== from_utf8 is a check, and it can fail ==
from_utf8(b"hello") = Ok("hello")
from_utf8(&[0xff, 0x41]) = Err(Utf8Error { valid_up_to: 0, error_len: Some(1) })
from_utf8(&"café".as_bytes()[..4]) = Err(Utf8Error { valid_up_to: 3, error_len: None })
a read() that stops mid-character hands you exactly that last case.
Args::get gives bytes. Args::str runs this check for you and says None.
5 println!("\n== matching on bytes ==");
let line = b"pid 42\tstate: run\n";
let (mut digits, mut blanks, mut other) = (0, 0, 0);
for &b in line.iter() {
match b {
b'\n' => break,
b'0'..=b'9' => digits += 1,
b' ' | b'\t' => blanks += 1,
_ => other += 1,
}
}
println!("{:?}", str::from_utf8(line).unwrap());
println!("digits = {digits}, blanks = {blanks}, other = {other} (stopped at the newline)");
println!("b'0'..=b'9' is a range of u8. No char, no str, no table.");

5== matching on bytes ==
"pid 42\tstate: run\n"
digits = 2, blanks = 3, other = 12 (stopped at the newline)
b'0'..=b'9' is a range of u8. No char, no str, no table.
6 println!("\n== parsing a number from bytes, by hand ==");
println!("b'7' - b'0' = {} digits are consecutive in ASCII", b'7' - b'0');
for arg in [b"512".as_slice(), b"4096", b"12x", b"", b"99999999999999999999"] {
println!("parse_usize({:?}) = {:?}", String::from_utf8_lossy(arg), parse_usize(arg));
}
println!("`head -n 12x` is a usage error, not a panic, and overflow is not wrap.");

6== parsing a number from bytes, by hand ==
b'7' - b'0' = 7 digits are consecutive in ASCII
parse_usize("512") = Some(512)
parse_usize("4096") = Some(4096)
parse_usize("12x") = None
parse_usize("") = None
parse_usize("99999999999999999999") = None
`head -n 12x` is a usage error, not a panic, and overflow is not wrap.
7 println!("\n== where the newline is ==");
let buf = b"first line\nsecond\n";
println!("buf.iter().position(|&b| b == b'\\n') = {:?}", buf.iter().position(|&b| b == b'\n'));
println!("that Option<usize> is the whole of what ulib::Lines does per line:");
println!("find the newline, hand out &buf[..i], carry on from i + 1.");
7== where the newline is ==
buf.iter().position(|&b| b == b'\n') = Some(10)
that Option<usize> is the whole of what ulib::Lines does per line:
find the newline, hand out &buf[..i], carry on from i + 1.
}

/// Decimal bytes to a number. `None` on empty, on a non-digit, on overflow.
fn parse_usize(bytes: &[u8]) -> Option<usize> {
if bytes.is_empty() {
return None;
}
let mut n: usize = 0;
for &b in bytes {
if !b.is_ascii_digit() {
return None;
}
n = n.checked_mul(10)?.checked_add((b - b'0') as usize)?;
}
Some(n)
}
exit status 0

12 argv in, bytes out, and the slice that is re-pointed, not copied.

the slice is re-pointed, not copied · point at buf = &buf[n..]

//! 12 — argv in, bytes out, and the slice that is re-pointed, not copied.▸ 18 more lines
//!
//! The folded `// fold:` block below is 10c's `echo`, as it will be on rv6;
//! open it once yours passes. Above it is a 40-line stand-in for ulib: an
//! `Args` over `&[&[u8]]`, a `write` that may accept FEWER bytes than it was
//! given, and the `write_all` that copes:
//!
//! while !buf.is_empty() {
//! let n = write(fd, buf)?; maybe fewer than buf.len()
//! if n == 0 { return Err(..) } a stalled descriptor, not progress
//! buf = &buf[n..]; <- re-point the slice past what went
//! }
//!
//! `buf = &buf[n..]` copies nothing. It moves the pointer forward and shrinks
//! the length. The stand-in can be told to accept at most CHUNK bytes per
//! call, which is what a real UART, pipe, or socket does to you.
//!
//! Run: cargo run --bin 12_argv_and_write_all
//! cargo run --bin 12_argv_and_write_all -- one two three

use std::io::Write;
use std::sync::atomic::{AtomicUsize, Ordering};

// ---- a 40-line ulib ------------------------------------------------------

pub type Fd = i32;
pub const STDOUT: Fd = 1;

/// A failed call. rv6 says -1 and nothing more, so neither does this.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Error(pub i32);

/// The most bytes one `write` will accept. usize::MAX = "all of them".
static CHUNK: AtomicUsize = AtomicUsize::new(usize::MAX);
static CALLS: AtomicUsize = AtomicUsize::new(0);

/// May write fewer bytes than asked. Says how many.
pub fn write(fd: Fd, buf: &[u8]) -> Result<usize, Error> {
if fd != STDOUT {
return Err(Error(-1));
}
let n = buf.len().min(CHUNK.load(Ordering::Relaxed));
let _ = std::io::stdout().write_all(&buf[..n]);
let _ = std::io::stdout().flush();
CALLS.fetch_add(1, Ordering::Relaxed);
Ok(n)
}

/// Every byte, or an error. ulib/src/lib.rs, verbatim.
pub fn write_all(fd: Fd, mut buf: &[u8]) -> Result<(), Error> {
while !buf.is_empty() {
let n = write(fd, buf)?;
if n == 0 {
return Err(Error(-1));
}
buf = &buf[n..];
}
Ok(())
}

/// The command line: a borrowed slice of borrowed byte slices.
#[derive(Clone, Copy)]
pub struct Args<'a> {
argv: &'a [&'a [u8]],
}

impl<'a> Args<'a> {
pub fn len(&self) -> usize {
self.argv.len()
}
pub fn is_empty(&self) -> bool {
self.argv.is_empty()
}
pub fn get(&self, i: usize) -> Option<&'a [u8]> {
self.argv.get(i).copied()
}
}

// fold: ---- the command: 10c's echo, solved. Write yours first. -----------▸ 12 more lines

/// 10c's `echo`. This function would be byte-identical on rv6.
fn run(args: Args) -> i32 {
for i in 1..args.len() {
if i > 1 {
let _ = write_all(STDOUT, b" ");
}
let _ = write_all(STDOUT, args.get(i).unwrap());
}
let _ = write_all(STDOUT, b"\n");
0
}

// ---- what ulib::main! expands to on the host -----------------------------

fn main() {
// argv as owned bytes, then as borrowed slices. No UTF-8 check anywhere.
let owned: Vec<Vec<u8>> = {
let real: Vec<Vec<u8>> = std::env::args_os().map(|a| a.into_encoded_bytes()).collect();
if real.len() > 1 {
real
} else {
vec![b"echo".to_vec(), b"hello".to_vec(), b"world".to_vec()]
}
};
let refs: Vec<&[u8]> = owned.iter().map(|v| &v[..]).collect();
let args = Args { argv: &refs };

$ cargo run --bin 12_argv_and_write_all
$ cargo run --bin 12_argv_and_write_all -- one two three
1 println!("== argv, as the program sees it ==");
println!("args.len() = {} that is argc, and argv[0] is the name", args.len());
for i in 0..args.len() {
println!(" args.get({i}) = {:?}", args.get(i).map(String::from_utf8_lossy));
}
println!(" args.get({}) = {:?} past the end: None, not a crash",
args.len(), args.get(args.len()));

$ cargo run --bin 12_argv_and_write_all
1== argv, as the program sees it ==
args.len() = 3 that is argc, and argv[0] is the name
args.get(0) = Some("echo")
args.get(1) = Some("hello")
args.get(2) = Some("world")
args.get(3) = None past the end: None, not a crash
$ cargo run --bin 12_argv_and_write_all -- one two three
1== argv, as the program sees it ==
args.len() = 4 that is argc, and argv[0] is the name
args.get(0) = Some("target/debug/12_argv_and_write_all")
args.get(1) = Some("one")
args.get(2) = Some("two")
args.get(3) = Some("three")
args.get(4) = None past the end: None, not a crash
2 println!("\n== the loop, narrated ==");
for i in 1..args.len() {
let sep = if i > 1 { "write b\" \" first, then" } else { "no space before it;" };
println!(" i = {i}: {sep} write args.get({i}).unwrap()");
}
println!(" then exactly one b\"\\n\"");
println!("the space is a SEPARATOR: n words, n - 1 spaces. `i > 1` is that rule.");

$ cargo run --bin 12_argv_and_write_all
2== the loop, narrated ==
i = 1: no space before it; write args.get(1).unwrap()
i = 2: write b" " first, then write args.get(2).unwrap()
then exactly one b"\n"
the space is a SEPARATOR: n words, n - 1 spaces. `i > 1` is that rule.
$ cargo run --bin 12_argv_and_write_all -- one two three
2== the loop, narrated ==
i = 1: no space before it; write args.get(1).unwrap()
i = 2: write b" " first, then write args.get(2).unwrap()
i = 3: write b" " first, then write args.get(3).unwrap()
then exactly one b"\n"
the space is a SEPARATOR: n words, n - 1 spaces. `i > 1` is that rule.
3 println!("\n== run(args) -> ");
let code = run(args);
println!("run returned {code}. On the host, main() now calls process::exit({code}).");

$ cargo run --bin 12_argv_and_write_all
3== run(args) ->
hello world
run returned 0. On the host, main() now calls process::exit(0).
$ cargo run --bin 12_argv_and_write_all -- one two three
3== run(args) ->
one two three
run returned 0. On the host, main() now calls process::exit(0).
4 println!("\n== a console that takes three bytes at a time ==");
CHUNK.store(3, Ordering::Relaxed);
CALLS.store(0, Ordering::Relaxed);
print!("bare write(STDOUT, b\"hello world\\n\") -> ");
let n = write(STDOUT, b"hello world\n");
println!(" <- returned {n:?}: three bytes went, and nothing complained");
println!("a short write is not an error. It is a number you have to read.");

CALLS.store(0, Ordering::Relaxed);
print!("write_all(STDOUT, b\"hello world\\n\") -> ");
let r = write_all(STDOUT, b"hello world\n");
println!(" <- {r:?} after {} calls to write", CALLS.load(Ordering::Relaxed));
println!("same twelve bytes, four calls, and `buf = &buf[n..]` between them.");
CHUNK.store(usize::MAX, Ordering::Relaxed);

$ cargo run --bin 12_argv_and_write_all
4== a console that takes three bytes at a time ==
bare write(STDOUT, b"hello world\n") -> hel <- returned Ok(3): three bytes went, and nothing complained
a short write is not an error. It is a number you have to read.
write_all(STDOUT, b"hello world\n") -> hello world
<- Ok(()) after 4 calls to write
same twelve bytes, four calls, and `buf = &buf[n..]` between them.
$ cargo run --bin 12_argv_and_write_all -- one two three
4== a console that takes three bytes at a time ==
bare write(STDOUT, b"hello world\n") -> hel <- returned Ok(3): three bytes went, and nothing complained
a short write is not an error. It is a number you have to read.
write_all(STDOUT, b"hello world\n") -> hello world
<- Ok(()) after 4 calls to write
same twelve bytes, four calls, and `buf = &buf[n..]` between them.
5 println!("\n== the Result from write_all ==");
println!("three things you can do with it inside `fn run(..) -> i32`:");
println!(" write_all(STDOUT, b\"\\n\"); warning: unused Result that must be used");
println!(" write_all(STDOUT, b\"\\n\")?; error[E0277]: run returns i32, not Result");
println!(" let _ = write_all(STDOUT, b\"\\n\"); the honest discard");
println!("there is nothing an echo can do if the console is gone. Say so.");

$ cargo run --bin 12_argv_and_write_all
5== the Result from write_all ==
three things you can do with it inside `fn run(..) -> i32`:
write_all(STDOUT, b"\n"); warning: unused Result that must be used
write_all(STDOUT, b"\n")?; error[E0277]: run returns i32, not Result
let _ = write_all(STDOUT, b"\n"); the honest discard
there is nothing an echo can do if the console is gone. Say so.
$ cargo run --bin 12_argv_and_write_all -- one two three
5== the Result from write_all ==
three things you can do with it inside `fn run(..) -> i32`:
write_all(STDOUT, b"\n"); warning: unused Result that must be used
write_all(STDOUT, b"\n")?; error[E0277]: run returns i32, not Result
let _ = write_all(STDOUT, b"\n"); the honest discard
there is nothing an echo can do if the console is gone. Say so.
6 println!("\n== then the number ==");
println!("process::exit({code}) -- the shell reads it as $?");
std::process::exit(code);
$ cargo run --bin 12_argv_and_write_all
6== then the number ==
process::exit(0) -- the shell reads it as $?
$ cargo run --bin 12_argv_and_write_all -- one two three
6== then the number ==
process::exit(0) -- the shell reads it as $?
}
exit status 0
exit status 0

13 Who picks the type: the call site, the code, or the input.

who names the type: the call site, or the input · point at fn open(&mut self, k: Kind) -> &mut dyn Uart

//! 13 — Who picks the type: the call site, the code, or the input.▸ 19 more lines
//!
//! Programs 06 and 07 chose every type at compile time: `banner(&mut rec)`
//! names Recorder on the spot. A kernel cannot. `sys_write(1, ..)` has to
//! work whether fd 1 was left on the console or redirected into a file by a
//! shell that ran long after the kernel was built, so something has to hold
//! a choice the compiler never saw:
//!
//! who names the type how it dispatches in rv6
//! the call site generic: a copy per U Scheduler/RoundRobin
//! the code, once dyn: one body, a vtable Out in the shell
//! the input, at run time dyn, or a tag + match the fd table
//!
//! Only the third row is a question the compiler cannot answer. This program
//! answers it twice -- once with `&mut dyn Uart` and once with rv6's own
//! answer, a one-byte TAG and a `match` -- from the same argv, so the two
//! print the same bytes in the same places.
//!
//! Run: cargo run --bin 13_dispatch_at_run_time
//! cargo run --bin 13_dispatch_at_run_time -- file file null

use std::mem::size_of;

pub trait Uart {
fn putc(&mut self, b: u8);
fn puts(&mut self, s: &[u8]) {
for &b in s {
self.putc(b);
}
}
fn putln(&mut self, s: &[u8]) {
self.puts(s);
self.putc(b'\n');
}
}

pub struct Console;
impl Uart for Console {
fn putc(&mut self, b: u8) {
print!("{}", b as char);
}
}

pub struct Recorder(pub Vec<u8>);
impl Uart for Recorder {
fn putc(&mut self, b: u8) {
self.0.push(b);
}
}

pub struct Tally(pub usize);
impl Uart for Tally {
fn putc(&mut self, _: u8) {
self.0 += 1;
}
}

/// Static dispatch: the call site names U, so the compiler emits one copy of
/// this body per U, each at its own address.
fn log_line<U: Uart>(u: &mut U, s: &[u8]) {
u.putln(s);
}

/// Dynamic dispatch: one copy, at one address, whatever it is handed.
fn log_line_dyn(u: &mut dyn Uart, s: &[u8]) {
u.putln(s);
}

/// rv6's `FileKind`: what `sys_open` worked out, kept in the file table.
/// One byte wide, `Copy`, and no vtable anywhere near it.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Kind {
Tty,
File,
Null,
}

impl Kind {
/// The one line where a string the compiler never saw becomes a choice.
fn of(name: &str) -> Kind {
match name {
"file" => Kind::File,
"null" => Kind::Null,
_ => Kind::Tty,
}
}
}

/// xv6's `devsw[]`, with names where it has major numbers.
struct Sinks {
tty: Console,
file: Recorder,
null: Tally,
}

impl Sinks {
/// `dyn` in RETURN position: one slot, holding whichever sink the tag
/// names. Three arms, three types, and no cast anywhere.
fn open(&mut self, k: Kind) -> &mut dyn Uart {
match k {
Kind::Tty => &mut self.tty,
Kind::File => &mut self.file, // <- each arm unsizes itself: no `as`
Kind::Null => &mut self.null,
}
}

/// The same choice with no trait object: rv6's `sys_write`. Every arm is
/// a direct call into one of `log_line`'s copies.
fn write(&mut self, k: Kind, s: &[u8]) {
match k {
Kind::Tty => log_line(&mut self.tty, s),
Kind::File => log_line(&mut self.file, s),
Kind::Null => log_line(&mut self.null, s),
}
}
}

fn main() {
let mut script: Vec<String> = std::env::args().skip(1).collect();
if script.is_empty() {
script = ["tty", "file", "null"].iter().map(|s| s.to_string()).collect();
}
let mut sinks = Sinks { tty: Console, file: Recorder(Vec::new()), null: Tally(0) };

$ cargo run --bin 13_dispatch_at_run_time
$ cargo run --bin 13_dispatch_at_run_time -- file file null
1 println!("== one body, three addresses ==");
println!("log_line::<Console> at {:p}", log_line::<Console> as *const ());
println!("log_line::<Recorder> at {:p} a second function", log_line::<Recorder> as *const ());
println!("log_line::<Tally> at {:p} and a third", log_line::<Tally> as *const ());
println!("log_line_dyn at {:p} one copy, for all three",
log_line_dyn as *const ());
println!("is Console's copy Recorder's? {}",
log_line::<Console> as *const () == log_line::<Recorder> as *const ());
println!("program 06 printed three type names. These are the three functions");
println!("behind them: naming `log_line::<Console>` is what made the copy.");
println!("(the addresses move every run; that they DIFFER is the point.)");

$ cargo run --bin 13_dispatch_at_run_time
1== one body, three addresses ==
log_line::<Console> at 0x100f3cc6c
log_line::<Recorder> at 0x100f3cc9c a second function
log_line::<Tally> at 0x100f3cc3c and a third
log_line_dyn at 0x100f3bb3c one copy, for all three
is Console's copy Recorder's? false
program 06 printed three type names. These are the three functions
behind them: naming `log_line::<Console>` is what made the copy.
(the addresses move every run; that they DIFFER is the point.)
$ cargo run --bin 13_dispatch_at_run_time -- file file null
1== one body, three addresses ==
log_line::<Console> at 0x100c58c6c
log_line::<Recorder> at 0x100c58c9c a second function
log_line::<Tally> at 0x100c58c3c and a third
log_line_dyn at 0x100c57b3c one copy, for all three
is Console's copy Recorder's? false
program 06 printed three type names. These are the three functions
behind them: naming `log_line::<Console>` is what made the copy.
(the addresses move every run; that they DIFFER is the point.)
2 println!("\n== a tag the source does not know ==");
println!("script = {:?} <- argv, or this default", script);
for name in &script {
let k = Kind::of(name); // <- the whole run-time choice, in one byte
let out = sinks.open(k);
out.putln(b" rv6 ready");
println!(" file {:2} bytes, null {:2} <- Kind::of({:?}) = {:?}",
sinks.file.0.len(), sinks.null.0, name, k);
}
println!("ONE slot, re-pointed once per step, and `out` never learned which");
println!("sink it held. A step that opened the tty printed a line; the rest");
println!("went into a Vec and a counter, which is what the counts are.");

$ cargo run --bin 13_dispatch_at_run_time
2== a tag the source does not know ==
script = ["tty", "file", "null"] <- argv, or this default
rv6 ready
file 0 bytes, null 0 <- Kind::of("tty") = Tty
file 12 bytes, null 0 <- Kind::of("file") = File
file 12 bytes, null 12 <- Kind::of("null") = Null
ONE slot, re-pointed once per step, and `out` never learned which
sink it held. A step that opened the tty printed a line; the rest
went into a Vec and a counter, which is what the counts are.
$ cargo run --bin 13_dispatch_at_run_time -- file file null
2== a tag the source does not know ==
script = ["file", "file", "null"] <- argv, or this default
file 12 bytes, null 0 <- Kind::of("file") = File
file 24 bytes, null 0 <- Kind::of("file") = File
file 24 bytes, null 12 <- Kind::of("null") = Null
ONE slot, re-pointed once per step, and `out` never learned which
sink it held. A step that opened the tty printed a line; the rest
went into a Vec and a counter, which is what the counts are.
3 println!("\n== the same tag, with no vtable ==");
sinks.file.0.clear();
sinks.null.0 = 0; // <- the same script again, so the two blocks compare
for name in &script {
let k = Kind::of(name); // <- the same one byte, and no vtable after it
sinks.write(k, b" rv6 ready");
println!(" file {:2} bytes, null {:2} <- Kind::of({:?}) = {:?}",
sinks.file.0.len(), sinks.null.0, name, k);
}
println!("the same bytes in the same places, through a compare and a jump:");
println!("size_of::<Kind>() = {}, against size_of::<&mut dyn Uart>() = {}.",
size_of::<Kind>(), size_of::<&mut dyn Uart>());
println!("add `Pipe` to Kind and every match missing that arm stops");
println!("compiling. Add a fourth `impl Uart` and nothing stops -- or notices.");

$ cargo run --bin 13_dispatch_at_run_time
3== the same tag, with no vtable ==
rv6 ready
file 0 bytes, null 0 <- Kind::of("tty") = Tty
file 12 bytes, null 0 <- Kind::of("file") = File
file 12 bytes, null 12 <- Kind::of("null") = Null
the same bytes in the same places, through a compare and a jump:
size_of::<Kind>() = 1, against size_of::<&mut dyn Uart>() = 16.
add `Pipe` to Kind and every match missing that arm stops
compiling. Add a fourth `impl Uart` and nothing stops -- or notices.
$ cargo run --bin 13_dispatch_at_run_time -- file file null
3== the same tag, with no vtable ==
file 12 bytes, null 0 <- Kind::of("file") = File
file 24 bytes, null 0 <- Kind::of("file") = File
file 24 bytes, null 12 <- Kind::of("null") = Null
the same bytes in the same places, through a compare and a jump:
size_of::<Kind>() = 1, against size_of::<&mut dyn Uart>() = 16.
add `Pipe` to Kind and every match missing that arm stops
compiling. Add a fourth `impl Uart` and nothing stops -- or notices.
4 println!("\n== why open's return type cannot be a generic ==");
println!(" fn open(&mut self, k: Kind) -> &mut impl Uart // ONE type, not three");
println!(" error[E0308]: `match` arms have incompatible types");
println!(" Kind::File => &mut self.file, found to be `&mut Recorder`");
println!(" Kind::Null => &mut self.null, expected that, found `&mut Tally`");
println!("`impl Uart` is one type the compiler works out and you may not name.");
println!("`dyn Uart` is one type that stands in for all of them, which is why");
println!("it is what a slot filled at run time has to hold.");
println!("RUN ./show-errors.sh e0107");

$ cargo run --bin 13_dispatch_at_run_time
4== why open's return type cannot be a generic ==
fn open(&mut self, k: Kind) -> &mut impl Uart // ONE type, not three
error[E0308]: `match` arms have incompatible types
Kind::File => &mut self.file, found to be `&mut Recorder`
Kind::Null => &mut self.null, expected that, found `&mut Tally`
`impl Uart` is one type the compiler works out and you may not name.
`dyn Uart` is one type that stands in for all of them, which is why
it is what a slot filled at run time has to hold.
$ cargo run --bin 13_dispatch_at_run_time -- file file null
4== why open's return type cannot be a generic ==
fn open(&mut self, k: Kind) -> &mut impl Uart // ONE type, not three
error[E0308]: `match` arms have incompatible types
Kind::File => &mut self.file, found to be `&mut Recorder`
Kind::Null => &mut self.null, expected that, found `&mut Tally`
`impl Uart` is one type the compiler works out and you may not name.
`dyn Uart` is one type that stands in for all of them, which is why
it is what a slot filled at run time has to hold.
5 println!("\n== C fills the same table in by hand ==");
println!(" struct devsw {{ int (*read)(..); int (*write)(..); }}; // file.h");
println!(" devsw[CONSOLE].write = consolewrite; // console.c, at boot");
println!(" if (!devsw[major].write) return -1; // the slot may be empty");
println!("that is a vtable, written by hand, one per device instead of one per");
println!("type -- and that `if` guards a slot C cannot prove anybody filled.");

$ cargo run --bin 13_dispatch_at_run_time
5== C fills the same table in by hand ==
struct devsw { int (*read)(..); int (*write)(..); }; // file.h
devsw[CONSOLE].write = consolewrite; // console.c, at boot
if (!devsw[major].write) return -1; // the slot may be empty
that is a vtable, written by hand, one per device instead of one per
type -- and that `if` guards a slot C cannot prove anybody filled.
$ cargo run --bin 13_dispatch_at_run_time -- file file null
5== C fills the same table in by hand ==
struct devsw { int (*read)(..); int (*write)(..); }; // file.h
devsw[CONSOLE].write = consolewrite; // console.c, at boot
if (!devsw[major].write) return -1; // the slot may be empty
that is a vtable, written by hand, one per device instead of one per
type -- and that `if` guards a slot C cannot prove anybody filled.
6 println!("\n== which one to reach for ==");
println!("generic the call site names the type and the call is hot:");
println!(" Scheduler/RoundRobin, because pick_next runs every tick.");
println!("dyn one slot must hold a type chosen later, or one copy of a");
println!(" big body is worth an indirect call: Out in the shell.");
println!("tag the set is closed and small, and there is no allocator:");
println!(" FileKind in the fd table, read and write branching on it.");
println!("reach for dyn when the type must outlive the decision, not because");
println!("the word sounds dynamic. 06 pays code size for speed, 07 shows the");
println!("pointer, and this one shows who is doing the choosing.");
$ cargo run --bin 13_dispatch_at_run_time
6== which one to reach for ==
generic the call site names the type and the call is hot:
Scheduler/RoundRobin, because pick_next runs every tick.
dyn one slot must hold a type chosen later, or one copy of a
big body is worth an indirect call: Out in the shell.
tag the set is closed and small, and there is no allocator:
FileKind in the fd table, read and write branching on it.
reach for dyn when the type must outlive the decision, not because
the word sounds dynamic. 06 pays code size for speed, 07 shows the
pointer, and this one shows who is doing the choosing.
$ cargo run --bin 13_dispatch_at_run_time -- file file null
6== which one to reach for ==
generic the call site names the type and the call is hot:
Scheduler/RoundRobin, because pick_next runs every tick.
dyn one slot must hold a type chosen later, or one copy of a
big body is worth an indirect call: Out in the shell.
tag the set is closed and small, and there is no allocator:
FileKind in the fd table, read and write branching on it.
reach for dyn when the type must outlive the decision, not because
the word sounds dynamic. 06 pays code size for speed, 07 shows the
pointer, and this one shows who is doing the choosing.
}
exit status 0
exit status 0

14 A struct with a type parameter, and the guard that hands out the T.

one definition, a type for every T · point at impl<T> Lock<T> beside impl<T: Uart> Lock<T>

$ cargo run --bin 14_generic_struct_and_guard
//! 14 — A struct with a type parameter, and the guard that hands out the T.▸ 19 more lines
//!
//! Programs 06 and 07 put the parameter on a FUNCTION, where it makes copies
//! of one body. Put it on a STRUCT and it makes types: one definition, and a
//! separate type -- its own layout, its own methods -- for every T anybody
//! writes down.
//!
//! struct Lock<T> { held: bool, data: T } one definition
//!
//! Lock<Recorder> 32 bytes a Vec inside, and the flag
//! Lock<Console> 1 byte a zero-sized payload costs nothing
//! Lock<u64> 16 bytes Lock<Tally> is 16 too, and a different type
//!
//! Week 03's program 07 guarded a bool. This guards a T, which makes it
//! `rv6/src/spinlock.rs`: `SpinLock<T>` with a `SpinLockGuard<'a, T>`, the
//! type exercise 37k asks you to write. The kernel compiles two of them,
//! `SpinLock<i64>` for a semaphore's count and `SpinLock<FileSystem>` for the
//! one global filesystem, and they share no code at all.
//!
//! Run: cargo run --bin 14_generic_struct_and_guard

use std::mem::size_of;
use std::ops::{Deref, DerefMut};

pub trait Uart {
fn putc(&mut self, b: u8);
fn puts(&mut self, s: &[u8]) {
for &b in s {
self.putc(b);
}
}
fn putln(&mut self, s: &[u8]) {
self.puts(s);
self.putc(b'\n');
}
}

pub struct Console;
impl Uart for Console {
fn putc(&mut self, b: u8) {
print!("{}", b as char);
}
}

pub struct Recorder(pub Vec<u8>);
impl Uart for Recorder {
fn putc(&mut self, b: u8) {
self.0.push(b);
}
}

pub struct Tally(pub usize);
impl Uart for Tally {
fn putc(&mut self, _: u8) {
self.0 += 1;
}
}

/// A lock with a payload. NO bound on the struct: taking a lock does not
/// care what it is guarding, so `Lock<u64>` is as good as `Lock<Recorder>`.
struct Lock<T> {
held: bool,
data: T,
}

/// Holding this value IS holding the lock, and it is the only way to the T.
/// Two parameters: the lifetime of the borrow, and the type it guards.
struct Guard<'a, T> {
lock: &'a mut Lock<T>,
}

impl<T> Lock<T> {
/// A `const fn`, so a `static` can hold one before anything has run.
/// rv6 needs exactly this for `static FS: SpinLock<FileSystem>`.
const fn new(data: T) -> Self {
Lock { held: false, data }
}

/// Take the lock. On the host the borrow checker IS the lock: the guard
/// holds the only `&mut`, so nothing else can reach the data at all.
fn lock(&mut self) -> Guard<'_, T> {
self.held = true;
Guard { lock: self }
}

fn is_held(&self) -> bool {
self.held
}
}

/// The bound sits HERE, on one impl block, and not on the struct: `banner`
/// exists for a `Lock<Recorder>` and does not exist for a `Lock<u64>`.
impl<T: Uart> Lock<T> {
fn banner(&mut self) {
self.lock().putln(b"rv6 ready");
}
}

impl<T> Deref for Guard<'_, T> {
type Target = T;
fn deref(&self) -> &T {
&self.lock.data
}
}

impl<T> DerefMut for Guard<'_, T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.lock.data
}
}

impl<T> Drop for Guard<'_, T> {
fn drop(&mut self) {
self.lock.held = false;
println!(" [Drop for Guard: released]");
}
}

/// `const fn new` again, and this is as far as the host version goes: see
/// the last section for what the kernel has to do instead.
static TICKS: Lock<u64> = Lock::new(0);

fn main() {
1 println!("== one definition, a type for every T ==");
println!("size_of::<Recorder>() = {:2} the payload on its own",
size_of::<Recorder>());
println!("size_of::<Lock<Recorder>>() = {:2} the payload, the flag, the padding",
size_of::<Lock<Recorder>>());
println!("size_of::<Lock<Console>>() = {:2} a zero-sized payload costs nothing",
size_of::<Lock<Console>>());
println!("size_of::<Lock<u64>>() = {:2} the flag, seven bytes of padding, the u64",
size_of::<Lock<u64>>());
println!("size_of::<Lock<Tally>>() = {:2} the same size, not the same type",
size_of::<Lock<Tally>>());
println!("06 emitted one copy of a function body per type it was called");
println!("with. A struct with a parameter emits a TYPE per type: one");
println!("definition above, and the four Lock layouts here.");

1== one definition, a type for every T ==
size_of::<Recorder>() = 24 the payload on its own
size_of::<Lock<Recorder>>() = 32 the payload, the flag, the padding
size_of::<Lock<Console>>() = 1 a zero-sized payload costs nothing
size_of::<Lock<u64>>() = 16 the flag, seven bytes of padding, the u64
size_of::<Lock<Tally>>() = 16 the same size, not the same type
06 emitted one copy of a function body per type it was called
with. A struct with a parameter emits a TYPE per type: one
definition above, and the four Lock layouts here.
2 println!("\n== the guard is the permission ==");
let mut rec = Lock::new(Recorder(Vec::new()));
println!("is_held() before = {}", rec.is_held());
{
let mut g = rec.lock(); // <- to the closing brace, only g reaches the T
g.putln(b"rv6 ready");
println!("wrote {} bytes through the guard, which derefs to Recorder",
g.0.len());
}
println!("is_held() after = {}", rec.is_held());
println!("while the guard lived, `rec.is_held()` was not false, and not");
println!("true: it was error[E0502], cannot borrow `rec` as immutable");
println!("because it is also borrowed as mutable. Nobody can even ask.");

2== the guard is the permission ==
is_held() before = false
wrote 10 bytes through the guard, which derefs to Recorder
[Drop for Guard: released]
is_held() after = false
while the guard lived, `rec.is_held()` was not false, and not
true: it was error[E0502], cannot borrow `rec` as immutable
because it is also borrowed as mutable. Nobody can even ask.
3 println!("\n== where the bound goes ==");
let mut tal = Lock::new(Tally(0));
let mut ticks = Lock::new(0u64);
println!("banner takes the lock itself, so each call releases at its end:");
rec.banner();
tal.banner();
println!("Lock<Recorder> now holds {} bytes, and Lock<Tally> counted {}.",
rec.data.0.len(), tal.data.0);
println!("a Lock<u64> has no banner, and locks perfectly well without one:");
{
let mut g = ticks.lock();
*g += 1; // <- DerefMut on a u64: 37k's counter, 38k's `*count -= 1`
*g += 1;
}
println!("it counted to {}. Asking it for a banner does not compile:", ticks.data);
println!(" ticks.banner();");
println!(" error[E0599]: the method `banner` exists for struct");
println!(" `Lock<u64>`, but its trait bounds were not satisfied");
println!(" note: trait bound `u64: Uart` was not satisfied");
println!("`new`, `lock` and `is_held` live in `impl<T>` and exist for every");
println!("T. `banner` lives in `impl<T: Uart>`, so it exists for the sinks.");
println!("that is rv6: `SpinLock<T>` carries no bound, and `T: Send` sits");
println!("by itself on the `unsafe impl Sync`.");

3== where the bound goes ==
banner takes the lock itself, so each call releases at its end:
[Drop for Guard: released]
[Drop for Guard: released]
Lock<Recorder> now holds 20 bytes, and Lock<Tally> counted 10.
a Lock<u64> has no banner, and locks perfectly well without one:
[Drop for Guard: released]
it counted to 2. Asking it for a banner does not compile:
ticks.banner();
error[E0599]: the method `banner` exists for struct
`Lock<u64>`, but its trait bounds were not satisfied
note: trait bound `u64: Uart` was not satisfied
`new`, `lock` and `is_held` live in `impl<T>` and exist for every
T. `banner` lives in `impl<T: Uart>`, so it exists for the sinks.
that is rv6: `SpinLock<T>` carries no bound, and `T: Send` sits
by itself on the `unsafe impl Sync`.
4 println!("\n== a lifetime and a type ==");
println!("size_of::<Guard<'_, Recorder>>() = {:2} one thin pointer",
size_of::<Guard<'_, Recorder>>());
println!("size_of::<&mut dyn Uart>() = {:2} 07's fat pointer, for contrast",
size_of::<&mut dyn Uart>());
println!("the guard needs both parameters: `'a` for how long it borrows,");
println!("`T` for what it hands out. Leave the lifetime off and rustc says");
println!("error[E0106]: missing lifetime specifier, then writes the fix:");
println!(" struct Guard<'a, T> {{ lock: &'a Lock<T> }}");
println!("program 12's `Args<'a>` is this shape with the lifetime only.");

4== a lifetime and a type ==
size_of::<Guard<'_, Recorder>>() = 8 one thin pointer
size_of::<&mut dyn Uart>() = 16 07's fat pointer, for contrast
the guard needs both parameters: `'a` for how long it borrows,
`T` for what it hands out. Leave the lifetime off and rustc says
error[E0106]: missing lifetime specifier, then writes the fix:
struct Guard<'a, T> { lock: &'a Lock<T> }
program 12's `Args<'a>` is this shape with the lifetime only.
5 println!("\n== what rv6 writes instead ==");
println!(" pub struct SpinLock<T> {{ locked: AtomicBool, data: UnsafeCell<T> }}");
println!(" pub struct SpinLockGuard<'a, T> {{ lock: &'a SpinLock<T> }}");
println!(" unsafe impl<T: Send> Sync for SpinLock<T> {{}}");
println!("TICKS.is_held() = {}, because `new` is a const fn and a static",
TICKS.is_held());
println!("can hold one. `TICKS.lock()` is error[E0596]: cannot borrow");
println!("immutable static item `TICKS` as mutable -- and that is the whole");
println!("reason for the kernel's version. One CPU's borrow checker cannot");
println!("serialize two CPUs, so `lock` takes `&self`, spins on the atomic,");
println!("and buys the shared-to-mutable step with one UnsafeCell.");

5== what rv6 writes instead ==
pub struct SpinLock<T> { locked: AtomicBool, data: UnsafeCell<T> }
pub struct SpinLockGuard<'a, T> { lock: &'a SpinLock<T> }
unsafe impl<T: Send> Sync for SpinLock<T> {}
TICKS.is_held() = false, because `new` is a const fn and a static
can hold one. `TICKS.lock()` is error[E0596]: cannot borrow
immutable static item `TICKS` as mutable -- and that is the whole
reason for the kernel's version. One CPU's borrow checker cannot
serialize two CPUs, so `lock` takes `&self`, spins on the atomic,
and buys the shared-to-mutable step with one UnsafeCell.
6 println!("\n== when a struct needs a type parameter ==");
println!("it needs one when it HOLDS a T. The parameter has to appear in a");
println!("field, or the struct is error[E0392] and rustc offers PhantomData.");
println!("it does not when the set is closed -- 13's Sinks, rv6's FileKind");
println!("-- or when one slot must hold several types at once, which is");
println!("07's `&mut dyn Uart`. 06 pays code size for speed and 13 shows");
println!("who chooses; a generic struct pays in layout, one type per T.");
println!("RUN ./show-errors.sh e0392");
6== when a struct needs a type parameter ==
it needs one when it HOLDS a T. The parameter has to appear in a
field, or the struct is error[E0392] and rustc offers PhantomData.
it does not when the set is closed -- 13's Sinks, rv6's FileKind
-- or when one slot must hold several types at once, which is
07's `&mut dyn Uart`. 06 pays code size for speed and 13 shows
who chooses; a generic struct pays in layout, one type per T.
}
exit status 0

E0038 the trait Sink is not dyn compatible

a generic method, then &mut dyn Trait · fix: where Self: Sized, or take a slice, or go generic

$ ./show-errors.sh e0038
// error[E0038]: the trait `Sink` is not dyn compatible
//
// A vtable has one slot per method. `put_each` is GENERIC over I, so it would
// need one slot per type it is ever called with -- not a number the compiler
// knows. `fn new() -> Self` and methods without `self` fail for the same
// reason. `&mut dyn Sink` therefore cannot exist for this trait.
//
// FIX 1: `where Self: Sized` on put_each -- it exists for concrete types and
// vanishes from the vtable.
// FIX 2: make it non-generic: `put_each(&mut self, items: &[&[u8]])`.
// FIX 3: `fn banner<S: Sink>(s: &mut S)` -- go generic and never need dyn.
trait Sink {
fn put(&mut self, bytes: &[u8]);
fn put_each<I: Iterator<Item = &'static [u8]>>(&mut self, items: I) {
for item in items {
self.put(item);
}
}
}

struct Console;

impl Sink for Console {
fn put(&mut self, bytes: &[u8]) {
print!("{}", String::from_utf8_lossy(bytes));
}
}

fn banner(out: &mut dyn Sink) {
out.put(b"rv6\n");
}

fn main() {
banner(&mut Console);
}
Why, and 3 fixes e

A vtable has one slot per method. put_each is GENERIC over I, so it would need one slot per type it is ever called with -- not a number the compiler knows. fn new() -> Self and methods without self fail for the same reason. &mut dyn Sink therefore cannot exist for this trait.

  1. where Self: Sized on put_each -- it exists for concrete types and vanishes from the vtable.
  2. make it non-generic: put_each(&mut self, items: &[&[u8]]).
  3. fn banner<S: Sink>(s: &mut S) -- go generic and never need dyn.
$ rustc --edition 2021 --emit=metadata -o /dev/null e0038_not_dyn_compatible.rs
error[E0038]: the trait `Sink` is not dyn compatible
|
29 | fn banner(out: &mut dyn Sink) {
| ^^^^^^^^ `Sink` is not dyn compatible
|
note: for a trait to be dyn compatible it needs to allow building a vtable
for more information, visit <https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility>
|
12 | trait Sink {
| ---- this trait is not dyn compatible...
13 | fn put(&mut self, bytes: &[u8]);
14 | fn put_each<I: Iterator<Item = &'static [u8]>>(&mut self, items: I) {
| ^^^^^^^^ ...because method `put_each` has generic type parameters
= help: consider moving `put_each` to another trait
= help: only type `Console` implements `Sink`; consider using it directly instead.
error[E0038]: the trait `Sink` is not dyn compatible
|
34 | banner(&mut Console);
| ^^^^^^^^^^^^ `Sink` is not dyn compatible
|
note: for a trait to be dyn compatible it needs to allow building a vtable
for more information, visit <https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility>
|
12 | trait Sink {
| ---- this trait is not dyn compatible...
13 | fn put(&mut self, bytes: &[u8]);
14 | fn put_each<I: Iterator<Item = &'static [u8]>>(&mut self, items: I) {
| ^^^^^^^^ ...because method `put_each` has generic type parameters
= help: consider moving `put_each` to another trait
= help: only type `Console` implements `Sink`; consider using it directly instead.
error: aborting due to 2 previous errors
For more information about this error, try `rustc --explain E0038`.
exit status 1

E0046 not all trait items implemented, missing: put

overrode the default, skipped the required · fix: supply put; delete the override

$ ./show-errors.sh e0046
// error[E0046]: not all trait items implemented, missing: `put`
//
// `put_line` has a body in the trait, so it is a DEFAULT: optional. `put` has
// none, so it is REQUIRED. This impl wrote the optional one and skipped the
// required one -- and the default is BUILT ON `put`, so nothing works without
// it. The compiler names exactly what is missing.
//
// FIX 1: write `fn put(&mut self, b: &[u8])` and delete the override; the
// default `put_line` then works for free.
// FIX 2: keep the override only if it is genuinely different -- `put` is
// still required either way.
trait Sink {
fn put(&mut self, bytes: &[u8]);
fn put_line(&mut self, bytes: &[u8]) {
self.put(bytes);
self.put(b"\n");
}
}

struct Tally {
bytes: usize,
}

impl Sink for Tally {
fn put_line(&mut self, bytes: &[u8]) {
self.bytes += bytes.len() + 1;
}
}

fn main() {
let mut t = Tally { bytes: 0 };
t.put_line(b"rv6");
println!("{}", t.bytes);
}
Why, and 2 fixes e

put_line has a body in the trait, so it is a DEFAULT: optional. put has none, so it is REQUIRED. This impl wrote the optional one and skipped the required one -- and the default is BUILT ON put, so nothing works without it. The compiler names exactly what is missing.

  1. write fn put(&mut self, b: &[u8]) and delete the override; the default put_line then works for free.
  2. keep the override only if it is genuinely different -- put is still required either way.
$ rustc --edition 2021 --emit=metadata -o /dev/null e0046_missing_required_method.rs
error[E0046]: not all trait items implemented, missing: `put`
|
13 | fn put(&mut self, bytes: &[u8]);
| -------------------------------- `put` from trait
...
24 | impl Sink for Tally {
| ^^^^^^^^^^^^^^^^^^^ missing `put` in implementation
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0046`.
exit status 1

E0107 missing generics for struct FdTable

a generic struct used without its type argument · fix: name it, go generic too, &'a mut dyn Sink, or a tag

$ ./show-errors.sh e0107
// error[E0107]: missing generics for struct `FdTable`
//
// 07 asserts that `[&mut U; 3]` cannot hold three sinks, because U is ONE
// type. Here is the proof, twice: `FdTable<U>` is not a type until you name
// U, so every layer that touches one must name it too -- and once you have
// written `FdTable<Console>`, the assignment in main is error[E0308].
//
// FIX 1: `fn banner<S: Sink>(t: &mut FdTable<S>)` -- go generic too, and hand
// the choice one layer up. Every layer up pays the same tax.
// FIX 2: `struct FdTable<'a> { out: &'a mut dyn Sink }` -- one field type,
// any sink, re-pointed at run time. Program 13 does this.
// FIX 3: `enum Kind { Tty, File, Null }` and a match -- rv6's `FileKind`.
trait Sink {
fn put(&mut self, bytes: &[u8]);
}

struct Console;

impl Sink for Console {
fn put(&mut self, bytes: &[u8]) {
print!("{}", String::from_utf8_lossy(bytes));
}
}

struct VecSink(Vec<u8>);

impl Sink for VecSink {
fn put(&mut self, bytes: &[u8]) {
self.0.extend_from_slice(bytes);
}
}

struct FdTable<S: Sink> {
out: S,
}

fn banner(t: &mut FdTable) {
t.out.put(b"rv6 ready\n");
}

fn main() {
let mut fds = FdTable { out: Console };
banner(&mut fds);
fds.out = VecSink(Vec::new());
}
Why, and 3 fixes e

07 asserts that [&mut U; 3] cannot hold three sinks, because U is ONE type. Here is the proof, twice: FdTable<U> is not a type until you name U, so every layer that touches one must name it too -- and once you have written FdTable<Console>, the assignment in main is error[E0308].

  1. fn banner<S: Sink>(t: &mut FdTable<S>) -- go generic too, and hand the choice one layer up. Every layer up pays the same tax.
  2. struct FdTable<'a> { out: &'a mut dyn Sink } -- one field type, any sink, re-pointed at run time. Program 13 does this.
  3. enum Kind { Tty, File, Null } and a match -- rv6's FileKind.
$ rustc --edition 2021 --emit=metadata -o /dev/null e0107_one_type_per_call_site.rs
error[E0107]: missing generics for struct `FdTable`
|
37 | fn banner(t: &mut FdTable) {
| ^^^^^^^ expected 1 generic argument
|
note: struct defined here, with 1 generic parameter: `S`
|
33 | struct FdTable<S: Sink> {
| ^^^^^^^ -
help: add missing generic argument
|
37 | fn banner(t: &mut FdTable<S>) {
| +++
error[E0308]: mismatched types
|
44 | fds.out = VecSink(Vec::new());
| ------- ^^^^^^^^^^^^^^^^^^^ expected `Console`, found `VecSink`
| |
| expected due to the type of this binding
error: aborting due to 2 previous errors
Some errors have detailed explanations: E0107, E0308.
For more information about an error, try `rustc --explain E0107`.
exit status 1

E0277 the ? operator can only be used in a function that returns Result or Option (or another type that implements FromResidual)

? in a function returning i64 · fix: match at the boundary; ? only below it

$ ./show-errors.sh e0277
// error[E0277]: the `?` operator can only be used in a function that returns
// `Result` or `Option` (or another type that implements
// `FromResidual`)
//
// `?` is a `return Err(e)` in disguise, and this function returns i64 -- there
// is no Err to return. This IS the boundary: the place where `?` stops and
// `match` starts, because past here the caller only understands a number.
// 10c's `fn run(args: Args) -> i32` is the same wall.
//
// FIX 1: `match reg.lookup(name) { Ok(d) => .., Err(e) => -errno_for(e) }`
// -- collapse to a number HERE, exactly once.
// FIX 2: move the `?` down into a helper that returns Result; match its result here.
#[derive(Debug, Clone, Copy)]
struct Device {
major: u32,
}

#[derive(Debug)]
enum DevError {
NoSuchDevice,
}

struct Registry;

impl Registry {
fn lookup(&self, name: &str) -> Result<Device, DevError> {
if name == "console" { Ok(Device { major: 1 }) } else { Err(DevError::NoSuchDevice) }
}
fn getc(&self, dev: Device) -> Result<u8, DevError> {
if dev.major == 1 { Ok(b'l') } else { Err(DevError::NoSuchDevice) }
}
}

fn sys_getc(reg: &Registry, name: &str) -> i64 {
let dev = reg.lookup(name)?;
reg.getc(dev).map(|b| b as i64).unwrap_or(-1)
}

fn main() {
println!("{}", sys_getc(&Registry, "console"));
}
Why, and 2 fixes e

? is a return Err(e) in disguise, and this function returns i64 -- there is no Err to return. This IS the boundary: the place where ? stops and match starts, because past here the caller only understands a number. 10c's fn run(args: Args) -> i32 is the same wall.

  1. match reg.lookup(name) { Ok(d) => .., Err(e) => -errno_for(e) } -- collapse to a number HERE, exactly once.
  2. move the ? down into a helper that returns Result; match its result here.
$ rustc --edition 2021 --emit=metadata -o /dev/null e0277_question_mark_needs_result.rs
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
|
34 | fn sys_getc(reg: &Registry, name: &str) -> i64 {
| ---------------------------------------------- this function should return `Result` or `Option` to accept `?`
35 | let dev = reg.lookup(name)?;
| ^ cannot use the `?` operator in a function that returns `i64`
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0277`.
exit status 1

E0308 mismatched types expected Result<Device, DevError>, found Option<Device>

returned find's Option from lookup · fix: .ok_or(e), or the two-arm match

$ ./show-errors.sh e0308
// error[E0308]: mismatched types
// expected `Result<Device, DevError>`, found `Option<Device>`
//
// `find` reports ABSENCE: a name that is not there, nothing wrong. `lookup`
// must report FAILURE, and nobody said what absence MEANS here. The two enums
// do not convert themselves -- that decision is yours, and it is one line.
//
// FIX 1: `self.find(name).ok_or(DevError::NoSuchDevice)` -- Some -> Ok,
// None -> Err(the one you chose). That choice is the policy.
// FIX 2: `match self.find(name) { Some(d) => Ok(d), None => Err(..) }` --
// the same thing, longhand.
#[derive(Debug, Clone, Copy)]
struct Device {
major: u32,
}

#[derive(Debug)]
enum DevError {
NoSuchDevice,
NameTooLong,
}

struct Registry {
names: Vec<(&'static str, Device)>,
}

impl Registry {
fn find(&self, name: &str) -> Option<Device> {
self.names.iter().find(|(n, _)| *n == name).map(|(_, d)| *d)
}

fn lookup(&self, name: &str) -> Result<Device, DevError> {
if name.len() > 8 {
return Err(DevError::NameTooLong);
}
self.find(name)
}
}

fn main() {
let reg = Registry { names: vec![("console", Device { major: 1 })] };
println!("{:?}", reg.lookup("console").map(|d| d.major));
}
Why, and 2 fixes e

find reports ABSENCE: a name that is not there, nothing wrong. lookup must report FAILURE, and nobody said what absence MEANS here. The two enums do not convert themselves -- that decision is yours, and it is one line.

  1. self.find(name).ok_or(DevError::NoSuchDevice) -- Some -> Ok, None -> Err(the one you chose). That choice is the policy.
  2. match self.find(name) { Some(d) => Ok(d), None => Err(..) } -- the same thing, longhand.
$ rustc --edition 2021 --emit=metadata -o /dev/null e0308_option_is_not_result.rs
error[E0308]: mismatched types
|
32 | fn lookup(&self, name: &str) -> Result<Device, DevError> {
| ------------------------ expected `Result<Device, DevError>` because of return type
...
36 | self.find(name)
| ^^^^^^^^^^^^^^^ expected `Result<Device, DevError>`, found `Option<Device>`
|
= note: expected enum `Result<Device, DevError>`
found enum `Option<Device>`
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0308`.
exit status 1

E0392 type parameter T is never used

a generic struct that never holds its T · fix: hold one in a field, PhantomData<T>, or drop the parameter

$ ./show-errors.sh e0392
// error[E0392]: type parameter `T` is never used
//
// A generic FUNCTION names its parameter in the signature; a generic STRUCT
// has to name it in a FIELD. This lock declares a T and then guards only a
// bool, so `Lock<VecSink>` and `Lock<u64>` would be the same bytes.
//
// FIX 1: hold one -- `struct Lock<T> { held: bool, data: T }`, which is
// program 14, and rv6's `SpinLock<T>` with `UnsafeCell<T>`.
// FIX 2: `PhantomData<T>` for a T you want in the type and not in the
// bytes: units, a state machine, a device tag.
// FIX 3: drop it -- `struct Lock { held: bool }` is week03's program 07.
trait Sink {
fn put(&mut self, bytes: &[u8]);
}

struct VecSink(Vec<u8>);

impl Sink for VecSink {
fn put(&mut self, bytes: &[u8]) {
self.0.extend_from_slice(bytes);
}
}

struct Lock<T> {
held: bool,
}

fn main() {
let mut v = VecSink(Vec::new());
v.put(b"rv6 ready\n");
println!("{} bytes written, and nothing locked", v.0.len());
}
Why, and 3 fixes e

A generic FUNCTION names its parameter in the signature; a generic STRUCT has to name it in a FIELD. This lock declares a T and then guards only a bool, so Lock<VecSink> and Lock<u64> would be the same bytes.

  1. hold one -- struct Lock<T> { held: bool, data: T }, which is program 14, and rv6's SpinLock<T> with UnsafeCell<T>.
  2. PhantomData<T> for a T you want in the type and not in the bytes: units, a state machine, a device tag.
  3. drop it -- struct Lock { held: bool } is week03's program 07.
$ rustc --edition 2021 --emit=metadata -o /dev/null e0392_type_parameter_never_used.rs
error[E0392]: type parameter `T` is never used
|
24 | struct Lock<T> {
| ^ unused type parameter
|
= help: consider removing `T`, referring to it in a field, or using a marker such as `PhantomData`
= help: if you intended `T` to be a const parameter, use `const T: /* Type */` instead
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0392`.
exit status 1

E0506 cannot assign to table[_] because it is borrowed

assigning table[i] inside table.iter() · fix: iter_mut and *slot = …, or an index loop

$ ./show-errors.sh e0506
// error[E0506]: cannot assign to `table[_]` because it is borrowed
//
// The for loop's iterator holds a SHARED borrow of the whole table until the
// loop ends. `table[i] = None` writes into it while that borrow is alive.
// (On a Vec the same line is E0502 -- indexing a Vec is a method call.)
// This is exit() closing every descriptor; it is also 06r's alloc_slot.
//
// FIX 1: `for (i, slot) in table.iter_mut().enumerate() { *slot = None; }`
// -- one &mut per slot, handed out in turn, and the write goes through it.
// FIX 2: `for i in 0..table.len()` -- no iterator, so no borrow is held.
// (A `return` right after the write is accepted: the borrow checker can see
// the iterator is never used again. A loop that CONTINUES is not.)
fn close_all(table: &mut [Option<u32>]) -> usize {
let mut closed = 0;
for (i, slot) in table.iter().enumerate() {
if slot.is_some() {
table[i] = None;
closed += 1;
}
}
closed
}

fn main() {
let mut fds = [Some(1), Some(1), None, Some(7)];
println!("closed {}", close_all(&mut fds));
}
Why, and 2 fixes e

The for loop's iterator holds a SHARED borrow of the whole table until the loop ends. table[i] = None writes into it while that borrow is alive. (On a Vec the same line is E0502 -- indexing a Vec is a method call.) This is exit() closing every descriptor; it is also 06r's alloc_slot.

  1. for (i, slot) in table.iter_mut().enumerate() { *slot = None; } -- one &mut per slot, handed out in turn, and the write goes through it.
  2. for i in 0..table.len() -- no iterator, so no borrow is held.

(A return right after the write is accepted: the borrow checker can see the iterator is never used again. A loop that CONTINUES is not.)

$ rustc --edition 2021 --emit=metadata -o /dev/null e0506_assign_while_iterating.rs
error[E0506]: cannot assign to `table[_]` because it is borrowed
|
15 | for (i, slot) in table.iter().enumerate() {
| ------------------------
| |
| `table[_]` is borrowed here
| borrow later used here
16 | if slot.is_some() {
17 | table[i] = None;
| ^^^^^^^^^^^^^^^ `table[_]` is assigned to here but it was already borrowed
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0506`.
exit status 1

E0596 cannot borrow *table as mutable, as it is behind a & reference

iter_mut on a &[T] parameter · fix: change the parameter, not the loop

$ ./show-errors.sh e0596
// error[E0596]: cannot borrow `*table` as mutable, as it is behind a `&`
// reference
//
// The loop is fine. The PARAMETER is wrong. `&[T]` bought the right to read;
// `iter_mut` needs the right to write, and no loop can add that. The caller
// may well have passed `&mut table` -- the signature threw the `mut` away.
//
// FIX 1: `table: &mut [Option<u32>]` -- the signature says what the body does.
// FIX 2: if it really only reads, use `iter()` and return what you found.
fn alloc_slot(table: &[Option<u32>], pid: u32) -> Option<usize> {
for (i, slot) in table.iter_mut().enumerate() {
if slot.is_none() {
*slot = Some(pid);
return Some(i);
}
}
None
}

fn main() {
let mut procs = [None; 4];
println!("{:?}", alloc_slot(&mut procs, 42));
}
Why, and 2 fixes e

The loop is fine. The PARAMETER is wrong. &[T] bought the right to read; iter_mut needs the right to write, and no loop can add that. The caller may well have passed &mut table -- the signature threw the mut away.

  1. table: &mut [Option<u32>] -- the signature says what the body does.
  2. if it really only reads, use iter() and return what you found.
$ rustc --edition 2021 --emit=metadata -o /dev/null e0596_iter_mut_behind_shared_ref.rs
error[E0596]: cannot borrow `*table` as mutable, as it is behind a `&` reference
|
11 | for (i, slot) in table.iter_mut().enumerate() {
| ^^^^^ `table` is a `&` reference, so the data it refers to cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference
|
10 | fn alloc_slot(table: &mut [Option<u32>], pid: u32) -> Option<usize> {
| +++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0596`.
exit status 1

E0599 no method named put found for mutable reference &mut S in the current scope

a trait method on an unbounded type parameter · fix: add S: Sink

$ ./show-errors.sh e0599
// error[E0599]: no method named `put` found for mutable reference `&mut S`
// in the current scope
//
// `S` is SOME type -- the body may assume only what the bound says, and there
// is no bound. This is not a missing `use`; rustc's help text says the fix:
// "perhaps you need to restrict type parameter `S` with it: `S: Sink`"
//
// FIX 1: `fn log_all<S: Sink>(sink: &mut S, ..)` -- the bound is the contract
// in both directions: what the body may call, what the caller must prove.
// FIX 2: `sink: &mut impl Sink` (same thing), or `&mut dyn Sink` (one copy).
trait Sink {
fn put(&mut self, bytes: &[u8]);
}

struct VecSink(Vec<u8>);

impl Sink for VecSink {
fn put(&mut self, bytes: &[u8]) {
self.0.extend_from_slice(bytes);
}
}

fn log_all<S>(sink: &mut S, lines: &[&[u8]]) {
for line in lines {
sink.put(line);
}
}

fn main() {
let mut v = VecSink(Vec::new());
log_all(&mut v, &[b"boot", b"ok"]);
println!("{}", v.0.len());
}
Why, and 2 fixes e

S is SOME type -- the body may assume only what the bound says, and there is no bound. This is not a missing use; rustc's help text says the fix: "perhaps you need to restrict type parameter S with it: S: Sink"

  1. fn log_all<S: Sink>(sink: &mut S, ..) -- the bound is the contract in both directions: what the body may call, what the caller must prove.
  2. sink: &mut impl Sink (same thing), or &mut dyn Sink (one copy).
$ rustc --edition 2021 --emit=metadata -o /dev/null e0599_no_bound_no_method.rs
error[E0599]: no method named `put` found for mutable reference `&mut S` in the current scope
|
25 | sink.put(line);
| ^^^ method not found in `&mut S`
|
= help: items from traits can only be used if the type parameter is bounded by the trait
help: the following trait defines an item `put`, perhaps you need to restrict type parameter `S` with it:
|
23 | fn log_all<S: Sink>(sink: &mut S, lines: &[&[u8]]) {
| ++++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0599`.
exit status 1