printf refuses an operand it cannot read as a number - #428
Merged
Conversation
AGENTS.md names `printf '%d' 0xff` printing `0` as the shape to refuse, and printf was printing it. The conversions read a value through `s.parse().unwrap_or(0)` and `*f as i64`, so `0xff`, `abc` and `1e3` all answered `0`, `1.5` answered `1`, and `%c` of 4294967296 truncated through `as u32` into NUL. `007` was the worst of them: it answered `7`, a plausible number, while every other number position in kaish had just been taught that a leading zero is text. The rule printf now reads by is JSON's, the same one `fromjson` uses, so the answer does not depend on which builtin is asking: `1e3` is a number, `0xff` and `007` are not. Each refusal names the spelling that works — `255` or `$(( 0xff ))` for a based numeral, `8#7` or `7` for a leading zero, with the sign carried into both suggestions so a fix cannot flip the value. The seam worth explaining is `FormatArg`. printf and awk share the format engine, and they disagree about this on purpose: in POSIX awk a non-numeric string IS 0, and that is awk being correct, not awk being unfixed. So the trait's numeric conversions became fallible and each caller answers for itself — awk's impl is always `Ok`, printf's refuses. Two tests pin awk's coercion as the control that the refusal did not leak across the trait. A MISSING operand still converts as 0, which POSIX requires and which is not the fallback being removed here; only a value that is present and unreadable refuses. The refusal happens before any output, so a caller never reads a half-written line as a whole answer. This diverges from GNU for `0xff`, which GNU reads as 255 — deliberate, and the same divergence `$(( ))` already documents; for `abc` GNU also errors. Gates: clippy -D warnings, cargo test --all (204 suites, 6501 passed; +16 new, no existing test changed), insta --check, no-default-features, wasi build, rustdoc -D warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`printf '%d' "$x"` with `x` unset is the common way an empty operand reaches a number position, and the general refusal answered "`` is not a number" — it quoted nothing, so it named nothing the reader could act on. Refusing is the right answer and is not in question here: the arithmetic rewrite already made an unset or empty operand an error rather than 0, and printf reading it as 0 would put the two number positions back into disagreement. Only the wording changes. Also pins the two integer bounds, which the range guard's comparison against 2^63 exists to get right: `i64::MIN` is representable in f64 and must not be refused, and `i64::MAX` must survive the i64 parse path rather than being rounded up into the refusal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review found the refusal had left one silent answer standing, at the one boundary where rounding hides it. `printf '%d' '-9223372036854775809'` printed `-9223372036854775808` and exited 0: the operand is not an i64, so it fell through to the JSON reader, which has no arbitrary precision and read it as f64, where it rounds to exactly `i64::MIN`. The range guard then accepted it, correctly, because `i64::MIN` IS in range. Every step was locally right and the answer was wrong by one, which is the shape this whole conversion exists to refuse. The kernel already had the guard: `value_to_num` refuses an integer-shaped string that failed its i64 parse before any float sees it, because overflow is the only way that parse can fail for that shape. printf now makes the same call through the same predicate, so `is_i64_overflow_shape` gains a third caller and the three number positions name the 64-bit limit with one sentence. The positive side already refused, but by a longer route; it now gives the canonical message too. The same review found four messages that named the wrong fix or none. A leading `+` reached neither the leading-zero nor the overflow check, so `+007` answered 7 where `007` refuses — the sign is now split off first, with `-` carried into every suggestion and `+` dropped, since `+7` and `7` are the same number but `-7` is not. `007.5` was offered `8#7.5`, which is not a numeral in any base; a fractional value now gets only its decimal spelling. `0b101` and `0o17` said only "is not a number" where the arithmetic lexer names `2#101` and `8#17`, and now say the same thing it does. `1e999` said "is not a number" when the problem is magnitude; it names the range, while the word `inf` still does not, having no numeral in it. Tests: the two bounds cases were passing through the typed-integer arm rather than the reader under test, and every refusal case had an empty buffer at the moment it refused, so none of them could have caught a partial write. Both are pinned properly now — a literal and a good operand before the bad one, and a refusal in a second cycling pass — along with the negative boundary, the `+` spellings, the base spellings, and a missing-operand default for every numeric conversion rather than `%d` alone. Gates: clippy -D warnings, cargo test --all (204 suites, 6514 passed), insta --check, no-default-features, wasi build, rustdoc -D warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
AGENTS.md names
printf '%d' 0xffprinting0as the shape to refuse, and printf was printing it. The conversions read a value throughs.parse().unwrap_or(0)and*f as i64, so0xff,abcand1e3all answered0,1.5answered1, and%cof 4294967296 truncated throughas u32into NUL.007was the worst of them: it answered7, a plausible number, while every other number position in kaish had just been taught that a leading zero is text.printf now reads a number by JSON's rules, the same ones
fromjsonuses, so the answer does not depend on which builtin is asking. Each refusal names the spelling that works:A MISSING operand still converts as 0, which POSIX requires and which is not the fallback being removed; only a value that is present and unreadable refuses, and it refuses before any output so a caller never reads a half-written line as a whole answer.
The seam worth knowing about is
FormatArg, which printf and awk share. They disagree here on purpose: in POSIX awk a non-numeric string IS 0, and that is awk being correct, not awk being unfixed. The trait's numeric conversions became fallible so each caller answers for itself — awk's impl is alwaysOk. Two tests pin awk's coercion as the control that the refusal did not leak across the trait.A review of the first pass found the refusal had left one silent answer standing.
printf '%d' '-9223372036854775809'printed-9223372036854775808and exited 0: not an i64, so it fell to the JSON reader, which read it as f64, where it rounds to exactlyi64::MIN, which the range guard then accepted — correctly, becausei64::MINis in range. Every step was locally right and the answer was wrong by one. printf now uses the sameis_i64_overflow_shapeguardvalue_to_numalready had, refusing an integer-shaped operand before a float can round it.That review also corrected four messages that named the wrong fix or none (
+007dodged the leading-zero rule entirely and answered 7), and two tests that were passing through the wrong code path. Nothing in the existing suite changed behavior: 6485 → 6514 tests, all additions.This diverges from GNU for
0xff, which GNU reads as 255 — deliberate, and the same divergence$(( ))already documents. ForabcGNU also errors.Gates:
clippy -D warnings,cargo test --all(204 suites, 6514 passed, 0 failed),insta --check,no-default-features, the WASI build, and rustdoc-D warningsall clean.