Standard library index
A terse, complete index of the wisp standard library: what exists and what is
proposed. It is the working list for future stdlib milestones, not a promise of
final signatures. Every name is snake_case (the permanent convention; see
the stdlib guide).
Legend: [x] implemented, [ ] proposed. Implemented functions are documented
in the standard library reference. Signatures use T/U for
generic element types and K/V for dict key/value types.
A [ref] tag below marks a builtin that is also referenceable as a first-class
fn-value (eta-expansion via a thin wrapper). Referenceable builtins fall into
three classes: monomorphic (no annotation needed), overloaded (annotation
selects the arm), and generic (annotation selects the container shape). All
other builtins are call-only; wrap them in a fn to pass them as a value. See
the function references section for
the full rules, per-class examples, and the fallible-funcref position-
degradation note.
Conversions
Section titled “Conversions”[x] to_string(x: int | float | bool | string) -> string[x] to_int(x: string | float) -> int[x] to_float(x: int | string) -> float[x] to_bool(x: int | float | string) -> bool[x] parse_int(s: string) -> Optional[int]— parse, or None on bad input (no abort); fold withunwrap_or[x] parse_float(s: string) -> Optional[float][x] parse_bool(s: string) -> Optional[bool]
Strings
Section titled “Strings”[x] length(s: string) -> int[x] [ref] string.lower(s: string) -> string[x] [ref] string.upper(s: string) -> string[x] [ref] string.trim(s: string) -> string[x] string.replace(s: string, search: string, with: string) -> string— all occurrences[x] string.split(s: string, sep: string) -> string[][x] string.join— no static signature;string.join(parts: string[], sep: string) -> string[x] string.contains— no static signature;string.contains(s: string, sub: string) -> bool[x] string.starts_with(s: string, pre: string) -> bool[x] string.ends_with(s: string, suf: string) -> bool[x] string.index_of— no static signature;string.index_of(s: string, sub: string) -> Optional[int], Some(first byte index) or None; alsostring.index_of(xs: T[], x: T) -> Optional[int]for arrays of comparable element types[x] string.repeat— no static signature;string.repeat(s: string, n: int) -> string[x] string.substring(s: string, start: int, end: int) -> string— byte range [start, end)[x] string.char_at(s: string, i: int) -> string— the one-byte (or one-rune) slice at i[x] string.last_index_of(s: string, sub: string) -> Optional[int]— Some(rightmost byte index) or None[x] string.count(s: string, sub: string) -> int— non-overlapping occurrences[x] string.replace_first(s: string, search: string, with: string) -> string[x] string.trim_start(s: string) -> string[x] string.trim_end(s: string) -> string[x] string.trim_prefix(s: string, pre: string) -> string— remove pre if present[x] string.trim_suffix(s: string, suf: string) -> string[x] string.pad_start(s: string, width: int, fill: string) -> string[x] string.pad_end(s: string, width: int, fill: string) -> string[x] string.lines(s: string) -> string[]— split on newlines[x] string.is_empty(s: string) -> bool[x] string.reverse(s: string) -> string— (arrayreversealready exists; strings differ)[x] string.ord(s: string) -> int— byte/codepoint conversion[x] string.chr— no static signature;string.chr(code: int) -> string, byte/codepoint conversion, arg-domain-checked (code must be in range)
Decisions to settle for strings: byte vs rune (UTF-8) semantics for length,
substring, char_at, indexing. Today length is bytes; a rune-aware set may
warrant separate byte_* vs char_* names.
Numbers
Section titled “Numbers”[x] math.abs— no static signature; overloaded int/float (math.abs(x: int) -> int/math.abs(x: float) -> float)[x] math.min— no static signature;math.min(a, b), same ordered scalar type (int/float/bool/string/value-enum; funcref value form int/float only)[x] math.max— no static signature;math.max(a, b), same ordered scalar type (int/float/bool/string/value-enum; funcref value form int/float only)[x] math.clamp— no static signature;math.clamp(x, lo, hi), same numeric type[x] math.sign— no static signature; overloaded int/float (math.sign(x: int) -> int/math.sign(x: float) -> int), both arms return int: -1, 0, 1[x] math.pow(base: float, exp: float) -> float[x] [ref] math.sqrt(x: float) -> float— Newton’s method (basic arithmetic, no awk math); exact on perfect squares, within ~1 ulp otherwise; domain x >= 0 (negative aborts located)[x] math.floor(x: float) -> int[x] math.ceil(x: float) -> int[x] math.round(x: float) -> int[x] math.trunc(x: float) -> int— toward zero (to_int(float) already truncates)[x] math.gcd— no static signature;math.gcd(a: int, b: int) -> int[x] math.lcm(a: int, b: int) -> int[x] math.int_min() -> int— platform limits, computed at runtime by detecting shell integer width[x] math.int_max() -> int— platform limits, computed at runtime by detecting shell integer width[x] [ref] math.exp(x: float) -> float— e^x, pure-arithmetic Taylor series (no awk libm); a result outside the representable float range aborts located; usemath.exp(1.0)for Euler’s number — there is noeconstant, soestays free as the conventional error-binding name incatch (e)[x] [ref] math.ln(x: float) -> float— natural log; domain x > 0 (non-positive aborts located, no hang)[x] math.log10(x: float) -> float— base-10 log; domain x > 0[x] math.log2(x: float) -> float— base-2 log; domain x > 0[x] math.pi() -> float— compile-time constant 3.141592653589793 (no awk call)[x] string.format_float— no static signature;string.format_float(x: float, decimals: int) -> string, render x to exactlydecimalsdecimal places via awkprintf(f-conversion at the given precision); decimals must be >= 0 (negative aborts located); rounding follows platform printf (round-half-to-even on most libc, NOT guaranteed half-up); contrast withto_string(float)(full %.17g precision)
Trigonometry (sin/cos/tan/asin/acos/atan/atan2) is deferred: awk trig is absent on no-libm busybox builds, so each function needs a new pure-arithmetic implementation. All other math ops above are pure-arithmetic (basic +,-,*,/ only).
Arrays
Section titled “Arrays”length (documented under Strings above) also accepts an array argument:
length(xs: T[]) -> int.
-
[x] array.is_empty— no static signature;array.is_empty(xs: T[]) -> bool -
[x] array.push— no static signature;array.push(xs: T[], v: T) -> void -
[x] array.contains— no static signature;array.contains(xs: T[], x: T) -> bool, mirrorsstring.contains(one compiler entry, delegates through the same overload resolver); arrays of comparable element types -
[x] array.index_of— no static signature;array.index_of(xs: T[], x: T) -> Optional[int], mirrorsstring.index_of, Some(first matching index) or None; arrays of comparable element types -
[x] array.reverse— no static signature;array.reverse(xs: T[]) -> T[] -
[x] array.map— no static signature;array.map(xs: T[], f: fn(T) -> U) -> U[] -
[x] array.filter— no static signature;array.filter(xs: T[], f: fn(T) -> bool) -> T[] -
[x] array.each— no static signature;array.each(xs: T[], f: fn(T) -> void) -> void -
[x] array.reduce— no static signature;array.reduce(xs: T[], init: U, f: fn(U, T) -> U) -> U -
[x] array.sort— no static signature;array.sort(xs: T[]) -> T[], ascending, any ordered scalar element type (int/float/bool/string/value-enum) -
[x] array.sort_by— no static signature;array.sort_by(xs: T[], less: fn(T, T) -> bool) -> T[] -
[x] array.find— no static signature;array.find(xs: T[], f: fn(T) -> bool) -> Optional[int], Some(first matching index) or None -
[x] array.any— no static signature;array.any(xs: T[], f: fn(T) -> bool) -> bool -
[x] array.all— no static signature;array.all(xs: T[], f: fn(T) -> bool) -> bool -
[x] array.count_where— no static signature;array.count_where(xs: T[], f: fn(T) -> bool) -> int -
[x] array.slice— no static signature;array.slice(xs: T[], start: int, end: int) -> T[] -
[x] array.concat— no static signature;array.concat(a: T[], b: T[]) -> T[] -
[x] array.flatten— no static signature;array.flatten(xs: T[][]) -> T[] -
[x] array.unique— no static signature;array.unique(xs: T[]) -> T[], comparable element types, order-preserving -
[x] array.first— no static signature;array.first(xs: T[]) -> T, abort on empty (catchable) -
[x] array.last— no static signature;array.last(xs: T[]) -> T, abort on empty (catchable) -
[x] array.take— no static signature;array.take(xs: T[], n: int) -> T[] -
[x] array.drop— no static signature;array.drop(xs: T[], n: int) -> T[] -
[x] array.pop— no static signature;array.pop(xs: T[]) -> T, remove and return last; abort on empty -
[x] array.remove_at— no static signature;array.remove_at(xs: T[], i: int) -> void -
[x] array.insert_at— no static signature;array.insert_at(xs: T[], i: int, v: T) -> void -
[x] array.sum— no static signature; overloaded int/float (array.sum(xs: int[]) -> int/array.sum(xs: float[]) -> float) -
[x] array.range— no static signature;array.range(n: int) -> int[], 0..n-1; and/orrange_from(start, end, step) -
[x] array.zip— no static signature;array.zip(a: T[], b: U[]) -> (T, U)[]— IMPLEMENTED (tuples milestone)
[x] dict.has— no static signature;dict.has(d: {K: V}, k: K) -> bool[x] dict.is_empty— no static signature;dict.is_empty(d: {K: V}) -> bool[x] dict.keys— no static signature;dict.keys(d: {K: V}) -> K[][x] dict.values— no static signature;dict.values(d: {K: V}) -> V[], insertion order[x] dict.get— no static signature;dict.get(d: {K: V}, k: K) -> Optional[V], Some(value) if present, else None; fold withunwrap_orfor a value-or-fallback[x] dict.remove— no static signature;dict.remove(d: {K: V}, k: K) -> void[x] dict.size— no static signature;dict.size(d: {K: V}) -> int[x] dict.merge— no static signature;dict.merge(a: {K: V}, b: {K: V}) -> {K: V}, b wins on conflict[x] dict.clear— no static signature;dict.clear(d: {K: V}) -> void
I/O and system
Section titled “I/O and system”[x] print(msg: string, to: int = stdout) -> void[x] env.get(name: string) -> Optional[string][x] env.has(name: string) -> bool[x] fs.read_file(path: string) -> string[x] fs.write_file(path: string, content: string) -> void[x] fs.append_file(path: string, content: string) -> void[x] process.run— no static signature;process.run(argv: string[]) -> string, stdout; aborts on nonzero exit[x] exit(code: int) -> void[x] env.set(name: string, value: string) -> void— export name=value into the process env; all later children inherit it; name must match[A-Za-z_][A-Za-z0-9_]*(invalid aborts located); mutates global state (contrast withprocess.run_env)[x] env.unset(name: string) -> void— remove name from the process env; no-op success if not set; same name validation asenv.set[x] read_line() -> Optional[string]— one line from stdin; None on EOF[x] read_stdin() -> string— all of stdin[x] read_secret(prompt: string) -> Optional[string]— print prompt to stderr, read one line with echo suppressed (stty -echo, best-effort; no-op when stdin is not a TTY); Some(line) or None on EOF[x] set_stdin(content: string) -> void— replace fd 0 with content’s exact bytes; test stdin injection[x] [ref] fs.file_exists(path: string) -> bool[x] [ref] fs.is_dir(path: string) -> bool[x] [ref] fs.is_file(path: string) -> bool— true for a regular file or symlink to one (follows symlinks, liketest -f); total[x] [ref] fs.is_symlink(path: string) -> bool— true iffpathitself is a symlink (does not follow); total[x] fs.file_size(path: string) -> int— byte count viawc -c; leading whitespace stripped; aborts located on missing/unreadable/dir[x] fs.read_link(path: string) -> Optional[string]— immediate symlink target viareadlink -- "$path"; non-symlink or any failure -> None (never aborts); single-level, not canonicalized;readlinknot strict POSIX but present on every target[x] fs.chmod(path: string, mode: string) -> void—chmod -- "$mode" "$path"; single--guards leading-dash path; located abort on failure; statement-only (Void)[x] fs.symlink(target: string, link_path: string) -> void—ln -s -- "$target" "$link_path"; target stored verbatim (dangling links allowed); located abort if link_path exists; statement-only (Void)[x] fs.symlink_force(target: string, link_path: string) -> void— create-or-replace symlink; removes and re-creates link_path if it is a symlink or regular file; located abort if link_path is a real directory; NOT atomic, DESTRUCTIVE-ON-FAILURE (removes old link_path first); statement-only (Void)[x] fs.glob(pattern: string) -> string[]— shell pathname expansion; no match -> empty array (not the literal pattern); matched names are inert data; pattern word-splits (a pattern with a space is two globs); total; zsh parity via emulate-sh banner[x] fs.temp_file() -> string—mktemp; located abort on failure; caller owns cleanup viaremove_file;mktempnot strict POSIX but present on every target[x] fs.temp_dir() -> string—mktemp -d; located abort on failure; caller owns cleanup viaremove_dir[x] fs.list_dir(path: string) -> string[][x] fs.make_dir(path: string) -> void— mkdir -p semantics[x] fs.remove_file(path: string) -> void[x] fs.remove_dir(path: string) -> void[x] fs.rename(from: string, to: string) -> void— enables atomic write-via-rename[x] fs.cwd() -> string[x] fs.change_dir(path: string) -> void[x] fs.program_path() -> string— the script’s invocation path ($0 captured at top level before main; same value from any call depth; meaningful for built artifacts)[x] [ref] fs.dir_name(path: string) -> string— directory portion, POSIX dirname semantics, pure string (no external process)[x] [ref] fs.base_name(path: string) -> string— final component, POSIX basename semantics, pure string (no external process)[x] fs.which(name: string) -> Optional[string]— resolved path, or None if not found[x] process.run_status— no static signature;process.run_status(argv: string[]) -> int, run, return exit code, do not abort[x] process.run_full— no static signature;process.run_full(argv: string[]) -> RunResult, stdout + stderr + code; stdin=/dev/null[x] process.run_input— no static signature;process.run_input(argv: string[], stdin: string) -> string, run argv with stdin fed as exact bytes via printf %s (no added newline); returns stdout (trailing newlines stripped); aborts on nonzero child exit; empty argv aborts[x] process.run_input_full— no static signature;process.run_input_full(argv: string[], stdin: string) -> RunResult, same stdin feed; returns RunResult{stdout,stderr,code} without aborting; empty argv aborts[x] process.run_env— no static signature;process.run_env(argv: string[], env: {string: string}) -> string, run with augmented child env (adds/overrides; does not clear); aborts on nonzero exit likeprocess.run; env NAME must match[A-Za-z_][A-Za-z0-9_]*(invalid name aborts located); VALUES are arbitrary; for a supported argv[0],process.run_env(argv, {})==process.run(argv); argv[0] limitation: a name starting with-or containing=is unsupported (use a path-qualified form); usesenv(POSIX); non-goals: no clear-slate, no unset[x] process.run_env_status— no static signature;process.run_env_status(argv: string[], env: {string: string}) -> int, run with augmented child env, return exit code, do not abort; same NAME validation and empty-argv abort asrun_env[x] process.run_env_full— no static signature;process.run_env_full(argv: string[], env: {string: string}) -> RunResult, run with augmented child env, return RunResult (stdout+stderr+code), do not abort; same NAME validation and empty-argv abort asrun_env[x] process.exec_command— no static signature;process.exec_command(argv: string[]) -> void, replace the shell process with argv via POSIX exec (same PID, inherits all fds/signals); never returns on success; empty argv aborts located; exec failure exits 127/126 (not a wisp abort)- Non-goal:
mtime(no POSIX-portable epoch source; useprocess.run(["stat", ...])platform-specifically)
Signals and traps
Section titled “Signals and traps”Signal and exit HOOKS for cleanup and graceful shutdown. Both lower to the POSIX
trap builtin. The handler must be a user-declared fn() -> void (nullary, void
return); a builtin or wrong-type handler is a compile error. Both are
statement-only (void, not usable as a value). Replace semantics: a later call for
the same signal/EXIT replaces the first (POSIX trap). To run multiple steps,
compose one handler fn. Not full process supervision (forwarding/reaping a
long-running child is a future feature).
[x] on_exit(handler: fn() -> void) -> void— registerhandlerto run when the process exits (normal completion,exit(n), or located abort); the original exit code is preserved (__wisp_ec=$?; handler; exit "$__wisp_ec"); does NOT fire on an uncaught terminating signal (useon_signal+exitfor that); statement-only; total (cannot fail)[x] on_signal(sig: string, handler: fn() -> void) -> void— registerhandlerto run when the signal namedsigis received; execution RESUMES after the handler returns (a caught signal does not terminate); to terminate, the handler callsexit(n);sigmust be a STRING LITERAL from{INT, TERM, HUP, QUIT, USR1, USR2}(compile-time validated; EXIT/KILL/STOP and any non-literal are compile errors); statement-only; total (no runtime validation);exit()from anon_signalhandler triggers theon_exithandler (the canonical shared-cleanup shutdown pattern)- Non-goal: no handler accumulation/stacking (replace-only); no
EXITviaon_signal(useon_exit); noKILL/STOP(untrappable); no handler arguments; nountrap/ignore/default; no re-entrancy protection
Background processes
Section titled “Background processes”Explicit async: launch a command in the background, then poll or block on it.
No promises, no event loop, no callbacks. Process is an opaque handle (only
.pid: int is readable; no compare/interpolate/arithmetic). REAPING CONTRACT:
the caller must wait every spawned process to reclaim its temp files and reap
the zombie; a never-waited Process leaks until the shell exits. wait_any
returns ONE process — wait the others too. wisp manages the spawned command,
not grandchildren the command itself forks (same boundary as run).
[x] process.spawn— no static signature;process.spawn(argv: string[]) -> Process, launchargvin the background; stdout/stderr captured to temp files, stdin/dev/null. Empty argv aborts.[x] process.wait— no static signature;process.wait(p: Process) -> RunResult, block untilpfinishes; returns its RunResult. IDEMPOTENT (cached; reclaims temps on the first call).[x] process.is_done— no static signature;process.is_done(p: Process) -> bool, non-blocking, non-reaping liveness check (true once finished, normal or signaled).[x] process.pid_alive(pid: int) -> bool— total liveness probe: true iff a process withpidexists and is signalable by the caller (POSIXkill -0); nonexistent PID and EPERM both return false; never aborts. For a process wisp itself spawned, preferis_done(PID-reuse-resistant).[x] process.signal— no static signature;process.signal(p: Process, sig: string) -> void, sendsigto the spawned command;sigmust be a STRING LITERAL from{INT, TERM, HUP, QUIT, USR1, USR2, KILL, STOP, CONT}. Best-effort; no-op once finished. PID-reuse-RESISTANT (a documented microsecond residual race, unclosable without process groups). Observes the command’s CURRENT signal disposition (a not-yet-installed handler yields the default action, so an exact trapped exit code is not guaranteed from spawn-then-signal).[x] process.wait_any— no static signature;process.wait_any(ps: Process[], poll_secs: int) -> Process, pollpsin LIST order, return the first observed done;poll_secsis WHOLE SECONDS (sub-second sleep is non-portable), must be>= 0(0 = tight poll). Empty list / negative poll abort.[x] process.make_fifo— no static signature;process.make_fifo(path: string) -> void, create a named pipe (mkfifo) for IPC; aborts if the path exists.
Pipelines
Section titled “Pipelines”[x] process.pipe— no static signature;process.pipe(stages: string[][]) -> RunResult, run a pipelinestage1 | stage2 | ..., capturing the last stage’s stdout/stderr/code. Last-stage status only (no PIPESTATUS; intermediate failures invisible). First stage’s stdin is/dev/null; only the last stage’s stderr is captured (earlier stages’ stderr inherits to the script’s stderr). An empty outer array aborts located; an empty inner argv yields 127 from that stage. argv is injection-safe (inert).
Time and randomness
Section titled “Time and randomness”[x] now() -> int— current Unix epoch in whole seconds viadate +%s;date +%sis a near-universal extension present on every dash/busybox ash/bash/zsh target; wall clock (not monotonic — can move backward under NTP/adjustment)[x] sleep(secs: int) -> void— pause forsecswhole seconds via the externalsleep;secs >= 0required (negative aborts located:sleep: negative duration); statement-only (Void, not usable as a value); sub-second/fractional sleep is not supported (not portable)[x] math.random— no static signature;math.random(max: int) -> int, non-cryptographic random int in[0, max);max > 0required (non-positive aborts located:random: max must be positive); uses awksrand/rand(works on dash, which has no$RANDOM; present in busybox awk); seed varied per call via wall-clock time + process id + monotonic per-process counter; not for security/tokens; two runs in the same second with the same PID reproduce the sequence (documented non-crypto limitation)
Argument parsing
Section titled “Argument parsing”[x] parse_args— no static signature;parse_args(args: string[], value_flags: string[]) -> ({string: string}, string[], string[]), pure; splits args into (valued-flag dict, boolean-switch array, positional array);value_flagsnames which flags consume the next token; space formF VALUEand equals formF=VALUEboth supported;--terminates (space-form value-flag consumes a following--as its value); emptyF=yields present empty string; last occurrence wins; lone-is a positional; non-goals: no short bundling, no alias resolution, no type coercion, no help gen
Errors
Section titled “Errors”[x] error(msg: string) -> error[x] throw <error>(keyword) /[x] try { } catch (e) { } finally { }[x] error_with(code: int, msg: string) -> error[x] wrap(err: error, msg: string) -> error— new error withmessage=msg,code=0, carryingerras its cause;erris unchanged; no message concatenation (reach the inner viacause); cause survives value paths (throw/catch,unwrap_err,map_err/or_else) but not the fault path (unwrapclears it)[x] cause(err: error) -> Optional[error]—Some(inner)iferrwas produced bywrap, elseNone; walk the chain withmatch (cause(cur)) { case Some(inner) {...} case None {...} }
Debugging
Section titled “Debugging”[x] debug(x) -> string— structural render of ANY value (scalars,T[],Optional/Result, dict, struct, tuple,error, funcref,RunResult); does not print. Idiom:print(debug(x), stderr)
Optional and Result access
Section titled “Optional and Result access”[x] is_some(o: Optional[T]) -> bool[x] is_none(o: Optional[T]) -> bool[x] is_ok(r: Result[T]) -> bool[x] is_err(r: Result[T]) -> bool[x] unwrap— no static signature; overloaded Optional/Result (unwrap(o: Optional[T]) -> T/unwrap(r: Result[T]) -> T) — abort on None/Err (catchable)[x] unwrap_or— no static signature; overloaded Optional/Result (unwrap_or(o: Optional[T], fallback: T) -> T/unwrap_or(r: Result[T], fallback: T) -> T) — total; fallback evaluated eagerly[x] unwrap_err(r: Result[T]) -> error— the carried error, or abort on Ok (catchable)
Optional[T] supports ==/!= when T is comparable (int/bool/string/
float, an enum, or nested comparable Optional); Result and aggregate
carriers stay non-comparable.
Combinators
Section titled “Combinators”Overloaded over Optional and/or Result; the function runs only on the activating branch.
array.map (documented under Arrays above) is also the Optional/Result
combinator: array.map(o: Optional[T], f: fn(T) -> U) -> Optional[U] /
array.map(r: Result[T], f: fn(T) -> U) -> Result[U] (U != void) —
success-side transform.
[x] and_then— no static signature; overloaded Optional/Result (and_then(o: Optional[T], f: fn(T) -> Optional[U]) -> Optional[U]/and_then(r: Result[T], f: fn(T) -> Result[U]) -> Result[U]) — flat-map
array.filter (documented under Arrays above) is also the Optional
combinator: array.filter(o: Optional[T], f: fn(T) -> bool) -> Optional[T] —
Optional-only; not defined over Result.
map and filter here are the same array.map/array.filter builtins
applied through Optional’s value-or-empty representation, not a separate
combinator.
[x] or_else— no static signature; overloaded Optional/Result (or_else(o: Optional[T], f: fn() -> Optional[T]) -> Optional[T]/or_else(r: Result[T], f: fn(error) -> Result[T]) -> Result[T]) — lazy fallback; Err passes the error handle to f[x] map_err— no static signature;map_err(r: Result[T], f: fn(error) -> error) -> Result[T]— Result-only; transform the Err side
Regular expressions
Section titled “Regular expressions”POSIX ERE, whole-match only, byte-based under LC_ALL=C. A malformed pattern is a catchable located abort.
[x] regex.matches(s: string, pattern: string) -> bool[x] regex.find(s: string, pattern: string) -> Optional[string]— first whole match, or None[x] regex.find_all(s: string, pattern: string) -> string[]— every non-empty whole match, left to right[x] regex.replace(s: string, pattern: string, replacement: string) -> string— all matches;&is the whole match,\&a literal&; no group backrefs
JSON (import "json")
Section titled “JSON (import "json")”The first core module: a reserved namespace opted into with import "json".
json.Value is an opaque handle storing canonical JSON text (numbers/strings
verbatim, so no precision loss). See the stdlib guide.
[x] json.decode— no static signature;json.decode[T](s: string) -> T, validate + parse;Tdefaults tojson.Value, orstring/int/float/bool; malformed aborts located[x] json.encode(v: json.Value) -> string— canonical text[x] json.from_string(x: string) -> json.Value[x] json.from_int(x: int) -> json.Value[x] json.from_float(x: float) -> json.Value[x] json.from_bool(x: bool) -> json.Value[x] json.null() -> json.Value[x] json.array(v: json.Value[]) -> json.Value[x] json.object(v: {string: json.Value}) -> json.Value[x] json.type_of(v: json.Value) -> string[x] json.get(v: json.Value, key: string) -> Optional[json.Value][x] json.at(v: json.Value, i: int) -> Optional[json.Value][x] json.as_string(v: json.Value) -> string— type mismatch aborts located[x] json.as_int(v: json.Value) -> int— type mismatch aborts located[x] json.as_float(v: json.Value) -> float— type mismatch aborts located[x] json.as_bool(v: json.Value) -> bool— type mismatch aborts located
Deferred / needs a language feature first
Section titled “Deferred / needs a language feature first”- User-defined generic functions (
fn f[T, U](...)) with local, one-pass type inference at call sites — IMPLEMENTED (generics slice 1, “M-gen-1”). Type parameters are usable wherever a type annotation appears and a bare type parameter is barred from every operation that needs a concrete type. Codegen is unchanged: a generic function emits one type-erased shell function. The generics program continues in further slices, each its own milestone:- M-gen-2: the
comparablebound (fn f[T: comparable](...)) — IMPLEMENTED. AcomparableTunlocks==/!=, with the inferred type restricted toint/bool/string/float(and value enums) and checked at the call site. Codegen unchanged. Ordered comparison and arithmetic on aTremain errors under any bound (they need monomorphization). - M-gen-3: the
numericbound (fn f[T: numeric](...)) — IMPLEMENTED. AnumericTunlocks+,-,*,/,<,<=,>,>=,==,!=, and unary-;%(modulo) is rejected because it is undefined for float. Onlyintorfloatsatisfies the bound at a call site. Codegen monomorphizes: a numeric-bounded function emits one shell function per concrete type (__int,__float), so the arithmetic dispatches correctly. - M-gen-4: generic structs (
struct Box[T]) — IMPLEMENTED (R7-generics-mgen4). Explicit type arguments at call sites (f[int](...)) remain unsupported; the postfix[is array indexing, so that syntax is decided in a later slice.
- M-gen-2: the
- Tuples (
(T1, T2, ...), n >= 2): fixed-arity, immutable, opaque — IMPLEMENTED.zip,process.run_full, and tuple destructuring (let (a, b) = ...) are also implemented; only a destructuringmatchremains deferred. - An optional/maybe type (
Optional[T]=Some(T) | None) — IMPLEMENTED.find/index_of/last_index_ofnow returnOptional[int]and the dict accessordict.get(d, k) -> Optional[V]was added, alongside the access builtinsis_some/is_none/unwrap/unwrap_or.Optionalis opaque (no==/to_string()/switch);Noneconcretizes atlet/return/assignment only.first/last/char_atkeep their catchable aborts. -
matchbinding +Result[T]— IMPLEMENTED.match (o) { case Some(x) { } case None { } }(andOk(x)/Err(e)) binds the matched-variant payload into the matching arm’s block only.Result[T]is a taggedOk(T) | Err(error)value (single type parameter; the error payload is always the built-inerrorhandle, conceptuallyResult[T, error]), with access builtinsis_ok/is_err/unwrap/unwrap_err/unwrap_or—unwrap/unwrap_orare overloaded acrossOptionalandResult.unwrapof anErrperforms a catchable located abort carrying the error’s.message;Ok(x)infers fully likeSome(x), whileErr(e)(likeNone) concretizes itsTatlet/return/assignment only.Resultis opaque (no==/to_string()/switch). A destructuringmatchremains deferred. - Rune-aware (UTF-8) string operations as a parallel
char_*family. - Regular expressions (POSIX ERE, whole-match only) — IMPLEMENTED.
matches/regex_find/regex_find_all/regex_replace, byte-based underLC_ALL=C, backed byawkwith the pattern/subject/replacement passed as inert data viaENVIRON. Capture groups, flags, compiled patterns, andregex_splitare deferred (busybox/mawk lackmatch(s, re, arr));regex_splitis the clean fast-follow. - [DONE] Broader shell targets (bash, zsh). The
#!/bin/shoutput now runs under busybox ash, dash, bash, and zsh, all tested in CI. bash was free (POSIX superset that word-splits unquoted$var). zsh needed a guarded header shimif [ -n "${ZSH_VERSION:-}" ]; then emulate sh 2>/dev/null || setopt shwordsplit; fi(in the current shell, not a subshell), because native zsh disables the unquoted-$varword-splitting that the dict/arrayfor x in $listloops depend on; the shim is a no-op under the other three. A self-contained tooling milestone: it touched the codegen header and CI, not the language. - Time (
now/sleep) and randomness (random) — IMPLEMENTED. Wall-clock epoch seconds viadate +%s, whole-second sleep, and non-crypto[0, max)random int via awksrand/randwith per-call-varied seed. See the Time and randomness section above.
Suggested grouping into milestones
Section titled “Suggested grouping into milestones”These are a starting point, to be turned into specs one at a time.
- Collections core:
sort,sort_by,find,any,all,slice,concat,first/last,sum,range, plus dictvalues/get/remove/merge. - String round-out:
substring,char_at,last_index_of,count,trim_start/trim_end/trim_prefix/trim_suffix,pad_start/pad_end,lines,is_empty. - Numeric/math:
clamp,sign,floor/ceil/round,sqrt,gcd(powdeferred to a portable-math milestone; see Numbers above). - Filesystem metadata:
is_file,is_symlink,file_size,read_link,chmod,symlink,glob,temp_file,temp_dir— IMPLEMENTED. See the I/O and system section above. - Optional type (
Optional[T]), unlockingfind/index_of/last_index_ofand dictgetwithout sentinels — shipped. -
matchbinding form +Result[T](Ok/Err,is_ok/is_err/unwrap_err, overloadedunwrap/unwrap_or) — shipped. - Optional/Result chaining combinators (
map/and_then/filter/or_else/map_err) — IMPLEMENTED. Next: a destructuringmatchand a richerrun.
Testing
Section titled “Testing”These builtins are only meaningful inside *_test.wisp files (test bodies, setup, teardown).
Documented in the stdlib guide and the testing guide.
[x] assert(cond: bool, msg: string = "") -> void[x] assert_eq[T: comparable](got: T, want: T) -> void[x] assert_ne[T: comparable](got: T, want: T) -> void[x] assert_some(o: Optional[T]) -> void[x] assert_none(o: Optional[T]) -> void[x] assert_ok(r: Result[T]) -> void[x] assert_err(r: Result[T]) -> void[x] assert_contains(s: string, sub: string) -> void/assert_contains(xs: T[], x: T) -> void[x] skip(reason: string) -> void[x] test_tmpdir() -> string