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 particularmap,filter, andreduceare NOT builtins — they arelib/list.eigs(import list→list.filter of [xs, fn], orload_file of "lib/list.eigs"for bare names), even thoughsort_byIS a builtin (#734). Rule of thumb: file/process/network, tensor math, JSON encode-decode, regex, channels/tasks, and the interrogatives are builtins (here); everything youimportis 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_ofmiss-1,file_existsof a missing path0) 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
| 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. |
| 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.
| 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 |
| 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 |
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 |
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 |
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. |
| 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. |
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).
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 |
|---|
| 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. |
| 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 |
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 |
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 |
| 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).
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 |
| 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 |
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.
| 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 "" |
| 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 |
| 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 |
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.
Raw-mode keyboard input and ANSI cursor rendering. Terminal is restored automatically at exit.
| Name | Signature | Description |
|---|
| Name | Signature | Description |
|---|---|---|
args |
args of null |
List of arguments after the script name |
| 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...) |
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.
| 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.
| 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) |
| 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) |
| 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) |
| 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) |
| 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). |
| 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 |
| 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 |
| 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). |
| 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 |
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).
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 get503, so one source can't hold all 256 slots. Set to0when 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); otherwise408.EIGS_HTTP_HEADER_MIN_RATE(default 256 bytes/sec,0disables) — 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 |
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.
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.
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).
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.
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 |
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
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'.
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) |
| 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.
| 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]. |
| 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. |
| Name | Signature | Description |
|---|