tur/string-slice
StringSlice: a zero-copy, bounds-checked view into a String.
A StringSlice is a { parent String; offset; len } view that RETAINS its parent String. Because String is immutable and refcounted, the borrowed byte range stays valid and unchanging for the slice's lifetime -- so this is genuinely safe zero-copy ranged access, unlike a raw pointer+len view over a cstr whose backing buffer can be freed or mutated underneath it.
Sub-slicing (slice/sub) is O(1): a new view over the same parent. Indexing (slice/byte-at) is bounds-checked (returns -1 out of range). Materialize to an owned String (slice/to-string) or a fresh cstr (slice/to-cstr) only at the boundary where you need one.
This supersedes the inert borrowed-view str (stdlib/str.tur) for the safe, owned case: prefer a String + StringSlice when the view must not outlive -- or be invalidated by -- its source. str remains for zero-ownership interop over a caller-managed buffer.
Every op has an interpreter native (src/turi/string_native.c), so StringSlice behaves identically compiled and under --interpret.
Since: v2 (owned-string-type-plan -- String slices)
StringSlice
(defopaque StringSlice :ptr<void>)
string/slice
(string/slice [s :String off :int len :int] :StringSlice)
a zero-copy view of String s over bytes [off, off+len).
The slice retains s, so the range stays valid until the slice is released. off/len are clamped to s's bounds, so the result is always a valid view.
| s | the parent String (retained by the slice) | |
| off | start byte offset (clamped into [0, len s]) | |
| len | number of bytes (clamped so off+len <= len s) |
A StringSlice viewing the requested range.
(slice/to-cstr (string/slice (string/from-cstr "hello world") 6 5)) ; => "world"
Since: v2
string/slice-cstr
(string/slice-cstr [s :cstr off :int len :int] :StringSlice)
slice a raw cstr safely by copying it into an owned String the slice solely owns (freed when the slice is released).
Use this to get safe ranged access to a cstr without worrying about the cstr's lifetime: the bytes are copied once, and the copy lives exactly as long as the slice (and its sub-slices).
Since: v2
slice/retain
(slice/retain [sl :StringSlice] :StringSlice)
increment the slice's refcount, returning a second handle.
Since: v2
slice/release
(slice/release [sl :StringSlice] :void)
decrement the slice's refcount; at zero, release the parent.
Since: v2
slice/len
(slice/len [sl :StringSlice] :int)
byte length of the slice.
Since: v2
slice/empty?
(slice/empty? [sl :StringSlice] :bool)
true iff the slice has length zero.
Since: v2
slice/byte-at
(slice/byte-at [sl :StringSlice i :int] :int)
the byte (0..255) at index i, or -1 if out of range.
Since: v2
slice/byte-is?
(slice/byte-is? [sl :StringSlice i :int c :Char] :bool)
is the byte at index i of the slice the character c?
string/byte-is? for a slice. The index is relative to the slice, and so is the bounds check: a byte of the parent String outside the view is out of range. c should be ASCII, as for string/byte-is?.
| sl | the slice to look in (borrowed) | |
| i | a byte index into the slice; out of range is not an error | |
| c | the character to compare with |
true iff 0 <= i < (slice/len sl) and the byte at i is c's code.
(slice/byte-is? (string/slice (string/from-cstr "a,b") 1 2) 0 #\,) ; => true
Since: markdown-library-plan S1
slice/char-at
(slice/char-at [sl :StringSlice i :int] :(Option Char))
the byte at index i of the slice as a Char, or (none).
string/char-at for a slice; the index and the bounds are the slice's.
| sl | the slice to look in (borrowed) | |
| i | a byte index into the slice; out of range is not an error |
(some c) holding the byte at i, or (none) when i is out of range.
(slice/char-at (string/slice (string/from-cstr "hi") 1 1) 0) ; => (some #\i)
Since: markdown-library-plan S1
slice/-match-at
(slice/-match-at [sl :StringSlice p :String at :int k :int n :int] :bool)
internal: do bytes [k, n) of p equal sl's from at+k on?
slice/starts-with-at?
(slice/starts-with-at? [sl :StringSlice prefix :String at :int] :bool)
does prefix occur in the slice at byte offset at?
string/starts-with-at? for a slice.
| sl | the slice to look in (borrowed) | |
| prefix | the bytes to look for (borrowed) | |
| at | the byte offset in the slice to test |
true iff the slice's bytes from at on begin with prefix.
(slice/starts-with-at? (string/slice (string/from-cstr "x <!-- y") 2 6) (string/from-cstr "<!--") 0) ; => true
Since: markdown-library-plan S2
slice/-index-of-loop
(slice/-index-of-loop [sl :StringSlice needle :String i :int last :int] :(Option int))
internal: first match of needle at i..last.
slice/index-of-from
(slice/index-of-from [sl :StringSlice needle :String start :int] :(Option int))
offset of the first needle at or after start.
string/index-of-from for a slice.
| sl | the slice to search (borrowed) | |
| needle | the bytes to find (borrowed) | |
| start | the first slice offset a match may begin at |
(some i) for the lowest i >= start where needle occurs, else (none).
(slice/index-of-from (string/slice (string/from-cstr "a,b,c") 0 5) (string/from-cstr ",") 2) ; => (some 3)
Since: markdown-library-plan S2
slice/index-of
(slice/index-of [sl :StringSlice needle :String] :(Option int))
offset of the first occurrence of needle in the slice.
string/index-of for a slice.
| sl | the slice to search (borrowed) | |
| needle | the bytes to find (borrowed) |
(some i) for the lowest i where needle occurs, or (none).
(slice/index-of (string/slice (string/from-cstr "<p>a</p>") 3 5) (string/from-cstr "</")) ; => (some 1)
Since: markdown-library-plan S2
slice/-last-index-of-loop
(slice/-last-index-of-loop [sl :StringSlice needle :String i :int] :(Option int))
internal: last match of needle at i down to 0.
slice/last-index-of
(slice/last-index-of [sl :StringSlice needle :String] :(Option int))
offset of the last occurrence of needle in the slice.
string/last-index-of for a slice.
| sl | the slice to search (borrowed) | |
| needle | the bytes to find (borrowed) |
(some i) for the highest i where needle occurs, or (none).
(slice/last-index-of (string/slice (string/from-cstr "a/b/c/") 0 5) (string/from-cstr "/")) ; => (some 3)
Since: markdown-library-plan S2
slice/-index-of-byte-loop
(slice/-index-of-byte-loop [sl :StringSlice b :int i :int n :int] :(Option int))
internal: first i in [i, n) holding byte b.
slice/index-of-char-from
(slice/index-of-char-from [sl :StringSlice c :Char start :int] :(Option int))
offset of the first c at or after start.
string/index-of-char-from for a slice.
| sl | the slice to search (borrowed) | |
| c | the character to find (ASCII) | |
| start | the first slice offset to look at |
(some i) for the lowest i >= start holding c, or (none).
(slice/index-of-char-from (string/slice (string/from-cstr "a]b]") 0 4) #\] 2) ; => (some 3)
Since: markdown-library-plan S2
slice/index-of-char
(slice/index-of-char [sl :StringSlice c :Char] :(Option int))
offset of the first c in the slice.
string/index-of-char for a slice.
| sl | the slice to search (borrowed) | |
| c | the character to find (ASCII) |
(some i) for the lowest i holding c, or (none).
(slice/index-of-char (string/slice (string/from-cstr "x [a] y") 2 3) #\]) ; => (some 2)
Since: markdown-library-plan S2
slice/-span-loop
(slice/-span-loop [sl :StringSlice pred :(fn [Char] bool) i :int n :int] :int)
internal: first i in [i, n) whose byte fails pred, else n.
slice/span-while
(slice/span-while [sl :StringSlice start :int pred :(fn [Char] bool)] :int)
the slice index where a run satisfying pred ends.
string/span-while for a slice: the run stops at the slice's end, not its parent's.
| sl | the slice to scan (borrowed) | |
| start | the slice offset to start at (clamped into [0, (slice/len sl)]) | |
| pred | the test each byte must pass to continue the run |
The end of the run: an index in [start, (slice/len sl)].
(slice/span-while (string/slice (string/from-cstr "x abc") 1 5) 0 space?) ; => 2
Since: markdown-library-plan S2
slice/-run-loop
(slice/-run-loop [sl :StringSlice b :int i :int n :int] :int)
internal: first i in [i, n) not holding byte b, else n.
slice/run-length
(slice/run-length [sl :StringSlice start :int c :Char] :int)
how many c in a row start at start in the slice.
string/run-length for a slice: the run stops at the slice's end.
| sl | the slice to scan (borrowed) | |
| start | the slice offset the run starts at | |
| c | the character the run is made of (ASCII) |
The run's length in bytes, 0 or more.
(slice/run-length (string/slice (string/from-cstr "````x") 0 3) 0 #\`) ; => 3
Since: markdown-library-plan S2
slice/-new-vec
(slice/-new-vec :(Vec StringSlice))
internal: an empty (Vec StringSlice) (a let-bound vec-new is not inferred).
string/-line-slices-loop
(string/-line-slices-loop [s :String out :(Vec StringSlice) start :int i :int n :int] :(Vec StringSlice))
internal: push views of s's lines from start into out.
string/line-slices
(string/line-slices [s :String] :(Vec StringSlice))
zero-copy views of s's lines, without their endings.
string/lines without the copies: the same LF / CRLF / CR rule, the same lines, but each is a StringSlice viewing s. This is the form a parser wants for its input: one String for the document, one O(1) view per line.
| s | the String to split (borrowed; each slice retains it) |
A fresh (Vec StringSlice), possibly empty. Free it with slice/release-all.
(vec-len (string/line-slices (string/from-cstr "# Title\n\nBody\n"))) ; => 3
Since: markdown-library-plan S3
slice/-split-char-loop
(slice/-split-char-loop [sl :StringSlice c :Char out :(Vec StringSlice) start :int] :(Vec StringSlice))
internal: split sl on c from start into out.
slice/split-char
(slice/split-char [sl :StringSlice c :Char] :(Vec StringSlice))
zero-copy views of the parts of sl between each c.
string/split-char for a slice, without the copies: empty parts are kept, and each part is a sub-slice of sl over the same parent String. A table row's cells, for instance.
| sl | the slice to split (borrowed; each part retains its parent) | |
| c | the separator character (ASCII) |
A fresh (Vec StringSlice), at least one part. Free it with slice/release-all.
(vec-len (slice/split-char (string/slice (string/from-cstr "|a|b|") 0 5) #\|)) ; => 4
Since: markdown-library-plan S3
slice/-release-loop
(slice/-release-loop [v :(Vec StringSlice) i :int n :int] :void)
internal: release v's elements [i, n).
slice/release-all
(slice/release-all [v :(Vec StringSlice)] :void)
release every StringSlice in v, then free v itself.
The one call that frees what string/line-slices and slice/split-char return. Releasing the last slice over a String releases that String's reference too.
| v | a (Vec StringSlice) whose elements this call owns |
(slice/release-all (string/line-slices (string/from-cstr "a\nb")))
Since: markdown-library-plan S3
slice/-trim-end-loop
(slice/-trim-end-loop [sl :StringSlice lo :int i :int] :int)
internal: the index past the last non-space byte in [lo, i).
slice/trim
(slice/trim [sl :StringSlice] :StringSlice)
a view of sl without its leading and trailing ASCII whitespace.
string/trim without the copy. Whitespace is char.tur's space?, the set string/trim strips. An all-whitespace slice gives an empty one.
| sl | the slice to trim (borrowed) |
A fresh StringSlice over the same parent; release it with slice/release.
(slice/to-cstr (slice/trim (string/slice (string/from-cstr "| a b |") 1 5))) ; => "a b"
Since: markdown-library-plan S4
slice/trim-start
(slice/trim-start [sl :StringSlice] :StringSlice)
a view of sl without its leading ASCII whitespace.
| sl | the slice to trim (borrowed) |
A fresh StringSlice over the same parent; release it with slice/release.
(slice/to-cstr (slice/trim-start (string/slice (string/from-cstr " code") 0 7))) ; => "code"
Since: markdown-library-plan S4
slice/trim-end
(slice/trim-end [sl :StringSlice] :StringSlice)
a view of sl without its trailing ASCII whitespace.
| sl | the slice to trim (borrowed) |
A fresh StringSlice over the same parent; release it with slice/release.
(slice/to-cstr (slice/trim-end (string/slice (string/from-cstr "# Hi ") 0 6))) ; => "# Hi"
Since: markdown-library-plan S4
slice/-parse-digits
(slice/-parse-digits [sl :StringSlice i :int n :int acc :int] :(Option int))
internal: string/-parse-digits for a slice.
slice/parse-int
(slice/parse-int [sl :StringSlice] :(Option int))
the decimal integer sl spells, or (none).
string/parse-int for a slice, with no copy: an optional + or -, then one or more digits, and nothing else in the slice. Out of the 64-bit int range is (none). An ordered list item's start number, sliced out of its line, parses straight from the view.
| sl | the slice to parse (borrowed) |
(some n) when sl is exactly a decimal integer n, else (none).
(slice/parse-int (string/slice (string/from-cstr "12. item") 0 2)) ; => (some 12)
Since: markdown-library-plan S4
slice/compare
(slice/compare [a :StringSlice b :StringSlice] :int)
lexicographic byte compare of two slices: -1, 0, or 1.
Since: v2
slice/eq?
(slice/eq? [a :StringSlice b :StringSlice] :bool)
content equality of two slices.
Since: v2
slice/hash
(slice/hash [sl :StringSlice] :int)
content hash over the slice's byte range.
Since: v2
slice/sub
(slice/sub [sl :StringSlice off :int len :int] :StringSlice)
a sub-view [off, off+len) RELATIVE to this slice (clamped).
O(1): the result views the same parent String; no bytes are copied.
(let [w (string/slice (string/from-cstr "hello world") 6 5)] ; "world" (slice/to-cstr (slice/sub w 0 3))) ; => "wor"
Since: v2
builder/push-slice!
(builder/push-slice! [b :StringBuilder sl :StringSlice] :void)
append a slice's bytes to the builder.
Copies the viewed range straight from the parent String, so a parser can assemble output from views of its input without a slice/to-string temporary for each. The slice is borrowed, not released.
| b | the builder to append to | |
| sl | the slice whose bytes to append (borrowed) |
(let [b (builder/new)]
(do (builder/push-slice! b (string/slice (string/from-cstr "hello world") 6 5))
(string/to-cstr (builder/finish b)))) ; => "world"
Since: markdown-library-plan S5
BuilderPush[StringSlice]
(definstance BuilderPush [StringSlice])
the viewed bytes, as builder/push-slice!.
slice/to-string
(slice/to-string [sl :StringSlice] :String)
copy the slice's range into a fresh owned String.
Since: v2
slice/to-cstr
(slice/to-cstr [sl :StringSlice] :cstr)
copy the slice's range into a fresh NUL-terminated cstr.
The result is a freshly heap-allocated cstr (the caller frees it, or adopts it with string/adopt-cstr). A slice is not NUL-terminated at its end, so there is deliberately no borrowed cstr view -- this always copies.
Since: v2
Eq[StringSlice]
(definstance Eq [StringSlice])
content equality (no allocation).
Ord[StringSlice]
(definstance Ord [StringSlice])
lexicographic byte ordering.
Show[StringSlice]
(definstance Show [StringSlice])
the slice content as a fresh owned String. slice/to-cstr always mallocs, so adopting it is leak-free.
Hash[StringSlice]
(definstance Hash [StringSlice])
content hash over the byte range.