Skip to content

Latest commit

 

History

History
963 lines (809 loc) · 84.2 KB

File metadata and controls

963 lines (809 loc) · 84.2 KB

EigenScript Builtin Reference

Builtins are organized by module. eigenscript --api prints the live index. Core builtins are always available; extension builtins (HTTP, DB, model, gfx, audio) require a full build or the gfx target.

Builtins vs. library. This file is the compiled-in surface — functions the binary provides directly. The pure-EigenScript lib/ layer (DataFrames, stats, distributions, matrices, sets, sorting, the GUI toolkit, the STEM shelf, ...) lives in STDLIB.md — start at its "Finding Things" index if you know the task but not the function. In particular map, filter, and reduce are NOT builtins — they are lib/list.eigs (import list → list.filter of [xs, fn], or load_file of "lib/list.eigs" for bare names), even though sort_by IS a builtin (#734). Rule of thumb: file/process/network, tensor math, JSON encode-decode, regex, channels/tasks, and the interrogatives are builtins (here); everything you import is library (there). To resolve a name mechanically, eigenscript --api [--json] dumps the full surface — every builtin, extension (by group), and lib function with its parameter list — in one call.

Strict mode is the default (#1361). In rows that explicitly describe a strict-mode stand-in, wrong-typed or out-of-domain arguments raise unless the run sets EIGS_STRICT=0; those rows state the non-strict stand-in (0, "", [], -1, or another value) explicitly. Documented answers for valid-but-absent input (index_of miss -1, file_exists of a missing path 0) are the same in both modes (docs/SPEC.md, Strict mode).

New since 0.8.1: concurrency (spawn, thread_join, channel, send, recv, try_recv, recv_timeout, close_channel, channel_closed), (nearest_in_range), hashing (sha256, md5, store_close, store_put, store_get, store_delete, store_query, store_count, store_update, store_collections, store_drop — every one of which raises a catchable value error on a handle it cannot resolve: not a handle, already closed, or stale because its table slot was recycled. They do not answer null/0/[] for a bad handle, which they did before #1146; see docs/CONCURRENCY.md, "Thread handles"), audio (audio_open, audio_close, audio_pause, audio_play, audio_play_loop, audio_volume, audio_stop, audio_queue_size, audio_clear, audio_sine, audio_saw, audio_sweep, audio_square, audio_noise, audio_mix, audio_gain, audio_envelope, audio_capture_open, audio_capture_read, audio_capture_close, audio_stream_open, audio_stream_push, audio_stream_queued, audio_stream_clear, audio_stream_close), and

Runtime status and control

Function Usage Description
math_flags math_flags of null Return sticky overflow, invalid, and underflow numeric-status booleans.
must_not_yield must_not_yield of fn Run a callback while rejecting cooperative suspension.
inflate inflate of bytes Decode raw DEFLATE bytes when zlib support is enabled.

Core Language

Type System

Name Signature Description
print print of value Output value to stdout with newline
len len of value Length of string or list count
str str of value Convert to string representation
num num of value Convert to number (parse string or coerce). num of "inf" saturates to 1e308; num of "nan" raises a value error naming num by default, while under EIGS_STRICT=0 it is 0 and sets math_flags.invalid (#971).
type type of value Return type name: "num", "str", "list", "dict", "buffer", "text_builder", "fn", "builtin", "none" (the null value — SPEC.md is normative and its gated example prints none; the string "null" is never produced)
assert assert of [cond, msg] Raise catchable error "ASSERT FAIL: <msg>" if condition is false
exit exit of N Terminate the program with exit code N (default 0). Uncatchable — a try/catch does not intercept it — and unwinds through normal teardown, so it is leak-clean even with live closures. Code after it does not run. The request is scoped to the evaluating thread and cleared at each host eval entry, so under the embedding API a script that calls exit does not disable try/catch for the host's next eval (#739).
coalesce coalesce of [value, default] Return value unless empty/null, else default
eval eval of code_string Execute EigenScript code, return result
throw throw of message Raise catchable error

Numeric values are finite by construction. NaN collapses to 0, and overflow or infinity saturates at +/-1e308. This applies to numeric literals, num conversion, scalar arithmetic, tensor arithmetic, and the numeric fast paths used by reassignment and unobserved blocks.

Lists

Name Signature Description
append append of [list, item] Append item to list (mutates list)
concat concat of [a, b] Concatenate two lists into new list
range range of n or range of [start, end] Generate integer list [0..n) or [start..end)
set_at set_at of [list, index, value] Set element at index (mutates list); negative indices count from the end, like []. Also takes a buffer in the first position — set_at of [buf, i, v], or set_at of [shaped_buf, row, col, v] — where a non-number value is refused (cannot store str in a buffer)
get_at get_at of [list, index] Get element at index; negative indices count from the end, like []. Also takes a buffer: get_at of [buf, i], or get_at of [shaped_buf, row, col]
copy_into copy_into of [dest, offset, src] Copy src elements into dest starting at offset — a list into a list, or a buffer / list of numbers into a buffer. Returns dest. Wrong arity, a non-number offset (the doc used to list [dest, src, offset], the code has always read [dest, offset, src]), a bad type, or a non-number element bound for a buffer RAISES (#1069; it silently returned null)
list_slice list_slice of [list, start, end] New list with the elements of [start, end) — dual of copy_into. Negative indices count from the end, like []; bounds then clamp to [0, len]. start >= end gives []. Never raises on bounds
num_copy num_copy of value Create independent copy of numeric value
hex hex of n or hex of [n, nibbles] Uppercase hex string of a non-negative integer, zero-padded to nibbles (never truncated). Raises on negatives, fractions, non-numbers
sort sort of list Sort an all-number or all-string list in-place (numeric / lexicographic). Mixed or non-scalar elements raise — use sort_by for records. Returns the list. A non-list argument raises by default; under EIGS_STRICT=0 it is handed back unchanged (#971).
list_truncate list_truncate of [list, new_len] Shrink list in-place to new_len items. No-op if new_len >= length. Returns the list
list_remove_at list_remove_at of [list, index] Remove element at index, shift tail down (mutates). No-op if out of bounds. Returns the list
list_insert_at list_insert_at of [list, index, value] Insert value at index, shift tail up (mutates) — dual of list_remove_at. index == len appends; any other out-of-bounds index is a no-op. Returns the list
list_index_of list_index_of of [list, value] Index of the first element structurally equal to value (the same comparison == uses — nested lists and dicts match by structure), -1 if none
list_contains list_contains of [list, value] true if any element structurally equals value, else false — the list counterpart of the string-only contains
sort_by sort_by of [list, key_fn] Sort list by numeric keys from key_fn (qsort, O(n log n), stable). Returns a new sorted list
dispatch dispatch of [table, key, arg] Index list table by numeric key and call the resulting function with arg — a jump table (mirrors the OP_DISPATCH fast path). key must be a number. An ordinary builtin, not a special form: a user binding of the name wins (#459 — the fast path steps aside for any unit that rebinds dispatch, references eval, or compiles against an env where it is already rebound), and the parenthesized dispatch of ([t, k, a]) form is one argument per #355/#405

Strings

Name Signature Description
str_lower str_lower of s Convert to lowercase
str_upper str_upper of s Convert to uppercase
char_at char_at of [s, index] Single character at index as string ("" if out of range); negative indices count from the end, like []
contains contains of [haystack, needle] true if haystack contains needle, else false (non-string operands are false, never a spurious match)
starts_with starts_with of [s, prefix] true if s starts with prefix, else false
ends_with ends_with of [s, suffix] true if s ends with suffix, else false
index_of index_of of [haystack, needle] First index of needle in haystack, or -1 (non-string operands are -1)
substr substr of [s, start, length] Extract substring
split split of [s, delim] Split string by delimiter into list. A non-string s or delim raises by default; under EIGS_STRICT=0, s splits as "" (so answers [""]) and delim falls back to " " (#971).
scan_int_tokens scan_int_tokens of s or scan_int_tokens of [s, comment_marker] Token rows [text, line, col, start, end, is_int, value]
trim trim of s Strip leading/trailing whitespace
str_replace str_replace of [s, old, new] Replace all occurrences of old with new
chr chr of byte One-byte string from a byte value 1–255 (the writing inverse of ord). Raises outside 1–255 — including 0, since strings are NUL-terminated — and on fractions; for a Unicode codepoint use utf8_encode (lib/utf8.eigs).
join join of [list, sep] Concatenate list elements with separator (C-backed, O(n))
text_builder_new text_builder_new of null Create a native growable text builder
text_builder_append text_builder_append of [builder, value] Append one value as text
text_builder_append_line text_builder_append_line of [builder, value] Append one value and a newline
text_builder_to_string text_builder_to_string of builder Render buffered text
secure_equals secure_equals of [a, b] Constant-time string equality (true/false). Compares every byte regardless of where a mismatch occurs, so comparison time doesn't leak how much of a secret matched. Non-strings → false

Regex

POSIX ERE (extended regular expressions). No lookahead, named groups, or lazy quantifiers.

Name Signature Description
regex_match regex_match of [s, pattern] [full_match, group1, ...] or []
regex_find regex_find of [s, pattern] All matches as [match1, match2, ...]

On a successful match, group n is always at index n, for every group in the pattern (there is no cap on group count). A capture group that did not participate in the match — e.g. an unmatched optional (x)? — is emitted as null, not as "", so a non-participating group is distinguishable from one that matched the empty string. Example: regex_match of ["ab", "(x)?(a)(b)"] → ["ab", null, "a", "b"] (mirrors Python's (None, 'a', 'b')). A complete non-match still returns []. | regex_replace | regex_replace of [s, pattern, replacement] | Replace all matches |

Bitwise

Native operators &, |, ^, ~, <<, >> are preferred. The builtin-call forms below are retained for backward compatibility.

Name Signature Description
bit_and bit_and of [a, b] Bitwise AND (prefer a & b)
bit_or bit_or of [a, b] Bitwise OR (prefer a | b)
bit_xor bit_xor of [a, b] Bitwise XOR (prefer a ^ b)
bit_not bit_not of x Bitwise NOT (prefer ~x)
bit_shl bit_shl of [a, b] Left shift (prefer a << b)
bit_shr bit_shr of [a, b] Unsigned right shift (prefer a >> b)
sign_extend sign_extend of [val, bits] Sign-extend val from given bit width. E.g. sign_extend of [0xFF, 8] returns -1

Buffers

Compact typed arrays of doubles with O(1) indexed access. Iterable with for x in buf: and list comprehensions.

Name Signature Description
buffer buffer of count Create zero-filled buffer of given size (or buffer of [rows, cols] for a shaped one). A non-numeric size raises by default; under EIGS_STRICT=0 it makes an empty buffer (#971).
buf_get buf_get of [buf, index] Read element; out-of-range raises index_range (#502 — folding to 0 was indistinguishable from a real stored 0), matching the buf[i] operator
buf_set buf_set of [buf, index, value] Write element
buf_len buf_len of buf Return buffer element count
buf_from_list buf_from_list of list Convert numeric list to buffer
buf_copy buf_copy of [src, src_off, dst, dst_off, count] Bulk copy between buffers (memmove, overlap-safe). Out-of-range windows / negative count raise (index_range/value); count 0 is a no-op
buf_mix buf_mix of [dst, src, dst_off, src_off, count, gain] In-place mix: dst[dst_off+i] += src[src_off+i] * gain over the window — the audio mix-down kernel (#597). VM-identical arithmetic (byte-equal to the interpreted loop); same-buffer overlap runs forward in index order. Bad windows raise
buf_scale_range buf_scale_range of [b, off, count, gain] In-place multiply over a window: b[off+i] *= gain (fades, normalize). Bad windows raise
buf_fill buf_fill of [b, off, count, value] Bulk store over a window: b[off+i] = value. Bad windows raise
buf_peak buf_peak of [b, off, count] Max absolute value over a window (normalize/meter scans); 0 for an empty window. Bad windows raise
buf_dot buf_dot of [a, b, a_off, b_off, count] Windowed dot product: sum of a[a_off+i] * b[b_off+i] (YIN autocorrelation). Like dot, summation order/association is unspecified (backends may reassociate); no-NaN/Inf preserved. Bad windows raise
buf_from_pcm16le buf_from_pcm16le of [bytes, byte_off, count] Decode count little-endian signed 16-bit PCM samples starting at byte_off into a NEW float buffer: v = b0 + 256*b1, two's-complement fold at 32768, v / 32767 — bit-identical to the interpreted per-sample loop it replaces (#602, the WAV-import kernel). Bad windows raise
buf_to_pcm16le buf_to_pcm16le of [floats, off, count] Encode count samples into a NEW byte buffer (2 bytes/sample, LE): clamp to [-1, 1], round(x*32767), two's complement via +65536 — bit-identical to the interpreted write loop (#602). Bad windows raise
buf_deinterleave buf_deinterleave of [src, channel, nch, count?] Every nch-th sample starting at index channel into a NEW buffer (interleaved channel split). count defaults to the full available tail; bad channel/nch raise value, count overrun raises index_range (#602)
buf_resample_linear buf_resample_linear of [src, dst_len] Endpoint-inclusive linear resample into a NEW buffer: pos = i*(n-1)/(dst_len-1), lerp between the floor/ceil neighbors — bit-identical to DeslanStudio's interpreted ab_resample_linear (#603). The kernel is linear interpolation, not Fourier/sinc resampling (the consumer-documented divergence from scipy.signal.resample; no anti-aliasing). dst_len 0 → empty buffer; empty src with dst_len > 0 raises value
read_bytes_buf read_bytes_buf of path / read_bytes_buf of [path, max_bytes] Read binary file as buffer. 1-arg form caps at 10MB; a file over the cap raises a catchable io error naming size + cap (#601 — was a silent null indistinguishable from "file missing"). max_bytes is the bounded opt-in, hard ceiling 512MB (a buffer stores one double per byte — 8x expansion — so the ceiling bounds worst-case memory at the amplification point). Missing/unopenable file returns null
write_bytes write_bytes of [path, <list|buffer> {, append}] Write raw bytes to a file. Binary-clean (NUL written verbatim, unlike write_text). append (default 0): 0 truncates, nonzero appends. Returns bytes written, 0 on failure.
rename rename of [old, new] Rename/replace a file. Atomic on POSIX (rename(2)) — a crash leaves either the old or the new file whole, never a mix; basis for crash-safe swaps. Returns true/false.
remove_file remove_file of path Delete a file. Returns true/false.

Self-hosting

Name Signature Description
vm_run_bytecode vm_run_bytecode of <descriptor> Assemble a chunk from a descriptor and run it on the C VM, returning the result. Descriptor: [abi, code, constants, functions?, param_count?, name?, local_names?] — abi is the bytecode ABI revision the producer was built against (currently 1; see below); code is a list of byte ints (opcodes + little-endian operands, 16-bit except OP_LINE's, which is 32-bit since #630); constants is the pool; functions is a list of nested descriptors referenced by OP_CLOSURE (nested descriptors carry no abi element); local_names (slot order) sizes the call frame and names parameters. The minimal [abi, code, constants] form is a flat module chunk. The bridge for an EigenScript-written compiler: emit bytecode as data, execute it on the same VM (and JIT) the C compiler's output uses. Caller supplies a well-formed chunk ending in OP_RETURN. The chunk must also be stack-balanced. vm_run_bytecode rejects an invalid chunk by raising a catchable value error whose diagnostic names the malformed field or stack-verification failure; sandbox_run returns its structured rejection {ok: false, error: {kind: "value", message: "invalid chunk descriptor", line: 0}} instead. No instruction may be reached with fewer operands on the stack than it consumes, and the stack depth must be the same on every path into a given instruction — the JVM/Wasm rule, which keeps verification linear. So an if/else whose two arms leave different depths is rejected, as is a CALL that cannot see its own callee; the depth an instruction runs at is measured from the chunk's own frame base, and consuming below it would reach the caller's operands. This is the shape the C compiler already emits — a conditional's arms each push their value before the join — so a producer that mirrors compiler output needs no change. A descriptor whose abi is missing or does not match the runtime raises (kind value) rather than executing — #704: opcode numbers and operand widths are the bytecode ABI, and before the stamp a producer built against an older revision ran misaligned garbage at exit 0 with no error. Producers must hardcode the revision as a literal; a producer that reads the runtime's current value back would always agree and the check would be decoration. A non-list descriptor also raises (it silently returned null before #704).
record_history record_history of flag Enable (nonzero) / disable (0) per-assignment history recording that prev of x and <kw> is x at <line> temporal queries read (sets both value- and observer-state history). The C compiler auto-enables it when compiling a temporal query; a self-hosted compiler calls this. The flag must be a number — a non-numeric flag raises (it is not silently treated as disable). Returns the previous setting.

Bytes ↔ values

For serialization: reconstruct strings/floats from raw bytes (the inverse of an ord loop / manual bit-packing), covering cases 32-bit bitwise can't.

Name Signature Description
str_from_bytes str_from_bytes of <list|buffer> Build a string from raw byte values (0–255) — the list form of chr (chr of n == str_from_bytes of [n] for 1–255), inverting an ord-over-bytes loop. Nonnumeric list elements raise type_mismatch by default and convert to numeric zero under EIGS_STRICT=0. Strings are NUL-terminated: a numeric byte that converts to 0 ends conversion, and later elements are not inspected — keep NUL-bearing binary in a buffer.
f64_to_bytes f64_to_bytes of x List of 8 ints: the big-endian IEEE-754 encoding of double x (network byte order, portable across host endianness). A non-number raises by default; under EIGS_STRICT=0 it encodes as 0.0 (#971).
f64_from_bytes f64_from_bytes of <list|buffer> Decode a double from the first 8 big-endian IEEE-754 bytes. Inverse of f64_to_bytes. A NaN bit pattern raises a value error by default; under EIGS_STRICT=0 it collapses to 0 and sets math_flags.invalid (#971).

Buffers also support direct indexing (buf[i], buf[i] is val) and compound assignment (buf[i] += val).

Compression

DEFLATE codecs (#684), thin wrappers over the system zlib. Byte representation mirrors read_bytes/write_bytes: input is a list of ints 0–255 (values taken mod 256) or a buffer; output is always a fresh list of ints 0–255. Corrupt or truncated input raises a catchable value error; decompressed output is capped at 256 MiB (limit, zip-bomb bound).

Requires the zlib build (make zlib, -DEIGENSCRIPT_EXT_ZLIB=1 -lz) — the minimal build stays zero-dependency: the zlib names listed by eigenscript --api are still registered there (so type of inflate is builtin and the sandbox allowlist can name them) but every call raises value: "compiled without zlib support". Feature-detect with try/catch.

Name Signature Description

JSON

Name Signature Description
json_encode json_encode of value Serialize value to JSON string. Raises on a value nested deeper than 200 levels — which includes any cyclic value (dict_set of [d, "self", d], append of [a, a]), since a cycle has no depth. Catchable.
json_decode json_decode of s Parse JSON string to value. Raises past the same 200-level limit, so a document that decodes always re-encodes. \uXXXX surrogate pairs are combined into one code point; unpaired surrogates, an escaped NUL, and malformed \u escapes raise (strict decode — lenient callers such as json_path receive the complete document with U+FFFD in place of the bad scalar).
json_build json_build of [k1, v1, k2, v2, ...] Build JSON object from key-value pairs. json_build of null is {}; any other non-list raises by default and answers {} under EIGS_STRICT=0 (#971).
json_path json_path of [json_str, "dot.path"] Extract nested value by dot-notation path; "" when there is no value at that path (absent key, index out of range, JSON null). The document is parsed leniently: a malformed document is walked as far as it parsed, so a parse failure also answers "" or a partial value. By default, a document that json_decode would reject (structural error, a repaired \u scalar, trailing garbage) raises a catchable value error json_path: invalid JSON at position N instead (#971 Phase C); JSON false/null/absent keys stay answers in both modes.

Dictionaries

Name Signature Description
keys keys of dict List of keys
values values of dict List of values
has_key has_key of [dict, "key"] true or false
dict_set dict_set of [dict, "key", value] Set key in dict (mutates), return dict
dict_remove dict_remove of [dict, "key"] Remove key from dict (mutates), return dict

Interrogatives

Six keywords for querying a value's observer state. Asking is cheap — the state is already there — but note that maintaining it is not free: every assignment outside unobserved: is sampled whether or not you ever ask, and inside one a scalar still pays the O(1) value-window sample (#1049). See OBSERVER.md.

Name Syntax Returns
what what is x Current value (scalar), or length (list/string)
who who is x Variable name as string
when when is x Observation age (number of assignments)
where where is x Entropy (information content)
why why is x dH (rate of change)
how how is x Currently degenerate — returns 0 (1 only at zero entropy); see OBSERVER.md, #412

Temporal

Query a binding's assignment history. Always on for top-level bindings; null on a miss. See SYNTAX.md and TRACE.md.

Name Syntax Returns
prev prev of x Value of x just before its most recent assignment
at what is x at L State at or before line L — works with all six interrogatives and prev
state_at state_at of line Dict of every tracked binding's value at or before line

Observer

Name Signature Description
observe observe of value Return [status, entropy, dH, prev_dH] snapshot
get_observer_thresholds get_observer_thresholds of null [dh_zero, dh_small, h_low]
classify classify of t or classify of [t, "entropy"] Classify a trajectory snapshot (from trajectory of x, #421): value-channel label by default, entropy-channel with "entropy". Raises type_mismatch on a non-snapshot — a bare value never silently classifies

report and report_value are reserved (#1102). They cannot be bound or used as first-class values. Non-identifier operands, including report of 5 and report_value of (x + 0.0), are compile-time E005 errors; assign the expression to a variable first. Dict keys such as d.report remain legal. See OBSERVER.md for their trajectory classifications.

The VM retains the old report registry entry for bytecode compatibility: vm_run_bytecode can still resolve the string "report" with GET_NAME and CALL it, returning "equilibrium" for data or "opaque" for a callable. This entry is absent from the source builtin table and LSP Function completions; it does not make report a callable name in source. report_value has no runtime builtin registration.

report, report_value, observe, and trajectory on a plain variable are observer special forms (decided in #459): like the predicates and interrogatives, report of x / report_value of x / observe of x / trajectory of x are resolved by the compiler to the named binding's slot trajectory — an operation on the name, not the value. The report words are reserved; a user rebinding of observe or trajectory does not change their name-keyed forms (--lint W013 warns on those shadowing attempts). trajectory of x (#421) snapshots the slot's observer windows into a plain dict (kind/rel/raw/dh/entropy/…) that survives a call boundary, for classify to read on the other side — the binding slot itself is binding-identity and a passed value arrives with no history. The non-ident forms of observe / trajectory (observe of expr) are ordinary calls to the value-path builtins. dispatch is deliberately NOT in this set — it is a plain builtin and a user rebinding wins (see Lists above).

Predicates

Boolean keywords that check the most recently observed value:

Name True when
converged Entropy very low and stable
stable Entropy changing slowly
improving Entropy decreasing
oscillating dH sign-flipping
diverging Entropy increasing
equilibrium dH near zero

File I/O

Name Signature Description
load_file load_file of "path.eigs" Execute a file in the current scope, yielding its top-level return value. Uses the same file-based resolution chain as import (below). A missing/unreadable path raises a catchable io error naming the roots tried; a parse/compile failure raises parse.
file_exists file_exists of "path" true if the path exists (any kind: file, directory, device, fifo), false otherwise. A stat probe — never blocks (#1070: the old fopen probe hung on a reader-less fifo). Trace-recorded, so replay is deterministic (#585)
is_dir is_dir of "path" true if the path names a directory, false for a plain file / missing path (#576 — replaces the file_exists of "path/." probe). Trace-recorded, so replay is deterministic
is_file is_file of "path" true iff the path names a REGULAR file (S_ISREG); false for a directory, a device/fifo/socket, a missing path, or a non-string. read_file_util admits only regular files, so this is the probe a driver uses to match that contract (#1058). Trace-recorded, so replay is deterministic
read_text read_text of "path" Read file contents as string ("" on failure, 10 MB cap)
read_line read_line of null Blocking line read from stdin: next line without its trailing newline (\r\n stripped as one unit), null at EOF; an empty line is "". Works on pipes — the stream-safe primitive read_text of "/dev/stdin" can't be (fseek fails on unseekable fds, #558). Trace-recorded, so replay is deterministic
read_bytes read_bytes of "path" Read a file's raw bytes as a list of integers 0–255 (null on failure, 10 MB cap). Trace-recorded, so replay is deterministic
write_text write_text of ["path", text] Write string to file (true on success, false on failure)
exec_capture exec_capture of ["cmd", "arg1", ...] Run subprocess, return [exit_code, stdout_text]. No shell (direct exec). Child stdin is /dev/null. Returns [-1, ""] on failure, [-2, partial] on timeout. 10 MB output cap. Timeout form: exec_capture of [["cmd", ...], seconds]
env_get env_get of "VAR_NAME" Get environment variable (empty string if unset)
random_hex random_hex of n Generate n random hex characters from /dev/urandom ("" for n <= 0 or n > 256). A non-number n raises by default; under EIGS_STRICT=0 it answers "" (#971).
try_parse try_parse of code_string true if string is valid EigenScript syntax, false otherwise
mkdir mkdir of "path" Create directory (and parents). true on success, false on failure. Trace-recorded: replay serves the recorded bool and does not re-create the directory (#585)
ls ls of "path" List non-hidden directory entries as bytewise-sorted strings (the order of LC_ALL=C ls -1). Trace-recorded, so replay is deterministic (#585)
getcwd getcwd of null Current working directory as string. Trace-recorded, so replay is deterministic (#585)
exe_path exe_path of null Absolute path of the running interpreter binary. Lets a script re-invoke the same interpreter (e.g. exec_capture of [exe_path of null, file]) without assuming eigenscript is on PATH. Trace-recorded, so replay is deterministic (#585)
mktemp mktemp of null Create a temporary file and return its path. Under EIGS_REPLAY, raises a catchable filesystem-boundary error before creating a file
rm rm of "path" Remove a file. true on success, false on failure
write write of value Write to stdout without newline
flush flush of null Flush stdout

Streaming Tensor I/O

Single-handle streaming writer for the tensor binary format. Use when producing tensors without materialising all values in memory; the file still has the shared 10,000,000-element limit.

Name Signature Description
stream_open stream_open of ["path", count] Open file, write header for an integral count from 1 through 10,000,000 float64 values. Counts outside that range or fractional counts raise a catchable limit error naming the path and cap, including under EIGS_STRICT=0. Returns true on success, false on an I/O failure. One stream per thread: opening a second closes the first, and an unclosed stream is flushed and closed when the thread ends (#739)
stream_write stream_write of value Append one float64 to the open stream. true on success, false on failure
stream_close stream_close of null Close the stream. true on success, false on failure

load_file and import resolve an absolute path as-is; otherwise they try the containing file's directory, the eigs_modules walk (stopping at eigs.json), the nearest eigs.json project root, then <exe>/../<path>, <exe>/../lib/eigenscript/<path> and its leading-lib/-stripped form, then $HOME/.local/lib/eigenscript/<path> and its leading-lib/-stripped form. <exe> is the executable's directory. There is no process cwd lookup or one-parent fallback. The REPL (including piped input) and the embed API without a file path use their working directory as the containing directory. Loaded files and their functions retain their own directory. Consumers using root-relative paths from subdirectory files need an eigs.json at their root. Errors name the containing directory, project root (or no eigs.json above <dir>), and stdlib roots. See Modules for import collision handling.

Path Manipulation

Name Signature Description
path_join path_join of [a, b] Join two path segments with /
path_dir path_dir of path Directory portion ("a/b/c" → "a/b")
path_base path_base of path Filename portion ("a/b/c.txt" → "c.txt")
path_ext path_ext of path Extension including dot (".eigs"), or ""

Random

Name Signature Description
random random of null Random float in [0, 1)
random_int random_int of [lo, hi] Random integer in [lo, hi] inclusive; raises on non-finite or out-of-int64 bounds and on a span over 2^31. A malformed argument (not [lo, hi], or non-numeric bounds) raises by default; under EIGS_STRICT=0 it answers 0 (#971).
seed_random seed_random of n Seed the RNG for deterministic sequences

Time

Name Signature Description
monotonic_ns monotonic_ns of null Nanoseconds from CLOCK_MONOTONIC (jump-free)
monotonic_ms monotonic_ms of null Milliseconds from CLOCK_MONOTONIC
clock_unix clock_unix of null Seconds since the Unix epoch (float, wall clock; tape-captured for replay)
usleep usleep of microseconds Pause execution

Trace & Replay

Nondeterministic builtins (random*, monotonic_*, clock_unix, env_get, read_*, HTTP request/response accessors) are recorded to a tape when EIGS_TRACE=<path> is set, and served back from a recorded tape when EIGS_REPLAY=<path> is set — subsequent runs produce byte-identical output. Full tape format and replay semantics: TRACE.md.

Terminal

Raw-mode keyboard input and ANSI cursor rendering. Terminal is restored automatically at exit.

Name Signature Description

Command-Line Arguments

Name Signature Description
args args of null List of arguments after the script name

Scalar Math

Name Signature Description
abs abs of x Absolute value
min min of [n1, n2, ...] Smallest of a list of numbers (any length >= 1)
max max of [n1, n2, ...] Largest of a list of numbers (any length >= 1)
floor floor of x Round down to integer
ceil ceil of x Round up to integer
round round of x Round to nearest integer
sin sin of x Sine (radians)
cos cos of x Cosine (radians)
tan tan of x Tangent (radians)
asin asin of x Inverse sine; input is clamped to [-1, 1]
acos acos of x Inverse cosine; input is clamped to [-1, 1]
atan atan of x Inverse tangent
atan2 atan2 of [y, x] Two-argument inverse tangent
pi pi of null The constant π (3.14159265...)

Tensor Math

Numeric work wants a buffer. A tensor argument (t, a, b, matrix) is any of: a number, a flat list of numbers, a nested list of lists (the 2-D tensor), or a buffer — flat double[], one 8-byte element instead of a boxed Value per number, and the container the JIT and the AOT compile against. Every builtin in this section that accepts a flat numeric list accepts a buffer in the same position; a 1-D buffer reads as a 1-D tensor and a shaped buffer (buffer of [r, c], reshape of [buf, r, c]) as its r x c 2-D tensor. The numbers are identical either way.

Container of the result: a builtin that returns a tensor returns a buffer when every tensor operand was a buffer, and a list otherwise (so mixing a buffer with a list yields a list). Reductions (sum, mean, norm) return a number from either. shape always returns a list.

zeros of n returns a buffer (#1093, breaking — it used to return a list); zeros of [rows, cols] still returns the nested list. Reach for zeros of n / buffer of n for numeric vectors and keep lists for heterogeneous or nested data.

Arithmetic

Name Signature Description
add add of [a, b] Element-wise addition
subtract subtract of [a, b] Element-wise subtraction
multiply multiply of [a, b] Element-wise multiplication
divide divide of [a, b] Element-wise division; a zero denominator raises divide: division by zero by default (as the / operator does), while under EIGS_STRICT=0 it returns 0; overflow saturates (#971).
pow pow of [base, exp] Element-wise exponentiation; overflow saturates. A negative base with a fractional exponent raises a value error naming pow by default; under EIGS_STRICT=0 its NaN collapses to 0 and sets math_flags.invalid (#971).
negative negative of t Element-wise negation

The arithmetic builtins listed above take shaped buffers wherever they take a flat numeric list (#1093/#973), through one implementation — the same tensor_elementwise shape algebra, container for container, so every rule below reads the same for buffers and lists: two operands of equal count are combined elementwise (keeping the first's shape); a [rows × cols] operand with a [cols] one broadcasts the vector over every row and with a [rows] one applies it per row, in either operand order (the bias shape add of [x @ W, b]); a number broadcasts over every element; operands of unequal, non-broadcastable length truncate to the shorter as they always have (the one place the two containers still differ: buffers truncate flat, so add of [buf[5×4], buf[3]] is [3] where the same shapes as lists are [3, 4] — neither is a meaningful answer, both are pinned in tests/test_autograd.eigs); a non-numeric partner (a string, a dict) raises by default and answers 0 under EIGS_STRICT=0. Before #1093 only equal-count add had a buffer path and every other buffer case answered a silent 0. Same num_guard kernels either way, so the numbers are byte-identical.

Functions

Name Signature Description
sqrt sqrt of t Element-wise square root; negative input returns 0
exp exp of t Element-wise e^x; overflow saturates
log log of t Element-wise natural log. Positive input, however small, is exact (log of 1e-15 = -34.538…); a non-positive or NaN element sets the invalid math flag and stands in for ln(1e-10) = -23.025… (#865, #1041) — never -inf
softmax softmax of t Row-wise softmax normalization (a scalar is the one-element case → 1.0). A shaped buffer computes row-wise on its shape and returns a buffer of the same shape (a 1-D buffer is one row) — #973
log_softmax log_softmax of t Row-wise log(softmax) (a scalar → log(1) = 0.0); buffers as for softmax. The [tensor, dim] form is recognised only as exactly [list, number] — it used to fire on every 2-D list and answer for row 0 alone (#973)
relu relu of t Element-wise max(0, x) (accepts a scalar; buffers keep their shape)
leaky_relu leaky_relu of t Element-wise max(0.01x, x) (accepts a scalar; buffers keep their shape — #973)

Linear Algebra

Name Signature Description
matmul matmul of [a, b] Matrix multiplication. Two shaped buffers multiply on the flat data and give a buffer; a 1-D left operand gives a 1-D result. Mixed list/buffer operands give a list. An accumulation that reaches inf - inf is NaN; by default that raises a catchable value error naming matmul (#971). Kernels may retain raw non-finite values in buffer storage, but every scalar read applies the finite-number guard (#1417): infinity saturates at ±1e308, and with strict mode off a NaN becomes 0 and sets math_flags.invalid, matching the list result.
matmul_at matmul_at of [a, b] aᵀ·b without materialising the transpose: a is (m × k), b is (m × n), result (k × n) — the weight gradient dW = Xᵀ·dY of a linear layer. Buffers and nested lists; byte-identical to matmul of the explicitly transposed operand (same tiled kernel order). Two 1-D operands give their (k × n) outer product. Shape/type/size errors raise like matmul (#973)
matmul_bt matmul_bt of [a, b] a·bᵀ: a is (m × k), b is (n × k), result (m × n) — the input gradient dX = dY·Wᵀ. A 1-D left operand is a row vector and yields a 1-D result, as for matmul (#973)
gather gather of [matrix, indices, dim] Gather one element per row: out[i] = matrix[i][indices[i]]. matrix may be a shaped buffer and indices a list or a buffer; a shaped-buffer matrix gives a buffer. gather of [vec, i] on a 1-D tensor returns element i. An out-of-range index raises index_range — in every form, list or buffer (#973/#1093, settled at integration: there is no element there, and scatter_add raises on the same index). A row that is not a row (a 1-D tensor in the per-row form) still answers 0.0 for that row
scatter_add scatter_add of [dst, indices, values] The gradient of gather, accumulated in place into the buffer dst (returned). A shaped [rows × cols] dst does dst[i][indices[i]] += values[i] per row; a 1-D dst does the flat dst[indices[j]] += values[j]. values is a buffer, a list of numbers, or one number broadcast to every index; repeats accumulate. Lengths must line up exactly — a non-scalar values shorter or longer than indices, or a per-row dst whose row count differs from the index count, raises value rather than truncating (a dropped gradient entry is a silent wrong number). Every index is validated before anything is written, so a raise (index_range, value, or type_mismatch for a non-buffer dst / non-numeric index or value) leaves dst untouched (#973)

Reductions

Name Signature Description
mean mean of t Average of all elements (list, nested list or buffer; an empty buffer or list is 0.0)
sum sum of t Sum of all elements (list, nested list or buffer)

Construction

Name Signature Description
zeros zeros of n or zeros of [rows, cols] zeros of n returns a buffer of n zeros (type of is buffer, print of shows <buffer:n>); zeros of [rows, cols] returns the nested-list 2-D tensor. Breaking change in #1093 — zeros of n used to return a list; write [0 for i in range of n] if you need that. Both spellings cap at 10,000,000 elements and charge the sandbox budget
zeros_like zeros_like of t Zero tensor matching t's shape and container: a buffer gives a buffer (shape preserved), a list gives a list, a number gives 0.0
random_normal random_normal of [rows, cols, scale] Gaussian random tensor
shape shape of t Return dimensions as list
reshape reshape of [buffer, rows, cols] New numeric buffer with the same data reinterpreted as rows×cols (requires rows*cols == count; null otherwise)

Persistence

Name Signature Description
tensor_save tensor_save of [tensor, "path"] Save a list or buffer tensor of at most 10,000,000 elements to a binary file (preserves observer state). An over-cap tensor raises a catchable limit error before opening the file, including under EIGS_STRICT=0
tensor_load tensor_load of "path" Load a tensor of at most 10,000,000 elements from a binary file (restores observer state). An over-cap header raises a catchable limit error naming the path, offending dimension, and cap, including under EIGS_STRICT=0. NaN bytes in the file raise a value error naming tensor_load by default; under EIGS_STRICT=0 they collapse to 0 and set math_flags.invalid (#971).

Gradients & SGD

Name Signature Description
numerical_grad numerical_grad of [loss_fn, params, eps] Central finite-difference gradient. params is a 1-D/2-D list or a shaped buffer (#973: each element is perturbed in place and restored; the gradient comes back with the parameter's shape). O(params) forward passes — the gradient-check oracle for lib/autograd.eigs, not a training path
numerical_grad_rows numerical_grad_rows of [loss_fn, params, rows, eps] Gradient for specific rows
numerical_grad_cols numerical_grad_cols of [loss_fn, params, cols, eps] Gradient for specific columns
sgd_update sgd_update of [params, grad, lr] In-place SGD: params -= lr * grad
sgd_update_rows sgd_update_rows of [params, grad, lr, rows] SGD for specific rows
sgd_update_cols sgd_update_cols of [params, grad, lr, cols] SGD for specific columns

Memory

Name Signature Description
arena_mark arena_mark of null Snapshot arena allocation point
arena_reset arena_reset of null Reclaim all allocations since mark
arena_stats arena_stats of null Return total bytes allocated

Tokenizer Introspection

Name Signature Description
token_name token_name of id Return token type name by ID ("?" for an unknown id). A non-number raises by default; under EIGS_STRICT=0 it answers "?" too (#971).

Corpus Preparation

Name Signature Description
build_corpus build_corpus of [files, top_n, stream_path, vocab_path] Three-pass C-backed corpus builder: tokenise files, emit top-n vocabulary and stream-format token IDs. The token stream, including file separators, is capped at 10,000,000 elements; exceeding it raises a catchable limit error naming the stream path and cap, including under EIGS_STRICT=0, before opening the stream file

Optional: Network Extension (TCP sockets)

Requires a make server build (EIGENSCRIPT_EXT_NET=1; not in the default release). Raw TCP sockets whose every nondeterministic outcome — accepted connections, received bytes, bytes-sent counts, dial results, kernel-assigned ports — rides the trace tape: a session recorded under EIGS_TRACE replays byte-identically under EIGS_REPLAY with no network present (the replay run performs zero socket syscalls). See TRACE.md. make net remains a compatibility alias for make server.

Builtin Form Returns
net_listen net_listen of port listener handle, or null (bind failed). Port 0 = kernel-assigned ephemeral port
net_port net_port of listener the locally bound port (the kernel's pick for port 0), or null
net_accept net_accept of listener / net_accept of [listener, timeout_ms] connection handle, or null on timeout
net_dial net_dial of [host, port] / net_dial of [host, port, timeout_ms] connection handle, or null (refused / unresolvable / timeout)
net_recv net_recv of [conn, max_bytes] / net_recv of [conn, max_bytes, timeout_ms] buffer of byte values (empty buffer = connection over), or null on timeout. max_bytes is clamped to 8192 per call — loop to drain; decode text with str_from_bytes
net_send net_send of [conn, data] — data is a string, buffer, or byte list bytes sent, or -1 (peer gone / bad handle)
net_close net_close of handle null; idempotent

Environment failures are values (null / -1 / empty buffer), never raises, so every outcome lands on the tape and a catch cannot desync replay; argument-shape mistakes (wrong type or arity) raise deterministically. A single-threaded program can be both ends of a connection: on loopback, net_dial completes against the listen backlog before net_accept runs (see examples/net_echo.eigs). Sockets left open at exit are closed by the runtime's handle-table drain. UDP is not yet exposed (#414 tracks it).

Optional: HTTP Extension

Requires full build. Provides an embedded HTTP server.

Request limits (DoS bounds). Each request body is capped by EIGS_HTTP_MAX_BODY (default 16 MiB; an over-cap Content-Length gets 400, oversized headers 431). Because that per-request cap times the concurrent- connection cap is still a large aggregate, the server also bounds the total request-body bytes in flight across all connections with EIGS_HTTP_MAX_BODY_TOTAL (default 128 MiB) — once exceeded, further connections are shed with 503 rather than letting concurrency × per-request size exhaust host memory.

Slow-loris bounds. The server is thread-per-connection with a global cap of 256 workers, so slow clients that each hold a worker are a denial-of-service axis. Three controls bound it:

  • EIGS_HTTP_MAX_CONN_PER_IP (default 48) — the maximum concurrent connections from a single source IP; further connections from that address get 503, so one source can't hold all 256 slots. Set to 0 when deploying behind a reverse proxy — every connection then carries the proxy's address, and a nonzero cap would throttle the proxy to N; do the per-IP limiting at the proxy instead (the recommended posture for a directly-exposed server).
  • EIGS_HTTP_HEADER_TIMEOUT (default 10s) — a request's headers must arrive within this window (separate from, and shorter than, the 30s total-request deadline that must accommodate a large body); otherwise 408.
  • EIGS_HTTP_HEADER_MIN_RATE (default 256 bytes/sec, 0 disables) — after a short grace period the header phase must sustain at least this byte rate, so a byte-per-second trickle is dropped (408) in a couple of seconds instead of holding a worker until the deadline.
Name Signature Description
http_route http_route of [method, path, body] or [method, path, "code", source] Register a route. body is a literal response body, not a callback — passing a function raises (#877); use the code form for per-request logic. Register before http_serve: once serving it raises, including from a code route (#1140)
http_route_authed http_route_authed of [method, path, body] or [method, path, "code", source] Register authenticated route; auth source published via shared_set of ["require_auth", "<source>"]. Register before http_serve: once serving it raises, including from a code route (#1140)
http_static http_static of [prefix, directory] Serve static files (realpath-confined to directory). Register before http_serve: once serving it raises, including from a code route (#1140)
http_early_bind http_early_bind of port, http_early_bind of null, or http_early_bind of [port, "/livez"] Bind/listen before initialization. null defaults to 5000; a positive PORT environment override still wins. During init every request receives 503 Service Unavailable with Retry-After: 1; never a fake 200 for arbitrary paths. Only an explicitly supplied liveness path answers GET/HEAD with 200, text/plain, body OK (HEAD has no body), by exact request-target match, including any query string. The path must start with / and contain no whitespace, CR, LF or NUL; invalid paths raise. The two scalar spellings configure no liveness path. Init-window capacity is the same number as the serving connection cap (HTTP_MAX_CONCURRENT_CONNS, 256): an idle or slow client cannot delay any other in-flight answer, a newcomer past that cap is accepted and answered a bodyless 503 (Content-Length 0, identical on the wire for every method including HEAD) immediately, and a client that never sends a complete request line is answered 503 at a 1 s per-client deadline. Init-window replies (liveness 200, init 503, capacity shed, handoff/teardown drain) are best-effort to a peer that is not reading: the responder never waits on a client. Once http_serve takes over, normal routing applies even to that path (404 unless registered)
http_response_header http_response_header of [name, value] Register before http_serve, including before or after http_early_bind; returns "response header registered". Both arguments must be strings. Name: RFC 7230 token, 1–64 bytes. Value: 0–1024 bytes, visible ASCII, space or tab only; no CR/LF/NUL. Runtime-owned and runtime-emitted names (Content-Length, Content-Type, Transfer-Encoding, Connection, Cache-Control, Retry-After, Allow, Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers) are refused case-insensitively. An existing name is replaced case-insensitively, including at capacity; at most 16 distinct headers. Every violation raises a loud error naming the builtin and rule; a rejected registration prevents http_serve from starting even if caught. Registered headers accompany every response: routes (string/file/code/authed), static files, HEAD, OPTIONS, errors, load shedding, init 503s and optional liveness 200s. Content-Type, Content-Length, status and body bytes are unchanged. Registration is deterministic configuration and adds no tape records
http_serve http_serve of port Start blocking HTTP server
http_request_body http_request_body of null Get current request body
http_session_id http_session_id of null Get current session ID
http_post http_post of [url, headers, body] HTTP POST via curl (no shell). headers is a JSON string, either an object ("{\"X-Key\": \"v\"}") or a flat alternating array ("[\"X-Key\", \"v\"]"); "" means no headers. Any other parsed shape raises type_mismatch rather than silently sending none (#755). Max 32 headers; CR/LF is stripped from both halves
http_request_headers http_request_headers of null Get current request headers

Per-worker code routes

A route declared as [method, path, "code", source] evaluates source in a fresh worker EigsState on every request — stdlib + the request-scoped HTTP builtins (http_request_body, http_session_id, http_request_headers, http_post, and the shared_* family below) are available; startup-scope globals are not. The final expression's value is sent as the response body; an uncaught error answers 500 with the generic body {"error": "internal error"} instead of a 200 (#1140). The message itself goes only to the server's stderr, since it can carry paths or values a client must not see. An exit inside the route is not an error and does not produce a 500. Per-worker isolation means concurrent requests don't race on script state and mutations don't leak across requests; cross-worker state goes through the shared store.

Shared store: cross-worker key/value primitive

JSON-serialized map living on the EigsHttpServer, mutex-guarded. Values cross worker boundaries by being encoded on write and re-parsed on read into a value owned by the caller's state. Function values can't be stored (encoded as null per json_encode). A cyclic or over-deep value can't be stored either — shared_set rejects it and json_encode raises, rather than the crash that used to take the whole server down with it. Total bytes are bounded by EIGS_HTTP_SHARED_MAX_BYTES (default 64 MiB); over-cap writes return null without mutating.

Name Signature Description
shared_set shared_set of [key, value] Store value (JSON-encoded). Returns null if over byte cap.
shared_get shared_get of key Return stored value (re-parsed) or null if absent.
shared_has shared_has of key Return true if key present, else false.
shared_delete shared_delete of key Remove key; return 1 if removed, 0 if absent.
shared_keys shared_keys of null Return list of keys.
shared_size shared_size of null Return current key count.
shared_clear shared_clear of null Drop all entries.
shared_incr shared_incr of [key, delta] Atomic single-lock read-modify-write. Missing key treated as 0. Returns new value, or null if existing value is non-numeric.

Individual op atomicity is guaranteed by the mutex. For read-modify-write atomicity use shared_incr; shared_get+shared_set sequences can lose updates under concurrent writers.

Authenticated routes (http_route_authed)

The auth source resolves from shared_get of "require_auth" first. When that key holds a string, the worker tokenizes/parses/compiles/ executes it on every authed request in a fresh env layered on the worker global. Empty value_to_string result allows the request; any non-empty result becomes the 401 Unauthorized response body verbatim. Hosts publish a session table or token-validity flag via shared_set and write the auth check as a small script that consults it. Re-evaluation happens per request, so flipping the shared state takes effect immediately.

If the require_auth key is absent, the worker falls back to a require_auth function in the global env (legacy path; default worker envs don't populate it).

Graphics (SDL2) Extension

Compiled into the hosted release (make). It dynamically loads libSDL2 at first use, so no SDL2 headers or link-time SDL dependency are needed.

Name Signature Description
gfx_open gfx_open of [width, height, title] Open window and renderer. Returns true on success or false when libSDL2 is unavailable. A non-numeric size raises by default; under EIGS_STRICT=0 it returns false (#1007 — it used to be read without a type check, so a string opened a 0x0 window and still answered 1).
gfx_close gfx_close of null Destroy window and quit SDL
gfx_clear gfx_clear of [r, g, b] / gfx_clear of null Clear backbuffer to color; null clears to black
gfx_rect gfx_rect of [x, y, w, h, r, g, b] or [..., a] Filled rectangle
gfx_line gfx_line of [x1, y1, x2, y2, r, g, b] Line segment
gfx_point gfx_point of [x, y, r, g, b] Single pixel
gfx_circle gfx_circle of [cx, cy, radius, r, g, b] Filled circle (midpoint)
gfx_rrect gfx_rrect of [x, y, w, h, radius, r, g, b] or [..., a] Filled rounded rectangle (scanline corner fill); radius clamps to half the smaller dimension, radius 0 = plain rect
gfx_clip gfx_clip of [x, y, w, h] / gfx_clip of null Set / clear the render clip rectangle
gfx_read gfx_read of [x, y] Read back one rendered pixel as [r, g, b] — the render-decode oracle primitive (#823). Reads the current back buffer: call after drawing, before gfx_present. Null with no window or a failed read. Nondeterministic input (font raster, driver), so it records/replays on the trace tape
gfx_text gfx_text of [x, y, text, r, g, b] or [..., scale] Text. Proportional antialiased TTF when libSDL2_ttf + a font are available (#593); the 5x7 bitmap font otherwise — see the font note below the table
gfx_text_width gfx_text_width of [text, scale?] or of "text" Pixel width of text under the active text renderer: TTF metrics when active, len * 6 * scale in bitmap mode. Works before gfx_open
gfx_text_height gfx_text_height of scale? Pixel line height under the active text renderer: TTF font height when active, 7 * scale in bitmap mode
gfx_present gfx_present of null Flip backbuffer to screen
gfx_poll gfx_poll of null Return next event as dict (quit, keydown, keyup, mousemove, mousedown, mouseup, wheel, resize), or null. Key, mouse, and wheel events carry shift/ctrl/alt (bools); wheel x/y are scroll deltas
gfx_ticks gfx_ticks of null Milliseconds since SDL_Init
gfx_delay gfx_delay of ms Sleep for ms (SDL-coordinated)
gfx_title gfx_title of "text" Update window title
gfx_fb gfx_fb of [buf, w, h, x, y, scale] Blit buffer (palette indices 0-3) as scaled texture
ppu_render_frame ppu_render_frame of [mem_buf, fb_buf] Full Game Boy PPU render (BG/window/sprites) into framebuffer

Wrong-typed and wrong-arity arguments (#1007). Every builtin in this extension that takes an argument at all — the drawing calls, the text calls, the framebuffer blit, the PPU renderer, and the whole audio surface below — type-checks its arguments before reading them, and by default a wrong type, a short argument list, a wrong-shaped argument container (a number or a string where a list belonged) or an out-of-domain value raises a catchable type error naming the builtin and the shape it wanted. Under EIGS_STRICT=0, the answer is byte-identical to before: the drawing calls still answer null, the generators still answer an empty list, the device calls still answer 0 or the device id they already answered.

Builtins that take no argument (gfx_poll, gfx_present, gfx_ticks, gfx_close, audio_close, audio_clear, audio_capture_close, audio_capture_read, audio_stream_close, audio_stream_clear, audio_stream_queued, audio_queue_size, audio_music_stop) still ignore an argument. Fixed- or optional-shape builtin argument lists reject surplus outer elements in strict mode with a catchable type_mismatch error naming the builtin and maximum width. The check precedes the call's mutation, I/O and tape effects. EIGS_STRICT=0 keeps each builtin's legacy result, including existing null/error stand-ins. Scalar overloads, lists used as data and genuinely variadic arguments keep their documented meaning. tools/strict_shape_contract.py discovers input-list surfaces and helper callers independently of the guards, then checks reviewed fixed-shape and exempt contracts, entry guards and typed control enrollment. The ordinary shape controls used by tools/strict_differential.sh report unavailable capabilities and any pending resource controls separately from executed successes. Its existing graphics container and pixel checks remain separate. What changed under EIGS_STRICT=0 is the read, not the answer. Value's union overlaps double num with char *str, so gfx_rect of [0, 0, 32, 32, "255", 0, 0] used to reinterpret a char * as a double, (int)-cast it, and draw a black rectangle where red was asked for — silently, in both modes. That read is gone in both modes; an unchecked union pun is not behaviour anything can depend on.

So be precise about what "byte-identical" covers: the returned value is unchanged in every case, and so is anything the call reports. What a rejected call draws is not, and cannot be — a 48-bit pointer read as a double is a subnormal that truncates to 0, so the parent painted a shape at coordinate 0, in colour 0, or at scale 1, and this build paints nothing at all. The same applies to the one drawing-surface builtin that answers with data: with a window open, gfx_read of ["1", 1] used to hand back the pixel at (0, 1) — the punned 0 — and now answers null. tools/strict_differential.sh measures exactly that surface on a gfx build (it opens a window under the dummy driver and diffs a readback digest against a baseline build). Under EIGS_STRICT=0 every row must draw byte-identically to the baseline, and by default every wrong-typed row must raise from its own guard.

A wrong-typed OPTIONAL argument follows the same rule, which makes the three text builtins differ on purpose. gfx_text_width and gfx_text_height type-checked their scale slot before this change, so a wrong-typed scale there is a coercion: under EIGS_STRICT=0 they still measure at scale 1. gfx_text did not — its scale slot was one of the unchecked reads — so a wrong-typed scale refuses the call and draws nothing. By default, all three raise. Layout code that sizes a box with gfx_text_width and then draws with gfx_text therefore sees a box with no text in it if it passes a stringy scale, which is the loudest signal available under EIGS_STRICT=0; leave strict mode at its default to get the error.

A few values are deliberately left quiet because they are the answer, not a rejected argument: a drawing call with no window open answers null (that is its answer on every path), gfx_poll answers null for "no event", gfx_rrect/gfx_fb answer null for a zero or negative width/height/scale (degenerate geometry covers no pixels), and every device builtin answers 0 when libSDL2 or the device is unavailable — environment state, not a caller mistake. The distinction is recorded per site in src/ext_gfx.c. What is checked against the binary is tools/strict_differential.sh: a guard probe raises under EIGS_STRICT, and a pinned documented answer does not.

Text rendering and fonts (#593). gfx_text lazily loads libSDL2_ttf-2.0.so.0 on first use and renders proportional antialiased text (TTF_RenderUTF8_Blended) when both the library and a font file are present. Font selection: the EIGS_GFX_FONT environment variable (absolute path to a .ttf) wins; when it is set but unreadable the runtime warns once and stays on the bitmap font (a nonexistent path is the deterministic off-switch). Otherwise a short list of common system fonts is probed (DejaVu Sans, Liberation Sans, Noto Sans under /usr/share/fonts/truetype/). Without SDL2_ttf or a font, gfx_text renders through the built-in 5x7 bitmap font exactly as before — the fallback is load-bearing (CI containers may have neither). Layout code should measure through gfx_text_width/gfx_text_height (as lib/ui.eigs does) rather than assuming the 6 * scale monospace advance. Text rendering is output-only: no trace-tape records in either mode.

Optional: Database Extension

Requires full build with libpq. PostgreSQL client.

Name Signature Description
db_connect db_connect of null Connect via DATABASE_URL env var; returns a status JSON, never raises
db_query_value db_query_value of sql or db_query_value of [sql, p1, p2] Execute query, return row 0 col 0 typed by its SQL type; null for SQL NULL, "" for no rows
db_execute db_execute of sql or db_execute of [sql, p1, p2] Execute command with optional params; returns "ok"
db_query_json db_query_json of sql or db_query_json of [sql, p1, p2] Execute query, return all rows as a JSON array of objects, each value typed by its SQL type

Failures raise (#888)

db_connect is the only one that reports by return value — it hands back {"status": ...} so a program can probe for a database without a try. Every other db builtin raises a catchable io error when the statement fails or there is no connection, carrying libpq's own first line (ERROR: relation "orders" does not exist). A genuinely empty result is still [] / "", and only that.

They used to return [] / "" for a syntax error, a missing table, a revoked permission and an empty table alike, so a reporting script kept printing "0 rows" forever after a schema change and a migration that did nothing looked healthy in CI.

The release build (make build) omits db bindings, so resolving the db name below raises a catchable value error naming the database capability and the server-db profile before evaluating call arguments. The db build (make server-db) raises a catchable io error when there is no connection (db: not connected — call db_connect first). Both paths are a failure of the query, so the executed example prints only the prefix — e.message depends on the compiled profile, depending on which binary you run it on, and pinning either one here would document the other build's absence.

try:
    rows is json_decode of (db_query_json of "SELECT * FROM orders")
catch e:
    print of "query failed"
query failed

SQL types survive the trip (#887)

Values carry their column's SQL type rather than arriving as strings:

SQL type Arrives as Note
NULL (any column type) null Distinct from "" — checked before the type
boolean true / false → true / false (bool) if row.is_admin: means what it reads as
smallint, integer, bigint, oid number bigint past 2^53 raises — see below
real, double precision number NaN/Infinity arrive as strings; JSON has no literal for them
numeric string Deliberate — see below
everything else string text, date, uuid, json, … unchanged

The mapping is a function of the column's SQL type alone, never of the row's value: a column that decoded as a number for row 1 and a string for row 100 would break row.n + 1 on data rather than on schema.

numeric stays a string. It is PostgreSQL's arbitrary-precision decimal — the money type — and an EigenScript number is a binary double, which cannot hold numeric(38,10) or even 0.1 exactly. Preserving the digits is the safe default; SELECT amount::float8 is the one-token opt-in to a number when approximate is fine. Note avg() and sum(numeric) return numeric, so those want the cast; count(*) and sum(integer) return bigint and are already numbers.

A bigint past 2^53 raises instead of silently rounding, naming the column and the fix:

Error line 3: db: column 'id' value 9007199254740993 exceeds the exact-integer
range of a number (2^53); select it as text (id::text) to keep the digits

Before this, every value was a string: SQL false arrived as "f", which is a non-empty string and therefore truthy, so if row.is_admin: passed for a non-admin; NULL and '' were both ""; and 9 > 10 was true because '9' > '1'.

Optional: Model Extension

Requires full build. Transformer model inference and training.

Name Signature Description
eigen_model_load eigen_model_load of "path.json" Load model weights from JSON
eigen_model_loaded eigen_model_loaded of null true if model loaded, false otherwise
eigen_model_info eigen_model_info of null JSON with model config and stats
eigen_generate eigen_generate of [prompt, temp, max_tokens] Generate text from prompt. Raises when the prompt exceeds the model's max_seq_len.
eigen_eval_loss eigen_eval_loss of [prompt, target] Return the target token's cross-entropy loss. Raises when the prompt exceeds the model's max_seq_len.
native_train_step_builtin native_train_step_builtin of [input, output, lr] Single training step. Raises when the combined input and output length exceeds the model's max_seq_len.
model_save_weights model_save_weights of "path.json" Save model weights to JSON
model_load_weights model_load_weights of "path.json" Load model weights (alias)

Concurrency

Name Signature Description
spawn spawn of fn or spawn of [fn, arg1, ...] Spawn a thread running fn. Bare-fn form passes no args; list form passes arg1... positionally. Missing trailing params bind to null. Extra args follow the direct-call rule (docs/SPEC.md, Functions): a callee of 2+ parameters raises call passes 3 arguments but the callee takes 2 at the spawn site (spawn of [two, 1, 2, 99]), and a 1-parameter callee receives the whole list (spawn of [one, 5, 6] binds [5, 6]). Args are shared by reference (unlike channel sends, which copy) — see thread-safety note below. Returns a thread handle dict.
thread_join thread_join of handle Block until thread completes. Returns the thread function's return value.
channel channel of null Create a bounded FIFO channel (capacity 64). Returns a channel handle dict.
send send of [channel, value] Send a value to the channel. Blocks if full. Sending to a closed channel raises a catchable value error (rather than silently dropping the value); recv on a closed empty channel returns null (EOF-like).
recv recv of channel Receive a value from the channel. Blocks until a value is available or the channel is closed.
try_recv try_recv of channel Non-blocking receive. Returns the value if available, null if the channel is empty.
recv_timeout recv_timeout of [channel, ms] Bounded-wait receive. Returns the value if one arrives before ms milliseconds elapse, else null. A close while waiting also returns null. Fractional ms is honored (ns precision on Linux); negative ms degenerates to a try_recv.
close_channel close_channel of channel Close the channel. Wakes all blocked senders/receivers.
channel_closed channel_closed of channel Returns true if closed, false otherwise. An unknown or reclaimed channel is closed (true). A value that is not a channel handle raises by default; under EIGS_STRICT=0 it answers true (#971).
task_spawn task_spawn of fn or task_spawn of [fn, arg1, ...] Create a cooperative task (#408) running fn on the single OS thread — deterministic by construction, unlike spawn's OS thread. Args are deep-COPIED (share-nothing, like channel sends), not shared by reference. Returns a numeric task id. A ready task can run when the current task explicitly yields, suspends in a blocking scheduler operation, or finishes.
task_alive task_alive of id Returns true while the task is runnable or suspended, false once it has finished (or for an unknown id).
task_self task_self of null The running task's own id (a number, in the same integer space task_spawn returns; the main task is 0, including before any task has been spawned). Lets a worker hand out its own id as a reply address — the message-link pattern a mailbox otherwise cannot express (#526). Deterministic — reads scheduler state, records no nondeterminism.
task_yield task_yield of null Cooperatively hand control to the next ready task; this task resumes round-robin. A no-op when no task has been spawned. Forbidden inside an arena_mark…arena_reset scope or a nested evaluation (raises value).
task_join task_join of id Block until task id finishes, then return its deep-copied result — or re-raise its uncaught error (as the same {kind, message, line} it died with). Joining an already-finished task returns immediately; an unknown id (or self) returns null. All tasks blocked with none runnable is a deadlock error, not a hang — catchable by a try/catch around the join on the main task (e.kind == "deadlock"); terminal only if unhandled.
task_send task_send of [id, value] Append a deep-copied message to task id's unbounded FIFO mailbox, waking it if it waits in task_recv. Returns true if delivered, 0 if id is finished/unknown (a silent drop — send-to-dead is never an error). Never blocks.
task_recv task_recv of null Return the next message from this task's mailbox, or block cooperatively until one arrives. Forbidden inside an arena_mark…arena_reset scope or a nested evaluation (raises value).
task_kill task_kill of id Tear down task id: drop its mailbox, mark it dead, wake any joiner with an interrupt error. Returns true if killed, 0 for a finished/unknown/self target.
task_detach task_detach of id Mark task id fire-and-forget (the pthread-detach precedent, #530): it is reaped the moment it finishes — or immediately if already finished — releasing its handle slot for reuse, so task-per-message workloads are bounded by concurrent tasks, not lifetime spawns. A detached task's uncaught death still prints its trace and still fails the process at exit (#493). A reaped id reads as unknown afterwards (task_join null, task_alive 0). A task may detach itself: task_detach of (task_self of null). Returns 1, or 0 for main/unknown.
task_sleep task_sleep of ticks Suspend this task until the virtual clock advances by ticks. The clock is logical (discrete-event): it only jumps forward — to the earliest sleeper — when nothing else is runnable, so sleeping stays deterministic, not wall-clock. A negative sleep is treated as 0. A no-op when no task has been spawned. Forbidden inside an arena_mark…arena_reset scope.
task_sched_seed task_sched_seed of n Install a scheduling seed: the scheduler switches from FIFO round-robin to picking the next ready task from a seeded, platform-independent PRNG. Same seed ⇒ same interleaving (byte-identical run + replay, zero tape nondeterminism); a different seed explores a different ordering — the lever a deterministic simulation tester uses to search interleavings. No seed ⇒ unchanged FIFO. Typically called once at program start. Returns null.

Thread safety: Values sent through a channel (or returned through thread_join) are deep-COPIED via val_clone_for_send — messages are share-nothing. Numbers, strings, and nested lists/dicts arrive as independent copies, so mutating the original after a send cannot be observed by the other thread (see docs/CONCURRENCY.md). The exceptions are handle-like values — buffer, text_builder, fn, and builtin — which remain shared by reference across a send; do not mutate those concurrently from sender and receiver.

Spatial Queries

Name Signature Description
nearest_in_range nearest_in_range of [entities, x, y, range, world_w, world_h] Find the nearest active entity within range using torus (wrapping) distance. entities is a list of dicts with "px", "py", "active" keys. Returns {"index", "dist", "dx", "dy"} or null. Optional extra args: custom key names [..., px_key, py_key, active_key].
nearest_in_range_all nearest_in_range_all of [entities, range, world_w, world_h] Like nearest_in_range, but returns ALL active entities within range (torus distance) as a list, not just the closest. Optional trailing custom key names [..., px_key, py_key, active_key].

Audio (additional)

Name Signature Description
audio_open audio_open of [freq, channels] or of null Open the mixer playback device. Defaults [44100, 1]. Returns the device id (>= 2), or 0 when SDL/audio is unavailable. Non-numeric freq/channels raise by default and answer 0 under EIGS_STRICT=0 (#1007 — they used to be read without a type check, so a string opened the device against a garbage spec and still answered a real id, taking the device with it). A short or non-list argument also raises by default; with EIGS_STRICT=0, it opens at the defaults and returns the device id or 0 if unavailable (#1007). of null is still the defaults.
audio_sweep audio_sweep of [freq_start, freq_end, duration, amplitude, waveform] Generate a frequency sweep with continuous phase. waveform: 0=sine, 1=sawtooth. Returns sample list.
audio_play audio_play of samples Play a clip once on a free mixer channel (oldest finite channel recycled when all 16 are busy). Returns the channel id, or 0 on bad args / closed device. A non-numeric element in samples raises a type_mismatch error (#1007 — it used to be coerced to 0, so a wrong-typed list played silence on a real channel id), and so does a samples that is not a list or buffer at all (#1007 — audio_play of 42 answered the documented "nothing to play" 0, indistinguishable from an empty clip). of null still plays nothing.
audio_play_loop audio_play_loop of [samples, loops] Play samples loops times on one mixer channel; loops == -1 loops forever (the mixer rewinds — no memory multiplication). Returns the channel id, or 0 on bad args / closed device. loops must be a number equal to -1 or in 1..10000; anything else raises by default and answers 0 under EIGS_STRICT=0 (#1007), and so does a samples slot that is not a list or buffer.
audio_volume audio_volume of [channel, vol] Live per-channel volume, 0.0–4.0. Returns 1, or 0 on a bad/inactive channel. A non-numeric channel or vol raises by default and answers 0 under EIGS_STRICT=0 (#1007); an out-of-range channel is simply inactive and stays quiet.
audio_stop audio_stop of channel Stop one mixer channel. Returns 1, or 0 on a bad/inactive channel. A non-numeric channel raises by default and answers 0 under EIGS_STRICT=0 (#1007).
audio_capture_open audio_capture_open of [freq, channels] Open the recording (microphone) device and start capturing (#579). Defaults [44100, 1]; SDL converts to exactly the requested format. Returns the device id, or 0 when SDL/capture is unavailable. Non-numeric freq/channels raise by default and answer 0 under EIGS_STRICT=0 (#1007), and so does a short or non-list argument, which used to open at the defaults and answer a real device id. of null is still the defaults. Re-opening closes the previous capture device. Trace-recorded — under EIGS_REPLAY no real device is opened.
audio_capture_read audio_capture_read of null Drain samples accumulated since the last read as a buffer of floats in [-1, 1] (interleaved when channels > 1). At most 2048 samples per call — loop until the returned buffer is empty to drain fully (keeps each trace record replayable). Empty buffer = nothing new yet; null = no capture device open. Trace-recorded — replay serves the recorded samples, never a live microphone.
audio_capture_close audio_capture_close of null Stop and close the recording device, dropping undrained samples. Safe to call twice or with no device open.
audio_stream_open audio_stream_open of [freq, channels] Open the live streaming playback device (queue mode, F-DS-17 — for on-the-fly synthesis like musical typing). Coexists with the audio_open mixer device. Defaults [44100, 1]. Returns the device id (>= 2), or 0 when SDL/audio is unavailable. Non-numeric freq/channels raise by default and answer 0 under EIGS_STRICT=0 (#1007 — they used to skip the override, open at 44100/1 and answer a real id, so a caller that asked for 48000 was told it got it). A short or non-list argument raises for the same reason: audio_stream_open of [48000] answered device id 2 opened at 44100/1. of null is still the defaults. Re-opening closes the previous stream device.
audio_stream_push audio_stream_push of samples Queue a block of float samples [-1, 1] (list or buffer) onto the live stream. Same size cap / clamp as audio_play. Pure output sink (not trace-recorded). Returns true on success, false on a closed device or bad shape; a samples that is not a list or buffer raises by default and answers 0 under EIGS_STRICT=0 (#1007).
audio_stream_queued audio_stream_queued of null Samples still buffered (not yet played) on the live stream — the refill pump pushes another block only while this stays under its latency target. Returns 0 when no stream is open. Trace-recorded (a live, timing-dependent value) — replay serves the recorded depth, keeping the session deterministic.
audio_stream_clear audio_stream_clear of null Drop any buffered audio on the live stream (flush for a panic / all-notes-off). Safe with no device.
audio_stream_close audio_stream_close of null Stop and close the live stream device, dropping buffered audio. Safe to call twice or with no device open.

Internal (sanitizer builds only)

Name Signature Description