2026-01-12 19:03:49 +01:00
2026-03-19 13:29:49 +01:00
#strcmp [*, addr, len, addr | len] -> [* | bool]
2026-01-12 19:03:49 +01:00
word strcmp
2026-03-19 13:29:49 +01:00
>r nip r> for
2026-03-20 15:37:12 +01:00
2dup c@ swap c@ != if drop drop 0 rdrop ret end
2026-03-19 13:29:49 +01:00
1 + swap 1 +
end
drop drop 1
2026-01-12 19:03:49 +01:00
end
2026-03-20 21:23:45 +01:00
# strdup [*, addr | len] -> [*, addr, len, addr1 | len1]
word strdup
dup alloc 2 pick 2 pick memcpy
end
2026-02-05 21:36:03 +01:00
#strconcat [*, addr, len, addr | len] -> [*, addr | len]
2026-01-12 19:03:49 +01:00
word strconcat
0 pick 3 pick +
dup
>r >r >r >r >r >r
5 rpick
alloc
r> r>
dup >r
memcpy
swap
r> dup -rot +
r> r>
memcpy
swap
3 pick
-
2026-03-09 14:04:45 +01:00
nip
2026-01-12 19:03:49 +01:00
swap
0 rpick
nip
rot
drop
2026-02-17 12:59:34 +01:00
rdrop rdrop
2026-01-12 19:03:49 +01:00
end
2026-02-05 21:36:03 +01:00
#strlen [* | addr] -> [* | len]
2026-01-12 19:03:49 +01:00
# for null terminated strings
2026-02-03 09:41:11 +01:00
word strlen
0 swap # len addr
while dup c@ 0 != do
1 + # addr++
swap 1 + swap # len++
end
drop # drop addr, leave len
end
2026-01-12 10:32:25 +01:00
2026-02-05 21:36:03 +01:00
#digitsN>num [*, d_{n-1}, d0 | n] -> [* | value]
word digitsN>num # digits bottom=MSD, top=LSD, length on top (MSD-most significant digit, LSD-least significant digit)
2026-01-12 10:32:25 +01:00
0 swap # place accumulator below length
for # loop n times using the length on top
r@ pick # fetch next digit starting from MSD (uses loop counter as index)
swap # acc on top
10 * # acc *= 10
+ # acc += digit
end
end
2026-02-05 21:36:03 +01:00
#toint [*, addr | len] -> [* | int]
# converts a string to an int
2026-01-12 10:32:25 +01:00
word toint
2026-03-09 14:04:45 +01:00
tuck
0 swap
2026-01-12 10:32:25 +01:00
dup >r
for
2026-03-09 14:04:45 +01:00
2dup +
2026-01-12 10:32:25 +01:00
c@ 48 -
swap rot
swap
1 +
end
2drop
r>
dup >r
digitsN>num
r> 1 +
for
2026-03-09 14:04:45 +01:00
nip
2026-01-12 10:32:25 +01:00
end
2026-01-13 09:26:05 +01:00
rdrop rdrop
2026-01-12 10:32:25 +01:00
end
2026-02-05 21:36:03 +01:00
#count_digits [* | int] -> [* | int]
# returns the amount of digits of an int
2026-01-12 14:54:48 +01:00
word count_digits
0
swap
while dup 0 > do
10 / swap 1 + swap
end
drop
end
2026-02-05 21:36:03 +01:00
#tostr [* | int] -> [*, addr | len]
# the function allocates a buffer, remember to free it
2026-01-12 14:54:48 +01:00
word tostr
dup
count_digits
2dup >r alloc
2026-02-02 15:51:58 +01:00
swap rot swap
2026-01-12 14:54:48 +01:00
for
dup 10 % swap 10 /
end
drop
r>
1 swap dup
for
dup
2 + pick
2 pick
2 + pick
3 pick rot +
2026-01-12 18:36:34 +01:00
swap 48 + swap 1 - swap c!
2026-01-12 14:54:48 +01:00
swap
1 +
swap
end
swap 0 +
pick 1 +
over for
rot drop
end drop
2026-02-05 21:36:03 +01:00
end
2026-02-17 12:59:34 +01:00
# ---------------------------------------------------------------------------
# String search helpers
# ---------------------------------------------------------------------------
#indexof [*, addr, len | char] -> [* | index]
# Finds the index of the first occurrence of char in the string.
# Returns -1 if not found. Consumes addr, len, and char.
word indexof
>r # char -> rstack
-1 0 # result=-1, i=0
while dup 3 pick < 2 pick -1 == band do # while i < len && result == -1
dup 4 pick + c@ # byte = addr[i]
r@ == if # byte == char?
2026-03-09 14:04:45 +01:00
nip dup # result = i
2026-02-17 12:59:34 +01:00
end
1 + # i++
end
drop nip nip # drop i, addr, len -> leave result
rdrop # clean char from rstack
end
#count_char_in_str [*, addr, len | char] -> [*, addr, len | count]
# Counts the number of occurrences of char in the string.
# Preserves addr and len on stack; pushes count on top.
word count_char_in_str
>r # char -> rstack
0 # count = 0
1 pick # push len (for loop count)
for
# for counter r@ goes len..1; char is at 1 rpick
# byte index = len - r@ (0 .. len-1)
1 pick r@ - 3 pick + c@ # byte at addr[len - r@]
1 rpick == # byte == char?
if 1 + end # increment count
end
rdrop # clean char from rstack
end
# ---------------------------------------------------------------------------
# Single-substitution helpers
# ---------------------------------------------------------------------------
#format1s [*, arg_addr, arg_len, fmt_addr | fmt_len] -> [*, result_addr | result_len]
# Replaces the first '%' in fmt with the arg string.
# Allocates a new result string (caller should free it).
# If no '%' is found, returns fmt as-is (NOT newly allocated).
word format1s
2dup 37 indexof # find first '%'
dup -1 == if
# no '%' — drop pos, drop arg, return fmt unchanged
drop 2swap 2drop
else
# pos is on TOS; save pos, fmt_addr, fmt_len to rstack
>r # rstack: [pos]
over >r # rstack: [pos, fmt_addr]
dup >r # rstack: [pos, fmt_addr, fmt_len]
# rpick 0=fmt_len 1=fmt_addr 2=pos
# stack: arg_addr, arg_len, fmt_addr, fmt_len
drop # drop fmt_len (we have it on rstack)
2 rpick # push pos
# stack: arg_addr, arg_len, fmt_addr, pos (= prefix pair)
2swap # stack: fmt_addr, pos, arg_addr, arg_len
strconcat # tmp = prefix + arg
# save tmp for later freeing
2dup >r >r
# rpick 0=tmp_addr 1=tmp_len 2=fmt_len 3=fmt_addr 4=pos
# build suffix: (fmt_addr + pos + 1, fmt_len - pos - 1)
3 rpick 4 rpick + 1 + # suffix_addr
2 rpick 4 rpick - 1 - # suffix_len
strconcat # result = tmp + suffix
# free tmp
r> r> free
# clean saved pos, fmt_addr, fmt_len
rdrop rdrop rdrop
end
end
#format1i [*, int_arg, fmt_addr | fmt_len] -> [*, result_addr | result_len]
# Replaces the first '%' in fmt with the decimal representation of int_arg.
# Allocates a new result string (caller should free it).
word format1i
rot tostr # convert int to string (allocates)
2dup >r >r # save tostr result for freeing
2swap # bring fmt on top
format1s # substitute
r> r> free # free the tostr buffer
end
# ---------------------------------------------------------------------------
# Multi-substitution format words
# ---------------------------------------------------------------------------
# Replaces each '%' in fmt with the corresponding string argument.
# s1 (just below fmt) maps to the first '%', s2 to the second, etc.
# The number of string-pair args must equal the number of '%' markers.
# Returns a newly allocated string, or fmt as-is when there are no '%'.
2026-02-18 13:58:08 +01:00
#formats [*, sN_addr, sN_len, ..., s1_addr, s1_len, fmt_addr | fmt_len] -> [*, result_addr | result_len]
2026-02-17 12:59:34 +01:00
word formats
2dup 37 count_char_in_str nip nip # count '%' -> n
2026-03-09 14:04:45 +01:00
dup not if
2026-02-17 12:59:34 +01:00
drop # no substitutions needed
else
1 - >r # remaining = n - 1
format1s # first substitution
while r@ 0 > do
2dup >r >r # save current result for freeing
format1s # next substitution
r> r> free # free previous result
r> 1 - >r # remaining--
end
rdrop # clean counter
end
end
2026-02-18 13:58:08 +01:00
2026-02-17 12:59:34 +01:00
# Replaces each '%' in fmt with the decimal string of the corresponding
# integer argument. i1 (just below fmt) maps to the first '%', etc.
# The number of int args must equal the number of '%' markers.
# Returns a newly allocated string, or fmt as-is when there are no '%'.
2026-02-18 13:58:08 +01:00
#formati [*, iN, ..., i1, fmt_addr | fmt_len] -> [*, result_addr | result_len]
2026-02-17 12:59:34 +01:00
word formati
2dup 37 count_char_in_str nip nip # count '%' -> n
2026-03-09 14:04:45 +01:00
dup not if
2026-02-17 12:59:34 +01:00
drop # no substitutions needed
else
1 - >r # remaining = n - 1
format1i # first substitution
while r@ 0 > do
2dup >r >r # save current result for freeing
format1i # next substitution
r> r> free # free previous result
r> 1 - >r # remaining--
end
rdrop # clean counter
end
end
# ---------------------------------------------------------------------------
# Mixed-type printf-style format (with %i and %s specifiers)
# ---------------------------------------------------------------------------
#find_fmt [*, addr | len] -> [* | pos, type]
# Scans for the first %i or %s placeholder in the string.
# Returns position of '%' and the type byte (105='i' or 115='s').
# Returns -1, 0 if no placeholder is found.
word find_fmt
over >r dup >r # rstack: 0=len, 1=addr
-1 0 0 # pos=-1, type=0, i=0
2026-03-09 14:04:45 +01:00
while dup 0 rpick 1 - < 2 pick not band do
2026-02-17 12:59:34 +01:00
1 rpick over + c@ # addr[i]
37 == if # '%'
1 rpick over + 1 + c@ # addr[i+1]
dup 105 == over 115 == bor if
rot drop rot drop over # pos=i, type=char, keep i
else
drop
end
end
1 +
end
drop # drop i
rdrop rdrop
2swap 2drop # remove addr, len copies; leave pos, type
end
#count_fmt [*, addr | len] -> [*, addr, len | count]
# Counts the number of %i and %s placeholders in a string.
word count_fmt
over >r dup >r # rstack: 0=len, 1=addr
0 0 # count=0, i=0
while dup 0 rpick 1 - < do
1 rpick over + c@
37 == if
1 rpick over + 1 + c@
dup 105 == over 115 == bor if
drop
swap 1 + swap
1 +
else
drop
end
end
1 +
end
drop # drop i
rdrop rdrop
end
#fmt_splice [*, repl_addr, repl_len, fmt_addr, fmt_len | pos] -> [*, result_addr | result_len]
# Replaces the 2-char placeholder at pos in fmt with repl.
# Allocates a new result string (caller should free it).
word fmt_splice
>r >r >r # rstack: [pos, fmt_len, fmt_addr]
# rpick: 0=fmt_addr, 1=fmt_len, 2=pos
0 rpick 2 rpick # push fmt_addr, pos (= prefix pair)
2swap # stack: fmt_addr, pos, repl_addr, repl_len
strconcat # tmp = prefix + repl
2dup >r >r # save tmp for freeing
# rpick: 0=tmp_addr, 1=tmp_len, 2=fmt_addr, 3=fmt_len, 4=pos
2 rpick 4 rpick + 2 + # suffix_addr = fmt_addr + pos + 2
3 rpick 4 rpick - 2 - # suffix_len = fmt_len - pos - 2
strconcat # result = tmp + suffix
r> r> free # free tmp
rdrop rdrop rdrop # clean fmt_addr, fmt_len, pos
end
#format1 [*, arg(s), fmt_addr | fmt_len] -> [*, result_addr | result_len]
# Replaces the first %i or %s placeholder in fmt.
# For %i: consumes one int from below fmt and converts via tostr.
# For %s: consumes one (addr, len) string pair from below fmt.
# Allocates a new result string (caller should free it).
word format1
2dup find_fmt # stack: ..., fmt_addr, fmt_len, pos, type
dup 105 == if
# %i
drop # drop type
>r >r >r # save pos, fmt_len, fmt_addr
tostr # convert int arg to string
2dup >r >r # save istr for freeing
r> r> r> r> r> # restore: istr_addr, istr_len, fmt_addr, fmt_len, pos
fmt_splice
2swap free # free tostr buffer
else 115 == if
# %s: stack already has repl_addr, repl_len, fmt_addr, fmt_len, pos
fmt_splice
else
drop # no placeholder, drop pos
end
end
#format [*, args..., fmt_addr | fmt_len] -> [*, result_addr | result_len]
# Printf-style formatting with %i (integer) and %s (string) placeholders.
# Arguments are consumed left-to-right: the closest arg to the format
# string maps to the first placeholder.
#
# Example: "bar" 123 "foo" "%s: %i and %s" format
# -> "foo: 123 and bar"
#
# Returns a newly allocated string (caller should free it), or the
# original format string unchanged if there are no placeholders.
word format
2dup count_fmt nip nip # n = placeholder count
2026-03-09 14:04:45 +01:00
dup not if
2026-02-17 12:59:34 +01:00
drop
else
>r
format1 # first substitution
r> 1 -
while dup 0 > do
>r
2dup >r >r # save current result for freeing
format1 # next substitution
r> r> free # free previous result
r> 1 -
end
drop # drop counter (0)
end
end
2026-03-18 15:27:01 +01:00
# rotate N elements of the top of the stack
# nrot [*, x1 ... xN - 1 | xN] -> [*, xN, xN - 1 ... x2 | x1]
word nrot
dup 1 + 1 swap for
dup pick swap 2 +
end
1 - 2 / pick
dup for
swap >r rswap
end
1 + for
nip
end
for
rswap r>
end
end
# convert a string to a sequence of ascii codes of its characters and push the codes on to the stack,
# Warning! the sequence is reversed so the ascii code of the last character ends up first on the stack
2026-03-24 16:35:00 +01:00
# toascii [*, addr | LEN] -> [*, x, x1 ... xLEN - 1 | xLEN]
2026-03-18 15:27:01 +01:00
word toascii
0 swap
for
2dup + c@
-rot
1 +
end
2drop
end
2026-03-20 21:09:44 +01:00
# rm_zero_len_str [*, addr0, len0 ... addrN, lenN | N] -> [*, addrX, lenX ... addrY, lenY | Z]
word rm_zero_len_str
dup for
swap dup 0 == if
drop nip 1 -
else
>r rswap swap >r rswap
end
end
dup 2 * for
rswap r> swap
end
end
# emit_strs [*, addr | len] -> [*, addr0, len0 ... addrN | lenN]
# given an addr and len emits pairs (addr, len) of strings in the given memopry region
word emit_strs
0 >r
>r
while r@ 0 > do
dup strlen dup r> swap - >r
over over + rswap r> 1 + >r rswap
while dup c@ 0 == do 1 + r> 1 - >r end
end
drop rdrop
end
# splitby_str [*, addr, len, addr1, len1] -> [*, addr0, len0 ... addrN, lenN | N]
# splits a string by another string and emmits a sequence of the new (addr, len) pairs on to the stack as well as the number of strings the oprtation resulted in.
2026-03-18 15:27:01 +01:00
word splitby_str
2026-03-20 21:09:44 +01:00
2 pick for
3 pick 0 2 pick 4 pick swap
strcmp 1 == if 3 pick over 0 memset_bytes end
>r >r swap 1 + swap r> r>
end
2drop 2dup - >r nip r> swap emit_strs r>
rm_zero_len_str
2026-03-18 15:27:01 +01:00
end
2026-03-20 21:09:44 +01:00
# splitby [*, addr, len, addr1 | len1] -> [*, addr0, len0 ... addrN, lenN | N]
# split a string by another string, delegates to either splitby_char or splitby_str based on the length of the delimiter.
2026-03-18 15:27:01 +01:00
word splitby
dup 1 == if
splitby_char
else
splitby_str
end
end
2026-03-20 21:09:44 +01:00
# splitby_char [*, addr, len, addr1 | len] -> [*, addr1, len1 ... addrN, lenN | N]
# split a string by a given character, the resulting (addr, len) pairs are pushed on to the stack followed by the number of the pushed strings.
2026-03-18 15:27:01 +01:00
word splitby_char
2026-03-20 21:09:44 +01:00
2 pick >r
2026-03-18 15:27:01 +01:00
>r >r 2dup r> r> 2swap 2dup
>r >r toascii 1 rpick nrot r> r>
dup 3 + pick c@
swap for
dup
2026-03-20 21:09:44 +01:00
3 pick == if over 0 c! end
2026-03-18 15:27:01 +01:00
swap 1 + swap >r nip r>
end
2drop 2drop drop
2026-03-20 21:09:44 +01:00
r>
emit_strs
r>
rm_zero_len_str
2026-03-18 15:27:01 +01:00
end
2026-03-25 08:59:35 +01:00
# ltrim [*, addr | len] -> [*, addr, | len]
word ltrim
dup for
over c@ 32 == if
swap 1 + swap 1 -
end
end
end
# rtrim [*, addr | len] -> [*, addr, | len]
word rtrim
swap tuck swap
swap over + 1 - swap
dup for
over c@ 32 == if
swap 1 - swap 1 -
end
end nip
end
# trim [*, addr | len] -> [*, addr | len]
word trim
ltrim rtrim
end
2026-03-25 10:10:14 +01:00
# startswith [*, addr, len, addr | len] -> [*, bool]
inline word startswith
strcmp
end
# endswith [*, addr, len, addr | len] -> [*, bool]
word endswith
dup 3 pick swap - 4 pick + over 2 pick 4 pick swap strcmp
nip nip nip nip
end