Lowent Manual←↑→

strings — string view operations

Source
lib/str.low
Layer
L0 — pure computation
Capabilities
none

Compares, searches, trims and splits strings without copying. Use it to split input on spaces, strip a prefix, or find a substring. The result is always a view over the original — a window pointing at someone else’s bytes — and not a single byte of new memory is used.

use strings .

if strings.starts_with line "GET " . do
  let rest slice u8 be strings.remove_prefix line "GET " .
end

rest points directly at part of line. This module was built only from what the language already has (len, index, subslice, eq, option, guard, while, actor) and adds no builtin. Most string operations need no ownership — cutting, searching and comparing only produce views. Work that needs a writable buffer, such as appending, belongs to strbuf.

Design and boundaries#

type str slice u8 . is a transparent alias. It is not a newtype, so string literals, slice u8 values and subslice results all fit a str position as they are. Only the str_splitter actor holds state — the original src, the separator byte sep, the cursor pos and the exhausted flag fin.

Ops at a glance#

Every fn is effects none, and the actor’s two procs are effects state. Call them qualified by module name, as in strings.find.

opSignatureWhen it cannot
eq_str(a str, b str) → boolfalse if different
has_byte(s str, b u8) → boolfalse if absent
find(hay str, needle str, from u64) → option u64none if not found or from > len hay
has(hay str, needle str) → bool—
starts_with(s str, prefix str) → bool—
ends_with(s str, suffix str) → bool—
remove_prefix(s str, prefix str) → strThe original as is if no prefix
remove_suffix(s str, suffix str) → strThe original as is if no suffix
trim_start(s str, cut str) → strAn empty view if everything is cut
trim_end(s str, cut str) → strAn empty view if everything is cut
split_next(src str, sep u8, pos u64) → option strnone if pos > len src
str_splitteractor: init(s2 slice u8, b u8) → u64 · next → option slice u8next is none after exhaustion

Table 50.1 — Ops of strings

Ops in detail#

The slice u8 passed in is a view, and what comes back is another view over the same bytes. That is why it is not mut, is effects none, and is valid only while the original lives. The two arguments of comparison and search are always (the thing searched, the thing sought) — has_byte s b reads “is b in s”.

eq_str — byte-wise equality. If lengths differ it is false before looking at bytes. The builtin eq is scalar-only, so slice comparison lives here. That is why the name could not be shortened to eq.

has_byte — is the single byte b in s. A byte, not a string. The cut-set test of trim_start and trim_end uses it.

find — searches hay for needle from from and gives the start index as some. Why from exists is the character of this module: the cursor is a value the caller holds. The next search calls again with the position after the match as from. The library hides state nowhere, so scanning the same string from several places at once causes no interference. from > len hay gives none, and an empty needle gives some from.

has — when you only ask “is it in there”. Inside it is is_some (find hay needle 0).

starts_with · ends_with — prefix and suffix tests. If the piece is longer than s, it is false without looking.

remove_prefix · remove_suffix — a view with the prefix or suffix removed. If the piece is not there, the original is returned as is. That is not failure, so the return is not an option. If you need to know whether it was removed, ask starts_with or ends_with first.

trim_start · trim_end — a view with certain bytes stripped from the front or back. cut is not an ordered string but a set of bytes — " \t" means “space or tab”, not “space followed by tab”. If everything is cut, a zero-length view comes out.

split_next — when splitting src on the separator byte sep, gives as some the piece from pos up to the next sep. Advancing the cursor is the caller’s job — on receiving a piece, push pos ← pos + len(piece) + 1 yourself (+1 for the separator byte). The last piece with no sep after it is also some, and pos > len src giving none is the end signal. "a,b" gives "a", "b"; "a," gives "a", "" (a trailing empty piece); "" gives one "".

str_splitter — the pull version of split_next. The actor’s state holds the cursor. After putting the original and separator into init, next takes no arguments, giving pieces as option until it is exhausted and gives none.

Using it#

The module name is the module declaration in the file. The module of lib/str.low is strings.

use strings from "lib/str.low" .   rem write the path directly (relative to the using file)
use strings .                            rem resolved from the standard module location
use strings as s .                       rem alias --- then call s.eq_str

Finding and splitting:

module demo .

use strings .

fn t_find output u64 . do
  let r option u64 . be strings.find "hello world" "world" 0 .
  guard is_some r . else return 99 .
  return some_value r .
end

fn t_split output u64 . do
  rem "aa,b,,cc" split on ',' (byte 44): "aa", "b", "", "cc" --- four pieces
  var pos u64 be 0 .
  var pieces u64 be 0 .
  var r option slice u8 . be strings.split_next "aa,b,,cc" 44 pos .
  while is_some r . do
    set pieces (add pieces 1) .
    set pos (add pos (add (len (some_value r)) 1)) .
    set r (strings.split_next "aa,b,,cc" 44 pos) .
  end
  return pieces .
end

t_find gives 6 and t_split gives 4. If you want state, use the actor version — the actor holds the cursor.

proc t_splitter output u64 . effects state . do
  var sp strings.str_splitter . be spawn actor strings.str_splitter .
  let d u64 be send sp init "one,two,three" 44 .
  var pieces u64 be 0 .
  var r option slice u8 . be send sp next .
  while is_some r . do
    set pieces (add pieces 1) .
    set r (send sp next) .
  end
  return pieces .
end

The only difference between the two is who holds the cursor. If the same original must be scanned along several paths at once, split_next fits; if a single loop just pulls to the end, the actor version is shorter.

Counter-examples#

Counter-example. Comparing slices with the builtin eq

guard eq "abc" "abc" . else return 0 .        rem ✗ eq is scalar-only

It is E-VM-TYPE. Compare slices with strings.eq_str.

Counter-example. Not advancing the cursor in a split loop

var r option slice u8 . be strings.split_next src 44 pos .
while is_some r . do
  set r (strings.split_next src 44 pos) .   rem ✗ pos stays the same
end

It is not a translation error — the program never ends. Giving the same pos yields the same piece forever. Advance by pos + len(piece) + 1 each time.

Counter-example. Using an option as a value

let r option u64 . be strings.find "abc" "zz" 0 .
return some_value r .                         rem ✗ no none check

It translates, and stops at run time with E-VM-NONE the moment nothing is found. Testing only with inputs that are found never reveals it. guard is_some r . else … comes first (chapter 11).

Counter-example. Changing the original a view points at, later

let piece slice u8 be strings.remove_prefix line "GET " .
set (index line 4) 88 .                                     rem ✗ the original was changed

With no error, the content of piece silently changes — because it is a window, not a copy. To hold on to the content, copy it with strbuf.

Cautions#