News for hackers. Stuff that matters.
Re:Using unwrap() in Rust is okay by fatherzine on Monday August 08, 2022 @01:25PM (#32388403)
* Short term: add explicit runtime checks for every step that may panic. With the way jump prediction works in modern processors, the runtime cost may be smaller than one may naively assume it is. Some of us would take, say, a 10% perf. degradation instead of chasing prod panics in the middle of the night.

* Long term: plug-in a richer type (logic) system, so one can safely prove the most costly runtime invariants at compile time.

Re:Using unwrap() in Rust is okay by burntsushi on Monday August 08, 2022 @01:39PM (#32388586)
> add explicit runtime checks for every step that may panic

And do... what when the check fails? Can you please write the code for it? Because I don't understand what the heck you mean here. If you don't know Rust, pseudo code is fine.

> Long term: plug-in a richer type (logic) system, so one can safely prove the most costly runtime invariants at compile time.

This is just copying what I already said in the blog post. Until someone can show me how to prove the correctness of arbitrary DFA construction and search from a user provided regular expression pattern and demonstrate its use in a practical programming language like Rust, I consider this a total non-answer, not a "long term" answer.

Basically, your answer here confirms for me that your views on how code should be structured are incoherent.

Re:Using unwrap() in Rust is okay by fatherzine on Monday August 08, 2022 @02:12PM (#32388977)

    fn is_match(&self, haystack: &[u8]) -> Result<bool, Error> { ... }
Re:Using unwrap() in Rust is okay by burntsushi on Monday August 08, 2022 @02:17PM (#32389023)
That's exactly what the next section of my post explores and explains why it's complete bonkers: https://blog.burntsushi.net/unwrap/#why-not-return-an-error-...

So it looks like I already provided a rewrite of the code for you in your preferred style:

    // Returns true if the DFA matches the entire 'haystack'.
    // This routine always returns either Ok(true) or Ok(false) for all inputs.
    // It never returns an error unless there is a bug in its implementation.
    fn is_match(&self, haystack: &[u8]) -> Result<bool, &'static str> {
      let mut state_id = self.start_id;
      for &byte in haystack {
        let row = match state_id.checked_mul(256) {
          None => return Err("state id too big"),
          Some(row) => row,
        };
        let row_offset = match row.checked_add(usize::from(byte)) {
          None => return Err("row index too big"),
          Some(row_offset) => row_offset,
        };
        state_id = match self.transitions.get(row_offset) {
          None => return Err("invalid transition"),
          Some(&state_id) => state_id,
        };
        match self.is_match_id.get(state_id) {
          None => return Err("invalid state id"),
          Some(&true) => return Ok(true),
          Some(&false) => {}
        }
      }
      Ok(false)
    }
My favorite part is the docs that say "this never returns an error." Because if it did, the code would be buggy. And now all sorts of internal implementation details are leaked.

You can't even document the error conditions in a way that is related to the input of the routine, because if you could, you would have discovered a bug!

Another nail in the coffin of your preferred style is that this will absolutely trash the performance of a DFA search loop.

Re:Using unwrap() in Rust is okay by fatherzine on Monday August 08, 2022 @02:33PM (#32389204)
Man, I only gave some friendly feedback ;)

"this will absolutely trash the performance of a DFA search loop."

Do you happen to have some hard data to quantify? I'm curious.

Re:Using unwrap() in Rust is okay by ninkendo on Monday August 08, 2022 @03:46PM (#32390152)
One additional data point: The Rust compiler got ~2% faster when they made some of the core traits in the serializer infallible (ie. panic instead of using Result): https://github.com/rust-lang/rust/pull/93066

Writeup from the contributor: https://nnethercote.github.io/2022/02/25/how-to-speed-up-the...

> The Decoder trait used for metadata decoding was fallible, using Result throughout. But decoding failures should only happen if something highly unexpected happens (e.g. metadata is corrupted) and on failure the calling code would just abort. This PR changed Decoder to be infallible throughout—panicking immediately instead of panicking slightly later—thus avoiding lots of pointless Result propagation, for wins across many benchmarks of up to 2%.

Re:Using unwrap() in Rust is okay by burntsushi on Monday August 08, 2022 @02:56PM (#32389479)
Yes. I work on regex engines. Here's the code with all the crazy checks that return errors that can never happen:

    fn find_fwd_imp<A: Automaton + ?Sized>(
        dfa: &A,
        input: &Input<'_, '_>,
        pre: Option<&'_ dyn Prefilter>,
        earliest: bool,
    ) -> Result<Option<HalfMatch>, MatchError> {
        let mut mat = None;
        let mut sid = init_fwd(dfa, input)?;
        let mut at = input.start();
        while at < input.end() {
            let byte = match input.haystack().get(at) {
                None => return Err(MatchError::GaveUp { offset: at }),
                Some(&byte) => byte,
            };
            sid = dfa.try_next_state(sid, byte)?;
            if dfa.is_special_state(sid) {
                if dfa.is_match_state(sid) {
                    let pattern = dfa.match_pattern(sid, 0);
                    mat = Some(HalfMatch::new(pattern, at));
                } else if dfa.is_dead_state(sid) {
                    return Ok(mat);
                }
            }
            at = match at.checked_add(1) {
                None => return Err(MatchError::GaveUp { offset: at }),
                Some(at) => at,
            };
        }
        eoi_fwd(dfa, input, &mut sid, &mut mat)?;
        Ok(mat)
    }
And here's a search (I ran it a few times):

    $ regex-cli count matches dfa dense @$medium '\w{42}'
                parse time:  45.331µs
            translate time:  28.767µs
          compile nfa time:  9.577558ms
                nfa memory:  698148
    compile dense dfa time:  516.497476ms
          dense dfa memory:  6562848
     dense alphabet length:  113
              dense stride:  128
               search time:  531.808124ms
                    counts:  [2]
Here's code that's safe that will panic when there's a bug:

    fn find_fwd_imp<A: Automaton + ?Sized>(
        dfa: &A,
        input: &Input<'_, '_>,
        pre: Option<&'_ dyn Prefilter>,
        earliest: bool,
    ) -> Result<Option<HalfMatch>, MatchError> {
        let mut mat = None;
        let mut sid = init_fwd(dfa, input)?;
        let mut at = input.start();
        while at < input.end() {
            sid = dfa.next_state(sid, input.haystack()[at]);
            if dfa.is_special_state(sid) {
                if dfa.is_match_state(sid) {
                    let pattern = dfa.match_pattern(sid, 0);
                    mat = Some(HalfMatch::new(pattern, at));
                } else if dfa.is_dead_state(sid) {
                    return Ok(mat);
                }
            }
            at += 1;
        }
        eoi_fwd(dfa, input, &mut sid, &mut mat)?;
        Ok(mat)
    }
And here's a search with it:

    $ regex-cli count matches dfa dense @$medium '\w{42}'
                parse time:  33.701µs
            translate time:  24.264µs
          compile nfa time:  9.55935ms
                nfa memory:  698148
    compile dense dfa time:  550.959698ms
          dense dfa memory:  6562848
     dense alphabet length:  113
              dense stride:  128
               search time:  468.932568ms
                    counts:  [2]
So that's 10% right there. We can keep going though. Let's get rid of the bounds checks. We have to use unsafe though. The stakes have risen. Now if there's a bug, we probably won't get a panic. Instead we get UB. Which might lead to all sorts of bad stuff.

    fn find_fwd_imp<A: Automaton + ?Sized>(
        dfa: &A,
        input: &Input<'_, '_>,
        pre: Option<&'_ dyn Prefilter>,
        earliest: bool,
    ) -> Result<Option<HalfMatch>, MatchError> {
        let mut mat = None;
        let mut sid = init_fwd(dfa, input)?;
        let mut at = input.start();
        while at < input.end() {
            sid = unsafe {
                let byte = *input.haystack().get_unchecked(at);
                dfa.next_state_unchecked(sid, byte)
            };
            if dfa.is_special_state(sid) {
                if dfa.is_match_state(sid) {
                    let pattern = dfa.match_pattern(sid, 0);
                    mat = Some(HalfMatch::new(pattern, at));
                } else if dfa.is_dead_state(sid) {
                    return Ok(mat);
                }
            }
            at += 1;
        }
        eoi_fwd(dfa, input, &mut sid, &mut mat)?;
        Ok(mat)
    }
And now the same search:

    $ regex-cli count matches dfa dense @$medium '\w{42}'
                parse time:  22.511µs
            translate time:  26.843µs
          compile nfa time:  8.709352ms
                nfa memory:  698148
    compile dense dfa time:  565.365005ms
          dense dfa memory:  6562848
     dense alphabet length:  113
              dense stride:  128
               search time:  456.402963ms
                    counts:  [2]
For a smaller gain.
Re:Using unwrap() in Rust is okay by fatherzine on Monday August 08, 2022 @03:19PM (#32389784)
This is so awesome, thanks for sharing! I am happy that the "~10% perf hit" intuition is reasonably close to what the data shows. What is the right tradeoff? I am not an author of widely popular regex engines, so I'll leave that evaluation to more qualified people.
Re:Using unwrap() in Rust is okay by burntsushi on Monday August 08, 2022 @05:02PM (#32390969)
10% is an excellent win. And given that I consider the first code snippet to be effectively nonsense, the right trade off is obvious to me. :)

I bet if me and you sat down with a big codebase side-by-side, I could convince you that the style you're proposing is totally unworkable.

Now, whether it's worth it to use unsafe and elide bounds checks is harder. The gain I showed here is small, but it can be bigger in other examples. It depends. But it is certainly less clear. It helps that this is probanly the easiest kind of unsafe to justify, because you just have to make sure the index is in bounds. Other types of unsafe can be quite tricky to justify.

Re:Using unwrap() in Rust is okay by winstonewert on Monday August 08, 2022 @01:45PM (#32388663)
Your short term suggestion appears confused. Panics are already the result of runtime checks. The issue isn't whether or not we have runtime checks, but what to do when those checks fail.

But to your original point, its true that a multiplexing server should probably try to stay up even if there is a bug uncovered in handling a request. But that's precisely why rust allows you to catch panics.

Re:Using unwrap() in Rust is okay by fatherzine on Monday August 08, 2022 @02:13PM (#32388994)
"It is not recommended to use this function for a general try/catch mechanism. The Result type is more appropriate to use for functions that can fail on a regular basis. Additionally, this function is not guaranteed to catch all panics, see the “Notes” section below."

https://doc.rust-lang.org/std/panic/fn.catch_unwind.html

Edit. Re. already existing runtime checks, I'd expect a decent compiler to optimize away redundant checks, i.e. explicitly coded checks vs. checks injected automatically by the compiler, as long as their semantics is identical.

Re:Using unwrap() in Rust is okay by burntsushi on Monday August 08, 2022 @02:21PM (#32389065)
Can you point to any compiler in the world that can see through the construction of a DFA from user input to the point that it can automatically optimize away the transition table lookup?

> The Result type is more appropriate to use for *functions that can fail on a regular basis.*

Emphasis mine. The DFA::is_match routine can never fail on any input. Yet, you want to use a 'Result' with it.

Seriously. Please please pretty please read the blog post. You are literally caught in precisely the tangled mess that I tried to untangle in the post. If my post didn't do it for you, then please consider responding to specific points you disagree with in the post.

Re:Using unwrap() in Rust is okay by fatherzine on Monday August 08, 2022 @02:35PM (#32389225)
Sorry, I only meant to address with some nuance the statement "A failure with logic should nuke due to the app being in an inconsistent state". If you are confident that the specific DFA code in the blog post will not fail, more power to you! And to us, which transitively trust the code via your judgment ;)

Edit. For technical geeking around:

* The point is about redundant check elimination, not full check elimination. A decent compiler should be able to eliminate redundant checks, though perhaps the language could offer some intrinsics (was: pragmas) to make this 100% reliable.

* For DFA proofs, perhaps something like https://courses.grainger.illinois.edu/cs373/fa2013/Lectures/... could be a start. A great intern may tackle it over a summer ;)