curayious Language Lab

Rust — lab notes

The working file. Kept for the wrong turns.


Installing without admin

No Rust here at the start, and no Visual Studio either. rustup is the official installer and needs no elevation. It writes to %USERPROFILE%\.rustup and %USERPROFILE%\.cargo and adds .cargo\bin to the user PATH. Wrapped as tools/install-rust.ps1 in the same shape as the Go and Elixir installers: download, check SHA-256, install, print the version.

It failed twice before it worked, both times on Windows PowerShell 5.1, not on rustup.

1. The checksum file comes back as bytes

checksum mismatch
  expected 54
  actual   6f4bef66261261fcb43131be8720bab817d403a09edec7455c371974b90bdb7e

54 is the ASCII code for 6, the first character of the real hash. The .sha256 file is served as application/octet-stream, so Invoke-WebRequest(...).Content is a byte[], and -split on a byte array splits its decimal values. Fix: save it with -OutFile and read it back with Get-Content -Raw.

2. rustup's progress messages are fatal

rustup-init.exe : info: profile set to minimal
    + FullyQualifiedErrorId : NativeCommandError

rustup writes info: lines to stderr. With $ErrorActionPreference = "Stop", PowerShell 5.1 turns the first stderr line of a native command into a terminating error. The install had already started, so this left a half-installed toolchain. Fix: set the preference to Continue around that one call and judge success by $LASTEXITCODE. Re-run with -Force.

The third run printed:

rustc 1.98.1 (48a229cea 2026-09-01)
cargo 1.98.1 (797e8a9bc 2026-08-05)

Why -gnu

The default Windows host is x86_64-pc-windows-msvc, which links with link.exe from the Visual Studio Build Tools. Those need admin to install. The x86_64-pc-windows-gnu host links with MinGW, and rustup ships a MinGW linker in the toolchain itself:

~/.rustup/toolchains/stable-x86_64-pc-windows-gnu/lib/rustlib/x86_64-pc-windows-gnu/bin/self-contained/
    GCC-WARNING.txt  dlltool.exe  ld.exe  libwinpthread-1.dll  x86_64-w64-mingw32-gcc.exe

GCC-WARNING.txt says that gcc.exe "cannot be used for compiling C files - it is only used as a linker". So it cannot replace MSYS2's gcc for the C track. The two coexist without conflict.

The OCaml track hit a Device Guard policy that blocks flexlink.exe. The worry was that the same policy would block this linker. It did not. The first cargo build --release finished in 2 seconds.

One oddity: immediately after a rebuild, running the new logagg.exe failed once with Permission denied. Two seconds later the same command worked. The likely cause is Defender scanning a freshly linked executable; it has not recurred, and verify.py has not hit it. If a verify run ever fails that way, re-run before debugging.

First build, first diff

$ cargo build --release
    Finished `release` profile [optimized] target(s) in 2.01s
$ ./target/release/logagg.exe ../../bench/data/access.log > out.txt; echo rc=$?
rc=4
$ cmp out.txt ../../bench/expected/logagg.txt && echo serial-identical
serial-identical

Identical on the first run. That is less impressive than it sounds: this is the seventh track, and SPEC 2's integer arithmetic removes the usual source of disagreement. The things that broke the first diff in earlier tracks (CRLF on stdout, %-20s not truncating) are handled by default in Rust. Stdout is not translated, and {:<20.20} truncates.

cargo clippy -- -W clippy::pedantic found two things:

cargo fmt then expanded split6's one-line array of six it.next()? onto six lines. It reads worse. rustfmt, like gofmt, is not a matter of taste, so it stays.

The three errors, reproduced

The README says Rust rejects the C track's bugs. These are the programs and the actual output from rustc 1.98.1. Each file is under 20 lines and was compiled with rustc --edition 2024.

E0502 — storing a borrowed key (the C track's Q3 bug)

let mut services: HashMap<&str, u64> = HashMap::new();
let mut buf = String::new();
loop {
    buf.clear();
    if reader.read_line(&mut buf).unwrap() == 0 { break; }
    let service = buf.split_ascii_whitespace().nth(1).unwrap_or("");
    *services.entry(service).or_default() += 1;   // the C track's bug, typed
}
error[E0502]: cannot borrow `buf` as mutable because it is also borrowed as immutable
  --> bug.rs:9:9
   |
 9 |         buf.clear();
   |         ^^^^^^^^^^^ mutable borrow occurs here
10 |         if reader.read_line(&mut buf).unwrap() == 0 { break; }
11 |         let service = buf.split_ascii_whitespace().nth(1).unwrap_or("");
   |                       --- immutable borrow occurs here
...
14 |     println!("{}", services.len());
   |                    -------- immutable borrow later used here

It reports the error at buf.clear(), not at the entry call. The mistake is storing the key; the error appears where the stored borrow collides with the next mutation. Reading from the bottom up (line 14, then 11, then 9) is the useful direction. That tells you where the borrow is still alive and why.

A second E0502 follows for read_line(&mut buf), same cause.

E0597 — thread::spawn instead of thread::scope

let text = std::fs::read_to_string("access.log").unwrap();
let lines: Vec<&str> = text.lines().collect();
let handles: Vec<_> = lines
    .chunks(50)
    .map(|chunk| thread::spawn(move || count(chunk)))   // no scope
    .collect();
error[E0597]: `text` does not live long enough
...
10 |         .map(|chunk| thread::spawn(move || count(chunk)))   // no scope
   |                      ----------------------------------- argument requires that `text` is borrowed for `'static`
...
13 | }
   | - `text` dropped here while still borrowed

'static means "for the rest of the program". spawn demands it because a spawned thread may outlive the function that started it. The two usual fixes are to clone the data into each thread or wrap it in Arc. thread::scope removes the demand instead.

E0499 — one map, two threads

let mut services: HashMap<String, u64> = HashMap::new();
thread::scope(|s| {
    for _ in 0..2 {
        s.spawn(|| *services.entry("api".into()).or_default() += 1);  // one shared map, no lock
    }
});
error[E0499]: cannot borrow `services` as mutable more than once at a time

In C this compiles and is undefined behaviour. The Rust fix is either a Mutex around the map or a map per thread. logagg uses a map per thread.

--shard N

No earlier track implemented SPEC 6. Here it is about 33 lines: tally_sharded and Tally::merge. The check:

$ for n in 1 2 3 4 7 16 64 500; do
>   ./target/release/logagg.exe --shard $n access.log | cmp - logagg.txt && echo "shard $n identical"
> done
shard 1 identical
...
shard 500 identical

500 is more shards than the file has lines (200). lines.len().div_ceil(500) is 1, so it runs 200 workers with one line each, which exercises the merge more than any realistic count. The line numbers in the malformed: block stay right because each worker numbers its lines from chunk_index * per + 1, and the merge appends the workers' lists in chunk order.

verify.py does not run the shard path. It tests the contract every track shares. If a second track implements --shard, that is the time to add it to the verifier.

Where the Rust version differs from the Python reference

Three edge cases the fixture does not reach. Recorded so they are not rediscovered:

Two things worth knowing next time