Why cant this be flagged by the compiler?

Hi, I have a program that converts a boolean string into a boolean value.

fn main() {
    let flag_str = "1";
    let flag : bool = flag_str.parse().unwrap();

    println!("Hello, world!: {}", flag);
}

I understand that this is a wrong way of doing the conversion, but the compiler does not produce any diagnostic and happy with the code.

However, runtime fails with the following:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/bool_conv`
thread 'main' panicked at src/main.rs:4:40:
called `Result::unwrap()` on an `Err` value: ParseBoolError
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

My question is, can't the unwrap operation be compared at compile time and return error rather at runtime?

Thanks

.parse() is a runtime operation that should work with runtime strings, and you aren't calling it in const context, so the compiler doesn't run it during compile time. The compiler doesn't know the outcome, because it didn't try, so it wouldn't be able to know if .unwrap() fails.

8 Likes

Even if the code was executed in a const context, Result::unwrap() and Option::unwrap() are, at this time, not defined to be const. So that would fail with a compile error.

Option::unwrap actually became const fn in 1.83 (end of November '24).

6 Likes

I think, in binary, i.e, the computer circuit, 1 means true, why doesn't Rust allow to parse "1" to true?

This is a design decision made by the standard library authors.

I'd assume it is intended to reduce ambiguity, i.e. to have one representation only for each of the boolean states.

Because if you start walking that path then sooner or later you would and with non-associative comparisons (e.g. in JavaScript "2" < 3, 3 < "10", "10" < "2"), or your language would declare that "0x10" == "16" (like in PHP), or do something equally crazy.

Because it's very easy to add such “simple and obvious” simplifications, but very quickly they start to interact in a very non-obvious way.

It's easy to say that bool is true or false or 0 or 1, or Yes and No, but when you would add enough of these “simplifications” – your language starts becoming unpredictable or just “strange”. I mean: how many people would agree that "1000" and "1e3" are identical strings, like PHP decrees?

6 Likes

I think that this does not apply here, since we're only talking about parsing (FromStr), not comparing different strings to each other or comparing values of different types (PartialEq, Eq or PartialOrd).

Another possible issue is consistency. When allowing to parse from the strings "1" and "0" respectively, which are both just one character long in both ASCIII and UTF-8, one could also argue, that we need TryFrom<char> for bool to parse it from '1' and '0' respectively. That could lead down a rabbit hole...

You can still do something like

let b1: bool = "1".parse::<u8>()? != 0;
let b2: bool = "1".parse::<u8>()? == 1;
let b3: bool = "1".parse::<u8>()? & 0b1 == 0b1;

depending on your use case.

3 Likes

No, TryFrom<char> is not for parsing. For example u32::try_from('7').unwrap() returns 55 not 7.

Personally I don't see a problem with FromStr being permissive and accepting a few different formats.

2 Likes

What I like about the current rule is that only the literals are accepted as format which makes it very easy to remember.

2 Likes

The problem is all the places that that would then change what third-party crates accept.

I could imagine someone using bool: FromStr to parse json's true/false tokens, and being rather annoyed that all of a sudden it starts accepting other stuff too. Or being surprised that their web service now accepts ?foo=1 instead of just ?foo=true.

(If it had been that forever that's one thing, and I agree it could have been plausible, but no-longer-erroring is also a breaking change for people.)

6 Likes

If I hadn't seen your comment, I don't know when I would have noticed this change. It allowed me to change some NonZero*::new_unchecked calls for const variables into NonZero*::new.unwrap() calls. I requested such a lint to be added to Clippy, and it was already merged.

1 Like

Also,

let val = 0x80;

println!("{:02X}", !!val);

results in val. The same in C returns either 0 or 1.

Any reason why this distinctive behavior in rust?

I prefer the choice Rust went with: there is a type called bool and it is not the same as an integer.


Conditionals (i.e. if) only works with bool then. If you want to negate a condition, you only need to swap true with false and vice-versa. Unlike C’s if which works with integers, in Rust there is no implicit “0 is considered false, everything else is considered true”. And hence we also need no special “logical negation” to model a negated if-condition.

So in Rust there’s only one negation operator: bit-wise negation. C has this operator, too, but writes it as ~.

C doesn’t have let, or println, syntax etc…; so I’m assuming by “the same in C” you mean that all syntax is translated to the “equivalent” in the other language. You should translate the !!val in Rust into ~~val in C; same behavior then.

If you want to do a “true if number is 0” kind of operation in Rust, we simply write this as n == 0, not !n. If you want “true if number is non-zero”, you can write this as val != 0, instead of !!val. If you want “true if number is non-zero, but as an actual number type, not a bool”, you can add a cast, i.e. (val != 0) as …type…. In your example you have i32, so that’s

let val = 0x80;

println!("{:02X}", (val != 0) as i32);
9 Likes

The content of flag_str is opaque to the compiler and very well could have contained the valid text, "true" or "false", instead of something else. The uncertainty is reflected in the result type of parse which you ignoredwith unwrap in preference for that runtime error.

My $0.02.