{
let name = argv.first().ok_or(ShellError::BadArgs)?;
let image = load(name)?; // load returns Err(ExecError) — ? wraps it, silently
Ok(image)
}
```
```text
run_cmd(&["nope"]) -> Err(Exec(NotFound)) nobody wrote the wrapping at the call
```
When the inner and outer error types match, `From` is the identity and you
never notice it was there.
---
## Where a `Result` Becomes a Number
flowchart TD
A["dirlookup — fs.rs\nResult of Inode or FsError"] -->|"?"| B["read_file — fs.rs\nResult of bytes or FsError"]
B -->|"match, exactly once"| C["sys_read — syscall.rs\ni64: 13, or -2 / -21 / -36"]
C -->|"a0: one register"| T["══ the trap boundary ══\nno types cross it, only a number"]
T --> U["ulib::read — lib.rs\nResult of usize or Error(-1)"]
U -->|"?"| P["cat.rs\nfn run(args) -> i32"]
---
## The Errno Boundary
```rust
pub fn sys_read(&self, name: &str) -> i64 {
match self.read_file(name) {
Ok(bytes) => bytes.len() as i64, // >= 0: success, and the value
Err(FsError::NotFound) => -2, // ENOENT
Err(FsError::IsADirectory) => -21, // EISDIR
Err(FsError::NameTooLong) => -36, // ENAMETOOLONG
}
}
```
A user program gets one register, `a0`. The convention (Linux's, and ours from
`50k` on): `>= 0` success, negative is minus an error number.
Collapse in exactly one place. Everything behind this
match speaks FsError; everything in front of it
speaks integers. Nothing else in the kernel ever spells 21.
rv6 today writes `Err(_) => -1`. The cause is lost at the wall — which is why
`ulib::Error` is a thin `i32` and not an enum: the kernel genuinely does not
say more.
**RUN** `cargo run --bin 10_errno_boundary` code + output ↗
---
## `unwrap`, `expect`, and the Dead Machine
```rust
let n = read_fd(&fds, 3).expect("fd 3 was just opened"); // honest: the line above proved it
let n = read_fd(&fds, 9).unwrap(); // panics: called `Result::unwrap()`
// on an `Err` value: BadFd
```
| where | a panic is |
|---|---|
| in a test | one red test, with a message |
| in `10c`, on the host | the process dies with exit code 101 |
| in the kernel | the word `panic` on the UART, and the machine stops |
**The rule:** `Result` for what the world did to you — a bad name, a full table,
a missing device. Panic for what *you* did to yourself — an index you just
proved is in range. `args.get(i).unwrap()` inside `for i in 1..args.len()` is
honest.
The siblings that never panic: `unwrap_or(default)`, `if let Ok(v) = ..`,
`match`, `.is_err()`, `.map(..)`.
---
## `#[must_use]`: the `-1` You Forgot
```rust
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
```
`Result` carries `#[must_use]`. Ignoring one is not an error — it is a warning
that says, in the source, "you did not look".
let _ = says "I looked, and I chose not to care", where the next
reader can see it. There is nothing an echo can do if the console
is gone. Say so.
This is `10c`'s one place for it — and `08_option_vs_result` builds with this
exact warning on purpose, so that everyone has seen it once.
---
# Part IV
## Bytes, argv, and a program that runs twice
---
## Four Types That Are Not Text
| type | size | it is |
|---|---|---|
| `u8` | 1 | one byte |
| `char` | 4 | one Unicode scalar value |
| `&str` | 16 | pointer + length, **UTF-8 guaranteed** |
| `&[u8]` | 16 | pointer + length, **no promise at all** |
```rust
let flag: &[u8; 2] = b"-n"; // a byte-string literal is an array reference
let s: &[u8] = flag; // ... and coerces to a slice
arg == b"-n" // slices compare byte by byte
arg == "-n" // error[E0277]: can't compare `[u8]` with `str`
```
The kernel boundary is defined in bytes: a UART sends bytes, a disk block is
bytes, the argv `exec` builds is NUL-terminated bytes. `&str` is bytes *plus a
promise* — and a promise has to be checked.
**RUN** `cargo run --bin 11_bytes_vs_strings` code + output ↗
---
## `from_utf8` Is a Check, and It Can Fail
```rust
pub fn from_utf8(v: &[u8]) -> Result<&str, Utf8Error> // a Result. Part III again.
```
```text
"café".len() = 5 bytes: 63 61 66 c3 a9
"café".chars().count() = 4
from_utf8(b"hello") -> Ok("hello")
from_utf8(&[0xff, 0x41]) -> Err(Utf8Error { valid_up_to: 0, .. })
from_utf8(&"café".as_bytes()[..4]) -> Err(Utf8Error { valid_up_to: 3, .. }) a chunk boundary
```
A `read()` that stops mid-character hands you exactly the last case. That is
why `Args::get` gives bytes and `Args::str` runs the check and says `None`.
Every byte of a multi-byte character is >= 0x80. A byte-wise
search for b'\n' (0x0a) can never land inside one.
Splitting a byte stream into lines is correct for free — and that is
ulib::Lines.
---
## A Command Is `argv` In, Bytes Out, a Number Back
```rust
fn run(args: Args) -> i32 // the whole interface of a Unix program
```
| piece | is |
|---|---|
| `args.len()` | `argc` |
| `args.get(0)` | `argv[0]`: the name you were invoked as |
| `args.get(i)` | `Option<&[u8]>` — bytes, and `None` past the end |
| fd 1 | the answer |
| fd 2 | the complaint, so `cmd > out` never puts "cannot open" in the data |
| the `i32` | `$?` |
```sh
$ basename /usr/lib/ ; echo $?
lib
0
$ basename ; echo $?
basename: missing operand <- fd 2
1
```
---
## Separator, Not Terminator
```rust
for i in 1..args.len() {
if i > 1 { let _ = write_all(STDOUT, b" "); } // the space goes BEFORE, except the first
let _ = write_all(STDOUT, args.get(i).unwrap()); // honest: 1..len is always in range
}
let _ = write_all(STDOUT, b"\n"); // exactly one, always
```
```text
echo a b -> "a b\n" n words, n - 1 spaces
echo a "" b -> "a b\n" the empty argument is still an argument
echo -> "\n" bare echo prints an empty line
```
Separator versus terminator is decided before the loop is written.
10c_echo_example's basename uses a terminator on
purpose — one \n after every operand — so the two rules sit side
by side.
---
## `write_all`, and the `Result` You May Discard
`write` may accept **fewer** bytes than it was given, and says so in its return.
A UART, a pipe, and a socket all do this to you:
```text
write(STDOUT, b"hello world\n") -> Ok(3) "hel" went. Nothing complained.
```
```rust
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)); } // a stalled descriptor, not progress
buf = &buf[n..]; // re-point the slice: no copy
}
Ok(())
}
```
`ulib`'s host harness never short-writes, so a bare `write` in your command
passes every test — and truncates on rv6. `10c_echo_example`'s harness can be
told to, and then it fails one.
**RUN** `cargo run --bin 12_argv_and_write_all` code + output ↗
---
## The Two-Line Ceremony
```rust
#![cfg_attr(target_os = "none", no_std, no_main)] // line 1
use ulib::{Args, STDOUT};
ulib::main!(run); // line 2
fn run(args: Args) -> i32 { .. } // no cfg anywhere below this
```
| when building for | `target_os` | line 1 does |
|---|---|---|
| your laptop | `"macos"` / `"linux"` | nothing |
| rv6 (`riscv64gc-unknown-none-elf`) | `"none"` | `no_std`: no standard library; `no_main`: no runtime calls `main` |
`target_os` is derived from the target triple you hand cargo. It **cannot**
disagree with what you are building. A cargo feature could — and the failure
mode is an unreadable wall of `no_std` link errors.
---
## What `main!` Expands To
```rust
// on the host // on rv6
fn main() { #[no_mangle] #[link_section = ".text.start"]
let owned = host_argv(); pub extern "C" fn _start(argc: usize,
let refs: Vec<&[u8]> = ..; argv: *const *const u8) -> ! {
let code = run(Args::from_slice(&refs)); let args = unsafe { argv_slices(argc, argv, ..) };
std::process::exit(code); exit(run(Args::from_slice(args)))
} }
```
Same `run`. On the host the runtime calls `main`; on rv6 there is no runtime,
so `_start` is placed first in the image by the linker script and `exec` jumps
to the first byte, with `argc`/`argv` read off the stack `exec` built.
Your command file contains neither half. That is the whole point of the
façade: the file you write on September 18 is the file that runs on your own
kernel in December.
---
## Testing Through the Seam
```rust
#[test]
fn joins_arguments_with_single_spaces() {
let out = testing::run(&["echo", "hello", "world"], super::run); // calls run() DIRECTLY
assert_eq!(out.out(), "hello world\n");
assert_eq!(out.code, 0);
}
```
No process is spawned. fd 1 and fd 2 are pointed at two `Vec`s, `run` is called
as a function, and the buffers are handed back. The source under test is the
source that ships — no `dyn Write` threaded through, no test-only path.
| `10c`'s four tests | argv | expects |
|---|---|---|
| normal | `echo hello world` | `"hello world\n"` |
| none | `echo` | `"\n"` |
| single | `echo solo` | `"solo\n"` — no trailing space |
| empty argument | `echo a "" b` | `"a b\n"` |
---
## The Nine Failures
| file | error | fix |
|---|---|---|
| `e0506_assign_while_iterating` | assigning `table[i]` inside `table.iter()` | `iter_mut` and `*slot` |
| `e0596_iter_mut_behind_shared_ref` | `iter_mut` on a `&[T]` parameter | change the parameter |
| `e0046_missing_required_method` | overrode the default, skipped the required | supply `put` |
| `e0599_no_bound_no_method` | a trait method on an unbounded `S` | add `S: Sink` |
| `e0038_not_dyn_compatible` | a generic method, then `&mut dyn Trait` | `where Self: Sized` |
| `e0107_one_type_per_call_site` | `FdTable` without its type argument | name it, or `dyn Sink` |
| `e0392_type_parameter_never_used` | a generic struct that never holds its `T` | hold one, or `PhantomData` |
| `e0308_option_is_not_result` | returned `find`'s `Option` from `lookup` | `.ok_or(e)` |
| `e0277_question_mark_needs_result` | `?` in a function returning `i64` | `match` at the boundary |
Predict each one before it scrolls past. Then fix each file two different ways.
**RUN** `./show-errors.sh` all the errors ↗
---
## Do This Next
1. `oslings run 06r_collections` and `oslings run 07r_traits` — due **Thursday**
2. `oslings run 08r_errors` and `oslings run 10c_echo` — due **Friday**
3. Read the tests first. They are the contract, and every one is a sentence.
All four are cargo test on your laptop. Stuck? oslings hint
gives you two hints; the third is the code, and it is not released until after
the session.
Each exercise has a worked companion in `inclass/week04/` — the same shape in a
different domain, complete and passing, with a mapping table:
```text
06r_collections_example/ a file-descriptor table cargo run --bin basic_table
07r_traits_example/ Sink + Allocator + trace cargo run --bin basic_trait
08r_errors_example/ a device registry cargo run --bin basic_result
10c_echo_example/ basename, on a ulib façade cargo run --bin basic_command -- a b
```
---
## Where This Lands in the Kernel
| this week | the kernel exercise |
|---|---|
| the lowest-free search over a fixed table | `sys_open` in `50k_file_descriptors` |
| `Scheduler` with `RoundRobin` behind it | the scheduler in `36k_scheduling` |
| `&mut dyn Out` | the shell in `46k_shell`, driven by the UART and by tests |
| `FsError` and `?` | `dirlookup` in `40k_filesystem` — the enum grows to eight variants |
| the one `match` that turns a `Result` into `-errno` | every syscall in `50k` |
| `putc`-shaped byte output | the UART driver in `45k_console` |
| `write_all` and `Lines` over `&[u8]` | `11c_cat` through `14c_head`, next week |
| a generic struct with a guard | `SpinLock` in `37k_spinlocks` — `lock` and `try_lock` are yours |
Every one of them is one of today's fourteen programs with the nouns changed.
---
## Summary
- **Write through the slot, not around it.** `iter_mut` gives one `&mut` per slot; `*slot = x` lands in the caller's array.
- **The signature decides the loop.** `&[T]` bought reading. Fix the parameter, not the body.
- **A trait is a promise; a default is written once**, against a `self` that does not exist yet.
- **The bound is the whole contract.** E0599 is a missing bound, not a missing `use`.
- **`dyn` is a fat pointer; generics are copies.** Storage versus inlining.
- **Absence is `Option`; failure is `Result`; `.ok_or` is the decision**, made in one place.
- **`?` all the way down, one `match` at the top** — the only place errno is spelled.
- **Bytes, not strings** — and `write_all`, always, with `let _ =` on the `Result` you cannot act on.