Skip to content

Standard Library

Warning

This page is under construction.

MindScript’s standard library is built for the kind of work scripts actually do: read some data, turn it into values, transform it, then write something useful back out—often in a way that composes in shell pipelines and CI.

You will see the same design pattern across the library:

  • At boundaries, functions usually return null on failure, often with an annotation explaining why.
  • Inside your program, you keep values JSON-shaped (arrays, objects, strings, numbers) and use small, predictable primitives to transform them.
  • When you truly cannot continue, something panics (a hard stop). You can catch hard failures with try(...) when you want to keep going.

This chapter is not a reference list. It shows how the standard tools fit together in real scripts.


Seeing what you have

When you’re building a transformation, the fastest way to debug is to print intermediate values without restructuring your code.

println(x) prints a readable representation and returns x, so you can insert it into an expression pipeline.

let s = readAll(STDIN)
if s == null then
    s
else
    let x = jsonRepair(s)
    if x == null then
        x
    else
        println(x)    # inspect, then keep going
        x
    end
end

When you need a guaranteed representation (for logs, snapshots, or error reports), use formatting explicitly:

  • formatValue(x) produces a stable formatted string for any value.
  • str(x) converts only “data values” (null/bool/numbers/strings/arrays/maps) to a string, returning null for opaque runtime objects (handles, types, functions).

A common pattern for human-facing error messages:

let x = jsonParse(input)
if x == null then
    println("bad input: " + (noteGet(x) or "<no details>"))
    null
else
    x
end

Working with arrays and objects

Most scripts transform JSON-like structures:

  • arrays: [ {...}, {...} ]
  • objects: { user: {...}, items: [...] }

Arrays: indexing, mutation, copying

Arrays are mutable. Indexing supports negatives (xs[-1] is the last element). If you need to avoid shared mutation, deep-copy with clone.

let xs = [1, 2, 3]
push(xs, 4)      # [1, 2, 3, 4]
pop(xs)          # 4
xs[-1]           # 3

let ys = clone(xs)
ys[0] = 999
xs[0]            # still 1

len(xs) gives the element count.

Objects: property access, computed keys, safe edits

Objects are ordered maps. Use dot access when the key is a normal identifier, and computed access when it isn’t (or when the key is a runtime value).

let user = {name: "Ada", "x-coordinate": 10}

user.name                 # "Ada"
user."x-coordinate"       # 10

let k = "name"
user.(k)                  # "Ada"

Accessing an unknown property is a hard failure. If missing keys are normal in your data, check first:

let stripDebug = fun(obj: {}) -> {} do
    if mapHas(obj, "debug") then
        mapDelete(obj, "debug")   # in-place
    end
    obj
end

Iteration: arrays, objects, and iterator functions

for loops can iterate over:

  • an array (yields elements),
  • an object (yields [key, value] pairs),
  • an iterator function of type Null -> Any? (returns next item or null to stop).

The prelude builds on this with iter, range, map, filter, reduce, and list, which lets you write “pipeline-style” data processing without inventing custom loop patterns every time.

Turning values into iterators

iter(v) returns an iterator function:

let it1 = iter([10, 20, 30])
it1()   # 10
it1()   # 20

let it2 = iter({a: 1, b: 2})
it2()   # ["a", 1]
it2()   # ["b", 2]

A small pipeline example

let clean = fun(s: Str) -> Str do strip(toLower(s)) end
let nonEmpty = fun(s: Str) -> Bool do s != "" end

let out = list(
    filter(nonEmpty,
        map(clean, iter(["  Ada  ", "", "  BOB "]))
    )
)

out   # ["ada", "bob"]

Counting with range

range(start, stop) is stop-exclusive; use null for an open-ended range.

for i in range(0, 3) do
    println(i)
end

Strings and regular expressions

Most boundary work is text cleanup: trimming, splitting, normalizing, and light pattern replacement.

Strings are treated as Unicode text for operations like len and substr (they use rune indices, not bytes).

let s = "  Hello   World \n"
strip(s)                    # "Hello   World"
replace("\\s+", " ", strip(s))   # "Hello World"

For delimited text:

let parts = split("a,b,c", ",")     # ["a", "b", "c"]
join(parts, "|")                   # "a|b|c"

Regex matching returns a list of matched substrings (non-overlapping matches). Replacement substitutes matches (no capture-group backrefs).


JSON: strict parsing, permissive parsing, and printing

JSON is the default interchange format, so the standard library gives you both:

  • strict parsing for clean inputs, and
  • a permissive “repair” path for real-world messy inputs.
let x = jsonParse(input)
if x == null then
    x = jsonRepair(input)
end
x

To print JSON, use jsonStringify(x). It returns null if x contains non-JSON kinds (functions, handles, types, modules).

A canonical stdin → stdout JSON transformer looks like this:

let s = readAll(STDIN)
if s == null then
    s
else
    let x = jsonRepair(s)
    if x == null then
        x
    else
        x.processedAt = nowMillis()
        jsonStringify(x)
    end
end

Schemas at boundaries

In MindScript, types are runtime values (Type) used mainly as schemas: validate incoming data before you index it, and validate outgoing data before you ship it.

let User = type { id!: Str, name!: Str, email: Str? }

let x = jsonRepair(input)
if x == null then
    x
elif not isType(x, User) then
    null  # <not a User>
else
    x.name
end

When you need to understand an unfamiliar value, start with typeOf(x).


Files, streams, and OS basics

Scripts often read stdin, write stdout, and touch the filesystem. MindScript uses handles (STDIN, STDOUT, STDERR, file handles, network handles) so the same I/O functions work across domains.

A line-based filter

readLine(h) returns a line (without the newline) or null at EOF.

while true do
    let line = readLine(STDIN)
    if line == null then
        break(null)
    end

    line = strip(line)
    if line != "" then
        write(STDOUT, line + "\n")
    end
end

flush(STDOUT)

write(...) is buffered; call flush(...) when you need output to appear immediately.

Whole-file convenience

For small scripts, whole-file helpers keep code short:

let cfg = readFile("config.json")
if cfg == null then
    cfg
else
    jsonRepair(cfg)
end

There are also basic OS utilities (stat/mkdir/rename/remove/cwd/chdir, env get/set, tempDir) for glue work.


HTTP and TCP networking

For typical API calls, use http(...), which returns a response object (status, headers, body). For large downloads, use httpStream(...), which gives you a readable handle bodyH.

GET JSON → transform

let r = http({url: "https://api.example.com/items"})
if r == null then
    r
else
    jsonRepair(r.body)
end

Streaming download skeleton

let r = httpStream({url: url})
if r == null then
    r
else
    let out = open("download.bin", "w")
    if out == null then
        out
    else
        while true do
            let chunk = readN(r.bodyH, 64_000)
            if chunk == null then break(null) end
            if len(chunk) == 0 then break(null) end
            write(out, chunk)
        end
        close(r.bodyH)
        close(out)
    end
end

TCP primitives (netConnect/netListen/netAccept) exist for cases where you need raw connections; they integrate with the same read/write functions.


Running external commands

Sometimes the right tool already exists. exec([cmd...], opts?) runs a process without relying on shell quoting and returns {status, stdout, stderr} (non-zero status is not an error by itself).

let r = exec(["git", "rev-parse", "HEAD"], {cwd: pathDir(runtime.path)})
if r == null then
    r
elif r.status != 0 then
    null  # <git failed>
else
    strip(r.stdout)
end

Time: timestamps, parsing, retry loops

Time utilities are for timestamps, RFC3339 formatting/parsing, and retries.

let attempt = 0
let r = null

while attempt < 5 and r == null do
    r = http({url: url, timeoutMs: 5000})
    attempt = attempt + 1
    if r == null then
        sleep(200 * (attempt * attempt))
    end
end

r

dateNow() returns local time fields. timeFormatRFC3339(millis) formats UTC.


Bytes in Str: encoding, URLs, crypto, compression

Several APIs treat Str as a byte container (HTTP bodies, gzip, crypto). Use encoding helpers to render bytes safely and move them through text channels.

let b = randBytes(32)
if b == null then
    b
else
    base64Encode(b)
end

URL helpers let you parse and modify queries structurally:

let u = urlParse("https://example.com/search?q=ada")
if u == null then
    u
else
    u.query.q = ["ada", "lovelace"]
    urlBuild(u)
end

Crypto primitives (sha256, hmacSha256, ctEqual) and gzip helpers exist for common integration needs (integrity checks, signatures, payload compression).


Modules: organizing real scripts

As soon as a script grows beyond a single file, use modules. Imports are extensionless:

let testing = import("testing")
let llm = import("llm")

For tooling and tests, importCode(name, src) loads a module from a string.


Advanced tools (not day one)

MindScript also ships with powerful capabilities that are best learned after you’re comfortable with data + boundaries:

  • concurrency (procs, channels, actors),
  • AST tooling (astParse, astValidate, reflect, reify),
  • FFI (ffiOpen) where supported.

They exist for advanced workflows, but most scripts never need them.