Skip to content

fix(evaluator): a write into a &T parameter's value is E0908 (S2); a nested comptime_expect_error keeps the outer one's mode - #1436

Merged
shd101wyy merged 3 commits into
developfrom
fix/field-write-through-shared-borrow
Oct 18, 2026
Merged

shd101wyy merged 3 commits into
developfrom
fix/field-write-through-shared-borrow

Conversation

@shd101wyy

Copy link
Copy Markdown
Owner

Summary

Fixes the S2 soundness hole issues/fixed/a-field-write-through-a-shared-borrow-parameter-is-accepted-and-lost.md. Before this change, p.n = v on a p : &T parameter compiled and wrote only the callee's by-value copy, so the caller never saw the write. Every write into a &T parameter's value is now E0908 (decision 42: x : &T is read-only). The existing code for a write through a borrow is reused.

shared_borrow_param_root_of_place (src/evaluator/exprs/assignment.yo) walks a place back to its root, but only through steps that stay inside the borrowed value: a value-struct or tuple field, a fixed-array element, a unique Box payload. If the root is an is_imm_borrow parameter (a fn, a method's self : &Self, or a closure), the place is inside that parameter's value. Three write sites consult it:

  • Field or element assignment. Covers p.n = v, p.inner.m = v, p.arr(i) = v, p.pair.0 = v, a(i) = v, b.n = v through b : &Box(P), and self.n = v in a self : &Self method. A write whose old value holds RC data keeps its existing borrowed-copy message.
  • A plain-data place lent to a &mut parameter or a self : &mut Self receiver the callee writes. Covers p.inc(), p.inner.bump(), f(&mut p.inner), g(&mut p.n, v), g(&mut p.inner.m, v), g(&mut p.arr(i), v) and g(&mut n, v) on n : &i32. These go through the existing D3 pending entries, which are judged against the callee's mutation mask.
  • A &mut local borrow of such a place (q := &mut p.inner.m).

Scoped on purpose: handles are exempt. A step through a handle (a ref object, an Rc, a std collection) reaches storage the handle shares with the caller, so it stays legal. Lending a handle place to a &mut receiver also stays legal: h.xs.push(v) on h : &H, em.emit(...) on em : &Emitter. The &mut argument check therefore applies only when the lent place's type holds no RC data. A value aggregate that holds RC data already gets the borrowed-copy E0908.

The task said to fix any std/src sites the rule rejects. I scoped the rule instead, because plans/VALUES_BY_DEFAULT.md keeps handle writes legal until the V2b flip. The unscoped version rejected 66 sites in src/ (check ./src: 69 files failed). Almost all were em/emitter : &Emitter receivers, plus files, registry, and saved_names : &ArrayList(String) in src/verifier/vc.yo:2491. Every one is a handle whose writes the caller does see. check ./std was clean under both versions.

Two more bugs turned up while writing the test. Both are fixed here, each with a test that fails on develop:

  • issues/fixed/2026-10-17-comptime-expect-error-nested-in-another-resets-its-propagate-mode.md (S3). The element place y.arr(i) looks arr up as a method, which forces the type's impls. An impl member that holds its own comptime_expect_error then ran inside the outer one's argument. On exit it reset the global propagate mode to false and cleared the observations, so check re-raised the outer, expected error. evaluate_comptime_expect_error now saves the propagate mode, the observed flag and the messages, and restores them on exit.
  • issues/fixed/2026-10-17-e0908-prints-an-element-place-with-unbalanced-parentheses.md (S3). An E0908 named `y.arr)(usize(0)`. _place_source_text now uses local_borrow_place_text, which strips only a pair of parentheses that encloses the whole text.

A third bug was filed but is not fixed here: issues/2026-10-17-a-fixed-array-literal-of-runtime-strings-emits-invalid-c.md (S2). [String.from("a"), String.from("b")] as a [String ; 2] emits /* skip generating value */ inside a C initializer. It fails on develop too, and it is outside this change.

The E0908 explanation (yo explain E0908, en + zh-CN) no longer says that plain-data fields of a &T parameter can be written, and it names the handle exemption. The handover, plans/VALUES_BY_DEFAULT.md and plans/OWNERSHIP_AND_BORROWING_DOC_DRAFT.md now cite the fixed doc.

Verification

Tree-built compiler at the rebased head (2f5301589, on develop 4d7c15caa; develop has since moved with src drift in files this branch does not touch (#1416, #1427), and a trial merge is clean), yo build --std-path ./std:

  • yo check ./src --std-path ./std: 278/278 files passed
  • yo check ./std --std-path ./std: 172/172 files passed
  • Feature tests at the rebased head: tests/shared_borrow_write.test.yo 5/5, tests/deref_auto.test.yo 22/22
  • Internal tests at the rebased head, one file per call: tests/internal/assignment.test.yo 9/9, initialization_assignment.test.yo 4/4, diagnostics_registry.test.yo 9/9, diagnostics_registry_examples.test.yo 1/1 (run with YO_TEST_LEAK_VERDICT=0 as CI runs tests/internal; without it the batch fails only on the documented in-process evaluator-registry leak verdict, with no assertion failure)
  • Borrow and parameter-mode tests and heavy comptime_expect_error users, on the pre-rebase build (same branch diff, before develop's Async Mutex follows Rust: move-only, shared as Arc(Mutex(T)); E0909 accepts & through Arc; D3 checks every hop; with_lock no longer holds the guard (S2) #1380/fix(evaluator): D3 judges every callee by its mutation mask, not by its file (S1, B1 of #1380) #1397 src drift): parameter_modes 43, local_borrows 16, ref_local_binding 16, move_only 55, closure_capture_list 41, for_macro_borrow 27, index 76, string/string 302, async/sm_ownership 45, async_await 281, type_soundness 71, basic 57, comptime 47, expected_error_keeps_prelude_env_clean 1, safe_code_structural_gates 1. All passed.
  • cli-diff: 398 of 399 cases pass, 1 SKIP (network). The first full run gave PASS 391. The other 7 cases (build-stamp-dotted-dir, init*, skills-install*) were GOLDEN-DIFF only because the copied gate binary sat outside the tree and could not find .github/skills. They were re-run with the in-tree binary: PASS 7/7. No goldens were recorded.
  • yo fmt --check with the tree binary on every changed .yo file; bash scripts/check-issue-refs.sh passes.
  • Fails before, passes after. On develop (1a4cf8df6 binary), tests/shared_borrow_write.test.yo fails yo check at its first case (p.n = i32(1) compiles). The nested-comptime_expect_error and element-text cases fail on develop as described in their issue docs.
  • Docs in docs/: no change needed. The user docs already say &T is read-only, and none of them said plain fields could be written.

LLM attribution

  • Model: Claude Opus 5.5
  • Agent harness (optional): Claude Code (VBD campaign workflow)

Deferred

  • Handle writes through &T stay legal until the V2b flip makes the collections values. This includes src/verifier/vc.yo:2491, which passes a bare &ArrayList(String) to a &mut parameter. A callee that reassigns the whole handle loses that write, which is the same family as this bug. There is also an asymmetry: g(&mut h.xs) is accepted while q := &mut h.xs is E0908.
  • issues/2026-10-17-a-fixed-array-literal-of-runtime-strings-emits-invalid-c.md (S2) is open. The suspected one-line fix in evaluate_array_value mirrors the tuple literal's.
  • The full language suite, yo test ./std, the fixpoint and the full tests/internal are left to develop's battery (Tier 2/3).

🤖 Generated with Claude Code

shd101wyy added a commit that referenced this pull request Oct 17, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
shd101wyy added a commit that referenced this pull request Oct 17, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
shd101wyy added a commit that referenced this pull request Oct 17, 2026
… E0908; rc.test's fwb_scratch writes an owned copy

Review of #1436: the shared-param and borrowed-copy E0908 entries were judged
only when the callee was a FuncVal; an unknown mask now counts as a write.
tests/rc.test.yo wrote a plain field through a &T parameter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shd101wyy
shd101wyy force-pushed the fix/field-write-through-shared-borrow branch from 4840824 to 4837a85 Compare October 17, 2026 16:39
shd101wyy added a commit that referenced this pull request Oct 17, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
shd101wyy added a commit that referenced this pull request Oct 17, 2026
… E0908; rc.test's fwb_scratch writes an owned copy

Review of #1436: the shared-param and borrowed-copy E0908 entries were judged
only when the callee was a FuncVal; an unknown mask now counts as a write.
tests/rc.test.yo wrote a plain field through a &T parameter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shd101wyy
shd101wyy force-pushed the fix/field-write-through-shared-borrow branch from 958fed2 to 2ea8c79 Compare October 17, 2026 20:53
shd101wyy added a commit that referenced this pull request Oct 18, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
shd101wyy added a commit that referenced this pull request Oct 18, 2026
… E0908; rc.test's fwb_scratch writes an owned copy

Review of #1436: the shared-param and borrowed-copy E0908 entries were judged
only when the callee was a FuncVal; an unknown mask now counts as a write.
tests/rc.test.yo wrote a plain field through a &T parameter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shd101wyy
shd101wyy force-pushed the fix/field-write-through-shared-borrow branch from 7ba9a05 to 7fc559d Compare October 18, 2026 00:50
…a nested comptime_expect_error keeps the outer one's mode

Rebased onto develop e7e348c (squash replay of the branch's 21 commits;
import-list conflicts in assignment.yo and initialization_assignment.yo
resolved as the union of develop's #1440 imports and this branch's).
…als rule

yo fix --migrate mut-bindings (tree binary) on this branch's changed test
files: 2 marks (fwb_scratch's owned copy; the x lent &mut to _through_mut).
4 parameter writes listed for hand rebinding follow in the next commit.
The migration lists the four writes of the by-value parameter p (its
fields, element and &mut self receiver): a parameter takes no mut, so the
body rebinds it (mut(q) := p) and writes the owned copy, as
tests/mut_bindings.test.yo teaches.
@shd101wyy
shd101wyy force-pushed the fix/field-write-through-shared-borrow branch from 7fc559d to 866182c Compare October 18, 2026 12:54
@shd101wyy
shd101wyy merged commit ceb4a26 into develop Oct 18, 2026
2 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant