openlang

Where does the cursor stay after an incomplete record?

Result tells the caller of a streaming parser whether a record came out. It says nothing about whether the parser moved a cursor, touched scratch storage or left the input alone, and after an incomplete record that is what matters. Who owns those bytes now, and where does the next call begin?

The shape I'd propose. The caller owns a byte vector and lends the parser a shared slice. The parser owns its cursor and its temporary decoded fields. On success the cursor moves past one complete record. On incomplete input it stays at that record's start and the tentative fields are discarded. Malformed input also keeps the start and returns the offending offset. An incomplete diagnostic carries the record start plus the position scanning reached, which records what was inspected and consumes nothing.

Returning Err gives none of this. The parser has to stage updates and commit only on success. Then the caller keeps the unconsumed suffix, appends bytes and retries. If a successful record borrows from the vector, though, every read of it has to finish before the append begins.

Taking ownership of the vector would raise its own question, whether failure hands it back. Borrowing avoids that, and the failure position and allowed next calls still belong in the docs.

后续讨论 9

顺着彼此的依据读下去
  1. 固定链接

    In OCaml that diagnostic is Incomplete (start, scanned). The pattern picks the constructor and binds both positions, and a guard such as when start = scanned tells a scan that never moved from one that did. Writing Incomplete (p, p) to mean equal positions isn't an option. The compiler rejects a variable bound twice in one pattern.

    None of that touches consumption, though. The shape makes the positions easy to look at. Whether the cursor moved is still something only the documented cursor rule can promise.

  2. 固定链接

    A new goroutine retrying the same suffix gets you nothing. After incomplete input you either read more bytes or report truncation once the source has ended, and an unbuffered channel from the reader only synchronizes the handoff of a freshly filled buffer. Which suffix to keep is still your cursor rule's call, so make waiting for input and retrying the parser two explicit steps.

  3. 固定链接

    try only hands the incomplete-input error to the caller. It won't restore the cursor or drop the tentative fields, so Zig saves you none of the work in the implementation you described. catch unreachable is worse. It asserts incomplete input can't happen, a single network fragment proves otherwise, and in a safety-checked build the wrong assertion crashes. What would make that error impossible here? A prevalidated frame might, if neither the validation nor anything after it can produce an error the assertion covers.

  4. 固定链接

    Incomplete input shouldn't be something the parser asserts away or reports as a crash. It's an ordinary result, and alden's cursor rule says what goes with it. The cursor stays at the record start, and the caller appends and calls again. If records carry a length prefix, check it before decoding any field and there are no tentative fields to throw away either. An Elixir binary match gets that ordering for free, since a short buffer simply doesn't match.

  5. 固定链接

    start and scanned read clearly in a sum type. On the C side, where I spend most of my time, the caller gets an output struct instead, and that struct needs its own rules.

    After an incomplete return, which fields may the caller read? Both positions, or only the status? Are the tentative decoded fields zeroed, left over from an earlier call, or off limits entirely? If the header does not say, each caller ends up guessing which assignments the parser happened to make.

    Pointers come next. If a diagnostic points into the input, the documentation has to say so and state how long that input must stay valid. Under alden's borrowing design the caller owns the buffer, so the parser never frees it.

  6. 固定链接

    nullbyte's questions don't go away when someone wraps the parser. They move up a layer. If a PHP wrapper throws on incomplete input, the catch block learns that something failed and nothing about which fields of the output are still safe to read. So every caller writes its own little check of the tentative fields, and no two of those checks agree for long. One documented failure shape in the wrapper costs a bit more code up front and a lot less after.

  7. 固定链接

    Every normal return should hand back a tagged result with all of its fields initialized. Then the C side has nothing left to guess.

    Incomplete carries its own start and scanned as plain numbers and no tentative record at all. Malformed carries the offending offset. Only success exposes a record. If that record borrows from the input, the caller finishes reading it before mutating the buffer. If it is owned, it is fully initialized and its lifetime is independent of the buffer, at the price of a copy. Records here are small, so paying for the copy is simpler than documenting the borrow.

    brandt_p's wrapper can then build its PHP failure value from the owned diagnostic and never look at half-written fields. A wrapper that catches an exception and converts it is a different case. Someone has to write down which diagnostic fields survive the exception.

  8. 固定链接

    In Rust the borrow checker catches exactly the case from the root. Keeping a successful record alive across the append is rejected.

    let rec = parse(&buf).unwrap();
    buf.extend_from_slice(more);
    println!("{:?}", rec);

    That's E0502, buf borrowed as mutable while rec still holds it as immutable. Finish the read first or copy the record out, in that order of preference.

    What the root doesn't count is rescanning. Incomplete keeps the cursor at the record start, so every retry scans that record from the beginning again. If bytes arrive one at a time, a long record costs quadratic work. scanned already says how far the scan got, and the parser could resume from there, but only by keeping partial state between calls, which is the tentative state I said to discard. For records this size the rescan is a fair price. For large ones, max8's step of reading more bytes before retrying matters more than I gave it credit for.

  9. 固定链接

    One tool does exist for half of this. try restores nothing, agreed, but errdefer runs on every error return from its scope. Save the cursor at entry, write errdefer self.pos = start;, and it goes back for Incomplete and Malformed alike, whichever try failed. Success falls through without running it, which is the commit.

    It only resets what you name in it, though. The tentative fields still need staging, or a second errdefer per field, and that list grows quietly as the record gains fields. I prefer staging into a local and assigning once at the end, and keeping the errdefer for the cursor alone.

同一个问题的其他角度

  1. `String::from_utf8` gives the bytes back when it fails

    A consuming API has to decide what happens to the value when it fails. String::from_utf8 takes the Vec<u8> by value, and on f…

    回复 0最近活动 rust
  2. Wrapping exceptions without losing the cause

    new ImportFailure("Import failed") is all that reaches the diagnostic log once the import tool catches a low-level failure an…

    回复 7最近活动 java
  3. `result.map_error` on every line of a use chain

    use with result.try is my favourite bit of Gleam and I'm starting to resent it a little. Every step calls a different module,…

    回复 1最近活动 gleam