Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion crates/scarlet/tests/golden/wire_format.stdout
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ field name: Ok(Content-Length)
lowercased: Ok(content-length)
is content-length: True
is content-type: False
past the end: <<>>
past the end: Err(Nil)
one byte over: Err(Nil)
drop 36: <<102, 13, 10>>
drop 900: <<>>
byte_at 0: 67
byte_at 900: -1
parse_int(Dec): Ok(4210)
Expand Down
3 changes: 2 additions & 1 deletion crates/scarlet/tests/programs/http_parse.scrl
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// the connection driver against a loopback listener.

import scarlet/binary
import scarlet/result
import scarlet/string
import scarlet/array
import scarlet/http/h1.{
Expand Down Expand Up @@ -55,7 +56,7 @@ fn vis_from(b Binary, i Int, len Int, acc String) String {
10 -> '\\n'
9 -> '\\t'
0 -> '\\0'
_ -> binary.to_string(binary.slice_bytes(b, i, 1)) or '?'
_ -> result.then(binary.slice_bytes(b, i, 1), binary.to_string) or '?'
}
vis_from(b, i + 1, len, '${acc}${piece}')
}
Expand Down
2 changes: 1 addition & 1 deletion crates/scarlet/tests/programs/numerics.scrl
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ fn parse_int(s String) Result(Int, Nil) {
fn parse_signed(s String) Result(Int, Nil) {
b = binary.from_string(s)
if binary.byte_size(b) > 0 && binary.byte_at(b, 0) == 45 {
rest = binary.slice_bytes(b, 1, binary.byte_size(b) - 1)
rest = binary.drop_bytes(b, 1)
result.map(binary.parse_int(rest, Dec), fn(n) -n)
} else {
binary.parse_int(b, Dec)
Expand Down
91 changes: 87 additions & 4 deletions crates/scarlet/tests/stdlib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -394,11 +394,11 @@ fn stdlib_binary() {
"import scarlet/binary\n\
pub fn main() {\n\
\tb = binary.from_string('ABC')\n\
\tprintln(binary.slice(b, 8, 8))\n\
\tprintln(binary.slice(b, 0, 99))\n\
\tprintln(binary.slice_bits(b, 8, 8))\n\
\tprintln(binary.slice_bits(b, 0, 99))\n\
\tjoined = binary.append(binary.from_string('AB'), binary.from_string('C'))\n\
\tprintln(binary.to_string(joined))\n\
\tprintln(binary.bit_size(binary.slice(b, 0, 5) or binary.from_string('')))\n\
\tprintln(binary.bit_size(binary.slice_bits(b, 0, 5) or binary.from_string('')))\n\
}\n",
"Ok(<<66>>)\nErr(Nil)\nOk(ABC)\n5\n",
);
Expand Down Expand Up @@ -447,7 +447,7 @@ fn stdlib_binary() {
run_outputs(
"import scarlet/binary\n\
pub fn main() {\n\
\tprintln(binary.slice(binary.from_string('ABC'), 0 - 1, 8))\n\
\tprintln(binary.slice_bits(binary.from_string('ABC'), 0 - 1, 8))\n\
}\n",
"Err(Nil)\n",
);
Expand All @@ -458,6 +458,89 @@ fn stdlib_binary() {
);
}

/// The unit a window is measured in lives in the function's name, and the two
/// names differ by a factor of eight. Before this, `slice` was bit-indexed and
/// `byte_size` answered in bytes, so a byte-indexed caller of `slice` read an
/// eighth of the window it asked for and was told nothing: `slice(b, 0, 2)`
/// answered `Ok(<<0:size(2)>>)`, a valid binary, rather than the first two
/// bytes. The old spelling no longer exists, so that call is now a compile
/// error rather than a wrong answer.
#[test]
fn stdlib_binary_slice_units() {
// The name `slice` is gone. This is the guard on reintroducing it: a
// function of that name could only pick one of the two units, and the
// callers meaning the other one would keep compiling.
check_rejects(
"import scarlet/binary\n\
pub fn main() { println(binary.slice(<<1, 2, 3, 4, 5>>, 0, 2)) }\n",
// Spelled out: bare `slice` is a substring of `slice_bits`, so the
// loose form would pass on a diagnostic about a different name.
"has no member 'slice'",
);
// Both units, over the same measured windows from T-208, side by side.
run_outputs(
"import scarlet/binary\n\
pub fn main() {\n\
\tb = <<1, 2, 3, 4, 5>>\n\
\tprintln(binary.slice_bytes(b, 0, 2))\n\
\tprintln(binary.slice_bits(b, 0, 2))\n\
\tprintln(binary.slice_bytes(b, 1, 2))\n\
\tprintln(binary.slice_bits(b, 8, 16))\n\
\tprintln(binary.slice_bytes(b, 0, 5))\n\
\tprintln(binary.slice_bits(b, 0, 40))\n\
}\n",
"Ok(<<1, 2>>)\nOk(<<0:size(2)>>)\nOk(<<2, 3>>)\nOk(<<2, 3>>)\n\
Ok(<<1, 2, 3, 4, 5>>)\nOk(<<1, 2, 3, 4, 5>>)\n",
);
// T-214: every out-of-range case that used to collapse to `<<>>` — one
// byte over, wholly past the end, negative offset, negative length. The
// last two are why a Gleam `slice(b, byte_size(b), -n)` cannot be
// transliterated: it is an error here, not a backwards window.
run_outputs(
"import scarlet/binary\n\
pub fn main() {\n\
\tb = <<1, 2, 3, 4, 5>>\n\
\tprintln(binary.slice_bytes(b, 4, 1))\n\
\tprintln(binary.slice_bytes(b, 4, 2))\n\
\tprintln(binary.slice_bytes(b, 5, 1))\n\
\tprintln(binary.slice_bytes(b, 0 - 1, 2))\n\
\tprintln(binary.slice_bytes(b, 5, 0 - 4))\n\
}\n",
"Ok(<<5>>)\nErr(Nil)\nErr(Nil)\nErr(Nil)\nErr(Nil)\n",
);
// `byte_size` rounds up, so the last byte of a 12-bit binary is not a
// whole byte and is not addressable by slice_bytes. Reaching it is what
// slice_bits is for.
run_outputs(
"import scarlet/binary\n\
pub fn main() {\n\
\tb = <<1, 2:4>>\n\
\tprintln(binary.byte_size(b))\n\
\tprintln(binary.slice_bytes(b, 0, 2))\n\
\tprintln(binary.slice_bits(b, 8, 4))\n\
}\n",
"2\nErr(Nil)\nOk(<<2:size(4)>>)\n",
);
// drop_bytes is total because it is given a position, not a length: there
// is no requested size for the answer to fall short of. Past the end the
// remainder really is empty, and it reaches the trailing bits that
// slice_bytes cannot.
run_outputs(
"import scarlet/binary\n\
import scarlet/string\n\
pub fn main() {\n\
\tb = <<1, 2, 3, 4, 5>>\n\
\tprintln(string.inspect(binary.drop_bytes(b, 0)))\n\
\tprintln(string.inspect(binary.drop_bytes(b, 3)))\n\
\tprintln(string.inspect(binary.drop_bytes(b, 5)))\n\
\tprintln(string.inspect(binary.drop_bytes(b, 900)))\n\
\tprintln(string.inspect(binary.drop_bytes(b, 0 - 2)))\n\
\tprintln(binary.bit_size(binary.drop_bytes(<<1, 2:4>>, 1)))\n\
}\n",
"<<1, 2, 3, 4, 5>>\n<<4, 5>>\n<<>>\n<<>>\n<<1, 2, 3, 4, 5>>\n4\n",
);
}

#[test]
fn stdlib_binary_byte_at() {
// byte_at is -1 out of bounds on both sides; a view reads through its
Expand Down
2 changes: 1 addition & 1 deletion crates/scarlet_core/src/bytecode/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ fn builtin_op(name: &str) -> Option<Op> {
"binary__to_string" => Op::BinToString,
"binary__bit_size" => Op::BinBitSize,
"binary__byte_size" => Op::BinByteSize,
"binary__slice" => Op::BinSlice,
"binary__slice_bits" => Op::BinSlice,
"binary__append" => Op::BinAppend,
"binary__index_of" => Op::BinIndexOf,
"binary__byte_at" => Op::BinByteAt,
Expand Down
56 changes: 46 additions & 10 deletions crates/scarlet_core/src/std/scarlet/binary.scrl
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,53 @@ pub fn bit_size(b Binary) Int
@vm(binary__byte_size)
pub fn byte_size(b Binary) Int

@vm(binary__slice)
pub fn slice(b Binary, at_bit Int, take_bits Int) Result(Binary, Nil)
// The window `b[at_bit .. at_bit+take_bits]`, in BITS. O(1): shares `b`'s
// backing and copies nothing. A negative bound, or one that runs past
// `bit_size(b)`, is `Err(Nil)` — never a short read.
//
// Every offset and length in this module states its unit in its own name,
// because the two differ by a factor of 8 and a mistaken window is a valid
// binary rather than a failure. There is deliberately no function called
// `slice`: it used to mean bits, and a name that changed unit would leave
// working code silently reading an eighth of what it asked for.
@vm(binary__slice_bits)
pub fn slice_bits(b Binary, at_bit Int, take_bits Int) Result(Binary, Nil)

// Byte-oriented slice of `b[start .. start+len]` (offsets and length in bytes;
// `slice` itself takes bits). O(1): shares `b`'s backing and copies nothing.
// An out-of-range request yields an empty binary, so a parser can take field
// views by offset without per-call error handling.
pub fn slice_bytes(b Binary, start Int, len Int) Binary {
match slice(b, start * 8, len * 8) {
Ok(v) -> v
Err(Nil) -> <<>>
// The window `b[at .. at+len]`, in BYTES. O(1): shares `b`'s backing and
// copies nothing. A negative bound, or one that runs past the end, is
// `Err(Nil)`: a length that came off the wire is exactly the one that must
// not fail quietly.
//
// `byte_size` rounds up, so the last byte of a binary whose bit length is not
// a multiple of 8 is not addressable here — ask for it with `slice_bits`.
pub fn slice_bytes(b Binary, at Int, len Int) Result(Binary, Nil) {
slice_bits(b, at * 8, len * 8)
}

// Everything from byte `at` onwards. Total, and honestly so: past the end the
// remainder really is `<<>>`, and a negative `at` drops nothing — there is no
// requested length for the answer to fall short of, so nothing is swallowed.
// Use it instead of `slice_bytes(b, at, byte_size(b) - at)`, which repeats the
// bound the callee already knows.
pub fn drop_bytes(b Binary, at Int) Binary {
total = bit_size(b)
// Clamped before the ×8 so a huge `at` cannot wrap into a negative offset.
skip = if at <= 0 {
0
} else if at >= byte_size(b) {
total
} else {
at * 8
}
if skip >= total {
<<>>
} else {
match slice_bits(b, skip, total - skip) {
Ok(v) -> v
// Unreachable: 0 <= skip < total == bit_size(b), so the window is
// exactly the tail. The arm exists because Result has two of them.
Err(Nil) -> <<>>
}
}
}

Expand Down
11 changes: 5 additions & 6 deletions crates/scarlet_core/src/std/scarlet/decimal.scrl
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ fn pow10(n Int) Int {
}

fn tail(b Binary) Binary {
binary.slice_bytes(b, 1, binary.byte_size(b) - 1)
binary.drop_bytes(b, 1)
}

fn parse_unsigned(b Binary) Option(Decimal) {
Expand All @@ -324,11 +324,10 @@ fn parse_unsigned(b Binary) Option(Decimal) {
Ok(n) -> Some(Decimal(n, 0))
Err(Nil) -> None
}
Some(i) ->
parse_parts(
binary.slice_bytes(b, 0, i),
binary.slice_bytes(b, i + 1, binary.byte_size(b) - i - 1),
)
Some(i) -> match binary.slice_bytes(b, 0, i) {
Ok(whole) -> parse_parts(whole, binary.drop_bytes(b, i + 1))
Err(Nil) -> None
}
}
}

Expand Down
21 changes: 15 additions & 6 deletions crates/scarlet_core/src/std/scarlet/http.scrl
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,12 @@ fn to_method(m Binary) Method {
// first `?`, or the whole target if there is no query. O(1) zero-copy slice.
pub fn path(r Request) Binary {
match binary.index_of(r.target, QUESTION, 0) {
Some(i) -> binary.slice_bytes(r.target, 0, i)
// `index_of` answers with a byte index inside `r.target`, so `0..i` is
// in range and the Err arm is unreachable.
Some(i) -> match binary.slice_bytes(r.target, 0, i) {
Ok(p) -> p
Err(Nil) -> EMPTY
}
None -> r.target
}
}
Expand All @@ -171,7 +176,7 @@ pub fn path(r Request) Binary {
// (not including it), or empty if there is no `?`.
pub fn query(r Request) Binary {
match binary.index_of(r.target, QUESTION, 0) {
Some(i) -> binary.slice_bytes(r.target, i + 1, binary.byte_size(r.target) - i - 1)
Some(i) -> binary.drop_bytes(r.target, i + 1)
None -> EMPTY
}
}
Expand Down Expand Up @@ -348,11 +353,10 @@ fn flush(sock Socket, pending Array(Binary)) Result(Nil, NetError) {
// concatenate, so the freshly read bytes ARE the new buffer: taking that exit
// keeps the read off the copy path entirely.
fn carry(buf Binary, off Int, more Binary) Binary {
rest = binary.byte_size(buf) - off
if rest == 0 {
if binary.byte_size(buf) - off == 0 {
more
} else {
binary.append(binary.slice_bytes(buf, off, rest), more)
binary.append(binary.drop_bytes(buf, off), more)
}
}

Expand Down Expand Up @@ -421,7 +425,12 @@ fn read_body(
_ <- result.then(maybe_continue(sock, head.flags))
avail = binary.byte_size(buf) - consumed
buffered = int.min(avail, n)
head_bytes = binary.slice_bytes(buf, consumed, buffered)
// `buffered <= avail == byte_size(buf) - consumed`, so the window ends at
// or before the end of `buf` and the Err arm is unreachable.
head_bytes = match binary.slice_bytes(buf, consumed, buffered) {
Ok(v) -> v
Err(Nil) -> <<>>
}
need = n - buffered
tail <- result.then(body.collect(body.content_length(sock, need, deadline), MAX_BODY))
// Pending responses were flushed before the body was read, so this
Expand Down
21 changes: 17 additions & 4 deletions crates/scarlet_core/src/std/scarlet/http/headers.scrl
Original file line number Diff line number Diff line change
Expand Up @@ -182,17 +182,30 @@ fn has_token(v Binary, token Binary) Bool {
value_has_token(v, 0, binary.byte_size(v), token)
}

// Whether `v[lo..hi]` is `token`, case-insensitively. Empty list elements
// (`a,,b`, a trailing comma, an empty value) are ignored per RFC 9110 —
// without the `hi > lo` guard an empty element would "match" an empty token.
// A window past the end of `v` is likewise not the token; callers pass bounds
// derived from `byte_size(v)`, so that arm should be unreachable.
fn element_is(v Binary, lo Int, hi Int, token Binary) Bool {
if hi <= lo {
False
} else {
match binary.slice_bytes(v, lo, hi - lo) {
Ok(e) -> binary.eq_ignore_ascii_case(e, token)
Err(Nil) -> False
}
}
}

fn value_has_token(v Binary, from Int, len Int, token Binary) Bool {
to = match binary.index_of(v, comma, from) {
Some(i) -> i
None -> len
}
lo = skip_ows(v, from, to)
hi = trim_ows(v, lo, to)
// Empty list elements (`a,,b`, a trailing comma, an empty value) are
// ignored per RFC 9110 — without the hi > lo guard an empty element would
// "match" an empty token.
if hi > lo && binary.eq_ignore_ascii_case(binary.slice_bytes(v, lo, hi - lo), token) {
if element_is(v, lo, hi, token) {
True
} else if to < len {
value_has_token(v, to + 1, len, token)
Expand Down
2 changes: 1 addition & 1 deletion crates/scarlet_core/src/std/scarlet/json/decode.scrl
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ fn parse_signed(s String) Result(Int, Nil) {
b = binary.from_string(s)
// 45 is `-`.
if binary.byte_at(b, 0) == 45 {
match binary.parse_int(binary.slice_bytes(b, 1, binary.byte_size(b) - 1), binary.Dec) {
match binary.parse_int(binary.drop_bytes(b, 1), binary.Dec) {
Ok(n) -> Ok(0 - n)
Err(Nil) -> Err(Nil)
}
Expand Down
6 changes: 3 additions & 3 deletions crates/scarlet_vm/src/vm/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,9 @@ impl VM {
}

pub(super) fn bin_slice(&mut self) -> VmResult<()> {
let take = self.pop_int("binary.slice")?;
let at = self.pop_int("binary.slice")?;
let bin_v = self.pop_binary("binary.slice")?;
let take = self.pop_int("binary.slice_bits")?;
let at = self.pop_int("binary.slice_bits")?;
let bin_v = self.pop_binary("binary.slice_bits")?;
let bin = bin_ref(&bin_v);
let v = if at < 0 || take < 0 || (at as u64) + (take as u64) > bin.bit_len() {
self.make_err_nil()?
Expand Down
Loading
Loading