On this page

Value book

Value method dispatch

Dispatch precedence, complete method families, collection and string semantics, path effects, outcome forwarding, task methods, and extension workflow.

Status
Method standard-library reference
For
Runtime, language, and completion contributors
On this page
  1. Capability boundary
  2. Dispatch precedence
  3. General collection surface
  4. Ordering and conditions
  5. String surface
  6. Record and receiver-polymorphic methods
  7. Path method split
  8. Numeric methods
  9. Outcome forwarding
  10. Stream methods
  11. Task lifecycle methods
  12. Method suggestions
  13. Arity helpers
  14. Change workflow
  15. Known sharp edges

Value methods form Shoal’s standard library. Most live in shoal-value, where they are pure or use the narrow CallCtx bridge. Methods needing evaluator capabilities—filesystem inspection, .feed, channels, or namespaces—are intercepted before this crate sees the call.

Sources: methods/mod.rs and its receiver modules under methods/.

Capability boundary🔗

The value crate’s CallCtx exposes only:

call_closure(function, positional_values) -> VResult<Value>
cwd() -> PathBuf

This is enough for functional collection stages and pure path absolutization. It is not enough to spawn a command or inspect a filesystem. Those operations remain evaluator responsibilities.

flowchart LR
accTitle: Capability boundary
accDescr: Shows the components and relationships described in Capability boundary.
  AST["recv.method(args)"] --> Eval["evaluator intercepts"]
  Eval --> Feed["feed / channel / namespace / fs path methods"]
  Eval --> Core["shoal_value::methods::call_method"]
  Core --> Pure["collection / string / record / number"]
  Core --> Ctx["CallCtx: closure calls + cwd"]
  Core --> Effects["save / append via direct std::fs::OpenOptions"]

call_method attaches the call span with or_span, preserving a more precise error span produced inside a closure or argument operation.

Dispatch precedence🔗

Order is observable:

  1. .tap and .also act on the original receiver;
  2. lazy CAS bytes answer cheap methods, materialize for .load/.bytes, or materialize and redispatch for other methods;
  3. outcomes forward through their outcome-specific layer;
  4. streams use lazy combinators/sinks;
  5. pure path component methods intercept ambiguous names such as .abs;
  6. the general method match handles collections, strings, records, numbers, persistence, and tasks;
  7. unknown-method suggestions are produced for the receiver type.
flowchart TD
accTitle: Dispatch precedence
accDescr: Method dispatch checks special wrappers and type surfaces in order; Outcome methods use metadata first and otherwise forward to their parsed payload before general lookup.
  Method --> Tap{"tap/also?"}
  Tap -->|yes| TapRun["call f(recv), return unchanged recv"]
  Tap -->|no| CAS{"CasBytes?"}
  CAS -->|yes| CheapOrLoad
  CAS -->|no| Outcome{"Outcome?"}
  Outcome -->|yes| Native{"outcome-native method?"}
  Native -->|yes| Metadata["status / ok / stdout / stderr / duration"]
  Native -->|no| Parsed{"parsed payload supports method?"}
  Parsed -->|yes| Forward["forward to outcome.out"]
  Parsed -->|no| Unknown
  Outcome -->|no| Stream{"Stream?"}
  Stream -->|yes| StreamSurface
  Stream -->|no| Path{"Path component method?"}
  Path -->|yes| PathSurface
  Path -->|no| General
  General --> Unknown["typed suggestion"]

.tap(f)/.also(f) calls f(receiver) for its effect, discards the closure result, and returns an unchanged clone of the receiver. Intercepting before outcome forwarding ensures a tap sees the full outcome rather than only .out.

General collection surface🔗

The sequence adapter accepts List, Table, and Range. Tables convert to values containing their records; ranges materialize integers. Raw streams are rejected here because stream driving needs context and boundedness rules.

MethodContract
len, countcharacter count for strings; bytes/elements/fields/range length otherwise
is_emptylen == 0 on supported receivers
first, lastzero args returns value/null; integer arg returns a list slice
collectmaterializes range; returns list/table unchanged
streamfinite list/table/range or text lines to a lazy stream
teesplit finite sequence into n coordinated branches
mapcall closure once per element, return list
reduce, foldaccumulator plus element closure
where, filterretain items whose closure condition is true
eachcall closure for every finite element, discard all results, return null
any, allshort-circuit predicate aggregation
findfirst matching element or null
flat_mapmap then flatten collection results
sort_bystable/implementation sort by closure-extracted key
sortdirect total-order sort, or sort_by when closure supplied
reversereverse element order
uniqstructural de-duplication
sum, min, maxzero-argument aggregate
flattenone-level collection flattening
enumerateattach index/value structure
skip, takenonnegative count slice
chunksdivide into fixed-sized lists
zippair with another finite collection
group, group_byList of {key, values} records
joinconcatenate with string separator, default empty

The zero-argument aggregates explicitly reject projection arguments and suggest .map(f).sum()/.min()/.max(). This prevents a plausible-looking lambda from being silently ignored.

first() and last() return a single value; first(n) and last(n) always return a list. Empty single-value selection returns null.

Ordering and conditions🔗

Direct sort, min, and max delegate to the same total-order comparator used by relational operators. Comparable families include numeric, strings, paths, sizes, durations, datetimes, times, and booleans according to ops::compare. Mixing incomparable values returns an error rather than falling back to rendered text.

Predicate methods call Value::as_condition, so only bool and outcome are accepted. A closure returning null, zero, or an empty list is a type error.

String surface🔗

MethodResult
lineslist of lines, strips trailing \r per line
wordsUnicode-whitespace-separated strings
charsUnicode scalar values as one-character strings
trimtrimmed string
upper, lowerUnicode case conversion
split(separator)list of substrings; separator is required
starts_with(prefix)boolean; prefix required
ends_with(suffix)boolean; suffix required
contains(value)receiver-polymorphic containment
replace(pattern, replacement)string/regex-aware replacement path
matches(pattern)all match projection defined in strops.rs
match(pattern)first match projection
parse_int, parse_floatnumeric parsing or typed error
strcanonical string conversion
displaydisplay-oriented string conversion
jsoncompact JSON through value_to_json

Required string arguments use req_str_arg; missing arguments are arg_error, not an empty-string default. This prevents calls like "x".starts_with() from returning the misleading value true.

String len counts Unicode scalar values (chars().count()), not UTF-8 bytes and not user-perceived grapheme clusters.

Record and receiver-polymorphic methods🔗

MethodReceiverBehavior
keysrecordordered key list
valuesrecordvalues in insertion order
itemsrecordList of two-element [key, value] lists
set(key, value)recordpersistent-style updated record
merge(other)recordmerge with right/other values winning; existing positions stay, new keys append
get(key, default = null)record/list/string-like supported casessafe lookup
contains(value)string/collection/record-supported pathreceiver-specific membership

Records use IndexMap, so key, value, item, JSON, and render order follows insertion order. Callers must not replace them with unordered maps without updating visible contracts.

Path method split🔗

Pure path methods live in shoal-value:

MethodBehavior
namefilename component
stemfilename without final extension
extfinal extension
parentparent path or null-like result as implemented
join(value)append path component
absmake absolute using CallCtx::cwd

Filesystem-backed path methods such as read, lines, size, metadata, or existence are intercepted in the evaluator because they require the injected Fs port. This split is easy to miss when adding completion metadata: both sets are language-visible methods on path.

The general save and append methods write a receiver to a supplied path with direct std::fs::OpenOptions calls in methods/path.rs; stream .save does likewise in methods/stream.rs. CallCtx is not a filesystem capability. Evaluator call sites can surround some value saves with journal undo hooks, but the actual I/O bypasses the injected Fs port and Leash/policy effect boundary. This is architectural debt: fake filesystems and policy enforcement do not cover every language-visible write today.

Numeric methods🔗

abs is supported on numeric values, but path .abs wins earlier in dispatch. Integer absolute value uses checked arithmetic so i64::MIN cannot silently overflow. round, floor, and ceil accept an optional nonnegative precision argument and preserve numeric semantics implemented in num.rs.

Outcome forwarding🔗

An outcome has native fields/methods for command metadata and output. If a method is not outcome- specific, the dispatcher forwards it to out_value(). That makes a structured builtin or adapted command behave like its table/record/list:

ls.where(row => row.size > 1mb).sort_by(row => row.name)

while raw stdout/stderr remain explicitly accessible.

Forwarding means a newly added general method can automatically become available on outcomes. Test both a parsed structured outcome and an unparsed text outcome.

Stream methods🔗

Streams are intercepted before finite collection dispatch. Lazy combinators return a new StreamVal without driving upstream; sinks drive it with CallCtx for closure stages. A method that is neither a stream-native combinator nor sink may collect only when the stream is statically/operationally bounded; unbounded sources raise stream_unbounded rather than hang forever.

The dedicated streams and channels chapter maps the pull and tee state machines.

Task lifecycle methods🔗

MethodBehavior
await, waitblock until task result, then return value/error
cancelrequest cancellation and invoke registered hook
is_donecurrent completion boolean
suspendrequest/mark suspension through hooks
resumeresume through hooks
is_suspendedcurrent suspension boolean

All are zero-argument methods. Task identity survives cloning, so lifecycle calls through any clone affect the same shared task.

Method suggestions🔗

suggest.rs owns Levenshtein distance, the global method-name inventory, receiver-specific method lists, and unknown-method construction. This metadata serves diagnostics and may serve completion. Adding dispatch without adding suggestion metadata creates a method that works but is absent from helpful discovery; adding metadata without dispatch advertises a nonexistent method.

Arity helpers🔗

HelperRule
arg(args, n)required positional or arg_error
no_argsrejects any positional or named arguments
agg_no_argssame, with aggregate-specific correction hint
int_argoptional nonnegative integer with nonnegative default
str_argoptional string with default
req_str_argrequired string with caller-supplied missing message

Most general methods do not accept named arguments. A method implementation must reject stray arguments rather than accidentally ignoring them.

Change workflow🔗

  1. decide whether the method belongs in evaluator interception, stream/outcome special dispatch, a receiver module, or the general match;
  2. choose its position relative to lazy bytes, outcomes, streams, and path ambiguity;
  3. validate all positional and named arity explicitly;
  4. reuse CallCtx only for closure invocation/cwd; add broader effects at the evaluator boundary;
  5. specify lazy versus eager behavior and boundedness;
  6. preserve table/list duality where semantically appropriate;
  7. attach span with or_span so nested locations win;
  8. update method_names and methods_for suggestion metadata;
  9. test direct receiver, outcome-forwarded receiver, lazy-CAS receiver, and unknown-method hint;
  10. update external method reference and internal value/wire docs.

Known sharp edges🔗

  • Dispatch is a hand-ordered chain; moving an arm can change meaning on path, outcome, stream, or lazy bytes receivers.
  • save/append and stream .save call std::fs::OpenOptions directly, bypassing evaluator Fs injection and policy ports; port unification must preserve streaming and append semantics.
  • Some method aliases expand the discoverable surface (reduce/fold, where/filter, group/group_by, tap/also). Registry/help must include both.
  • Unicode string length counts scalar values, not graphemes.
  • Outcome forwarding can make a method appear to exist on outcomes even when its behavior differs between structured and raw output.
  • Generic .json() on lazy bytes materializes, while nested value_to_json is bounded; the call-site distinction is intentional but nonobvious.
  • Receiver metadata currently advertises .get across the shared sequence family, but dispatch implements only list+integer and record+string lookup; table/range .get type-errors. Completion is broader than behavior.
  • Boolean .str()/.display() succeeds in strops::to_str, but bool’s receiver-specific method table omits both, so valid calls are absent from completion/unknown-method hints.
Type to search every guide navigate open esc close
Diagram