On this page

Standard library reference

Namespace reference

Every member of Shoal's json, yaml, toml, csv, math, http, os, and config namespaces, plus environment and secret access.

Status
All namespace dispatch arms checked against shoal-eval
For
Shoal programmers and integration authors
On this page
  1. Complete member inventory
  2. JSON
    1. json.parse
    2. json.stringify
  3. YAML
    1. yaml.parse
    2. yaml.stringify
  4. TOML
    1. toml.parse
    2. toml.stringify
  5. CSV
    1. csv.parse
    2. csv.stringify
  6. Math constants
  7. Math functions
    1. One-argument functions
    2. Two-argument functions
    3. Three-argument function
  8. HTTP
    1. Signatures
    2. Response record
  9. OS
  10. Configuration
    1. config.all
    2. config.get
    3. Projected fields
  11. Environment root value
  12. Secret access
  13. Shadowing and diagnostics

Shoal exposes eight unbound root names as namespaces: json, yaml, toml, csv, math, http, os, and config. A user binding with the same name shadows the namespace. Functions must be called; only math constants and projected configuration fields support bare field access.

json.parse('{"ready":true}')
math.pi
os.platform()
config.history

Complete member inventory🔗

NamespaceConstants/fieldsFunctions
jsonnoneparse, stringify
yamlnoneparse, stringify
tomlnoneparse, stringify
csvnoneparse, stringify
mathpi, e, tau, inf, nan, sqrt2sqrt, cbrt, sin, cos, tan, asin, acos, atan, atan2, ln, log10, log2, log, exp, floor, ceil, round, trunc, abs, sign, pow, min, max, hypot, clamp
httpnoneget, delete, post, put
osnoneplatform, arch, pid, hostname, username, cpus, uptime, env
configresolved top-level keysall, get

JSON🔗

json.parse🔗

json.parse(text: str) -> value

Parses one JSON document and maps it into Shoal values:

JSONShoal
nullnull
booleanbool
integer in rangeint
other numberfloat
stringstr
arraytable when non-empty and every item is an object; otherwise list
objectordered record
let doc = json.parse('{"name":"shoal","ports":[80,443]}')
doc.ports.map(x => x + 1)

Missing/wrong argument and malformed input raise arg_error.

json.stringify🔗

json.stringify(value, pretty: bool = false) -> str
json.stringify(value, true) -> str

The second positional boolean and named pretty: true both request indentation. Values without a direct JSON representation degrade through the runtime conversion rules: paths and temporal values become display strings; outcomes and special values use their safe representation rather than exposing secret content.

json.stringify({name: "shoal", ready: true})
json.stringify((ls .).out, pretty: true)

No value is arg_error; serialization failure uses custom.

YAML🔗

yaml.parse🔗

yaml.parse(text: str) -> value

Parses YAML through a JSON-compatible intermediate representation and returns native Shoal values. As with JSON, a non-empty array made entirely of objects becomes a table; empty, scalar, or mixed arrays remain lists.

yaml.parse('name: shoal\nready: true\n')

Non-string/missing input and malformed YAML are arg_error.

yaml.stringify🔗

yaml.stringify(value) -> str

Serializes the JSON-compatible form of a Shoal value.

yaml.stringify({services: [{name: "api", port: 8080}]})

No value is arg_error; serializer failure uses custom.

TOML🔗

toml.parse🔗

toml.parse(text: str) -> value
let manifest = toml.parse((cat "Cargo.toml").out.str())
manifest.package.name

Malformed TOML is arg_error.

toml.stringify🔗

toml.stringify(value) -> str

TOML requires a table-like top-level value. A scalar or incompatible nested value raises arg_error with that constraint in the message.

toml.stringify({package: {name: "demo", version: "0.1.0"}})

CSV🔗

csv.parse🔗

csv.parse(text: str) -> table<record<str>>

The first row supplies headers. Every field remains a string; CSV namespace parsing does not infer numbers, booleans, dates, or paths.

let rows = csv.parse("name,count\napi,3\nweb,2\n")
rows.map(r => {name: r.name, count: r.count.parse_int()})

Malformed CSV is arg_error.

csv.stringify🔗

csv.stringify(table_or_records) -> str

Accepted inputs are a table, a list containing only records, or one record. Header order comes from the first record. Later missing keys become empty fields. Later extra keys not present in the first record are not emitted. Strings write as text; other values use compact inline rendering.

csv.stringify([
    {name: "api", count: 3},
    {name: "web", count: 2},
])

A wrong receiver shape is type_error; writer errors use custom; an impossible UTF-8 result uses utf8_error.

Math constants🔗

Constants are fields, not calls:

MemberValue
math.piπ
math.eEuler’s number
math.tau
math.infpositive infinity
math.nanIEEE NaN
math.sqrt2√2
2.0 * math.pi
math.sqrt2 * math.sqrt2

Calling a constant is not the namespace contract. Accessing a function without () produces a field_missing teaching error.

Math functions🔗

Every math function accepts int or float, converts to floating point, and returns float.

One-argument functions🔗

FunctionOperation
math.sqrt(x)square root
math.cbrt(x)cube root
math.sin(x)sine, radians
math.cos(x)cosine, radians
math.tan(x)tangent, radians
math.asin(x)inverse sine
math.acos(x)inverse cosine
math.atan(x)inverse tangent
math.ln(x)natural logarithm
math.log10(x)base-10 logarithm
math.log2(x)base-2 logarithm
math.exp(x)
math.floor(x)floor
math.ceil(x)ceiling
math.round(x)nearest integer-valued float
math.trunc(x)truncate fractional part
math.abs(x)absolute value
math.sign(x)signum

Two-argument functions🔗

FunctionOperation
math.atan2(y, x)quadrant-aware arctangent
math.log(x, base)logarithm in arbitrary base
math.pow(x, exponent)floating exponentiation
math.min(a, b)IEEE floating minimum
math.max(a, b)IEEE floating maximum
math.hypot(x, y)√(x²+y²)

Three-argument function🔗

math.clamp(x, lo, hi) -> float

lo > hi is arg_error. Missing arguments are arg_error; non-numbers are type_error. Floating-domain conditions follow the Rust/IEEE operation: for example, a negative square root produces NaN rather than a Shoal error.

math.sqrt(81)
math.pow(2, 10)
math.clamp(cpu, 0, 100)

HTTP🔗

The HTTP namespace performs synchronous requests with a 30-second global timeout and a 64 MiB response-body read cap.

Signatures🔗

http.get(url: str|path, headers: record = {}) -> response
http.delete(url: str|path, headers: record = {}) -> response
http.post(url: str|path, body = "", headers: record = {}) -> response
http.put(url: str|path, body = "", headers: record = {}) -> response

Headers may be supplied as headers: or in the method-specific final positional record.

let response = http.get(
    "https://api.example.test/v1/items",
    headers: {Accept: "application/json"},
)

let created = http.post(
    "https://api.example.test/v1/items",
    json.stringify({name: "demo"}),
    headers: {"Content-Type": "application/json"},
)

Response record🔗

Every HTTP call returns:

FieldTypeMeaning
statusintHTTP status code
okbooltrue for 200–299
bodystrdecoded response body
jsonvalue or nullparsed JSON when the trimmed body is valid JSON
headersrecord<str>response headers

HTTP 4xx/5xx statuses are not raised as errors; inspect .ok or .status. Transport, timeout, and body-read failures raise net_error.

Request body serialization uses the same feedability rules as .feed: strings are UTF-8 bytes, bytes are raw, scalar data renders to text, and records/tables/lists become compact JSON. A path is a name, not file content; use path.read or path.read_bytes.

Header values accept strings, secrets, and other renderable values. A secret is intentionally permitted here for authentication header injection, but remains redacted in ordinary output.

Current limitations:

  • no custom timeout argument;
  • no streaming request or response body;
  • no explicit redirect/TLS/proxy configuration surface;
  • response body must decode as text for the returned record;
  • repeated response header names collapse into one record key.

OS🔗

Every os member is a nullary function. Passing any positional or named argument is arg_error.

FunctionResultSource
os.platform()strRust target OS name, such as linux or macos
os.arch()strRust target architecture, such as x86_64 or aarch64
os.pid()intcurrent host process id
os.hostname()strlibc gethostname, or unknown
os.username()strsession USER, LOGNAME, USERNAME, then libc user database, then unknown
os.cpus()intavailable parallelism, at least 1
os.uptime()`durationnull`
os.env()record<str>evaluator session environment, UTF-8 entries only
{platform: os.platform(), arch: os.arch(), cpus: os.cpus()}
os.env().get("CI", "false")

os.uptime() is best effort. The current implementation uses libc and Unix clock APIs and is not a Windows implementation.

Configuration🔗

The host injects a snapshot of the already layered and validated configuration. The namespace does not independently walk the filesystem, so it cannot disagree with configuration the host applied.

config.all🔗

Despite its name, this is a nullary function:

config.all() -> record
config.all()

Calling config.all without parentheses is field access and reads a top-level key literally named all; absent means null.

config.get🔗

config.get(key: str) -> value|null

This performs one top-level lookup. It does not parse dotted paths.

config.get("history")
config.get("missing") ?? {}

Projected fields🔗

Any top-level key can be read as a field:

config.version
config.history
config.render

A missing top-level key returns null, unlike ordinary record field access, which raises field_missing.

If no host snapshot was injected, config.all() is {} and every lookup is null. The normal shoal interactive, script, and -c host inject the resolved configuration; embedded evaluator tests and some kernel-less integrations may not.

See Configuration and prompt for every schema key and wiring status.

Environment root value🔗

env is also a canonical builtin command, but env.NAME and assignment have dedicated language behavior:

env.HOME
env.BUILD_MODE = "release"
env.BUILD_MODE

Reads return str|null. Writes update the evaluator’s session environment and therefore later child processes. The root environment is not one of the eight namespace-dispatch names; it is evaluator-provided state.

Secret access🔗

secret is another special root receiver, with exactly one implemented member:

secret.get(name: str) -> secret
let token = secret.get("api-token")
http.get(url, headers: {Authorization: token})

The value renders redacted and cannot be fed as generic stdin. Store access errors use permission; an unknown name uses not_found; a non-UTF-8 stored value uses utf8_error; wrong arguments use arg_error.

Shadowing and diagnostics🔗

A lexical binding wins over a namespace:

let json = {parse: "shadowed"}
json.parse

Namespace functions accessed as fields produce a field_missing message that tells you to call them. Unknown called members also use field_missing:

json.parse     # function must be called
json.decode(x) # unknown method

Use a local binding only when shadowing is deliberate; otherwise choose names such as json_doc, http_response, or os_info.

Type to search every guide navigate open esc close
Diagram