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 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, idx, subslice, eq, option, guard, while, actor) and adds no core op. 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#

There is no type name for strings — the ops take and return slice u8, so string literals, slice u8 values and subslice results all fit as they are. A slice cannot be given another name with type (chapter 13). 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 slice u8, b slice u8) → boolfalse if different
has_byte(s slice u8, b u8) → boolfalse if absent
find(hay slice u8, needle slice u8, from usize) → option usizenone if not found or from > len hay
has(hay slice u8, needle slice u8) → bool—
starts_with(s slice u8, prefix slice u8) → bool—
ends_with(s slice u8, suffix slice u8) → bool—
remove_prefix(s slice u8, prefix slice u8) → slice u8The original as is if no prefix
remove_suffix(s slice u8, suffix slice u8) → slice u8The original as is if no suffix
trim_start(s slice u8, cut slice u8) → slice u8An empty view if everything is cut
trim_end(s slice u8, cut slice u8) → slice u8An empty view if everything is cut
split_next(src slice u8, sep u8, pos usize) → option slice u8none 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 core op 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 usize . do
  let r option usize 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 usize 0 .
  var pieces u64 0 .
  var r option slice u8 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 spawn actor strings.str_splitter . .
  let d u64 send sp. init "one,two,three" 44 . .
  var pieces u64 0 .
  var r option slice u8 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 core op 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 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 usize 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 strings.remove_prefix line. "GET " . .
set idx 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#