Two conversions, three decisions
A type migration that writes .into() at every broken site sounds like the dangerous version. It holds up when the analyzer decides which conversions stay: only the ones that type-check and break nothing else are kept; the rest are taken back and left for a person. Type-correct, not judged right.
On this page · 6 sections
Part 18 built the reporting half of a type migration:
change a declared type, type-check the workspace, list every site that no longer fits. It argued,
at some length, against writing the conversions. For a u64 becoming a Duration,
Duration::from_secs(x) is almost always right and silently wrong wherever the number was not
seconds, and a page of confident conversions, some of them wrong, is worse than a list.
That argument is about choosing a conversion. This post is about the one conversion that involves no choice.
This account follows the Rust implementation at revision
4e2227fc,
merged as PR #121 on September 23, 2026 from the request in
issue #119. The run outcomes, cargo check results and timings below are the ones
reported in PR #121; this review checked the pinned source and the test definitions and did not
repeat those runs. The tool changed afterwards: PR #729 extended convert to other
languages and added a transitive mode, and PR #937 split the module into smaller files.
.into() is the type’s own answer
x.into() compiles only if an Into<Target> exists for the source type, which in practice comes
from a From impl on the target (a direct Into impl is possible, and rare), written by the
type’s author as the conversion. Type-checking says the conversion exists, not that it is the
right one at a given site. u64 declares one from u32, since every u32 fits.
Box<str> declares one from String. Duration declares none from any integer, precisely because
the unit is ambiguous. So .into() never picks between from_secs and from_millis: where the
choice exists, .into() does not compile.
That makes the automatic half simple to state. With convert, code_migrate_type writes .into()
at every site whose error is expected one of the two types, found the other. Then it checks the whole overlay again and keeps a
conversion only where the analyzer accepts it.
Six sites, two conversions
The scratch crate from issue #119: a timeout: u32 field migrated to u64, read and written in
two files. Before convert, the issue’s run left all six sites to a person. PR #121 reports the
run below; where the PR text abbreviates the repeated note on the tried sites, it is written out
here as the pinned renderer prints it.
.into(). The analyzer decides which ones stay. Cases from the scratch-crate run reported in PR #121.2 site(s) converted with `.into()`, each accepted by the analyzer:
src/caller.rs:8 `secs as u32` → `(secs as u32).into()`
src/lib.rs:9 `secs` → `secs.into()`
4 site(s) in 1 file(s) do not fit the new type:
src/caller.rs
4:5 expected u32, found u64 [E0308]
l.timeout * 2
try: `.into()` was tried here and does not type-check: …
16:20 expected u32, found u64 [E0308]
let t: u32 = l.timeout;
try: `.into()` was tried here and does not type-check: …
17:5 expected u32, found u64 [E0308]
t + build(1, "x").timeout
try: `.into()` was tried here and does not type-check: …
17:5 the trait bound `u32: Add<u64>` is not satisfied [E0277]
t + build(1, "x").timeout
The two widenings convert. The four errors that are left, on three lines (line 17 has two), are
the migration’s real questions. A
place that still wants a u32 is now handed a u64: truncate, fail, or migrate it too?
That is a decision about overflow, and .into() has no opinion on it, which is exactly why it
does not compile there. The tool tried, the analyzer said no, the conversion was taken back, and
the report says it was tried, so nobody tries it again by hand.
One conversion keeps what the code did and is not pretty: (secs as u32).into() still truncates
to 32 bits, as the code did before. A person might write secs as u64, which no longer
truncates - a different program, and so not the tool’s to write. It adds only .into(), and
parentheses around anything that is not a path, a call chain or a literal, such as a cast or
a * 2; it never edits what was already there.
Migrating the other field, name: String to Box<str>, converts both of its sites. The
timeout field migrated to Duration converts none of its four candidates, which is the argument of
part 18 settled by the types themselves. PR #121 reports that in all three cases cargo check on
a build node agreed with the analyzer once the change was applied or forced through.
The rules that keep it honest
A rejected conversion is taken back. After each round of conversions the overlay is checked again, and every conversion whose line still has an error is reverted. The remaining ones are checked again, since reverting one can change what the others see, for up to four rounds.
A conversion that breaks something else drops them all. If the kept conversions type-check where they are but an error appears on a line that was not in the original report, none of them is kept, and the report names the new error. A conversion that compiles only by moving the problem somewhere else is the plausible damage the whole approach exists to avoid.
apply writes the declaration with the kept conversions, and still refuses while any site
remains, unless forced.
Two things the analyzer’s answers taught it
PR #121 records two runs that went wrong before the type-name and range fixes it carries. The
first Box<str> run converted nothing: no site was even a candidate. The analyzer writes
Box<str, Global>, allocator included, so the comparison with the Box<str> the user asked for
failed. Type names are now compared the way the analyzer spells them. The same change fixed an
older bug in the suggestions: the rule that turned std::time::Duration into Duration split on
the last ::, and made Vec<std::string::String> into String>.
The second run converted one site and left the other, which should have converted. The reason was
the analyzer’s range for a method call: for label.to_string() it covers to_string, the method’s
name, without the parentheses. .into() written at the end of that range gave
label.to_string.into()(). The re-check caught it, as it should, and took it back. A range
followed by an argument list is now extended over it. A range that starts after a . gets
.into() only in its plain form, because parentheses there would cut the chain in two; where it
would need them, the site is left unconverted.
What it costs, and how it is pinned
PR #121 reports runs with convert on the scratch crate at 0.40 to 0.84 seconds, each being the
first check plus at least one round of conversions and its own check. Those are reported timings
from one scratch crate, not a benchmark, and they were not repeated for this post.
The integration test plays the analyzer from the text it is actually sent. For that, the scripted
test gateway now shows didOpen and didChange to its script. The field is u32 to u64, and
.into() is accepted in one file and rejected in the other. Switch off the taking back and the
test fails on the count of conversions, two instead of one, and PR #121 reports that run. Switch
off the check for new errors, and the second half of the test fails, also reported there: that half makes the kept conversion break another line,
and the conversion is kept anyway.
What it does not do
- Pick a constructor.
Duration::from_secs,PathBuf::from,Some(…)written by name: none of them. Only.into(), which coversSome(Option<T>declares aFrom<T>) and any other conversion a type declares. - Migrate further. A site that should become
u64too, such as one whose surrounding code still requiresu32, is reported. Migrating it is its own run, with its own report. - Judge a
Fromimpl. If a type declares a conversion that is wrong for a particular site, the tool writes it..into()means what the type’s author said it means. - Rust only, at this revision. PR #729 of September 29, 2026 added conversions for TypeScript, Python, C++, Swift and Go and a transitive mode; that later work is outside this post.
Cite this article
Alexander Panasenko (2026-10-19). Two conversions, three decisions. https://prod.codes/blog/two-conversions-three-decisions/