binary: the unit goes in the name, and a short window stops being silent - #36
Merged
Conversation
binary.slice indexed in BITS while byte_size answered in BYTES, so a
byte-indexed caller got a short window and no error until offset+len passed
8*size. slice_bytes existed and was byte-indexed but returned a bare Binary
and collapsed every out-of-range case to <<>>. Two silences, and fixing
either alone steers callers into the other, so they are one change.
slice is DELETED rather than renamed. T-208 suggested renaming it to
slice_bits and adding a byte-indexed slice alongside; that is the worst
option on the table, because every existing call keeps compiling and
silently means something eight times different. Deleting the name is what
produces a compiler error on the old spelling. It cost two call sites:
24 of 26 in-tree slice calls were already slice_bytes.
slice_bits / slice_bytes / drop_bytes each carry the unit in the name.
slice_bytes is Result(Binary, Nil).
Unit types were considered and rejected with an argument, not on cost.
Scarlet can express `pub type Bytes { Bytes(Int) }` - Radix is that shape -
but cannot make it pay. A newtype at the leaf checks nothing: every source
of the quantity is a bare Int (byte_size, bit_size, byte_at, index_of), so
the caller writes Bytes(byte_size(b) - i - 1) and the x8 mistake lives
inside that expression exactly as before. Wrapping the sources too needs
+, - and comparison on Bytes, and Scarlet has no operator overloading;
every site touched here does arithmetic on the offset. T-286.
drop_bytes is new and takes a POSITION, not a length. That is what makes it
honestly total: there is no requested size for the answer to fall short of,
so past the end the remainder genuinely is <<>>. No take_bytes sibling on
purpose - clamping a requested length is a short read, which is the silence
this commit removes.
Plants, both directions, both watched. Re-adding slice as a bit-indexed
alias reds the new test rc=101. Restoring T-214's silence wearing a Result
(Err(Nil) -> Ok(<<>>)) reds it on the Err assertions. One assertion was
tightened after writing it: check_rejects(.., "slice") would have passed on
a diagnostic about slice_bits, since bare slice is a substring.
Four call sites now carry an Err(Nil) arm argued unreachable in a comment
rather than by a type - http.path, http.read_body, http_client's head_bytes,
drop_bytes' tail. That is the shape T-286 would close.
wire_format's parse_payload on 'a b=1' computed a negative-length window; it
used to yield <<>> and fail later in parse_int with the same message, and
now fails at the window. Same outcome, honest cause.
fmt 0, clippy -D warnings 0, test --workspace 0, gen-editor-syntax 0,
SCARLET_GC_STRESS=1 0. dylint/hawk/ts-corpus red identically on stashed
master - boundary experiment run, none of those files are in this diff.
alii
force-pushed
the
binary-slice-units
branch
from
August 13, 2026 04:47
b59d1a3 to
07753e0
Compare
alii
added a commit
that referenced
this pull request
Aug 13, 2026
#36 and #33 each merge clean and the merged tree does not build: 14 calls expect a bare Binary. Fourteen unwraps would restore exactly the silence #36 exists to remove, with a Result in the signature to make it look handled, so every site was classified instead. FIVE were the tail, slice_bytes(b, at, byte_size(b) - at). drop_bytes already names that operation and is honestly total, so they carry no Err arm at all: url.split_bracketed's rest, url.split_at_colon's port_text, url.port_of's digits, client.read_head's rest, http_response's wire_loop. ONE takes master's own idiom, result.then(slice_bytes(..), to_string) or '?', byte-identical to what #36 did to http_parse.scrl's twin line. ONE is a genuine error and is now visible. http_response's "consumed threads to the body" asserts a 5-byte window against a Content-Length of 5, so a consumed that does not thread there has to print as an error rather than as an empty window. SEVEN carry an Err arm argued unreachable, each naming the bound it rests on: an index_of hit, first_of's clamp to size, the leading-[ guard, the byte_size >= n guard, int.min. That is an argument in a comment rather than a type, which is T-286. Six of the seven fall through to a loud error anyway (UnsupportedScheme, EmptyHost, MalformedResponse); target_of's does not, and says so. url.scrl's malformed-URL cases turn out to be rejected before any window is computed — scheme_sep, split_bracketed and port_of all fail first — so none of its 8 became a new UrlError variant. A variant nothing can construct is worse than the comment it replaces. Not done here: those 7 are all one operation, "cut b at a position something else already proved is inside b". drop_bytes names the tail half and the head half has no name. T-309 carries the site list and the argument that split_at_bytes is total for drop_bytes's own reason, and is not the take_bytes #36 refused — window_bytes(b, from, to) is, which is why it is not that. dis.rs's pool ceiling re-measured on the merged tree: 714 entries, not the 712 the comment claimed. The 1000 ceiling is unchanged and not close. Plants, all watched red and restored. #33's load-bearing one first: dropping the whitespace-before-colon disjunct in vm/http.rs reds http_parse AND http_response from that one edit, rc=101 — the response head parser is still literally the request head parser after a 14-site edit. Then two of mine. Truncating the framed fixture prints "OUT OF RANGE" in place of a short window, rc=101, so the genuine error reaches the caller. Weakening read_exactly's guard to >= n - 1 prints Err MalformedResponse(BadFraming) in place of a truncated body, rc=101, so the argued-unreachable arm is live and loud when its bound is wrong. fmt 0, clippy -D warnings 0, test --workspace 0, gen-editor-syntax --check 0, SCARLET_GC_STRESS=1 0, dylint --all 0 with the cache cleared and sources touched (0 findings), hawk check -D warnings 0 (0 findings). Filed alongside: T-309 (split_at_bytes), T-310 (h1.scrl's HeadFlags still carries two connection bools where the Rust side is one ConnTokens).
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.
binary.sliceindexed in BITS whilebyte_sizeanswered in BYTES, so a byte-indexed caller got a short window and no error untiloffset+lenpassed8*size.slice_bytesexisted and was byte-indexed, but returned a bareBinaryand collapsed every out-of-range case to<<>>. Two silences — and fixing either alone steers callers straight into the other, which is why they are one change.Fixes T-208 and T-214.
sliceis deleted, not renamedT-208 suggested renaming it to
slice_bitsand adding a byte-indexedslicealongside. That is the worst option available: every existingbinary.slicecall keeps compiling and silently means something eight times different.Deleting the name is what produces a compiler error on the old spelling — the property this project accepts as proof. It cost two call sites, because almost nobody used the bit form: 24 of 26 in-tree slice calls were already
slice_bytes.slice_bits/slice_bytes/drop_byteseach carry the unit in the name.slice_bytesreturnsResult(Binary, Nil).Unit types were rejected with an argument, not on cost
Scarlet can express
pub type Bytes { Bytes(Int) }—Radixis that shape. It cannot make it pay.A newtype at the leaf checks nothing: for the type to protect anything it must reach the source of the quantity, and every source is a bare
Int(byte_size,bit_size,byte_at,index_of -> Option(Int)). Leave those and the caller writesBytes(byte_size(b) - i - 1)— the ×8 mistake lives inside that wrapping expression exactly as before. Wrap the sources too and you need+,-and comparison onBytes, and Scarlet has no operator overloading. Every site touched here does arithmetic on the offset.Filed as T-286, including the residual hole: nothing stops a bit count being passed to
slice_bytes.drop_bytestakes a position, not a lengthslice_bytes(b, i + 1, byte_size(b) - i - 1)was 7 of the 24 call sites, and that arithmetic is itself where an off-by-one lives.drop_bytes(b, at)is total honestly: there is no requested size for the answer to fall short of, so past the end the remainder genuinely is<<>>.No
take_bytessibling, deliberately — clamping a requested length is a short read, which is the silence this PR removes.Plants, both directions, watched
sliceas a bit-indexed aliasexpected 'al check' to REJECTResult(Err(Nil)→Ok(<<>>))Err(Nil)assertionsBefore/after on the T-214 case, with the in-range control on the same code path:
One assertion was tightened after being written:
check_rejects(…, "slice")would have passed on a diagnostic aboutslice_bits, since baresliceis a substring.Gates
fmt0 ·clippy -- -D warnings0 ·test --workspace0 ·gen-editor-syntax --check0 ·SCARLET_GC_STRESS=1 cargo test --workspace0.dylint,hawkandts-corpusare red — and identically red on stashed master. The boundary experiment was run rather than argued from the file list: dylint is theHeadFlagsfinding (T-278/T-279, in flight), hawk is 3unnecessary_restricted_visibilityinvm/mailbox.rsandvm/supervision.rs, ts-corpus is 2 parse errors inexamples/supervision.scrlandexamples/chat.scrl. None of those files are in this diff, and every.scrlfile that is parses clean under tree-sitter.One golden moved, four lines of 67
wire_format.stdout— the T-214 fix itself (past the end: <<>>→Err(Nil)) plus three new demo lines. Not a type-id shift, so line-for-line rather than a shift proof. Thefield name:block restructured into amatchproduces byte-identical output.Known residual
Four call sites now carry an
Err(Nil)arm argued unreachable in a comment rather than by a type —http.path,http.read_body,http_client'shead_bytes, anddrop_bytes' own tail. In each the bound comes fromindex_oforbyte_size(buf) - consumed. That is exactly the shape T-286 would close.A
split_at_bytes(b, at) (Binary, Binary)primitive would collapse theindex_of+ prefix + suffix idiom at ~6 sites and remove three of those four arms. Not built — a third new name is outside what T-208 asked for.