From 4990e7b76c6e1db2421b7cfc7efa33e505fdf0bd Mon Sep 17 00:00:00 2001 From: sanohiro Date: Sat, 1 Aug 2026 15:43:59 +0900 Subject: [PATCH] feat(sema): require mutable buffer locals for native fills --- HANDOFF.md | 29 +++- .../align_driver/tests/capability_linking.rs | 2 +- crates/align_driver/tests/m11_crypto.rs | 40 ++--- crates/align_driver/tests/m11_net.rs | 26 ++- crates/align_driver/tests/m12_file_io.rs | 4 +- crates/align_driver/tests/m12_read_line.rs | 33 ++-- crates/align_driver/tests/m9_fs.rs | 2 +- crates/align_driver/tests/m9_io.rs | 31 ++-- crates/align_driver/tests/net_timeout.rs | 4 +- crates/align_sema/src/lib.rs | 156 ++++++++++++++++-- docs/design-notes.md | 3 + docs/guide/02-language-basics.md | 1 + docs/guide/13-std-os.md | 6 +- docs/guide/ja/02-language-basics.md | 1 + docs/guide/ja/13-std-os.md | 6 +- docs/impl/07-roadmap.md | 2 +- ...2-pipeline-closure-memory-io-simd-audit.md | 2 +- docs/impl/std-design/ja/net.md | 2 +- docs/impl/std-design/net.md | 2 +- docs/language-spec.md | 4 + docs/little-aligner/11-do-it-until.md | 4 +- docs/little-aligner/ja/11-do-it-until.md | 4 +- docs/open-questions.md | 8 + draft.md | 11 +- examples/file_copy.align | 20 +-- 25 files changed, 293 insertions(+), 110 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index b119ca91..24a281a1 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -5,8 +5,8 @@ about the present state, the next decision, and operational facts. The former per-PR journal is preserved in [`docs/archive/HANDOFF-2026-07-25.md`](docs/archive/HANDOFF-2026-07-25.md). -_Last updated: 2026-08-01. `main` includes the shipped wave through #683. L2b-a2-am-f -return completeness is complete; am-w task-wait dominance is next. +_Last updated: 2026-08-01. `main` includes the shipped wave through #685. L2b-a2-am-w +task-wait dominance is complete; am-v native output-buffer mutability is next. #667 adds the canonical recursive Drop plan and sound `Option` fields; #668 admits one direct recursively Move payload per tagged arm; #669 admits multiple Move payloads; #670 completes nested tagged payload representation and the exact pkg.db L1b acceptance shape. @@ -100,6 +100,7 @@ facts must live in this repository. #673 named/direct/imported parameter-root inference #674 product return-provenance refinement #675 eager MIR continuation closure +#685 path-complete task-wait dominance ``` #639 fixes Unit-call values across direct, indirect, pipeline, and per-unit @@ -301,6 +302,30 @@ Required verification was slow but progressing: the final locked all-target Clip 11m56s, and several test binaries had their known long startup delay. Those timings do not justify weakening the gate. +### PR #685 delivery retrospective + +PR #685 was squash-merged as `257c704`. Its earliest-commit-to-merge proxy was 5h17m; +the public PR was open for 4h18m. The implementation changed 4 files by +1,042/-18, +including the complete compiler-only task-wait proof checker and its owner tests. The +vertical remained intentionally atomic because splitting proof state, control joins, or +TaskGet diagnostics could authorize an uninitialized task result or reject an outer proof. + +The avoidable delay was review execution and checkpoint churn, not the final compiler fix. +The first host review spent its bound reading the large repository context and repeatedly +hit macOS `xcodebuild` cache errors. A later host pass found a real P1: join tokens were +keyed by incoming generation/epoch, so loop headers could allocate a new token on every +state change and miss the required fixed point. The fix made join generation/epoch tokens +stable by syntax site, then replaced an invalid owner fixture that tried to move a Task +directly out of a conditional expression with a legal branch/loop reassignment twin. + +Durable rules: a review that has no verdict is never a clean result; inspect process state, +log growth, and the last completed action, then stop at the hard bound and rerun against a +small explicit complete patch. For control-flow token interning, keys that define a join +identity are syntax-site-only; incoming state may key Spawn/Wait/Err events but must not +create fresh identities for the same loop or branch join. Finally, every owner fixture must +be legal under the language's own Move/arena rules before it can test compiler-only proof +transport. + #660's final verification records 48/48 `align_driver` `par_map` tests, including 65,537-element worker-range tests for both materializing chunks and direct chunk reduction, a chunk filter, a cross-worker i8 wrapping fold, and diff --git a/crates/align_driver/tests/capability_linking.rs b/crates/align_driver/tests/capability_linking.rs index f0031e86..4ce5d3c6 100644 --- a/crates/align_driver/tests/capability_linking.rs +++ b/crates/align_driver/tests/capability_linking.rs @@ -83,7 +83,7 @@ fn http_client_requests_the_full_tls_set() { fn ct_equal_and_random_touch_no_library() { // `crypto.ct_equal` is a constant-time byte compare and `crypto.random` is OS getrandom — neither // links libcrypto. This guards against over-linking the whole `std.crypto` surface. - let src = "import std.crypto\nfn main() -> Result<(), Error> {\n b := buffer(16)\n crypto.random(b)\n print(b.len() as i64)\n return Ok(())\n}\n"; + let src = "import std.crypto\nfn main() -> Result<(), Error> {\n mut b := buffer(16)\n crypto.random(b)\n print(b.len() as i64)\n return Ok(())\n}\n"; assert_eq!(gated_link_libs("cap-rand", src), Vec::::new()); } diff --git a/crates/align_driver/tests/m11_crypto.rs b/crates/align_driver/tests/m11_crypto.rs index 1dda395a..9475d805 100644 --- a/crates/align_driver/tests/m11_crypto.rs +++ b/crates/align_driver/tests/m11_crypto.rs @@ -130,7 +130,7 @@ fn random_fills_and_two_fills_differ() { import std.crypto import std.encoding pub fn main() -> Result<(), Error> { - b := buffer(32) + mut b := buffer(32) crypto.random(b) print(b.len()) h1 := encoding.hex_encode(b.bytes()) @@ -156,7 +156,7 @@ fn random_large_fill_not_all_zero() { import std.crypto import std.encoding pub fn main() -> Result<(), Error> { - b := buffer(4096) + mut b := buffer(4096) crypto.random(b) print(b.len()) print(encoding.hex_encode(b.bytes())) @@ -180,7 +180,7 @@ fn random_impure_rejected_by_par_map() { let src = "\ import std.crypto fn f(x: i64) -> i64 { - b := buffer(8) + mut b := buffer(8) crypto.random(b) return x + b.len() } @@ -210,7 +210,7 @@ pub fn main() -> Result<(), Error> { assert!(check_errs("m11cr-noimport", src), "crypto.* without `import std.crypto` must error"); let diags = check_diagnostics( "m11cr-diag", - "pub fn main() -> Result<(), Error> {\n b := buffer(8)\n crypto.random(b)\n return Ok(())\n}\n", + "pub fn main() -> Result<(), Error> {\n mut b := buffer(8)\n crypto.random(b)\n return Ok(())\n}\n", ); assert!(diags.contains("import std.crypto"), "diagnostic should name the capability: {diags}"); } @@ -344,13 +344,13 @@ fn sha256_large_input_deterministic() { let prog = "\ import std.crypto pub fn main() -> Result<(), Error> { - b := buffer(1048576) + mut b := buffer(1048576) crypto.random(b) print(b.len()) // the input is 1 MiB d1 := crypto.sha256(b.bytes()) d2 := crypto.sha256(b.bytes()) print(crypto.constant_time_equal(d1[0..d1.len()], d2[0..d2.len()])) // same input → same digest - c := buffer(1048576) + mut c := buffer(1048576) crypto.random(c) e := crypto.sha256(c.bytes()) print(crypto.constant_time_equal(d1[0..d1.len()], e[0..e.len()])) // different input → differ @@ -697,9 +697,9 @@ fn aead_round_trips_both_ciphers() { let prog = "\ import std.crypto pub fn main() -> Result<(), Error> { - k := buffer(32) + mut k := buffer(32) crypto.random(k) - n := buffer(12) + mut n := buffer(12) crypto.random(n) // Empty plaintext → 16-byte tag-only output; opens back to empty. s0 := crypto.aes_gcm_seal(k.bytes(), n.bytes(), \"\", \"aad\")? @@ -711,7 +711,7 @@ pub fn main() -> Result<(), Error> { o1 := crypto.chacha20_poly1305_open(k.bytes(), n.bytes(), s1.bytes(), \"\")? print(crypto.constant_time_equal(o1.bytes(), \"hello world\")) // Large ~1 MiB plaintext round-trips (aes). - big := buffer(1048576) + mut big := buffer(1048576) crypto.random(big) s2 := crypto.aes_gcm_seal(k.bytes(), n.bytes(), big.bytes(), \"meta\")? print(s2.len()) @@ -745,12 +745,12 @@ fn aead_open_failures_are_invalid_and_yield_nothing() { "\ import std.crypto pub fn main() -> Result<(), Error> {{ - k := buffer(32) + mut k := buffer(32) crypto.random(k) - n := buffer(12) + mut n := buffer(12) crypto.random(n) sealed := crypto.aes_gcm_seal(k.bytes(), n.bytes(), \"top secret\", \"ctx\")? - wrong := buffer(32) + mut wrong := buffer(32) crypto.random(wrong) pt := {open_expr}? print(pt.len()) @@ -792,9 +792,9 @@ fn aead_cross_cipher_confusion_is_invalid() { let prog = "\ import std.crypto pub fn main() -> Result<(), Error> { - k := buffer(32) + mut k := buffer(32) crypto.random(k) - n := buffer(12) + mut n := buffer(12) crypto.random(n) sealed := crypto.aes_gcm_seal(k.bytes(), n.bytes(), \"secret\", \"aad\")? pt := crypto.chacha20_poly1305_open(k.bytes(), n.bytes(), sealed.bytes(), \"aad\")? @@ -820,9 +820,9 @@ fn aead_wrong_key_or_nonce_length_is_invalid() { "\ import std.crypto pub fn main() -> Result<(), Error> {{ - k := buffer(64) + mut k := buffer(64) crypto.random(k) - input := buffer(64) + mut input := buffer(64) crypto.random(input) r := crypto.{op}(k.bytes()[0..{key_slice}], k.bytes()[0..{nonce_slice}], input.bytes(), \"aad\")? print(r.len()) @@ -861,9 +861,9 @@ fn aead_impure_rejected_by_par_map() { let seal_src = "\ import std.crypto fn f(x: i64) -> i64 { - k := buffer(32) + mut k := buffer(32) crypto.random(k) - n := buffer(12) + mut n := buffer(12) crypto.random(n) r := crypto.aes_gcm_seal(k.bytes(), n.bytes(), \"pt\", \"\") return x + match r { Ok(b) => b.len(), Err(_) => 0 } @@ -881,9 +881,9 @@ pub fn main() -> i32 { let open_src = "\ import std.crypto fn f(x: i64) -> i64 { - k := buffer(32) + mut k := buffer(32) crypto.random(k) - n := buffer(12) + mut n := buffer(12) crypto.random(n) r := crypto.chacha20_poly1305_open(k.bytes(), n.bytes(), \"0123456789abcdef\", \"\") return x + match r { Ok(b) => b.len(), Err(_) => 0 } diff --git a/crates/align_driver/tests/m11_net.rs b/crates/align_driver/tests/m11_net.rs index e68c6355..8573e46e 100644 --- a/crates/align_driver/tests/m11_net.rs +++ b/crates/align_driver/tests/m11_net.rs @@ -174,7 +174,7 @@ pub fn main(args: array) -> Result<(), Error> { w := conn.writer() w.write(\"ping\\n\")? r := conn.reader() - b := buffer(64) + mut b := buffer(64) n := r.read(b)? print(n) io.stdout.write(b.bytes())? @@ -362,13 +362,13 @@ pub fn main(args: array) -> Result<(), Error> { conn1 := l.accept()? r1 := conn1.reader() w1 := conn1.writer() - b1 := buffer(64) + mut b1 := buffer(64) n1 := r1.read(b1)? w1.write(b1.bytes())? conn2 := l.accept()? r2 := conn2.reader() w2 := conn2.writer() - b2 := buffer(64) + mut b2 := buffer(64) n2 := r2.read(b2)? w2.write(b2.bytes())? return Ok(()) @@ -531,7 +531,7 @@ fn make(p: str) -> Result { } pub fn main(args: array) -> Result<(), Error> { r := make(args[1])? - buf := buffer(4) + mut buf := buffer(4) n := r.read(buf)? print(n) return Ok(()) @@ -546,14 +546,12 @@ pub fn main(args: array) -> Result<(), Error> { ); } -/// Adversarial-review F1: the conservative counterpart to the test above. `open_it`'s own reader is -/// an unrelated fixed-path `fs.open` — it borrows nothing from `tag` — but `region_of(Call)` has no -/// per-fn "does this borrow arg i" fact, so it folds in *every* argument's region regardless. Passing -/// a `Frame`-region argument (a `string` local auto-borrowed to `str`, MMv2 slice 7b) taints the call -/// result, so returning it out of `steal` (past the frame) is conservatively rejected — sound (never -/// miscompiles), just imprecise. (Documented on `tracks_region`'s `Reader | Writer` arm.) +/// Adversarial-review F1: a direct constructor's named return summary is `None`, so an unrelated +/// frame-region argument does not taint the returned owned reader. This is the precise counterpart +/// to the static-argument constructor test above: only a callee that explicitly returns a view of an +/// argument contributes that argument's region to the call result. #[test] -fn reader_through_call_with_frame_arg_conservatively_rejected() { +fn reader_through_call_with_frame_arg_direct_constructor_is_precise() { let src = "\ import std.fs fn open_it(tag: str) -> Result { @@ -571,8 +569,8 @@ pub fn main() -> Result<(), Error> { } "; assert!( - check_errs("m11net-reader-call-frame-arg", src), - "a reader returned through a user call with a Frame-region argument must be conservatively rejected" + !check_errs("m11net-reader-call-frame-arg", src), + "an unrelated frame-region argument must not taint a direct owned-reader constructor" ); } @@ -613,7 +611,7 @@ pub fn main(args: array) -> Result<(), Error> { c.flag_i64(\"peer\", 0) p := c.parse(args)? u := udp.bind(\"127.0.0.1\", p.get_i64(\"port\"))? - b := buffer(64) + mut b := buffer(64) n := u.recv_from(b)? u.send_to(b.bytes(), \"127.0.0.1\", p.get_i64(\"peer\"))? return Ok(()) diff --git a/crates/align_driver/tests/m12_file_io.rs b/crates/align_driver/tests/m12_file_io.rs index 74a9026c..f828c8e9 100644 --- a/crates/align_driver/tests/m12_file_io.rs +++ b/crates/align_driver/tests/m12_file_io.rs @@ -54,7 +54,7 @@ pub fn main(args: array) -> Result<(), Error> { f.pwrite(\"Hello, \", 0)? f.pwrite(\"World!\", 7)? f.pwrite(\"Z\", 20)? - buf := buffer(6) + mut buf := buffer(6) n := f.pread(buf, 7)? io.stdout.write(buf.bytes())? return Ok(()) @@ -257,6 +257,6 @@ fn file_constructors_require_std_fs_import() { // With the import, a bound file's methods type-check (regression: the gates don't over-reach). assert!(!check_errs( "m12-file-bound-ok", - "import std.fs\nimport std.io\npub fn main(args: array) -> Result<(), Error> {\n f := fs.create_rw(args[1])?\n f.pwrite(\"hi\", 0)?\n buf := buffer(2)\n n := f.pread(buf, 0)?\n print(f.len()?)\n return Ok(())\n}\n", + "import std.fs\nimport std.io\npub fn main(args: array) -> Result<(), Error> {\n f := fs.create_rw(args[1])?\n f.pwrite(\"hi\", 0)?\n mut buf := buffer(2)\n n := f.pread(buf, 0)?\n print(f.len()?)\n return Ok(())\n}\n", ), "a bound file's pwrite/pread/len must stay allowed"); } diff --git a/crates/align_driver/tests/m12_read_line.rs b/crates/align_driver/tests/m12_read_line.rs index 5bc30126..232bf081 100644 --- a/crates/align_driver/tests/m12_read_line.rs +++ b/crates/align_driver/tests/m12_read_line.rs @@ -50,7 +50,7 @@ import std.io pub fn main(args: array) -> Result<(), Error> { base := fs.open(args[1])? r := base.buffered() - buf := buffer(8) + mut buf := buffer(8) w := io.stdout mut acc: array_builder := array_builder() loop { @@ -92,7 +92,7 @@ import std.io pub fn main(args: array) -> Result<(), Error> { base := fs.open(args[1])? r := base.buffered() - buf := buffer(64) + mut buf := buffer(64) w := io.stdout n := r.read_line(buf)? line := buf.bytes().as_str()? @@ -133,7 +133,7 @@ import std.io pub fn main(args: array) -> Result<(), Error> { base := fs.open(args[1])? r := base.buffered() - buf := buffer(64) + mut buf := buffer(64) n := r.read_line(buf)? print(n) w := fs.create(args[2])? @@ -179,7 +179,7 @@ import std.io pub fn main(args: array) -> Result<(), Error> { base := fs.open(args[1])? r := base.buffered() - buf := buffer(16) + mut buf := buffer(16) n := r.read_line(buf)? line := buf.bytes().as_str()? print(line.len()) @@ -206,7 +206,7 @@ import std.io pub fn main(args: array) -> Result<(), Error> { base := fs.open(args[1])? r := base.buffered() - buf := buffer(8) + mut buf := buffer(8) mut count := 0 loop { n := r.read_line(buf)? @@ -237,7 +237,7 @@ import std.io pub fn main(args: array) -> Result<(), Error> { base := fs.open(args[1])? r := base.buffered() - buf := buffer(8) + mut buf := buffer(8) n := r.read_line(buf)? print(buf.len()) print(n) @@ -266,7 +266,7 @@ Rec { n: i64 } pub fn main(args: array) -> Result<(), Error> { base := fs.open(args[1])? r := base.buffered() - buf := buffer(8) + mut buf := buffer(8) mut total := 0 loop { k := r.read_line(buf)? @@ -294,7 +294,7 @@ import std.fs import std.io pub fn main(args: array) -> Result<(), Error> { r := fs.open(args[1])? - buf := buffer(8) + mut buf := buffer(8) n := r.read_line(buf)? print(n) return Ok(()) @@ -322,7 +322,7 @@ import std.io pub fn main(args: array) -> Result<(), Error> { base := fs.open(args[1])? r := base.buffered() - buf := buffer(8) + mut buf := buffer(8) n := r.read_line(buf)? line := buf.bytes().as_str()? print(line.len()) @@ -364,17 +364,18 @@ fn unbuffered_read_path_unchanged() { let prog = "\ import std.fs import std.io -fn drain(r: reader, buf: buffer, w: writer) -> Result<(), Error> { - n := r.read(buf)? - if n == 0 { return Ok(()) } - w.write(buf.bytes())? - return drain(r, buf, w) +fn drain(r: reader, w: writer) -> Result<(), Error> { + mut buf := buffer(4) + loop { + n := r.read(buf)? + if n == 0 { break Ok(()) } + w.write(buf.bytes())? + } } pub fn main(args: array) -> Result<(), Error> { r := fs.open(args[1])? - buf := buffer(4) w := io.stdout - drain(r, buf, w)? + drain(r, w)? return Ok(()) } "; diff --git a/crates/align_driver/tests/m9_fs.rs b/crates/align_driver/tests/m9_fs.rs index 91acb356..f3eec045 100644 --- a/crates/align_driver/tests/m9_fs.rs +++ b/crates/align_driver/tests/m9_fs.rs @@ -126,7 +126,7 @@ import std.fs import std.io pub fn main(args: array) -> Result<(), Error> { r := fs.open(args[1])? - buf := buffer(64) + mut buf := buffer(64) n := r.read(buf)? fs.write_file(args[2], buf.bytes())? return Ok(()) diff --git a/crates/align_driver/tests/m9_io.rs b/crates/align_driver/tests/m9_io.rs index 298d3e20..5a4b4046 100644 --- a/crates/align_driver/tests/m9_io.rs +++ b/crates/align_driver/tests/m9_io.rs @@ -52,17 +52,18 @@ fn file_copy_byte_exact_through_read_write_loop() { let prog = "\ import std.fs import std.io -fn copy_all(r: reader, w: writer, buf: buffer) -> Result<(), Error> { - n := r.read(buf)? - if n == 0 { return Ok(()) } - w.write(buf.bytes())? - return copy_all(r, w, buf) +fn copy_all(r: reader, w: writer) -> Result<(), Error> { + mut buf := buffer(4) + loop { + n := r.read(buf)? + if n == 0 { break Ok(()) } + w.write(buf.bytes())? + } } pub fn main(args: array) -> Result<(), Error> { r := fs.open(args[1])? w := fs.create(args[2])? - buf := buffer(4) - copy_all(r, w, buf)? + copy_all(r, w)? return Ok(()) } "; @@ -88,7 +89,7 @@ import std.io pub fn main(args: array) -> Result<(), Error> { r := fs.open(args[1])? w := fs.create(args[2])? - buf := buffer(65536) + mut buf := buffer(65536) n := r.read(buf)? print(n) w.write(buf.bytes())? @@ -114,7 +115,7 @@ import std.fs import std.io pub fn main(args: array) -> Result<(), Error> { r := fs.open(args[1])? - buf := buffer(16) + mut buf := buffer(16) n := r.read(buf)? print(n) return Ok(()) @@ -209,7 +210,7 @@ fn stdin_reader_reads_from_fd_zero() { import std.io pub fn main() -> Result<(), Error> { r := io.stdin - buf := buffer(64) + mut buf := buffer(64) n := r.read(buf)? print(n) return Ok(()) @@ -262,7 +263,7 @@ import std.io pub fn main(args: array) -> Result<(), Error> { match fs.open(args[1]) { Ok(r) => { - buf := buffer(4) + mut buf := buffer(4) n := r.read(buf)? print(n) return Ok(()) @@ -292,7 +293,7 @@ fn reusing_a_moved_reader_is_rejected() { import std.fs import std.io fn count(r: reader) -> Result { - buf := buffer(8) + mut buf := buffer(8) return r.read(buf) } pub fn main(args: array) -> Result<(), Error> { @@ -438,7 +439,7 @@ import std.io pub fn main(args: array) -> Result<(), Error> { n := arena { r := fs.open(args[1])? - buf := buffer(3) + mut buf := buffer(3) r.read(buf)? } print(n) @@ -546,7 +547,7 @@ pub fn main(args: array) -> Result<(), Error> { w := fs.create(args[2])? n := io.copy(r, w)? print(n) - buf := buffer(8) + mut buf := buffer(8) m := r.read(buf)? print(m) w.write(\"!\")? @@ -696,7 +697,7 @@ pub fn main(args: array) -> Result<(), Error> { w.flush()? print(n) sin := io.stdin - b := buffer(4) + mut b := buffer(4) k := sin.read(b)? return Ok(()) } diff --git a/crates/align_driver/tests/net_timeout.rs b/crates/align_driver/tests/net_timeout.rs index 5cc99838..53f77ba4 100644 --- a/crates/align_driver/tests/net_timeout.rs +++ b/crates/align_driver/tests/net_timeout.rs @@ -44,7 +44,7 @@ pub fn main(args: array) -> Result<(), Error> { conn := tcp.connect(\"127.0.0.1\", p.get_i64(\"port\"))? conn.read_timeout_ns(200000000) r := conn.reader() - b := buffer(64) + mut b := buffer(64) code := match r.read(b) { Ok(n) => 0, Err(e) => match e { @@ -102,7 +102,7 @@ pub fn main(args: array) -> Result<(), Error> { w := conn.writer() w.write(\"ping\\n\")? r := conn.reader() - b := buffer(64) + mut b := buffer(64) n := r.read(b)? print(n) io.stdout.write(b.bytes())? diff --git a/crates/align_sema/src/lib.rs b/crates/align_sema/src/lib.rs index 4fb60513..3d09f45d 100644 --- a/crates/align_sema/src/lib.rs +++ b/crates/align_sema/src/lib.rs @@ -9294,16 +9294,13 @@ impl<'a> EscapeCheck<'a> { // An *owned* reader/writer constructed **directly** by a builtin (`io.stdin`/`fs.open`/ // `io.stdout`/`fs.create`) has region `Static` (its `region_of` producer — `ReaderOpen` / // `WriterCreate` — is classified `Static`), so it stays freely returnable. - // But a reader/writer threaded through a **user** function call is not so lucky: - // `region_of(Call)` conservatively folds in *every* argument's region (it has no per-fn - // "does this actually borrow arg i" fact), so calling that user fn with a Frame/Arena- - // region argument taints the whole result — even when the callee's own reader is an - // unrelated direct `fs.open`. Returning that call's result past the tainted region is - // then rejected, even though nothing is actually borrowed. This is sound (never - // miscompiles) but imprecise; the precise fix needs interprocedural return-borrow - // summaries, independently of the intraprocedural region-flow CFG. This arm only makes - // the escape check *consult* the region either way. (A `tcp_conn` itself is always - // owned, never a borrow, so it is deliberately NOT here.) + // A reader/writer threaded through a **user** function call consults the inferred + // named return-region summary: a callee that returns a view of parameter i contributes + // only argument i, a direct owned constructor publishes `None`, and an unavailable + // summary conservatively folds every argument. This keeps unrelated frame/arena + // arguments from tainting a direct `fs.open` result while still failing closed for an + // unknown borrowing boundary. (A `tcp_conn` itself is always owned, never a borrow, + // so it is deliberately NOT here.) Ty::Reader | Ty::Writer | Ty::Fn(_) => return true, // Scalar/register values and owned handles carry no inferred borrow region. This list // is exhaustive so every future type must make an explicit escape-analysis choice. @@ -27233,6 +27230,9 @@ impl<'a, 't> Checker<'a, 't> { self.diags.error(format!("'.recv_from()' fills a buffer, got {}", ty_name(buffer.ty)), args[0].span); return err; } + if !self.require_mut_buffer_local(&args[0], ".recv_from()") { + return err; + } return Expr { kind: ExprKind::UdpRecvFrom { sock: Box::new(recv_expr), buffer: Box::new(buffer) }, ty: i64_result, @@ -27968,8 +27968,8 @@ impl<'a, 't> Checker<'a, 't> { span, }; } - // `random(out)` — fill a `buffer` (mirrors `reader.read`'s mut-buffer argument: any expr of - // type `buffer`, filled in place, not consumed). + // `random(out)` — fill a `buffer` (mirrors `reader.read`'s mut-buffer argument: only a bare + // mutable local, filled in place, not consumed). if args.len() != 1 { self.diags .error(format!("'crypto.random' expects 1 argument (a mut buffer), got {}", args.len()), span); @@ -27984,6 +27984,9 @@ impl<'a, 't> Checker<'a, 't> { .error(format!("'crypto.random' fills a buffer, got {}", ty_name(out.ty)), args[0].span); return err; } + if !self.require_mut_buffer_local(&args[0], "crypto.random") { + return err; + } Expr { kind: ExprKind::CryptoRandom { out: Box::new(out) }, ty: Ty::Unit, span } } @@ -29522,6 +29525,27 @@ impl<'a, 't> Checker<'a, 't> { } } + /// Require a native output buffer to be a bare mutable local. The operation-specific type and + /// arity checks run first so this helper never masks an earlier child/type diagnostic. + fn require_mut_buffer_local(&mut self, arg: &ast::Expr, operation: &str) -> bool { + let Some((id, _)) = self.place_local(arg) else { + self.diags.error( + format!("'{operation}' needs a mut buffer local (bind it first, then pass that local)"), + arg.span, + ); + return false; + }; + if !self.locals[id as usize].is_mut { + let name = self.locals[id as usize].name.clone(); + self.diags.error( + format!("cannot fill immutable buffer '{name}' in '{operation}' (declare with mut)"), + arg.span, + ); + return false; + } + true + } + /// `r.read(b: mut buffer)` on a `reader` ([`Ty::Reader`]), the receiver already evaluated. Fills /// `b` up to its capacity (overwriting its length), yielding `Result` (bytes read; /// `0` = EOF). Borrows both reader and buffer (neither consumed). @@ -29555,6 +29579,9 @@ impl<'a, 't> Checker<'a, 't> { self.diags.error(format!("'.read()' fills a buffer, got {}", ty_name(buffer.ty)), args[0].span); return err; } + if !self.require_mut_buffer_local(&args[0], ".read()") { + return err; + } Expr { kind: ExprKind::ReaderRead { reader: Box::new(recv_expr), buffer: Box::new(buffer) }, ty: Ty::Result(Scalar::Int(IntTy { bits: 64, signed: true }), Scalar::Enum(self.error_enum_id)), @@ -29602,6 +29629,9 @@ impl<'a, 't> Checker<'a, 't> { self.diags.error(format!("'.read_line()' fills a buffer, got {}", ty_name(buffer.ty)), args[0].span); return err; } + if !self.require_mut_buffer_local(&args[0], ".read_line()") { + return err; + } Expr { kind: ExprKind::ReaderReadLine { reader: Box::new(recv_expr), buffer: Box::new(buffer) }, ty: Ty::Result(Scalar::Int(IntTy { bits: 64, signed: true }), Scalar::Enum(self.error_enum_id)), @@ -29675,6 +29705,9 @@ impl<'a, 't> Checker<'a, 't> { if !self.require_i64_arg(offset.ty, off_arg.span, "'.pread()' offset") { return err; } + if !self.require_mut_buffer_local(buf_arg, ".pread()") { + return err; + } Expr { kind: ExprKind::FilePread { file: Box::new(recv_expr), buffer: Box::new(buffer), offset: Box::new(offset) }, ty: i64_result, @@ -34532,6 +34565,105 @@ fn main() -> i32 = 0 } } + #[test] + fn native_output_buffers_require_mut_locals_at_all_five_sites() { + let cases = [ + ( + ".read()", + "import std.fs\nfn main() -> Result<(), Error> {\n r := fs.open(\"input\")?\n", + "r.read(ARG)?", + ), + ( + ".read_line()", + "import std.fs\nfn main() -> Result<(), Error> {\n r := fs.open(\"input\")?\n br := r.buffered()\n", + "br.read_line(ARG)?", + ), + ( + ".pread()", + "import std.fs\nfn main() -> Result<(), Error> {\n f := fs.open_rw(\"input\")?\n", + "f.pread(ARG, 0)?", + ), + ( + ".recv_from()", + "import std.net\nfn main() -> Result<(), Error> {\n u := udp.bind(\"127.0.0.1\", 1)?\n", + "u.recv_from(ARG)?", + ), + ( + "crypto.random", + "import std.crypto\nfn main() -> Result<(), Error> {\n", + "crypto.random(ARG)\n", + ), + ]; + for (operation, prefix, call) in cases { + let source = format!( + "{prefix} mut b := buffer(4)\n {}\n return Ok(())\n}}\n", + call.replace("ARG", "\"not a buffer\"") + ); + let (_, diagnostics) = check(&source); + assert!( + diagnostics.has_errors(), + "{operation} with a wrong buffer type must fail" + ); + assert!( + !diagnostics.iter().any(|diagnostic| { + diagnostic.message.contains("needs a mut buffer local") + || diagnostic.message.contains("cannot fill immutable buffer") + }), + "{operation} must preserve the earlier wrong-type diagnostic: {:?}", + diagnostics.iter().map(|diagnostic| &diagnostic.message).collect::>() + ); + + for (label, arg, expected) in [ + ( + "non-local", + "buffer(4)", + format!("'{operation}' needs a mut buffer local (bind it first, then pass that local)"), + ), + ( + "immutable", + "b", + format!("cannot fill immutable buffer 'b' in '{operation}' (declare with mut)"), + ), + ] { + let source = format!( + "{prefix} b := buffer(4)\n {}\n return Ok(())\n}}\n", + call.replace("ARG", arg) + ); + let (_, diagnostics) = check(&source); + assert!( + diagnostics.iter().any(|diagnostic| diagnostic.message == expected), + "{operation} {label} must report {expected:?}; got {:?}", + diagnostics.iter().map(|diagnostic| &diagnostic.message).collect::>() + ); + } + + let source = format!( + "{prefix} mut b := buffer(4)\n {}\n return Ok(())\n}}\n", + call.replace("ARG", "b") + ); + let (_, diagnostics) = check(&source); + assert!( + !diagnostics.has_errors(), + "{operation} with a mut local must check: {:?}", + diagnostics.iter().map(|diagnostic| &diagnostic.message).collect::>() + ); + } + + let (_, diagnostics) = check( + "import std.fs\nfn main() -> Result<(), Error> {\n f := fs.open_rw(\"input\")?\n b := buffer(4)\n f.pread(b, \"bad offset\")?\n return Ok(())\n}\n", + ); + assert!( + diagnostics.iter().any(|diagnostic| diagnostic.message.contains("'.pread()' offset must be i64")), + "pread must retain the later offset diagnostic before the mutability gate: {:?}", + diagnostics.iter().map(|diagnostic| &diagnostic.message).collect::>() + ); + assert!( + !diagnostics.iter().any(|diagnostic| diagnostic.message.contains("cannot fill immutable buffer")), + "pread must not mask the invalid offset with the mutability diagnostic: {:?}", + diagnostics.iter().map(|diagnostic| &diagnostic.message).collect::>() + ); + } + #[test] fn lifted_function_origin_metadata_is_explicit() { let (program, diagnostics) = check( diff --git a/docs/design-notes.md b/docs/design-notes.md index 636dabbf..40a15019 100644 --- a/docs/design-notes.md +++ b/docs/design-notes.md @@ -786,6 +786,9 @@ Consequences already in the design: - cost is visible (no hidden alloc / copy / async / thread) - memory layout is a first-class, explicit choice (type- and scope-driven, never whole-program inferred) - I/O is sink-first, buffered, region-scoped (mmap views, writev, io.copy) +- native operations that fill a `buffer` take only a bare `mut` local: the mutation and stable + address are visible, while temporaries and immutable aliases fail before any syscall or entropy + operation is formed - the std library encodes performance rails, not only convenience APIs - diagnostics explain resource mistakes in plain terms (the perf-rail lints) ``` diff --git a/docs/guide/02-language-basics.md b/docs/guide/02-language-basics.md index 60b251ac..c497bd83 100644 --- a/docs/guide/02-language-basics.md +++ b/docs/guide/02-language-basics.md @@ -101,6 +101,7 @@ What remains — iteration whose trip count is decided *by the iteration itself* ```align mut total := 0 +mut buf := buffer(4096) n_read := loop { n := r.read(buf)? // errors still exit the function, as always if n == 0 { break total } // break is the only loop exit diff --git a/docs/guide/13-std-os.md b/docs/guide/13-std-os.md index 9595badb..e30fb034 100644 --- a/docs/guide/13-std-os.md +++ b/docs/guide/13-std-os.md @@ -68,7 +68,8 @@ The streaming tier, for data bigger than memory. Its control shape is the `loop` ```align import std.fs -fn pump(r: reader, w: writer, buf: buffer) -> Result<(), Error> { +fn pump(r: reader, w: writer) -> Result<(), Error> { + mut buf := buffer(4096) loop { n := r.read(buf)? // fill buf to capacity; 0 = EOF if n == 0 { break Ok(()) } // break carries the loop's value out @@ -79,8 +80,7 @@ fn pump(r: reader, w: writer, buf: buffer) -> Result<(), Error> { pub fn main(args: array) -> Result<(), Error> { r := fs.open(args[1])? // reader — owns the fd, closes on drop w := fs.create(args[2])? // writer - buf := buffer(4096) // reused across the whole loop - pump(r, w, buf)? + pump(r, w)? return Ok(()) } ``` diff --git a/docs/guide/ja/02-language-basics.md b/docs/guide/ja/02-language-basics.md index f03db846..08e1333f 100644 --- a/docs/guide/ja/02-language-basics.md +++ b/docs/guide/ja/02-language-basics.md @@ -101,6 +101,7 @@ Align には `for` も `while` もありません。これは機能の欠落で ```align mut total := 0 +mut buf := buffer(4096) n_read := loop { n := r.read(buf)? // errors still exit the function, as always if n == 0 { break total } // break is the only loop exit diff --git a/docs/guide/ja/13-std-os.md b/docs/guide/ja/13-std-os.md index e48b6722..e9ad0fc2 100644 --- a/docs/guide/ja/13-std-os.md +++ b/docs/guide/ja/13-std-os.md @@ -70,7 +70,8 @@ pub fn main(args: array) -> Result<(), Error> { ```align import std.fs -fn pump(r: reader, w: writer, buf: buffer) -> Result<(), Error> { +fn pump(r: reader, w: writer) -> Result<(), Error> { + mut buf := buffer(4096) loop { n := r.read(buf)? // fill buf to capacity; 0 = EOF if n == 0 { break Ok(()) } // break carries the loop's value out @@ -81,8 +82,7 @@ fn pump(r: reader, w: writer, buf: buffer) -> Result<(), Error> { pub fn main(args: array) -> Result<(), Error> { r := fs.open(args[1])? // reader — owns the fd, closes on drop w := fs.create(args[2])? // writer - buf := buffer(4096) // reused across the whole loop - pump(r, w, buf)? + pump(r, w)? return Ok(()) } ``` diff --git a/docs/impl/07-roadmap.md b/docs/impl/07-roadmap.md index 4506ec49..700bebfb 100644 --- a/docs/impl/07-roadmap.md +++ b/docs/impl/07-roadmap.md @@ -1959,7 +1959,7 @@ green. **Next: M13** (codegen quality & link hygiene, the pre-LLVM-upgrade wave) (the #297 arm; a view of a buffer's bytes stays pinned to the buffer). This is the one bytes→text path; a bespoke line-view op on `buffer` was REJECTED (a second bytes→str path, and a text op leaking into the deliberately-binary container). - - Canonical loop: `loop { n := r.read_line(buf)?; if n == 0 { break }; + - Canonical loop: bind `mut buf := buffer(cap)` first, then `loop { n := r.read_line(buf)?; if n == 0 { break }; line := buf.bytes().as_str()?; rec := json.decode(line)?; …array_builder… }`. - Documented edges: exactly one `\r?\n` is stripped; a lone `\r` (old-Mac) is not a terminator; a BOM is never stripped (no hidden transformation — json fails line 1; diff --git a/docs/impl/12-pipeline-closure-memory-io-simd-audit.md b/docs/impl/12-pipeline-closure-memory-io-simd-audit.md index 772ddd54..d65ee14a 100644 --- a/docs/impl/12-pipeline-closure-memory-io-simd-audit.md +++ b/docs/impl/12-pipeline-closure-memory-io-simd-audit.md @@ -339,7 +339,7 @@ import std.io pub fn main(args: array) -> Result<(), Error> { base := fs.open(args[1])? r := base.buffered() - line := buffer(1024) + mut line := buffer(1024) consumed := r.read_line(line)? w := fs.create(args[2])? copied := io.copy(r, w)? diff --git a/docs/impl/std-design/ja/net.md b/docs/impl/std-design/ja/net.md index 4a8cce4d..be4845fa 100644 --- a/docs/impl/std-design/ja/net.md +++ b/docs/impl/std-design/ja/net.md @@ -31,7 +31,7 @@ l.accept() -> Result // UDP udp.bind(host: str, port: i64) -> Result u.send_to(data: bytes, host: str, port: i64) -> Result -u.recv_from(buf: mut buffer) -> Result // fills caller buffer, returns {n, peer} +u.recv_from(buf: mut buffer) -> Result // fills caller buffer, returns byte count (v1) // DNS dns.resolve(host: str) -> Result, Error> // owned IP strings ``` diff --git a/docs/impl/std-design/net.md b/docs/impl/std-design/net.md index c64ad7e9..ae012163 100644 --- a/docs/impl/std-design/net.md +++ b/docs/impl/std-design/net.md @@ -31,7 +31,7 @@ l.accept() -> Result // UDP udp.bind(host: str, port: i64) -> Result u.send_to(data: bytes, host: str, port: i64) -> Result -u.recv_from(buf: mut buffer) -> Result // fills caller buffer, returns {n, peer} +u.recv_from(buf: mut buffer) -> Result // fills caller buffer, returns byte count (v1) // DNS dns.resolve(host: str) -> Result, Error> // owned IP strings ``` diff --git a/docs/language-spec.md b/docs/language-spec.md index e4bc6511..421bd5aa 100644 --- a/docs/language-spec.md +++ b/docs/language-spec.md @@ -729,6 +729,10 @@ directly, and programmer errors abort. A failing syscall in a `Result`-returning through one fixed errno table (`ENOENT`→`NotFound`, `EACCES`/`EPERM`→`Denied`, `EINVAL`→`Invalid`, else `Code(errno)`). (`draft.md` §18.2, M9.) +All native buffer-fill operations require the argument to be a bare local declared with `mut`; +temporaries and immutable locals are rejected before the operation is formed. The same rule applies +to `read_line`, positional `pread`, UDP `recv_from`, and `crypto.random`. + `std.encoding`: `base64`/`base64url`/`hex`/`percent` (RFC 3986 URI components — everything outside the unreserved set becomes `%XX`) / `form` (`application/x-www-form-urlencoded` — the same rule but space is `+`; encode one key or value at a time, the `=`/`&` joining them are structure) encode+decode, diff --git a/docs/little-aligner/11-do-it-until.md b/docs/little-aligner/11-do-it-until.md index 85036f7e..9619ea01 100644 --- a/docs/little-aligner/11-do-it-until.md +++ b/docs/little-aligner/11-do-it-until.md @@ -19,7 +19,8 @@ **A3.** The one loop the language has: ```align -fn pump(r: reader, w: writer, buf: buffer) -> Result<(), Error> { +fn pump(r: reader, w: writer) -> Result<(), Error> { + mut buf := buffer(4096) loop { n := r.read(buf)? if n == 0 { break Ok(()) } @@ -43,6 +44,7 @@ fn pump(r: reader, w: writer, buf: buffer) -> Result<(), Error> { **A5.** ```align +mut buf := buffer(4096) mut total := 0 sum := loop { n := r.read(buf)? diff --git a/docs/little-aligner/ja/11-do-it-until.md b/docs/little-aligner/ja/11-do-it-until.md index 037d3693..b425e9c2 100644 --- a/docs/little-aligner/ja/11-do-it-until.md +++ b/docs/little-aligner/ja/11-do-it-until.md @@ -19,7 +19,8 @@ **A3.** この言語が持つ、たったひとつのループ構文の出番です。 ```align -fn pump(r: reader, w: writer, buf: buffer) -> Result<(), Error> { +fn pump(r: reader, w: writer) -> Result<(), Error> { + mut buf := buffer(4096) loop { n := r.read(buf)? if n == 0 { break Ok(()) } @@ -43,6 +44,7 @@ fn pump(r: reader, w: writer, buf: buffer) -> Result<(), Error> { **A5.** ```align +mut buf := buffer(4096) mut total := 0 sum := loop { n := r.read(buf)? diff --git a/docs/open-questions.md b/docs/open-questions.md index c03fa351..54047601 100644 --- a/docs/open-questions.md +++ b/docs/open-questions.md @@ -363,6 +363,14 @@ must preserve. - **Binary parsing arrived (2026-07-10, align-LLM runway A2, branch `runway-a2-binary-codec`).** The consumer settled the byte-level op shape as **typed binary decode/encode** rather than the originally-listed raw `.push(b)` / `buf[i]` ops: reads `bytes._(off)` and writes `buffer.put__(v)` (the endian-explicit typed pair — see the Open → "align-LLM runway" A2 record for the full design), plus `buffer.append(bytes)` for a raw blob. `put_u8` **supersedes** the settled `.push(b)` (a typed single-byte append), and `.append(slice)` shipped as specified. Still deferred (no A2 consumer): `buf[i]` random-access read/write and `freeze → array` / `.to_string()` — a growable-then-freeze output is the `loop` / growable-`array` story, not binary parsing. Record: `draft.md` §12/§18.2, `impl/07-roadmap.md` M9 Slice 1, `crates/align_driver/tests/m9_io.rs`. +### Native buffer fills — design SETTLED + BUILT 2026-08-01 +**Decision: native operations that fill a `buffer` require an explicit mutable local.** +`reader.read`, `reader.read_line`, `file.pread`, `udp_socket.recv_from`, and `crypto.random` accept +only a bare `buffer` local declared with `mut`. Passing a temporary, expression, or immutable local +is a semantic error before the operation is formed; this keeps the in-place mutation and stable +address visible and uniform across I/O, networking, and entropy surfaces. +Record: `draft.md` §18.2, `docs/impl/17-library-boundary-prerequisites.md` Am-v, `crates/align_sema/src/lib.rs`. + ### `str` I/O UTF-8 validation — SETTLED + BUILT (2026-07-04) **Decision: every I/O boundary that produces a `str`/`string` validates its bytes as UTF-8; invalid content fails rather than yielding a malformed `str`.** `draft.md` §7/§12 make "a `str` is always valid UTF-8" a load-bearing invariant (it lets `str` APIs — `chars`, slicing, `find`, display — assume well-formedness), but the M9 std.fs / core.json surfaces produced `str` values directly from raw file/mmap/JSON bytes **without checking**, so a binary file → a malformed `str` → broken invariant (external report, 2026-07-03). - **Fixed at every `str`-returning entry point:** `fs.read_file` (both the fast `read_exact` path and the copy fallback), `fs.read_file_view` (both the mmap path — validated before the view is registered on the arena, `munmap` on failure — and the arena-copy fallback), and `json.decode` (validate the **whole input once** at the head; a decoded `str` field is a zero-copy substring view into that input, so one pass covers every field — the same one-shot check simdjson does). Invalid → `Error.Invalid` for the `fs.*` errno-mapped surfaces; a decode error for `json.decode` (whose error channel is `Error.Code`). diff --git a/draft.md b/draft.md index 1e35b607..0322a732 100644 --- a/draft.md +++ b/draft.md @@ -334,6 +334,7 @@ never yields, and like a `match` whose arms all diverge it satisfies any expecte ```align mut total := 0 +mut buf := buffer(4096) n_read := loop { n := r.read(buf)? if n == 0 { break total } @@ -2465,6 +2466,11 @@ f.pwrite(data: bytes, off: i64) -> Result // writes ALL of data f.len() -> Result // live fstat (not cached) ``` +Every native operation that fills a `buffer` (`read`, `read_line`, `pread`, `recv_from`, and +`crypto.random`) requires a bare source local declared with `mut`. Bind a temporary first; passing +an immutable local or an expression directly is rejected before the operation is formed. This keeps +the mutation, address stability, and ownership boundary explicit. + `file` is the offset-addressed block read+write handle (`fs.create_rw` / `fs.open_rw`, below). Every access carries an explicit `off` — there is **no cursor and no `seek`** (a settable cursor is hidden mutable state), and there is **no read-only constructor** (pure random reads stay `reader` or the @@ -2503,8 +2509,9 @@ call), so `b.len()` is the body length and the return is the bytes consumed incl = EOF, an empty line returns `1` with body length `0`, a final unterminated line returns its bare length). **Growth asymmetry:** unlike `r.read`, which caps at the buffer's capacity, `read_line` **grows** `b` as needed — a line has no caller-chosen bound — up to a 64 MiB line cap (`Error.Invalid` -beyond it, so a terminator-free/binary input can't grow the buffer without bound). The canonical loop is -`loop { n := r.read_line(buf)?; if n == 0 { break }; line := buf.bytes().as_str()?; … }`. **Warning:** the +beyond it, so a terminator-free/binary input can't grow the buffer without bound). The canonical loop +starts with `mut buf := buffer(4096)` and is `loop { n := r.read_line(buf)?; if n == 0 { break }; +line := buf.bytes().as_str()?; … }`. The output must be that bare `mut` local. **Warning:** the per-iteration line view (`buf.bytes()` / its `as_str`) must **not** be hoisted across iterations — the next `read_line` overwrites `buf`, so a view kept from a previous line reads stale/overwritten bytes (a borrow-liveness gap not yet caught by the compiler; `.clone()` a line you need to keep). diff --git a/examples/file_copy.align b/examples/file_copy.align index 4e6ece86..1b2cee61 100644 --- a/examples/file_copy.align +++ b/examples/file_copy.align @@ -3,22 +3,20 @@ module main import std.fs // Byte-exact file copy (std.io Slice 1): `fs.open` a reader, `fs.create` a writer, then stream -// through a fixed-window `buffer` — memory stays O(buffer), never O(file size). Align has no loop -// keyword, so the read/write loop is expressed by tail recursion, threading the moved handles; at -// EOF (`read` returns 0) the handles drop, closing both files. -fn copy_all(r: reader, w: writer, buf: buffer) -> Result<(), Error> { - n := r.read(buf)? - if n == 0 { - return Ok(()) +// through a fixed-window `buffer` — memory stays O(buffer), never O(file size). The output buffer is +// a visible `mut` local owned by the loop; at EOF (`read` returns 0) both handles drop. +fn copy_all(r: reader, w: writer) -> Result<(), Error> { + mut buf := buffer(65536) + loop { + n := r.read(buf)? + if n == 0 { break Ok(()) } + w.write(buf.bytes())? } - w.write(buf.bytes())? - return copy_all(r, w, buf) } pub fn main(args: array) -> Result<(), Error> { r := fs.open(args[1])? w := fs.create(args[2])? - buf := buffer(65536) - copy_all(r, w, buf)? + copy_all(r, w)? return Ok(()) }