tur/string
owned, immutable, refcounted String.
Unlike cstr (a borrowed raw const char* with no length or ownership) and str (a borrowed pointer+length view), a String OWNS its bytes: it is a refcounted immutable heap payload { rc; len; bytes[len+1] }, NUL-terminated so a borrowed cstr view is O(1). Reach for String when a string must outlive its source -- a value returned past its buffer, a stored field, or a Map/Set key. As a MapKey the map copies and owns the key bytes (mk-owned? = 1), so a String key never dangles the way a computed cstr key does.
String is immutable; every transform returns a fresh String. Build one incrementally with StringBuilder so repeated concatenation stays linear.
Since: v2 (owned-string-type-plan)
String
(defopaque String :ptr<void> :non-null)
StringBuilder
(defopaque StringBuilder :ptr<void> :non-null)
string/from-cstr
(string/from-cstr [s :cstr] :String)
copy a NUL-terminated C string into a fresh owned String.
| s | the source cstr (a literal or borrowed buffer); its bytes are copied |
A new String (refcount 1) owning a private copy of s's bytes.
(string/from-cstr "hello") ; => String "hello"
Since: v2
string/to-cstr
(string/to-cstr [s :String] :cstr)
borrow the String's NUL-terminated bytes (O(1), no copy).
The returned cstr points into the String's payload and stays valid only while the String is retained; do not use it after the String is released.
| s | the String to borrow from |
A borrowed cstr view of the same bytes.
(string/to-cstr (string/from-cstr "hi")) ; => "hi"
Since: v2
string/adopt-cstr
(string/adopt-cstr [s :cstr] :String)
take ownership of a freshly-allocated cstr as a String.
Copies the bytes of s into a fresh owned String and FREES s. This is the owned-adoption bridge for the many stdlib functions that return a malloc'd cstr ("caller frees the result") -- wrap such a call in string/adopt-cstr and the result is a real owned String, leak-free:
(string/adopt-cstr (path/join base seg)) ; owned path String (string/adopt-cstr (digest/sha256-hex ...)) ; owned digest String
WARNING: s MUST be a heap buffer the caller owns. Never pass a string literal or a borrowed cstr (an accessor's return, a struct field) -- freeing that is undefined behavior. When in doubt, use string/from-cstr (which copies without freeing).
| s | a freshly heap-allocated NUL-terminated cstr; consumed (freed) |
A new owned String holding a copy of s's bytes.
Since: v2 (string-adoption-stdlib-plan, batch 1)
int->string
(int->string [v :int] :String)
format a signed integer as a fresh owned decimal String.
The owned counterpart of int->str (which returns a caller-frees cstr); builds the String directly with no cstr intermediate.
| v | the integer to render |
A new owned String, e.g. (int->string -7) => String "-7".
(string/to-cstr (int->string 42)) ; => "42"
Since: v2 (string-adoption-stdlib-plan, batch 1)
string/retain
(string/retain [s :String] :String)
increment the refcount, returning a second owning handle.
Since: v2
string/release
(string/release [s :String] :void)
decrement the refcount; free the payload at zero.
Since: v2
string/len
(string/len [s :String] :int)
byte length of the String (not counting the NUL).
Since: v2
string/empty?
(string/empty? [s :String] :bool)
true iff the String has length zero.
Since: v2
string/byte-at
(string/byte-at [s :String i :int] :int)
the byte (0..255) at index i, or -1 if out of range.
Since: v2
string/byte-is?
(string/byte-is? [s :String i :int c :Char] :bool)
is the byte at index i the character c?
The bounds-checked test a byte scanner needs: false when i is out of range, so it replaces (if (< i (string/len s)) (= (string/byte-at s i) 44) false) and the magic number with it. It compares one byte with c's code, so c should be ASCII; a character whose code is above 127 is several bytes in UTF-8 and is not what this finds.
| s | the String to look in (borrowed) | |
| i | a byte index; out of range is not an error | |
| c | the character to compare with |
true iff 0 <= i < (string/len s) and the byte at i is c's code.
(string/byte-is? (string/from-cstr "a,b") 1 #\,) ; => true
Since: markdown-library-plan S1
string/char-at
(string/char-at [s :String i :int] :(Option Char))
the byte at index i as a Char, or (none) out of range.
For byte-at-a-time scanning that wants to compare with character literals: (match (string/char-at s i) (Some c) (eq? c #\#) (None) false). The Char is the raw byte (0..255); decoding UTF-8 is a separate step.
| s | the String to look in (borrowed) | |
| i | a byte index; out of range is not an error |
(some c) holding the byte at i, or (none) when i is out of range.
(string/char-at (string/from-cstr "hi") 1) ; => (some #\i)
Since: markdown-library-plan S1
string/eq?
(string/eq? [a :String b :String] :bool)
content equality (length then byte compare).
Since: v2
string/compare
(string/compare [a :String b :String] :int)
lexicographic byte compare: -1, 0, or 1.
Since: v2
string/hash
(string/hash [s :String] :int)
content hash over the exact byte range (length-aware).
Since: v2
string/starts-with?
(string/starts-with? [s :String prefix :String] :bool)
true iff s begins with prefix.
Since: v2
string/ends-with?
(string/ends-with? [s :String suffix :String] :bool)
true iff s ends with suffix.
Since: v2
string/contains?
(string/contains? [s :String needle :String] :bool)
true iff needle occurs as a substring of s.
Since: v2
string/-match-at
(string/-match-at [s :String p :String at :int k :int n :int] :bool)
internal: do bytes [k, n) of p equal s's from at+k on?
string/starts-with-at?
(string/starts-with-at? [s :String prefix :String at :int] :bool)
does prefix occur in s at byte offset at?
string/starts-with? at any offset: the test a scanner makes before it consumes a token. An offset out of range, or a prefix that would run past the end of s, gives false, not an error. The empty prefix occurs at every offset from 0 to (string/len s).
| s | the String to look in (borrowed) | |
| prefix | the bytes to look for (borrowed) | |
| at | the byte offset in s to test |
true iff the bytes of s from at on begin with prefix.
(string/starts-with-at? (string/from-cstr "a --> b") (string/from-cstr "-->") 2) ; => true
Since: markdown-library-plan S2
string/-index-of-loop
(string/-index-of-loop [s :String needle :String i :int last :int] :(Option int))
internal: first match of needle at i..last.
string/index-of-from
(string/index-of-from [s :String needle :String start :int] :(Option int))
byte offset of the first needle at or after start.
A negative start searches from 0; a start past the end finds nothing. The empty needle is found at start itself, while start <= (string/len s).
| s | the String to search (borrowed) | |
| needle | the bytes to find (borrowed) | |
| start | the first byte offset a match may begin at |
(some i) for the lowest i >= start where needle occurs, else (none).
(string/index-of-from (string/from-cstr "a,b,c") (string/from-cstr ",") 2) ; => (some 3)
Since: markdown-library-plan S2
string/index-of
(string/index-of [s :String needle :String] :(Option int))
byte offset of the first occurrence of needle in s.
| s | the String to search (borrowed) | |
| needle | the bytes to find (borrowed) |
(some i) for the lowest i where needle occurs, or (none). The empty needle is found at 0.
(string/index-of (string/from-cstr "key: value") (string/from-cstr ": ")) ; => (some 3)
Since: markdown-library-plan S2
string/-last-index-of-loop
(string/-last-index-of-loop [s :String needle :String i :int] :(Option int))
internal: last match of needle at i down to 0.
string/last-index-of
(string/last-index-of [s :String needle :String] :(Option int))
byte offset of the last occurrence of needle in s.
| s | the String to search (borrowed) | |
| needle | the bytes to find (borrowed) |
(some i) for the highest i where needle occurs, or (none). The empty needle is found at (string/len s).
(string/last-index-of (string/from-cstr "a/b/c") (string/from-cstr "/")) ; => (some 3)
Since: markdown-library-plan S2
string/-index-of-byte-loop
(string/-index-of-byte-loop [s :String b :int i :int n :int] :(Option int))
internal: first i in [i, n) holding byte b.
string/index-of-char-from
(string/index-of-char-from [s :String c :Char start :int] :(Option int))
byte offset of the first c at or after start.
Compares single bytes, so c should be ASCII (see string/byte-is?). A negative start searches from 0.
| s | the String to search (borrowed) | |
| c | the character to find | |
| start | the first byte offset to look at |
(some i) for the lowest i >= start holding c, or (none).
(string/index-of-char-from (string/from-cstr "a]b]") #\] 2) ; => (some 3)
Since: markdown-library-plan S2
string/index-of-char
(string/index-of-char [s :String c :Char] :(Option int))
byte offset of the first c in s.
| s | the String to search (borrowed) | |
| c | the character to find (ASCII; see string/byte-is?) |
(some i) for the lowest i holding c, or (none).
(string/index-of-char (string/from-cstr "[link](url)") #\]) ; => (some 5)
Since: markdown-library-plan S2
string/-span-loop
(string/-span-loop [s :String pred :(fn [Char] bool) i :int n :int] :int)
internal: first i in [i, n) whose byte fails pred, else n.
string/span-while
(string/span-while [s :String start :int pred :(fn [Char] bool)] :int)
the index where a run of bytes satisfying pred ends.
Scans from start while pred holds and returns the first index where it fails, or (string/len s) when it holds to the end. The char.tur predicates plug straight in: indentation width, the end of a name, a run of digits. pred sees each byte as a Char, so a multi-byte UTF-8 character is several calls. start is clamped into [0, (string/len s)].
| s | the String to scan (borrowed) | |
| start | the byte offset to start at | |
| pred | the test each byte must pass to continue the run |
The end of the run: an index in [start, (string/len s)].
(string/span-while (string/from-cstr " - item") 0 space?) ; => 3
Since: markdown-library-plan S2
string/-run-loop
(string/-run-loop [s :String b :int i :int n :int] :int)
internal: first i in [i, n) not holding byte b, else n.
string/run-length
(string/run-length [s :String start :int c :Char] :int)
how many c in a row start at start.
Counts a fence (three or more backticks), an ATX heading's #s, a thematic break's dashes. Zero when start is out of range or the byte there is not c.
| s | the String to scan (borrowed) | |
| start | the byte offset the run starts at | |
| c | the character the run is made of (ASCII) |
The run's length in bytes, 0 or more.
(string/run-length (string/from-cstr "## Title") 0 #\#) ; => 2
Since: markdown-library-plan S2
string/concat
(string/concat [a :String b :String] :String)
concatenate two Strings into a fresh String.
Since: v2
string/substring
(string/substring [s :String start :int len :int] :String)
fresh String of len bytes starting at start (clamped).
Out-of-range start/len are clamped to the String's bounds rather than erroring, so a substring past the end yields the empty String.
Since: v2
string/to-upper
(string/to-upper [s :String] :String)
ASCII-uppercased copy.
Since: v2
string/to-lower
(string/to-lower [s :String] :String)
ASCII-lowercased copy.
Since: v2
string/trim
(string/trim [s :String] :String)
copy with ASCII leading/trailing whitespace removed.
Since: v2
builder/new
(builder/new :StringBuilder)
a fresh empty StringBuilder (mutable, growable byte buffer).
Since: v2
builder/push-cstr!
(builder/push-cstr! [b :StringBuilder s :cstr] :void)
append a cstr's bytes to the builder.
Since: v2
builder/push-string!
(builder/push-string! [b :StringBuilder s :String] :void)
append a String's bytes to the builder.
Since: v2
builder/push-byte!
(builder/push-byte! [b :StringBuilder c :int] :void)
append a single byte (0..255) to the builder.
Since: v2
builder/push-char!
(builder/push-char! [b :StringBuilder c :Char] :void)
append one character (a Char, 0..255) to the builder.
| b | the builder to append to | |
| c | the character to append |
(let [b (builder/new)]
(do (builder/push-char! b #\h)
(builder/push-char! b #\i)
(builder/finish b))) ; => String "hi"
Since: parsec-guide-plan P1
builder/push-int!
(builder/push-int! [b :StringBuilder v :int] :void)
append the decimal digits of an int to the builder.
Writes the digits straight into the builder, with a leading - when v is negative, so there is no temporary String to release, unlike pushing (int->string v).
| b | the builder to append to | |
| v | the number to write |
(let [b (builder/new)]
(do (builder/push-int! b -42)
(string/to-cstr (builder/finish b)))) ; => "-42"
Since: markdown-library-plan S5
builder/push-repeat!
(builder/push-repeat! [b :StringBuilder c :Char n :int] :void)
append n copies of the character c to the builder.
Indentation, a rule of dashes, the padding of a column. Nothing is appended when n is 0 or less. c is one byte, so it should be ASCII.
| b | the builder to append to | |
| c | the character to repeat | |
| n | how many copies |
(let [b (builder/new)]
(do (builder/push-repeat! b #\- 3)
(string/to-cstr (builder/finish b)))) ; => "---"
Since: markdown-library-plan S5
char->string
(char->string [c :Char] :String)
a fresh one-character String holding c.
| c | the character (0..255) |
A new owned String of length 1.
(string/to-cstr (char->string #\A)) ; => "A"
Since: parsec-guide-plan P1
builder/len
(builder/len [b :StringBuilder] :int)
number of bytes accumulated so far.
Since: v2
builder/finish
(builder/finish [b :StringBuilder] :String)
freeze the accumulated bytes into an immutable String.
Consumes the builder (frees it); do not use the builder afterward.
(let [b (builder/new)]
(do (builder/push-cstr! b "a")
(builder/push-cstr! b "b")
(builder/finish b))) ; => String "ab"
Since: v2
string/-new-vec
(string/-new-vec :(Vec String))
internal: an empty (Vec String) (a let-bound vec-new is not inferred).
string/-copy-range
(string/-copy-range [out :(Vec String) s :String start :int end :int] :void)
internal: push a fresh String of s's bytes [start, end) onto out.
string/-split-loop
(string/-split-loop [s :String sep :String out :(Vec String) start :int] :(Vec String))
internal: split s on sep from start into out.
string/split
(string/split [s :String sep :String] :(Vec String))
split s at each occurrence of sep.
The parts between separators, in order, including empty ones: "a,,b," split on "," is "a", "", "b", "". A string with no sep gives one part, a copy of itself; the empty string gives one empty part. An empty sep also gives one part, the whole string, since it would match everywhere.
| s | the String to split (borrowed) | |
| sep | the separator (borrowed) |
A fresh (Vec String) of fresh owned parts, at least one. Free it with string/release-all.
(vec-len (string/split (string/from-cstr "a, b, c") (string/from-cstr ", "))) ; => 3
Since: markdown-library-plan S3
string/-split-char-loop
(string/-split-char-loop [s :String c :Char out :(Vec String) start :int] :(Vec String))
internal: split s on byte b from start into out.
string/split-char
(string/split-char [s :String c :Char] :(Vec String))
split s at each occurrence of the character c.
string/split with a one-byte separator, so c should be ASCII. Empty parts are kept: "|a||" split on #\| is "", "a", "", "".
| s | the String to split (borrowed) | |
| c | the separator character |
A fresh (Vec String) of fresh owned parts, at least one. Free it with string/release-all.
(vec-len (string/split-char (string/from-cstr "| a | b |") #\|)) ; => 4
Since: markdown-library-plan S3
string/-lines-loop
(string/-lines-loop [s :String out :(Vec String) start :int i :int n :int] :(Vec String))
internal: push s's lines from start into out, scanning at i.
string/lines
(string/lines [s :String] :(Vec String))
split s into lines, without their line endings.
A line ends at LF, CRLF or CR, the three endings CommonMark recognises. A final line ending does not start another line, so "a\nb\n" is two lines, "a\n\n" is "a" and "", and the empty string has no lines.
| s | the String to split (borrowed) |
A fresh (Vec String) of fresh owned lines, possibly empty. Free it with string/release-all.
(vec-len (string/lines (string/from-cstr "one\r\ntwo\n"))) ; => 2
Since: markdown-library-plan S3
string/-join-loop
(string/-join-loop [parts :(Vec String) sep :String b :StringBuilder i :int n :int] :void)
internal: push parts [i, n) onto b, sep between them.
string/join
(string/join [parts :(Vec String) sep :String] :String)
concatenate parts with sep between each pair.
Borrows the parts and sep: none is released. No parts give the empty String. The inverse of string/split: joining what split returned with the same separator gives the original string back.
| parts | the Strings to join, in order (borrowed) | |
| sep | the separator to put between them (borrowed) |
A fresh owned String.
(let [v (string/split-char (string/from-cstr "a b c") #\space)] (string/to-cstr (string/join v (string/from-cstr "-")))) ; => "a-b-c"
Since: markdown-library-plan S3
string/-release-loop
(string/-release-loop [v :(Vec String) i :int n :int] :void)
internal: release v's elements [i, n).
string/release-all
(string/release-all [v :(Vec String)] :void)
release every String in v, then free v itself.
The one call that frees what string/split, string/split-char and string/lines return. A Vec does not own its elements, so vec-free alone would leak every String in it. Do not use v, or any String taken out of it without string/retain, afterwards.
| v | a (Vec String) whose elements this call owns |
(string/release-all (string/lines (string/from-cstr "a\nb")))
Since: markdown-library-plan S3
string/-trim-end-loop
(string/-trim-end-loop [s :String lo :int i :int] :int)
internal: the index past the last non-space byte in [lo, i).
string/trim-start
(string/trim-start [s :String] :String)
copy with the leading ASCII whitespace removed.
| s | the String to trim (borrowed) |
A fresh owned String: s from its first non-whitespace byte on.
(string/to-cstr (string/trim-start (string/from-cstr " - item "))) ; => "- item "
Since: markdown-library-plan S4
string/trim-end
(string/trim-end [s :String] :String)
copy with the trailing ASCII whitespace removed.
| s | the String to trim (borrowed) |
A fresh owned String: s up to and including its last non-whitespace byte.
(string/to-cstr (string/trim-end (string/from-cstr " # Title \t"))) ; => " # Title"
Since: markdown-library-plan S4
string/-push-range!
(string/-push-range! [b :StringBuilder s :String i :int end :int] :void)
internal: append s's bytes [i, end) to b.
string/-replace-loop
(string/-replace-loop [s :String from :String to :String b :StringBuilder start :int] :void)
internal: copy s from start into b, with to in place of each from.
string/replace-all
(string/replace-all [s :String from :String to :String] :String)
copy of s with every occurrence of from replaced by to.
A literal replace: from is matched byte for byte, left to right, and the scan resumes after each match, so occurrences never overlap and to is never searched. An empty from matches nothing, so s comes back as a copy, the same rule string/split has for an empty separator. For a pattern, use re/replace-all.
| s | the String to copy (borrowed) | |
| from | the text to replace (borrowed) | |
| to | the text to put in its place (borrowed; may be empty) |
A fresh owned String.
(string/to-cstr (string/replace-all (string/from-cstr "a&b&c")
(string/from-cstr "&")
(string/from-cstr "&"))) ; => "a&b&c"
Since: markdown-library-plan S4
string/-repeat-loop
(string/-repeat-loop [b :StringBuilder s :String n :int] :void)
internal: append s to b n times.
string/repeat
(string/repeat [s :String n :int] :String)
s written n times in a row.
| s | the String to repeat (borrowed) | |
| n | how many copies; 0 or less gives the empty String |
A fresh owned String of (* n (string/len s)) bytes.
(string/to-cstr (string/repeat (string/from-cstr "ab") 3)) ; => "ababab"
Since: markdown-library-plan S4
string/pad-start
(string/pad-start [s :String width :int c :Char] :String)
s with copies of c in front, to make it width bytes.
Lengths are in bytes, as everywhere in String, so c should be ASCII. A String already width bytes or longer comes back as an unchanged copy; it is never cut.
| s | the String to pad (borrowed) | |
| width | the length to pad to | |
| c | the padding character |
A fresh owned String of (max width (string/len s)) bytes.
(string/to-cstr (string/pad-start (int->string 42) 5 #\0)) ; => "00042"
Since: markdown-library-plan S4
string/pad-end
(string/pad-end [s :String width :int c :Char] :String)
s with copies of c after it, to make it width bytes.
string/pad-start, padding on the right: a table cell or a column of names. A String already width bytes or longer comes back as a copy.
| s | the String to pad (borrowed) | |
| width | the length to pad to | |
| c | the padding character (ASCII) |
A fresh owned String of (max width (string/len s)) bytes.
(string/to-cstr (string/pad-end (string/from-cstr "ab") 4 #\.)) ; => "ab.."
Since: markdown-library-plan S4
string/-parse-digits
(string/-parse-digits [s :String i :int n :int acc :int] :(Option int))
internal: s's digits [i, n) folded into acc as a negative int, or (none).
string/-signed-int
(string/-signed-int [neg :bool digits :(Option int)] :(Option int))
internal: the parsed value from the negated digits, or (none).
string/parse-int
(string/parse-int [s :String] :(Option int))
the decimal integer s spells, or (none).
The whole of s must be the number: an optional + or -, then one or more digits 0-9, and nothing else -- no spaces, no underscores, no 0x. Leading zeros are fine. A number outside the 64-bit int range is (none), never a wrapped or clamped value. str->int-checked, by contrast, takes a cstr, skips leading spaces and clamps on overflow.
| s | the String to parse (borrowed) |
(some n) when s is exactly a decimal integer n, else (none).
(string/parse-int (string/from-cstr "-042")) ; => (some -42) (string/parse-int (string/from-cstr "12.")) ; => (none)
Since: markdown-library-plan S4
BuilderPush
(defclass BuilderPush [a])
types builder/push! can append; builder/-push-into! appends x to b.
The value comes first because a class method dispatches on its first argument; builder/push! puts the builder first, like the other pushes.
(builder/-push-into! [x :a b :StringBuilder] :void)
BuilderPush[cstr]
(definstance BuilderPush [cstr])
the bytes up to the NUL.
BuilderPush[String]
(definstance BuilderPush [String])
the String's bytes; it is borrowed, not released.
BuilderPush[Char]
(definstance BuilderPush [Char])
the one character.
BuilderPush[int]
(definstance BuilderPush [int])
the decimal digits, as builder/push-int!.
BuilderPush[bool]
(definstance BuilderPush [bool])
true or false.
builder/push!
(builder/push! [A])
append any BuilderPush value to the builder.
One name for every push: a cstr, String or StringSlice appends its bytes, a Char its character, an int its decimal digits and a bool true or false. The value is borrowed, so a String passed in is not released. string/of is the usual way to reach this.
| b | the builder to append to | |
| x | the value to append |
(let [b (builder/new)]
(do (builder/push! b "n=")
(builder/push! b 42)
(string/to-cstr (builder/finish b)))) ; => "n=42"
Since: markdown-library-plan S5
string/-push-all__
(string/-push-all__ [b & ^syntax xs])
internal: (builder/push! b x) for each x, in order.
string/of
(string/of [& ^syntax xs])
one fresh String made of all its arguments, in order.
Each argument is any BuilderPush value: a cstr, String, StringSlice, Char, int or bool. It expands to one builder, a builder/push! per argument and builder/finish, so nothing in between is allocated, and the one String it returns is the only thing to release. Arguments are borrowed and evaluated left to right. A call that returns a fresh String, such as (int->string n), is borrowed too and would leak; pass n itself.
| xs | the parts, zero or more |
A fresh owned String; (string/of) is the empty String.
(string/to-cstr (string/of "x=" 3 #\, "ok=" true)) ; => "x=3,ok=true"
Since: markdown-library-plan S5
Eq[String]
(definstance Eq [String])
content equality.
Ord[String]
(definstance Ord [String])
lexicographic byte ordering.
Hash[String]
(definstance Hash [String])
content hash over the byte range.
Clone[String]
(definstance Clone [String])
O(1) retain (refcount++), returning a second owning handle.
Drop[String]
(definstance Drop [String])
release one owning handle (refcount--), freeing at zero.
Paired with Clone [String] (retain), this lets the closure drop-glue treat a captured String as a shared refcounted owner: retained when captured, released when the capturing env dies -- so an owned String captured into an escaping closure (e.g. httpd mw-cors config strings) is reclaimed instead of leaking.
Since: v2 (Model R)
MapKey[String]
(definstance MapKey [String])
owned key: the map copies the bytes into a boxed key it owns and frees (mk-owned? = 1), so a String key never dangles. mk-cmp is the size-aware content comparator over boxed keys.
Internal definitions
string/autolink-hint-- internal marker triggering tur_string.c compilation.