On this page

Value book

Runtime value algebra

Every Value variant, equality and condition contracts, error anatomy, feed serialization, JSON projection, rendering, and lazy CAS bytes.

Status
Canonical runtime data model
For
Language, runtime, protocol, and rendering contributors
On this page
  1. Complete variant inventory
  2. Ownership and cloning
  3. Equality matrix
  4. There is no general truthiness
  5. Error values and thrown errors
  6. Feed serialization is not rendering
  7. Outcome anatomy
  8. Lazy CAS bytes
  9. JSON projection into values
  10. Value projection to JSON
  11. Inline rendering contract
  12. Block rendering contract
  13. Change checklist for a new variant
  14. Known sharp edges

shoal-value is the common data plane between evaluation, methods, adapters, execution, rendering, journaling, and the kernel wire. Its Value enum is a closed algebra: adding a variant requires an explicit decision at every serialization, equality, rendering, method, and protocol boundary.

Sources: lib.rs, value_types.rs, outcome.rs, json.rs, and render.rs.

Complete variant inventory🔗

VariantPayloadtype_name()Semantic class
Nullnonenullabsence/unit-like result
Boolboolboolcondition
Inti64intscalar number
Floatf64floatscalar number
StrUTF-8 Stringstrtext
PathPathBufpathbytes/native filesystem name
Globpattern, cwd, optionsglobdeferred path selection
Regexcompiled regex plus sourceregexpattern object
Sizedecimal bytes as u64sizedimensional scalar
Durationnanoseconds as i64durationsigned dimensional scalar
DateTimeboxed jiff::Zoneddatetimeinstant plus zone
Timehour/minute/secondtimewall-clock time of day
BytesArc<Vec<u8>>bytesresident opaque bytes
CasByteslazy reference, length, preview, loaderbytesspilled opaque bytes
ListVec<Value>listordered heterogeneous sequence
Recordordered IndexMap<String, Value>recordordered string-key map
TableVec<Record>tablesemantic list<record>
Rangeinteger bounds/inclusive flagrangefinite integer sequence
Streamsingle-consumption stream statestreamlazy temporal sequence
ErrorArc<ErrorVal>errorcaught/reified failure
OutcomeArc<OutcomeVal>outcomecommand result envelope
Taskshared task handletaskasync/job lifecycle handle
Closurecaptured functionclosurecallable language value
CmdRefstructured AST commandcommandcallable command alias
Secretname plus opaque shared textsecretprotected spawn-only material
classDiagram
accTitle: Complete variant inventory
accDescr: Shows the components and relationships described in Complete variant inventory.
  class Value
  class Data { Null Bool Int Float Str Path Size Duration DateTime Time Bytes List Record Table Range }
  class Deferred { Glob Regex CasBytes Stream }
  class Execution { Outcome Task Error }
  class Callable { Closure CmdRef }
  class Protected { Secret }
  Value <|-- Data
  Value <|-- Deferred
  Value <|-- Execution
  Value <|-- Callable
  Value <|-- Protected

type_name intentionally reports CasBytes as bytes; backing strategy is not a language type. Likewise, a Table is distinct enough for rendering and methods but semantically comparable with a list of records.

Ownership and cloning🔗

Small scalars clone by value. Large or identity-bearing objects use Arc or their own shared handle. Cloning a Value::Bytes clones the Arc, not the byte vector. Cloning an Outcome, closure, regex, or lazy bytes value similarly preserves shared identity/backing state. Lists, records, tables, paths, and strings clone their containers.

This distinction matters for equality: some variants compare content, some compare selected logical fields, and some compare shared identity.

Equality matrix🔗

PairEquality rule
same primitive typepayload equality
Int and Floatnumeric promotion to f64
Path and Strlossy path display compared to string
Glob and Globpattern text only
Regex and Regexsource text
DateTime and DateTimetimestamps, not zone presentation
Bytes and Bytesbyte-vector content
CasBytes and CasByteshash and length, without loading
resident Bytes and CasBytesalways unequal until explicitly materialized
list/record/table/range/errorstructural equality
Table and list of recordselement-wise semantic equality
streamsshared stream identity via same
tasksshared task identity via same
outcomesArc pointer identity
closuresArc pointer identity
command refsstructured AST equality
secretsname and secret content
all unlisted mixed pairsfalse

Path/string equality is pragmatic but lossy on platforms with non-UTF-8 path bytes. Glob equality ignores captured cwd/options, so it is pattern equality rather than full execution identity. Secret equality compares the protected value internally even though render/JSON never reveal it.

There is no general truthiness🔗

as_condition accepts only:

  • Bool, returning the contained boolean;
  • Outcome, returning its ok flag.

Everything else is type_error with a hint toward explicit predicates such as .is_empty(), .is_some(), or comparison with null. Empty strings, zero, empty lists, and null are not false.

This rule is relied on by if, while, predicate methods, logical operators, and assertion paths. Adding truthiness in one caller would fragment the language.

Error values and thrown errors🔗

VResult<T> is Result<T, ErrorVal>. The same ErrorVal can be wrapped in Value::Error by language catch behavior.

FieldMeaning
codestable machine-facing category
msghuman-facing detail
spanoptional source byte range
hintoptional corrective guidance
stderroptional captured process stderr context
statusoptional external exit status

with_span overwrites/set a span. or_span fills it only when absent, so the innermost location wins. Use type_error and arg_error constructors for common categories, but preserve domain-specific codes such as not_found, feed_error, stream_consumed, or cmd_failed where callers rely on them.

An error’s display is only <code>: <msg>; renderers and protocols must explicitly project the other fields.

Feed serialization is not rendering🔗

feed_bytes defines what .feed(command) places on stdin. It is deliberately different from inline and block display.

ValueFed bytes
StrUTF-8 verbatim, no newline added
resident/lazy bytesraw full content; lazy content resolves
Int, Float, Bool, Size, Duration, DateTime, Timeinline text, no newline
List<Str>each item plus newline, including a final newline
other list, record, tablecompact JSON
Outcomestructured .out recursively; otherwise raw stdout
Patherror: a name is not file content
Streamhonest not-implemented error; no buffering fake
Secretfeed_error; secrets enter only at spawn injection
task/closure/error/glob/regexfeed_error
null/command reference/othertype_error

A path’s .read must be invoked before feed. A stream must currently be bounded and collected first. These refusals prevent accidental filename-as-data behavior and unbounded buffering.

Outcome anatomy🔗

OutcomeVal is the process/builtin result envelope:

FieldPurpose
statusoptional numeric exit status
signaloptional termination signal name/number representation
oknormalized success boolean
stdoutresident preview/full bytes depending capture strategy
stdout_refoptional lazy/full-content backing object
stderrcaptured stderr bytes
dur_nsmeasured duration
pidchild PID, or zero for builtins
cmddisplay command
parsedoptional adapter/builtin structured output
streamedwhether bytes already reached the terminal
spanoptional invocation span

stdout_bytes() loads from stdout_ref when present. stdout_value() exposes bytes/lazy bytes. out_value() returns parsed output when present, otherwise a string for UTF-8 stdout or bytes-like content as implemented. Outcome value methods frequently forward to this logical .out.

Lazy CAS bytes🔗

Large captures spill out of the resident path and become CasBytesVal. The value retains a bounded preview, true length, content hash/reference, and a loader capability. Its cheap methods (len, count, is_empty, and reference access) must not load content.

stateDiagram-v2
accTitle: Lazy CAS bytes
accDescr: Shows the components and relationships described in Lazy CAS bytes.
  [*] --> Resident: small capture
  [*] --> Lazy: spill over RAM cap
  Lazy --> Metadata: len / ref / preview render
  Lazy --> Materialized: load / bytes / unsupported value method
  Metadata --> Lazy
  Materialized --> Resident

There are deliberately two JSON call-site behaviors:

  • general value_to_json(CasBytes) returns a bounded bytes_ref metadata/preview object without loading, including when nested in a larger value;
  • the .json() method first goes through method dispatch, whose bare lazy-bytes fallback materializes, then encodes resident bytes.

Do not simplify this distinction without re-auditing memory bounds and user expectations.

JSON projection into values🔗

json_to_value maps JSON primitives normally. JSON arrays become a Table when every element is an object, otherwise a List. Objects become ordered Record values.

This array heuristic is semantic: an empty array vacuously satisfies “all objects” only if the implementation’s explicit handling says so; callers should consult tests before depending on empty array type. Mixed arrays remain lists.

Value projection to JSON🔗

Value familyJSON shape
null/bool/int/float/stringcorresponding primitive
path/glob/regex/datetime/timedisplay/source string
size/durationnumeric base unit
byteslossy UTF-8 string
lazy bytesbounded tagged reference object
list/rangearray
recordobject
tablearray of objects
outcomeobject with status, ok, text out, text err
errorobject with code, msg
secretredacted secret(name) string
task/closure/stream/command and fallbackinline-rendered string

JSON projection is for language data interchange, not a lossless wire encoding. Paths lose invalid Unicode bytes, bytes are lossy text, outcome metadata is partial, identity objects stringify, and secrets redact. The kernel has its own tagged wire projection and should not blindly substitute this function when round-trip fidelity matters.

Inline rendering contract🔗

render_inline is canonical one-line language display and is directly asserted by conformance.

  • strings are quoted and escape quote, slash, newline, tab, carriage return, and NUL;
  • paths are unquoted unless the lossy display contains a space;
  • sizes use decimal SI units with up to two trimmed decimals;
  • durations decompose across weeks through nanoseconds;
  • datetimes render their timestamp; times use HH:MM or HH:MM:SS;
  • bytes render a size marker; lazy bytes also show their content reference;
  • records preserve insertion order and quote non-identifier-shaped keys;
  • streams/tasks/functions/commands/secrets render opaque descriptors;
  • outcomes render compact status/signal and ok, not captured output.

Block rendering contract🔗

render_block is presentation for top-level host output:

  • strings become raw text;
  • tables and lists of records become aligned ANSI-colored tables;
  • ordinary lists render one element per line;
  • records render aligned key/value lines;
  • successful structured outcomes render their parsed table/record/list;
  • other outcomes render captured stdout text, a success marker when genuinely outputless, or a failure marker;
  • resident bytes decode lossily; lazy bytes use a bounded preview/marker path.

Table columns use first-seen key order across rows. Cells cap at 60 display columns and the widest column is shrunk to fit the requested width when possible. Unicode display width, not byte length, governs padding and truncation.

Inline rendering, block rendering, feed bytes, builtin redirect bytes, JSON, and kernel wire values are six different contracts. Treating one as a universal serializer is a recurring architecture bug.

Change checklist for a new variant🔗

  1. add the enum variant, type_name, clone/ownership choice, and equality rule;
  2. decide condition behavior explicitly—usually reject;
  3. define inline and block rendering without leaking protected content;
  4. define feedability and newline rules;
  5. define json_to_value/value_to_json behavior and whether it is lossy;
  6. add value methods and suggestion metadata where appropriate;
  7. add operator and comparison behavior or explicit type errors;
  8. add kernel tagged-wire encode/decode and round-trip tests if transportable;
  9. audit adapter validation/coercion, journal serialization, transcript storage, and MCP projection;
  10. test nesting inside list, record, table, outcome, stream, and errors—not only the top level.

Known sharp edges🔗

  • Mixed path/string equality and JSON path projection are lossy on non-UTF-8 names.
  • Float equality follows IEEE behavior, including NaN != NaN.
  • Lazy bytes and resident bytes are unequal without materialization.
  • Generic JSON conversion is intentionally not round-trip-complete for runtime handles.
  • Secrets compare by content internally, so equality code is a sensitive-data path even though display is redacted.
  • Outcome equality is identity, not equivalent result content.
  • Table/list duality is implemented in equality and many methods but can still diverge in callers that match only one variant.
Type to search every guide navigate open esc close
Diagram