On this page

Standard library reference

Value types and every method

The complete runtime value and method surface, organized by receiver type with signatures, return shapes, consumption rules, and errors.

Status
Method inventory checked against shoal-value and evaluator-hosted dispatch
For
Shoal programmers and tooling authors
On this page
  1. Runtime type inventory
  2. Access before methods
  3. Methods available on every receiver
    1. tap and also
    2. json
    3. save and append
    4. feed
    5. pick
  4. Collection receiver set
    1. Complete collection method index
    2. Size and endpoints
    3. collect
    4. stream
    5. tee
    6. map
    7. reduce and fold
    8. where and filter
    9. each
    10. any and all
    11. find
    12. flat_map
    13. sort and sort_by
    14. reverse
    15. uniq
    16. sum
    17. min and max
    18. flatten
    19. enumerate
    20. skip and take
    21. chunks
    22. zip
    23. group and group_by
    24. join
    25. contains
    26. get
  5. String methods
  6. Numeric methods
  7. Boolean conversion
  8. Path fields and methods
  9. Bytes methods
    1. Lazy content-addressed bytes
  10. Record methods
  11. Table-specific notes
  12. Range methods
  13. Glob fields and methods
  14. Regex values
  15. Temporal fields
    1. Datetime
    2. Duration
    3. Time and size
  16. Error fields
  17. Outcome fields and forwarding
  18. Task methods
  19. Stream methods
    1. Lazy combinators
    2. Consuming sinks
    3. Live tee
  20. Channel handle methods
  21. Closure, command, and secret values
  22. Feedability by type
  23. Conversion matrix
    1. .str()
    2. .display()
    3. Parse methods
  24. Method errors and suggestions

Shoal’s method library is receiver-oriented. Some methods are pure value transformations; others are explicit sinks; several evaluator-hosted methods need filesystem, process, event-bus, or terminal access. This chapter inventories both layers.

Runtime type inventory🔗

Runtime nameRepresentation and role
nullabsence
booltrue or false; no general truthiness
intsigned 64-bit integer
floatIEEE 64-bit floating point
strUnicode UTF-8 text
pathplatform path bytes through PathBuf; not synonymous with file content
globpattern plus origin directory and hidden-match flag
regexcompiled regular expression
sizeunsigned byte quantity
durationsigned nanoseconds
datetimezoned calendar instant
timetime of day
bytesresident bytes or lazy content-addressed bytes
listordered heterogeneous values
recordinsertion-ordered string keys to values
tableordered records with tabular rendering
rangeinteger interval
streamsingle-consumer bounded or live value source
errorcaught evaluator failure
outcomecommand status, raw output, parsed value, and metadata
taskasynchronous computation/job handle
closurefunction/lambda with captured environment
commandparsed command reference used by aliases
secretopaque secret value with redacted presentation

Access before methods🔗

value.field first attempts direct field access. For most non-record values, a missing field falls back to calling a zero-argument method, making name.upper and path.read useful projection shorthands. Records are deliberately strict: a missing record field does not fall through, because user data might collide with method names. Call a record method with parentheses.

["a", "b"].map(.upper)   # method projection
(ls .).map(.name)         # path/record field projection
row.keys                  # looks for a real field named keys; may fail
row.keys()                # record method

Optional ?. protects only a null receiver. It does not turn an absent record field into null.

Methods available on every receiver🔗

tap and also🔗

value.tap(f) -> value
value.also(f) -> value

These aliases call f(value), discard the callback result, and return the original receiver unchanged.

let result = expensive()
    .tap(x => x.json().append("trace.jsonl"))
    .map(transform)

If the callback errors, the method errors. The receiver is cloned for the callback, but resource identity/single-consumption rules still apply to streams and tasks.

json🔗

value.json() -> str

Returns compact JSON from Shoal’s generic value encoder. This is a representation helper, not a stable protocol schema for tasks, streams, errors, or outcomes. Secrets remain redacted. A content-addressed bytes value used as the top-level receiver is fully loaded; the same value nested inside another record/table becomes a bounded preview/ref object.

save and append🔗

value.save(path: str|path) -> value
value.append(path: str|path) -> value

Strings and resident bytes write verbatim. Other values write compact JSON. save truncates/replaces; append appends. Both return the original receiver.

report.save("report.json")
"next line\n".append("events.log")

Current security boundary: these value methods call std::fs::OpenOptions directly. They do not use the evaluator Fs port, do not inherently consult Leash policy, and do not automatically install journal undo. Treat them as direct host filesystem effects in the preview implementation.

feed🔗

value.feed(command) -> outcome

This is evaluator-hosted rather than a pure method. It serializes the receiver and starts a child with those bytes as stdin. The complete serialization table appears under Feedability.

pick🔗

value.pick(prompt: str = "> ", multi: bool = false) -> value|list

Opens an alternate-screen fuzzy picker. Lists/tables supply their items; a record or scalar is treated as one selectable item. Single mode returns the selected value or null; multi mode returns a list.

It requires both stdin and stdout to be terminals. Non-interactive use is arg_error; cancellation raises custom.

Collection receiver set🔗

The eager collection core accepts list, table, and range. Many operations convert a table row into a record and return an ordinary list rather than preserving table identity.

Complete collection method index🔗

MethodSignatureResult
len, count()integer element count
is_empty()boolean
first() / (n)item/null or list
last() / (n)item/null or list
collect()list/range materialization; table remains table
stream()finite stream
tee(n = 2)list of streams
map(item => value)list
reduce, fold(initial, (acc,item)=>acc)final accumulator
where, filter(item => bool)list
each(item => any)null
any, all(item => bool)bool
find(item => bool)item or null
flat_map(item => collection)flattened list
sort() or (item => key)list
sort_by(item => key)list
reverse()list
uniq()list
sum()accumulated value; empty is integer 0
min, max()item or null
flatten()one-level flattened list
enumerate()list of [index, item]
skip, take(n)list
chunks(positive_n)list of lists
zip(other_collection)list of [left,right]
group, group_by(item => key)list of {key,values} records
join(separator = "")string; elements must be strings
contains(item)bool
getsee belowonly list+int, not table/range

Size and endpoints🔗

items.len()
items.count()
items.is_empty()
items.first()
items.first(3)
items.last()
items.last(3)

first()/last() return one item or null. With a count they always return a list. Counts must be non-negative integers.

len also works on strings, bytes, and records. A range length is computed without first materializing it.

collect🔗

(1..5).collect()   # [1,2,3,4]

On a list or table, eager collect() returns the receiver unchanged. It does not turn a table into a list. Streams have a separate consuming collect() described later.

stream🔗

[1, 2, 3].stream()
(ls .).out.stream()
"a\nb\n".stream()

Collections stream their elements. A string or bytes value streams decoded lines; bytes use lossy UTF-8. The returned stream is bounded and single-consumer.

tee🔗

let forks = items.tee(2)
let left = forks[0]
let right = forks[1]

For an eager collection, each fork replays a cloned list. n must be positive. Live stream tee behavior is different and described under streams.

map🔗

collection.map(f) -> list

Calls f(item) in order and collects its result. Errors stop iteration.

(ls .).map(row => {name: row.name, kib: row.size / 1kib})

reduce and fold🔗

collection.reduce(initial, f) -> value
collection.fold(initial, f) -> value

These aliases perform a left fold: acc = f(acc, item) in input order.

[1, 2, 3, 4].reduce(0, (acc, n) => acc + n)

where and filter🔗

collection.where(predicate) -> list
collection.filter(predicate) -> list

The predicate must return bool or an outcome condition. Selected original values are preserved.

(ls .).where(.type == "file")

each🔗

collection.each(f) -> null

Calls f(item) for side effects and discards every result. It is eager and stops at the first error.

paths.each(p => p.display().append("paths.log"))

any and all🔗

Both short-circuit and require condition results.

rows.any(.status >= 500)
rows.all(.ready)

any on empty is false. all on empty is true.

find🔗

Returns the first item whose predicate succeeds, or null.

services.find(.name == "api")

flat_map🔗

The callback must return a list, table, or range. Its elements are appended one level into the result.

projects.flat_map(.members)

sort and sort_by🔗

[3, 1, 2].sort()
rows.sort(.name)
rows.sort_by(.modified)

sort(f) is the projection alias of sort_by(f). Directly comparable compatible types are int/float, string, path, size, duration, datetime, time, and bool. Heterogeneous or otherwise incomparable keys raise type_error; comparison errors are not silently converted to equality.

reverse🔗

Collections return a reversed list. A string receiver returns its Unicode characters in reverse order as a string.

uniq🔗

Keeps the first occurrence of each structurally equal value using a stable O(n²) scan. It does not sort and accepts no projection.

sum🔗

Starts with the first item and applies +, preserving homogeneous value types such as float, size, or duration. An empty collection returns integer 0.

(ls .).map(.size).sum()

sum takes no callback. Use .map(f).sum().

min and max🔗

Return the selected item or null for empty. They take no callback; project with .map(f) first if you want the key itself, or sort records when you need the whole row.

flatten🔗

Each outer item must itself be a list/table/range. Only one level is flattened.

enumerate🔗

Returns [[0,item0], [1,item1], ...]. It does not create {index,value} records.

skip and take🔗

items.skip(10)
items.take(10)

Return lists. Counts are non-negative. On strings these methods slice by Unicode scalar count and return a string.

chunks🔗

Splits into lists of at most n. Zero is arg_error.

zip🔗

Pairs elements until either side ends. Both receivers must be eager collection types.

group and group_by🔗

rows.group_by(.type)
words.group(x => x.lower())

Both require a key function and return an ordinary list in first-key-seen order:

[
  {key: KEY, values: [ITEM...]},
  ...
]

Keys use structural equality.

join🔗

Every element must be a string. The separator defaults to the empty string.

["a", "b", "c"].join(",")

contains🔗

For a list/table/range it performs structural item membership. For a string it requires a string substring. For a record it requires a string key.

get🔗

The implemented signatures are exactly:

list.get(index: int, default = null) -> value
record.get(key: str, default = null) -> value

Negative list indexes count from the end; out-of-range returns the default. table.get and range.get are not implemented even though current completion/did-you-mean metadata groups get with sequence methods.

String methods🔗

MethodSignatureMeaning
len, count()Unicode scalar count
is_empty()no characters
lines()list of logical lines, CR removed at line end
words()Unicode whitespace split
chars()list of one-scalar strings
trim()trim surrounding whitespace
upper()Unicode uppercase
lower()Unicode lowercase
split(separator: str)literal split into list
starts_with(prefix: str)bool
ends_with(suffix: str)bool
contains(substring: str)bool
replace`(strregex, replacement: str)`
matches(regex)list of full match strings
match(regex)first full match string or null
parse_int()integer or arg_error
parse_float()float or arg_error
take, skip(n)substring by scalar count
reverse()reversed scalar order
stream()bounded stream of lines
str, display()identity

split, starts_with, and ends_with require their text argument; a missing argument is not treated as "".

" alpha beta ".trim().words()
"v1.2.3".starts_with("v")
"a-b-c".replace("-", "/")
"abc-123".replace(re"([a-z]+)-([0-9]+)", "$2:$1")

Regex replacement expands $1 and named captures. .matches() returns every non-overlapping full match, not capture records. .match() returns only the first full match.

Numeric methods🔗

int and float implement:

MethodSignatureResult
abs()same numeric kind; min-int overflow errors
round(decimal_places = 0)int unchanged; rounded float
floor(decimal_places = 0)int unchanged; floored float at precision
ceil(decimal_places = 0)int unchanged; ceiling float at precision
str, display()canonical number text

Decimal places must be a non-negative integer. A scaling overflow leaves an already finite value unchanged rather than producing a spurious infinity/NaN.

(-5).abs()
3.14159.round(2)
3.14159.floor(3)

The richer transcendental surface is under the math namespace.

Boolean conversion🔗

bool.str() and bool.display() return "true" or "false". Booleans also support generic JSON/save/append/feed/tap operations. They do not have numeric methods.

Path fields and methods🔗

MemberCall formResult
namefield or ()basename string or null
stemfield or ()basename without final extension, or null
extfield or ()extension string or null
parentfield or ()parent path or null
join`(strpath)`
abs()absolute against session cwd
readfield or ()UTF-8 string
read_bytesfield or ()raw bytes
linesfield or ()UTF-8 line list
existsfield or ()bool
is_dirfield or ()bool
is_filefield or ()bool
sizefield or ()size
modifiedfield or ()datetime or null
str()strict UTF-8 conversion
display()lossy display conversion
let p = path("src/main.rs")
{name: p.name, parent: p.parent, bytes: p.size, changed: p.modified}
p.parent.join("lib.rs")
p.read.lines()

Relative filesystem methods resolve against the session cwd. read and lines use strict UTF-8 and raise utf8_error; read_bytes does not. exists/is_dir/is_file convert metadata errors to false, while size raises. modified may return null when the timestamp cannot be represented.

A bare path is not feedable because it denotes a name, not contents:

p.read.feed(^consumer)
p.read_bytes.feed(^consumer)

Bytes methods🔗

Resident bytes implement len, count, is_empty, stream, str, display, JSON/save/append/feed, and universal methods.

  • .str() requires valid UTF-8 and raises utf8_error otherwise.
  • .display() decodes lossily.
  • .stream() decodes lossily and yields logical lines.
  • .len() counts bytes, not characters.

Lazy content-addressed bytes🔗

Large command capture may spill into the journal CAS and retain only a resident preview. It still reports runtime type bytes, with additional host methods:

MethodBehavior
len, counttrue total length from metadata, no load
is_emptymetadata-only
refval:blake3:HASH, no load
load, bytesload and return resident bytes
any other bytes methodload full content, then dispatch

Writing the short ref as a string value in the same journal-aware evaluator can resolve it for subsequent methods. Unknown/unavailable refs raise not_found or io_error.

If capture itself exceeded the hard spill cap, truncated metadata means even CAS content is only a prefix; loading cannot reconstruct bytes never stored.

Record methods🔗

MethodSignatureResult
keys()ordered list of string keys
values()ordered list of values
items()ordered list of [key,value] lists
set(key: str, value)new record
merge(other: record)new record, right wins
get(key: str, default = null)value/default
contains(key: str)bool
len, count, is_empty()size predicates

Records are immutable values. set does not mutate the receiver. Replacing an existing key keeps its position. merge overlays the right record; existing key positions remain and new keys append.

let base = {a: 1, b: 2}
base.set("b", 20).merge({c: 3})
base.items()

Record field data wins over method shorthand. Use parentheses for these methods.

Table-specific notes🔗

A table is semantically an ordered record collection, but method outputs are usually lists:

  • map, where, filter, sort, reverse, and grouping return list;
  • collect() returns the table unchanged;
  • len counts rows;
  • get(index) is currently not implemented;
  • table[index] is also not implemented by strict index access;
  • use .first(), .last(), .find(), .take(n), or convert via a mapping when row selection is needed.

This inconsistency is a current API rough edge.

Range methods🔗

Ranges share eager collection transforms and materialize integers when needed. 1..5 yields 1,2,3,4; 1..=5 includes 5. They support iteration, length, collect, transforms, aggregations, and finite stream conversion. .get() is not implemented despite sequence completion metadata.

Glob fields and methods🔗

MemberMeaning
.pattern / .pattern()source pattern string
.expand()sorted list<path>
any collection methodexpand, then dispatch on list
let rust = glob("crates/**/*.rs")
rust.pattern
rust.where(.name.ends_with("test.rs"))

The glob stores its creation cwd, so later session navigation does not change the expansion root. Passing a glob as a command argument expands at the callee boundary. glob(..., hidden: true) includes hidden entries. The accepted follow: constructor argument is currently not stored/applied.

Regex values🔗

Regex values are consumed by string .match, .matches, and .replace. There is no public regex field for its source and no capture-record method today. Generic rendering/JSON uses a safe representation. Feeding a regex is feed_error.

Temporal fields🔗

Datetime🔗

Direct fields:

.year .month .day .hour .minute .second

Each returns an integer. Datetime values otherwise use comparison/operators and generic JSON/save/append/feed/tap.

Duration🔗

Direct relative-anchor fields:

duration.ago
duration.from_now

They use the evaluator clock and return datetime, with overflow on an unrepresentable instant. Duration values can be added/subtracted where the operator table permits and summed in collections.

Time and size🔗

These quantity scalars use comparison/arithmetic where defined and generic JSON/save/append/feed. They do not implement .str(); interpolate for presentation:

"limit={10mb} timeout={250ms} at={09:30}"

Error fields🔗

A caught error exposes exactly:

FieldType
.codestr
.msgstr
.hint`str
.stderr`str
.status`int

No .span field is exposed, even though the internal diagnostic may carry one.

try {
    risky()
} catch err {
    {code: err.code, message: err.msg, status: err.status}
}

Outcome fields and forwarding🔗

FieldTypeBehavior
.status`intnull`
.okboolsuccess according to accepted codes
.signal`strnull`
.durdurationelapsed time
.pidintchild pid; builtin outcomes use 0
.cmdstrcommand head/description
.stdoutbytesraw stdout, possibly CAS-backed
.stderrbytesraw stderr
.outstructured value or bytes/stringraises cmd_failed when non-ok
.errstderr bytesalso raises cmd_failed when non-ok in current field path

Unknown fields and methods on a successful outcome forward to .out:

ls.where(.type == "file")
(stat Cargo.toml).size

On a failed outcome, .out, .err, and forwarded access raise cmd_failed. .stdout, .stderr, .status, .ok, and metadata remain directly readable.

Outcome method .stdout() and .stderr() are also accepted. Other methods forward to .out. Use explicit fields in protocol-oriented code to make failure behavior obvious.

Task methods🔗

MethodResultEffect
await, waittask result or its errorblock until complete
cancelnullset cancel state and run hooks
is_doneboolcompletion query
suspendsame taskset suspended state and run process hook
resumesame taskclear state and run resume hook
is_suspendedboolsuspension query
let t = spawn { slow_work() }
if (!t.is_done()) { t.cancel() }
t.await()

Task handles compare by identity. Awaiting observes the stored completion. Local evaluator suspension can signal registered process groups; kernel wire suspend/resume currently returns TASK_CONTROL_UNAVAILABLE and is not equivalent.

Stream methods🔗

A stream is single-consumer. Calling a lazy combinator moves its source into a new stream. Driving a sink consumes it. Reusing the original handle after either operation raises stream_consumed.

Lazy combinators🔗

MethodSignatureEmission
map(item => value)transformed item
where, filter(item => bool)selected original item
scan(initial, (acc,item)=>acc)each new accumulator
flat_map`(item => collectionstream-compatible)`
take(n)first n, then end; makes bounded
take_until`(predicateother_stream)`
dedupe()remove adjacent duplicates
distinct()remove all previously seen duplicates
debounce(duration)quiet-period emission
throttle(duration)rate-limited emission
window`(positive_countduration)`
buffer(capacity = 1)identity stage today; no queue or pacing effect
enumerate()[index,item]
merge(stream)interleaved streams
zip(stream)paired [left,right] until one ends
every(1s)
    .map(t => {at: t, kind: "tick"})
    .take(10)
    .collect()

Durations must be non-negative. Window count must be positive. merge/zip require a stream.

Consuming sinks🔗

MethodResultNotes
each(f)nullrun callback per item
collect()listrequires a bounded stream
save(path)pathappends one encoded line per item
append(path)pathsame implementation as stream save today
tee(n=2)list of streamsbounded replay or live shared queues
into(channel)nullemit every item to named channel
render()nulldrive items to evaluator statement sink

Stream save and append both open the file in append mode today; neither truncates. Strings/bytes become their bytes plus newline; other items become compact JSON plus newline.

Like value save, stream file sinks call the platform filesystem directly rather than the evaluator Fs/Leash policy path. They are not a policy-safe logging primitive in the current preview.

For a bounded stream, unfamiliar eager methods such as .sort(), .sum(), or .len() first collect the entire stream and then dispatch to collection logic. For a live/unbounded stream this raises stream_unbounded; bound it explicitly with .take(n) or .take_until(...).

.feed(command) on a stream is not implemented. It returns a type error suggesting bounded collection; there is no incremental child-stdin bridge yet.

Live tee🔗

Bounded streams materialize once and create independent replay streams. A live stream uses one shared upstream with a queue capped at 64 items for each fork. If a fork is not driven and its queue fills, later values for that fork are dropped and counted. The fork subsequently receives an in-order {dropped: n} marker once space is available or the queue drains; overflow does not raise. All forks still obey single-consumer semantics.

Channel handle methods🔗

channel(name) returns a one-field record recognized specially by the evaluator. It implements:

MethodSignatureResult
emit(value)null
events(since: int?)stream of event records
latest()latest payload or null
take(timeout: duration?)next future payload

Event stream items are {channel, seq, ts, payload}. .events() replays the ring; .take() subscribes only to future events. Channel specifics are in Streams and channels.

Closure, command, and secret values🔗

These resource/capability-like types intentionally have no dedicated general method set beyond universal presentation/tap/save methods:

  • a closure is invoked with call syntax;
  • a command reference is invoked as a command/callable;
  • a secret is passed only through explicitly supported secure injection paths.

Generic .save()/.json() on a secret writes only the redacted/tagged representation. .feed() on secret, closure, command, task, error, glob, or regex is rejected.

Feedability by type🔗

ReceiverBytes sent to stdin
strUTF-8 verbatim, no added newline
resident/lazy bytesraw full bytes
int, float, bool, size, duration, datetime, timeinline decimal/display text, no newline
list<str>each string plus newline, including final newline
other list, record, tablecompact JSON
outcomeencoded structured .out; raw stdout for ordinary text
pathrejected: name is not content
streamrejected: incremental feed not implemented
secretfeed_error
task, closure, error, glob, regexfeed_error
null, command, other unsupportedtype_error
"abc".feed(^wc -c)
["a", "b"].feed(^wc -l)
{name: "api"}.feed(^jq .name)
path("input").read_bytes.feed(^consumer)

Conversion matrix🔗

.str()🔗

Accepted:

  • string identity;
  • path only when its bytes are valid UTF-8;
  • bytes only when valid UTF-8;
  • int, float, and bool canonical rendering.

Other types raise type_error with an interpolation hint.

.display()🔗

Same as .str(), except path/bytes use lossy UTF-8. It is not a universal “format anything” method; use interpolation for arbitrary values.

Parse methods🔗

Only strings implement .parse_int() and .parse_float(). There is no general .parse_bool, .parse_duration, or .parse_size; use literals, typed function/adapter word coercion, or explicit format parsing as appropriate.

Method errors and suggestions🔗

Unknown methods raise field_missing and may include a receiver-aware suggestion. Completion tables are harvested from the method dispatcher but currently have known over-approximations (get for table/range) and omit dynamic/evaluator-hosted surfaces such as stream combinators, pick, glob methods, channel methods, and CAS-specific methods.

Do not use completion presence as a runtime capability test. The definitive behavior is the tables in this chapter and a caught error during preview development.

Type to search every guide navigate open esc close
Diagram