Open and answer this issue on GitHub.
ready · opened 2026-10-31 · updated 2026-11-03 · 2 comments
ADR-277 D17 (#1012, option E) says a trait method’s keeps column covers every by-value parameter, code or data, and that “by value no longer means kept”. Brief b1012c was meant to build the data half. It stopped (Result: blocked, docs/briefs/b1012c-trait-column-data.md.report.md) because the column is not what refuses the lending:
lends_with (crates/nikaia-std/src/tools/lends.nika:51) only lends a by-value parameter when !signature.takes_a_receiver() || (own && a_function_value(ty)). So no method (inherent or trait) lends a by-value data parameter, whatever its keeps says.emit_arg_only_read (tools/emit_core_args.nika:192) returns false for any callee that takes a receiver.lends_in gives the reason: “A method’s inferred lending waits on the emitter knowing which entry a call goes to (ADR-288)”. No ADR refuses it. This is a build gap that was left open, so it is kind 2 (not decided).Probe (an inherent method with an empty keeps), refused with NK2105:
struct Count { base: i64 }
impl Count { fn size(ref self, text: String) -> i64 sync { return self.base + text.len() } }
fn main() { let c = Count { base: 1 }; let own = f"{c.base}x"; println(c.size(own)); println(own) }
The same probe under a trait bound is refused the same way. With the keeps.rs seed removed, the ledger column of Measure::size becomes empty, but nothing lowers differently.
1. Lift the exclusion for trait methods and their impls only.
FnContract has no trait flag today, so one has to be added. The checker, the emitter and the Rust host impls follow.2. Lift it for every method.
tools/*.nika whose body only reads a by-value parameter changes its lowering (&T / &str instead of T). Its Rust callers then have to lend, so lowered files and hand-written callers change in one go.3. Neither for now. D17 then holds for code parameters only, and gets amended to say so.
2, built in two steps:
The end state is one rule for every callee, as ADR-094 D2 says. Option 1 as the end state would write a distinction into the language that only the build history explains.
An amendment line in ADR-277 D17 (and ADR-094 D2 if 2), and a To build list on #1012 (and a new issue for the inherent step if 2). b1012c is out of the batch until then.
I checked each answer against today’s code on develop. A probe was run where the code alone did not settle the question.
The checker resolves every method call statically and writes the result down. The emitter decides lending later, from that record.
x.m(a). check_core_call_on takes the name of the receiver’s type and looks up resolve_method(world, "{Type}::m") (check_scope.nika:142). That function tries the program’s own contracts first, then the library’s, then the key without its module. When it finds the method, check_core_call_on_described stores the key in facts.method_callees, keyed by the receiver’s node. The emitter reads that into walk.method_callee (emit_core_calls.nika:1202). It already uses it to decide kept code and kept bytes per position (#842: self is position 0). Known: yes.[T: H], t.m(a). Looking up T::m fails, so check_core_call_on_undescribed asks bound_that_answers (check_decls.nika:48). That function returns the trait H that declares m, and the call is resolved again on H. The recorded contract is therefore the trait’s, which is exactly where ADR-277 D17 puts the union of the keeps column. Known: yes, and it is the trait’s contract. An unbounded T reaching a method is already refused (NK1126).Box::take(b, s) on fn take(ref self, s: String) is refused with NK1101 (“takes 1 argument”), because the path form does not take self. So no third route has to be covered.Unknown receiver or a refused call records nothing in method_callees. Such a call stays as it is today: nothing is lent, because lends_with needs a contract.The rule is the same, but which column decides differs, and that difference is the point of D17.
mut, and it is not in the method’s keeps.impl the same way. Rust requires every impl H for X to match the trait’s signature exactly. If one impl lent &str while the trait said String, the generated code would not compile. So an impl whose own body would allow lending still takes String when another impl keeps the value. A direct call x.m(a) on the concrete type must therefore read lending from the trait’s column, not from X::m’s own. This is the one place that needs care when this is built. Behaviour does not differ: the same program means the same thing, and only the lowered signature follows the trait.dyn and no fn f(x: Summarize)”. A call through a bound becomes a Rust generic, so every target is known when the program is compiled. The dyn Fn the emitter writes in one place (emit_function.nika:589) is a closure parameter on a recursive cycle, not a trait object, and lending a method does not touch it.Expr::Variable becomes a named function value (#757), and emit_arg_lent_params already returns nothing for a contract with a receiver. A function value inside a method’s parameter list is already lent today for the program’s own methods (lends_in_own). That stays as it is.&str, &[T] or &T in the signature, & at the call site (emit_arg_passing), and a closure adapter where a lent function is handed over as a value. For methods, the call-site side keys on method_callee (the same key #842 uses). The signature side is the existing parameter code in emit_function. Two things are new:
lends_with and emit_arg_only_read stop excluding receivers;tools/*.nika is itself lowered. A search of all hand-written Rust under crates/ outside lowered/ and rs2nika found no call to any of the affected methods. So lowering again with lower-std brings every caller along. For a program’s own Rust (extern or host code that calls a Nikaia method), the signature changes from String to &str. Rust’s type checker finds every such caller when the program is compiled; nothing breaks silently.ref String). After the change, the ledgers of packages/json and packages/yaml and the examples have to be written again, and --locked refuses a stale one. Run 2701 showed exactly this for a free function (examples/json, unescape). Today neither packages/json nor packages/yaml has a method with a by-value, moving, non-kept parameter in its ledger, so I expect no change to their method signatures there. If one appears later, it raises only that package’s own version (ADR-344 D1).String, Vec, Bytes or collection parameter that is neither ref nor mut. This is the upper bound before the keeps filter, which removes every parameter the body stores:
crates/nikaia-std/src/tools/*.nika and packages/*. Examples: fold_bound::{whole_fields, field_type, methods_of, sum_of_both}, emit_entry::emit_held_struct (5), emit_types::boxed_ty, emit_core_calls::emit_call_required, project_members::crate_root, modules::parse_unit;examples/ and packages/;crates/rs2nika/convert. That is the converter, which does not raise the version.Only the methods that keep nothing actually change. Those are the ones that copy text today without needing to.
lower-std --check writes all of lowered/*.rs again, and cargo build with clippy -D warnings type-checks every caller. A missed &, a mismatch between trait and impl, or a caller on the old convention cannot pass this. The check is complete because Rust checks it.nikaia test --locked on every package and example refuses any ledger the change makes stale (as run 2701 showed)..nika tests with their .stdout cover the cases:
[T: H];The rest of the suite in CI shows that the existing programs still print the same output.
The recommendation does not change: option 2, built in two steps.
Generated by Claude Code
Decided (owner, 2026-10-31): option 2, with additional tests.
Every method lends a by-value data parameter its body only reads, as every free function does (ADR-094 D2). The receiver exclusion in lends_with / emit_arg_only_read and the comment that justifies it go away. In the end there is one rule for every callee.
Built in two steps, as recommended:
keeps column decides for every impl, because an impl must match the trait’s Rust signature. This unblocks b1012c / #1012.tools/, none in examples or package ledgers.Additional tests (owner’s condition). Each step brings its own tests/compiler/ programs. As a minimum:
String, the caller uses own after the call), and the same through a trait bound and through a generic T: Trait;self or returns it): no lending, and a use after the call is still refused;Next: an amendment line in ADR-277 D17 and ADR-094 D2, the To build list for step 1 on #1012, and a new issue for step 2 (label ready). b1012c goes back into the batch after that.
Generated by Claude Code