--- url: 'https://sema-lang.com/docs/stdlib.md' --- # Standard Library Sema ships with a **comprehensive standard library** of built-in functions across many modules, covering everything from string manipulation and file I/O to HTTP requests, regex, and cryptographic hashing. ## Naming Conventions Sema's stdlib follows consistent naming patterns: | Pattern | Convention | Example | | ----------------- | -------------------- | -------------------------------------- | | `module/function` | Slash-namespaced | `string/trim`, `file/read`, `math/gcd` | | `legacy-name` | Scheme compat aliases | `string-append` → `string/append` | | `type->type` | Arrow conversions | `string/to-symbol`, `list->vector` | | `predicate?` | Predicate suffix | `null?`, `list?`, `even?` | ### Naming aliases Several functions are registered under both a legacy (Scheme-style) name and a canonical slash-namespaced or `predicate?` name (Decision #24). Both forms are kept for backward compatibility; new code should prefer the canonical form on the right. | Legacy name | Canonical alias | | --------------------- | ---------------------- | | `any` | `any?` | | `every` | `every?` | | `time-ms` | `time/now-ms` | | `hash-map` | `map/new` | | `promise-forced?` | `async/forced?` | | `tools->routes` | `route/from-tools` | | `make-bytevector` | `bytevector/make` (also `bytevector/new`) | | `bytevector-length` | `bytevector/length` | | `bytevector-u8-ref` | `bytevector/u8-ref` (also `bytevector/ref`) | | `bytevector-u8-set!` | `bytevector/u8-set!` (also `bytevector/set!`) | | `bytevector-copy` | `bytevector/copy` | | `bytevector-append` | `bytevector/append` | | `bytevector->list` | `bytevector/to-list` | | `list->bytevector` | `bytevector/from-list` (also `list/to-bytevector`) | Predicates (`bytevector?` etc.) and the bare `bytevector` varargs constructor keep their short canonical names — predicates always stay un-namespaced. ## Quick Reference ### [Math & Arithmetic](./math) | Function | Description | | ------------------------------------------------------------------------------ | ------------------------- | | `+`, `-`, `*`, `/`, `mod` | Basic arithmetic | | `<`, `>`, `<=`, `>=`, `=` | Comparison | | `abs`, `min`, `max`, `pow`, `sqrt`, `log` | Numeric utilities | | `floor`, `ceil`, `round`, `truncate` | Rounding | | `sin`, `cos`, `math/tan` | Trigonometry | | `math/asin`, `math/acos`, `math/atan`, `math/atan2` | Inverse trig | | `math/sinh`, `math/cosh`, `math/tanh` | Hyperbolic | | `math/exp`, `math/log10`, `math/log2` | Exponential & logarithmic | | `math/gcd`, `math/lcm`, `math/quotient`, `math/remainder` | Integer math | | `math/random`, `math/random-int` | Random numbers | | `math/clamp`, `math/sign`, `math/lerp`, `math/map-range` | Interpolation & clamping | | `math/degrees->radians`, `math/radians->degrees` | Angle conversion | | `even?`, `odd?`, `positive?`, `negative?`, `zero?` | Numeric predicates | | `math/nan?`, `math/infinite?` | Float predicates | | `pi`, `e`, `math/infinity`, `math/nan` | Constants | | `bit/and`, `bit/or`, `bit/xor`, `bit/not`, `bit/shift-left`, `bit/shift-right` | Bitwise operations | ### [Strings & Characters](./strings) | Function | Description | | ----------------------------------------------------------------------------------- | ------------------------- | | `string/append`, `string/length`, `string/ref`, `string/slice` | Core string ops | | `str`, `format` | Conversion & formatting | | `string/split`, `string/join`, `string/trim` | Split, join, trim | | `string/upper`, `string/lower`, `string/capitalize`, `string/title-case` | Case conversion | | `string/contains?`, `string/starts-with?`, `string/ends-with?` | Search predicates | | `string/replace`, `string/index-of`, `string/last-index-of`, `string/reverse` | Manipulation | | `string/chars`, `string/repeat`, `string/pad-left`, `string/pad-right` | Utilities | | `string/map`, `string/number?`, `string/empty?` | Higher-order & predicates | | `string/after`, `string/before`, `string/between`, `string/take` | Slicing & extraction | | `string/chop-start`, `string/chop-end`, `string/ensure-start`, `string/ensure-end` | Prefix & suffix | | `string/wrap`, `string/unwrap`, `string/remove` | Wrapping & removal | | `string/replace-first`, `string/replace-last` | Targeted replacement | | `string/snake-case`, `string/kebab-case`, `string/camel-case`, `string/pascal-case` | Case conversion | | `string/headline`, `string/words` | Headline & word splitting | | `char/to-integer`, `integer/to-char`, `char/alphabetic?`, ... | Character operations | | `string/to-number`, `number/to-string`, `string/to-symbol`, ... | Type conversions | ### [Lists](./lists) | Function | Description | | ----------------------------------------------------------------------- | ----------------------------- | | `list`, `cons`, `car`, `cdr`, `first`, `rest` | Construction & access | | `cadr`, `caddr`, `last`, `nth` | Positional access | | `length`, `append`, `reverse`, `range` | Basic operations | | `map`, `filter`, `foldl`, `foldr`, `reduce`, `flat-map` | Higher-order functions | | `sort`, `sort-by`, `apply`, `for-each` | Ordering & application | | `take`, `drop`, `flatten`, `flatten-deep`, `zip`, `partition` | Sublists | | `member`, `any`, `every`, `list/index-of`, `list/unique`, `list/dedupe` | Searching | | `list/group-by`, `list/interleave`, `list/chunk`, `frequencies` | Grouping | | `list/sum`, `list/min`, `list/max` | Aggregation | | `list/shuffle`, `list/pick` | Random | | `list/repeat`, `make-list`, `iota` | Construction | | `list/split-at`, `list/take-while`, `list/drop-while` | Splitting | | `assoc`, `assq`, `assv` | Association lists | | `interpose` | Interleaving | | `list/reject`, `list/find`, `list/sole` | Filtering & searching | | `list/pluck`, `list/key-by` | Map extraction | | `list/avg`, `list/median`, `list/mode` | Statistics | | `list/diff`, `list/intersect`, `list/duplicates` | Set operations | | `list/sliding`, `list/page`, `list/cross-join` | Windowing & pagination | | `list/pad`, `list/join`, `list/times` | Padding, joining & generation | | `tap` | Utility | ### [Vectors](./vectors) | Function | Description | | ------------------------------ | --------------- | | `vector` | Create a vector | | `vector->list`, `list->vector` | Conversion | ### [Maps & HashMaps](./maps) | Function | Description | | -------------------------------------------------- | ---------------------------- | | `map/new`, `get`, `assoc`, `dissoc`, `merge` | Core map ops | | `keys`, `vals`, `contains?`, `count` | Inspection | | `map/entries`, `map/from-entries` | Entry conversion | | `map/map-vals`, `map/map-keys`, `map/filter` | Higher-order | | `map/select-keys`, `map/update` | Selection & update | | `map/sort-keys`, `map/except`, `map/zip` | Sorting, exclusion & zipping | | `hashmap/new`, `hashmap/get`, `hashmap/assoc`, ... | HashMap operations | ### [Predicates & Type Checking](./predicates) | Function | Description | | ----------------------------------------------------------------- | --------------------- | | `null?`, `nil?`, `empty?`, `list?`, `pair?` | Collection predicates | | `number?`, `integer?`, `float?`, `string?`, `symbol?`, `keyword?` | Type predicates | | `char?`, `record?`, `bytevector?`, `bool?`, `fn?` | More type predicates | | `map?`, `vector?` | Container predicates | | `promise?`, `promise-forced?` | Promise predicates | | `eq?`, `=`, `zero?`, `even?`, `odd?`, `positive?`, `negative?` | Equality & numeric | | `prompt?`, `message?`, `conversation?`, `tool?`, `agent?` | LLM type predicates | ### [File I/O & Paths](./file-io) | Function | Description | | ------------------------------------------------------------------------------------------------------------- | ---------------------------- | | `display`, `println`, `pprint`, `print`, `io/print-error`, `io/println-error`, `newline`, `io/read-line`, `io/read-stdin`, `io/eof?`, `io/flush` | Console I/O | | `file/read`, `file/write`, `file/append` | File read/write | | `file/read-bytes`, `file/write-bytes` | Binary file I/O | | `file/read-lines`, `file/write-lines` | Line-based I/O | | `file/for-each-line`, `file/fold-lines`, `file/fold-lines-bytes` | Streaming line I/O | | `file/delete`, `file/rename`, `file/copy` | File management | | `file/exists?`, `file/is-file?`, `file/is-directory?`, `file/is-symlink?` | File predicates | | `file/list`, `file/mkdir`, `file/info` | Directory operations | | `file/glob` | File globbing | | `path/join`, `path/dirname`, `path/basename`, `path/extension`, `path/absolute` | Path manipulation | | `path/ext`, `path/stem`, `path/dir`, `path/filename`, `path/absolute?` | Path predicates & components | ### [PDF Processing](./pdf) | Function | Description | | ------------------------ | ----------------------------------------------------- | | `pdf/extract-text` | Extract all text from a PDF | | `pdf/extract-text-pages` | Extract text per page (returns list) | | `pdf/page-count` | Get number of pages | | `pdf/metadata` | Get metadata map (`:title`, `:author`, `:pages`, ...) | ### [HTTP & JSON](./http-json) | Function | Description | | ------------------------------------------------------------------ | ------------------ | | `http/get`, `http/post`, `http/put`, `http/delete`, `http/request` | HTTP methods | | `json/encode`, `json/encode-pretty`, `json/decode` | JSON serialization | ### [Web Server](./web-server) | Function | Description | | -------------------------------------------------------------------------------- | ---------------------- | | `http/serve` | Start an HTTP server | | `http/router` | Data-driven routing | | `http/ok`, `http/created`, `http/no-content`, `http/not-found`, `http/error` | JSON response helpers | | `http/redirect` | HTTP redirect | | `http/html`, `http/text` | Content-type responses | | `http/file` | Serve a file from disk | | `http/stream` | SSE streaming | | `http/websocket` | WebSocket connections | ### [Regex](./regex) | Function | Description | | --------------------------------------------------- | ----------------------- | | `regex/match?`, `regex/match`, `regex/find-all` | Matching | | `regex/replace`, `regex/replace-all`, `regex/split` | Replacement & splitting | ### [CSV, Crypto & Encoding](./csv) | Function | Description | | --------------------------------------------- | --------------- | | `csv/parse`, `csv/parse-maps`, `csv/encode` | CSV operations | | `uuid/v4` | UUID generation | | `base64/encode`, `base64/decode` | Base64 encoding | | `base64/encode-bytes`, `base64/decode-bytes` | Binary Base64 | | `hash/sha256`, `hash/md5`, `hash/hmac-sha256` | Hashing | ### [Date & Time](./datetime) | Function | Description | | --------------------------- | -------------------- | | `time/now`, `time-ms` | Current time | | `time/format`, `time/parse` | Formatting & parsing | | `time/date-parts` | Date decomposition | | `time/add`, `time/diff` | Arithmetic | | `sleep` | Delay execution | ### [System](./system) | Function | Description | | ----------------------------------------------------------- | --------------------- | | `env`, `sys/env-all`, `sys/set-env` | Environment variables | | `sys/args`, `sys/cwd`, `sys/platform`, `sys/os`, `sys/arch` | System info | | `sys/pid`, `sys/tty`, `sys/which`, `sys/elapsed` | Process info | | `sys/interactive?`, `sys/hostname`, `sys/user` | Session info | | `sys/home-dir`, `sys/temp-dir` | Directory paths | | `sys/term-size` | Terminal size (Unix) | | `sys/on-signal`, `sys/check-signals` | Signal hooks (Unix) | | `shell` | Run shell commands | | `exit` | Exit process | ### [Serial Ports](./serial) | Function | Description | | ---------------------------------------------------------- | ---------------------------------------- | | `serial/list` | List available device paths | | `serial/open`, `serial/close` | Open/close a port (returns int handle) | | `serial/write`, `serial/read-line` | Raw I/O | | `serial/send` | Write line + read JSON response | ### [Bytevectors](./bytevectors) | Function | Description | | -------------------------------------------------------------- | ----------------- | | `bytevector`, `bytevector/new` | Construction | | `bytevector/length`, `bytevector/ref`, `bytevector/set!` | Access & mutation | | `bytevector/copy`, `bytevector/append` | Copy & append | | `bytevector/to-list`, `list/to-bytevector` | List conversion | | `utf8/to-string`, `string/to-utf8` | String conversion | | `bytes/length`, `bytes/ref`, `bytes/find`, `bytes/slice` | Byte-oriented ops (hot loops) | | `bytes/->string`, `bytes/parse-int10` | Byte decoding & parsing | ### [Streams](./streams) | Function | Description | | --------------------------------------------------------------------- | ------------------------ | | `stream/from-string`, `stream/from-bytes`, `stream/byte-buffer` | In-memory streams | | `stream/open-input`, `stream/open-output` | File streams | | `stream/read`, `stream/read-byte`, `stream/read-line`, `stream/read-all` | Reading | | `stream/write`, `stream/write-byte`, `stream/write-string` | Writing | | `stream/close`, `stream/flush`, `stream/copy` | Control | | `stream?`, `stream/readable?`, `stream/writable?`, `stream/available?` | Predicates | | `stream/type`, `stream/to-bytes`, `stream/to-string` | Introspection & extraction | | `*stdin*`, `*stdout*`, `*stderr*` | Standard I/O globals | | `with-stream` | Resource management macro | ### [Concurrency](./concurrency) | Function | Description | | --------------------------------------------------------------------- | ------------------------ | | `async/spawn`, `async/await`, `async/all`, `async/race` | Async task management | | `async/resolved`, `async/rejected` | Pre-settled promises | | `async/run`, `async/sleep`, `async/timeout` | Scheduler control & deadlines | | `async/cancel`, `async/cancelled?` | Cancellation | | `async/promise?`, `async/resolved?`, `async/rejected?`, `async/pending?` | Promise predicates | | `channel/new`, `channel/send`, `channel/recv`, `channel/try-recv` | Channel operations | | `channel/close` | Channel lifecycle | | `channel?`, `channel/closed?`, `channel/empty?`, `channel/full?`, `channel/count` | Channel predicates | ### [Records](./records) | Function | Description | | -------------------- | -------------------- | | `define-record-type` | Define a record type | | `record?` | Record predicate | | `type` | Get record type tag | ### [Terminal Styling](./terminal) | Function | Description | | ---------------------------------------------------------------- | ----------------------------------- | | `term/bold`, `term/red`, `term/green`, ... | Individual style functions | | `term/style` | Apply multiple styles with keywords | | `term/rgb` | 24-bit true color | | `term/strip` | Remove ANSI escape codes | | `term/spinner-start`, `term/spinner-stop`, `term/spinner-update` | Animated spinners | | `io/tty-raw!`, `io/tty-restore!` | Raw-mode TTY (Unix) | | `io/read-key`, `io/read-key-timeout` | Per-keystroke input (Unix) | ### [Text Processing](./text-processing) | Function | Description | | ------------------------------------------------------------------------- | ------------------------------------------ | | `text/chunk`, `text/chunk-by-separator`, `text/split-sentences` | Text chunking | | `text/clean-whitespace`, `text/strip-html` | Text cleaning | | `text/truncate`, `text/word-count`, `text/trim-indent` | Text utilities | | `text/excerpt`, `text/normalize-newlines` | Excerpt extraction & newline normalization | | `prompt/template`, `prompt/render` | Prompt templates | | `document/create`, `document/text`, `document/metadata`, `document/chunk` | Document metadata | ### [SQLite](./sqlite) | Function | Description | | -------------------------------- | ------------------------------ | | `db/open`, `db/open-memory` | Open file or in-memory database | | `db/exec`, `db/exec-batch` | Execute statements | | `db/query`, `db/query-one` | Query rows as maps | | `db/last-insert-id` | Last inserted rowid | | `db/tables` | List tables | | `db/close` | Close connection | ### [Typed Arrays](./typed-arrays) | Function | Description | | --------------------------------------------------------- | ---------------------- | | `f64-array`, `i64-array` | Create from values | | `f64-array/make`, `i64-array/make` | Create with fill | | `f64-array/range`, `i64-array/range` | Create from range | | `f64-array/from-list`, `i64-array/from-list` | Convert from list | | `f64-array/ref`, `i64-array/ref` | Index access | | `f64-array/set!`, `i64-array/set!` | Set element (CoW) | | `f64-array/length`, `i64-array/length` | Length | | `f64-array/sum`, `i64-array/sum` | Fast sum | | `f64-array/dot` | Dot product | | `f64-array/map`, `i64-array/map` | Map over elements | | `f64-array/fold`, `i64-array/fold` | Fold over elements | | `f64-array?`, `i64-array?` | Type predicates | ### [Mutable Containers](./mutable) | Function | Description | | ----------------------------------------------------------- | ---------------------------------- | | `mutable-array/new` | Create (empty, capacity, or n×fill) | | `mutable-array/push!`, `mutable-array/set!` | In-place update (return the array) | | `mutable-array/get`, `mutable-array/length` | Access | | `mutable-array/->vector` | Freeze to an immutable vector | | `mutable-cell/new`, `mutable-cell/get`, `mutable-cell/set!` | Single mutable slot | ### [Context](./context) | Function | Description | | ----------------------------------------------------------------- | ---------------------------------------- | | `context/set`, `context/get`, `context/has?` | Core key-value context | | `context/remove`, `context/pull`, `context/all` | Retrieval & cleanup | | `context/merge`, `context/clear` | Bulk operations | | `context/with` | Scoped overrides (auto-restores on exit) | | `context/push`, `context/stack`, `context/pop` | Named stacks | | `context/set-hidden`, `context/get-hidden`, `context/has-hidden?` | Hidden (non-logged) context | ### [Key-Value Store](./kv-store) | Function | Description | | ------------------------------- | ------------------------------ | | `kv/open`, `kv/close` | Open/close a JSON-backed store | | `kv/get`, `kv/set`, `kv/delete` | CRUD operations | | `kv/keys` | List all keys | ### [TOML](./toml) | Function | Description | | ------------------------------- | -------------------- | | `toml/decode` | Decode TOML to Sema | | `toml/encode` | Encode Sema to TOML | ### [Playground & WASM](./playground) | Function | Description | | --------------------- | ------------------------------------------------------ | | `web/user-agent` | Browser user agent string (WASM only) | | `web/user-agent-data` | Structured browser info map (Chromium only, WASM only) | --- --- url: 'https://sema-lang.com/docs/stdlib/math.md' --- # Math & Arithmetic ## Domain & error policy Sema's numeric error behavior follows one rule, split by type: * **Integer division or modulo by zero raises an error.** `(/ 1 0)`, `(modulo 7 0)`, and `(mod 7 0)` all raise (`division by zero` / `modulo by zero`). Integers have no infinity or NaN to return, so the failure surfaces where it happens. * **Floating-point follows IEEE 754** — overflow and undefined real-domain results return `inf`, `-inf`, or `NaN` instead of raising: ```sema (/ 1.0 0) ; => inf (/ -1.0 0) ; => -inf (/ 0.0 0.0) ; => NaN (log 0) ; => -inf (log -1) ; => NaN (pow 0 0) ; => 1 (pow 2 -1) ; => 1/2 ``` This matches the hardware and mainstream numeric languages, so `NaN` propagates and `inf` accumulates rather than forcing error handling around every operation. If you need to reject these, test with `math/nan?` / `math/infinite?` explicitly. > **No integer overflow** — Sema has a full numeric tower, so exact integer arithmetic never wraps. A result beyond `i64` range promotes to an arbitrary-precision bignum: `(+ 9223372036854775807 1)` → `9223372036854775808`, `(* 1000000000000 1000000000000)` → `1000000000000000000000000`. Exact division yields rationals (`(/ 1 3)` → `1/3`) and `(sqrt -1)` → `0+1i`. (See ADR #64 and the numeric-tower ADR.) ## The Numeric Tower Sema implements the full R7RS **numeric tower**, a nested hierarchy of number types: ``` integer ⊂ rational ⊂ real ⊂ complex ``` Every integer is a rational, every rational is real, every real is complex — so [`complex?`](/docs/stdlib/predicates#complex) is true for *every* number, and [`integer?`](/docs/stdlib/predicates#integer) is the narrowest test. Two independent axes describe a value: its **level** in the tower (integer / rational / real / complex) and its **exactness** (exact vs. inexact). * **Exact numbers** — integers (any size — they promote to bignums instead of overflowing), exact rationals like `1/3`, and complex numbers whose parts are both exact — carry no rounding error. * **Inexact numbers** — floats and any complex with a floating-point part — follow IEEE 754. * **Exactness contagion:** an operation is exact only when *all* its operands are exact; a single float makes the whole result inexact. This is why `(/ 1 3)` is the exact `1/3` but `(/ 1.0 3)` is `0.3333…`. ### Literal grammar The reader parses each tower type directly, and every literal round-trips through print → read: | Form | Example | Reads as | | --- | --- | --- | | Rational | `1/3`, `3/6` (→ `1/2`) | exact ratio, reduced to lowest terms | | Complex (rectangular) | `3+4i`, `1.5+2.5i`, `3-4i` | real + imaginary part | | Pure imaginary | `+i`, `-i`, `2i`, `-2i` | `0 ± ni` | | Radix prefix | `#xFF` (255), `#o17` (15), `#b1010` (10), `#d42` | base 16 / 8 / 2 / 10 integer | | Exactness prefix | `#e1.5` (→ `3/2`), `#i1/2` (→ `0.5`) | force exact / inexact | | Combined | `#e#xFF`, `#x#eFF` | radix + exactness, either order | | Bignum | `99999999999999999999999999` | out-of-range integer literals become bignums | See the [reader internals](/docs/internals/reader#numeric-literals) for the full grammar. ## Basic Arithmetic ### `+` Add numbers together. Accepts any number of arguments. ```sema (+ 1 2 3) ; => 6 (+ 10) ; => 10 (+) ; => 0 ``` ### `-` Subtract numbers. With one argument, negates. With multiple, subtracts left to right. ```sema (- 10 3) ; => 7 (- 10 3 2) ; => 5 (- 5) ; => -5 ``` ### `*` Multiply numbers together. ```sema (* 4 5) ; => 20 (* 2 3 4) ; => 24 (*) ; => 1 ``` ### `/` Divide numbers. Exact-by-exact division is *exact*: it yields a reduced rational when the result is not a whole number (so `(/ 10 3)` is `10/3`, not `3.333…`), collapsing back to an integer when the denominator reduces to 1. A float operand makes the result a float (inexact contagion). For truncated integer division use [`quotient`](#quotient) (or its `math/quotient` alias). ```sema (/ 10 2) ;; => 5 (/ 10 3) ;; => 10/3 (/ 10 4) ;; => 5/2 (/ 10.0 3) ;; => 3.3333333333333335 ``` ### `mod` Modulo. On exact integers this is *floored* division — the result takes the sign of the **divisor**, per R7RS — so `(mod -7 2)` is `1`, not `-1`. (Float operands keep the IEEE truncated `%`, whose sign follows the dividend.) For the truncated integer counterpart whose sign follows the dividend, use [`remainder`](#remainder). ```sema (mod 10 3) ; => 1 (mod 7 2) ; => 1 (mod -7 2) ; => 1 ``` ## Comparison ### `<` Less than. Supports chaining. ```sema (< 1 2) ; => #t (< 1 2 3) ; => #t (< 3 2) ; => #f ``` ### `>` Greater than. ```sema (> 3 2) ; => #t (> 1 2) ; => #f ``` ### `<=` Less than or equal. ```sema (<= 1 2) ; => #t (<= 2 2) ; => #t ``` ### `>=` Greater than or equal. ```sema (>= 3 2) ; => #t (>= 2 2) ; => #t ``` ### `=` Equality. For numbers this is numeric equality (so `(= 1 1.0)` is `#t`); for non-numbers it falls back to structural equality. Unlike `<` / `>`, comparing non-numbers does not error. ```sema (= 1 1) ; => #t (= 1 1.0) ; => #t (= 1 2) ; => #f (= "abc" "abc") ; => #t (structural, not an error) ``` ## Numeric Utilities ### `abs` Absolute value. ```sema (abs -5) ; => 5 (abs 3) ; => 3 (abs -3.14) ; => 3.14 ``` ### `min` Return the smallest of 1 or more numbers (the no-arg case errors). ```sema (min 1 2 3) ;; => 1 (min 5) ;; => 5 (min) ;; error: Arity error: min expects 1+ args, got 0 ``` ### `max` Return the largest of 1 or more numbers (the no-arg case errors). ```sema (max 1 2 3) ;; => 3 (max 5) ;; => 5 (max) ;; error: Arity error: max expects 1+ args, got 0 ``` ### `pow` Raise a number to a power. ```sema (pow 2 10) ; => 1024 (pow 3 3) ; => 27 ``` ### `sqrt` Square root. The square root of an exact perfect square is returned *exactly* (`(sqrt 16)` is `4`, not `4.0`); otherwise the result is an inexact float. The square root of a negative number is complex. ```sema (sqrt 16) ; => 4 (sqrt 2) ; => 1.4142135623730951 (sqrt -1) ; => 0+1i ``` ### `log` Natural logarithm. ```sema (log 1) ; => 0.0 (log 100) ; => 4.605... ``` ### `floor` Round down toward negative infinity. **Exactness-preserving**: a float argument rounds to a float (`3.7` → `3.0`), while an exact rational rounds to an exact integer (`7/2` → `3`). ```sema (floor 3.7) ; => 3.0 (floor -2.3) ; => -3.0 (floor 7/2) ; => 3 ``` ### `ceil` Round up toward positive infinity. Exactness-preserving, like `floor`. ```sema (ceil 3.2) ; => 4.0 (ceil -2.7) ; => -2.0 (ceil 7/2) ; => 4 ``` ### `round` Round to the nearest integer, ties to even (banker's rounding). Exactness-preserving: a float rounds to a float, an exact rational to an exact integer. ```sema (round 3.5) ; => 4.0 (round 3.4) ; => 3.0 (round 7/2) ; => 4 ``` ### `math/round-to` Round to `places` decimal places, returning a float (where `round` only rounds to a whole integer). ```sema (math/round-to 3.14159 2) ; => 3.14 (math/round-to 0.46666 3) ; => 0.467 ``` ### `math/format-fixed` Format a number as a fixed-decimal **string**, padding trailing zeros to `places` digits — for money/metrics display where `math/round-to` (a float, which drops trailing zeros) isn't enough. ```sema (math/format-fixed 1.2 3) ; => "1.200" (math/format-fixed 3.14159 2) ; => "3.14" ``` ## Trigonometry ### `sin` Sine (argument in radians). ```sema (sin 0) ; => 0.0 (sin pi) ; => ~0.0 ``` ### `cos` Cosine (argument in radians). ```sema (cos 0) ; => 1.0 (cos pi) ; => -1.0 ``` ### `math/tan` Tangent (argument in radians). ```sema (math/tan 0) ; => 0.0 (math/tan (/ pi 4)); => ~1.0 ``` ### `math/asin` Inverse sine. Returns radians. ```sema (math/asin 1) ; => ~1.5707 (π/2) (math/asin 0) ; => 0.0 ``` ### `math/acos` Inverse cosine. Returns radians. ```sema (math/acos 0) ; => ~1.5707 (π/2) (math/acos 1) ; => 0.0 ``` ### `math/atan` Inverse tangent. Returns radians. ```sema (math/atan 1) ; => ~0.7854 (π/4) (math/atan 0) ; => 0.0 ``` ### `math/atan2` Two-argument inverse tangent. Returns the angle in radians between the positive x-axis and the point (x, y). ```sema (math/atan2 1 1) ; => ~0.7854 (π/4) (math/atan2 0 -1) ; => ~3.1416 (π) ``` ## Hyperbolic Functions ### `math/sinh` Hyperbolic sine. ```sema (math/sinh 0) ; => 0.0 (math/sinh 1) ; => 1.1752... ``` ### `math/cosh` Hyperbolic cosine. ```sema (math/cosh 0) ; => 1.0 (math/cosh 1) ; => 1.5430... ``` ### `math/tanh` Hyperbolic tangent. ```sema (math/tanh 0) ; => 0.0 (math/tanh 1) ; => 0.7615... ``` ## Exponential & Logarithmic ### `math/exp` Euler's number raised to a power (e^x). ```sema (math/exp 1) ; => 2.71828... (math/exp 0) ; => 1.0 ``` ### `math/log10` Base-10 logarithm. ```sema (math/log10 100) ; => 2.0 (math/log10 1000) ; => 3.0 ``` ### `math/log2` Base-2 logarithm. ```sema (math/log2 8) ; => 3.0 (math/log2 1024) ; => 10.0 ``` ## Integer Division These operate on exact integers, are **bignum-aware** (they promote past `i64` automatically), and raise on a zero divisor. Each has a slash-namespaced alias (`math/quotient`, `math/remainder`, `math/gcd`, `math/lcm`) that is the identical function. ### `quotient` Truncated integer division: `n ÷ d` rounded **toward zero**, so `(quotient -7 2)` is `-3` (not floored to `-4`), per R7RS. Alias: `math/quotient`. ```sema (quotient 10 3) ; => 3 (quotient -7 2) ; => -3 (quotient 100000000000000000000 7) ; => 14285714285714285714 ``` ### `remainder` Remainder of truncated division: the result takes the sign of the **dividend** `n`. Pairs with `quotient` so that `(+ (* (quotient n d) d) (remainder n d))` reconstructs `n`. Contrast [`mod`](#mod), whose sign follows the divisor. Alias: `math/remainder`. ```sema (remainder 10 3) ; => 1 (remainder -7 2) ; => -1 (remainder 7 -2) ; => 1 ``` ### `gcd` Greatest common divisor — the largest non-negative integer dividing every argument. Variadic and sign-independent; `(gcd)` is `0`. Alias: `math/gcd`. ```sema (gcd 12 8) ; => 4 (gcd 15 10 25) ; => 5 (gcd -12 8) ; => 4 (gcd) ; => 0 ``` ### `lcm` Least common multiple — the smallest non-negative integer every argument divides. Variadic and sign-independent; `0` if any argument is `0`; `(lcm)` is `1`. Alias: `math/lcm`. ```sema (lcm 4 6) ; => 12 (lcm 2 3 4) ; => 12 (lcm 0 5) ; => 0 (lcm) ; => 1 ``` ## Exactness & Rationals Utilities for moving between exact and inexact forms and for working with exact rationals. See [The Numeric Tower](#the-numeric-tower) for the underlying model. ### `exact` Convert a number to its exact form. A finite float becomes the *exact* rational it actually represents (reduced, and normalized to an integer when the denominator is 1); already-exact numbers pass through. `inexact->exact` is the longer R7RS spelling. ```sema (exact 0.5) ; => 1/2 (exact 2.0) ; => 2 (exact 1/3) ; => 1/3 (exact 3.14159) ; => 3537115888337719/1125899906842624 ``` The last result is exact but surprising: `3.14159` is not representable in binary, so `exact` returns the precise fraction the double stores. Use [`rationalize`](#rationalize) for a tidy approximation. ### `inexact` Convert a number to inexact (floating-point) form — an exact rational becomes its nearest `f64`, and each part of a complex becomes a float. `exact->inexact` is the longer R7RS spelling. ```sema (inexact 1/3) ; => 0.3333333333333333 (inexact 42) ; => 42.0 (inexact 3+4i) ; => 3.0+4.0i ``` ### `exact->inexact` R7RS spelling of [`inexact`](#inexact) — identical behavior. ```sema (exact->inexact 1/3) ; => 0.3333333333333333 (exact->inexact 42) ; => 42.0 ``` ### `inexact->exact` R7RS spelling of [`exact`](#exact) — identical behavior. ```sema (inexact->exact 0.5) ; => 1/2 (inexact->exact 2.0) ; => 2 (inexact->exact 0.1) ; => 3602879701896397/36028797018963968 ``` `0.1` shows the caveat: it has no finite binary expansion, so its exact value is that large power-of-two fraction, not `1/10`. ### `numerator` Numerator of an exact rational, taken in lowest terms (the sign lives on the numerator). An integer `n` is `n/1`. A float or complex argument raises a type error — convert with `exact` first. ```sema (numerator 22/7) ; => 22 (numerator -6/4) ; => -3 (numerator 42) ; => 42 ``` ### `denominator` Denominator of an exact rational, in lowest terms. An integer's denominator is `1`. ```sema (denominator 22/7) ; => 7 (denominator -6/4) ; => 2 (denominator 42) ; => 1 ``` ### `rationalize` Find the *simplest* rational within `tol` of `x` (smallest denominator in `[x-|tol|, x+|tol|]`), per R7RS. Exactness follows contagion: the result is exact only when **both** arguments are exact. ```sema (rationalize 1/3 1/1000) ; => 1/3 (rationalize (exact 3.14159) 1/100) ; => 22/7 (rationalize 3.14159 1/100) ; => 3.142857142857143 (rationalize 1/2 0.01) ; => 0.5 ``` The last two show contagion — an inexact `x` *or* an inexact `tol` gives an inexact (float) answer. ### `exact-integer-sqrt` Exact integer square root of a non-negative integer. Returns a two-element list `(s r)` with `s = ⌊√n⌋` and `s*s + r = n`; exact even for bignums. ```sema (exact-integer-sqrt 17) ; => (4 1) (exact-integer-sqrt 100) ; => (10 0) (exact-integer-sqrt 15241578750190521) ; => (123456789 0) ``` ## Complex Numbers A complex number `a+bi` has a real and an imaginary part. A complex whose imaginary part is *exact* zero collapses to a real, so `real?` is true for `3+0i`. Polar conversions run through `sin`/`cos`/`atan2` in floating point, so [`make-polar`](#make-polar), [`magnitude`](#magnitude), and [`angle`](#angle) are always inexact. ### `make-rectangular` Construct a complex from a real and an imaginary part. An *exact*-zero imaginary part collapses to the real; an *inexact* zero (`0.0`) stays complex. ```sema (make-rectangular 3 4) ; => 3+4i (make-rectangular 1/3 1/2) ; => 1/3+1/2i (make-rectangular 2 0) ; => 2 (make-rectangular 3 0.0) ; => 3+0.0i ``` ### `make-polar` Construct a complex from magnitude `r` and angle `θ` (radians): `r·cos θ + r·sin θ·i`. Always inexact. ```sema (make-polar 2 0) ; => 2.0+0.0i (make-polar 5 (math/atan2 3 4)) ; => 4.0+3.0i ``` ### `real-part` Real part of a number (the number itself for a real). Preserves exactness. ```sema (real-part 3+4i) ; => 3 (real-part 5i) ; => 0 (real-part 2.5) ; => 2.5 ``` ### `imag-part` Imaginary part of a number (exact `0` for any real). Preserves exactness. ```sema (imag-part 3+4i) ; => 4 (imag-part 5i) ; => 5 (imag-part 2.5) ; => 0 ``` ### `magnitude` Magnitude (modulus, absolute value). For a complex `a+bi` this is `√(a²+b²)`, computed in floating point (so it is inexact); for a real it is the absolute value and preserves exactness. ```sema (magnitude 3+4i) ; => 5.0 (magnitude -5) ; => 5 (magnitude 1/3) ; => 1/3 ``` ### `angle` Angle (argument) of a complex in radians, in (-π, π]: `atan2(b, a)`. Always inexact — a positive real gives `0.0`, a negative real gives π. ```sema (angle 3+4i) ; => 0.9272952180016122 (angle 5) ; => 0.0 (angle -5) ; => 3.141592653589793 ``` ## Number ↔ String ### `number->string` Render any number in the tower as a string. An optional radix of 2, 8, 10, or 16 selects the output base — but a non-decimal radix accepts only exact integers. ```sema (number->string 42) ; => "42" (number->string 1/3) ; => "1/3" (number->string 3+4i) ; => "3+4i" (number->string 255 16) ; => "ff" (number->string 5 2) ; => "101" ``` ### `string->number` Parse a string as a number, returning `#f` (never an error) on invalid input. The default radix 10 accepts the whole tower (integers, rationals, floats, complex); a radix of 2, 8, or 16 parses an integer in that base. ```sema (string->number "42") ; => 42 (string->number "1/3") ; => 1/3 (string->number "3+4i") ; => 3+4i (string->number "ff" 16) ; => 255 (string->number "nope") ; => #f ``` ## Random Numbers ### `math/random` Return a random float between 0.0 (inclusive) and 1.0 (exclusive). ```sema (math/random) ; => 0.7291... (varies) ``` ### `math/random-int` Return a random integer in a range (inclusive on both ends). ```sema (math/random-int 1 100) ; => 42 (varies) (math/random-int 0 9) ; => 7 (varies) ``` ## Interpolation & Clamping ### `math/clamp` Clamp a value to a range. ```sema (math/clamp 15 0 10) ; => 10 (math/clamp -5 0 10) ; => 0 (math/clamp 5 0 10) ; => 5 ``` ### `math/sign` Return the sign of a number: -1, 0, or 1. ```sema (math/sign -5) ; => -1 (math/sign 0) ; => 0 (math/sign 42) ; => 1 ``` ### `math/lerp` Linear interpolation between two values. `(math/lerp a b t)` returns `a + (b - a) * t`. ```sema (math/lerp 0 100 0.5) ; => 50.0 (math/lerp 0 100 0.25) ; => 25.0 (math/lerp 10 20 0.0) ; => 10.0 ``` ### `math/map-range` Map a value from one range to another. `(math/map-range value in-min in-max out-min out-max)`. ```sema (math/map-range 5 0 10 0 100) ; => 50.0 (math/map-range 0.5 0 1 0 255) ; => 127.5 ``` ## Angle Conversion ### `math/degrees->radians` Convert degrees to radians. ```sema (math/degrees->radians 180) ; => 3.14159... (math/degrees->radians 90) ; => 1.5707... ``` ### `math/radians->degrees` Convert radians to degrees. ```sema (math/radians->degrees pi) ; => 180.0 (math/radians->degrees 1) ; => 57.295... ``` ## Numeric Predicates ### `even?` Test if an integer is even. ```sema (even? 4) ; => #t (even? 3) ; => #f ``` ### `odd?` Test if an integer is odd. ```sema (odd? 3) ; => #t (odd? 4) ; => #f ``` ### `positive?` Test if a number is positive. ```sema (positive? 1) ; => #t (positive? -1) ; => #f (positive? 0) ; => #f ``` ### `negative?` Test if a number is negative. ```sema (negative? -1) ; => #t (negative? 1) ; => #f ``` ### `zero?` Test if a number is zero. ```sema (zero? 0) ; => #t (zero? 1) ; => #f ``` ### `math/nan?` Test if a value is NaN (not a number). ```sema (math/nan? math/nan) ; => #t (math/nan? 42) ; => #f ``` ### `math/infinite?` Test if a value is infinite. ```sema (math/infinite? math/infinity) ; => #t (math/infinite? 42) ; => #f ``` ## Constants ### `pi` The mathematical constant π (3.14159...). ```sema pi ; => 3.141592653589793 ``` ### `e` Euler's number (2.71828...). ```sema e ; => 2.718281828459045 ``` ### `math/infinity` Positive infinity. ```sema math/infinity ; => Inf ``` ### `math/nan` Not a number. ```sema math/nan ; => NaN ``` ## Scheme Aliases ### `modulo` Alias for `mod`. ```sema (modulo 10 3) ; => 1 ``` ### `expt` Alias for `pow` (Scheme name for exponentiation). With exact integer arguments the result is exact and bignum-aware — no overflow — and a negative exponent yields an exact rational. ```sema (expt 2 10) ; => 1024 (expt 2 100) ; => 1267650600228229401496703205376 (expt 2 -1) ; => 1/2 ``` ### `ceiling` Alias for `ceil` (exactness-preserving, so a float rounds to a float). ```sema (ceiling 3.2) ; => 4.0 ``` ### `truncate` Round toward zero (drop the fractional part). Exactness-preserving: a float truncates to a float, an exact rational to an exact integer. ```sema (truncate 3.7) ; => 3.0 (truncate -3.7) ; => -3.0 (truncate 7/2) ; => 3 ``` ## Bitwise Operations ### `bit/and` Bitwise AND. ```sema (bit/and 5 3) ; => 1 (bit/and 15 9) ; => 9 ``` ### `bit/or` Bitwise OR. ```sema (bit/or 5 3) ; => 7 (bit/or 8 4) ; => 12 ``` ### `bit/xor` Bitwise XOR. ```sema (bit/xor 5 3) ; => 6 ``` ### `bit/not` Bitwise NOT (complement). ```sema (bit/not 5) ; => -6 ``` ### `bit/shift-left` Left bit shift. ```sema (bit/shift-left 1 4) ; => 16 (bit/shift-left 3 2) ; => 12 ``` ### `bit/shift-right` Right bit shift. ```sema (bit/shift-right 16 2) ; => 4 (bit/shift-right 8 1) ; => 4 ``` --- --- url: 'https://sema-lang.com/docs/stdlib/strings.md' --- # Strings & Characters ## Core String Operations ### `string/split` Split a string by a delimiter. ```sema (string/split "a,b,c" ",") ; => ("a" "b" "c") (string/split "hello world" " ") ; => ("hello" "world") ``` ### `string/lines` Split into lines on `\n` or `\r\n` (Clojure `split-lines` semantics). A trailing newline does not produce a final empty line — handy for processing logs, config, or file contents. Use `string/split` when you need a literal separator instead. ```sema (string/lines "a\nb\r\nc\n") ; => ("a" "b" "c") (string/lines "single") ; => ("single") ``` ### `string/join` Join a list of strings with a separator. ```sema (string/join '("a" "b" "c") ", ") ; => "a, b, c" (string/join '("x" "y") "-") ; => "x-y" ``` ### `string/trim` Remove whitespace from both ends. ```sema (string/trim " hello ") ; => "hello" (string/trim "\thello\n") ; => "hello" ``` ### `string/trim-left` Remove whitespace from the left. ```sema (string/trim-left " hi") ; => "hi" ``` ### `string/trim-right` Remove whitespace from the right. ```sema (string/trim-right "hi ") ; => "hi" ``` ### `string/upper` Convert string to uppercase. ```sema (string/upper "hello") ; => "HELLO" ``` ### `string/lower` Convert string to lowercase. ```sema (string/lower "HELLO") ; => "hello" ``` ### `string/capitalize` Uppercase the first character and lowercase the rest. ```sema (string/capitalize "hello") ; => "Hello" (string/capitalize "hELLO") ; => "Hello" ``` ### `string/title-case` Capitalize the first character of each word. ```sema (string/title-case "hello world") ; => "Hello World" ``` ### `string/contains?` Test if a string contains a substring. ```sema (string/contains? "hello" "ell") ; => #t (string/contains? "hello" "xyz") ; => #f ``` ### `string/starts-with?` Test if a string starts with a prefix. ```sema (string/starts-with? "hello" "he") ; => #t (string/starts-with? "hello" "lo") ; => #f ``` ### `string/ends-with?` Test if a string ends with a suffix. ```sema (string/ends-with? "hello" "lo") ; => #t (string/ends-with? "hello" "he") ; => #f ``` ### `string/replace` Replace all occurrences of a substring. ```sema (string/replace "hello" "l" "r") ; => "herro" (string/replace "aaa" "a" "b") ; => "bbb" ``` ### `string/index-of` Return the character index of the first occurrence of a substring, or `nil` if not found. ```sema (string/index-of "hello" "ll") ; => 2 (string/index-of "hello" "xyz") ; => nil ``` ### `string/last-index-of` Find the last occurrence of a substring. Returns the character index or `nil` if not found. ```sema (string/last-index-of "abcabc" "abc") ; => 3 (string/last-index-of "hello" "xyz") ; => nil ``` ### `string/chars` Convert a string to a list of characters. ```sema (string/chars "abc") ; => (#\a #\b #\c) ``` ### `string/repeat` Repeat a string N times. ```sema (string/repeat "ab" 3) ; => "ababab" (string/repeat "-" 5) ; => "-----" ``` ### `string/pad-left` Pad a string on the left to a given width. ```sema (string/pad-left "42" 5 "0") ; => "00042" (string/pad-left "hi" 5) ; => " hi" ``` ### `string/pad-right` Pad a string on the right to a given width. ```sema (string/pad-right "hi" 5) ; => "hi " (string/pad-right "42" 5 "0") ; => "42000" ``` ### `string/width` Terminal **display width** in columns (not character count): wide characters (CJK, most emoji) count as 2, combining marks as 0, and ANSI escape sequences as 0\. Use it for terminal layout, padding, and alignment, where `string-length` is wrong for non-ASCII or styled text. ```sema (string/width "hello") ; => 5 (string/width "日本語") ; => 6 (string-length is 3) (string/width "👋") ; => 2 ``` ### `string/word-wrap` Word-wrap text to a list of lines of at most N display columns. Wraps on spaces, hard-breaks over-long words on grapheme boundaries, preserves newlines, and measures with `string/width` (correct for non-ASCII). Distinct from `string/wrap`, which wraps a string in delimiters. ```sema (string/word-wrap "the quick brown fox" 10) ; => ("the quick" "brown fox") (string/word-wrap "日本語 の テスト" 8) ; => ("日本語" "の" "テスト") ``` ### `string/truncate-width` Clamp a string to a target **display width**, in columns — the truncation counterpart to `string/width`. Splits on grapheme-cluster boundaries, so wide characters (CJK, most emoji) are never cut in half. A string already at or under the width is returned unchanged; like `string/pad-left`/`string/pad-right` only pad, this only shrinks. An optional `ellipsis` string is appended within the width budget when the input is truncated. ```sema (string/truncate-width "hello world" 5) ; => "hello" (string/truncate-width "hi" 10) ; => "hi" ; already fits, unchanged (string/truncate-width "日本語です" 6) ; => "日本語" (string/truncate-width "hello world" 6 "…") ; => "hello…" ``` ### `string/number?` Test if a string represents a valid number. ```sema (string/number? "42") ; => #t (string/number? "3.14") ; => #t (string/number? "hello") ; => #f ``` ### `string/empty?` Test if a string is empty. ```sema (string/empty? "") ; => #t (string/empty? "hello") ; => #f ``` ### `string/map` Apply a character function to each character in a string, returning a new string. ```sema (string/map char/upcase "hello") ; => "HELLO" ``` ### `string/reverse` Reverse a string. ```sema (string/reverse "hello") ; => "olleh" ``` ## Unicode & Encoding ### `string/byte-length` Return the UTF-8 byte length of a string (as opposed to character count from `string/length`). Useful for understanding the actual memory footprint — emoji and CJK characters use more bytes than ASCII. ```sema (string/byte-length "hello") ; => 5 (ASCII: 1 byte each) (string/byte-length "héllo") ; => 6 (é is 2 bytes in UTF-8) (string/byte-length "日本語") ; => 9 (CJK: 3 bytes each) (string/byte-length "😀") ; => 4 (emoji: 4 bytes) ``` Compare with `string/length` which counts characters: ```sema (string/length "😀") ; => 1 (one character) (string/byte-length "😀") ; => 4 (four bytes) ``` ### `string/codepoints` Return a list of Unicode codepoint integers for each character in a string. This reveals the internal structure of composed characters and emoji sequences. ```sema (string/codepoints "ABC") ; => (65 66 67) (string/codepoints "é") ; => (233) (string/codepoints "😀") ; => (128512) ``` Emoji that appear as a single glyph are often multiple codepoints joined by Zero Width Joiner (U+200D = 8205): ```sema ;; 👨‍👩‍👦 is actually 👨 + ZWJ + 👩 + ZWJ + 👦 (string/codepoints "👨‍👩‍👦") ; => (128104 8205 128105 8205 128102) ;; 👋🏽 is 👋 + skin tone modifier (string/codepoints "👋🏽") ; => (128075 127997) ``` ### `string/from-codepoints` Construct a string from a list of Unicode codepoint integers. This is the inverse of `string/codepoints` and enables building emoji programmatically by combining codepoints. ```sema (string/from-codepoints (list 65 66 67)) ; => "ABC" (string/from-codepoints (list 233)) ; => "é" ``` Build emoji by combining people with ZWJ (8205): ```sema ;; Build a family: 👨 + ZWJ + 👩 + ZWJ + 👧 (string/from-codepoints (list 128104 8205 128105 8205 128103)) ;; => 👨‍👩‍👧 ;; Build a profession: 👩 + ZWJ + 💻 (string/from-codepoints (list 128105 8205 128187)) ;; => 👩‍💻 ;; Add skin tone: 👋 + modifier (string/from-codepoints (list 128075 127997)) ;; => 👋🏽 ;; Build flags from Regional Indicators (A=127462): (string/from-codepoints (list 127475 127476)) ;; => 🇳🇴 (NO = Norway) ``` Roundtrip any string through codepoints: ```sema (string/from-codepoints (string/codepoints "Hello 世界")) ;; => "Hello 世界" ``` ### `string/normalize` Normalize a string to a Unicode normalization form. Supported forms: `:nfc`, `:nfd`, `:nfkc`, `:nfkd` (as keywords or strings). * **NFC** — Canonical Decomposition, followed by Canonical Composition (most common) * **NFD** — Canonical Decomposition * **NFKC** — Compatibility Decomposition, followed by Canonical Composition * **NFKD** — Compatibility Decomposition ```sema ;; NFC: combine decomposed characters ;; e + combining acute accent → é (string/normalize "e\u0301" :nfc) ; => "é" ;; NFD: decompose composed characters (string/length (string/normalize "é" :nfd)) ; => 2 (e + combining accent) ;; NFKC/NFKD: compatibility decomposition (ligatures, etc.) (string/normalize "\uFB01" :nfkc) ; => "fi" (fi ligature → two letters) ;; String form names also work (string/normalize "e\u0301" "NFC") ; => "é" ``` ### `string/foldcase` Apply Unicode case folding to a string. Useful for case-insensitive comparisons and normalization. Uses full Unicode-aware lowercasing. ```sema (string/foldcase "HELLO") ; => "hello" (string/foldcase "Hello World") ; => "hello world" (string/foldcase "Straße") ; => "strasse" (full folding maps ß → ss) (string/foldcase "ΩΜΕΓΑ") ; => "ωμεγα" ``` ### `string-ci=?` Case-insensitive string equality comparison. Compares two strings after applying case folding to both. ```sema (string-ci=? "Hello" "hello") ; => #t (string-ci=? "ABC" "abc") ; => #t (string-ci=? "CAFÉ" "café") ; => #t (string-ci=? "hello" "world") ; => #f ``` ## Scheme Compatibility Aliases These functions use legacy Scheme/R7RS naming conventions. They work identically to their modern equivalents and are kept for compatibility. Prefer the `string/` namespaced variants in new code. ### `string/append` Concatenate strings together. ```sema (string/append "hello" " " "world") ; => "hello world" (string/append "a" "b" "c") ; => "abc" ``` ### `string/length` Return the number of characters in a string. ```sema (string/length "hello") ; => 5 (string/length "") ; => 0 (string/length "héllo") ; => 5 (string/length "日本語") ; => 3 ``` ### `string/ref` Return the character at a given index. ```sema (string/ref "hello" 0) ; => #\h (string/ref "hello" 4) ; => #\o ``` ### `string/slice` Extract a substring by start and end character index. ```sema (string/slice "hello" 1 3) ; => "el" (string/slice "hello" 0 5) ; => "hello" (string/slice "héllo" 1 2) ; => "é" ``` ### `str` Convert any value to its string representation. ```sema (str 42) ; => "42" (str #t) ; => "#t" (str '(1 2 3)) ; => "(1 2 3)" ``` ### `format` Format a string with `~a` placeholders. ```sema (format "~a is ~a" "Sema" "great") ; => "Sema is great" (format "~a + ~a = ~a" 1 2 3) ; => "1 + 2 = 3" ``` ## Characters Character literals are written with the `#\` prefix. ```sema #\a ; character literal #\space ; named character: space #\newline ; named character: newline #\tab ; named character: tab ``` ### `char/to-integer` Convert a character to its Unicode code point. ```sema (char/to-integer #\A) ; => 65 (char/to-integer #\a) ; => 97 ``` ### `integer/to-char` Convert a Unicode code point to a character. ```sema (integer/to-char 65) ; => #\A (integer/to-char 955) ; => #\λ ``` ### `char/alphabetic?` Test if a character is alphabetic. ```sema (char/alphabetic? #\a) ; => #t (char/alphabetic? #\5) ; => #f ``` ### `char/numeric?` Test if a character is numeric. ```sema (char/numeric? #\5) ; => #t (char/numeric? #\a) ; => #f ``` ### `char/whitespace?` Test if a character is whitespace. ```sema (char/whitespace? #\space) ; => #t (char/whitespace? #\a) ; => #f ``` ### `char/upper-case?` Test if a character is uppercase. ```sema (char/upper-case? #\A) ; => #t (char/upper-case? #\a) ; => #f ``` ### `char/upcase` Convert a character to uppercase. ```sema (char/upcase #\a) ; => #\A ``` ### `char/downcase` Convert a character to lowercase. ```sema (char/downcase #\Z) ; => #\z ``` ### `char/to-string` Convert a character to a single-character string. ```sema (char/to-string #\a) ; => "a" ``` ### `string/to-char` Convert a single-character string to a character. ```sema (string/to-char "a") ; => #\a ``` ## Character Comparison (R7RS) ### `char=?` Character equality. ```sema (char=? #\a #\a) ; => #t (char=? #\a #\b) ; => #f ``` ### `char #t ``` ### `char>?` Character greater-than. ```sema (char>? #\b #\a) ; => #t ``` ### `char<=?` Character less-than-or-equal. ```sema (char<=? #\a #\b) ; => #t (char<=? #\a #\a) ; => #t ``` ### `char>=?` Character greater-than-or-equal. ```sema (char>=? #\b #\a) ; => #t ``` ### `char-ci=?` Case-insensitive character equality. ```sema (char-ci=? #\A #\a) ; => #t ``` ## Type Conversions ### `string/to-number` Parse a string as a number. ```sema (string/to-number "42") ; => 42 (string/to-number "3.14") ; => 3.14 ``` ### `number/to-string` Convert a number to a string. ```sema (number/to-string 42) ; => "42" (number/to-string 3.14) ; => "3.14" ``` ### `string/to-symbol` Convert a string to a symbol. ```sema (string/to-symbol "foo") ; => foo ``` ### `symbol/to-string` Convert a symbol to a string. ```sema (symbol/to-string 'foo) ; => "foo" ``` ### `string/to-keyword` Convert a string to a keyword. ```sema (string/to-keyword "name") ; => :name ``` ### `keyword/to-string` Convert a keyword to a string. ```sema (keyword/to-string :name) ; => "name" ``` ### `string/to-list` Convert a string to a list of characters. ```sema (string/to-list "abc") ; => (#\a #\b #\c) ``` ### `list->string` Convert a list of characters to a string. ```sema (list->string '(#\h #\i)) ; => "hi" ``` ## Slicing & Extraction ### `string/after` Everything after the first occurrence of a needle. Returns the original string if needle not found. ```sema (string/after "hello@world.com" "@") ; => "world.com" (string/after "no-match" "@") ; => "no-match" ``` ### `string/after-last` Everything after the last occurrence of a needle. ```sema (string/after-last "a.b.c" ".") ; => "c" ``` ### `string/before` Everything before the first occurrence of a needle. ```sema (string/before "hello@world.com" "@") ; => "hello" (string/before "no-match" "@") ; => "no-match" ``` ### `string/before-last` Everything before the last occurrence of a needle. ```sema (string/before-last "a.b.c" ".") ; => "a.b" ``` ### `string/between` Extract the portion between two delimiters. ```sema (string/between "[hello]" "[" "]") ; => "hello" (string/between "start:middle:end" "start:" ":end") ; => "middle" ``` ### `string/take` Take the first N characters (positive) or last N characters (negative). ```sema (string/take "hello" 3) ; => "hel" (string/take "hello" -2) ; => "lo" ``` ## Prefix & Suffix ### `string/chop-start` Remove a prefix if present, otherwise return unchanged. ```sema (string/chop-start "Hello World" "Hello ") ; => "World" (string/chop-start "Hello" "Bye") ; => "Hello" ``` ### `string/chop-end` Remove a suffix if present. ```sema (string/chop-end "file.txt" ".txt") ; => "file" (string/chop-end "file.txt" ".md") ; => "file.txt" ``` ### `string/ensure-start` Ensure a string starts with a prefix (adds it if missing). ```sema (string/ensure-start "/path" "/") ; => "/path" (string/ensure-start "path" "/") ; => "/path" ``` ### `string/ensure-end` Ensure a string ends with a suffix. ```sema (string/ensure-end "path" "/") ; => "path/" (string/ensure-end "path/" "/") ; => "path/" ``` ### `string/wrap` Wrap a string with left and right delimiters. ```sema (string/wrap "hello" "(" ")") ; => "(hello)" (string/wrap "hello" "**") ; => "**hello**" ``` ### `string/unwrap` Remove surrounding delimiters if both present. ```sema (string/unwrap "(hello)" "(" ")") ; => "hello" (string/unwrap "hello" "(" ")") ; => "hello" ``` ## Replacement ### `string/replace-first` Replace only the first occurrence of a substring. ```sema (string/replace-first "aaa" "a" "b") ; => "baa" ``` ### `string/replace-last` Replace only the last occurrence. ```sema (string/replace-last "aaa" "a" "b") ; => "aab" ``` ### `string/remove` Remove all occurrences of a substring. ```sema (string/remove "hello world" "o") ; => "hell wrld" ``` ## Case Conversion ### `string/snake-case` Convert to snake\_case. ```sema (string/snake-case "helloWorld") ; => "hello_world" (string/snake-case "Hello World") ; => "hello_world" ``` ### `string/kebab-case` Convert to kebab-case. ```sema (string/kebab-case "helloWorld") ; => "hello-world" (string/kebab-case "Hello World") ; => "hello-world" ``` ### `string/camel-case` Convert to camelCase. ```sema (string/camel-case "hello_world") ; => "helloWorld" (string/camel-case "Hello World") ; => "helloWorld" ``` ### `string/pascal-case` Convert to PascalCase. ```sema (string/pascal-case "hello_world") ; => "HelloWorld" (string/pascal-case "hello world") ; => "HelloWorld" ``` ### `string/headline` Convert to Title Case headline. ```sema (string/headline "hello_world") ; => "Hello World" (string/headline "helloWorld") ; => "Hello World" ``` ### `string/words` Split an identifier into words. Breaks on `_`, `-`, spaces, and `.`, plus camelCase / acronym case transitions. Other punctuation stays attached. ```sema (string/words "hello_world") ; => ("hello" "world") (string/words "helloWorld") ; => ("hello" "World") ; case transition (string/words "Hello World!") ; => ("Hello" "World!") ; "!" is not a boundary ``` --- --- url: 'https://sema-lang.com/docs/stdlib/lists.md' --- # Lists Lists are the fundamental data structure in Sema. They are built from cons pairs and support a rich set of operations. ## Construction & Access ### `list` Create a new list. ```sema (list 1 2 3) ; => (1 2 3) (list) ; => () (list "a" "b") ; => ("a" "b") ``` ### `cons` Prepend an element to a list. ```sema (cons 0 '(1 2 3)) ; => (0 1 2 3) (cons 1 '()) ; => (1) ``` ### `car` Return the first element of a list. ```sema (car '(1 2 3)) ; => 1 ``` ### `cdr` Return the rest of a list (everything after the first element). ```sema (cdr '(1 2 3)) ; => (2 3) (cdr '(1)) ; => () ``` ::: details Where these names come from `car` and `cdr` are inherited from the [IBM 704](http://bitsavers.informatik.uni-stuttgart.de/pdf/ibm/704/24-6661-2_704_Manual_1955.pdf) (1955), the machine Lisp was originally implemented on. The 704 stored cons cells in a single 36-bit word, with two 15-bit pointer fields: the **address** field (bits 21-35) pointed to the first element, and the **decrement** field (bits 3-17) pointed to the rest of the list. `car` stands for "Contents of the Address Register" and `cdr` for "Contents of the Decrement Register" — they were single hardware instructions that extracted these sub-fields. Sema also provides `first`/`rest` as more readable aliases. ::: ### `first` Alias for `car`. Return the first element. ```sema (first '(1 2 3)) ; => 1 ``` ### `rest` Alias for `cdr`. Return the rest of the list. ```sema (rest '(1 2 3)) ; => (2 3) ``` ### `cadr`, `caddr`, ... Compositions of `car` and `cdr`. Available: `caar`, `cadr`, `cdar`, `cddr`, `caaar`, `caadr`, `cadar`, `caddr`, `cdaar`, `cdadr`, `cddar`, `cdddr`. ```sema (cadr '(1 2 3)) ; => 2 (caddr '(1 2 3)) ; => 3 ``` ### `last` Return the last element of a list. ```sema (last '(1 2 3)) ; => 3 ``` ### `nth` Return the element at index N (zero-based). ```sema (nth '(10 20 30) 1) ; => 20 (nth '(10 20 30) 0) ; => 10 ``` ## Association Lists ### `assoc` Look up a key in an association list (list of pairs). Uses `equal?` comparison. ```sema (define alist '(("a" 1) ("b" 2) ("c" 3))) (assoc "b" alist) ; => ("b" 2) (assoc "z" alist) ; => #f ``` ### `assq` Like `assoc` but uses `eq?` comparison (pointer/symbol equality). ```sema (assq 'b '((a 1) (b 2))) ; => (b 2) ``` ### `assv` Find the first pair whose key equals `key`. In Sema this compares by value, so `assv`, `assq`, and `assoc` all match structurally equal keys (including compound keys) — they are not distinguished by object identity the way Scheme's `eqv?`/`eq?` would be. ```sema (assv 2 '((1 "one") (2 "two"))) ; => (2 "two") ``` ## Basic Operations ### `length` Return the number of elements in a list. ```sema (length '(1 2 3)) ; => 3 (length '()) ; => 0 ``` ### `append` Concatenate lists. ```sema (append '(1 2) '(3 4)) ; => (1 2 3 4) (append '(1) '(2) '(3)) ; => (1 2 3) ``` ### `reverse` Reverse a list. ```sema (reverse '(1 2 3)) ; => (3 2 1) ``` ### `range` Generate a list of integers. With one argument, generates 0 to N-1. With two, generates from start to end-1. ```sema (range 5) ; => (0 1 2 3 4) (range 1 5) ; => (1 2 3 4) ``` ## Higher-Order Functions ### `map` Apply a function to each element of one or more lists. ```sema (map (fn (x) (* x x)) '(1 2 3)) ; => (1 4 9) (map + '(1 2 3) '(10 20 30)) ; => (11 22 33) ``` ### `map-indexed` Like `map`, but calls the function with the index and the element: `(f index element)`. ```sema (map-indexed (fn (i x) (list i x)) '(10 20 30)) ; => ((0 10) (1 20) (2 30)) ``` ### `enumerate` Pair each element with its 0-based index. ```sema (enumerate '(10 20 30)) ; => ((0 10) (1 20) (2 30)) ``` ### `filter` Return elements that satisfy a predicate. ```sema (filter even? '(1 2 3 4 5)) ; => (2 4) (filter string? '(1 "a" 2)) ; => ("a") ``` ### `foldl` Left fold. `(foldl f init list)` — accumulates from left to right. ```sema (foldl + 0 '(1 2 3 4 5)) ; => 15 (foldl cons '() '(1 2 3)) ; => (3 2 1) ``` ### `foldr` Right fold. `(foldr f init list)` — accumulates from right to left. ```sema (foldr cons '() '(1 2 3)) ; => (1 2 3) ``` ### `reduce` Like `foldl` but uses the first element as the initial value. ```sema (reduce + '(1 2 3 4 5)) ; => 15 ``` ### `for-each` Apply a function to each element for side effects. ```sema (for-each println '("a" "b" "c")) ;; prints: a, b, c (each on a new line) ``` ### `sort` Sort a list in ascending order. ```sema (sort '(3 1 4 1 5)) ; => (1 1 3 4 5) ``` ### `sort-by` Sort a list by a key function. ```sema (sort-by length '("bb" "a" "ccc")) ; => ("a" "bb" "ccc") (sort-by abs '(-3 1 -2)) ; => (1 -2 -3) ``` ### `flat-map` Map a function over a list and flatten the results by one level. ```sema (flat-map (fn (x) (list x (* x 10))) '(1 2 3)) ; => (1 10 2 20 3 30) ``` ### `apply` Apply a function to a list of arguments. ```sema (apply + '(1 2 3)) ; => 6 (apply max '(3 1 4)) ; => 4 ``` ## Sublists ### `take` Take the first N elements. ```sema (take 3 '(1 2 3 4 5)) ; => (1 2 3) (take 10 '(1 2)) ; => (1 2) ``` ### `drop` Drop the first N elements. ```sema (drop 2 '(1 2 3 4 5)) ; => (3 4 5) ``` ### `list/take-last` Take the last N elements (the tail counterpart to `take`). Clamps to the list length. ```sema (list/take-last 2 '(1 2 3 4)) ; => (3 4) (list/take-last 9 '(1 2)) ; => (1 2) ``` ### `list/drop-last` Drop the last N elements (drops from the tail; the counterpart to `drop`). Clamps to empty. ```sema (list/drop-last 2 '(1 2 3 4)) ; => (1 2) (list/drop-last 9 '(1 2)) ; => () ``` ### `flatten` Flatten nested lists into a single list. ```sema (flatten '(1 (2 (3)) 4)) ; => (1 2 3 4) ``` ### `flatten-deep` Recursively flatten all nested lists. ```sema (flatten-deep '(1 (2 (3 (4))))) ; => (1 2 3 4) ``` ### `zip` Combine corresponding elements from two lists into pairs. ```sema (zip '(1 2 3) '("a" "b" "c")) ; => ((1 "a") (2 "b") (3 "c")) ``` ### `partition` Split a list into two lists based on a predicate. Returns a list of two lists: elements that satisfy the predicate and those that don't. ```sema (partition even? '(1 2 3 4 5)) ; => ((2 4) (1 3 5)) ``` ## Searching ### `member` Return the tail of the list starting from the first matching element. ```sema (member 3 '(1 2 3 4)) ; => (3 4) (member 9 '(1 2 3)) ; => #f ``` ### `list/contains?` Return `#t` if the list contains the element, else `#f`. Unlike `member` (which returns the Scheme-style tail or `#f`), this reads as a predicate. ```sema (list/contains? '(1 2 3) 2) ; => #t (list/contains? '(1 2 3) 9) ; => #f ``` ### `list/nth-or` Indexed access with a fallback: returns the element at `index`, or `default` when out of bounds (the safe counterpart to `nth`, which errors). ```sema (list/nth-or '(10 20 30) 1 :none) ; => 20 (list/nth-or '(10 20 30) 9 :none) ; => :none ``` ### `any` Test if any element satisfies a predicate. ```sema (any even? '(1 3 5 6)) ; => #t (any even? '(1 3 5)) ; => #f ``` ### `every` Test if all elements satisfy a predicate. ```sema (every even? '(2 4 6)) ; => #t (every even? '(2 3 6)) ; => #f ``` ### `list/index-of` Return the index of the first occurrence of a value, or `nil` if not found. ```sema (list/index-of '(10 20 30) 20) ;; => 1 (list/index-of '(10 20 30) 99) ;; => nil ``` ### `list/unique` Remove duplicate elements, preserving order. ```sema (list/unique '(1 2 2 3 3 3)) ; => (1 2 3) ``` ### `list/dedupe` Remove consecutive duplicates from a list. ```sema (list/dedupe '(1 1 2 2 3 3 2)) ; => (1 2 3 2) ``` ## Grouping ### `list/group-by` Group elements by a function, returning a map. ```sema (list/group-by even? '(1 2 3 4 5)) ; => {#f (1 3 5) #t (2 4)} ``` ### `list/interleave` Interleave elements from two lists. ```sema (list/interleave '(1 2 3) '(a b c)) ; => (1 a 2 b 3 c) ``` ### `list/chunk` Split a list into chunks of a given size. ```sema (list/chunk 2 '(1 2 3 4 5)) ; => ((1 2) (3 4) (5)) (list/chunk 3 '(1 2 3 4 5 6)) ; => ((1 2 3) (4 5 6)) ``` ### `frequencies` Count occurrences of each element, returning a map. ```sema (frequencies '(a b a c b a)) ; => {a 3 b 2 c 1} ``` ### `interpose` Insert a separator between elements. ```sema (interpose ", " '("a" "b" "c")) ; => ("a" ", " "b" ", " "c") ``` ## Aggregation ### `list/sum` Sum all numbers in a list. ```sema (list/sum '(1 2 3 4 5)) ; => 15 ``` ### `list/min` Return the minimum value in a list. ```sema (list/min '(3 1 4 1 5)) ; => 1 ``` ### `list/max` Return the maximum value in a list. ```sema (list/max '(3 1 4 1 5)) ; => 5 ``` ## Random ### `list/shuffle` Return a randomly shuffled copy of a list. ```sema (list/shuffle '(1 2 3 4 5)) ; => (3 1 5 2 4) (varies) ``` ### `list/pick` Pick a random element from a list. ```sema (list/pick '(1 2 3 4 5)) ; => 3 (varies) ``` ## Construction ### `list/repeat` Create a list by repeating a value N times. ```sema (list/repeat 3 0) ; => (0 0 0) (list/repeat 4 "x") ; => ("x" "x" "x" "x") ``` ### `make-list` Alias for `list/repeat`. ```sema (make-list 3 0) ; => (0 0 0) ``` ### `iota` Generate a list of numbers. `(iota count)`, `(iota count start)`, or `(iota count start step)`. ```sema (iota 5) ; => (0 1 2 3 4) (iota 3 10) ; => (10 11 12) (iota 4 0 2) ; => (0 2 4 6) ``` ## Splitting ### `list/split-at` Split a list at a given index, returning two lists. ```sema (list/split-at '(1 2 3 4 5) 3) ; => ((1 2 3) (4 5)) ``` ### `list/take-while` Take elements from the front while a predicate holds. ```sema (list/take-while (fn (x) (< x 4)) '(1 2 3 4 5)) ; => (1 2 3) ``` ### `list/drop-while` Drop elements from the front while a predicate holds. ```sema (list/drop-while (fn (x) (< x 4)) '(1 2 3 4 5)) ; => (4 5) ``` ## Filtering ### `list/reject` Return elements that do NOT satisfy a predicate (inverse of `filter`). ```sema (list/reject even? '(1 2 3 4 5)) ; => (1 3 5) ``` ### `list/find` Return the first element that satisfies a predicate, or `nil` if none found. ```sema (list/find even? '(1 3 4 5 6)) ; => 4 (list/find even? '(1 3 5)) ; => nil ``` ### `list/sole` Return the single element matching a predicate. Errors if zero or more than one match. ```sema (list/sole (fn (x) (> x 4)) '(1 2 3 4 5)) ; => 5 ``` ## Set Operations ### `list/diff` Return elements in the first list that are not in the second list. ```sema (list/diff '(1 2 3 4 5) '(3 4)) ; => (1 2 5) ``` ### `list/intersect` Return elements present in both lists. ```sema (list/intersect '(1 2 3 4 5) '(3 4 6)) ; => (3 4) ``` ### `list/duplicates` Return values that appear more than once in a list. ```sema (list/duplicates '(1 2 2 3 3 3 4)) ; => (2 3) ``` ## Extraction ### `list/pluck` Extract a specific key from each map in a list. ```sema (define people (list {:name "Alice" :age 30} {:name "Bob" :age 25})) (list/pluck :name people) ; => ("Alice" "Bob") ``` ### `list/key-by` Transform a list of maps into a map keyed by a function result. ```sema (list/key-by (fn (p) (get p :id)) people) ; => map keyed by :id ``` ## Statistics ### `list/avg` Return the average of a numeric list. ```sema (list/avg '(2 4 6)) ; => 4.0 ``` ### `list/median` Return the statistical median. ```sema (list/median '(3 1 2)) ; => 2.0 (list/median '(1 2 3 4)) ; => 2.5 ``` ### `list/mode` Return the most frequent value. If tied, returns a list. ```sema (list/mode '(1 2 2 3 3 3)) ; => 3 (list/mode '(1 1 2 2)) ; => (1 2) ``` ## Windowing ### `list/sliding` Create a sliding window over a list. Optional step parameter. ```sema (list/sliding '(1 2 3 4 5) 2) ; => ((1 2) (2 3) (3 4) (4 5)) (list/sliding '(1 2 3 4 5 6) 2 3) ; => ((1 2) (4 5)) ``` ### `list/page` Paginate a list. `(list/page items page per-page)` — 1-indexed pages. ```sema (list/page (range 20) 1 5) ; => (0 1 2 3 4) (list/page (range 20) 2 5) ; => (5 6 7 8 9) ``` ### `list/cross-join` Cartesian product of two lists. ```sema (list/cross-join '(1 2) '(3 4)) ; => ((1 3) (1 4) (2 3) (2 4)) ``` ## Padding & Joining ### `list/pad` Pad a list to a target length with a fill value. ```sema (list/pad '(1 2 3) 5 0) ; => (1 2 3 0 0) ``` ### `list/join` Join list elements into a string. Optional final separator. ```sema (list/join '(1 2 3) ", ") ; => "1, 2, 3" (list/join '(1 2 3) ", " " and ") ; => "1, 2 and 3" ``` ## Generation ### `list/times` Generate a list by calling a function N times with the index (0-based). ```sema (list/times 5 (fn (i) (* i i))) ; => (0 1 4 9 16) ``` ## Utility ### `tap` Apply a side-effect function to a value, then return the original value. ```sema (tap 42 (fn (x) (println x))) ; prints 42, returns 42 ``` --- --- url: 'https://sema-lang.com/docs/stdlib/vectors.md' --- # Vectors Vectors are **indexed, immutable** collections written with square-bracket syntax. They're ideal when you want **O(1) indexed access** and a compact literal form. Most list functions also accept vectors, but some return lists even when passed a vector — see [Vectors vs Lists](#vectors-vs-lists) for details. ## Literal Syntax ```sema [1 2 3] ; vector of integers ["a" "b" "c"] ; vector of strings [] ; empty vector [1 [2 3] 4] ; nested vectors ``` ## Construction ### `vector` Create a vector from its arguments. ```sema (vector 1 2 3) ; => [1 2 3] (vector) ; => [] (vector "a" "b") ; => ["a" "b"] ``` ## Predicates & Introspection ### `vector?` Test whether a value is a vector. ```sema (vector? [1 2 3]) ; => #t (vector? '(1 2 3)) ; => #f (vector? 42) ; => #f ``` ### `length` / `count` / `empty?` Vectors participate in Sema's generic collection functions: ```sema (length [10 20 30]) ; => 3 (count [10 20 30]) ; => 3 (empty? []) ; => #t (empty? [1]) ; => #f ``` ## Indexed Access ### `nth` Return the element at index `n` (zero-based). Works on both lists and vectors. ```sema (nth [10 20 30] 0) ; => 10 (nth [10 20 30] 2) ; => 30 ``` Out of bounds is an error: ```sema (nth [10 20 30] 3) ; => error: index 3 out of bounds (length 3) ``` ::: tip Use `first` for safe "index 0" access — it returns `nil` on empty sequences. ::: ### `first` Return the first element of a vector (or list). Returns `nil` for empty vectors. ```sema (first [1 2 3]) ; => 1 (first []) ; => nil ``` ### `rest` Return everything after the first element. **Preserves type** — vector in, vector out. ```sema (rest [1 2 3]) ; => [2 3] (rest []) ; => [] (rest [1]) ; => [] ``` ## Conversion ### `vector->list` Convert a vector to a list. ```sema (vector->list [1 2 3]) ; => (1 2 3) (vector->list []) ; => () ``` ### `list->vector` Convert a list to a vector. ```sema (list->vector '(1 2 3)) ; => [1 2 3] (list->vector '()) ; => [] ``` ## Vectors vs Lists Both lists and vectors work as "sequences", but they're optimized for different things. ### When to use a vector * You need **fast indexed access** (`nth` is O(1) on vectors) * You have a **fixed-size** structure (e.g. `[x y]`, `[start end]`, `[status body]`) * You want compact literals for configuration-style data ### When to use a list * You're building data incrementally with `cons`, `append`, or recursion * You expect to process data head+tail style * You want idiomatic Lisp code-as-data ### Return type behavior Many sequence functions accept vectors but return lists: | Function | Vector in → | Type preserved? | |----------|-------------|-----------------| | `rest` | vector out | ✅ Yes | | `reverse` | vector out | ✅ Yes | | `map` | list out | ❌ No | | `filter` | list out | ❌ No | | `append` | list out | ❌ No | ```sema (map #(* % 2) [1 2 3]) ; => (2 4 6) — list! (reverse [1 2 3]) ; => [3 2 1] — vector (append [1 2] [3 4]) ; => (1 2 3 4) — list! ``` If you need a vector result after a transformation, convert at the end: ```sema (->> [1 2 3] (map #(* % 2)) (list->vector)) ; => [2 4 6] ``` ## Destructuring Sema supports **sequential destructuring** with a vector pattern in `let`, `define`, and function parameters. This works on both list and vector values. ### Exact destructuring ```sema (let (([x y] [10 20])) (+ x y)) ; => 30 (let (([x y] '(10 20))) (+ x y)) ; => 30 ``` ### Rest destructuring with `&` ```sema (let (([head second & tail] [1 2 3 4 5])) [head second tail]) ; => [1 2 (3 4 5)] ``` Note: `tail` is a **list**, not a vector. ### Nested destructuring ```sema (let (([a [b c] d] [1 [2 3] 4])) (+ a b c d)) ; => 10 ``` ## Pattern Matching `match` supports vector patterns: ```sema (define (describe-point p) (match p ([0 0] "origin") ([x 0] (string/append "on x-axis at " (number/to-string x))) ([0 y] (string/append "on y-axis at " (number/to-string y))) ([x y] (string/append "point " (number/to-string x) ", " (number/to-string y))))) (describe-point [0 0]) ; => "origin" (describe-point '(5 0)) ; => "on x-axis at 5" ``` ## Practical Examples ### Tuple-style returns Vectors are great for fixed-arity return values: ```sema (define (min-max xs) [(list/min xs) (list/max xs)]) (min-max '(3 1 4 1 5)) ; => [1 5] ``` ### Chunking and re-vectorizing ```sema (->> (range 10) (list/chunk 3) (map list->vector)) ; => ([0 1 2] [3 4 5] [6 7 8] [9]) ``` ## Performance | Operation | Complexity | |-----------|-----------| | `nth` | O(1) | | `first` | O(1) | | `rest` | O(n) — creates a new vector | | `length` | O(1) | Vectors are **immutable** — there is no `vector-set!`. To "update" a vector, construct a new one. --- --- url: 'https://sema-lang.com/docs/stdlib/maps.md' --- # Maps & HashMaps Sema provides two map types: sorted **maps** (BTreeMap-backed, deterministic ordering) and **hashmaps** (for O(1) performance-critical lookups). ## Maps Maps use curly-brace literal syntax with keyword keys: ```sema {:name "Ada" :age 36} ; map literal {:a 1 :b 2 :c 3} ; keywords as keys ``` Keywords are callable — when used as a function, they look up their value in a map: ```sema (:name {:name "Ada" :age 36}) ; => "Ada" ``` ### `map/new` Create a map from key-value pairs. ```sema (map/new :a 1 :b 2) ; => {:a 1 :b 2} ``` ### `get` Look up a value by key. Works on both maps and hashmaps. ```sema (get {:a 1 :b 2} :a) ; => 1 (get {:a 1 :b 2} :z) ; => nil ``` ### `assoc` Add or update a key-value pair, returning a new map. ```sema (assoc {:a 1} :b 2) ; => {:a 1 :b 2} (assoc {:a 1} :a 99) ; => {:a 99} ``` ### `dissoc` Remove a key, returning a new map. Works on both maps and hashmaps. ```sema (dissoc {:a 1 :b 2} :a) ; => {:b 2} (dissoc (hashmap/new :a 1 :b 2) :a) ; hashmap without :a ``` ### `merge` Merge multiple maps together. Later maps override earlier ones. Works on both maps and hashmaps — the result type matches the first argument. ```sema (merge {:a 1} {:b 2} {:c 3}) ; => {:a 1 :b 2 :c 3} (merge {:a 1} {:a 99}) ; => {:a 99} (merge (hashmap/new :a 1) {:b 2}) ; hashmap with :a and :b ``` ### `keys` Return the keys of a map as a list. ```sema (keys {:a 1 :b 2}) ; => (:a :b) ``` ### `vals` Return the values of a map as a list. ```sema (vals {:a 1 :b 2}) ; => (1 2) ``` ### `contains?` Test if a map contains a key. ```sema (contains? {:a 1} :a) ; => #t (contains? {:a 1} :b) ; => #f ``` ### `count` Return the number of key-value pairs. ```sema (count {:a 1 :b 2}) ; => 2 ``` ### `map/entries` Return the entries as a list of key-value pairs. ```sema (map/entries {:a 1 :b 2}) ; => ((:a 1) (:b 2)) ``` ### `map/from-entries` Create a map from a list of key-value pairs. ```sema (map/from-entries '((:a 1) (:b 2))) ; => {:a 1 :b 2} ``` ## Higher-Order Map Operations ### `map/map-vals` Apply a function to every value in a map. ```sema (map/map-vals (fn (v) (* v 2)) {:a 1 :b 2}) ; => {:a 2 :b 4} ``` ### `map/map-keys` Apply a function to every key in a map. ```sema (map/map-keys (fn (k) (string/to-keyword (string/upper (keyword/to-string k)))) {:a 1}) ; => {:A 1} ``` ### `map/filter` Filter entries by a predicate that takes key and value. ```sema (map/filter (fn (k v) (> v 1)) {:a 1 :b 2 :c 3}) ; => {:b 2 :c 3} ``` ### `map/select-keys` Select only the given keys from a map. ```sema (map/select-keys {:a 1 :b 2 :c 3} '(:a :c)) ; => {:a 1 :c 3} ``` ### `map/update` Update a value at a key by applying a function. ```sema (map/update {:a 1} :a (fn (v) (+ v 10))) ; => {:a 11} ``` ## HashMaps For performance-critical workloads with many keys, use `hashmap` for O(1) lookups instead of the sorted `map`. ### `hashmap/new` Create a new hashmap from key-value pairs. ```sema (hashmap/new :a 1 :b 2 :c 3) ; create a hashmap (hashmap/new) ; empty hashmap ``` ### `hashmap/get` Look up a value in a hashmap. ```sema (hashmap/get (hashmap/new :a 1) :a) ; => 1 ``` ### `hashmap/assoc` Add a key-value pair to a hashmap. ```sema (hashmap/assoc (hashmap/new) :a 1) ; hashmap with :a 1 ``` ### `hashmap/to-map` Convert a hashmap to a sorted map. ```sema (hashmap/to-map (hashmap/new :b 2 :a 1)) ; => {:a 1 :b 2} ``` ### `hashmap/keys` Return the keys of a hashmap (unordered). ```sema (hashmap/keys (hashmap/new :a 1 :b 2)) ; => (:a :b) ``` ### `hashmap/contains?` Test if a hashmap contains a key. ```sema (hashmap/contains? (hashmap/new :a 1) :a) ; => #t ``` ### Generic Operations on HashMaps The generic functions `get`, `assoc`, `dissoc`, `keys`, `vals`, `merge`, `count`, `contains?`, and all `map/*` higher-order operations also work on hashmaps, preserving the hashmap type: ```sema (get (hashmap/new :a 1 :b 2) :a) ; => 1 (assoc (hashmap/new) :x 42) ; hashmap with :x 42 (dissoc (hashmap/new :a 1 :b 2) :a) ; hashmap without :a (merge (hashmap/new :a 1) {:b 2}) ; hashmap with :a and :b (count (hashmap/new :a 1 :b 2)) ; => 2 (map/map-vals (fn (v) (* v 2)) (hashmap/new :a 1)) ; hashmap with :a 2 (map/filter (fn (k v) (> v 1)) (hashmap/new :a 1 :b 2)) ; hashmap with :b ``` ### `map/sort-keys` Sort a map by its keys. Converts hashmaps to sorted maps. ```sema (map/sort-keys (hashmap/new :c 3 :a 1 :b 2)) ; => {:a 1 :b 2 :c 3} ``` ### `map/except` Remove specified keys from a map (inverse of `map/select-keys`). ```sema (map/except {:a 1 :b 2 :c 3} '(:b)) ; => {:a 1 :c 3} (map/except {:a 1 :b 2 :c 3} '(:a :c)) ; => {:b 2} ``` ### `map/zip` Create a map from a list of keys and a list of values. ```sema (map/zip '(:a :b :c) '(1 2 3)) ; => {:a 1 :b 2 :c 3} ``` ## Nested Map Operations ### `map/get-in` Access a value at a nested key path. Returns `nil` (or a default) if any key is missing. ```sema (map/get-in {:a {:b {:c 42}}} [:a :b :c]) ; => 42 (map/get-in {:a {:b 1}} [:a :c]) ; => nil (map/get-in {:a {:b 1}} [:a :c] "default") ; => "default" ``` ### `map/assoc-in` Set a value at a nested key path. Creates intermediate maps if they don't exist. ```sema (map/assoc-in {:a {:b 1}} [:a :b] 42) ; => {:a {:b 42}} (map/assoc-in {} [:a :b :c] 99) ; => {:a {:b {:c 99}}} ``` ### `map/update-in` Update a value at a nested key path by applying a function. ```sema (map/update-in {:a {:b 10}} [:a :b] #(+ % 1)) ; => {:a {:b 11}} ``` ### `map/deep-merge` Recursively merge maps. Nested maps are merged rather than replaced. Non-map values in the overlay override the base. ```sema (map/deep-merge {:a {:b 1 :c 2}} {:a {:b 99}}) ; => {:a {:b 99 :c 2}} (map/deep-merge {:a {:b 1}} {:a 42}) ; => {:a 42} (map/deep-merge {:a 1} {:b 2} {:c 3}) ; => {:a 1 :b 2 :c 3} ``` --- --- url: 'https://sema-lang.com/docs/stdlib/predicates.md' --- # Predicates & Type Checking Predicates return `#t` or `#f` and conventionally end with `?`. ## Emptiness Predicates These three predicates overlap but are not interchangeable. `null?` returns `#t` for both `'()` and `nil` — it tests for "absence of a value or empty list". `nil?` is true only for the `nil` value itself (not for `'()`). `empty?` is the broadest: it accepts `nil`, strings, lists, vectors, maps, and other collections, returning `#t` when the value has no elements. Reach for `empty?` when you have a collection of any shape; reach for `nil?` when you specifically need to distinguish `nil` from `'()`. ### `null?` Test if a value is the empty list or `nil`. ```sema (null? '()) ;; => #t (null? nil) ;; => #t (null? '(1)) ;; => #f ``` ### `nil?` Test if a value is `nil` specifically (not the empty list). ```sema (nil? nil) ;; => #t (nil? '()) ;; => #f (nil? 0) ;; => #f ``` ### `empty?` Test if a collection, string, or `nil` is empty. Accepts strings, lists, vectors, maps, and `nil`. ```sema (empty? "") ;; => #t (empty? '()) ;; => #t (empty? nil) ;; => #t (empty? "hello") ;; => #f (empty? [1 2 3]) ;; => #f ``` ## Collection Predicates ### `list?` Test if a value is a list. ```sema (list? '(1)) ; => #t (list? 42) ; => #f ``` ### `pair?` Test if a value is a non-empty list (Scheme compatibility). ```sema (pair? '(1 2)) ; => #t (pair? '()) ; => #f ``` ### `vector?` Test if a value is a vector. ```sema (vector? [1]) ; => #t (vector? '(1)) ; => #f ``` ### `map?` Test if a value is a map. ```sema (map? {:a 1}) ; => #t (map? '()) ; => #f ``` ## Numeric Predicates Sema implements the full R7RS [numeric tower](/docs/stdlib/math#the-numeric-tower), so two families of predicates apply to numbers: **type/level** tests (`number?`, `integer?`, `rational?`, `real?`, `complex?`, `float?`) and **exactness** tests (`exact?`, `inexact?`, `exact-integer?`). The type predicates nest — every integer is rational, every rational is real, every real is complex — so `complex?` is the widest and true for *all* numbers. ### `number?` Test if a value is a number — anything in the tower (integer, bignum, rational, float, or complex). Equivalent to [`complex?`](#complex). ```sema (number? 42) ; => #t (number? 3.14) ; => #t (number? 1/3) ; => #t (number? "42") ; => #f ``` ### `integer?` Test if a value is an integer, per R7RS: true for any exact integer (including bignums) **and** for an integer-valued float like `3.0`. A float with a fractional part is not an integer. To exclude integer-valued floats, use [`exact-integer?`](#exact-integer); to test representation, use [`float?`](#float). ```sema (integer? 42) ; => #t (integer? 3.14) ; => #f (integer? 3.0) ; => #t ; integer-valued float ``` ### `rational?` Test if a number is rational — exact and expressible as a ratio of two integers. Every exact integer and exact rational qualifies; floats and non-real complex numbers do not. (This tracks *exactness*, so it is stricter than strict R7RS where a finite float is also rational.) ```sema (rational? 42) ; => #t (rational? 1/3) ; => #t (rational? 3.14) ; => #f (rational? 3+4i) ; => #f ``` ### `real?` Test if a number is real — has no non-zero imaginary part. Every integer, rational, and float is real; `real?` is false *only* for a complex with a genuine imaginary component. A complex whose imaginary part is exact zero collapses to a real, so `3+0i` is real. ```sema (real? 42) ; => #t (real? 3.14) ; => #t (real? 3+4i) ; => #f (real? 3+0i) ; => #t ``` ### `complex?` Test if a value is a number. In R7RS the number types nest, so `complex?` is true for *every* number in the tower and false only for non-numbers. It is the widest numeric predicate. ```sema (complex? 42) ; => #t (complex? 3.14) ; => #t (complex? 3+4i) ; => #t (complex? "hi") ; => #f ``` ### `float?` Test if a value is a floating-point number. ```sema (float? 3.14) ; => #t (float? 42) ; => #f ``` ### `exact?` Test if a number is exact — represented without floating point. Exact numbers are integers, exact rationals, and complex numbers whose parts are both exact. The complement of [`inexact?`](#inexact) on numbers. ```sema (exact? 42) ; => #t (exact? 1/3) ; => #t (exact? 3.14) ; => #f (exact? 3+4i) ; => #t (exact? 3.0+4i) ; => #f ``` ### `inexact?` Test if a number is inexact — carries a floating-point component. True for any float and for any complex with at least one inexact part. The complement of [`exact?`](#exact) on numbers. ```sema (inexact? 42) ; => #f (inexact? 3.14) ; => #t (inexact? 1/3) ; => #f (inexact? 3.0+4i) ; => #t ``` ### `exact-integer?` Test if a value is an exact integer — true exactly when both `exact?` and `integer?` hold. Stricter than a bare `integer?`: `2.0` is an integer value but inexact, so it fails. ```sema (exact-integer? 42) ; => #t (exact-integer? 1/2) ; => #f (exact-integer? 2.0) ; => #f (exact-integer? 3+0i) ; => #t ``` ### `zero?` Test if a number is zero. ```sema (zero? 0) ; => #t (zero? 1) ; => #f ``` ### `even?` Test if an integer is even. ```sema (even? 4) ; => #t (even? 3) ; => #f ``` ### `odd?` Test if an integer is odd. ```sema (odd? 3) ; => #t (odd? 4) ; => #f ``` ### `positive?` Test if a number is positive. ```sema (positive? 1) ; => #t (positive? -1) ; => #f ``` ### `negative?` Test if a number is negative. ```sema (negative? -1) ; => #t (negative? 1) ; => #f ``` ## Type Predicates ### `string?` Test if a value is a string. ```sema (string? "hi") ; => #t (string? 42) ; => #f ``` ### `symbol?` Test if a value is a symbol. ```sema (symbol? 'x) ; => #t (symbol? "x") ; => #f ``` ### `keyword?` Test if a value is a keyword. ```sema (keyword? :k) ; => #t (keyword? "k") ; => #f ``` ### `char?` Test if a value is a character. ```sema (char? #\a) ; => #t (char? "a") ; => #f ``` ### `bool?` Test if a value is a boolean. `boolean?` is an alias. ```sema (bool? #t) ; => #t (bool? 0) ; => #f ``` ### `fn?` Test if a value is a function. `procedure?` is an alias. ```sema (fn? car) ; => #t (fn? 42) ; => #f ``` ### `record?` Test if a value is a record instance. ```sema (record? my-record) ; => #t (record? 42) ; => #f ``` ### `bytevector?` Test if a value is a bytevector. ```sema (bytevector? #u8()) ; => #t (bytevector? '()) ; => #f ``` ## Promise Predicates ### `promise?` Test if a value is a promise (created with `delay`). ```sema (promise? (delay 1)) ; => #t (promise? 42) ; => #f ``` ### `promise-forced?` Test if a promise has been forced (evaluated). ```sema (define p (delay (+ 1 2))) (promise-forced? p) ; => #f (force p) (promise-forced? p) ; => #t ``` ## Equality ### `eq?` Test structural equality. `equal?` is an alias. ```sema (eq? 'a 'a) ; => #t (eq? '(1 2) '(1 2)) ; => #t (eq? 1 2) ; => #f ``` ### `=` Equality. For numbers this is numeric equality (so `(= 1 1.0)` is `#t`); for non-numbers it falls back to structural equality. Unlike `<` / `>`, comparing non-numbers does not error. ```sema (= 1 1) ; => #t (= 1 1.0) ; => #t (= 1 2) ; => #f (= "abc" "abc") ; => #t (structural, not an error) ``` ## LLM Type Predicates ### `prompt?` Test if a value is an LLM prompt. ```sema (prompt? (prompt (user "hi"))) ; => #t ``` ### `message?` Test if a value is an LLM message. ```sema (message? (message :user "hi")) ; => #t ``` ### `conversation?` Test if a value is a conversation. ```sema (conversation? (conversation/new {})) ; => #t ``` ### `tool?` Test if a value is a tool definition. ```sema (deftool my-tool "A test tool" {:x {:type :string}} (lambda (x) x)) (tool? my-tool) ; => #t (tool? 42) ; => #f ``` ### `agent?` Test if a value is an agent. ```sema (defagent my-agent {:system "test"}) (agent? my-agent) ; => #t (agent? 42) ; => #f ``` --- --- url: 'https://sema-lang.com/docs/stdlib/bytevectors.md' --- # Bytevectors Bytevectors are sequences of unsigned 8-bit integers (0–255), useful for binary data and string encoding. ## Literal Syntax ```sema #u8(1 2 3) ; bytevector literal #u8() ; empty bytevector #u8(255 0 128) ; arbitrary byte values ``` ## Construction ### `bytevector` Create a bytevector from byte values. ```sema (bytevector 1 2 3) ; => #u8(1 2 3) (bytevector) ; => #u8() ``` ### `bytevector/new` Create a bytevector of a given length, optionally filled with a value. ```sema (bytevector/new 4) ; => #u8(0 0 0 0) (bytevector/new 3 255) ; => #u8(255 255 255) ``` ## Access & Mutation ### `bytevector/length` Return the length of a bytevector. ```sema (bytevector/length #u8(1 2 3)) ; => 3 (bytevector/length #u8()) ; => 0 ``` ### `bytevector/ref` Return the byte at a given index. ```sema (bytevector/ref #u8(10 20 30) 1) ; => 20 (bytevector/ref #u8(10 20 30) 0) ; => 10 ``` ### `bytevector/set!` Set the byte at a given index. Uses copy-on-write — the original bytevector is unchanged. ```sema (bytevector/set! #u8(1 2 3) 0 9) ; => #u8(9 2 3) ``` ## Copy & Append ### `bytevector/copy` Copy a slice of a bytevector. `(bytevector/copy bv start end)`. ```sema (bytevector/copy #u8(1 2 3 4 5) 1 3) ; => #u8(2 3) ``` ### `bytevector/append` Concatenate bytevectors. ```sema (bytevector/append #u8(1 2) #u8(3 4)) ; => #u8(1 2 3 4) ``` ## List Conversion ### `bytevector/to-list` Convert a bytevector to a list of integers. ```sema (bytevector/to-list #u8(65 66)) ; => (65 66) ``` ### `list/to-bytevector` Convert a list of integers to a bytevector. ```sema (list/to-bytevector '(1 2 3)) ; => #u8(1 2 3) ``` ## String Conversion ### `utf8/to-string` Decode a bytevector as a UTF-8 string. ```sema (utf8/to-string #u8(104 105)) ; => "hi" (utf8/to-string #u8(72 101 108)) ; => "Hel" ``` ### `string/to-utf8` Encode a string as a UTF-8 bytevector. ```sema (string/to-utf8 "hi") ; => #u8(104 105) (string/to-utf8 "Hello") ; => #u8(72 101 108 108 111) ``` ## Byte-Oriented Operations The `bytes/*` family is built for byte-oriented hot loops — parse-heavy pipelines (like scanning millions of `Name;-12.3` lines) that skip UTF-8 work until text is actually needed. Where noted, functions accept an optional `start`/`end` byte range (`start` inclusive, `end` exclusive, defaulting to the length), so a loop can read a sub-range in place instead of allocating a copy with `bytes/slice`. These compose with [`file/fold-lines-bytes`](/docs/stdlib/file-io#file-fold-lines-bytes) for allocation-light file scans. ### `bytes/length` Return the length of a bytevector in bytes. Same result as `bytevector/length`. ```sema (bytes/length (string/to-utf8 "abc")) ; => 3 ``` ### `bytes/ref` Return the byte (0–255) at a zero-based index. Out of bounds is an error. ```sema (bytes/ref (string/to-utf8 "abc") 1) ; => 98 ``` ### `bytes/find` Find the first occurrence of a needle: a memchr-style byte search. The needle is a single byte (int 0–255), a bytevector, or a string (searched as its UTF-8 bytes). Returns the absolute byte index, or `nil` when absent. The optional `start` offset resumes a scan without slicing. ```sema (bytes/find (string/to-utf8 "Oslo;-12.3") 59) ; => 4 (the ';' byte) (bytes/find (string/to-utf8 "hello") "llo") ; => 2 (bytes/find (string/to-utf8 "a;b;c") 59 2) ; => 3 (bytes/find (string/to-utf8 "abc") 59) ; => nil ``` ### `bytes/slice` Copy the byte range `start..end` out of a bytevector. Indices are plain byte offsets — no UTF-8 validation or char-boundary rules, unlike `substring`. ```sema (bytes/slice (string/to-utf8 "hello") 1 3) ; => #u8(101 108) (bytes/slice (string/to-utf8 "hello") 3) ; => #u8(108 111) ``` In hot loops, prefer the optional `start`/`end` arguments of `bytes/find`, `bytes/->string`, and `bytes/parse-int10` — they read the same range without this copy. ### `bytes/->string` Decode a bytevector — or just the `start..end` range of it — as a UTF-8 string. Invalid UTF-8 is an error, like `utf8/to-string`. ```sema (bytes/->string (string/to-utf8 "Oslo;-12.3") 0 4) ; => "Oslo" (bytes/->string (string/to-utf8 "abc")) ; => "abc" ``` ### `bytes/parse-int10` Parse ASCII `-?digits(.digit)?` as a base-10 integer scaled by 10: `"-12.3"` → `-123`, `"5"` → `50`. This is the fixed-point trick for one-decimal measurements — the value times ten as an exact int, with no float math or string allocation. At most one fractional digit is accepted; anything else (empty input, stray characters, more decimals) is an error. The optional `start`/`end` range parses a sub-slice in place. ```sema (bytes/parse-int10 (string/to-utf8 "-12.3")) ; => -123 (bytes/parse-int10 (string/to-utf8 "5")) ; => 50 (bytes/parse-int10 (string/to-utf8 "Oslo;-12.3") 5) ; => -123 ``` --- --- url: 'https://sema-lang.com/docs/stdlib/typed-arrays.md' --- # Typed Arrays Typed arrays provide contiguous, unboxed numeric storage for performance-critical workloads. Unlike regular lists (which NaN-box every element), typed arrays store raw `f64` or `i64` values in a flat `Vec`, giving better cache locality and avoiding per-element boxing overhead. Two types are available: * **`f64-array`** — 64-bit floating-point arrays * **`i64-array`** — 64-bit signed integer arrays Both support copy-on-write mutation via `Rc::make_mut`. ## Construction ### `f64-array` Create an f64 array from values. ```sema (f64-array 1.0 2.5 3.7) ; => #f64(1 2.5 3.7) (f64-array) ; => #f64() ``` ### `i64-array` Create an i64 array from values. ```sema (i64-array 1 2 3) ; => #i64(1 2 3) (i64-array) ; => #i64() ``` ### `f64-array/make` Create an f64 array of a given length, optionally filled with a value (default `0.0`). ```sema (f64-array/make 5) ; => #f64(0 0 0 0 0) (f64-array/make 3 1.5) ; => #f64(1.5 1.5 1.5) ``` ### `i64-array/make` Create an i64 array of a given length, optionally filled with a value (default `0`). ```sema (i64-array/make 5) ; => #i64(0 0 0 0 0) (i64-array/make 3 42) ; => #i64(42 42 42) ``` ### `f64-array/range` Create an f64 array from a numeric range. `(f64-array/range start end)` or `(f64-array/range start end step)`. ```sema (f64-array/range 0 5) ; => #f64(0 1 2 3 4) (f64-array/range 0 1 0.25) ; => #f64(0 0.25 0.5 0.75) ``` ### `i64-array/range` Create an i64 array from an integer range. ```sema (i64-array/range 0 5) ; => #i64(0 1 2 3 4) (i64-array/range 0 10 2) ; => #i64(0 2 4 6 8) ``` ### `f64-array/from-list` Convert a list of numbers to an f64 array. ```sema (f64-array/from-list '(1 2 3)) ; => #f64(1 2 3) ``` ### `i64-array/from-list` Convert a list of integers to an i64 array. ```sema (i64-array/from-list '(10 20 30)) ; => #i64(10 20 30) ``` ## Access & Mutation ### `f64-array/ref` / `i64-array/ref` Get the element at a given index. ```sema (f64-array/ref (f64-array 1.0 2.0 3.0) 1) ; => 2.0 (i64-array/ref (i64-array 10 20 30) 0) ; => 10 ``` ### `f64-array/set!` / `i64-array/set!` Set the element at a given index. Uses copy-on-write -- the original array is unchanged unless it has a single reference. ```sema (f64-array/set! (f64-array 1.0 2.0 3.0) 1 9.9) ; => #f64(1 9.9 3) (i64-array/set! (i64-array 10 20 30) 2 99) ; => #i64(10 20 99) ``` ### `f64-array/length` / `i64-array/length` Return the number of elements. ```sema (f64-array/length (f64-array 1.0 2.0 3.0)) ; => 3 (i64-array/length (i64-array/make 10)) ; => 10 ``` ## Aggregation ### `f64-array/sum` / `i64-array/sum` Sum all elements. Runs in a tight Rust loop with no boxing overhead. ```sema (f64-array/sum (f64-array 1.0 2.0 3.0)) ; => 6.0 (i64-array/sum (i64-array 1 2 3 4 5)) ; => 15 ``` ### `f64-array/dot` Compute the dot product of two f64 arrays (must be the same length). ```sema (f64-array/dot (f64-array 1.0 2.0 3.0) (f64-array 4.0 5.0 6.0)) ; => 32.0 (1*4 + 2*5 + 3*6) ``` ## Higher-Order Functions ### `f64-array/map` / `i64-array/map` Apply a function to each element, returning a new typed array. The callback must return the matching numeric type. ```sema (f64-array/map (lambda (x) (* x 2.0)) (f64-array 1.0 2.0 3.0)) ; => #f64(2 4 6) (i64-array/map (lambda (x) (* x x)) (i64-array 1 2 3 4)) ; => #i64(1 4 9 16) ``` ### `f64-array/fold` / `i64-array/fold` Fold over a typed array with an accumulator. ```sema (f64-array/fold (lambda (acc x) (+ acc x)) 0.0 (f64-array 1.0 2.0 3.0)) ; => 6.0 (i64-array/fold (lambda (acc x) (max acc x)) 0 (i64-array 3 1 4 1 5)) ; => 5 ``` ## Type Predicates ### `f64-array?` / `i64-array?` Test whether a value is a typed array. ```sema (f64-array? (f64-array 1.0 2.0)) ; => #t (f64-array? '(1.0 2.0)) ; => #f (i64-array? (i64-array 1 2)) ; => #t ``` ## Examples ### Embedding similarity with dot product ```sema ;; Compute cosine similarity between two embedding vectors (define (cosine-similarity a b) (let ((dot (f64-array/dot a b)) (mag-a (sqrt (f64-array/dot a a))) (mag-b (sqrt (f64-array/dot b b)))) (/ dot (* mag-a mag-b)))) (define v1 (f64-array 1.0 0.0 0.0)) (define v2 (f64-array 0.707 0.707 0.0)) (cosine-similarity v1 v2) ; => ~0.707 ``` ### Numeric computation ```sema ;; Sum of squares of even numbers from 0 to 99 (define nums (i64-array/range 0 100)) (define evens (i64-array/map (lambda (x) (if (even? x) (* x x) 0)) nums)) (i64-array/sum evens) ; => 161700 ``` --- --- url: 'https://sema-lang.com/docs/stdlib/mutable.md' --- # Mutable Containers Sema's default collections — lists, vectors, maps — are **immutable**: every "update" produces a new value (copy-on-write), and that is the right default for almost all code. Mutable containers are the imperative escape hatch for the cases where copy-on-write dominates the runtime: hot accumulation loops, in-place statistics, a counter threaded through callbacks. There are two of them: * **`mutable-array`** — an in-place mutable array of values (Janet-style) * **`mutable-cell`** — a single in-place mutable slot holding one value (a boxed value) Both are **shared by reference**: mutating through one binding is visible through every other binding to the same container. When the loop is done, freeze the result with `mutable-array/->vector` to hand data back to the immutable world. ## When to Reach for Them Prefer the immutable structures plus `foldl`/`reduce` by default — they are safe to share, safe to use as map keys, and fast enough for typical workloads. Reach for a mutable container when: * **A hot loop accumulates into a growing collection.** Pushing onto a `mutable-array` is a true in-place append; rebuilding a persistent vector per element copies. * **You update a fixed set of slots millions of times.** In-place stats like `[min max sum count]` per key: `mutable-array/set!` overwrites one slot with no copy. * **A running value must survive across callbacks.** A `mutable-cell` gives a counter or "best so far" that side-effecting callbacks (`for-each`, event handlers) can update without rebuilding a container per event. ## Semantics **Reference sharing** — mutable containers are heap values shared by reference. Passing one to a function passes the *same* container; mutations are visible to every holder. **Equality and ordering are content-based.** `equal?` compares elements (with an identity fast path — a container is trivially equal to itself), and ordering functions like `sort` order by contents, just like vectors. The comparison is cycle-safe: an array that contains itself compares without hanging. ```sema (define a (mutable-array/new)) (define b (mutable-array/new)) (mutable-array/push! a 1) (mutable-array/push! b 1) (equal? a b) ; => #t — same contents ``` **Mutable containers cannot be map keys.** Map keys must be deeply immutable, and the check is deep — a vector *containing* a mutable array is rejected too, because the key could still mutate underneath the map and corrupt its ordering. ```sema (assoc {} (mutable-array/new) 1) ; => error: expected immutable map key, got mutable-array ; hint: freeze the key first (mutable-array/->vector or mutable-cell/get) ``` Mutable containers as map *values* are fine — that is the standard pattern for per-key accumulators (see the example below). **Printing shows length only** — a mutable array prints as `` (it can contain itself, so contents are not printed) and a cell as ``. Freeze with `mutable-array/->vector` when you want to see or return the elements. **`nth` works on mutable arrays**, like it does on lists and vectors. ## Mutable Arrays ### `mutable-array/new` Create a mutable array. With no arguments it is empty; with one argument it is still empty but pre-allocates capacity for that many pushes; with two arguments it holds `n` copies of `fill`, ready for indexed `mutable-array/set!`. ```sema (mutable-array/new) ; empty (mutable-array/new 1024) ; empty, capacity for 1024 pushes (mutable-array/new 3 0) ; three zeros: contents [0 0 0] ``` ### `mutable-array/push!` Append a value to the end, in place. Returns the array itself, so pushes chain and work as the accumulator of a fold. ```sema (define a (mutable-array/new)) (mutable-array/push! (mutable-array/push! a 1) 2) (mutable-array/->vector a) ; => [1 2] ;; As a fold accumulator: (mutable-array/->vector (foldl (fn (acc x) (mutable-array/push! acc (* x x))) (mutable-array/new) '(1 2 3))) ; => [1 4 9] ``` ### `mutable-array/get` Read the element at a zero-based index. Out of bounds is an error unless a default is supplied. ```sema (define a (mutable-array/new 2 :x)) (mutable-array/get a 1) ; => :x (mutable-array/get a 9 :missing) ; => :missing ``` ### `mutable-array/set!` Overwrite the element at a zero-based index, in place. The slot must already exist (`index < length`) — use `mutable-array/push!` to grow. Returns the array. Unlike `vector` updates, no copy is made: every binding to the array sees the new value. ```sema (define stats (mutable-array/new 4 0)) ; [min max sum count] accumulator (mutable-array/set! stats 2 (+ (mutable-array/get stats 2) 57)) (mutable-array/->vector stats) ; => [0 0 57 0] ``` ### `mutable-array/length` Return the number of elements currently in the array (not its capacity). ```sema (mutable-array/length (mutable-array/new 64)) ; => 0 (capacity only) (mutable-array/length (mutable-array/new 3 :x)) ; => 3 ``` ### `mutable-array/->vector` Freeze a mutable array into an immutable vector — a snapshot copy: later mutation of the array does not change the returned vector. This is the hand-off point from an imperative accumulation loop back to the persistent world (sortable, printable, usable as map values or keys). ```sema (define a (mutable-array/new)) (mutable-array/push! a 1) (define v (mutable-array/->vector a)) (mutable-array/set! a 0 9) v ; => [1] — the snapshot is unaffected ``` ## Mutable Cells ### `mutable-cell/new` Create a mutable cell holding one value. ```sema (define counter (mutable-cell/new 0)) ``` ### `mutable-cell/get` Read the current contents of a cell. ```sema (mutable-cell/get (mutable-cell/new :ready)) ; => :ready ``` ### `mutable-cell/set!` Replace the contents of a cell, in place. Returns the cell. Every binding to the cell sees the new value. ```sema (define counter (mutable-cell/new 0)) (mutable-cell/set! counter (+ 1 (mutable-cell/get counter))) (mutable-cell/get counter) ; => 1 ``` ## Example: Per-Key Stats in a Fold The canonical use case: fold over a large file of `Name;-12.3` measurement lines, keeping `[min max sum count]` per station. The map is immutable, but each station's stats live in one mutable array that is updated in place — the map itself only changes when a *new* station appears. Combined with [`file/fold-lines-bytes`](/docs/stdlib/file-io#file-fold-lines-bytes) and [`bytes/*`](/docs/stdlib/bytevectors#byte-oriented-operations) parsing, the per-line work allocates almost nothing. ```sema ;; [min max sum count], all ints ×10 — one allocation per station. (define (stats-new x) (mutable-array/set! (mutable-array/new 4 x) 3 1)) ; set! returns the array (define (stats-add! s x) (mutable-array/set! s 0 (min (mutable-array/get s 0) x)) (mutable-array/set! s 1 (max (mutable-array/get s 1) x)) (mutable-array/set! s 2 (+ (mutable-array/get s 2) x)) (mutable-array/set! s 3 (+ (mutable-array/get s 3) 1))) (define stats (file/fold-lines-bytes "measurements.txt" (fn (acc line) (let* ((semi (bytes/find line 59)) ; 59 = ';' (name (bytes/->string line 0 semi)) (temp (bytes/parse-int10 line (+ semi 1))) (s (get acc name))) (if (nil? s) (assoc acc name (stats-new temp)) ; new station: map grows once (begin (stats-add! s temp) acc)))) ; known station: mutate in place {})) ;; Freeze for the immutable world before printing or returning. (map/map-vals mutable-array/->vector stats) ; => {"Bergen" [59 59 59 1] "Oslo" [-123 30 -93 2]} ``` ## Example: Counting Across Callbacks A `mutable-cell` threads a running value through side-effecting iteration without rebuilding anything per step: ```sema (define matches (mutable-cell/new 0)) (file/for-each-line "app.log" (fn (line) (when (string/contains? line "ERROR") (mutable-cell/set! matches (+ 1 (mutable-cell/get matches)))))) (mutable-cell/get matches) ; => number of ERROR lines ``` --- --- url: 'https://sema-lang.com/docs/stdlib/file-io.md' --- # File I/O & Paths ::: tip Sandbox capabilities File operations work without extra configuration in Sema's default mode. In a sandboxed run, reads, listings, predicates, file watching, and paths that access the filesystem require `fs-read` (`FS_READ`). Writes, deletes, renames, copies, and directory creation require `fs-write` (`FS_WRITE`). A denied operation returns `PermissionDenied`. See the [CLI sandbox documentation](/docs/cli#sandbox) for the full function list. ::: ## Console I/O ### `display` Print a value without a trailing newline. ```sema (display "no newline") (display 42) ``` ### `println` Print a value followed by a newline. ```sema (println "with newline") (println 42) ``` ### `print` Write values in read-syntax form (strings are quoted) like Scheme's `write`. No trailing newline. Use `display` for human-readable output without quotes. ```sema (print "hello") ;; outputs: "hello" (display "hello") ;; outputs: hello ``` ### `io/print-error` Print to stderr without a trailing newline. ```sema (io/print-error "warning: something happened") ``` ### `io/println-error` Print to stderr with a trailing newline. ```sema (io/println-error "error: file not found") ``` ### `newline` Print a newline character. ```sema (newline) ``` ### `io/read-line` Read a line of input from stdin (trailing `\n` / `\r\n` stripped). ```sema (define name (io/read-line)) ``` Returns `nil` when stdin is closed (Ctrl-D in cooked mode, end of a piped file). Use this to distinguish "user pressed Enter on an empty line" (returns `""`) from "stdin is exhausted" (returns `nil`). ```sema (let loop () (let ((line (io/read-line))) (cond ((nil? line) (println "(eof)")) ((= line "") (loop)) ; blank line, keep reading (else (println "got: " line) (loop))))) ``` ::: warning Breaking change in 1.14.0 Previously `io/read-line` returned `""` on both EOF and empty input, making them indistinguishable. It now returns `nil` on EOF. If you don't want to refactor for this, use `io/eof?` after the call instead. ::: ### `io/read-stdin` Read all of stdin as a string (until EOF). ```sema (define input (io/read-stdin)) ``` ### `io/eof?` Return `#t` after any stdin read (`io/read-line`, `io/read-stdin`, `io/read-key`) has signalled EOF. Non-breaking alternative to checking `io/read-line` for `nil`. ```sema (define line (io/read-line)) (when (io/eof?) (println "stdin closed")) ``` ### `io/flush` Flush stdout. Useful when writing a prompt without a trailing newline before reading input. ```sema (display "name> ") (io/flush) (define name (io/read-line)) ``` ## File Operations ### `file/read` Read the entire contents of a file as a string. ```sema (file/read "data.txt") ; => "file contents..." ``` ### `file/write` Write a string to a file, overwriting any existing content. ```sema (file/write "out.txt" "content") ``` ### `file/append` Append a string to a file. ```sema (file/append "log.txt" "new line\n") ``` ### `file/read-lines` Read a file as a list of lines. Handles both `\n` and `\r\n` line endings. An empty file returns an empty list. ```sema (file/read-lines "data.txt") ; => ("line 1" "line 2" "line 3") (file/read-lines "empty.txt") ; => () ``` ### `file/write-lines` Write a list of strings to a file, one per line. ```sema (file/write-lines "out.txt" '("a" "b" "c")) ``` ### `file/for-each-line` Iterate over lines of a file, calling a function on each line. Memory-efficient for large files. The streaming line operations use a 64 KiB read buffer and bounded batches. Each line may contain at most 256 KiB of content. A trailing `\n` or `\r\n` does not count toward the limit; a longer line raises an error. ```sema (file/for-each-line "data.txt" (fn (line) (println line))) ``` ### `file/fold-lines` Fold over lines of a file with an accumulator. ```sema (file/fold-lines "data.csv" (fn (acc line) (+ acc 1)) 0) ; => number of lines ``` ### `file/fold-lines-bytes` Like `file/fold-lines`, but the reducer receives each line as a **bytevector** — trailing `\n` / `\r\n` stripped, no UTF-8 validation. Built for [`bytes/*`](/docs/stdlib/bytevectors#byte-oriented-operations) parsing pipelines that avoid per-line string decoding: `bytes/find` the separator, `bytes/parse-int10` the number, `bytes/->string` only what must become text. ```sema ;; Sum one-decimal temperatures from "Name;-12.3" lines as ints ×10. (file/fold-lines-bytes "measurements.txt" (fn (acc line) (let ((semi (bytes/find line 59))) ; 59 = ';' (+ acc (bytes/parse-int10 line (+ semi 1))))) 0) ``` ### `file/delete` Delete a file. ```sema (file/delete "tmp.txt") ``` ### `file/rename` Rename or move a file. ```sema (file/rename "old.txt" "new.txt") ``` ### `file/copy` Copy a file. ```sema (file/copy "src.txt" "dst.txt") ``` ## Binary File I/O ### `file/read-bytes` Read a file as a bytevector (binary data). ```sema (file/read-bytes "image.png") ; => #u8(137 80 78 71 ...) ``` ### `file/write-bytes` Write a bytevector to a file. ```sema (file/write-bytes "output.bin" my-bytes) ``` ## File Predicates ### `file/exists?` Test if a file or directory exists. ```sema (file/exists? "data.txt") ; => #t or #f ``` ### `file/is-file?` Test if a path is a regular file. ```sema (file/is-file? "data.txt") ; => #t ``` ### `file/is-directory?` Test if a path is a directory. ```sema (file/is-directory? "src/") ; => #t ``` ### `file/is-symlink?` Test if a path is a symbolic link. ```sema (file/is-symlink? "link") ; => #t or #f ``` ## Directory Operations ### `file/list` List entries in a directory. ```sema (file/list "src/") ; => ("main.rs" "lib.rs" ...) ``` ### `file/mkdir` Create a directory. ```sema (file/mkdir "new-dir") ``` ### `file/glob` Find files matching a glob pattern. ```sema (file/glob "src/**/*.rs") ; => ("src/main.rs" "src/lib.rs" ...) (file/glob "*.txt") ; => ("readme.txt" "notes.txt") ``` ### `file/info` Get file metadata. Returns a map with `:size` (bytes), `:modified` (Unix epoch **milliseconds**), `:is-file`, and `:is-dir`. ```sema (file/info "data.txt") ; => {:is-dir #f :is-file #t :modified 1782248141021 :size 1234} ``` ## Path Manipulation ### `path/join` Join path components. ```sema (path/join "src" "main.rs") ; => "src/main.rs" (path/join "a" "b" "c.txt") ; => "a/b/c.txt" ``` ### `path/dir` Return the directory portion of a path. Returns `""` when the path has no parent component. ```sema (path/dir "/a/b/c.txt") ;; => "/a/b" (path/dir "foo") ;; => "" ``` `path/dirname` is a legacy alias for `path/dir` — same implementation, same return value. ### `path/filename` Return the filename portion of a path. Returns `""` when there is no filename component (e.g. for `""`). ```sema (path/filename "/a/b/c.txt") ;; => "c.txt" (path/filename "plain.rs") ;; => "plain.rs" ``` `path/basename` is a legacy alias for `path/filename` — same implementation, same return value. ### `path/extension` Return the file extension (without the dot). Returns `""` when the path has no extension. ```sema (path/extension "file.rs") ;; => "rs" (path/extension "file.tar.gz") ;; => "gz" (path/extension "Makefile") ;; => "" (path/extension ".hidden") ;; => "" ``` `path/ext` is a legacy alias for `path/extension` — same implementation, same return value. ::: warning Behavior change Previous versions registered `path/dirname`, `path/basename`, and `path/extension` as independent functions that returned `nil` on the no-parent / no-filename / no-extension case. As of the current release, all six names share one implementation per concept and consistently return `""` (matching `path/dir`, `path/filename`, `path/ext`). ::: ### `path/absolute` Return the absolute path. ```sema (path/absolute ".") ; => "/full/path/to/current/dir" ``` ### `path/stem` Return the filename without extension. ```sema (path/stem "file.rs") ; => "file" (path/stem "archive.tar.gz") ; => "archive.tar" ``` ### `path/absolute?` Test if a path is absolute. ```sema (path/absolute? "/usr/bin") ; => #t (path/absolute? "relative") ; => #f ``` ## File watching Watch a path for changes and drain events non-blockingly. Requires `fs-read` in a sandboxed run. ```sema (define w (fs/watch "src" {:recursive true})) (for-each (lambda (ev) (println (:kind ev) (:paths ev))) ; :create/:modify/:remove/... (fs/watch-events w)) ; non-blocking drain (fs/unwatch w) ``` ## Path safety Helpers for sandboxing file access — `path/within?` is the cornerstone (it resolves symlinks, so it catches `../` *and* symlink escapes). ```sema (path/within? "/repo" "/repo/src/x") ; => #t (catches ../ and symlink escapes) (path/canonicalize "./src/../x") ; real absolute path (errors if missing) (path/relative-to "/a/b" "/a/b/c/d") ; => "c/d" ``` --- --- url: 'https://sema-lang.com/docs/stdlib/pdf.md' --- # PDF Processing Pure-Rust PDF text extraction, page counting, and metadata reading. No external tools required — works cross-platform including macOS, Linux, and Windows. ::: tip Sandbox capabilities PDF functions work without extra configuration in Sema's default mode. Because they read PDF files, they require `fs-read` in a sandboxed run and return `PermissionDenied` when it is denied. See the [CLI sandbox documentation](/docs/cli#sandbox). ::: ::: tip These functions use the `pdf-extract` and `lopdf` Rust crates internally. They work with text-based PDFs. For scanned/image-only PDFs, consider using [`llm/extract-from-image`](../llm/extraction) with vision models instead. ::: ## Text Extraction ### `pdf/extract-text` Extract all text from a PDF file, concatenated across all pages. ```sema (pdf/extract-text "invoice.pdf") ; => "Invoice\nDate: 2025-01-15\nAmount: $50.00 USD\n..." ;; Clean up whitespace for LLM processing (text/clean-whitespace (pdf/extract-text "invoice.pdf")) ; => "Invoice Date: 2025-01-15 Amount: $50.00 USD ..." ``` ### `pdf/extract-text-pages` Extract text from a PDF, returning a list of strings — one per page. ```sema (pdf/extract-text-pages "report.pdf") ; => ("Page 1 content..." "Page 2 content..." "Page 3 content...") ;; Get text from a specific page (nth (pdf/extract-text-pages "report.pdf") 0) ; => "Page 1 content..." ;; Process each page separately (for-each (fn (page-text) (println (format "Page has ~a words" (text/word-count page-text)))) (pdf/extract-text-pages "report.pdf")) ``` ## Metadata ### `pdf/page-count` Return the number of pages in a PDF. ```sema (pdf/page-count "report.pdf") ; => 12 ``` ### `pdf/metadata` Return a map of PDF metadata fields. Always includes `:pages`; other fields (`:title`, `:author`, `:subject`, `:creator`, `:producer`) are included when present in the PDF. ```sema (pprint (pdf/metadata "document.pdf")) ; => {:author "John Doe" ; :creator "LibreOffice Writer" ; :pages 5 ; :producer "LibreOffice" ; :title "Quarterly Report"} ;; Access individual fields (get (pdf/metadata "document.pdf") :title) ; => "Quarterly Report" (get (pdf/metadata "document.pdf") :pages) ; => 5 ``` ## Example: Receipt Processor Combine PDF extraction with [LLM structured extraction](../llm/extraction) to build an intelligent document processor: ```sema ;; Extract text from a PDF invoice (define text (text/clean-whitespace (pdf/extract-text "invoice.pdf"))) (define pages (pdf/page-count "invoice.pdf")) (println (format "Extracted ~a chars from ~a page(s)" (string/length text) pages)) ;; Use LLM to classify and extract structured data (llm/auto-configure) (define result (llm/extract {:isReceipt {:type :boolean :description "Is this a receipt or invoice?"} :vendor {:type :string :description "The seller/merchant name"} :amount {:type :string :description "Total amount with currency"} :date {:type :string :description "Invoice date in YYYY-MM-DD format"}} text)) (println (format "Vendor: ~a" (get result :vendor))) (println (format "Amount: ~a" (get result :amount))) ``` See the full [GLaDOS receipt processor example](https://github.com/sema-lisp/sema/blob/main/examples/glados-downloads.sema) for a complete implementation. --- --- url: 'https://sema-lang.com/docs/stdlib/csv.md' --- # CSV Functions for parsing and encoding CSV (Comma-Separated Values) data. Sema uses the Rust [`csv`](https://docs.rs/csv) crate, which handles RFC 4180 edge cases like quoted fields, embedded commas, and newlines within fields. ::: tip Type mapping All CSV values are returned as **strings**. Use `string/to-number`, `string/to-symbol`, etc. to convert fields to the types you need. ::: ## Parsing ### `csv/parse` Parse a CSV string into a list of lists (rows of fields). No header processing — every row is returned as-is. **Signature:** `(csv/parse csv-string) → list` ```sema (csv/parse "a,b\n1,2\n3,4") ; => (("a" "b") ("1" "2") ("3" "4")) ``` Quoted fields with commas and newlines are handled correctly: ```sema (csv/parse "name,bio\n\"Ada\",\"Mathematician, writer\"\n") ; => (("name" "bio") ("Ada" "Mathematician, writer")) ``` ### `csv/parse-maps` Parse a CSV string into a list of maps. The first row is used as headers, which become keyword keys in each map. **Signature:** `(csv/parse-maps csv-string) → list` ```sema (csv/parse-maps "name,age\nAda,36\nBob,25") ; => ({:age "36" :name "Ada"} {:age "25" :name "Bob"}) ``` Access fields by keyword: ```sema (define rows (csv/parse-maps "name,age\nAda,36\nBob,25")) (:name (first rows)) ; => "Ada" ``` ## Encoding ### `csv/encode` Encode a list of lists (or vectors) into a CSV string. Each inner list/vector becomes one row. Non-string values are stringified automatically. **Signature:** `(csv/encode rows) → string` ```sema (csv/encode '(("a" "b") ("1" "2"))) ; => "a,b\n1,2\n" ``` Numeric and other values are converted to strings: ```sema (csv/encode '(("name" "score") ("Ada" 100))) ; => "name,score\nAda,100\n" ``` ## Examples ### Round-trip example ```sema (define csv-text "name,age\nAda,36\nBob,25\n") (define parsed (csv/parse csv-text)) (csv/encode parsed) ; => "name,age\nAda,36\nBob,25\n" ``` ### Pipeline: file → CSV → processing ```sema ;; Read a CSV file and extract a column (define data (csv/parse-maps (file/read "users.csv"))) (map (lambda (row) (:name row)) data) ``` --- --- url: 'https://sema-lang.com/docs/stdlib/toml.md' --- # TOML Functions for encoding and decoding [TOML](https://toml.io/) data. Sema itself uses TOML for project configuration (`sema.toml`), making these functions useful for both general config parsing and meta-tooling. ## `toml/decode` `(toml/decode toml-string)` → Sema value Parse a TOML string into Sema data structures. Tables become maps with keyword keys, arrays become lists, and scalar types map to their native Sema equivalents. ```sema (toml/decode "[package]\nname = \"my-app\"\nversion = \"1.0.0\"") ; => {:package {:name "my-app" :version "1.0.0"}} ``` ### Nested Tables TOML dotted keys and sub-tables are decoded into nested maps: ```sema (toml/decode " [server] host = \"localhost\" port = 8080 [server.tls] enabled = true cert = \"/path/to/cert.pem\" ") ; => {:server {:host "localhost" :port 8080 :tls {:enabled true :cert "/path/to/cert.pem"}}} ``` ### Arrays and Arrays of Tables Plain arrays become lists. `[[double-bracket]]` arrays of tables become lists of maps: ```sema (toml/decode " colors = [\"red\", \"green\", \"blue\"] [[fruits]] name = \"apple\" color = \"red\" [[fruits]] name = \"banana\" color = \"yellow\" ") ; => {:colors ("red" "green" "blue") ; :fruits ({:color "red" :name "apple"} ; {:color "yellow" :name "banana"})} ``` ### Inline Tables Inline tables are decoded identically to standard tables: ```sema (toml/decode "point = { x = 1, y = 2 }") ; => {:point {:x 1 :y 2}} ``` ### Datetime Handling TOML datetime values are converted to strings. This includes offset datetimes, local datetimes, local dates, and local times: ```sema (toml/decode "created = 2024-01-15T10:30:00Z") ; => {:created "2024-01-15T10:30:00Z"} ``` ### Error Handling Invalid TOML throws a `SemaError`: ```sema (toml/decode "invalid = ") ; => Error: toml/decode: ... ``` ## `toml/encode` `(toml/encode map)` → TOML string Serialize a Sema map to a TOML string. The top-level value **must** be a map — passing any other type is an error. ```sema (toml/encode {:package {:name "my-app" :version "1.0.0"}}) ; => "[package]\nname = \"my-app\"\nversion = \"1.0.0\"\n" ``` ### Nested Maps Nested maps become TOML tables: ```sema (toml/encode {:database {:host "localhost" :port 5432 :credentials {:user "admin" :password "secret"}}}) ``` ### Error Handling The top-level value must be a map: ```sema (toml/encode "hello") ; => Error: toml/encode: top-level value must be a map ``` `nil` values cannot be encoded (TOML has no null): ```sema (toml/encode {:key nil}) ; => Error: toml/encode: cannot encode nil ``` Non-encodable types like functions and records throw errors: ```sema (toml/encode {:callback println}) ; => Error: toml/encode: cannot encode native-fn ``` ## Type Mapping ### TOML → Sema (decoding) | TOML Type | Sema Type | Example | |-----------|-----------|---------| | Table | map (keyword keys) | `{:key "val"}` | | Array | list | `("a" "b" "c")` | | String | string | `"hello"` | | Integer | int | `42` | | Float | float | `3.14` | | Boolean | bool | `#t` / `#f` | | Datetime | string | `"2024-01-15T10:30:00Z"` | ### Sema → TOML (encoding) | Sema Type | TOML Type | Notes | |-----------|-----------|-------| | map / hashmap | Table | Keys converted via `key_to_string` | | list / vector | Array | | | string | String | | | int | Integer | | | float | Float | | | bool | Boolean | | | keyword | String | `:foo` → `"foo"` | | symbol | String | `'foo` → `"foo"` | | nil | ❌ Error | TOML has no null type | | function / record | ❌ Error | Not representable in TOML | ## Practical Examples ### Reading a Config File ```sema (define config (-> "config.toml" file/read toml/decode)) (println "Server:" (map/get-in config [:server :host]) ":" (map/get-in config [:server :port])) ``` ### Updating Config Values ```sema (define config (-> "config.toml" file/read toml/decode)) ;; Update the port and add a new setting (define updated (-> config (map/assoc-in [:server :port] 9090) (map/assoc-in [:server :debug] true))) (file/write "config.toml" (toml/encode updated)) ``` ### Round-Trip ```sema (define config-str " [server] host = \"0.0.0.0\" port = 3000 [server.cors] origins = [\"https://example.com\"] ") (define config (toml/decode config-str)) (define new-config (map/assoc-in config [:server :port] 8080)) (toml/encode new-config) ``` ## TOML vs JSON | | TOML | JSON | |---|------|------| | **Use case** | Configuration files | Data interchange | | **Comments** | ✅ Yes | ❌ No | | **Null type** | ❌ No | ✅ `null` | | **Date/time** | ✅ Native | ❌ Strings only | | **Top-level** | Must be a table | Any value | | **Sema decode** | `toml/decode` | `json/decode` | | **Sema encode** | `toml/encode` | `json/encode` | ::: tip sema.toml Sema uses TOML for its own project configuration file (`sema.toml`). You can read and manipulate it programmatically: ```sema (define project (-> "sema.toml" file/read toml/decode)) (println "Project:" (map/get-in project [:package :name])) ``` ::: --- --- url: 'https://sema-lang.com/docs/stdlib/archive.md' --- # Archives Gzip, zip, and tar. ::: tip Sandbox capabilities Archive operations work without extra configuration in Sema's default mode. `zip/list` requires `fs-read`; `zip/create`, `zip/extract`, `tar/create`, and `tar/extract` require `fs-write`. A denied operation returns `PermissionDenied`. The in-memory `gzip/*` functions require neither capability. See the [CLI sandbox documentation](/docs/cli#sandbox). ::: ```sema (gzip/compress (string->bytevector "hello")) ; => gzip bytevector (gzip/decompress bytes) (zip/create "out.zip" '("a.txt" "b.txt")) ; => entry count (zip/extract "out.zip" "dest/") ; zip-slip guarded (zip/list "out.zip") (tar/create "out.tar.gz" '("a.txt")) ; .tar.gz/.tgz auto-gzips (tar/extract "out.tar.gz" "dest/") ; traversal + symlink guarded ``` Extraction refuses entries that would escape the destination (`..`, absolute paths, traversal symlinks) and rejects two entries that map to the same target. --- --- url: 'https://sema-lang.com/docs/stdlib/diff.md' --- # Diff & Patch Produce, inspect, and apply unified diffs. ::: tip Sandbox capabilities The in-memory `diff/*` functions are not restricted. `patch/apply-file` changes a file and requires `fs-write` in a sandboxed run. It returns `PermissionDenied` when that capability is denied. See the [CLI sandbox documentation](/docs/cli#sandbox). ::: ```sema (define patch (diff/unified old-text new-text)) ; unified diff string (diff/apply old-text patch) ; => new-text (diff/stat patch) ; => {:added :removed :hunks} (diff/hunks patch) ; list of hunk maps (diff/parse patch) ; structured representation (patch/apply-file "src/main.rs" patch) ; apply to a file in place ``` `diff/apply` tolerates small drift (context shifted by a few lines) and errors rather than mis-applying when a hunk's context can't be found. --- --- url: 'https://sema-lang.com/docs/stdlib/markup.md' --- # Markdown & HTML Render Markdown and query HTML. ```sema (markdown/to-html "# Title\n\nHello **world**.") (markdown/headings md) ; list of {:level :text} (markdown/frontmatter md) ; {:frontmatter :body} (html/parse html) ; parsed document (html/select html "a.button") ; list of matched elements' outer HTML (html/select-text html "h1") ; list of matched elements' text (html/text html) ; all visible text, whitespace-collapsed ``` --- --- url: 'https://sema-lang.com/docs/stdlib/http-json.md' --- # HTTP & JSON ## HTTP HTTP request functions make synchronous requests and return a response map. ::: tip Sandbox capabilities `http/get`, `http/post`, `http/put`, `http/query`, `http/delete`, and `http/request` work without extra configuration in Sema's default mode. In a sandboxed run they require the `network` capability (`NETWORK`) and return `PermissionDenied` when it is denied. The `json/*` functions do not require a capability. See the [CLI sandbox documentation](/docs/cli#sandbox). ::: ### Response Map All HTTP functions return a map with three keys: | Key | Type | Description | |------------|--------|------------------------------------------------------| | `:status` | int | HTTP status code (e.g., `200`, `404`, `500`) | | `:headers` | map | Response headers as keyword-keyed map | | `:body` | string | bytevector | Response body — a string by default, or a bytevector with `{:as :bytes}` | ```sema (define resp (http/get "https://httpbin.org/get")) (:status resp) ; => 200 (:headers resp) ; => {:content-type "application/json" :server "..." ...} (:body resp) ; => "{\"args\": {}, ...}" ``` Headers are returned with keyword keys derived from the header name (e.g., `Content-Type` becomes `:content-type`). The body is a raw string by default — use `json/decode` to parse JSON responses, or `{:as :bytes}` to get a bytevector for binary downloads (see [Binary bodies & downloads](#binary-bodies-downloads)). ### Options Map The `http/get`, `http/post`, `http/put`, `http/delete`, and `http/request` functions accept an optional **options map** with the following keys: | Key | Type | Description | |--------------|------|--------------------------------------------------------------------------| | `:headers` | map | Request headers (string or keyword keys both work) | | `:timeout` | int | Request timeout in milliseconds | | `:as` | keyword | Response body decoding: `:text` (default) or `:bytes` (a bytevector) | | `:multipart` | list | Send a `multipart/form-data` body (file uploads) — see [Multipart & file uploads](#multipart-file-uploads) | ```sema ;; Custom headers and timeout (http/get "https://api.example.com/data" {:headers {"Authorization" "Bearer tok_abc123" "Accept" "application/json"} :timeout 5000}) ``` ### Binary bodies & downloads The request **body** may be a **bytevector** to send raw bytes (a binary upload); it's sent verbatim with no JSON encoding. Set your own `Content-Type` header if the server needs one. Pass `{:as :bytes}` to receive the response `:body` as a **bytevector** instead of a string — required for binary payloads (audio, images, PDFs) that would be corrupted by UTF-8 text decoding. Pair with `file/write-bytes` to save a download. ```sema ;; Download binary data and save it to disk (let ((resp (http/get "https://api.example.com/audio.mp3" {:as :bytes}))) (file/write-bytes "out.mp3" (:body resp))) ;; Upload raw bytes (e.g. an image read from disk) (http/post "https://api.example.com/upload" (file/read-bytes "photo.jpg") {:headers {"Content-Type" "image/jpeg"}}) ``` ### Multipart & file uploads Set `:multipart` in the options map to a **list of part maps** to send a `multipart/form-data` body. Each part is `{:name "..." :content ...}` plus optional `:filename` and `:content-type`. A `:filename` (or bytevector content) marks the part as an uploaded file. When `:multipart` is present the positional `body` is ignored. | Part key | Type | Description | |-----------------|-----------------------|------------------------------------------------| | `:name` | string (required) | The form field name | | `:content` | string | bytevector | The field value or file bytes (required) | | `:filename` | string (optional) | Upload as a file with this name | | `:content-type` | string (optional) | MIME type for the part | ```sema ;; Upload a file alongside a text field (http/post "https://api.example.com/documents" {} ; positional body ignored when :multipart is set {:headers {"Authorization" "Bearer tok_abc123"} :multipart (list {:name "purpose" :content "rag-ingest"} {:name "file" :filename "report.pdf" :content (file/read-bytes "report.pdf") :content-type "application/pdf"})}) ``` ### `http/get` ``` (http/get url) (http/get url opts) ``` Make an HTTP GET request. * **url** — string, the request URL * **opts** — optional [options map](#options-map): `:headers`, `:timeout`, `:as` (`:text`/`:bytes`), `:multipart` ```sema ;; Simple GET (http/get "https://httpbin.org/get") ;; GET with custom headers (http/get "https://api.example.com/users" {:headers {:authorization "Bearer my-token"}}) ``` ### `http/post` ``` (http/post url body) (http/post url body opts) ``` Make an HTTP POST request. * **url** — string, the request URL * **body** — request body: a map (auto-encoded as JSON with `Content-Type: application/json`), a string (sent as-is), or a bytevector (sent as raw bytes). Ignored when `:multipart` is set. * **opts** — optional [options map](#options-map): `:headers`, `:timeout`, `:as` (`:text`/`:bytes`), `:multipart` ```sema ;; POST with a map body (auto-JSON-encoded) (http/post "https://httpbin.org/post" {:name "Ada" :age 36}) ;; POST with string body and custom headers (http/post "https://api.example.com/webhook" "raw payload" {:headers {"Content-Type" "text/plain"}}) ;; POST with JSON body and auth (http/post "https://api.example.com/users" {:name "Ada" :role "admin"} {:headers {"Authorization" "Bearer tok_abc123"} :timeout 10000}) ``` ### `http/put` ``` (http/put url body) (http/put url body opts) ``` Make an HTTP PUT request. Behaves identically to `http/post` — map bodies are auto-JSON-encoded. * **url** — string, the request URL * **body** — request body (string or map) * **opts** — optional [options map](#options-map): `:headers`, `:timeout`, `:as` (`:text`/`:bytes`), `:multipart` ```sema (http/put "https://api.example.com/users/42" {:name "Ada Lovelace" :role "admin"}) ``` ### `http/delete` ``` (http/delete url) (http/delete url opts) ``` Make an HTTP DELETE request. * **url** — string, the request URL * **opts** — optional [options map](#options-map): `:headers`, `:timeout`, `:as` (`:text`/`:bytes`), `:multipart` ```sema (http/delete "https://api.example.com/users/42" {:headers {"Authorization" "Bearer tok_abc123"}}) ``` ### `http/request` ``` (http/request method url) (http/request method url opts) (http/request method url opts body) ``` Make an HTTP request with any method. Use this for methods not covered by the convenience functions (e.g., `PATCH`, `HEAD`). * **method** — string, HTTP method (case-insensitive, converted to uppercase). Supported: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD` * **url** — string, the request URL * **opts** — optional [options map](#options-map): `:headers`, `:timeout`, `:as` (`:text`/`:bytes`), `:multipart` * **body** — optional request body (string or map) ```sema ;; PATCH request (http/request "PATCH" "https://api.example.com/users/42" {:headers {"Content-Type" "application/json"}} {:name "Updated Name"}) ;; HEAD request (body will be empty) (define resp (http/request "HEAD" "https://example.com")) (:status resp) ; => 200 (:body resp) ; => "" ``` ### Error Handling Network errors (DNS failure, connection refused, timeout) throw a `SemaError::Io` error. Use `try`/`catch` to handle them: ```sema ;; Handle network errors (try (http/get "https://unreachable.invalid") (catch e (println "Request failed:" e))) ;; Check status codes (define resp (http/get "https://api.example.com/data")) (cond ((= (:status resp) 200) (json/decode (:body resp))) ((= (:status resp) 404) (error "Not found")) ((>= (:status resp) 500) (error "Server error")) (else (error (format "Unexpected status: ~a" (:status resp))))) ;; Timeout handling (try (http/get "https://slow-api.example.com/data" {:timeout 3000}) (catch e (println "Request timed out or failed:" e))) ``` ### Common Patterns #### GET + JSON Decode Pipeline ```sema ;; Fetch JSON data and extract fields (define data (-> (http/get "https://api.example.com/users/1") (:body) (json/decode))) (:name data) ; => "Ada" (:email data) ; => "ada@example.com" ``` #### POST with JSON Body and Auth Headers ```sema (define resp (http/post "https://api.example.com/posts" {:title "Hello World" :body "Content here"} {:headers {"Authorization" "Bearer tok_abc123" "X-Request-Id" "req-001"}})) (when (= (:status resp) 201) (println "Created:" (:body resp))) ``` #### Paginated API Requests ```sema (define (fetch-all-pages base-url) (let loop ((page 1) (results '())) (define resp (http/get (format "~a?page=~a" base-url page))) (define data (json/decode (:body resp))) (define items (:items data)) (if (empty? items) results (loop (+ page 1) (append results items))))) ``` *** ## JSON Functions for encoding Sema values to JSON strings and decoding JSON strings back into Sema values. ### Type Mapping #### Encoding (Sema → JSON) | Sema Type | JSON Type | Notes | |-------------|-----------|--------------------------------------------| | `int` | number | `42` → `42` | | `float` | number | `3.14` → `3.14`. NaN/Infinity cause errors | | `string` | string | `"hello"` → `"hello"` | | `keyword` | string | `:name` → `"name"` | | `symbol` | string | `'foo` → `"foo"` | | `#t` / `#f` | boolean | `#t` → `true`, `#f` → `false` | | `nil` | null | `nil` → `null` | | list | array | `'(1 2 3)` → `[1, 2, 3]` | | vector | array | `[1 2 3]` → `[1, 2, 3]` | | map | object | `{:a 1}` → `{"a": 1}` | | hashmap | object | Same as map | | function | *error* | Cannot encode functions as JSON | | record | *error* | Cannot encode records as JSON | #### Decoding (JSON → Sema) | JSON Type | Sema Type | Notes | |-----------|-----------|-------------------------------------------------| | number | int/float | Integers decode as `int`, decimals as `float` | | string | string | `"hello"` → `"hello"` | | boolean | bool | `true` → `#t`, `false` → `#f` | | null | nil | `null` → `nil` | | array | list | `[1, 2]` → `(1 2)` | | object | map | Keys become keywords: `{"a": 1}` → `{:a 1}` | ### `json/encode` ``` (json/encode value) → string ``` Encode a Sema value as a compact JSON string. Uses **strict** conversion — errors on values that cannot be represented in JSON (functions, records, NaN, Infinity). * **value** — any JSON-encodable Sema value ```sema (json/encode 42) ; => "42" (json/encode "hello") ; => "\"hello\"" (json/encode #t) ; => "true" (json/encode nil) ; => "null" (json/encode '(1 2 3)) ; => "[1,2,3]" (json/encode [1 2 3]) ; => "[1,2,3]" (json/encode {:name "Ada" :age 36}) ; => "{\"age\":36,\"name\":\"Ada\"}" ``` Encoding errors: ```sema ;; NaN and Infinity cannot be represented in JSON (json/encode (/ 0.0 0.0)) ; Error: cannot encode NaN/Infinity as JSON ;; Functions cannot be encoded (json/encode println) ; Error: cannot encode native-fn as JSON ``` ### `json/encode-pretty` ``` (json/encode-pretty value) → string ``` Encode a Sema value as a pretty-printed JSON string with 2-space indentation. Same strict conversion rules as `json/encode`. * **value** — any JSON-encodable Sema value ```sema (json/encode-pretty {:name "Ada" :scores [95 87 92]}) ;; => ;; { ;; "name": "Ada", ;; "scores": [ ;; 95, ;; 87, ;; 92 ;; ] ;; } ``` ### `json/decode` ``` (json/decode json-string) → value ``` Decode a JSON string into a Sema value. JSON objects become maps with keyword keys, arrays become lists. See the [type mapping table](#decoding-json-sema) for full details. * **json-string** — a string containing valid JSON ```sema (json/decode "42") ; => 42 (json/decode "3.14") ; => 3.14 (json/decode "\"hello\"") ; => "hello" (json/decode "true") ; => #t (json/decode "null") ; => nil (json/decode "[1, 2, 3]") ; => (1 2 3) (json/decode "{\"name\": \"Ada\"}") ; => {:name "Ada"} ``` Decoding errors: ```sema ;; Invalid JSON throws an error (json/decode "not json") ; Error: json/decode: expected value at line 1 column 1 ;; Argument must be a string (json/decode 42) ; Error: type error: expected string, got int ``` ### JSON Roundtrips Values that survive an encode → decode roundtrip preserve their structure, though some types are normalized: ```sema ;; Vectors become lists after roundtrip (json/decode (json/encode [1 2 3])) ; => (1 2 3) ;; Keywords in maps are preserved (json/decode (json/encode {:a 1 :b 2})) ; => {:a 1 :b 2} ;; Nested structures work (define data {:users [{:name "Ada"} {:name "Bob"}] :count 2 :active #t}) (define roundtripped (json/decode (json/encode data))) (:count roundtripped) ; => 2 (:active roundtripped) ; => #t ``` ### Error Handling JSON encoding and decoding errors can be caught with `try`/`catch`: ```sema ;; Catch encoding errors (try (json/encode (/ 0.0 0.0)) (catch e (println "Encode failed:" e))) ;; Catch decoding errors (try (json/decode "invalid json {{{") (catch e (println "Decode failed:" e))) ``` --- --- url: 'https://sema-lang.com/docs/stdlib/web-server.md' --- # Web Server Sema includes a built-in HTTP server powered by [axum](https://github.com/tokio-rs/axum), with data-driven routing, middleware as function composition, SSE streaming, and WebSocket support. The server runs on a background thread with a Tokio runtime while keeping all Sema evaluation single-threaded — the same model as Node.js. ::: tip Sandbox capabilities The server and WebSocket client work without extra configuration in Sema's default mode. In a sandboxed run, `http/serve` and `ws/connect` require `network`, while `http/file` requires `fs-read`. A denied operation returns `PermissionDenied`. Response constructors, routers, and operations on an already-open connection require no capability. See the [CLI sandbox documentation](/docs/cli#sandbox). ::: ## Quick Start ```sema (define (handler req) (http/ok {:message "Hello from Sema!"})) (http/serve handler {:port 3000}) ``` ```bash $ curl http://localhost:3000 {"message":"Hello from Sema!"} ``` ## Serving ### `http/serve` Start an HTTP server. Takes a handler function and an optional options map. The handler receives a request map and returns a response map. This function blocks — it becomes the server's run loop. ```sema (http/serve handler) (http/serve handler {:port 3000}) (http/serve handler {:port 8080 :host "127.0.0.1"}) ``` | Option | Default | Description | | ---------------- | ----------- | ---------------------------------------------------------------- | | `:port` | `3000` | TCP port to bind | | `:host` | `"0.0.0.0"` | Address to bind to | | `:port-fallback` | `false` | If the port is taken, bind the next free port instead of failing | | `:on-listen` | — | Function called once bound with `{:host :port :url}` | The handler is any function `(request-map -> response-map)`. This can be a plain function, a router, or a middleware-wrapped stack. #### Automatic port fallback By default `http/serve` fails fast when the port is in use. Pass `:port-fallback true` to walk to the next free port instead. Since the bound port may then differ from the one requested, use `:on-listen` to learn where the server ended up: ```sema (http/serve handler {:port 3000 :port-fallback true :on-listen (fn (info) (println (string-append "Ready at " (:url info))))}) ``` `:on-listen` runs once, on the main thread, right after the socket binds. ## Routing ### `http/router` Create a handler function from a list of route definitions. Each route is a vector of `[method pattern handler]`. ```sema (define routes [[:get "/" handle-home] [:get "/users/:id" handle-user] [:post "/users" handle-create] [:any "/echo" handle-echo]]) (define app (http/router routes)) (http/serve app {:port 3000}) ``` Supported methods: `:get`, `:post`, `:put`, `:patch`, `:delete`, `:any` (matches all methods), `:ws` (WebSocket upgrade), and `:static` (static file directory). Routes are matched top-to-bottom — first match wins. Unmatched routes return 404. ### Path Parameters Use `:param` syntax to capture path segments. Extracted values appear in the request's `:params` map. ```sema ;; Route: [:get "/users/:id" handle-user] ;; Request: GET /users/42 (define (handle-user req) (let ((id (:id (:params req)))) (http/ok {:user-id id}))) ; => {"user-id":"42"} ``` Multiple parameters work as expected: ```sema [:get "/users/:uid/posts/:pid" handler] ;; GET /users/1/posts/99 → {:uid "1" :pid "99"} ``` ### Wildcard Routes Use `*` to capture the rest of the path. ```sema [:get "/files/*" handle-files] ;; GET /files/docs/readme.md → {:* "docs/readme.md"} ``` ## Request Map Every handler receives a request map with the following fields: ```sema {:method :get ; HTTP method as keyword :path "/users/42" ; Request path :headers {"content-type" "application/json" ...} ; Headers (string keys) :query {:search "term" :page "1"} ; Query params (keyword keys) :params {:id "42"} ; Route params (keyword keys) :body "{\"name\": \"Ada\"}" ; Raw body string :json {:name "Ada"}} ; Parsed JSON body (if applicable) ``` The `:json` field is automatically populated when the request has `Content-Type: application/json`. > **Request body limit.** Request bodies are capped at **16 MiB**. A larger body is rejected with `413 Payload Too Large` instead of being buffered into memory, so a client can't exhaust the server's memory with an oversized upload. ### Accessing Request Data ```sema ;; Method (:method req) ; => :get ;; Path (:path req) ; => "/users/42" ;; A specific header (get (:headers req) "authorization") ; => "Bearer ..." ;; Query parameter (:page (:query req)) ; => "2" ;; Route parameter (:id (:params req)) ; => "42" ;; JSON body field (:name (:json req)) ; => "Ada" ``` ## Response Map Handlers return a response map with `:status`, `:headers`, and `:body`: ```sema {:status 200 :headers {"content-type" "application/json"} :body "{\"message\": \"ok\"}"} ``` You can construct these by hand, but the response helpers below are more convenient. ## Response Helpers ### `http/ok` Return 200 with a JSON-encoded body. ```sema (pprint (http/ok {:message "success"})) ; => {:body "{"message":"success"}" ; :headers {"content-type" "application/json"} ; :status 200} (pprint (http/ok [1 2 3])) ; => {:body "[1,2,3]" :headers {"content-type" "application/json"} :status 200} ``` ### `http/created` Return 201 with a JSON-encoded body. ```sema (http/created {:id 42 :name "Ada"}) ``` ### `http/no-content` Return 204 with an empty body. ```sema (http/no-content) ``` ### `http/not-found` Return 404 with a JSON-encoded body. ```sema (http/not-found {:error "User not found"}) ``` ### `http/error` Return a custom status code with a JSON-encoded body. ```sema (http/error 422 {:errors ["Invalid email" "Name required"]}) (http/error 503 {:error "Service unavailable"}) ``` ### `http/redirect` Return a 302 redirect to a URL. ```sema (http/redirect "https://example.com/login") ``` ### `http/html` Return 200 with `Content-Type: text/html`. ```sema (http/html "

Hello

Welcome to Sema.

") ``` ### `http/text` Return 200 with `Content-Type: text/plain`. ```sema (http/text "OK") ``` ### `http/file` Return a file from disk with automatic MIME type detection. The file is read on the I/O thread (not the evaluator), so it handles binary files efficiently. ```sema (http/file "public/index.html") (http/file "data/report.pdf" "application/pdf") ; explicit content type ``` The path is resolved relative to the current working directory. If the file doesn't exist, an error is raised. The MIME type is guessed from the file extension (e.g. `.html` → `text/html`, `.css` → `text/css`, `.js` → `application/javascript`). ## Static File Serving ### `:static` Routes Serve an entire directory of static files using the `:static` route type in `http/router`. Files are served with automatic MIME types, cache headers, and path traversal protection. ```sema (define routes [[:static "/assets" "./public"] [:get "/*" handle-spa]]) (http/serve (http/router routes) {:port 3000}) ``` ```bash $ curl http://localhost:3000/assets/style.css body { color: red; } $ curl -I http://localhost:3000/assets/style.css Content-Type: text/css Cache-Control: public, max-age=3600 ``` The `:static` route takes a URL prefix and a directory path. Requests matching the prefix are mapped to files in the directory: * `GET /assets/style.css` → reads `./public/style.css` * `GET /assets/js/app.js` → reads `./public/js/app.js` * `GET /assets/` → reads `./public/index.html` (directory index) **Fallthrough**: If a file doesn't exist, the route does *not* match — the router continues to the next route. This enables SPA (single-page application) patterns where a catch-all route serves `index.html` for client-side routing: ```sema (define routes [[:static "/assets" "./dist/assets"] [:get "/*" (fn (_) (http/file "./dist/index.html"))]]) (http/serve (http/router routes) {:port 3000}) ``` **Security**: Path traversal attempts (e.g. `../etc/passwd`) are rejected with a 400 response. Only GET and HEAD methods are accepted. ## Middleware Middleware in Sema is just function composition — a function that takes a handler and returns a new handler. No special framework needed. ### Writing Middleware ```sema ;; Logging middleware (define (with-logging handler) (fn (req) (let ((resp (handler req))) (println (:method req) (:path req) "->" (:status resp)) resp))) ``` ```sema ;; CORS middleware (define (with-cors handler) (fn (req) (let ((resp (handler req))) (assoc resp :headers (merge (or (:headers resp) {}) {"access-control-allow-origin" "*" "access-control-allow-methods" "GET, POST, PUT, DELETE"}))))) ``` ```sema ;; Auth middleware (define (with-auth handler) (fn (req) (let ((token (get (:headers req) "authorization"))) (if token (handler req) (http/error 401 {:error "Unauthorized"}))))) ``` ### Composing Middleware Stack middleware by nesting function calls. The outermost middleware runs first. ```sema (define app (with-logging (with-cors (with-auth (http/router routes))))) (http/serve app {:port 3000}) ``` Or use the threading macro for a cleaner pipeline: ```sema (define app (-> (http/router routes) with-auth with-cors with-logging)) ``` ## SSE Streaming ### `http/stream` Return a Server-Sent Events stream. Takes a handler function that receives a `send` callback. ```sema (define (handle-events req) (http/stream (fn (send) (send "connected") (sleep 1000) (send "update 1") (sleep 1000) (send "update 2")))) ``` The stream stays open as long as the handler is running. When the handler returns, the stream closes. ```sema ;; Route it like any other handler (define routes [[:get "/events" handle-events]]) ``` ```bash $ curl -N http://localhost:3000/events data: connected data: update 1 data: update 2 ``` ### Streaming LLM Responses SSE is particularly useful for streaming LLM completions to the browser: ```sema (define (handle-chat req) (http/stream (fn (send) (let ((prompt (:prompt (:json req)))) ;; Stream each token as an SSE event (llm/stream prompt (fn (token) (send token))))))) ``` ## WebSocket ### `http/websocket` Handle bidirectional WebSocket connections. Takes a handler function that receives a connection map with `:send`, `:recv`, and `:close` functions. ```sema (define (handle-ws conn) (let ((msg ((:recv conn)))) (when msg ((:send conn) (string/append "echo: " msg)) (handle-ws conn)))) ``` The connection map: | Key | Description | | -------- | -------------------------------------------------------- | | `:send` | `(send message)` — send a string (text frame) or a bytevector (binary frame) | | `:recv` | `(recv)` — block until a message arrives; a text frame returns a string, a binary frame a bytevector, `nil` on close | | `:close` | `(close)` — Close the connection | ### WebSocket Routes Use the `:ws` method in the router: ```sema (define routes [[:get "/api/status" handle-status] [:ws "/ws/chat" handle-ws]]) (http/serve (http/router routes) {:port 3000}) ``` ### Chat Room Example ```sema (define clients (atom '())) (define (broadcast msg) (for-each (fn (send) (send msg)) @clients)) (define (handle-ws conn) ;; Add this client's send function to the list (swap! clients (fn (lst) (cons (:send conn) lst))) ;; Read loop (let loop ((msg ((:recv conn)))) (when msg (broadcast msg) (loop ((:recv conn)))))) (define routes [[:ws "/chat" handle-ws]]) (http/serve (http/router routes) {:port 3000}) ``` ## WebSocket Client Connect to a WebSocket server with `ws/connect`. A connection is a closeable stream, so `with-open` closes it automatically — on both the normal and the error path. ```sema (with-open (sock (ws/connect "wss://echo.websocket.events")) (ws/send sock "hello") (match (ws/recv sock) {:text msg} (println msg) {:binary buf} (handle-bytes buf) {:close info} :done)) ``` ### `ws/connect` `(ws/connect url)` / `(ws/connect url opts)` — open a connection to a `ws://` or `wss://` URL, returning a connection value. Blocks until the handshake completes (or fails). Requires the `network` capability. Inside an `async/spawn` task it yields cooperatively, so sibling tasks run while the handshake and later receives are in flight. `opts` is an optional map: | Key | Meaning | | -------------------- | ------------------------------------------------------------- | | `:headers` | map of extra HTTP headers on the upgrade (e.g. auth tokens) | | `:subprotocols` | list of `Sec-WebSocket-Protocol` values to offer | | `:timeout` | handshake timeout in milliseconds | | `:retries` | retry a failed handshake this many times (default `0`) | | `:retry-backoff-ms` | base backoff, doubled each retry and capped at 30s (default `500`) | ```sema (ws/connect "wss://api.example.com/socket" {:headers {"Authorization" "Bearer …"} :subprotocols ["chat"] :timeout 5000 :retries 3}) ``` ### `ws/send` `(ws/send conn msg)` — send a message. The frame type follows `msg`: | `msg` | Frame sent | | -------------------- | ------------------------------------------------------- | | string | text frame | | bytevector | binary frame | | `{:text s}` | text frame (explicit) | | `{:binary bv}` | binary frame (explicit) | | `{:json v}` | text frame: `v` encoded as JSON | | any other map | text frame: the map encoded as JSON | ### `ws/recv` and `ws/recv-timeout` `(ws/recv conn)` — receive the next message, blocking until one arrives. Returns a single-key tagged map so a `match` can dispatch on the frame type: | Return value | Meaning | | -------------------------- | ------------------------------------------------ | | `{:text "…"}` | a text frame | | `{:binary #u8(…)}` | a binary frame | | `{:close {:code :reason}}` | the server closed the connection | | `nil` | the connection is fully drained and closed | `(ws/recv-timeout conn ms)` is the same but returns the keyword `:timeout` if no message arrives within `ms` milliseconds (distinct from `nil`, which means closed). A protocol error surfaces as a thrown error you can `try`/`catch`. ### `ws/ping` `(ws/ping conn)` / `(ws/ping conn payload)` — send a ping frame (optional string or bytevector payload); the server replies with a matching pong. Incoming pings are answered automatically. ### `ws/close` and `ws/connected?` `(ws/close conn)` closes the connection (idempotent; also done for you by `with-open`). `(ws/connected? conn)` reports whether the socket is still live. ### `ws/listen` `(ws/listen conn handlers)` drives a receive loop, dispatching each frame to the matching handler. It spawns an async task and returns its promise — `async/await` it (or run the scheduler) to drive the loop. All handlers are optional: | Handler | Called with | When | | -------------- | ------------------- | -------------------------------------- | | `:on-open` | `(conn)` | once, before the loop | | `:on-message` | `(conn msg)` | each text (string) or binary (bytevector) frame | | `:on-close` | `(conn info)` | the connection closed (`info` is `{:code :reason}`) | | `:on-error` | `(conn err)` | a recv/protocol error (loop then stops) | ```sema (with-open (sock (ws/connect "wss://stream.example.com")) (async/await (ws/listen sock {:on-message (fn (conn msg) (println msg)) :on-close (fn (conn info) (println "closed"))}))) ``` > **Browser support.** The `ws/*` client also runs in the browser (Sema Web / > WASM), backed by the browser's native `WebSocket`: `ws/connect`, `ws/send` > (text/binary/JSON + `{:text}`/`{:binary}`/`{:json}` framing), `ws/close`, > `ws/connected?`, and `ws/listen` all work there. Because the browser main > thread cannot block, the pull-based `ws/recv` and `ws/recv-timeout` are > **native-only** — in the browser, receive with the evented `ws/listen` > (`:on-message` / `:on-open` / `:on-close` / `:on-error`), which mirrors how > browser SSE and `llm/chat-stream` deliver data. Connection `:headers`, > `:timeout`, and retry options are native-only too (the browser `WebSocket` API > only supports `:subprotocols`). See the > [Sema Web WebSocket guide](https://sema-lang.com/docs/web/websocket). ## Complete Examples ### REST API A JSON API with CRUD operations, middleware, and error handling. ```sema ;; In-memory data store (define db (atom {})) (define next-id (atom 0)) (define (gen-id) (swap! next-id (fn (n) (+ n 1))) @next-id) ;; Handlers (define (list-users _) (http/ok (vals @db))) (define (get-user req) (let ((id (:id (:params req))) (user (get @db id))) (if user (http/ok user) (http/not-found {:error "User not found"})))) (define (create-user req) (let ((data (:json req)) (id (str (gen-id))) (user (assoc data :id id))) (swap! db (fn (d) (assoc d id user))) (http/created user))) (define (delete-user req) (let ((id (:id (:params req)))) (swap! db (fn (d) (dissoc d id))) (http/no-content))) ;; Middleware (define (with-json-errors handler) (fn (req) (let ((resp (handler req))) (if (map? resp) resp (http/error 500 {:error "Internal server error"}))))) (define (with-cors handler) (fn (req) (let ((resp (handler req))) (assoc resp :headers (merge (or (:headers resp) {}) {"access-control-allow-origin" "*" "access-control-allow-methods" "GET, POST, DELETE"}))))) ;; Routes (define routes [[:get "/users" list-users] [:get "/users/:id" get-user] [:post "/users" create-user] [:delete "/users/:id" delete-user]]) ;; Start (define app (-> (http/router routes) with-json-errors with-cors)) (http/serve app {:port 3000}) ``` ### LLM-Powered API An API endpoint that uses Sema's built-in LLM primitives to generate responses. ```sema (define (handle-summarize req) (let ((text (:text (:json req)))) (if text (http/ok {:summary (llm/complete (str "Summarize this:\n\n" text))}) (http/error 400 {:error "Missing 'text' field"})))) (define (handle-extract req) (let ((text (:text (:json req)))) ;; llm/extract takes the schema first, then the text. (http/ok (llm/extract {:name "string" :date "string" :amount "number"} text)))) (define routes [[:post "/summarize" handle-summarize] [:post "/extract" handle-extract] [:get "/health" (fn (_) (http/ok {:status "up"}))]]) (http/serve (http/router routes) {:port 3000}) ``` ### HTML Application Serve dynamic HTML pages. ```sema (define (page title body) (http/html (str "" title "" "" "" body ""))) (define (handle-home _) (page "Home" "

Welcome

Built with Sema.

")) (define (handle-greet req) (let ((name (or (:name (:params req)) "world"))) (page "Greeting" (str "

Hello, " name "!

")))) (define routes [[:get "/" handle-home] [:get "/greet/:name" handle-greet]]) (http/serve (http/router routes) {:port 3000}) ``` ### SPA with Static Assets Serve a single-page application with static assets and a catch-all for client-side routing. ```sema (define routes [[:get "/api/health" (fn (_) (http/ok {:status "up"}))] [:static "/assets" "./dist/assets"] [:get "/*" (fn (_) (http/file "./dist/index.html"))]]) (http/serve (http/router routes) {:port 3000}) ``` CSS, JS, and images under `./dist/assets/` are served with correct MIME types and cache headers. All other GET requests serve `index.html` for client-side routing. ## Architecture Notes * **Single-threaded evaluation**: All Sema code runs on the main thread. HTTP I/O runs on a background Tokio runtime. Requests are bridged via channels. * **Concurrency model**: Requests are processed sequentially by the evaluator. For LLM-backed services (where each request takes 1–5s of LLM latency), this is fine. For high-throughput APIs, consider a reverse proxy. * **Graceful shutdown**: Ctrl+C breaks the channel and the server exits cleanly. * **Sandbox-aware**: `http/serve` requires the `network` capability in a sandboxed run. ## See Also * [HTTP Client & JSON](./http-json) — outbound HTTP requests and JSON encoding/decoding * [LLM Primitives](/docs/llm/) — building LLM-powered endpoints * [Key-Value Store](./kv-store) — persistent storage for server state --- --- url: 'https://sema-lang.com/docs/stdlib/system.md' --- # System ::: tip Sandbox capabilities System functions work without extra configuration in Sema's default mode. In a sandboxed run, `env`, `sys/cwd`, `sys/env-all`, `sys/home-dir`, `sys/temp-dir`, and `sys/user` require `env-read`; `sys/set-env` requires `env-write`; and `exit`, `sys/args`, `sys/pid`, and `sys/which` require `process`. `shell` requires both `shell` and `process`. Denied calls return `PermissionDenied`. Signal hooks and the other `sys/*` functions are not capability-gated. See the [CLI sandbox documentation](/docs/cli#sandbox). ::: ## Environment Variables ### `env` Get the value of an environment variable. Returns `nil` if not set. ```sema (env "HOME") ; => "/Users/ada" (env "PATH") ; => "/usr/bin:/bin:..." (env "MISSING") ; => nil ``` ### `sys/env-all` Return all environment variables as a map. ```sema (sys/env-all) ; => {:HOME "/Users/ada" :PATH "..." ...} ``` ### `sys/set-env` Set an environment variable for the current process. ```sema (sys/set-env "KEY" "value") (env "KEY") ; => "value" ``` ## System Information ### `sys/args` Return the command-line arguments as a list. ```sema (sys/args) ; => ("sema" "script.sema" "--flag") ``` ### `sys/cwd` Return the current working directory. ```sema (sys/cwd) ; => "/current/dir" ``` ### `sys/platform` Return a normalized platform name — always one of the closed set `"macos"`, `"linux"`, `"windows"`, or `"unknown"`. Anything unrecognized collapses to `"unknown"`, so it is safe to branch on exhaustively. For the raw, open-ended OS name, use `sys/os`. ```sema (sys/platform) ; => "macos" / "linux" / "windows" ``` ### `sys/os` Return the raw operating system name from the Rust target (`std::env::consts::OS`). This is an open set — besides `"macos"`, `"linux"`, and `"windows"` it can also report `"ios"`, `"android"`, `"freebsd"`, and others. Use `sys/platform` when you want a normalized, closed set. ```sema (sys/os) ; => "macos" ``` ### `sys/arch` Return the CPU architecture. ```sema (sys/arch) ; => "aarch64" / "x86_64" ``` ## Process Information ### `sys/pid` Return the current process ID. ```sema (sys/pid) ; => 12345 ``` ### `sys/tty` Return the TTY device path, or `nil` if not running in a terminal. ```sema (sys/tty) ; => "/dev/ttys003" or nil ``` ### `sys/which` Find the full path to an executable, or `nil` if not found. ```sema (sys/which "cargo") ; => "/Users/ada/.cargo/bin/cargo" (sys/which "nonexistent") ; => nil ``` ### `sys/elapsed` Return nanoseconds elapsed since the process started. ```sema (sys/elapsed) ; => 482937100 ``` ## Session Information ### `sys/interactive?` Test if stdin is a TTY (i.e., running interactively). ```sema (sys/interactive?) ; => #t in REPL, #f in scripts ``` ### `sys/hostname` Return the system hostname. ```sema (sys/hostname) ; => "my-machine" ``` ### `sys/user` Return the current username. ```sema (sys/user) ; => "ada" ``` ## Directory Paths ### `sys/home-dir` Return the user's home directory. ```sema (sys/home-dir) ; => "/Users/ada" ``` ### `sys/temp-dir` Return the system temporary directory. ```sema (sys/temp-dir) ; => "/tmp" ``` ## Terminal ### `sys/term-size` Return the terminal's current size as a map `{:rows N :cols M}`, or `nil` when no controlling TTY is attached (e.g., when stdout is redirected to a file). Queries `ioctl(TIOCGWINSZ)` against stdout, then stderr, then stdin. ```sema (sys/term-size) ;; => {:rows 47 :cols 180} ``` Pair with `sys/on-signal :winch` to redraw on terminal resize: ```sema (define (redraw size) ;; ... layout for size ... ) (redraw (sys/term-size)) (sys/on-signal :winch (fn () (redraw (sys/term-size)))) ``` ::: warning Unix only Returns `nil` on Windows and any non-Unix target. ::: ## Signals Async-signal-safe handlers backed by atomic flags. Signal handlers themselves only flip a flag — your callbacks run later, in the main thread, when you call `sys/check-signals`. This keeps the single-threaded `Rc`-based runtime intact. ::: warning Unix only Signal hooks are no-ops on Windows. ::: ### `sys/on-signal` Register a callback for a signal. Multiple callbacks per signal are supported; they fire in registration order. Supported signals: | Keyword | Signal | Typical use | |----------|------------|--------------------------------------| | `:winch` | `SIGWINCH` | Terminal resize — redraw the UI | | `:int` | `SIGINT` | Ctrl-C — clean shutdown | | `:term` | `SIGTERM` | Termination request — clean shutdown | ```sema (sys/on-signal :int (fn () (println "interrupted, cleaning up") (exit 0))) ``` ### `sys/check-signals` Dispatch any pending signal callbacks. Call this from your event loop (typically right after `io/read-key` / `io/read-key-timeout` returns) so handlers run in a predictable place rather than asynchronously interrupting Sema code. ```sema (let loop () (sys/check-signals) (let ((key (io/read-key-timeout 50))) (when key (handle-key key)) (loop))) ``` If no signals are pending, this is essentially free — it just checks three atomic booleans. ## Shell & Process Control ### `shell` Run a shell command. Returns a map with `:stdout`, `:stderr`, and `:exit-code`. A single-string command runs through the system shell (`sh -c` / `cmd /C`); passing extra arguments runs the command directly, without shell parsing. Requires both the `shell` and `process` capabilities in a sandboxed run. ```sema (shell "echo hello") ; => {:stdout "hello\n" :stderr "" :exit-code 0} (:stdout (shell "ls -la")) ; => "total 42\n..." (:exit-code (shell "false")) ; => 1 ``` ### `exit` Exit the process with a given status code. ```sema (exit 0) ; exit successfully (exit 1) ; exit with error ``` --- --- url: 'https://sema-lang.com/docs/stdlib/process.md' --- # Processes & PTYs Spawn and drive child processes with streaming I/O. Unlike [`shell`](/docs/stdlib/system) (which blocks and returns output only after the process exits), these hand you a live handle you poll. All require the `process` capability in a sandboxed run. They work without extra configuration in Sema's default mode and return `PermissionDenied` when `process` is denied. See the [CLI sandbox documentation](/docs/cli#sandbox). ## Streaming processes `proc/*` streams a child's output into pollable buffers, so you can show output as it happens. ```sema (define p (proc/spawn ["cargo" "test"] {:cwd "."})) (let loop () (let ((out (proc/read-stdout p))) (when (not (= out "")) (io/print-error out)) (when (proc/running? p) (sleep 50) (loop)))) (define code (proc/wait p)) ; exit code; flushes the tail first (proc/close p) ; free the handle ``` Full set: `proc/spawn`, `proc/read-stdout`, `proc/read-stderr`, `proc/write-stdin`, `proc/close-stdin`, `proc/wait`, `proc/exit-code`, `proc/running?`, `proc/kill`, `proc/close`. ## Pseudo-terminals Like `proc/*`, but the child runs under a real PTY, so programs that probe `isatty` (REPLs, editors, `top`, color-aware tools) behave normally. ```sema (define t (pty/spawn ["bash"] {:rows 40 :cols 120})) (pty/write t "ls -la\n") (sleep 100) (io/print-error (pty/read t)) ; output incl. control sequences (pty/resize t 50 200) ; delivers SIGWINCH (pty/kill t) (pty/close t) ``` Full set: `pty/spawn`, `pty/read`, `pty/write`, `pty/resize`, `pty/wait`, `pty/exit-code`, `pty/running?`, `pty/kill`, `pty/close`. --- --- url: 'https://sema-lang.com/docs/stdlib/git.md' --- # Git (read-only) Read-only helpers over the `git` binary — they never mutate the repository. All require the `process` capability in a sandboxed run. They work without extra configuration in Sema's default mode and return `PermissionDenied` when `process` is denied. See the [CLI sandbox documentation](/docs/cli#sandbox). ```sema (git/root) ; repo toplevel (git/current-branch) (git/status) ; list of {:path :status :staged :untracked} (git/changed-files) ; list of paths (git/diff-files) ; paths with unstaged changes (git diff --name-only) (git/diff) ; or (git/diff "path") — unified diff (git/recent-files 20) ; files touched by the last N commits (git/ignore-matches? "target/x") ; => #t ``` Paths are returned as real UTF-8 (quoting disabled), and renames / paths with spaces are parsed unambiguously via NUL-delimited porcelain. --- --- url: 'https://sema-lang.com/docs/stdlib/sqlite.md' --- # SQLite Sema includes built-in SQLite support via the `db/*` functions, backed by [rusqlite](https://docs.rs/rusqlite). Databases are opened by name (a logical handle) and can be either file-backed or in-memory. WAL mode and foreign keys are enabled by default. ::: tip Sandbox capabilities SQLite works without extra configuration under Sema's default unrestricted mode. In a sandboxed run, `db/open`, `db/open-memory`, `db/exec`, and `db/exec-batch` require the `fs-write` capability (`FS_WRITE`). If `fs-write` is denied, for example by `--sandbox=strict` or `--sandbox=no-fs-write`, these functions return a `PermissionDenied` error. SQLite query functions require `fs-read`. See the [CLI sandbox documentation](/docs/cli#sandbox) for all modes and capabilities. ::: ## Opening & Closing ### `db/open` Open (or create) a SQLite database file. Returns a handle string for use in subsequent calls. Enables WAL journal mode and foreign keys automatically. ```sema ;; Open with path as handle (db/open "mydata.db") ; => "mydata.db" ;; Open with a named handle (db/open "mydb" "/path/to/data.db") ; => "mydb" ``` ### `db/open-memory` Open an in-memory SQLite database. Useful for tests, temporary data, and caching. ```sema (db/open-memory) ; handle is ":memory:" (db/open-memory "testdb") ; handle is "testdb" ``` ### `db/close` Close a database connection and release the handle. Returns `nil`. ```sema (db/close "mydb") ``` ## Executing SQL ### `db/exec` Execute a SQL statement that modifies data (INSERT, UPDATE, DELETE, CREATE TABLE, etc.). Returns the number of affected rows as an integer. Supports parameterized queries. ```sema (db/exec "mydb" "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)") ; => 0 (db/exec "mydb" "INSERT INTO users (name, age) VALUES (?, ?)" "Alice" 30) ; => 1 (db/exec "mydb" "UPDATE users SET age = ? WHERE name = ?" 31 "Alice") ; => 1 ``` ### `db/exec-batch` Execute multiple SQL statements at once. **Static SQL only** — there is no parameter binding, so the entire string is run verbatim. Useful for schema setup and migrations. Returns `nil`. ::: danger SQL injection Never interpolate user-controlled input into the SQL string passed to `db/exec-batch` — doing so is a SQL injection vulnerability. For any value that comes from outside the program, use the parameterized [`db/exec`](#db-exec) (with `?` placeholders) instead, one statement at a time. ::: ```sema (db/exec-batch "mydb" " CREATE TABLE posts (id INTEGER PRIMARY KEY, user_id INTEGER, title TEXT); CREATE TABLE tags (id INTEGER PRIMARY KEY, name TEXT); CREATE INDEX idx_posts_user ON posts(user_id); ") ``` ## Querying ### `db/query` Execute a SELECT query and return all results as a list of maps. Column names become keyword keys. Supports parameterized queries. ```sema (db/query "mydb" "SELECT * FROM users") ; => ({:id 1 :name "Alice" :age 31}) (db/query "mydb" "SELECT name, age FROM users WHERE age > ?" 25) ; => ({:age 31 :name "Alice"}) ``` ### `db/query-one` Execute a SELECT query and return only the first row as a map, or `nil` if no rows match. ```sema (db/query-one "mydb" "SELECT * FROM users WHERE name = ?" "Alice") ; => {:id 1 :name "Alice" :age 31} (db/query-one "mydb" "SELECT * FROM users WHERE name = ?" "Nobody") ; => nil ``` ## Utility ### `db/last-insert-id` Return the rowid of the last inserted row. ```sema (db/exec "mydb" "INSERT INTO users (name, age) VALUES (?, ?)" "Bob" 25) (db/last-insert-id "mydb") ; => 2 ``` ### `db/tables` List all user-created tables in the database (excludes internal SQLite tables). Returns a list of strings. ```sema (db/tables "mydb") ; => ("posts" "tags" "users") ``` ## Type Mapping | Sema type | SQLite type | Notes | | ----------- | ----------- | ---------------------------- | | `nil` | NULL | | | Boolean | INTEGER | `#t` = 1, `#f` = 0 | | Integer | INTEGER | | | Float | REAL | | | String | TEXT | | | Bytevector | BLOB | | | Other | TEXT | Converted via `to-string` | SQLite values map back as: NULL to `nil`, INTEGER to int, REAL to float, TEXT to string, BLOB to bytevector. ## Examples ### Basic CRUD ```sema (db/open-memory "app") (db/exec "app" "CREATE TABLE todos (id INTEGER PRIMARY KEY, task TEXT, done INTEGER DEFAULT 0)") ;; Insert (db/exec "app" "INSERT INTO todos (task) VALUES (?)" "Buy groceries") (db/exec "app" "INSERT INTO todos (task) VALUES (?)" "Write docs") ;; Query (db/query "app" "SELECT * FROM todos WHERE done = 0") ; => ({:done 0 :id 1 :task "Buy groceries"} {:done 0 :id 2 :task "Write docs"}) ;; Update (db/exec "app" "UPDATE todos SET done = 1 WHERE id = ?" 1) ;; Delete (db/exec "app" "DELETE FROM todos WHERE done = 1") (db/close "app") ``` ### Using with LLM extraction ```sema (db/open-memory "contacts") (db/exec "contacts" "CREATE TABLE people (name TEXT, email TEXT, company TEXT)") ;; Extract structured data from text and insert directly (define info (llm/extract {:name {:type :string} :email {:type :string} :company {:type :string}} "Contact Alice at alice@acme.com, she works at Acme Corp")) (db/exec "contacts" "INSERT INTO people (name, email, company) VALUES (?, ?, ?)" (:name info) (:email info) (:company info)) (db/query "contacts" "SELECT * FROM people") ; => ({:company "Acme Corp" :email "alice@acme.com" :name "Alice"}) (db/close "contacts") ``` --- --- url: 'https://sema-lang.com/docs/stdlib/kv-store.md' --- # Key-Value Store Sema includes a persistent, JSON-backed key-value store for storing structured data across sessions. Data is automatically flushed to disk on every write. ::: tip Sandbox capabilities The key-value store works without extra configuration in Sema's default mode. In a sandboxed run, `kv/open`, `kv/set`, and `kv/delete` require `fs-write` (`FS_WRITE`) and return `PermissionDenied` when it is denied. See the [CLI sandbox documentation](/docs/cli#sandbox). ::: ## How It Works **File path** — You control where data is stored via the second argument to `kv/open`. Relative paths resolve from the current working directory. The file is **not created until the first write** (`kv/set` or `kv/delete`). **Store names** — The first argument to `kv/open` is a logical handle used to reference the store in subsequent calls. Store names are scoped to the current process. Opening the same name twice replaces the previous handle. **Flushing** — Every `kv/set` and `kv/delete` rewrites the entire backing file immediately. `kv/close` also flushes. There is no separate manual flush — persistence is automatic. **JSON format** — The backing file is pretty-printed JSON, so you can inspect or edit it with any text editor. If an existing file contains malformed JSON, `kv/open` raises an error. **Supported value types:** | Sema | JSON | Notes | |------|------|-------| | `nil` | `null` | | | `#t` / `#f` | `true` / `false` | | | Integers | number | | | Floats | number | `NaN` and `Infinity` become `null` | | Strings | string | | | Lists | array | Recursive | | Maps (keyword keys) | object | Keys become strings | **Performance** — Each write rewrites the whole file. This is ideal for small-to-medium stores (config, caches, counters). For large datasets or high-frequency writes, consider using `file/write` directly. ## Functions ### `kv/open` Open (or create) a named KV store backed by a JSON file. If the file exists, its contents are loaded. Returns the store name. ```sema (kv/open "config" "/path/to/config.json") ; => "config" (kv/open "cache" "cache.json") ; relative to CWD ``` If the file doesn't exist yet, no file is created — that happens on the first `kv/set`. ### `kv/get` Get a value by key. Returns `nil` if the key doesn't exist. ```sema (kv/get "config" "api-key") ; => "sk-..." or nil ``` ### `kv/set` Set a key-value pair. The value is serialized as JSON. Returns the value. Flushes to disk immediately. ```sema (kv/set "config" "api-key" "sk-...") (kv/set "config" "retries" 3) (kv/set "config" "tags" '("a" "b" "c")) (kv/set "config" "user" {:name "Alice" :role "admin"}) ``` ### `kv/delete` Delete a key. Returns `#t` if the key existed, `#f` otherwise. Flushes to disk immediately. ```sema (kv/delete "config" "api-key") ; => #t (kv/delete "config" "api-key") ; => #f (already deleted) ``` ### `kv/keys` List all keys in the store. Returns a list of strings. ```sema (kv/keys "config") ; => ("api-key" "retries" "tags") ``` ### `kv/close` Close a store, flushing data and freeing the handle. Returns `nil`. ```sema (kv/close "config") ``` Data is safe even without calling `kv/close` (every write already flushes), but closing frees memory and releases the store name. ## Examples ### Basic usage ```sema ;; Create a persistent store for caching API results (kv/open "cache" "api-cache.json") ;; Store some data (kv/set "cache" "user:123" {:name "Alice" :email "alice@example.com"}) (kv/set "cache" "user:456" {:name "Bob" :email "bob@example.com"}) ;; Retrieve it (kv/get "cache" "user:123") ; => {:email "alice@example.com" :name "Alice"} ;; List keys (kv/keys "cache") ; => ("user:123" "user:456") ;; Clean up (kv/delete "cache" "user:123") (kv/close "cache") ``` ### Application configuration with defaults ```sema (kv/open "config" "app-config.json") ;; Set defaults only if not already configured (when (nil? (kv/get "config" "theme")) (kv/set "config" "theme" "dark")) (when (nil? (kv/get "config" "max-retries")) (kv/set "config" "max-retries" 3)) ;; Use config values (def theme (kv/get "config" "theme")) (println (string/append "Using theme: " theme)) ``` On first run this creates `app-config.json` with defaults. On subsequent runs, existing values are preserved. ### Persistent run counter ```sema (kv/open "stats" "run-stats.json") ;; Increment run count across sessions (let ((runs (or (kv/get "stats" "run-count") 0))) (kv/set "stats" "run-count" (+ runs 1)) (kv/set "stats" "last-run" (time/format (time/now) "%Y-%m-%d %H:%M:%S"))) (println (string/append "Run #" (string (kv/get "stats" "run-count")))) (kv/close "stats") ``` ### Structured data with maps and lists ```sema (kv/open "contacts" "contacts.json") (kv/set "contacts" "alice" {:name "Alice" :email "alice@example.com" :tags '("admin" "dev")}) (kv/set "contacts" "bob" {:name "Bob" :email "bob@example.com" :tags '("dev")}) ;; Retrieve and destructure (def alice (kv/get "contacts" "alice")) (:name alice) ; => "Alice" (:tags alice) ; => ("admin" "dev") ;; List all contacts (for-each (fn (key) (println (:name (kv/get "contacts" key)))) (kv/keys "contacts")) (kv/close "contacts") ``` ## Tips * The backing file is human-readable JSON — you can inspect or hand-edit it between runs. * Store names are just logical handles. Choose descriptive names like `"config"`, `"cache"`, or `"sessions"`. * Use `kv/keys` with iteration for bulk operations like export or cleanup. * For write-heavy workloads on large datasets, consider writing JSON directly with `file/write` to avoid rewriting the entire file on each operation. --- --- url: 'https://sema-lang.com/docs/stdlib/serial.md' --- # Serial Ports Talk to microcontrollers, USB-CDC devices, and any UART over a host serial port. Wraps the cross-platform [`serialport`](https://crates.io/crates/serialport) crate. ::: warning Not available in WASM Serial ports require the host OS — this module is unavailable in the browser playground. ::: ::: tip Sandbox capabilities All `serial/*` functions require the `serial` capability in a sandboxed run. They work without extra configuration in Sema's default mode and return `PermissionDenied` when `serial` is denied, including under `--sandbox=strict` and `--sandbox=all`. See the [CLI sandbox documentation](/docs/cli#sandbox). ::: ## Connection Lifecycle ### `serial/list` List the available serial port device paths on the host. ```sema (serial/list) ;; macOS: ("/dev/tty.usbmodem1201" "/dev/tty.Bluetooth-Incoming-Port") ;; Linux: ("/dev/ttyUSB0" "/dev/ttyACM0") ``` ### `serial/open` ```sema (serial/open path baud) ; default 2000 ms read timeout (serial/open path baud timeout-ms) ``` Open a serial port and return an integer **handle** used by every other function in this module. Raises an error if the device is busy or doesn't exist; the message includes the path and baud rate as a hint. ```sema (define pico (serial/open "/dev/tty.usbmodem1201" 115200)) (define modem (serial/open "/dev/ttyUSB0" 9600 5000)) ; 5s read timeout ``` ### `serial/close` ```sema (serial/close handle) ``` Close the port and free the handle. Subsequent calls with that handle raise `invalid handle`. ## I/O ### `serial/write` ```sema (serial/write handle string) ``` Write a raw string to the port and flush. No newline appended — append `"\n"` yourself if your protocol expects it. ```sema (serial/write modem "AT\r\n") ``` ### `serial/read-line` ```sema (serial/read-line handle) → string ``` Read until `\n`, then trim trailing `\r` / `\n` and return the line. Blocks until either a newline arrives or the port's read timeout elapses (configured at `serial/open` time) — on timeout, raises an error. ```sema (serial/read-line pico) ; => "ready" ``` ### `serial/send` ```sema (serial/send handle command) → parsed-json | nil ``` Convenience for line-oriented JSON protocols (such as the [sema-bridge](https://github.com/sema-lisp/sema/tree/main/examples) firmware that ships with the Pico examples). Writes `command + "\n"`, flushes, reads one line back, and parses it as JSON. Returns `nil` if the response line is empty. ```sema (serial/send pico "{\"cmd\":\"led-on\",\"pin\":25}") ;; => {:ok #t} (serial/send pico "{\"cmd\":\"adc-read\",\"pin\":26}") ;; => {:ok #t :value 2048} ``` ## Example: Pico 2 LED control ```sema (define pico (serial/open "/dev/tty.usbmodem1201" 115200)) (println "bridge:" (serial/read-line pico)) ; "ready" (define (pico-cmd cmd) (let ((resp (serial/send pico cmd))) (when (not (get resp :ok)) (error (format "pico error: ~a" (get resp :error)))) resp)) (pico-cmd "{\"cmd\":\"led-on\",\"pin\":25}") (sleep 500) (pico-cmd "{\"cmd\":\"led-off\",\"pin\":25}") (serial/close pico) ``` See `examples/pico-blink.sema`, `pico-piano.sema`, `pico-jukebox.sema`, `pico-midi.sema`, and `pico-show.sema` for full demos. --- --- url: 'https://sema-lang.com/docs/stdlib/regex.md' --- # Regex Regular expression functions for pattern matching, searching, replacement, and splitting. Sema uses the Rust [`regex`](https://docs.rs/regex) engine. ::: warning Rust regex limitations Rust regex intentionally does **not** support features that require backtracking: * No lookahead / lookbehind (`(?=...)`, `(?!...)`, `(?<=...)`, `(?`) If you need those, consider a multi-step approach using string functions. ::: ## Regex Literals: `#"..."` Normal strings require double-escaping backslashes (`"\\d+"`). Sema's regex literal syntax avoids this: ```sema (regex/match? "\\d+" "abc123") ; normal string — needs \\ (regex/match? #"\d+" "abc123") ; regex literal — cleaner ``` Inside `#"..."`, backslashes are literal (no escape processing). The only special case is `\"` to insert a quote character. ::: tip Prefer `#"..."` for regex patterns. It's easier to read and avoids escaping mistakes. ::: ## Matching ### `regex/match?` Test if a pattern matches anywhere in a string. Returns `#t` or `#f`. ```sema (regex/match? #"\d+" "abc123") ; => #t (regex/match? #"\d+" "no digits") ; => #f (regex/match? #"^\d+$" "abc123") ; => #f (anchored — must match entire string) (regex/match? #"^\d+$" "123") ; => #t ``` ### `regex/match` Match a pattern and return match details as a map, or `nil` if no match. **Signature:** `(regex/match pattern text) → map | nil` The returned map contains: | Key | Value | |-----|-------| | `:match` | The full matched substring | | `:groups` | List of capture groups (group 1, 2, …) | | `:start` | Start byte offset in the input | | `:end` | End byte offset in the input | ```sema (regex/match #"(\d+)-(\w+)" "item-42-foo") ; => {:match "42-foo" :groups ("42" "foo") :start 5 :end 11} (regex/match #"xyz" "abc") ; => nil ``` Optional capture groups that don't participate in the match become `nil`: ```sema (regex/match #"(\d+)(?:-(\d+))?" "42") ; => {:match "42" :groups ("42" nil) :start 0 :end 2} ``` ::: info Byte offsets `:start` and `:end` are byte offsets (UTF-8). For ASCII text they match character indices, but for non-ASCII they may differ. ::: ### `regex/find-all` Find all non-overlapping matches of a pattern. ```sema (regex/find-all #"\d+" "a1b2c3") ; => ("1" "2" "3") (regex/find-all #"[A-Z]" "Hello World") ; => ("H" "W") ``` ## Replacement ### `regex/replace` Replace the **first** match of a pattern. **Signature:** `(regex/replace pattern replacement text) → string` ```sema (regex/replace #"\d+" "X" "a1b2c3") ; => "aXb2c3" ``` Capture group references (`$1`, `$2`, …) work in the replacement string: ```sema (regex/replace #"(\d+)-(\w+)" "$2:$1" "item-42-foo") ; => "item-foo:42" ``` Named capture groups also work: ```sema (regex/replace #"(?P\d+)-(?P\w+)" "$word:$num" "item-42-foo") ; => "item-foo:42" ``` ### `regex/replace-all` Replace **all** matches of a pattern. ```sema (regex/replace-all #"\d" "X" "a1b2") ; => "aXbX" (regex/replace-all #"\s+" " " "a b c") ; => "a b c" ``` ## Splitting ### `regex/split` Split a string by a regex delimiter. ```sema (regex/split #"," "a,b,c") ; => ("a" "b" "c") (regex/split #"\s+" "hello world") ; => ("hello" "world") (regex/split #"[,;]" "a,b;c,d") ; => ("a" "b" "c" "d") ``` ## Supported Syntax Sema uses Rust regex syntax. Common constructs: | Pattern | Meaning | |---------|---------| | `.` | Any character (except newline by default) | | `\d`, `\w`, `\s` | Digit, word char, whitespace | | `\D`, `\W`, `\S` | Negated versions | | `+`, `*`, `?` | One+, zero+, optional | | `{m,n}` | Between m and n repetitions | | `^`, `$` | Start/end anchors | | `(...)` | Capture group | | `(?:...)` | Non-capturing group | | `(?P...)` | Named capture group | | `[abc]`, `[^abc]` | Character class | | `a\|b` | Alternation | See the [Rust regex docs](https://docs.rs/regex) for the full reference. ## Escaping Guide ### Regex literals vs normal strings | Intent | Normal string | Regex literal | |--------|---------------|---------------| | One or more digits | `"\\d+"` | `#"\d+"` | | A literal dot | `"\\."` | `#"\."` | | A backslash | `"\\\\\\\\"` | `#"\\"` | ### Matching a literal `"` in a regex literal Inside `#"..."`, use `\"`: ```sema (regex/match? #"\"[^\"]+\"" "say \"hello\"") ; => #t ``` ## Regex vs String Functions Prefer string functions when possible — they're simpler and faster: | Need | String function | Regex equivalent | |------|----------------|------------------| | Contains? | `string/contains?` | `regex/match?` | | Starts with? | `string/starts-with?` | `regex/match?` with `^` | | Simple split | `string/split` | `regex/split` | | Simple replace | `string/replace` | `regex/replace` | Use regex when you need character classes, repetition, alternation, or capture groups. ## Practical Examples ### Validate an identifier ```sema (define (identifier? s) (regex/match? #"^[A-Za-z_][A-Za-z0-9_]*$" s)) (identifier? "foo_1") ; => #t (identifier? "1foo") ; => #f ``` ### Extract a number from text ```sema (define (extract-first-int s) (let ((m (regex/match #"\d+" s))) (if (nil? m) nil (:match m)))) (extract-first-int "x=42; y=9") ; => "42" ``` ### Normalize whitespace ```sema (regex/replace-all #"\s+" " " "a b\n\nc\t\t d") ; => "a b c d" ``` ### Parse key-value pairs ```sema (define (parse-kv line) (let ((m (regex/match #"^(\w+)\s*=\s*(.+)$" line))) (if (nil? m) nil (let ((groups (:groups m))) {:key (first groups) :value (first (rest groups))})))) (parse-kv "name = Alice") ; => {:key "name" :value "Alice"} ``` ### Find all email-like strings ```sema (regex/find-all #"[\w.+-]+@[\w-]+\.[\w.]+" "Contact ada@example.com or bob@test.org") ; => ("ada@example.com" "bob@test.org") ``` ## Performance Notes * Each function call **compiles the regex pattern** internally * For occasional use, this is fine * For hot loops, consider using `regex/find-all` once instead of many `regex/match?` calls * Rust regex guarantees **linear-time** matching — no catastrophic backtracking --- --- url: 'https://sema-lang.com/docs/stdlib/crypto.md' --- # Crypto & Encoding UUID generation, Base64 encoding, and cryptographic hashing. ## UUID ### `uuid/v4` Generate a random UUID v4 string. **Signature:** `(uuid/v4) → string` ```sema (uuid/v4) ; => "550e8400-e29b-41d4-a716-446655440000" (varies) ``` Each call returns a new unique identifier: ```sema (equal? (uuid/v4) (uuid/v4)) ; => #f ``` ## Base64 Encoding Functions for Base64 encoding and decoding of strings and binary data. Uses the standard Base64 alphabet (RFC 4648). ### `base64/encode` Encode a string to Base64. **Signature:** `(base64/encode string) → string` ```sema (base64/encode "hello") ; => "aGVsbG8=" (base64/encode "") ; => "" ``` ### `base64/decode` Decode a Base64 string back to a UTF-8 string. Errors if the decoded bytes are not valid UTF-8. **Signature:** `(base64/decode base64-string) → string` ```sema (base64/decode "aGVsbG8=") ; => "hello" ``` ### `base64/encode-bytes` Encode a bytevector to Base64. **Signature:** `(base64/encode-bytes bytevector) → string` ```sema (base64/encode-bytes #u8(104 101 108 108 111)) ; => "aGVsbG8=" ``` ### `base64/decode-bytes` Decode a Base64 string to a bytevector. Unlike `base64/decode`, this does not require valid UTF-8. **Signature:** `(base64/decode-bytes base64-string) → bytevector` ```sema (base64/decode-bytes "aGVsbG8=") ; => #u8(104 101 108 108 111) ``` ### Use cases **Data URIs:** ```sema (string/append "data:image/png;base64," (base64/encode-bytes (file/read-bytes "icon.png"))) ``` **API authentication (Basic Auth):** ```sema (define auth-header (string/append "Basic " (base64/encode (string/append username ":" password)))) ``` ## Hashing Cryptographic hash functions that return hex-encoded strings. ::: warning Security note **MD5** is cryptographically broken — do not use it for passwords, signatures, or any security-sensitive purpose. Use `hash/sha256` or `hash/hmac-sha256` instead. MD5 is still fine for checksums and non-security uses (cache keys, deduplication). ::: ### `hash/sha256` Compute the SHA-256 hash of a string. Returns a 64-character hex string. **Signature:** `(hash/sha256 string) → string` ```sema (hash/sha256 "hello") ; => "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" ``` ### `hash/md5` Compute the MD5 hash of a string. Returns a 32-character hex string. **Signature:** `(hash/md5 string) → string` ```sema (hash/md5 "hello") ; => "5d41402abc4b2a76b9719d911017c592" ``` ### `hash/hmac-sha256` Compute an HMAC-SHA256 message authentication code. Returns a 64-character hex string. **Signature:** `(hash/hmac-sha256 key message) → string` ```sema (hash/hmac-sha256 "secret-key" "message") ; => "hex-encoded-hmac..." ``` **Webhook verification example:** ```sema ;; Verify a webhook signature from a provider (define (verify-webhook payload secret signature) (equal? (hash/hmac-sha256 secret payload) signature)) ``` --- --- url: 'https://sema-lang.com/docs/stdlib/secret.md' --- # Secrets & Redaction Detect and redact secrets and PII in text — useful before logging or sending data to an LLM. ```sema (secret/detect "key AKIA... and tok eyJ...") ; list of {:type :match :start :end} (secret/redact text) ; => text with secrets → «redacted:» (pii/detect text) ; emails, IPv4, phone numbers (redact/spans text spans) ; redact caller-supplied {:start :end :label} ranges (hash/digest text) ; SHA-256 hex (fingerprint a redacted value) ``` --- --- url: 'https://sema-lang.com/docs/stdlib/reflect.md' --- # Reflection & Diagnostics Parse, format, and check Sema source from Sema. Diagnostics come back as data, which makes them ideal for agent repair loops. ::: tip Sandbox capabilities `sema/check-string` works in memory and requires no capability. `sema/check-file` requires `fs-read` in a sandboxed run and returns `PermissionDenied` when it is denied. See the [CLI sandbox documentation](/docs/cli#sandbox). ::: ```sema (read/string "(+ 1 2)") ; => the form (+ 1 2) (read/all "(a) (b)") ; => ((a) (b)) (format/form '(define x 1)) ; => "(define x 1)" (sema/check-string "(+ 1 2") ; => {:ok #f :diagnostics [{:level :error ; :code "syntax" :message ... ; :span {:line :col :end-line :end-col}}]} (sema/check-file "workflow.sema") ; same, reading a file ``` --- --- url: 'https://sema-lang.com/docs/stdlib/datetime.md' --- # Date & Time All timestamps in Sema are **UTC Unix timestamps** — the number of seconds since January 1, 1970 00:00:00 UTC. Timestamps are floating-point numbers with millisecond fractional precision. ::: tip All `time/` functions operate in UTC. There is no timezone conversion support — if you need local time handling, compute the offset manually with `time/add`. ::: ## Current Time ### `time/now` Return the current time as a UTC Unix timestamp in seconds, with fractional milliseconds. ```sema (time/now) ; => 1707955200.123 ``` The integer part is seconds since the Unix epoch; the fractional part provides millisecond precision. ```sema (define now (time/now)) (println "Current timestamp: " now) ;; Extract just the seconds (truncate fractional part) (define whole-seconds (floor now)) ``` ### `time-ms` Return the current time as Unix milliseconds (integer). Defined in the system module but useful alongside datetime operations. ```sema (time-ms) ; => 1707955200123 ``` ## Formatting ### `time/format` Format a UTC Unix timestamp using a [strftime](#strftime-format-directives)-style format string. ```sema (time/format timestamp format-string) ; => string ``` ```sema (define ts 1736943000.0) ; 2025-01-15 12:10:00 UTC (time/format ts "%Y-%m-%d") ; => "2025-01-15" (time/format ts "%H:%M:%S") ; => "12:10:00" (time/format ts "%Y-%m-%d %H:%M:%S") ; => "2025-01-15 12:10:00" (time/format ts "%A, %B %d, %Y") ; => "Wednesday, January 15, 2025" (time/format ts "%F") ; => "2025-01-15" (shorthand for %Y-%m-%d) (time/format ts "%T") ; => "12:10:00" (shorthand for %H:%M:%S) ``` ## Parsing ### `time/parse` Parse a date string into a UTC Unix timestamp using a [strftime](#strftime-format-directives)-style format string. The input is treated as a **UTC naive datetime** — no timezone information is expected or applied. ```sema (time/parse date-string format-string) ; => float (UTC timestamp) ``` ```sema (time/parse "2025-01-15 12:10:00" "%Y-%m-%d %H:%M:%S") ; => 1736943000.0 (time/parse "2025-01-15 00:00:00" "%Y-%m-%d %H:%M:%S") ; => 1736899200.0 (time/parse "15/01/2025 14:30:00" "%d/%m/%Y %H:%M:%S") ; => 1736951400.0 ``` ::: info The format string must provide enough directives to fully specify a date and time. Parsing a date-only string like `"%Y-%m-%d"` without time components will fail — always include time directives (e.g., `%H:%M:%S`). ::: ::: tip The wall-clock time in the string is **always interpreted as UTC**, regardless of any offset present. `time/parse` does not apply timezone offsets — `"2025-01-15 12:10:00"` always yields the UTC timestamp for 12:10:00 UTC. To work with another timezone, convert the value to UTC yourself (subtract the offset) before parsing, then format/compute in UTC. ::: **Roundtrip** — formatting a timestamp and parsing it back yields the original value: ```sema (define ts 1700000000.0) (define formatted (time/format ts "%Y-%m-%d %H:%M:%S")) (define parsed (time/parse formatted "%Y-%m-%d %H:%M:%S")) (= parsed ts) ; => #t ``` ::: warning `time/parse` returns whole seconds — sub-second precision from the original timestamp is lost when roundtripping through format/parse. ::: ## Date Decomposition ### `time/date-parts` Decompose a UTC Unix timestamp into a map of date/time components. ```sema (time/date-parts timestamp) ; => map ``` ```sema (define ts 1736943000.0) ; 2025-01-15 12:10:00 UTC (define parts (time/date-parts ts)) (get parts :year) ; => 2025 (get parts :month) ; => 1 (get parts :day) ; => 15 (get parts :hour) ; => 12 (get parts :minute) ; => 10 (get parts :second) ; => 0 (get parts :weekday) ; => "Wednesday" ``` The returned map contains these keys: | Key | Type | Description | Example | |-----|------|-------------|---------| | `:year` | integer | Four-digit year | `2025` | | `:month` | integer | Month (1–12) | `1` | | `:day` | integer | Day of month (1–31) | `15` | | `:hour` | integer | Hour (0–23) | `12` | | `:minute` | integer | Minute (0–59) | `10` | | `:second` | integer | Second (0–59) | `0` | | `:weekday` | string | Full weekday name | `"Wednesday"` | The `:weekday` value is the full English weekday name: `"Monday"`, `"Tuesday"`, `"Wednesday"`, `"Thursday"`, `"Friday"`, `"Saturday"`, `"Sunday"`. ## Arithmetic ### `time/add` Add seconds to a timestamp. Returns a new timestamp. Use negative values to subtract. ```sema (time/add timestamp seconds) ; => float (timestamp) ``` ```sema (define ts 1736943000.0) ; 2025-01-15 12:10:00 UTC (time/add ts 3600) ; one hour later => 1736946600.0 (time/add ts 86400) ; one day later => 1737029400.0 (time/add ts -3600) ; one hour earlier => 1736939400.0 (time/add ts (* 7 86400)) ; one week later ``` Common durations in seconds: | Duration | Seconds | |----------|---------| | 1 minute | `60` | | 1 hour | `3600` | | 1 day | `86400` | | 1 week | `604800` | | 30 days | `2592000` | ### `time/diff` Compute the difference between two timestamps in seconds. Returns `t1 - t2` (the first argument minus the second). The result can be negative. ```sema (time/diff t1 t2) ; => float (seconds) ``` ```sema (define morning 1736935800.0) ; 2025-01-15 10:10:00 UTC (define afternoon 1736943000.0) ; 2025-01-15 12:10:00 UTC (time/diff afternoon morning) ; => 7200.0 (2 hours) (time/diff morning afternoon) ; => -7200.0 (negative — morning is earlier) (time/diff morning morning) ; => 0.0 ``` ::: tip `time/diff` returns a signed value: positive when `t1 > t2`, negative when `t1 < t2`. Use `abs` if you need the absolute elapsed time regardless of order. ::: ## Delay ### `sleep` Pause execution for a given number of milliseconds. Returns `nil`. ```sema (sleep milliseconds) ; => nil ``` ```sema (sleep 1000) ; sleep for 1 second (sleep 500) ; sleep for 500ms (sleep 0) ; yield (no-op pause) ``` Note that `sleep` takes **milliseconds** (not seconds), unlike the `time/` functions which work in seconds. ## strftime Format Directives The `time/format` and `time/parse` functions use [chrono strftime](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) format directives. Here are the most common ones: ### Date | Directive | Description | Example | |-----------|-------------|---------| | `%Y` | Four-digit year | `2025` | | `%m` | Month (zero-padded, 01–12) | `01` | | `%d` | Day of month (zero-padded, 01–31) | `15` | | `%e` | Day of month (space-padded) | `15` | | `%B` | Full month name | `January` | | `%b` | Abbreviated month name | `Jan` | | `%A` | Full weekday name | `Wednesday` | | `%a` | Abbreviated weekday name | `Wed` | | `%u` | Day of week (1=Monday, 7=Sunday) | `3` | | `%j` | Day of year (001–366) | `015` | | `%F` | ISO 8601 date (`%Y-%m-%d`) | `2025-01-15` | ### Time | Directive | Description | Example | |-----------|-------------|---------| | `%H` | Hour, 24-hour (zero-padded, 00–23) | `12` | | `%I` | Hour, 12-hour (zero-padded, 01–12) | `12` | | `%M` | Minute (zero-padded, 00–59) | `10` | | `%S` | Second (zero-padded, 00–59) | `00` | | `%p` | AM/PM | `PM` | | `%T` | Time (`%H:%M:%S`) | `12:10:00` | | `%R` | Short time (`%H:%M`) | `12:10` | ### Combined & Special | Directive | Description | Example | |-----------|-------------|---------| | `%c` | Locale date and time | `Wed Jan 15 12:10:00 2025` | | `%s` | Unix timestamp (seconds) | `1736943000` | | `%Z` | Timezone abbreviation | `UTC` | | `%%` | Literal `%` | `%` | ## Common Patterns ### Measuring elapsed time ```sema (define start (time/now)) ;; ... do some work ... (define end (time/now)) (define elapsed (time/diff end start)) (println (format "Took ~a seconds" elapsed)) ``` ### ISO 8601 formatting ```sema (define ts (time/now)) (time/format ts "%Y-%m-%dT%H:%M:%SZ") ; => "2025-01-15T12:10:00Z" (time/format ts "%F") ; => "2025-01-15" (date only) ``` ### Calculating "N days ago" ```sema (define now (time/now)) (define one-week-ago (time/add now (* -7 86400))) (define thirty-days-ago (time/add now (* -30 86400))) (println "One week ago: " (time/format one-week-ago "%Y-%m-%d")) ``` ### Formatting for display ```sema (define ts (time/now)) (time/format ts "%A, %B %d, %Y") ; => "Wednesday, January 15, 2025" (time/format ts "%I:%M %p") ; => "12:10 PM" (time/format ts "%b %d at %H:%M") ; => "Jan 15 at 12:10" ``` ### Checking the day of the week ```sema (define parts (time/date-parts (time/now))) (define day (get parts :weekday)) (if (or (= day "Saturday") (= day "Sunday")) (println "It's the weekend!") (println "It's a weekday.")) ``` ### Computing duration between dates ```sema (define start (time/parse "2025-01-01 00:00:00" "%Y-%m-%d %H:%M:%S")) (define end (time/parse "2025-03-15 00:00:00" "%Y-%m-%d %H:%M:%S")) (define diff-seconds (time/diff end start)) (define diff-days (/ diff-seconds 86400)) (println (format "~a days between dates" diff-days)) ``` ## Edge Cases ### Unix epoch ```sema (time/format 0.0 "%Y-%m-%d %H:%M:%S") ; => "1970-01-01 00:00:00" (time/date-parts 0.0) ; => {:day 1 :hour 0 :minute 0 :month 1 :second 0 :weekday "Thursday" :year 1970} ``` ### Negative timestamps (dates before 1970) ```sema (time/format -86400.0 "%Y-%m-%d") ; => "1969-12-31" (time/format -31536000.0 "%Y-%m-%d") ; => "1969-01-01" ``` ### Sub-second precision `time/now` returns millisecond fractional precision. `time/add` and `time/diff` preserve fractional seconds. However, `time/parse` returns whole seconds only. ```sema (define ts (time/add 1736943000.0 0.5)) ; add 500ms (time/diff ts 1736943000.0) ; => 0.5 ``` --- --- url: 'https://sema-lang.com/docs/stdlib/context.md' --- # Context Sema provides an ambient context system — a key-value store that flows through your entire execution without explicit parameter passing. Inspired by [Laravel's Context](https://laravel.com/docs/12.x/context), it's designed for tracing, metadata propagation, and sharing configuration across deeply nested calls. Context data is automatically appended as metadata to log output (`log/info`, `log/warn`, `log/error`, `log/debug`). ## Core Functions ### `context/set` Set a key-value pair in the current context frame. ```sema (context/set :trace-id "abc-123") (context/set :user-id 42) ``` ### `context/get` Retrieve a value by key. Returns `nil` if the key doesn't exist. ```sema (context/get :trace-id) ; => "abc-123" (context/get :missing) ; => nil ``` ### `context/has?` Check if a key exists in the context. ```sema (context/has? :trace-id) ; => #t (context/has? :missing) ; => #f ``` ### `context/remove` Remove a key from all context frames. Returns the removed value, or `nil`. ```sema (context/set :temp "data") (context/remove :temp) ; => "data" (context/remove :temp) ; => nil (already gone) ``` ### `context/pull` Get a value and remove it in one step (identical to `context/remove`). ```sema (context/set :token "abc") (context/pull :token) ; => "abc" (context/has? :token) ; => #f ``` ### `context/all` Get all context as a merged map. ```sema (context/set :a 1) (context/set :b 2) (context/all) ; => {:a 1 :b 2} ``` ### `context/merge` Merge a map of key-value pairs into the current context. ```sema (context/merge {:trace-id "abc" :env "production" :version "1.0"}) (context/get :env) ; => "production" ``` ### `context/clear` Clear all context, resetting to an empty state. ```sema (context/clear) (context/all) ; => {} ``` ## Scoped Overrides ### `context/with` Push a temporary context frame for the duration of a thunk. The frame is automatically popped when the thunk completes — even if it raises an error. ```sema (context/set :env "production") (context/with {:env "staging" :debug #t} (lambda () (context/get :env) ; => "staging" (context/get :debug))) ; => #t (context/get :env) ; => "production" (restored) (context/get :debug) ; => nil (gone) ``` Scopes nest naturally — inner values shadow outer ones: ```sema (context/set :a 1) (context/with {:b 2} (lambda () (context/with {:c 3} (lambda () (list (context/get :a) (context/get :b) (context/get :c)))))) ; => (1 2 3) ``` ::: warning Values set with `context/set` inside a `context/with` block are written to the inner frame and discarded when the scope exits. If you need a value to persist, set it before entering `context/with`. ::: ## Stacks Context stacks are ordered lists of values that you can push to and pop from. Unlike key-value context, stacks are **not scoped** by `context/with` — pushes persist across scope boundaries. ### `context/push` Append a value to a named stack. ```sema (context/push :breadcrumbs "login") (context/push :breadcrumbs "dashboard") (context/push :breadcrumbs "settings") ``` ### `context/stack` Get all values in a named stack as a list. ```sema (context/stack :breadcrumbs) ; => ("login" "dashboard" "settings") ``` ### `context/pop` Remove and return the last value from a stack. Returns `nil` if the stack is empty. ```sema (context/pop :breadcrumbs) ; => "settings" (context/stack :breadcrumbs) ; => ("login" "dashboard") ``` ## Hidden Context Hidden context stores values that are **not visible** via `context/get`, `context/all`, or log metadata. Use it for sensitive data like API keys or internal state. ### `context/set-hidden` ```sema (context/set-hidden :api-key "sk-secret-123") ``` ### `context/get-hidden` ```sema (context/get-hidden :api-key) ; => "sk-secret-123" (context/get :api-key) ; => nil (not visible in regular context) ``` ### `context/has-hidden?` ```sema (context/has-hidden? :api-key) ; => #t ``` ## Log Integration When context is non-empty, `log/info`, `log/warn`, `log/error`, and `log/debug` automatically append the context map as metadata: ```sema (context/set :trace-id "abc-123") (context/set :user-id 42) (log/info "Request processed") ``` Output: ``` [INFO] Request processed {:trace-id "abc-123" :user-id 42} ``` Hidden context is **not** included in log output. ## Examples ### Request tracing ```sema (context/set :request-id (uuid/v4)) (context/set :method "GET") (context/set :path "/api/users") (log/info "Request started") ; [INFO] Request started {:method "GET" :path "/api/users" :request-id "a1b2c3..."} ;; All downstream functions automatically include this context in their logs (process-request) ``` ### Pipeline breadcrumbs ```sema (define (process-document doc) (context/push :steps "parse") (let ((parsed (parse doc))) (context/push :steps "validate") (let ((valid (validate parsed))) (context/push :steps "transform") (transform valid)))) (process-document input) (context/stack :steps) ; => ("parse" "validate" "transform") ``` ### Scoped configuration ```sema ;; Set default model (context/set :model "claude-sonnet") ;; Override for a specific block (context/with {:model "gpt-5.5" :temperature 0.9} (lambda () ;; Code here sees the overridden values (context/get :model))) ; => "gpt-5.5" (context/get :model) ; => "claude-sonnet" ``` ## Function Reference | Function | Args | Description | | --------------------- | ----------- | --------------------------------- | | `context/set` | `key value` | Set a context value | | `context/get` | `key` | Get a value (or `nil`) | | `context/has?` | `key` | Check if key exists | | `context/remove` | `key` | Remove and return value | | `context/pull` | `key` | Get and remove (alias for remove) | | `context/all` | | Get all context as a map | | `context/merge` | `map` | Merge map into context | | `context/clear` | | Clear all context | | `context/with` | `map thunk` | Scoped override | | `context/push` | `key value` | Push to named stack | | `context/stack` | `key` | Get stack as list | | `context/pop` | `key` | Pop from named stack | | `context/set-hidden` | `key value` | Set hidden value | | `context/get-hidden` | `key` | Get hidden value | | `context/has-hidden?` | `key` | Check hidden key exists | --- --- url: 'https://sema-lang.com/docs/stdlib/terminal.md' --- # Terminal Styling Functions for styling terminal output with ANSI escape codes, true color, and animated spinners. All style functions take a string and return a new string wrapped in ANSI escape sequences. The styled text is reset after the content, so styles don't bleed into subsequent output. ::: tip Terminal output Styled output renders correctly in terminals that support ANSI escape codes. When piping or redirecting output (e.g., to a file), the raw escape sequences are included in the output. Use `term/strip` to produce clean text for non-terminal destinations. ::: ## Modifiers Modifier functions change how text is displayed without altering its color. ### `term/bold` Render text in **bold** (increased intensity). ```sema (term/bold "important") (println (term/bold "Warning: check your input")) ``` ### `term/dim` Render text with decreased intensity. ```sema (term/dim "less important") ``` ### `term/italic` Render text in *italic*. ```sema (term/italic "emphasis") ``` ### `term/underline` Render text with an underline. ```sema (term/underline "click here") ``` ### `term/inverse` Swap foreground and background colors. ```sema (term/inverse "highlighted") ``` ### `term/strikethrough` Render text with a ~~strikethrough~~. ```sema (term/strikethrough "deprecated") ``` ## Colors Color functions set the foreground (text) color. ### `term/black` ```sema (term/black "dark text") ``` ### `term/red` ```sema (term/red "error message") ``` ### `term/green` ```sema (term/green "success") ``` ### `term/yellow` ```sema (term/yellow "warning") ``` ### `term/blue` ```sema (term/blue "info") ``` ### `term/magenta` ```sema (term/magenta "special") ``` ### `term/cyan` ```sema (term/cyan "highlight") ``` ### `term/white` ```sema (term/white "bright text") ``` ### `term/gray` ```sema (term/gray "muted text") ``` ## Combined Styles ### `term/style` Apply multiple styles at once using keywords. The first argument is the text, followed by one or more style keywords. ```sema (term/style "danger" :bold :red) (term/style "notice" :italic :yellow :underline) (term/style "subtle" :dim :gray) ``` Internally, `term/style` combines ANSI codes with `;` separators into a single escape sequence (e.g., `ESC[1;31m` for bold red), which is more efficient than nesting individual style functions. If called with no style keywords, the text is returned unstyled. ```sema (term/style "plain text") ; => "plain text" (no ANSI codes) ``` An unknown keyword produces an error: ```sema (term/style "text" :blink) ; Error: unknown style keyword :blink ``` #### Style keyword reference | Keyword | Effect | ANSI Code | |------------------|----------------|-----------| | `:bold` | Bold | 1 | | `:dim` | Dim | 2 | | `:italic` | Italic | 3 | | `:underline` | Underline | 4 | | `:inverse` | Inverse | 7 | | `:strikethrough` | Strikethrough | 9 | | `:black` | Black text | 30 | | `:red` | Red text | 31 | | `:green` | Green text | 32 | | `:yellow` | Yellow text | 33 | | `:blue` | Blue text | 34 | | `:magenta` | Magenta text | 35 | | `:cyan` | Cyan text | 36 | | `:white` | White text | 37 | | `:gray` | Gray text | 90 | ### Composing Styles There are two ways to combine styles: **Using `term/style` (recommended):** produces a single escape sequence with combined codes. ```sema (term/style "alert" :bold :red :underline) ;; Produces: ESC[1;31;4m alert ESC[0m ``` **Nesting individual functions:** each function wraps the text in its own escape sequence. This works but produces more verbose output. ```sema (term/bold (term/red (term/underline "alert"))) ;; Produces: ESC[1m ESC[31m ESC[4m alert ESC[0m ESC[0m ESC[0m ``` Both approaches render identically in terminals, but `term/style` is cleaner. ## True Color ### `term/rgb` Apply 24-bit true color to text. Takes the text followed by red, green, and blue values (integers 0–255). ```sema (term/rgb "orange" 255 165 0) (term/rgb "coral" 255 127 80) (term/rgb "teal" 0 128 128) (term/rgb "hot pink" 255 105 180) ``` Uses the `ESC[38;2;r;g;bm` escape sequence format, which is supported by most modern terminals. ```sema ;; Build a gradient (for-each (lambda (i) (display (term/rgb "█" (* i 25) 50 (- 255 (* i 25))))) (range 11)) (println) ``` ## Stripping ANSI Codes ### `term/strip` Remove all ANSI escape sequences from a string, returning plain text. ```sema (term/strip (term/bold "hello")) ; => "hello" (term/strip (term/style "hi" :red :bold)) ; => "hi" (term/strip (term/rgb "color" 255 0 0)) ; => "color" (term/strip "no codes here") ; => "no codes here" ``` This is useful when you need plain text for logging to files, comparisons, or passing to functions that don't understand ANSI codes: ```sema ;; Write clean text to a file, styled text to terminal (define msg (term/green "Build succeeded")) (println msg) ; styled on terminal (file/write "build.log" (term/strip msg)) ; clean in log file ``` ## Spinners Animated terminal spinners for indicating progress during long-running operations. Spinners use braille animation frames (`⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏`) cycling at 80ms intervals, and render to **stderr** so they don't interfere with stdout output. ### `term/spinner-start` Start a spinner with a message. Returns an integer spinner ID used to update or stop it. ```sema (define id (term/spinner-start "Loading data...")) ``` ### `term/spinner-update` Update the message displayed next to a running spinner. ```sema (term/spinner-update id "Processing records...") (term/spinner-update id "Almost done...") ``` ### `term/spinner-stop` Stop a running spinner and optionally display a final status line. The spinner line is cleared from the terminal before the final status is printed. **Without options** — just clears the spinner: ```sema (term/spinner-stop id) ``` **With options map** — displays a final symbol and text: ```sema (term/spinner-stop id {:symbol "✔" :text "Done"}) ``` The options map supports two keys: | Key | Type | Description | |-----------|--------|--------------------------------------| | `:symbol` | string | Symbol to display (e.g., `"✔"`, `"✗"`, `"⚠"`) | | `:text` | string | Final status message | Both keys are optional. The final line is printed to stderr as `symbol text`. ### Spinner Lifecycle Example ```sema ;; Start spinner (define spinner (term/spinner-start "Fetching data...")) ;; ... do some work ... (term/spinner-update spinner "Processing results...") ;; ... do more work ... (term/spinner-update spinner "Writing output...") ;; Stop with success indicator (term/spinner-stop spinner {:symbol "✔" :text "Complete"}) ``` Multiple spinners can run concurrently — each gets a unique ID: ```sema (define s1 (term/spinner-start "Task A...")) (define s2 (term/spinner-start "Task B...")) ;; ... work ... (term/spinner-stop s1 {:symbol "✔" :text "Task A done"}) (term/spinner-stop s2 {:symbol "✔" :text "Task B done"}) ``` ## Line Input Read whole lines from standard input (cooked mode — the terminal buffers a line until Enter). Useful for simple prompts and for piping data into a script. ### `io/read-line` Block until a full line is available on stdin and return it as a string (without the trailing newline). Returns `nil` at end of input. ```sema (define name (io/read-line)) (println (str "Hello, " name)) ``` ### `io/eof?` Return `#t` once stdin has hit end of input (set when `io/read-line` / `io/read-stdin` / `io/read-key` returns `nil`). Pair it with `io/read-line` to consume piped input line by line: ```sema (let loop () (let ((line (io/read-line))) (unless (io/eof?) (println (string/upper line)) (loop)))) ``` ## Raw-Mode Input Primitives for building interactive TUIs: per-keystroke input, EOF detection, and signal-aware event loops. **Unix only** — these functions are no-op stubs on Windows. In cooked mode (the default), the terminal driver buffers a whole line and only delivers it to your program when the user hits Enter. Raw mode disables that — every key press, including Ctrl-C and arrow keys, is delivered as it happens. Pair these with `sys/term-size` and `sys/on-signal` (in the [System](system) docs) to build full TUIs. ### `io/tty-raw!` Put stdin into raw mode. Returns an **integer restore-token** on success, or `nil` if stdin is not a TTY (e.g., when input is piped from a file). Always pair with `io/tty-restore!` so the user's shell isn't left in raw mode if your program crashes. ```sema (define tok (io/tty-raw!)) (when tok ;; ... read keys, draw UI ... (io/tty-restore! tok)) ``` ### `io/tty-restore!` Restore the TTY to cooked mode using the token returned by `io/tty-raw!`. ```sema (io/tty-restore! tok) ``` ### `io/read-key` Block until a single keypress arrives, then return a map describing it. Returns `nil` on EOF (after which `io/eof?` returns `#t`). ```sema (io/read-key) ;; => {:kind :char :char "a"} ``` The map's `:kind` field is one of: | `:kind` | Other keys | Meaning | |-----------|-------------------------|-------------------------------------------------| | `:char` | `:char` (string) | A printable character (UTF-8 multi-byte handled) | | `:ctrl` | `:char` (string) | Ctrl + letter (e.g., Ctrl-C → `{:kind :ctrl :char "c"}`) | | `:alt` | `:char` (string) | Alt/Meta + character (ESC + char; UTF-8 aware) | | `:key` | `:name` (keyword), optional `:mods` | Named key — see below | | `:mouse` | `:action` `:x` `:y` `:button` `:mods` | A mouse event (after `term/enable-mouse`) | | `:paste` | `:text` (string) | A bracketed paste (after `term/enable-bracketed-paste`) | | `:focus` | `:focused` (bool) | Focus gained/lost (after `term/enable-focus-events`) | | `:cpr` | `:row` `:col` | Cursor-position report (reply to `term/query-cursor-position`) | | `:device-attributes` | `:device` `:params` | Reply to `term/query-primary-da` / `-secondary-da` | | `:kitty-flags` | `:flags` (int) | Reply to `term/query-kitty-keys` | Named keys (`:kind :key`): `:enter` `:tab` `:backspace` `:esc` `:up` `:down` `:left` `:right` `:home` `:end` `:insert` `:delete` `:page-up` `:page-down` `:shift-tab` `:f1`–`:f12` CSI/SS3 escape sequences (arrows, F1–F12, Insert, Home/End, Page Up/Down, Delete) and UTF-8 continuation bytes are decoded for you. Modifier-carrying keys include an optional `:mods` list — e.g. Ctrl+Right → `{:kind :key :name :right :mods (:ctrl)}`, Shift+F5 → `{:kind :key :name :f5 :mods (:shift)}`. **Mouse** (after `term/enable-mouse`): SGR reports decode to `{:kind :mouse :action A :x col :y row :button N :mods (…)}`, where `A` is one of `:press` `:release` `:move` `:wheel-up` `:wheel-down` `:wheel-left` `:wheel-right`, coordinates are 1-based, and `:mods` (omitted when empty) lists `:shift`/`:alt`/`:ctrl`. **Kitty keyboard** (after `term/enable-kitty-keys!`, restore with `term/disable-kitty-keys!`): richer key events decode to the *same* `:char`/`:ctrl`/`:alt`/`:key` shapes above — so existing code is unaffected — plus an optional full `:mods` list (`:shift` `:alt` `:ctrl` `:super` `:hyper` `:meta` `:caps-lock` `:num-lock`), and, when the matching flags are enabled, `:event :press|:repeat|:release` and `:shifted-key`/`:base-key`. Mouse, kitty, paste, and focus decoding are all opt-in; plain keys are byte-identical either way. ### `io/read-key-timeout` Like `io/read-key`, but returns `nil` after `timeout-ms` milliseconds with no input. Backed by `select(2)`, so it doesn't burn CPU. ```sema (io/read-key-timeout 100) ; => key map, or nil after 100ms ``` Use this to drive an animation loop or to poll signals between renders: ```sema (let loop () (sys/check-signals) (let ((key (io/read-key-timeout 50))) (when key (handle-key key)) (loop))) ``` ### Terminal modes, paste, focus & queries Opt-in terminal features. Enabling a mode makes the terminal send extra reports that `io/read-key` decodes into the `:paste`/`:focus`/`:cpr`/`:device-attributes`/ `:kitty-flags` events above. Each has an `enable`/`disable` pair plus a `with-` guard that restores automatically on exit (even if the body throws). | Feature | Enable / disable | Guard | `io/read-key` event | |---------|------------------|-------|---------------------| | Mouse | `term/enable-mouse` / `term/disable-mouse` | `term/with-mouse` | `:mouse` | | Bracketed paste | `term/enable-bracketed-paste` / `term/disable-bracketed-paste` | `term/with-bracketed-paste` | `:paste` | | Focus events | `term/enable-focus-events` / `term/disable-focus-events` | `term/with-focus-events` | `:focus` | | Kitty keyboard | `term/enable-kitty-keys!` `[flags]` / `term/disable-kitty-keys!` | `term/with-kitty-keys` | richer `:key`/`:char` (+ `:mods`, `:event`) | **Bracketed paste** matters most for a prompt: without it, pasting multi-line text feeds the newlines in as Enter keys. With it, the whole paste arrives as one `{:kind :paste :text "…"}`. ```sema (io/with-raw-mode (term/with-bracketed-paste (let loop () (let ((ev (io/read-key))) (when ev (match (:kind ev) (:paste (insert-text (:text ev))) (:key (handle-key ev)) (_ nil)) (loop)))))) ``` **Queries and capability detection.** Some requests get a reply from the terminal that arrives as a later `io/read-key` event; others round-trip synchronously (they read the reply themselves, so call them in raw mode): | Function | Result | |----------|--------| | `term/query-cursor-position` | writes DSR; reply arrives as `{:kind :cpr :row :col}` | | `term/cursor-position` | round-trips synchronously → `{:row :col}` or `nil` | | `term/query-primary-da` / `term/query-secondary-da` | reply as `{:kind :device-attributes …}` | | `term/query-kitty-keys` | reply as `{:kind :kitty-flags :flags N}` | | `term/supports-kitty-keys?` | synchronous `#t`/`#f` (spec-recommended detection) | ```sema (io/with-raw-mode (when (term/supports-kitty-keys?) (term/enable-kitty-keys!)) (term/cursor-position)) ; => {:row 12 :col 40} ``` ::: warning tmux Inside tmux, kitty forwarding, focus events, mouse, and paste passthrough are all off by default, so auto-detection can silently fail. Prefer letting the user force-enable when `$TMUX` is set. ::: ### Minimal TUI skeleton Assumes interactive stdin — `io/tty-raw!` returns `nil` when stdin isn't a TTY, so guard with `when tok` if the program may run with input piped from a file. ```sema (define tok (io/tty-raw!)) (when tok (sys/on-signal :winch (fn () (redraw (sys/term-size)))) (sys/on-signal :int (fn () (io/tty-restore! tok) (exit 0))) (let loop () (sys/check-signals) (let ((key (io/read-key))) (cond ((nil? key) ; EOF (io/tty-restore! tok)) ((and (= (:kind key) :ctrl) (= (:char key) "c")) ; Ctrl-C (io/tty-restore! tok)) (else (handle-key key) (loop)))))) ``` ## Common Patterns ### Colored Log Levels ```sema (define (log-error msg) (println (term/style "✗ ERROR" :bold :red) " " msg)) (define (log-warn msg) (println (term/style "⚠ WARN " :bold :yellow) " " msg)) (define (log-info msg) (println (term/style "ℹ INFO " :bold :blue) " " msg)) (define (log-success msg) (println (term/style "✔ OK " :bold :green) " " msg)) (log-error "Connection refused") (log-warn "Retrying in 5s") (log-info "Connecting to server") (log-success "Connected") ``` ### CLI Status Output ```sema (define (print-step label detail) (println (term/style label :bold :cyan) " " (term/dim detail))) (print-step "Compile" "src/main.sema") (print-step "Link" "3 modules") (print-step "Write" "build/output") ``` ### Progress with Spinners ```sema (define steps '("Downloading" "Extracting" "Installing" "Configuring")) (define sp (term/spinner-start "Starting...")) (for-each (lambda (step) (term/spinner-update sp (string/append step "...")) (sleep 1000)) steps) (term/spinner-stop sp {:symbol "✔" :text "Installation complete"}) ``` ### Conditional Styling ```sema (define (color-status code) (cond ((< code 300) (term/green (number/to-string code))) ((< code 400) (term/yellow (number/to-string code))) (else (term/red (number/to-string code))))) (println "Status: " (color-status 200)) ; green "200" (println "Status: " (color-status 301)) ; yellow "301" (println "Status: " (color-status 404)) ; red "404" ``` ## Screen control Beyond styling, these emit ANSI/VT control sequences so you never hand-write escape codes. Each self-flushes. ```sema (term/enter-alt-screen) ; switch to a clean alternate screen (term/hide-cursor) (term/clear) (term/write-at 3 5 (term/rgb "status: ok" 200 168 85)) ; row, col, text (term/move-to 1 1) (term/set-title "my app") (term/show-cursor) (term/leave-alt-screen) ; restore the user's scrollback ``` Also: `term/clear-line`, `term/clear-below`, `term/cursor-home`, `term/save-cursor`, `term/restore-cursor`, `term/enable-mouse`, `term/disable-mouse`, `term/bell`, `term/flush`. ### Setup guards Setting up a TUI leaves the terminal in a fragile state — if your program exits (or crashes) without restoring, the shell is left in raw mode, the alt screen, or with mouse reporting spewing escape codes. Guard macros **always** restore on exit, even if the body throws: ```sema (io/with-raw-mode ; restores cooked mode (term/with-alt-screen ; restores the screen + cursor (term/with-mouse ; disables mouse reporting (run-tui)))) ; terminal is fully restored however this exits ``` Compose them outermost-restores-last. Each returns the body's value. --- --- url: 'https://sema-lang.com/docs/stdlib/playground.md' --- # Playground & WASM When running in the browser playground at [sema.run](https://sema.run), Sema executes as WebAssembly. Most stdlib functions work identically, but some behave differently due to browser sandbox constraints, and a few web-only functions are available. ::: warning Chromium ARM64 Note Chrome/Chromium builds before version 147 contain a V8 ARM64 WebAssembly compiler bug that can crash the renderer on some heavy tree-walker workloads in the playground. If you hit a reproducible tab crash on Apple Silicon, update Chrome or retry in Firefox, Safari, or Chrome 147+. ::: ## Web-Only Functions These functions are **only available in the WASM playground** — they access browser APIs that don't exist in the native CLI. ### `web/user-agent` Return the browser's `navigator.userAgent` string. Works in all browsers. ```sema (web/user-agent) ; => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ..." ``` ### `web/user-agent-data` Return structured browser information from `navigator.userAgentData`. Returns a map on Chromium-based browsers (Chrome, Edge, Opera), `nil` on Firefox and Safari. ```sema (web/user-agent-data) ; Chromium => {:mobile false :platform "macOS" :brands ("Chromium/120" "Google Chrome/120")} ; Firefox/Safari => nil ``` ::: tip `userAgentData` is the modern replacement for UA string parsing — it returns structured, reliable data instead of a messy string. However, it's Chromium-only. Use `web/user-agent` for cross-browser compatibility. ::: ## WASM Behavior Differences ### System Information System functions return web-appropriate values instead of OS-specific ones: | Function | Native | WASM | | ------------------ | ----------------------------------- | --------------------------- | | `sys/platform` | `"macos"` / `"linux"` / `"windows"` | `"web"` | | `sys/os` | `"macos"` | `"web"` | | `sys/arch` | `"aarch64"` / `"x86_64"` | `"wasm32"` | | `sys/cwd` | Current directory path | `"/"` | | `sys/interactive?` | `#t` in REPL | `#f` | | `sys/pid` | Process ID | `0` | | `sys/elapsed` | Nanoseconds since process start | Nanoseconds since page load | | `time-ms` | `SystemTime` milliseconds | `Date.now()` milliseconds | These always return `nil` in WASM: `sys/hostname`, `sys/user`, `sys/home-dir`, `sys/which`, `sys/tty`. ### File I/O (Virtual Filesystem) File operations work against an **in-memory virtual filesystem** (VFS). Files persist for the duration of your session but are **lost on page reload**. ```sema ;; These all work in the playground (file/write "/hello.txt" "Hello from WASM!") (file/read "/hello.txt") ; => "Hello from WASM!" (file/exists? "/hello.txt") ; => #t (file/mkdir "/mydir") (file/is-directory? "/mydir") ; => #t (file/list "/") ; => ("hello.txt") ``` All file functions are supported: `file/read`, `file/write`, `file/append`, `file/delete`, `file/rename`, `file/copy`, `file/exists?`, `file/list`, `file/mkdir`, `file/is-file?`, `file/is-directory?`, `file/is-symlink?`, `file/info`, `file/read-lines`, `file/write-lines`. Path functions (`path/join`, `path/dirname`, `path/basename`, `path/extension`, `path/absolute`) also work. **Quotas**: The VFS enforces limits to prevent runaway memory usage — 1 MB per file, 16 MB total, and 256 files max. Exceeding these limits returns an error. The `load` function reads from the VFS and evaluates the parsed expressions. ### Terminal Styling All `term/*` functions work but return text **without ANSI formatting** (since the browser has no terminal): ```sema (term/bold "hello") ; => "hello" (no bold applied) (term/red "error") ; => "error" (no color applied) (term/style "hi" :bold :cyan) ; => "hi" ``` ### HTTP Functions HTTP functions work in the playground via the browser's `fetch()` API. They return the same `{:status :headers :body}` map as the native CLI. ```sema (define resp (http/get "https://httpbin.org/get")) (:status resp) ; => 200 (:body resp) ; => "{\"args\": {}, ...}" (http/post "https://httpbin.org/post" {:name "sema"}) ; => {:status 200 :headers {...} :body "..."} ``` All HTTP functions are supported: `http/get`, `http/post`, `http/put`, `http/delete`, `http/request`. ::: warning CORS Restrictions Browser security rules (CORS) may block requests to servers that don't include `Access-Control-Allow-Origin` headers. Public APIs like httpbin.org work fine. If you get a network error, the target server likely doesn't allow cross-origin requests. ::: ### Not Available in WASM These functions return an error when called in the playground: | Function | Reason | | ------------ | -------------------------------------------- | | `shell` | No subprocess execution in browser | | `exit` | No process to exit | | `io/read-line` | No stdin in browser | | `io/read-stdin` | No stdin in browser | | `sleep` | Cannot block the browser main thread (no-op) | --- --- url: 'https://sema-lang.com/docs/stdlib/streams.md' --- # Streams Streams are first-class byte-oriented I/O handles for reading and writing data incrementally. They provide a unified interface across files, in-memory buffers, strings, and standard I/O — the same `stream/read` and `stream/write` work regardless of the underlying source. ::: tip Sandbox capabilities In-memory streams require no capability. In a sandboxed run, `stream/open-input` requires `fs-read` and `stream/open-output` requires `fs-write`; either returns `PermissionDenied` when its capability is denied. File streams work without extra configuration in Sema's default mode. See the [CLI sandbox documentation](/docs/cli#sandbox). ::: ```sema ;; Read a file line by line (with-stream (s (stream/open-input "data.txt")) (let loop ((line (stream/read-line s))) (when line (println line) (loop (stream/read-line s))))) ;; In-memory buffer (let ((buf (stream/byte-buffer))) (stream/write-string buf "hello") (stream/to-string buf)) ;; => "hello" ``` ## Creating Streams ### `stream/from-string` Create a read-only stream from a string's UTF-8 bytes. ```sema (define s (stream/from-string "hello world")) (stream/read-byte s) ;; => 104 (ASCII 'h') (stream/read s 5) ;; => #u8(101 108 108 111 32) ("ello ") ``` ### `stream/from-bytes` Create a readable stream from a bytevector. ```sema (define s (stream/from-bytes (bytevector 1 2 3))) (stream/read-byte s) ;; => 1 (stream/read-byte s) ;; => 2 ``` ### `stream/byte-buffer` Create a read/write in-memory buffer. Writes append to the buffer; reads consume from the current position. ```sema (define buf (stream/byte-buffer)) (stream/write buf (string->utf8 "hello")) (stream/to-string buf) ;; => "hello" ``` ### `stream/open-input` Open a file for reading. Returns a buffered input stream. Requires `fs-read` in a sandboxed run. ```sema (define s (stream/open-input "data.csv")) (define contents (stream/read-all s)) (stream/close s) ``` ### `stream/open-output` Open (or create) a file for writing. Returns a buffered output stream. Requires `fs-write` in a sandboxed run. ```sema (define s (stream/open-output "output.txt")) (stream/write-string s "hello world\n") (stream/close s) ``` ## Reading ### `stream/read` Read up to `n` bytes, returning a bytevector. Returns fewer bytes at EOF. ```sema (stream/read s 1024) ;; => bytevector (up to 1024 bytes) ``` ### `stream/read-byte` Read a single byte. Returns an integer 0–255, or `nil` at EOF. ```sema (stream/read-byte s) ;; => 65 (or nil at EOF) ``` ### `stream/read-line` Read until newline (`\n`), returning a string without the newline. Strips trailing `\r` for Windows line endings. Returns `nil` at EOF. ```sema (stream/read-line s) ;; => "first line" (or nil) ``` ### `stream/read-all` Read the stream into a bytevector. An optional byte cap defaults to 256 MiB; the call fails before growing its result beyond the cap. ```sema (define data (stream/read-all s (* 8 1024 1024))) ; 8 MiB maximum (utf8->string data) ; convert to string if text ``` ## Writing ### `stream/write` Write a bytevector. Returns the number of bytes written. ```sema (stream/write s (bytevector 72 101 108 108 111)) ;; => 5 ``` ### `stream/write-byte` Write a single byte (integer 0–255). ```sema (stream/write-byte s 10) ; write a newline ``` ### `stream/write-string` Write a string as UTF-8 bytes. Returns the number of bytes written. ```sema (stream/write-string s "hello") ;; => 5 ``` ## Control ### `stream/close` Close a stream, releasing the underlying resource. Double-close is a no-op. ```sema (stream/close s) (stream/close s) ; safe, does nothing ``` ### `stream/flush` Flush any buffered output to the underlying sink. ```sema (stream/flush s) ``` ### `stream/copy` Copy bytes from one stream to another. Returns total bytes copied. An optional byte cap defaults to 256 MiB, and the first over-limit chunk is rejected before it is written. Inside the cooperative runtime, stdin remains cancellable and a copy with one file-backed side is offloaded. File-to-file copy requires two resource gates and therefore fails promptly; use bounded `stream/read`/`stream/write` chunks for that case. ```sema (let ((in (stream/from-string "hello")) (out (stream/byte-buffer))) (stream/copy in out 1024)) ;; => 5 ``` ## Introspection ### `stream?` Type predicate — returns `#t` if the value is a stream. ```sema (stream? (stream/byte-buffer)) ;; => #t (stream? 42) ;; => #f ``` ### `stream/readable?`, `stream/writable?` Check the direction of a stream. ```sema (stream/readable? (stream/from-string "x")) ;; => #t (stream/writable? (stream/from-string "x")) ;; => #f (stream/writable? (stream/byte-buffer)) ;; => #t ``` ### `stream/available?` Returns `#t` if data is ready to read without blocking. ```sema (stream/available? (stream/from-string "x")) ;; => #t (stream/available? (stream/from-string "")) ;; => #f ``` ### `stream/type` Returns a string describing the stream implementation. ```sema (stream/type (stream/byte-buffer)) ;; => "byte-buffer" (stream/type (stream/from-string "x")) ;; => "string" (stream/type (stream/open-input "f.txt")) ;; => "file-input" (stream/type *stdout*) ;; => "stdout" ``` ## Extraction (Byte Buffers) ### `stream/to-bytes` Extract the accumulated contents of a byte-buffer stream as a bytevector. ```sema (let ((s (stream/byte-buffer))) (stream/write s (bytevector 1 2 3)) (stream/to-bytes s)) ;; => #u8(1 2 3) ``` ### `stream/to-string` Extract the contents of a byte-buffer stream as a UTF-8 string. ```sema (let ((s (stream/byte-buffer))) (stream/write-string s "hello") (stream/to-string s)) ;; => "hello" ``` ## Standard I/O Three global streams are available for console I/O: | Stream | Direction | Description | |--------|-----------|-------------| | `*stdin*` | Readable | Standard input | | `*stdout*` | Writable | Standard output | | `*stderr*` | Writable | Standard error | ```sema (stream/write-string *stdout* "prompt> ") (stream/flush *stdout*) (stream/write-string *stderr* "warning: something happened\n") ``` ## Resource Management ### `with-stream` Macro that binds a stream, executes the body, and automatically closes the stream on exit — even if an error is thrown. ```sema (with-stream (s (stream/open-input "data.txt")) (stream/read-all s)) ;; s is closed here, even if read-all threw an error ;; Write to a file (with-stream (out (stream/open-output "output.txt")) (stream/write-string out "line 1\n") (stream/write-string out "line 2\n")) ;; file is flushed and closed ``` ## Patterns ### Line-by-Line Processing ```sema (with-stream (s (stream/open-input "log.txt")) (let loop ((line (stream/read-line s)) (count 0)) (if (nil? line) count (loop (stream/read-line s) (+ count 1))))) ``` ### Building a String Incrementally ```sema (let ((buf (stream/byte-buffer))) (stream/write-string buf "{") (stream/write-string buf "\"key\": \"value\"") (stream/write-string buf "}") (stream/to-string buf)) ;; => "{\"key\": \"value\"}" ``` ### File Copy ```sema (with-stream (in (stream/open-input "photo.jpg")) (with-stream (out (stream/open-output "backup.jpg")) (let loop ((total 0)) (let ((chunk (stream/read in 8192))) (if (= (bytes/length chunk) 0) total (begin (stream/write out chunk) (loop (+ total (bytes/length chunk))))))))) ``` ## Error Handling Reading a closed stream or writing to a read-only stream throws an error caught with `try`/`catch`: ```sema (try (let ((s (stream/from-string "x"))) (stream/close s) (stream/read s 1)) ; throws "stream is closed" (catch e (println (str "Error: " e)))) ``` --- --- url: 'https://sema-lang.com/docs/stdlib/concurrency.md' --- # Concurrency Cooperative async concurrency with promises and channels. Tasks run on the VM's cooperative scheduler, interleaving at yield points (channel operations, `await`, `sleep`). ## Scheduling guarantees * **Spawn order is preserved.** When several tasks are simultaneously ready to run, the scheduler picks them in the order they were spawned. A pipeline of `(async (send-1)) (async (send-2)) (async (send-3))` followed by sequential receives yields `1 2 3`, not a reordered surface. * **Wake order is FIFO.** When a value becomes available on a channel, the longest-waiting receiver is woken first. * **Cooperation, not parallelism.** Tasks interleave at yield points (channel ops, `await`, `sleep`). CPU-bound tasks without yield points run to completion before other tasks get a turn. ## Promises ### `async/spawn` ```sema (async/spawn thunk) → async-promise ``` Spawn a zero-argument function as an async task. Returns a promise that resolves when the task completes. ```sema (define p (async/spawn (fn () (+ 1 2)))) (async/await p) ; => 3 ``` Usually called via the `async` special form: ```sema (define p (async (+ 1 2))) (await p) ; => 3 ``` ### `async/await` ```sema (async/await promise) → value ``` Wait for a promise to resolve. Inside an async task, yields to the scheduler. At the top level, runs the scheduler inline until the promise resolves. Raises an error if the promise was rejected. ### `async/all` ```sema (async/all promises) → list ``` Run all promises to completion and return a list of their results. Takes a list or vector of promises. ```sema (let ((p1 (async 10)) (p2 (async 20)) (p3 (async 30))) (async/all (list p1 p2 p3))) ; => (10 20 30) ``` ### `async/race` ```sema (async/race promises) → value ``` Return the value of the first promise to resolve. Takes a list or vector of promises. ### `async/resolved` ```sema (async/resolved value) → async-promise ``` Create an already-resolved promise wrapping `value`. ### `async/rejected` ```sema (async/rejected message) → async-promise ``` Create an already-rejected promise with `message`. ### `async/run` ```sema (async/run) ``` Run all pending async tasks to completion. ### `async/sleep` ```sema (async/sleep ms) ``` Inside an async task, yield for `ms` milliseconds on the scheduler's **virtual clock**. The clock only advances when every task is blocked, jumping to the nearest deadline — so a shorter sleep always wakes before a longer one, deterministically. The scheduler then waits the real time when it advances: on native via `thread::sleep`, and in the **browser playground** by running eval on a Web Worker that blocks on `Atomics.wait` (so a sleep really pauses while the page stays responsive). Browsers without cross-origin isolation fall back to advancing the clock instantly — durations still order tasks correctly, just without the real wait. Outside async, calls `thread::sleep` on native. Durations are capped at `86_400_000` ms (1 day). ### `async/timeout` ```sema (async/timeout ms promise) → value ``` Wait for `promise` to resolve, but raise an error if it takes longer than `ms` milliseconds. On expiry the target task **is cancelled** — and any in-flight offloaded I/O it holds is aborted for real (an HTTP connection is torn down, a subprocess is killed; LLM calls are best-effort — see [`async/cancel`](#async-cancel)). So a timed-out `http/get`/`shell` stops consuming resources immediately rather than running to completion in the background. ```sema (async/timeout 100 (async (do-slow-work))) ;; raises: async/timeout: operation timed out ``` A `ms = 0` (or very short) timeout still lets work that is **synchronously ready** finish — it only fires once the virtual clock actually reaches the deadline with the task still pending (i.e. the task had to block/wait). Durations are capped at `86_400_000` ms (1 day). ### `async/cancel` ```sema (async/cancel promise) → bool ``` Request cancellation of a spawned task. Returns `#t` if the call actually transitioned the promise into the `Cancelled` state, `#f` if there was nothing to cancel — the promise was already terminal (resolved, rejected, previously cancelled) or was never spawned in the first place (e.g. created via `async/resolved`). Cancellation is best-effort and never errors. The next time the task hits a yield point it transitions to `Cancelled`; subsequent `(await p)` raises `"async/await: task was cancelled"` (distinct from a normal rejection). **What actually gets aborted.** If the cancelled task is parked on offloaded I/O, the underlying work is aborted where the runtime allows it: * `http/*` — the in-flight request's future is dropped, **tearing down the connection** (no wasted round-trip). * `shell` — the subprocess is **killed** (`SIGKILL`), not left running in the background. * `llm/*` (`embed`, `complete`, `classify`, `extract`) — **best-effort**: the request runs on a blocking worker that can't be interrupted mid-call, so the in-flight call completes and its result is discarded. A multi-round caller stops issuing further rounds. ```sema (async/cancel (async/resolved 1)) ;; => #f (never spawned) (let ((p (async 42))) (await p) (async/cancel p)) ;; => #f (already resolved) (let ((p (async (async/sleep 100)))) (async/cancel p)) ;; => #t ``` ### `async/cancelled?` ```sema (async/cancelled? promise) → bool ``` `#t` if `promise` is in the `Cancelled` state — distinct from `async/rejected?`. Matches the state variant directly rather than the rejection message, so a user `(async/rejected "cancelled")` no longer aliases: ```sema (async/cancelled? (async/rejected "cancelled")) ;; => #f ``` ### Promise predicates The four predicates **partition** the terminal states: a promise is at most one of resolved / rejected / cancelled, and `pending?` is the complement of those three. | Function | Description | | --- | --- | | `(async/promise? x)` | Is `x` an async promise? | | `(async/resolved? p)` | Is promise `p` resolved? | | `(async/rejected? p)` | Is promise `p` rejected? (excludes cancelled) | | `(async/pending? p)` | Is promise `p` still pending? | | `(async/cancelled? p)` | Was promise `p` cancelled? | ### `async/pool-map` ```sema (async/pool-map f items n) → list ``` Map `f` over `items` with **bounded concurrency**: at most `n` calls run at once, results returned in input order. A semaphore (an `n`-capacity channel) gates how many tasks are in flight, so you can fan a large batch across a rate-limited resource without launching everything at once. The token is released on both success and error, so a failing item never deadlocks the pool. ```sema ;; Embed 10 000 chunks, but only 8 requests in flight at a time: (async/pool-map (fn (chunk) (llm/embed chunk)) chunks 8) ;; Fetch many URLs, 16 at a time: (async/pool-map (fn (u) (http/get u)) urls 16) ``` ### `async/map` ```sema (async/map f items) → list ``` Concurrent `map`: apply `f` to each item in its **own** task, results in input order. The unbounded sibling of `async/pool-map` (no cap — every item gets a task at once). Use `async/pool-map` when you need to limit how many run together. ```sema (async/map (fn (u) (http/get u)) urls) ; fetch every url concurrently (async/map (fn (i) (* i i)) '(1 2 3 4)) ; => (1 4 9 16) ``` ### `async/spawn-all` ```sema (async/spawn-all thunks) → list ``` Spawn a list of zero-arg functions concurrently and await them all, in input order — the ergonomic form of `(async/all (map (fn (th) (async/spawn th)) thunks))`. ```sema (async/spawn-all (list (fn () (http/get a)) (fn () (http/get b)))) ``` ## Concurrent I/O — what actually overlaps The scheduler's payoff is **latency overlap**: when several tasks each wait on I/O, the waits happen *simultaneously* instead of one after another. The blocking leaves below now yield to the scheduler while their work runs on a background runtime, so spawning them as tasks (via `async/spawn` + `async/all`, or `async/pool-map`) makes wall-clock approach `max(latency)` instead of `sum(latency)`: | Operation | Overlaps when spawned concurrently | | --- | --- | | `http/get` and the other `http/*` verbs | ✅ | | `shell` (subprocess) | ✅ | | `llm/embed` | ✅ | | `llm/complete`, `llm/classify`, `llm/extract` | ✅ | ```sema ;; Four independent LLM calls — concurrent, not serial: (async/all (map (fn (q) (async/spawn (fn () (llm/complete q)))) '("summarize A" "summarize B" "summarize C" "summarize D"))) ;; wall-clock ≈ one call, not four. ``` Outside a scheduler task (a plain top-level call) these run **synchronously**, byte-identical to before — the concurrency only engages inside `async`/`async/spawn`. Tasks still interleave at I/O boundaries on the single VM thread; this is cooperative concurrency, not parallel CPU execution. **Tracing nests across spawns.** Spans (`with-span`, the auto-instrumented `llm/*` spans) opened inside a spawned task nest under the spawning task's active span and share its trace — so `(with-span "batch" (async/map llm/complete prompts))` shows up as one connected tree in Jaeger/Phoenix/Langfuse (the `batch` span with the concurrent LLM spans beneath it), not a pile of disconnected single-span traces. Each task still keeps its own span stack, so concurrent spans never cross-contaminate. A spawn at the top level (no active span) starts its own trace. ## Channels Bounded FIFO channels for communication between async tasks. ### `channel/new` ```sema (channel/new) → channel ; capacity 1 (channel/new capacity) → channel ``` Create a bounded channel. Default capacity is 1. Capacity must be at least 1. ### `channel/send` ```sema (channel/send ch value) ``` Send a value to the channel. If the channel is full and inside an async task, yields until space is available. Outside async context, raises an error if full. Raises an error if the channel is closed. ### `channel/recv` ```sema (channel/recv ch) → value ``` Receive a value from the channel. If the channel is empty and inside an async task, yields until data is available. Outside async context, raises an error if empty. Returns `nil` if the channel is closed and empty. ### `channel/try-recv` ```sema (channel/try-recv ch) → value | nil ``` Non-blocking receive. Returns the next value or `nil` if the channel is empty. ### `channel/close` ```sema (channel/close ch) ``` Close the channel. Subsequent sends will error. Blocked receivers will wake with `nil`. ### Channel predicates | Function | Description | | --- | --- | | `(channel? x)` | Is `x` a channel? | | `(channel/closed? ch)` | Is the channel closed? | | `(channel/empty? ch)` | Is the channel buffer empty? | | `(channel/full? ch)` | Is the channel buffer at capacity? | | `(channel/count ch)` | Number of values in the buffer | ## Examples ### Producer/Consumer ```sema (let ((ch (channel/new 1))) (let ((producer (async (channel/send ch 10) (channel/send ch 20) (channel/send ch 30) (channel/close ch))) (consumer (async (let loop ((sum 0)) (let ((val (channel/recv ch))) (if (nil? val) sum (loop (+ sum val)))))))) (await consumer))) ; => 60 ``` ### Parallel computation ```sema (let ((p1 (async (fib 30))) (p2 (async (fib 31)))) (+ (await p1) (await p2))) ``` See [Scheduling guarantees](#scheduling-guarantees) above for the full ordering / cooperation rules. ## Async ops inside higher-order functions Stdlib higher-order functions like `for-each`, `map`, `filter`, `foldl`, `sort-by`, `apply`, `reduce`, `partition`, `any`, `every` can call **lambdas** that perform async operations (`channel/send`, `channel/recv`, `await`, `async/sleep`). The yield suspends inside the callback and resumes correctly: ```sema (let ((ch (channel/new 3))) (let ((producer (async (for-each (fn (n) (channel/send ch n)) (list 1 2 3 4 5 6 7)) (channel/close ch))) (consumer (async (let loop ((sum 0)) (let ((v (channel/recv ch))) (if (nil? v) sum (loop (+ sum v)))))))) (await consumer))) ;; => 28 ``` Yielding **native** functions (e.g., `channel/recv`, `async/sleep`) passed *directly* as the callback produce a clear error pointing to the workaround — when the native actually needs to suspend inside a scheduler task (i.e. in async context; at the top level a ready `channel/recv` just returns): ```sema ;; Inside a task — errors: yielding native passed directly to a higher-order function (await (async (map async/sleep (list 1 1 1)))) ;; Correct: wrap the native in a lambda (await (async (map (fn (ms) (async/sleep ms)) (list 1 1 1)))) ``` ## Event loop `event/select` polls a list of sources and returns the first that's ready (or `nil` on timeout) — the unified wait a TUI loop needs, over keypresses, subprocess output, and timers. ```sema (define proc (proc/spawn ["make" "watch"])) (let loop () (let ((ev (event/select (list {:type :key} ; a keypress {:type :proc :handle proc} ; output or exit (time/tick 16)) ; ~60fps redraw tick 1000))) ; ms timeout (cond ((nil? ev) (loop)) ; timed out ((= (:type ev) :key) (handle-key (:value ev))) ((= (:type ev) :proc) (drain-output proc)) ((= (:type ev) :timer) (redraw))) (loop))) ``` --- --- url: 'https://sema-lang.com/docs/stdlib/records.md' --- # Records Records are **user-defined, named product types** created with the `define-record-type` special form. They provide constructors, type predicates, and field accessors. ::: tip Records vs Maps If you need an *open* data shape that's easy to serialize and manipulate generically, use [maps](./maps). If you want a *closed* domain type with a predicate and fixed fields, use records. ::: ## Defining Record Types ### `define-record-type` Define a new record type, generating a constructor, predicate, and one accessor per field. ```sema (define-record-type point (make-point x y) ; constructor (positional args) point? ; predicate (x point-x) ; (field-name accessor-name) (y point-y)) ``` General syntax: ```sema (define-record-type ( ...) ( ) ...) ``` ### What Gets Defined For the `point` example above: | Binding | Signature | Purpose | |---------|-----------|---------| | `make-point` | `(x y) → point` | Constructor | | `point?` | `(value) → bool` | Type predicate | | `point-x` | `(point) → value` | Field accessor | | `point-y` | `(point) → value` | Field accessor | ```sema (define p (make-point 3 4)) (point? p) ; => #t (point? 42) ; => #f (point-x p) ; => 3 (point-y p) ; => 4 ``` ### Constructor Arity The constructor is positional — its arity must match exactly: ```sema (make-point 1 2) ; ok (make-point 1) ; error: wrong arity (make-point 1 2 3) ; error: wrong arity ``` ### Immutability Sema records are immutable. To "update" a record, construct a new one: ```sema (define (move-point p dx dy) (make-point (+ (point-x p) dx) (+ (point-y p) dy))) (move-point (make-point 10 20) 5 -2) ; => a new point record with x=15, y=18 ``` ## Equality Two records are `equal?` if they have the **same type** and their fields are pairwise `equal?`: ```sema (define a (make-point 1 2)) (define b (make-point 1 2)) (define c (make-point 9 9)) (equal? a b) ; => #t (same type, same fields) (equal? a c) ; => #f (same type, different fields) ``` Records of different types are never equal, even if they have the same field values. ## Introspection ### `record?` Test if a value is any record instance (of any record type). ```sema (record? (make-point 3 4)) ; => #t (record? {:x 3 :y 4}) ; => #f (record? 42) ; => #f ``` ### `type` Return the type of a value as a keyword. For records, returns the record's type name: ```sema (type (make-point 3 4)) ; => :point (type [1 2 3]) ; => :vector (type {:a 1}) ; => :map ``` ## Records vs Maps Both model "structured data", but they serve different purposes. ### Use records when… * You want a **distinct type**: `person?`, `invoice?`, `token?` * Your data has a **fixed schema** enforced at construction * You want named field accessors and clear domain boundaries ### Use maps when… * You need easy **serialization** (JSON, TOML, etc.) * You want to add/remove keys dynamically * You want generic operations like `get`, `assoc`, `merge`, `keys`, `map/get-in`, `map/update-in` * You're interacting with external APIs ::: tip Common pattern **Maps at the boundary, records internally.** Parse/validate external maps into records early, and convert records back to maps for output. ::: ## Nested Records Records can contain any values, including other records: ```sema (define-record-type address (make-address line1 city country) address? (line1 address-line1) (city address-city) (country address-country)) (define-record-type user (make-user id name addr) user? (id user-id) (name user-name) (addr user-addr)) (define u (make-user 123 "Ada" (make-address "12 St James" "London" "UK"))) (user-name u) ; => "Ada" (address-city (user-addr u)) ; => "London" ``` ## Pattern Matching with Records Records don't have a dedicated pattern form, but you can use binding patterns with `when` guards: ```sema (define (describe v) (match v (p when (point? p) (string/append "point(" (number/to-string (point-x p)) ", " (number/to-string (point-y p)) ")")) (_ "not a point"))) (describe (make-point 3 4)) ; => "point(3, 4)" (describe {:x 3 :y 4}) ; => "not a point" ``` You can also match on `type`: ```sema (define (record-type-name v) (match (type v) (:point "a point") (:person "a person") (_ "something else"))) ``` ## Domain Modeling Example Use records to represent values that have been validated: ```sema (define-record-type email (make-email value) email? (value email-value)) (define (parse-email s) (if (regex/match? #".+@.+\..+" s) (make-email s) (error "invalid email"))) (define e (parse-email "ada@example.com")) (email? e) ; => #t (email-value e) ; => "ada@example.com" ``` ## Multiple Record Types ```sema (define-record-type color (make-color r g b) color? (r color-r) (g color-g) (b color-b)) (define-record-type person (make-person name age) person? (name person-name) (age person-age)) (define red (make-color 255 0 0)) (define ada (make-person "Ada" 36)) (color? red) ; => #t (person? ada) ; => #t (color? ada) ; => #f (color-r red) ; => 255 (person-name ada) ; => "Ada" (type red) ; => :color (type ada) ; => :person ``` ## Serialization Records are **not JSON-encodable** directly. If you need to serialize a record, convert it to a map first: ```sema (define (point->map p) {:x (point-x p) :y (point-y p)}) (json/encode (point->map (make-point 1 2))) ; => "{\"x\":1,\"y\":2}" ``` Similarly, when loading data from JSON or the KV store, convert maps to records after parsing. ## Tips & Edge Cases * **Accessor type-checking:** calling `point-x` on a non-point value errors * **Type tag:** the tag returned by `type` is derived from the record type name — `point` → `:point` * **No generic field access:** you can't use `get` or keyword-as-function on records — use the generated accessors --- --- url: 'https://sema-lang.com/docs/stdlib/text-processing.md' --- # Text Processing Sema includes utilities for text chunking, cleaning, prompt templates, and structured documents — building blocks for LLM pipelines. ## Text Chunking ### `text/chunk` Recursively split text into chunks, trying natural boundaries (paragraphs, sentences, words) before hard-splitting. Takes text and an optional options map. ```sema (text/chunk "Long text here...") (text/chunk "Long text here..." {:size 500 :overlap 100}) ``` Options: `:size` (default 1000), `:overlap` (default 200). Returns a list of strings. ### `text/chunk-by-separator` Split text by a specific separator string. ```sema (text/chunk-by-separator "a\nb\nc" "\n") ; => ("a" "b" "c") ``` ### `text/split-sentences` Split text into sentences at `.`, `!`, `?` boundaries. ```sema (text/split-sentences "Hello world. How are you? Fine.") ; => ("Hello world." "How are you?" "Fine.") ``` ## Text Cleaning ### `text/clean-whitespace` Collapse multiple whitespace characters (spaces, newlines, tabs) into single spaces. ```sema (text/clean-whitespace " hello world \n\n foo ") ; => "hello world foo" ``` ### `text/strip-html` Remove HTML tags and decode common entities (`&`, `<`, `>`, `"`, `'`, `'`, ` `). ```sema (text/strip-html "

Hello world

") ; => "Hello world" (text/strip-html "a & b < c") ; => "a & b < c" ``` ### `text/truncate` Truncate text to a maximum length with a suffix. Takes text, max-length, and optional suffix (default `"..."`). ```sema (text/truncate "hello world" 5) ; => "he..." (text/truncate "hello world" 8 "…") ; => "hello w…" (text/truncate "hi" 10) ; => "hi" ``` ### `text/word-count` Count words in text (split by whitespace). ```sema (text/word-count "hello world foo bar") ; => 4 ``` ### `text/trim-indent` Remove common leading indentation from all lines. ```sema (text/trim-indent " hello\n world") ; => "hello\nworld" (text/trim-indent " hello\n world") ; => "hello\n world" ``` ### `text/excerpt` Extract a snippet around a search term with omission markers. Case-insensitive search. Returns `nil` if query not found. ```sema (text/excerpt "The quick brown fox jumps over the lazy dog" "fox" {:radius 10}) ; => "...ick brown fox jumps ove..." (text/excerpt "Hello world" "Hello") ; => "Hello world" ;; Custom omission marker (text/excerpt "Long text here..." "text" {:radius 5 :omission "[…]"}) ; => "Long text here[…]" ``` Options map (optional third argument): * `:radius` — number of characters to show on each side (default: 100) * `:omission` — marker string for truncated parts (default: `"..."`) ### `text/normalize-newlines` Convert `\r\n` (Windows) and `\r` (old Mac) line endings to `\n` (Unix). ```sema (text/normalize-newlines "line1\r\nline2\rline3") ; => "line1\nline2\nline3" ``` ## Prompt Templates ### `prompt/template` Create a template string for use with `prompt/render`. ```sema (define tmpl (prompt/template "Hello {{name}}, welcome to {{place}}.")) ``` ### `prompt/render` Render a template by substituting `{{key}}` placeholders with values from a map. Missing keys are left as-is. ```sema (prompt/render "Hello {{name}}, welcome to {{place}}." {:name "Alice" :place "Wonderland"}) ; => "Hello Alice, welcome to Wonderland." (prompt/render "Hello {{name}}, {{missing}}." {:name "Bob"}) ; => "Hello Bob, {{missing}}." ;; Non-string values are stringified (prompt/render "Count: {{n}}" {:n 42}) ; => "Count: 42" ``` ## Documents Structured documents with metadata, designed for use with chunking and vector stores. ### `document/create` Create a document map with `:text` and `:metadata`. ```sema (document/create "Hello world" {:source "test.txt" :page 1}) ; => {:metadata {:page 1 :source "test.txt"} :text "Hello world"} ``` ### `document/text` Extract the text from a document. ```sema (document/text doc) ; => "Hello world" ``` ### `document/metadata` Extract the metadata from a document. ```sema (document/metadata doc) ; => {:source "test.txt" :page 1} ``` ### `document/chunk` Chunk a document, preserving and extending metadata. Each chunk gets `:chunk-index` and `:total-chunks` added to its metadata. ```sema (document/chunk (document/create "long text..." {:source "paper.pdf"}) {:size 500}) ; => ({:text "chunk 1..." :metadata {:source "paper.pdf" :chunk-index 0 :total-chunks 3}} ; {:text "chunk 2..." :metadata {:source "paper.pdf" :chunk-index 1 :total-chunks 3}} ; ...) ``` --- --- url: 'https://sema-lang.com/docs/llm.md' --- # LLM Primitives Sema's differentiating feature: LLM operations are first-class language primitives with prompts, conversations, tools, and agents as native data types. ## Setup Set one or more API keys as environment variables: ```bash export ANTHROPIC_API_KEY=sk-ant-... export OPENAI_API_KEY=sk-... export DEEPSEEK_API_KEY=... export TOGETHER_API_KEY=... export FIREWORKS_API_KEY=... # or any other supported provider ``` Sema auto-detects and configures all available providers on startup. Use `--no-llm` to skip auto-configuration. See [Provider Management](./providers.md) for the full list of supported providers and configuration options. ## Features ### [Completion & Chat](./completion.md) Simple completions, multi-message chat, and streaming responses. ### [Prompts & Messages](./prompts.md) Prompts as composable s-expressions, message construction, and prompt inspection. ### [Conversations](./conversations.md) Persistent, immutable conversation state with automatic LLM round-trips. ### [Tools & Agents](./tools-agents.md) Define tools the LLM can invoke, and build agents with system prompts, tools, and multi-turn loops. ### [Embeddings & Similarity](./embeddings.md) Generate embeddings (as bytevectors), compute cosine similarity, and access embedding dimensions. ### [Structured Extraction](./extraction.md) Extract structured data from text and images, classify inputs, and work with multi-modal content. ### [Vector Store & Math](./vector-store.md) In-memory vector store for semantic search, plus vector math utilities (cosine similarity, dot product, normalize, distance). ### [Caching](./caching.md) In-memory LLM response caching for iterative development and deduplication. ### [Cassettes (Record & Replay)](./cassettes.md) Record real LLM/agent responses to a file once, then replay them deterministically — keyless, offline tests and reproducible demos. ### [Resilience & Retry](./resilience.md) Fallback provider chains, rate limiting, generic retry with exponential backoff, and convenience functions (`llm/summarize`, `llm/compare`). ### [Provider Management](./providers.md) Auto-configuration, runtime provider switching, custom providers, and OpenAI-compatible endpoints. ### [Cost Tracking & Budgets](./cost.md) Usage tracking, budget enforcement, and batch/parallel operations. ### [Workflows](./workflows.md) Define multi-phase agent pipelines as ordinary Sema code. Every step is journaled to a frozen JSONL run directory — resume, replay, or fork without losing state. Budget caps, parallel/pipeline fan-out, and a live web viewer. ### Observability (OpenTelemetry) Built-in, standards-compliant OpenTelemetry tracing + metrics for **every** LLM and agent run — no manual instrumentation. Each completion and tool call is auto-traced (`invoke_agent → chat → execute_tool`) with tokens, cost, and latency, exportable to any OTLP backend or a JSONL file — turned on with one environment variable or an `otel/configure` call. Off by default, zero-cost when off. * **[Tracing & Metrics](./observability.md)** — the GenAI spans and metrics, sessions, privacy controls, and embedding Sema in your own app. * **[Backend Compatibility](./otel-compat.md)** — label the data so tools that use their own attribute names (Arize Phoenix, Langfuse, Traceloop, LangSmith) read it too via `SEMA_OTEL_COMPAT`. Most other tools work with no extra setup. --- --- url: 'https://sema-lang.com/docs/llm/completion.md' --- # Completion & Chat ## Completion ### `llm/complete` Send a single prompt string and get a completion back. ```sema ;; Simple completion (llm/complete "Say hello in 5 words" {:max-tokens 50}) ``` With options: ```sema (llm/complete "Explain monads" {:model "claude-haiku-4-5-20251001" :max-tokens 200 :temperature 0.3 :system "You are a Haskell expert."}) ``` ### `llm/stream` Stream a completion, printing chunks as they arrive. ```sema (llm/stream "Tell me a story" {:max-tokens 200}) ``` With a callback function: ```sema (llm/stream "Tell me a story" (fn (chunk) (display chunk)) {:max-tokens 200}) ``` `llm/stream` **returns the full accumulated response string** once streaming finishes — so you can show the live stream *and* keep the final text: ```sema (define story (llm/stream "Tell me a story" (fn (c) (display c)) {:max-tokens 200})) ;; `story` is the complete text after the stream ends. ``` ## Chat ### `llm/chat` Send a list of messages and get a response. Supports system, user, and assistant messages. ```sema (llm/chat (list (message :system "You are a helpful assistant.") (message :user "What is Lisp? One sentence.")) {:max-tokens 100}) ``` When you pass `:tools`, `llm/chat` runs the tool-execution loop for you (see [Tools & Agents](./tools-agents)). Two options bound it: `:tool-mode :none` lets the model *see* the tools but never auto-executes them, and `:max-tool-rounds N` caps the loop (default 10). ### Multi-Modal Chat Send messages that include images alongside text using `message/with-image`. ```sema ;; Load an image and ask the LLM about it (define img (file/read-bytes "photo.jpg")) (define msg (message/with-image :user "Describe this image." img)) (llm/chat (list msg)) ``` Combine with regular messages: ```sema (llm/chat (list (message :system "You are an image analyst.") (message/with-image :user "What text is in this image?" (file/read-bytes "doc.png")))) ``` The image must be a bytevector. Media type (PNG, JPEG, GIF, WebP, PDF) is detected automatically from magic bytes. See [Vision Extraction](./extraction.md#vision-extraction) for structured data extraction from images. ### `llm/send` Send a prompt value (composed from `prompt` expressions) to the LLM. ```sema (define review-prompt (prompt (system "You are a code reviewer. Be concise.") (user "Review this function."))) (llm/send review-prompt {:max-tokens 200}) ``` ## Options All completion and chat functions accept an options map with these keys: | Key | Description | | -------------- | ------------------------------------------------------------- | | `:model` | Model name (e.g. `"claude-haiku-4-5-20251001"`) | | `:max-tokens` | Maximum tokens in response | | `:temperature` | Sampling temperature (0.0–1.0) | | `:system` | System prompt (for `llm/complete`) | | `:reasoning-effort` | Reasoning effort for thinking models — see below | | `:tools` | List of tool values (see [Tools & Agents](./tools-agents.md)) | | `:timeout` | Per-call HTTP timeout in **milliseconds** (network providers; non-streaming) | | `:tags` / `:metadata` | Observability tags/metadata — see [Backend Compatibility](./otel-compat.md) | ### Reasoning effort `:reasoning-effort` controls how much a reasoning/thinking model deliberates before answering. It takes a keyword or string: `:minimal`, `:low`, `:medium`, `:high`, `:none`, or `:xhigh`. It is a single **portable** option — Sema maps it to each provider's native control, so the same code works everywhere: ```sema (llm/complete "Prove that sqrt(2) is irrational." {:model "gpt-5.4-mini" :reasoning-effort :high :max-tokens 4000}) ``` | Provider | Mapped to | | --------- | ------------------------------------------------------------------------------------- | | OpenAI | native `reasoning_effort` (gpt-5 / o-series) | | Anthropic | extended **thinking** — effort sets the thinking `budget_tokens` (and raises `max_tokens` above it; `temperature` is forced to default while thinking) | | Gemini | `thinkingConfig.thinkingBudget` (`:none`/`:minimal` disable thinking) | Models and providers that don't support reasoning effort ignore the option (no-op). It is also accepted by `llm/chat` and per-run on `agent/run` (`{:reasoning-effort :high}`). --- --- url: 'https://sema-lang.com/docs/llm/tools-agents.md' --- # Tools & Agents ## Tools Tools let you define functions that the LLM can invoke during a conversation. The LLM sees the tool's name, description, and parameter schema, and can call it when appropriate. ### `deftool` Define a tool with a name, description, parameter schema, and handler function. ```sema (deftool lookup-capital "Look up the capital of a country" {:country {:type :string :description "Country name"}} (lambda (country) (cond ((= country "Norway") "Oslo") ((= country "France") "Paris") (else "Unknown")))) ``` An optional options map goes between the parameter schema and the handler. It accepts one key, `:policy-subjects`, which declares the file, network, command, or external action the tool performs. A workflow policy `:subjects` rule matches those declarations — see [Semantic subjects](/docs/llm/workflows#semantic-subjects). ```sema (deftool read-source "Read a source file." {:path {:type :string}} {:policy-subjects [{:kind :file-read :path-arg :path}]} (lambda (path) (file/read path))) ``` ### Using Tools with Chat Pass tools to `llm/chat` — the LLM will call them automatically when needed. ```sema (llm/chat (list (message :user "What is the capital of Norway?")) {:tools (list lookup-capital) :max-tokens 100}) ``` ### Inspecting Tools ### `tool/name` ```sema (tool/name lookup-capital) ; => "lookup-capital" ``` ### `tool/description` ```sema (tool/description lookup-capital) ; => "Look up the capital..." ``` ### `tool/parameters` ```sema (tool/parameters lookup-capital) ; => {:country {:type :string ...}} ``` ### `tool/policy-subjects` ```sema (tool/policy-subjects read-source) ; => [{:kind :file-read :path-arg :path}] (tool/policy-subjects lookup-capital) ; => [] ``` ### `tool?` ```sema (tool? lookup-capital) ; => #t ``` ## Agents Agents combine a system prompt, tools, and a multi-turn loop. They handle the back-and-forth of tool calls automatically. ### `defagent` Define an agent with a system prompt, tools, model, and turn limit. ```sema (deftool get-weather "Get weather for a city" {:city {:type :string}} (lambda (city) (format "~a: 22°C, sunny" city))) (defagent weather-bot {:system "You are a weather assistant. Use the get-weather tool." :tools [get-weather] :model "claude-haiku-4-5-20251001" :max-turns 3}) ``` ### `agent/run` Run an agent with a user message. The agent loops, calling tools as needed, until it has a final answer or hits the turn limit. The two-argument form returns the final answer as a **string**: ```sema (agent/run weather-bot "What's the weather in Tokyo?") ; => "It's sunny, 22°C." ``` An optional third argument takes per-run options. **Passing an options map changes the return value** to a map with the final reply *and* the full message history: ```sema (define result (agent/run weather-bot "What's the weather in Tokyo?" {:reasoning-effort :high ; reasoning effort for this run (see Completion) :messages prior-history ; seed the loop with prior conversation :memory mem ; persistent thread — see Agent Memory :on-tool-call observe-tool})) ; observe each tool call — see below (:response result) ; => the final answer string (:messages result) ; => the full conversation (to continue or inspect) ``` **Observing tool calls.** `:on-tool-call` fires once when each tool starts and once when it ends. The event is a map — branch on `(:event e)`, the string `"start"` or `"end"`: ```sema (define (observe-tool e) (when (= (:event e) "end") (println (:tool e) "→" (:result e) (format "(~ams)" (:duration-ms e))))) ``` The event map carries `:event` (`"start"` / `"end"`), `:tool` (the tool name), and `:args`; on `"end"` it adds `:result` (a preview of the return value), `:error` (a boolean), and `:duration-ms`. **Error recovery.** A tool that throws, isn't found, or is called with arguments that don't match its declared schema does **not** abort the run — the error is fed back to the model as the tool result so it can correct itself and continue. The loop is bounded by `:max-turns` and aborts after 5 consecutive tool errors. ### Inspecting Agents ### `agent/name` ```sema (agent/name weather-bot) ; => "weather-bot" ``` ### `agent/system` ```sema (agent/system weather-bot) ; => "You are a weather assistant..." ``` ### `agent/tools` ```sema (agent/tools weather-bot) ; => list of tool values ``` ### `agent/model` ```sema (agent/model weather-bot) ; => "claude-haiku-4-5-20251001" ``` ### `agent/max-turns` ```sema (agent/max-turns weather-bot) ; => 3 ``` ### `agent?` ```sema (agent? weather-bot) ; => #t ``` --- --- url: 'https://sema-lang.com/docs/llm/conversations.md' --- # Conversations Conversations are immutable data structures that maintain chat history. Each operation returns a new conversation value — the original is never modified. This means you always re-bind the result: ```sema (define conv (conversation/new {:model "claude-haiku-4-5-20251001"})) (define conv (conversation/set-system conv "You are a concise tutor.")) (define conv (conversation/say conv "Explain closures in 2 bullets.")) (println (conversation/last-reply conv)) ;; Branch to explore a different direction (define alt (conversation/fork conv)) (define alt (conversation/say alt "Now explain with JavaScript examples.")) ;; conv is unchanged — alt is an independent conversation ``` ## Creating Conversations ### `conversation/new` Create a new conversation, optionally with a model. `(conversation/new)` is equivalent to `(conversation/new {})`. ```sema (define conv (conversation/new {:model "claude-haiku-4-5-20251001"})) (define conv (conversation/new)) ``` ## Interacting ### `conversation/say` Send a user message to the LLM and get a response. Returns a new conversation with both the user message and the assistant's reply appended. ```sema (define conv (conversation/new {:model "claude-haiku-4-5-20251001"})) (define conv (conversation/say conv "Remember: the secret number is 7")) (define conv (conversation/say conv "What is the secret number?")) (conversation/last-reply conv) ; => "The secret number is 7." ``` With options: ```sema (define conv (conversation/say conv "Explain more" {:temperature 0.5 :max-tokens 500})) ``` ### `conversation/add-message` Manually add a message without making an LLM call. Useful for constructing conversation history programmatically. ```sema (define c (conversation/new)) (define c (conversation/add-message c :system "You are helpful.")) (define c (conversation/add-message c :user "hello")) (define c (conversation/add-message c :assistant "hi there")) ``` ### `conversation/say-as` Send a message with a different system prompt for one turn only. The override applies to the API call but doesn't change the conversation's stored system message. Accepts a system string or a prompt value. ```sema ;; With a prompt value — uses its system message for this turn (define argue-for (prompt (system "You argue IN FAVOR of Lisp."))) (define conv (conversation/new {:model "claude-sonnet-4-6"})) (define conv (conversation/say-as conv argue-for "Make your case.")) ;; With a plain string — treated as system content (define conv (conversation/say-as conv "You argue AGAINST Lisp." "Rebut the argument.")) ``` ## Inspecting ### `conversation/last-reply` Get the content of the last assistant message. ```sema (conversation/last-reply conv) ; => "The secret number is 7." ``` ### `conversation/messages` Get the full list of messages as message values. ```sema (conversation/messages conv) ; => list of message values (length (conversation/messages conv)) ; => 5 ``` ### `conversation/model` Get the model associated with the conversation. ```sema (conversation/model conv) ; => "claude-haiku-4-5-20251001" ``` ## System Message ### `conversation/system` Get the system message content, or `nil` if none is set. ```sema (define c (conversation/add-message (conversation/new) :system "Be helpful.")) (conversation/system c) ; => "Be helpful." (conversation/system (conversation/new)) ; => nil ``` ### `conversation/set-system` Set or replace the system message. All existing system messages are replaced with one new one; other messages are preserved. ```sema (define c (conversation/set-system (conversation/new) "You are a code reviewer.")) (conversation/system c) ; => "You are a code reviewer." ``` ## Filtering & Transforming ### `conversation/filter` Keep only messages matching a predicate. Returns a new conversation. ```sema ;; Keep only user messages (define user-only (conversation/filter conv (fn (m) (= (message/role m) :user)))) ;; Remove system messages (define no-system (conversation/filter conv (fn (m) (not (= (message/role m) :system))))) ``` ### `conversation/map` Apply a function to each message, returning a list of results (not a conversation). ```sema ;; Extract all message contents (conversation/map conv message/content) ;; Build a summary with role prefixes (conversation/map conv (fn (m) (string/append "[" (keyword/to-string (message/role m)) "] " (message/content m)))) ``` ## Usage & Cost ### `conversation/token-count` Estimated token count for the conversation (heuristic: ~4 characters per token). ```sema (conversation/token-count conv) ; => 342 ``` ### `conversation/cost` Estimated input cost in dollars based on the conversation's model pricing. Returns `nil` if pricing is unavailable for the model. ```sema (conversation/cost conv) ; => 0.00034 (or nil) ``` ## Branching ### `conversation/fork` Create an independent copy of a conversation. Since conversations are immutable, forking lets you explore different directions from the same point. ```sema (define conv (conversation/new {:model "claude-haiku-4-5-20251001"})) (define conv (conversation/say conv "Remember the number 7")) ;; Fork and take two different paths (define branch-a (conversation/say (conversation/fork conv) "What about Python?")) (define branch-b (conversation/say (conversation/fork conv) "What about Rust?")) ;; conv, branch-a, branch-b are all independent ``` ## Type Predicate ### `conversation?` Check if a value is a conversation. ```sema (conversation? conv) ; => #t (conversation? 42) ; => #f ``` --- --- url: 'https://sema-lang.com/docs/llm/memory.md' --- # Agent Memory A memory thread is a persistent, append-only conversation log with a name. Where a [conversation](/docs/llm/conversations) is an immutable in-memory value you pass around, a memory thread is a durable resource: it lives in a registry keyed on `(:namespace, :id)`, writes every turn to a JSONL sidecar under `./.sema/memory/`, and survives process restarts. Open the same thread tomorrow and the turns are still there. ```sema (define mem (memory/open {:id "user-42" :namespace "support"})) (memory/append mem {:role "user" :content "My invoice is wrong."}) (memory/append mem {:role "assistant" :content "Looking into it now."}) ;; …next session, next process: (define mem (memory/open {:id "user-42" :namespace "support"})) (conversation/messages (memory/messages mem)) ; both turns are back ``` ::: tip `memory/open` and `memory/append` require filesystem write capability (`FS_WRITE`) — a thread is a read-write resource. `memory/messages` is a pure in-memory read. ::: ## Functions ### `memory/open` Open (or return) the thread for `(:namespace, :id)`. Idempotent within a process: reopening an open thread returns the same working set. A fresh open loads any prior turns from disk. Returns a handle map that the other `memory/*` functions accept. ```sema (memory/open {:id "chat-1"}) ; namespace defaults to "default" (memory/open {:id "user-42" :namespace "support"}) ``` `:id` and `:namespace` must be plain names — they become file names under `./.sema/memory//.jsonl`, so path separators are rejected. ### `memory/append` Append one turn. The turn is immediately visible to `memory/messages` and durable on disk before the call resolves (inside async tasks the write happens off the scheduler, so sibling tasks keep running). Returns the handle, so calls chain. ```sema (memory/append mem {:role "user" :content "Hello"}) (memory/append mem {:content "role defaults to user"}) ``` ### `memory/messages` Snapshot the thread into a regular [conversation](/docs/llm/conversations) value — everything that works on conversations (filtering, `llm/chat`, forking) works on the result. ```sema (define conv (memory/messages mem)) (llm/chat conv {:model "claude-haiku-4-5-20251001"}) ``` ## Agents with memory Pass a handle as `:memory` in [`agent/run`](/docs/llm/tools-agents) opts and the loop uses the thread as its history: * **Seeding** — the thread's turns are sent ahead of the new user input. * **Writeback** — after the run, the user input and the assistant's text replies are appended to the thread. Tool-protocol traffic is not recorded: the thread stays a clean text transcript that is always valid to re-seed. * **Cancellation-safe** — if a run spawned with `async/spawn` is cancelled mid-conversation, the turns it had produced are still written back, so an interrupted session is not lost. ```sema (defagent support-bot {:system "You are a support agent." :model "claude-haiku-4-5-20251001"}) (define mem (memory/open {:id "ticket-981" :namespace "support"})) (agent/run support-bot "My invoice is wrong." {:memory mem}) ;; …later, even in a new process — the bot remembers the ticket: (agent/run support-bot "Any update?" {:memory mem}) ``` --- --- url: 'https://sema-lang.com/docs/llm/prompts.md' --- # Prompts & Messages Prompts in Sema are composable data structures — not string templates. They are built from message expressions, and can be inspected, transformed, and composed before being sent to an LLM. The core idea: build small prompt pieces, compose them together, fill in template slots, and send the result. Everything is a value you can pass around, store, and introspect. ```sema ;; Build reusable prompt pieces (define safety (prompt (system "Follow policy. Refuse unsafe requests."))) (define domain (prompt (system "You are a senior Lisp developer."))) (define task (prompt (user "Review this function:\n\n{{code}}"))) ;; Compose, fill, and send (define p (prompt/concat safety domain task)) (define ready (prompt/fill p {:code "(define (f x) (+ x 1))"})) (llm/send ready {:max-tokens 300}) ``` ## Messages A message is a role–content pair. The role is a keyword: `:system`, `:user`, or `:assistant`. ### `message` Create a message with a role and content. ```sema (message :system "You are a helpful assistant.") (message :user "What is Lisp?") (message :assistant "Lisp is a family of programming languages.") ``` ### `message/role` Get the role of a message as a keyword. ```sema (message/role (message :user "hi")) ; => :user ``` ### `message/content` Get the text content of a message. ```sema (message/content (message :user "hi")) ; => "hi" ``` ## Building Prompts ### `prompt` Build a prompt from message expressions. Inside `prompt`, use the shorthand constructors `(system ...)`, `(user ...)`, and `(assistant ...)` — these are equivalent to `(message :system ...)`, etc. ```sema (define review-prompt (prompt (system "You are a code reviewer. Be concise.") (user "Review this function."))) ``` ### `prompt/messages` Get the list of messages from a prompt. ```sema (prompt/messages my-prompt) ; => list of message values (length (prompt/messages my-prompt)) ; => 2 ``` ## Composing Prompts ### `prompt/append` Compose prompts by appending their messages together. Variadic — accepts 2 or more prompts. ```sema (define base (prompt (system "You are helpful."))) (define question (prompt (user "What is 2+2?"))) (define full (prompt/append base question)) ;; Three or more prompts (define safety (prompt (system "Be safe."))) (define full (prompt/append base safety question)) (llm/send full) ``` ### `prompt/concat` Alias for `prompt/append`. Use whichever name reads better in context. ```sema (define full (prompt/concat base-prompt safety-prompt domain-prompt)) ``` ## Templating ### `prompt/fill` Substitute `{{key}}` placeholders in all message contents using a map. Unfilled slots are left as-is, so you can partially fill a template and fill the rest later. ```sema (define template (prompt (system "You are a {{role}} reviewing {{language}} code.") (user "{{query}}"))) ;; Full fill (define filled (prompt/fill template {:role "expert" :language "Rust" :query "Explain this."})) ;; Partial fill — unfilled slots remain as {{...}} (define partial (prompt/fill template {:role "code reviewer"})) ;; partial still has {{language}} and {{query}} unfilled ``` ### `prompt/slots` Return a list of unfilled `{{slot}}` names as keywords. Duplicates are removed. ```sema (prompt/slots template) ; => (:role :language :query) ;; After partial fill, only unfilled slots remain (prompt/slots (prompt/fill template {:role "expert"})) ;; => (:language :query) ;; After full fill, no slots remain (prompt/slots filled) ; => () ``` Use `prompt/slots` to validate that all required slots are filled before sending: ```sema (when (not (null? (prompt/slots my-prompt))) (error "unfilled slots remain")) ``` ## Modifying Prompts ### `prompt/set-system` Replace all system messages with a single new one. Non-system messages are preserved. ```sema (define p (prompt (system "old system") (user "hello"))) (define p2 (prompt/set-system p "new system instructions")) ;; p2 has: [(system "new system instructions"), (user "hello")] ``` ## Type Predicates ### `prompt?` Check if a value is a prompt. ```sema (prompt? review-prompt) ; => #t (prompt? 42) ; => #f ``` ### `message?` Check if a value is a message. ```sema (message? (message :user "hi")) ; => #t (message? "not a message") ; => #f ``` --- --- url: 'https://sema-lang.com/docs/llm/extraction.md' --- # Structured Extraction Extract structured data from unstructured text using LLM-powered schema-based extraction and classification. ## Extraction ### `llm/extract` Extract structured data from text according to a schema. The schema defines the expected fields and their types. ```sema (llm/extract {:vendor {:type :string} :amount {:type :number} :date {:type :string}} "I bought coffee for $4.50 at Blue Bottle on Jan 15, 2025") ; => {:amount 4.5 :date "2025-01-15" :vendor "Blue Bottle"} ``` The schema map specifies field names as keys and type descriptors as values. Supported types include `:string`, `:number`, `:boolean`, and `:list`/`:array`. A field value can be written two ways, and they behave differently: * **Descriptor map** — `{:amount {:type :number}}`. This form is **type-checked**, and supports `:optional` and a custom `:validate` predicate (below). Use it for any field you want validated. * **Bare type keyword** — `{:amount :number}` is shorthand, but the type is sent to the model only as an untyped hint — it is **not** validated. Reach for the descriptor map when correctness matters. Only `:type`, `:optional`, and `:validate` on a field descriptor affect behavior; a `:description` on a field is currently ignored by extraction (it isn't sent to the model). ### Options `llm/extract` accepts an optional third argument — an options map: ```sema (llm/extract schema text {:model "claude-haiku-4-5-20251001"}) ``` | Option | Type | Default | Description | | ----------- | ------- | ------- | -------------------------------------------------- | | `:model` | string | — | Override the default model | | `:validate` | boolean | `#t` | Validate response against the schema | | `:retries` | integer | `2` | Max retry attempts on validation failure | | `:reask?` | boolean | `#t` | Feed validation errors back to the LLM on retry | ### Schema Validation By default, the extracted result is validated against the schema: * All required schema keys must be present in the result * Types must match: `:string` → string, `:number` → integer or float, `:boolean` → boolean, `:list`/`:array` → list or vector ```sema (llm/extract {:name {:type :string} :age {:type :number}} "Alice is 30 years old") ; => {:age 30 :name "Alice"} ``` If validation fails, an error is raised with details about which fields didn't match. ### Optional Fields Mark fields as optional with `:optional #t`. Missing optional fields won't trigger validation errors: ```sema (llm/extract {:name {:type :string} :nickname {:type :string :optional #t}} "Her name is Ada Lovelace.") ; => {:name "Ada Lovelace"} ;; No error even though :nickname is missing ``` ### Custom Validation Predicates Use `:validate` on individual field specs to run a custom predicate after type checking. If the predicate returns falsy, the field fails validation and triggers a retry: ```sema (llm/extract {:amount {:type :number :validate #(> % 0)} :vendor {:type :string :validate #(> (string/length %) 0)}} "Invoice from Acme Corp for $42.50") ; => {:amount 42.5 :vendor "Acme Corp"} ``` Add `:message` to provide a human-readable error description. This message is fed back to the LLM in re-ask prompts, helping it correct its response: ```sema (llm/extract {:age {:type :number :validate #(and (>= % 0) (<= % 150)) :message "age must be between 0 and 150"}} "She is 30 years old.") ; => {:age 30} ``` Without `:message`, the default error text includes the field value: `"custom validation failed for value -5"`. ### Retry on Mismatch Validation failures automatically trigger retries (up to `:retries`, default 2). On each retry, the validation errors are fed back to the LLM to improve the next attempt. After exhausting retries, the final validation error is raised. ```sema (llm/extract {:items {:type :list} :total {:type :number :validate pos?}} "3 apples, 2 oranges, total 5 items") ``` Disable automatic retries with `{:retries 0}` or disable validation entirely with `{:validate #f}`. ## Classification ### `llm/classify` Classify text into one of a set of categories. Returns the matching keyword. ```sema (llm/classify (list :positive :negative :neutral) "This product is amazing!") ; => :positive ``` Pass a list of keyword labels and the text to classify. The LLM picks the best-matching label. An optional third options map takes `:model` — handy for using a cheap, fast model for classification: ```sema (llm/classify (list :spam :ham) text {:model "claude-haiku-4-5-20251001"}) ``` The return type follows the labels: a list of **keywords** classifies to a keyword, a list of **strings** to a string. ## Vision Extraction ### `llm/extract-from-image` Extract structured data from images using vision-capable LLMs. Accepts a schema, an image source (file path or bytevector), and optional options. ```sema ;; Extract from a file path (llm/extract-from-image {:text :string :background_color :string} "assets/logo.png") ; => {:background_color "white" :text "Sema"} ;; Extract from a bytevector (define img (file/read-bytes "invoice.jpg")) (llm/extract-from-image {:invoice_number :string :date :string :total :string} img) ; => {:date "2025-03-15" :invoice_number "12345" :total "$139.96"} ``` Supported image formats (detected automatically via magic bytes): PNG, JPEG, GIF, WebP, PDF. ### Options `llm/extract-from-image` accepts an optional third argument — an options map: ```sema (llm/extract-from-image schema source {:model "gpt-5.5"}) ``` | Option | Type | Default | Description | | -------- | ------ | ------- | -------------------------- | | `:model` | string | — | Override the default model | ## Multi-Modal Messages ### `message/with-image` Create a message that includes both text and an image, for use with `llm/chat`. ```sema (define img (file/read-bytes "photo.jpg")) (define msg (message/with-image :user "What do you see?" img)) (llm/chat (list msg)) ``` The image must be a bytevector (use `file/read-bytes` to load from disk). The media type is detected automatically. You can combine image messages with regular messages: ```sema (llm/chat (list (message :system "You are a helpful image analyst.") (message/with-image :user "Describe this chart." (file/read-bytes "chart.png")))) ``` ### Provider Support Vision features work with providers that support multi-modal input: | Provider | `llm/extract-from-image` | `message/with-image` | | ------------- | ------------------------ | -------------------- | | **Anthropic** | ✅ | ✅ | | **OpenAI** | ✅ | ✅ | | **Gemini** | ✅ | ✅ | | **Ollama** | ✅ (model-dependent) | ✅ (model-dependent) | For Ollama, use a vision-capable model like `gemma3:4b` or `llava`. --- --- url: 'https://sema-lang.com/docs/llm/providers.md' --- # Provider Management ## Auto-Configuration Sema auto-detects and configures all available providers from environment variables on startup. No manual setup is required — just set the API key for your provider. ### `llm/auto-configure` Manually trigger auto-configuration (runs automatically on startup unless `--no-llm` is used). ```sema (llm/auto-configure) ``` ## Manual Configuration ### `llm/configure` Manually configure a known provider with specific options. ```sema (llm/configure :anthropic {:api-key "sk-..."}) ;; Ollama with custom host (llm/configure :ollama {:host "http://localhost:11434" :default-model "llama3"}) ``` ### OpenAI-Compatible Providers Any provider with an OpenAI-compatible API can be registered by passing `:api-key` and `:base-url` with any provider name — no custom code needed, just configuration. ```sema ;; Together AI (llm/configure :together {:api-key (env "TOGETHER_API_KEY") :base-url "https://api.together.xyz/v1" :default-model "meta-llama/Llama-3-70b-chat-hf"}) ;; Azure OpenAI (llm/configure :azure {:api-key (env "AZURE_OPENAI_KEY") :base-url "https://my-resource.openai.azure.com/openai/deployments/gpt-4/v1" :default-model "gpt-4"}) ;; Local vLLM / LiteLLM / text-generation-inference (llm/configure :local {:api-key "not-needed" :base-url "http://localhost:8000/v1" :default-model "my-model"}) ;; Once configured, use like any other provider (llm/complete "Hello from Together!" {:model "meta-llama/Llama-3-70b-chat-hf"}) ``` This works for any service that implements the OpenAI chat completions API: Together, Fireworks, Perplexity, Azure OpenAI, Anyscale, vLLM, LiteLLM, text-generation-inference, and others. > **Sandbox note.** Local endpoints like `http://localhost:8000/v1` and Ollama on `localhost:11434` work normally in the REPL, CLI, and notebook. When running **untrusted code under `--sandbox`**, a `:base-url`/`:host` pointing at a loopback or private address (`localhost`, `127.0.0.1`, `10.x`, `169.254.169.254`, …) is rejected to prevent SSRF. Run unsandboxed to use a local endpoint. ## Lisp-Defined Providers For full control over request/response handling, you can define providers entirely in Sema using `llm/define-provider`. The provider's `:complete` function receives the request as a map and returns either a string or a response map. ### `llm/define-provider` ```sema (llm/define-provider :name {:complete fn :default-model "..."}) ``` **Parameters:** * `:complete` — **(required)** A function that takes a request map and returns a response * `:default-model` — Model name used when none is specified (default: `"default"`) ### Request Map The `:complete` function receives a map with these keys: | Key | Type | Description | | ----------------- | -------------- | ---------------------------------- | | `:model` | string | Model name | | `:messages` | list of maps | Each has `:role` and `:content` | | `:max-tokens` | integer or nil | Token limit | | `:temperature` | float or nil | Sampling temperature | | `:system` | string or nil | System prompt | | `:tools` | list or nil | Tool schemas (if tools are in use) | | `:stop-sequences` | list or nil | Stop sequences for generation | ### Response Format The function can return either: * **A string** — used as the assistant's response content * **A map** with optional keys: | Key | Type | Default | | -------------- | ------ | ------------- | | `:content` | string | `""` | | `:role` | string | `"assistant"` | | `:model` | string | request model | | `:stop-reason` | string | `"end_turn"` | | `:usage` | map | zero tokens | | `:tool-calls` | list | empty list | The `:usage` map can contain `:prompt-tokens` and `:completion-tokens` (both integers). The `:tool-calls` list contains maps with `:id` (string), `:name` (string), and `:arguments` (map). This enables Lisp-defined providers to work with tool-calling agents. ### Examples **Echo provider** — returns the user's message back: ```sema (llm/define-provider :echo {:complete (fn (req) (string/append "Echo: " (:content (last (:messages req))))) :default-model "echo-v1"}) (llm/complete "hello") ;; => "Echo: hello" ``` **HTTP proxy** — forward to a custom API: ```sema (llm/define-provider :my-api {:complete (fn (req) (define resp (json/decode (http/post "https://my-api.example.com/chat" {:headers {"Authorization" (string/append "Bearer " (env "MY_API_KEY")) "Content-Type" "application/json"} :body (json/encode {:model (:model req) :prompt (:content (last (:messages req)))})}))) {:content (:text resp) :usage {:prompt-tokens (:input-tokens resp) :completion-tokens (:output-tokens resp)}}) :default-model "my-model-v2"}) ``` **Mock provider for testing** — deterministic responses without API calls: ```sema (define responses (list "First response" "Second response" "Third response")) (define call-count (atom 0)) (llm/define-provider :mock {:complete (fn (req) (let ((i (deref call-count))) (swap! call-count (fn (n) (+ n 1))) (nth responses (mod i (length responses))))) :default-model "mock-v1"}) ;; Now all llm/complete calls return deterministic values (llm/complete "anything") ;; => "First response" (llm/complete "anything") ;; => "Second response" ``` **Routing provider** — dispatch to different backends by model name: ```sema (llm/configure :anthropic {:api-key (env "ANTHROPIC_API_KEY")}) (llm/configure :openai {:api-key (env "OPENAI_API_KEY")}) (llm/define-provider :router {:complete (fn (req) (let ((model (:model req))) (cond ((string/starts-with? model "claude") (begin (llm/set-default :anthropic) (llm/complete (:content (last (:messages req))) {:model model}))) ((string/starts-with? model "gpt") (begin (llm/set-default :openai) (llm/complete (:content (last (:messages req))) {:model model}))) (else (error (string/append "Unknown model: " model)))))) :default-model "claude-sonnet-4-6"}) ``` ### Switching Between Providers Lisp-defined providers integrate with the standard provider management functions: ```sema (llm/define-provider :mock {:complete (fn (req) "mock response") :default-model "m1"}) (llm/configure :anthropic {:api-key (env "ANTHROPIC_API_KEY")}) (llm/set-default :mock) ;; use mock (llm/complete "test") ;; => "mock response" (llm/set-default :anthropic) ;; switch to real API (llm/complete "test") ;; => real API response ``` ## Runtime Provider Switching ### `llm/list-providers` List all configured providers. ```sema (llm/list-providers) ; => (:anthropic :gemini :openai ...) (llm/providers) ; => same (alias) ``` ### `llm/current-provider` Get the currently active provider and model. ```sema (llm/current-provider) ; => {:name :anthropic :model "claude-sonnet-4-6"} (llm/default-provider) ; => same (alias) ``` ### `llm/set-default` Switch the active provider at runtime. ```sema (llm/set-default :openai) ``` ## Supported Providers All providers are auto-configured from environment variables. Use `(llm/configure :provider {...})` for manual setup. ### Chat / Inference Providers | Provider | Type | Chat | Stream | Tools | Vision | Env Var | |----------|------|:----:|:------:|:-----:|:------:|---------| | **Anthropic** | Native | ✅ | ✅ | ✅ | ✅ | `ANTHROPIC_API_KEY` | | **OpenAI** | Native | ✅ | ✅ | ✅ | ✅ | `OPENAI_API_KEY` | | **Google Gemini** | Native | ✅ | ✅ | ✅ | ✅ | `GOOGLE_API_KEY` | | **Ollama** | Native (local) | ✅ | ✅ | ✅ | ✅ ² | `OLLAMA_HOST` | | **Groq** | OpenAI-compat | ✅ | ✅ | ✅ | — | `GROQ_API_KEY` | | **xAI** | OpenAI-compat | ✅ | ✅ | ✅ | — | `XAI_API_KEY` | | **Mistral** | OpenAI-compat | ✅ | ✅ | ✅ | — | `MISTRAL_API_KEY` | | **Moonshot** | OpenAI-compat | ✅ | ✅ | ✅ | — | `MOONSHOT_API_KEY` | | **DeepSeek** | OpenAI-compat | ✅ | ✅ | ✅ | — | `DEEPSEEK_API_KEY` | | **OpenRouter** | OpenAI-compat (meta) | ✅ | ✅ | ✅ | — | `OPENROUTER_API_KEY` | | **Together AI** | OpenAI-compat | ✅ | ✅ | ✅ | — | `TOGETHER_API_KEY` | | **Fireworks AI** | OpenAI-compat | ✅ | ✅ | ✅ | — | `FIREWORKS_API_KEY` | | **Cerebras** | OpenAI-compat | ✅ | ✅ | ✅ | — | `CEREBRAS_API_KEY` | | **SambaNova** | OpenAI-compat | ✅ | ✅ | ✅ | — | `SAMBANOVA_API_KEY` | | **Perplexity** | OpenAI-compat | ✅ | ✅ | ✅ | — | `PERPLEXITY_API_KEY` | | *Any OpenAI-compat* | `llm/configure` | ✅ | ✅ | ✅ | ✅ | — | | *Custom Lisp* | `llm/define-provider` | ✅ | ¹ | ✅ | — | — | ### Embedding / Reranking Providers | Provider | Embeddings | Reranking | Env Var | |----------|:----------:|:---------:|---------| | **OpenAI** | ✅ | — | `OPENAI_API_KEY` | | **Jina** | ✅ | ✅ | `JINA_API_KEY` | | **Voyage** | ✅ | ✅ | `VOYAGE_API_KEY` | | **Cohere** | ✅ | ✅ | `COHERE_API_KEY` | | **Nomic** | ✅ | ✅ | `NOMIC_API_KEY` | | **Together AI** | ✅ | ✅ | `TOGETHER_API_KEY` | | **Fireworks AI** | ✅ | ✅ | `FIREWORKS_API_KEY` | ¹ Streaming falls back to non-streaming (sends complete response as a single chunk). ² Vision requires a vision-capable model (e.g., `gemma3:4b`, `llava`). ### Default Models When you don't pass `:default-model` to `llm/configure` (or pin `:model` on a call), each provider uses the following default. #### Chat providers | Provider | Default model | |----------|---------------| | `:anthropic` | `claude-sonnet-4-6` | | `:openai` | `gpt-5.5` | | `:gemini` | `gemini-3.5-flash` | | `:ollama` | `gemma4` | | `:groq` | `llama-3.3-70b-versatile` | | `:xai` | `grok-4.3` | | `:mistral` | `mistral-large-latest` | | `:moonshot` | `kimi-k2.6` | | `:deepseek` | `deepseek-v4-flash` | | `:openrouter` | `openai/gpt-5.2` | | `:together` | `meta-llama/Llama-4-Scout-17B-16E-Instruct` | | `:fireworks` | `accounts/fireworks/models/gpt-oss-120b` | | `:cerebras` | `gpt-oss-120b` | | `:sambanova` | `Meta-Llama-3.3-70B-Instruct` | | `:perplexity` | `sonar-pro` | #### Embedding providers | Provider | Default model | |----------|---------------| | `:openai` | `text-embedding-3-small` | | `:jina` | `jina-embeddings-v3` | | `:voyage` | `voyage-3` | | `:cohere` | `embed-english-v3.0` | | `:nomic` | `nomic-embed-text-v1.5` | | `:together` | `BAAI/bge-base-en-v1.5` | | `:fireworks` | `fireworks/qwen3-embedding-8b` | #### Reranking providers | Provider | Default model | |----------|---------------| | `:jina` | `jina-reranker-v2-base-multilingual` | | `:voyage` | `rerank-2.5` | | `:cohere` | `rerank-v3.5` | | `:nomic` | `nomic-rerank-v1.5` | | `:together` | `BAAI/bge-reranker-v2-m3` | | `:fireworks` | `fireworks/qwen3-reranker-8b` | Override any of these per provider with `:default-model`, globally via `SEMA_CHAT_MODEL`, or per call with `:model`. ## Environment Variables | Variable | Description | |----------|-------------| | `ANTHROPIC_API_KEY` | Anthropic API key | | `OPENAI_API_KEY` | OpenAI API key (also used for embeddings fallback) | | `GOOGLE_API_KEY` | Google Gemini API key | | `GROQ_API_KEY` | Groq API key | | `XAI_API_KEY` | xAI/Grok API key | | `MISTRAL_API_KEY` | Mistral API key | | `MOONSHOT_API_KEY` | Moonshot/Kimi API key | | `DEEPSEEK_API_KEY` | DeepSeek API key | | `OPENROUTER_API_KEY` | OpenRouter API key | | `TOGETHER_API_KEY` | Together AI API key (chat + embeddings + reranking) | | `FIREWORKS_API_KEY` | Fireworks AI API key (chat + embeddings + reranking) | | `CEREBRAS_API_KEY` | Cerebras API key | | `SAMBANOVA_API_KEY` | SambaNova API key | | `PERPLEXITY_API_KEY` | Perplexity API key | | `OLLAMA_HOST` | Ollama server URL (default: `http://localhost:11434`) | | `JINA_API_KEY` | Jina embeddings + reranking API key | | `VOYAGE_API_KEY` | Voyage embeddings + reranking API key | | `COHERE_API_KEY` | Cohere embeddings + reranking API key | | `NOMIC_API_KEY` | Nomic embeddings + reranking API key | | `SEMA_CHAT_MODEL` | Default chat model name | | `SEMA_CHAT_PROVIDER` | Preferred chat provider | | `SEMA_EMBEDDING_MODEL` | Default embedding model name | | `SEMA_EMBEDDING_PROVIDER` | Preferred embedding provider | --- --- url: 'https://sema-lang.com/docs/llm/cost.md' --- # Cost Tracking & Budgets ## Usage Tracking ### `llm/last-usage` Get token usage from the most recent LLM call. ```sema (llm/last-usage) ; => {:prompt-tokens 42 :completion-tokens 15 :total-tokens 57 ; :cache-read-tokens 0 :cache-creation-tokens 0 ; :model "..." :cost-usd 0.0003} ``` ### `llm/session-usage` Get cumulative usage across all LLM calls in the current session. ```sema (llm/session-usage) ; => {:prompt-tokens 1280 :completion-tokens 410 :total-tokens 1690 ; :cache-read-tokens 1024 :cache-creation-tokens 0 :cost-usd 0.012} ``` #### Prompt-cache tokens `:cache-read-tokens` and `:cache-creation-tokens` report how many input tokens were served from (or written to) the provider's **prompt cache** — large savings when you repeat a stable prefix across calls. * **OpenAI** and **Gemini** (2.5+) cache *implicitly*: send the same long prefix twice and the second call reports `:cache-read-tokens` automatically. Reads are a subset of `:prompt-tokens`. * **Anthropic** reports `:cache-read-tokens` and `:cache-creation-tokens` *separately* from `:prompt-tokens` (caching there is opt-in via `cache_control`). * Providers that don't report cache counts leave these at `0`. > Cost is currently priced at the standard input rate; cached reads are reported > for visibility but not yet discounted in `:cost-usd`. ### `llm/reset-usage` Reset session usage counters. ```sema (llm/reset-usage) ``` ## Pricing Sources Sema tracks LLM costs using pricing data from these sources, checked in this order: 1. **Custom pricing** — set via `(llm/set-pricing "model" input output)`, always wins 2. **Bundled price list** — a [models.dev](https://models.dev) snapshot (2,400+ models) that ships with Sema, so cost tracking works fully offline with no network calls 3. **Unknown** — if no source matches, cost tracking returns `nil` and budget enforcement is best-effort The embedded snapshot is refreshed by maintainers with `jake update-pricing` and shipped in patch releases. Prices are matched by model id, preferring the canonical first-party listing; when the serving provider is known (e.g. inside an `llm/with-fallback` chain), a reseller/gateway that lists the same model at a different rate is priced correctly. ### `llm/pricing-status` Check the pricing source and the snapshot date. ```sema (llm/pricing-status) ; => {:source "embedded" :updated-at "2026-06-18"} ``` ## Budget Enforcement > **Note:** If pricing is unknown for a model (not in any source), budget enforcement operates in best-effort mode — the call proceeds with a one-time warning. Use `(llm/set-pricing)` to set pricing for unlisted models. ### `llm/set-budget` Set a spending limit (in dollars) for the session. LLM calls that would exceed the budget will fail. ```sema (llm/set-budget 1.00) ; set $1.00 spending limit ``` ### `llm/budget-remaining` Check current budget status. ```sema (llm/budget-remaining) ; => {:limit 1.0 :spent 0.05 :remaining 0.95} ``` ### `llm/with-budget` Scoped budget — sets spending limits for the duration of a thunk, then restores the previous budget when done. At least one of `:max-cost-usd` or `:max-tokens` is required. When both are provided, **whichever limit is hit first** triggers the error. ```sema ;; Cost-based budget (llm/with-budget {:max-cost-usd 0.50} (lambda () (llm/complete "Expensive operation"))) ;; Token-based budget (useful when pricing is unknown or stale) (llm/with-budget {:max-tokens 10000} (lambda () (llm/complete "Limited tokens"))) ;; Both limits — whichever is reached first stops execution (llm/with-budget {:max-cost-usd 1.00 :max-tokens 50000} (lambda () (llm/complete "Double-capped") (println (format "Budget: ~a" (llm/budget-remaining))))) ``` When a token budget is active, `llm/budget-remaining` includes `:token-limit`, `:tokens-spent`, and `:tokens-remaining` in addition to the cost fields. #### Streaming and the budget By default, budgets enforce on **non-streaming** calls (the spend is known after each call completes). A stream's cost isn't known until it ends, so streams aren't budget-gated unless you opt in with `:on-stream :pre-gate` — which refuses to **open** a stream once the scope's spend is already at the cap: ```sema (llm/with-budget {:max-cost-usd 0.50 :on-stream :pre-gate} (lambda () (llm/stream "..." on-token))) ; blocked at open once $0.50 is spent ``` A single in-flight stream can still push *past* the cap (you only learn its cost when it finishes), but the next call is blocked. Usage is tracked either way. ### `llm/clear-budget` Remove the spending limit. ```sema (llm/clear-budget) ``` ### `llm/set-pricing` Set custom pricing for a model (overrides both dynamic and built-in pricing). Costs are per million tokens. ```sema (llm/set-pricing "my-model" 1.0 3.0) ; $1.00/M input, $3.00/M output ``` ## Batch & Parallel ### `llm/batch` Send multiple prompts concurrently and collect all results. ```sema (llm/batch (list "Translate 'hello' to French" "Translate 'hello' to Spanish" "Translate 'hello' to German")) ``` ### `llm/pmap` Map a function over items, sending all resulting prompts in parallel. ```sema (llm/pmap (fn (word) (format "Define: ~a" word)) '("serendipity" "ephemeral" "ubiquitous") {:max-tokens 50}) ``` --- --- url: 'https://sema-lang.com/docs/llm/caching.md' --- # Response Caching Sema caches LLM responses so identical calls don't hit the API twice. The cache is **persistent**: responses are written to `~/.sema/cache/llm/` (one JSON file per entry, named by a SHA-256 key), so a re-run of a script — even in a new process — serves the answer recorded by an earlier run. An in-memory layer sits on top for the current session. A call is a cache hit when its **model, temperature, system prompt, and full message list** all match a stored entry. `:max-tokens` and `:tools` are *not* part of the key. Caching is **off by default** — turn it on for a block with `llm/with-cache`. > For replay that you **commit and share** (deterministic tests, offline demos), see > [Cassettes](./cassettes) instead. They're a different tool: a cassette stores a tape > next to your code rather than in your personal cache dir, and the response cache is > turned off inside `llm/with-cassette`. ## Cache scope ### `llm/with-cache` Run a thunk with caching enabled for every LLM call inside it. The **options map comes first** when you pass one; with a single argument it's just the thunk. `:ttl` sets the time-to-live in seconds (default 3600). Previous cache settings are restored on exit. ```sema ;; thunk only (llm/with-cache (lambda () (llm/complete "hello"))) ;; with options — opts FIRST, then the thunk (llm/with-cache {:ttl 7200} (lambda () (llm/complete "hello"))) ``` A cache hit costs nothing: it makes no provider call, so it reports **zero** token usage and spends nothing against a [budget](./cost). The two calls below show a miss then a hit: ```sema (llm/with-cache (lambda () (llm/complete "what is 2+2?") ; miss — calls the model, stores the answer (llm/complete "what is 2+2?") ; hit — served from the cache, no API call (llm/cache-stats))) ; => {:hits 1 :misses 1 :size 1} ``` ## Inspection & debugging ### `llm/cache-key` Generate the SHA-256 cache key for a prompt and options — handy for debugging why two calls do or don't share a cache entry. Takes a prompt string and an optional options map. ```sema (llm/cache-key "hello" {:model "gpt-4" :temperature 0.5}) ``` ### `llm/cache-stats` Returns `{:hits :misses :size}`. Note that `:size` counts only the entries loaded into memory **this session** — a cold start can serve hits from disk before `:size` reflects them. ```sema (llm/cache-stats) ; => {:hits 0 :misses 0 :size 0} ``` ## Cache management ### `llm/cache-clear` Clear cached responses — both the in-memory entries and the files in `~/.sema/cache/llm/`. Returns the number of entries cleared. ```sema (llm/cache-clear) ; => 0 ``` --- --- url: 'https://sema-lang.com/docs/llm/resilience.md' --- # Resilience & Retry ## Fallback Provider Chains ### `llm/with-fallback` Wraps a thunk with a fallback chain of providers. If the LLM call fails with one provider, automatically tries the next provider in the list. ```sema (llm/with-fallback [:anthropic :openai :deepseek] (lambda () (llm/complete "Hello"))) ``` #### Model selection across the chain Model ids are provider-specific (a Claude id is meaningless to OpenAI), so each chain entry resolves its own model: * A **bare provider keyword** (e.g. `:anthropic`) uses that provider's [default model](./providers#default-models), or whatever you set via `(llm/configure :anthropic {:default-model "..."})`. This is the recommended form — leave the body's `(llm/complete ...)` **unpinned** so every provider gets a model id valid for itself. * If the body pins a `:model`, that exact string is sent to **every** provider in the chain. That's fine for a homogeneous chain, but pinning a provider-specific id (e.g. a Claude model) will fail on any other provider it falls back to. #### Per-provider model overrides To target a different model per provider within a single chain, give chain entries as `[provider model]` pairs or `{:provider :model}` maps. A per-provider override **wins over any `:model` pinned in the body**: ```sema ;; Anthropic uses Opus, OpenAI uses GPT-5.5, DeepSeek uses its default (llm/with-fallback [[:anthropic "claude-opus-4-8"] [:openai "gpt-5.5"] :deepseek] (lambda () (llm/complete "Hello"))) ;; Map form is equivalent and lets you omit :model to use the provider default (llm/with-fallback [{:provider :anthropic :model "claude-opus-4-8"} {:provider :openai}] (lambda () (llm/complete "Hello"))) ``` ## Automatic Retry on Transient Errors LLM calls (`llm/complete`, `llm/chat`, `agent/run`, and the fallback-chain path) **automatically retry transient failures** — no configuration needed: * Retried: HTTP 429 (rate limited), 5xx server errors, and network/timeout errors. * Not retried: 4xx client errors other than 429 (e.g. 400 bad request), and parse errors — these won't succeed on a retry, so they fail fast. * Backoff: capped **exponential backoff with full jitter** (base 500ms, doubling per attempt, capped at 30s), up to 3 retries. A 429 honors the provider's `retry-after` hint when present. This is distinct from [`llm/with-fallback`](#fallback-provider-chains) (which switches *providers* on failure) and the generic [`retry`](#generic-retry) (which wraps *any* thunk). They compose: each provider in a fallback chain does its own transient-error retry before the chain moves on. ### Streaming and resilience `llm/stream` applies these guarantees **at stream-open** — before the first token: * **Fallback** — if a provider fails to *open* the stream, the chain fails over to the next, just like non-streaming. Once the first token has been delivered, a **mid-stream** failure is **not** failed over (switching providers mid-answer would re-emit the partial you already received); the error surfaces and the partial text is kept. * **Rate-limiting** — `llm/with-rate-limit` gates the stream-open call the same as a non-streaming one. * **Budget** — opt in with `llm/with-budget {... :on-stream :pre-gate}`: the stream is refused at open if the scope's spend is already at the cap. By default streams are **not** budget-gated (a stream's cost is unknown until it ends), though usage is still tracked afterward. Two things still **don't** apply to streams: the **response cache** (a live stream isn't cached — for deterministic replay use [cassettes](/docs/llm/cassettes)) and **mid-stream retry** (a retry would duplicate already-emitted output — see above). ## Rate Limiting ### `llm/with-rate-limit` Wraps a thunk with token-bucket rate limiting. Takes a rate (requests per second) and a thunk. Useful to avoid hitting API rate limits. ```sema (llm/with-rate-limit 5 (lambda () (llm/complete "Hello"))) ``` ## Generic Retry ### `retry` Retries a thunk on failure with exponential backoff. Takes a thunk and an optional options map. ```sema ;; Default: 3 retries, 100ms base delay, 2.0 backoff (retry (lambda () (http/get "https://example.com"))) ;; Custom options (retry (lambda () (http/get "https://example.com")) {:max-attempts 5 :base-delay-ms 200 :backoff 1.5}) ``` Options: | Key | Type | Default | Description | | ---------------- | ------- | ------- | ---------------------------------- | | `:max-attempts` | integer | 3 | Maximum number of attempts | | `:base-delay-ms` | integer | 100 | Initial delay between retries (ms) | | `:backoff` | float | 2.0 | Backoff multiplier | > **Note:** `retry` is in the stdlib (not LLM-specific) — it works with any function. ## LLM Convenience Functions ### `llm/summarize` Summarize text using an LLM. Takes text and an optional options map. ```sema (llm/summarize "Long article text here...") (llm/summarize "Long text" {:model "claude-haiku-4-5-20251001" :max-tokens 200}) ``` ### `llm/compare` Compare two texts using an LLM. Takes two strings and an optional options map. ```sema (llm/compare "Text A" "Text B") (llm/compare "Text A" "Text B" {:model "claude-haiku-4-5-20251001"}) ``` --- --- url: 'https://sema-lang.com/docs/llm/embeddings.md' --- # Embeddings & Similarity Generate vector embeddings from text and compute similarity between them. On startup `(llm/auto-configure)` picks an embedding provider by **precedence** — `JINA_API_KEY`, then `VOYAGE_API_KEY`, then `COHERE_API_KEY`, then `NOMIC_API_KEY`; if none is set it falls back to `OPENAI_API_KEY` (`text-embedding-3-small`). If `TOGETHER_API_KEY` or `FIREWORKS_API_KEY` is set, those are also detected. The first key present wins. ## Configuration ### `llm/configure-embeddings` Configure a dedicated embedding provider separately from the chat provider — so you can use one provider for chat and another for embeddings. Pass `:default-model` to pick the model (otherwise each provider uses its default: `jina-embeddings-v3`, `voyage-3`, or `text-embedding-3-small`): ```sema (llm/configure-embeddings :voyage {:api-key (env "VOYAGE_API_KEY") :default-model "voyage-3-large"}) (llm/configure-embeddings :nomic {:api-key (env "NOMIC_API_KEY")}) (llm/configure-embeddings :together {:api-key (env "TOGETHER_API_KEY")}) (llm/configure-embeddings :fireworks {:api-key (env "FIREWORKS_API_KEY")}) ;; OpenAI-compatible embedding provider, with a model and optional base URL (llm/configure-embeddings :openai {:api-key (env "OPENAI_API_KEY") :default-model "text-embedding-3-large"}) ``` ## Generating Embeddings ### `llm/embed` Generate an embedding for a string or a list of strings. Returns a **bytevector** containing densely-packed f64 values in little-endian format. This format avoids per-element unboxing overhead for similarity computations compared to a list of floats. ```sema ;; Single embedding (returns a bytevector) (define v1 (llm/embed "hello world")) ;; Pick the model per call with an options map (llm/embed "hello world" {:model "text-embedding-3-small"}) ;; Batch embeddings (llm/embed ["cat" "dog" "fish"]) ; => list of bytevectors ``` ## Embedding Accessors ### `embedding/length` Returns the number of dimensions (f64 elements) in an embedding bytevector. ```sema (define v (llm/embed "hello")) (embedding/length v) ; => 1024 (depends on provider) ``` ### `embedding/ref` Access a specific dimension by index. ```sema (define v (llm/embed "hello")) (embedding/ref v 0) ; => 0.0123 (first dimension) ``` ### `embedding/->list` Convert an embedding bytevector to a list of floats (useful for interop). ```sema (define v (llm/embed "hello")) (embedding/->list v) ; => (0.0123 -0.0456 ...) ``` ### `embedding/list->embedding` Convert a list of numbers to an embedding bytevector. ```sema (define v (embedding/list->embedding '(0.1 0.2 0.3))) (embedding/length v) ; => 3 ``` ## Computing Similarity ### `llm/similarity` Compute cosine similarity between two embedding vectors. Returns a value between -1.0 and 1.0. Accepts both bytevectors (fast path) and lists of floats (backward compatible). ```sema (define v1 (llm/embed "hello world")) (define v2 (llm/embed "hi there")) (llm/similarity v1 v2) ; => 0.87 (cosine similarity) ;; Also works with plain lists (llm/similarity '(0.1 0.2 0.3) '(0.4 0.5 0.6)) ``` ## Reranking ### `llm/rerank` Reorder a list of candidate documents by their relevance to a query using a hosted **cross-encoder** reranker (Cohere, Jina, Voyage, Nomic, Together AI, or Fireworks AI — the same **API key** you already use for embeddings; see [Supported Embedding Providers](#supported-embedding-providers) below for setup). Where `llm/similarity` / `vector-store/search` embed the query and documents *independently* (a bi-encoder), a reranker reads the query and each document *together*, so it's far more precise. The standard pattern is to retrieve a generous shortlist by vector search, then rerank it to the best few. ```sema (llm/rerank "how do I read a file?" (list "vectors are cool" "use file/read to read a file" "unrelated trivia") {:top-k 2}) ;; => ({:index 1 :score 0.91 :document "use file/read to read a file"} ...) ``` Returns `{:index :score :document}` maps, highest relevance first; `:index` points back into the input list. Options: `:top-k`, `:model`, and `:provider` (`:cohere` / `:jina` / `:voyage` / `:nomic` / `:together` / `:fireworks`). See the **[RAG guide](/docs/llm/rag)** for the full retrieve → rerank → answer pipeline. ## Token Counting ### `llm/token-count` Estimate the number of tokens in a string or list of strings. Uses a heuristic (chars/4) — no tokenizer dependency required. ```sema (llm/token-count "hello world") ; => 3 (llm/token-count '("hello" "world")) ; => sum of individual counts ``` ### `llm/token-estimate` Returns a detailed estimate map with the token count and the estimation method used. ```sema (llm/token-estimate "hello world") ; => {:method "chars/4" :tokens 3} ``` ## Supported Embedding Providers | Provider | Env Variable | Reranking | Default model | |----------|-------------|:---------:|---------------| | Jina | `JINA_API_KEY` | ✅ | `jina-embeddings-v3` | | Voyage | `VOYAGE_API_KEY` | ✅ | `voyage-3` | | Cohere | `COHERE_API_KEY` | ✅ | `embed-english-v3.0` | | Nomic | `NOMIC_API_KEY` | ✅ | `nomic-embed-text-v1.5` | | Together AI | `TOGETHER_API_KEY` | ✅ | `BAAI/bge-base-en-v1.5` | | Fireworks AI | `FIREWORKS_API_KEY` | ✅ | `fireworks/qwen3-embedding-8b` | | OpenAI | `OPENAI_API_KEY` | — | `text-embedding-3-small` | See [Provider Management](./providers.md) for the full provider capability table. --- --- url: 'https://sema-lang.com/docs/llm/vector-store.md' --- # Vector Store & Math ## In-Memory Vector Store Sema includes an in-memory vector store for semantic search over embeddings. Create named stores, add documents with embeddings and metadata, and search by cosine similarity. Stores can optionally be persisted to disk as JSON. ### `vector-store/create` Create a named in-memory vector store. Returns the store name. ```sema (vector-store/create "my-store") ``` ### `vector-store/open` Open a named store backed by a file. If the file exists, its contents are loaded; otherwise an empty store is created. The path is remembered for subsequent `vector-store/save` calls. ```sema (vector-store/open "my-store" "embeddings.json") ``` ### `vector-store/add` Add a document with an ID, embedding (bytevector), and metadata map. ```sema (vector-store/add "my-store" "doc-1" (llm/embed "Hello world") {:source "greeting.txt" :page 1}) ``` If a document with the same ID exists, it is replaced. ### `vector-store/search` Search by cosine similarity. Takes store name, query embedding, and k (number of results). Returns a list of maps with `:id`, `:score`, and `:metadata`. ```sema (vector-store/search "my-store" (llm/embed "Hi there") 5) ;; => ({:id "doc-1" :score 0.92 :metadata {:source "greeting.txt" :page 1}} ...) ``` ### `vector-store/delete` Delete a document by ID. Returns `#t` if found, `#f` otherwise. ```sema (vector-store/delete "my-store" "doc-1") ; => #t ``` ### `vector-store/count` Return the number of documents in a store. ```sema (vector-store/count "my-store") ; => 42 ``` ### `vector-store/save` Save a store to disk as JSON. If the store was opened with `vector-store/open`, the path is used automatically. Otherwise, pass a path explicitly. ```sema ;; Explicit path (vector-store/save "my-store" "embeddings.json") ;; Implicit path (if opened with vector-store/open) (vector-store/save "my-store") ``` The file format is a JSON document with base64-encoded embeddings and full metadata, portable across platforms. ## Vector Math These functions operate on embedding bytevectors (packed f64 arrays in little-endian format, as returned by `llm/embed` or `embedding/list->embedding`). ### `vector/cosine-similarity` Cosine similarity between two embedding vectors. Returns a float between -1.0 and 1.0. ```sema (vector/cosine-similarity (embedding/list->embedding '(1.0 0.0)) (embedding/list->embedding '(0.0 1.0))) ; => 0.0 ``` ### `vector/dot-product` Dot product of two embedding vectors. ```sema (vector/dot-product (embedding/list->embedding '(1.0 2.0 3.0)) (embedding/list->embedding '(4.0 5.0 6.0))) ; => 32.0 ``` ### `vector/normalize` Return a unit-length copy of the vector. ```sema (vector/normalize (embedding/list->embedding '(3.0 4.0))) ;; => embedding with values (0.6 0.8) ``` ### `vector/distance` Euclidean distance between two embedding vectors. ```sema (vector/distance (embedding/list->embedding '(0.0 0.0)) (embedding/list->embedding '(3.0 4.0))) ; => 5.0 ``` ## Full Example A RAG-style workflow: embed documents, store them, search semantically, and persist to disk. ```sema ;; Open a persistent store (creates file if it doesn't exist) (vector-store/open "docs" "my-docs.json") (define texts '("Rust is a systems language" "Python is great for ML" "Lisp is homoiconic")) (for-each (lambda (text) (vector-store/add "docs" text (llm/embed text) {:text text})) texts) ;; Save to disk (vector-store/save "docs") ;; Retrieve the most relevant chunks for a question... (define question "Which language is homoiconic?") (define hits (vector-store/search "docs" (llm/embed question) 2)) ;; ...then generate an answer grounded in only that context (the "G" in RAG) (define context (string/join (map (lambda (h) (:text (:metadata h))) hits) "\n")) (llm/complete (prompt (system "Answer using only the provided context. Be concise.") (user (format "Context:\n~a\n\nQuestion: ~a" context question))) {:max-tokens 120}) ;; => "Lisp — it is homoiconic." ``` Next time you run, `(vector-store/open "docs" "my-docs.json")` will load the saved embeddings instantly — no re-embedding needed. ::: tip Sharpen results with a reranker Cosine `vector-store/search` has high recall but coarse ordering. For better precision, retrieve a larger shortlist and reorder it with the cross-encoder [`llm/rerank`](/docs/llm/embeddings#reranking) — the standard *retrieve-many → rerank-to-a-few* RAG move. See the **[RAG guide](/docs/llm/rag)** for the full pipeline. ::: ::: warning Use one embedding model per store Every document and the query must share the same embedding dimensions. Mixing embedding models (or providers) in one store raises a *dimension-mismatch* error at search time — so pick one embedding model per store. ::: --- --- url: 'https://sema-lang.com/docs/llm/cassettes.md' --- # Cassettes (Record & Replay) A **cassette** saves the answers from real LLM calls to a file the first time you run, then plays them back on every run after — no API key, no network, the same output every time. It's like recording a conversation once and replaying the tape. Two things this makes easy: * **Tests that don't need a key.** Record a run once, commit the file, and your `llm/complete` and `agent/run` tests run offline and give the same result forever — so they pass reliably in CI with no secrets and no flakiness. * **Demos and docs that always work.** A playground example or a notebook can ship its tape and render the exact same output every time, offline, with no model drift. Because the saved answer keeps its real token counts, cost and budget tracking keep working on replay too — so even cost tests become repeatable. ## Quick start ```sema ;; First run: calls the real model and saves the answer to the file. ;; Every run after: plays the saved answer back — offline, identical. (llm/with-cassette "tapes/greeting.jsonl" {:mode :auto} (fn () (llm/complete "Say hello in one word." {:model "gpt-5-mini"}))) ;; => "Hello" ``` Run it once with an API key set to capture the tape, commit `tapes/greeting.jsonl`, and from then on the call is offline and deterministic. That's the whole idea. ## The three modes `:mode` decides what happens on each call: | Mode | If the call is on the tape | If it's a new call | | --- | --- | --- | | `:auto` *(default)* | play it back | call the model and record it | | `:replay` | play it back | **error** — the call wasn't recorded | | `:record` | call the model and record it | call the model and record it | `:auto` is the friendly default for writing tapes: it records what's missing and replays what it already has. `:replay` is what you want in CI — it never touches the network, and a call that isn't on the tape is a **hard error** that names the request. That error is a feature: if you change a prompt, the matching recording disappears, and the failure tells you exactly which call drifted instead of silently hitting a live model. ## What you can record Cassettes cover the everyday LLM calls. Each is matched and replayed independently: | Call | Works? | Notes | | --- | --- | --- | | `llm/complete`, `llm/chat` | ✅ | the answer, model, tokens, and finish reason | | `llm/extract` and structured calls | ✅ | the structured result is rebuilt from the saved answer | | `agent/run` and tool loops | ✅ | **each turn is saved separately**, so a full multi-turn run (model → tool call → result → final answer) replays exactly — your tool handlers still run on replay | | `llm/stream` (streaming) | ✅ | the text chunks are saved and replayed in order — see [Streaming](#streaming-in-detail) | | `llm/embed` (embeddings) | ✅ | the vectors are saved and replayed byte-for-byte | A note on **agents**: because each model turn is recorded on its own, your tools execute normally during replay — the cassette only stands in for the *model's* responses, not for your tool code. That's usually what you want: deterministic model output, real tool logic. ## Using cassettes ### `llm/with-cassette` — record/replay for a block The usual way: wrap the calls you want recorded in a function. The tape is saved when the block finishes, and the caller's prior cassette is restored. ```sema (llm/with-cassette "tapes/weather-agent.jsonl" {:mode :auto} (fn () (define bot (agent {:model "gpt-5-mini" :tools [get-weather]})) (agent/run bot "What's the weather in Oslo?"))) ``` The options map is optional and currently takes `:mode` (`:auto`, `:record`, or `:replay`, default `:auto`). The file — and any missing folders — is created when the tape is written. A task spawned inside the block captures its cassette scope. It can finish after the block returns or be awaited later; any recordings it produces are flushed when the last task using that captured scope finishes. ### Turning it on by hand If your setup and teardown aren't a single block — for example in a test harness or a notebook — use the imperative trio: ```sema (llm/cassette-load "tapes/suite.jsonl" {:mode :replay}) ; turn it on ;; ... run many calls ... (llm/cassette-save) ; write the tape to disk (returns #t if a cassette is active) (llm/cassette-eject) ; write the tape and turn it back off ``` `llm/cassette-load` affects subsequent calls in the current evaluation. Tasks spawned after the load inherit the cassette; tasks already spawned keep the scope they captured. Ejecting removes the cassette from the current scope but does not detach it from those existing tasks. ### Forcing replay across a whole run (CI) Two environment variables initialize the cassette for a Sema run, so a whole suite — or a whole notebook — runs offline without changing any code: ```bash SEMA_LLM_CASSETTE=tapes/suite.jsonl \ SEMA_LLM_CASSETTE_MODE=replay \ sema test/agents.sema ``` `SEMA_LLM_CASSETTE_MODE` is `replay`, `record`, or `auto` (default `auto`). This is ignored under `--sandbox`, since it reads and writes a file. A common CI pattern: record tapes locally once with a key, commit them, and run the suite with `SEMA_LLM_CASSETTE_MODE=replay` so any un-recorded call fails loudly. ## Streaming in detail Streaming hands you the answer in pieces — *chunks* — as the model generates them, by calling a function you pass for each piece (a typing effect, a progress bar, live output). A cassette records **the exact sequence of chunks**, then on replay feeds those same chunks to your callback in the same order. So a streaming UI behaves identically offline: ```sema ;; Record once, then replay forever — the chunks arrive the same way both times. (llm/with-cassette "tapes/story.jsonl" {:mode :auto} (fn () (llm/stream "Tell me a two-line story." (fn (chunk) (display chunk)) ; called once per recorded chunk, in order {:model "gpt-5-mini"}))) ``` Things worth knowing about streamed replay: * **Boundaries are preserved.** If the recording arrived as `"Hel" "lo"`, replay calls your function with `"Hel"` then `"lo"` — not one combined `"Hello"`. Code that depends on chunking sees the same shape. * **Replay is instant.** The chunks are delivered as fast as your callback accepts them; the original network timing between chunks is *not* reproduced. Replay is for determinism, not for re-simulating latency. * **The full answer is saved too.** Alongside the chunks, the complete text, model, and token counts are recorded — so cost tracking and `llm/last-usage` work on a replayed stream just like a normal call. If you only print the chunks (no callback), `llm/stream` writes to stdout; recording and replay work the same way. ## Embeddings in detail `llm/embed` returns vectors (as bytevectors). A cassette saves those vectors and returns them verbatim on replay — so similarity scores, vector-store contents, and any math built on them are exactly reproducible offline: ```sema (llm/with-cassette "tapes/embeddings.jsonl" {:mode :auto} (fn () (define v (llm/embed "semantic search query" {:model "text-embedding-3-small"})) (vector/cosine-similarity v (llm/embed "another phrase")))) ``` Both `llm/embed` calls are recorded (keyed by their text), so the similarity number is identical every run. Batch embeddings — passing a list of strings — are saved as a set of vectors and replayed in order. ## Using cassettes in notebooks Cassettes are a great fit for [notebooks](../notebook): record the LLM cells once with a key, commit the tape next to the `.sema-nb` file, and the notebook re-runs the same way forever — offline, for anyone, in CI. There are two clean patterns. ### A setup cell that turns it on Put one cell near the top of the notebook that loads a cassette; every LLM cell after it records or replays automatically (cells in a notebook share one environment): ```sema ;; Cell 1 — setup (llm/cassette-load "tapes/notebook.jsonl" {:mode :auto}) ``` ```sema ;; Cell 2 — a normal LLM cell; recorded on first run, replayed after (llm/complete "Summarize the Sema language in one sentence." {:model "gpt-5-mini"}) ``` ```sema ;; Last cell — flush the tape so the recording is written (llm/cassette-save) ``` Run the notebook once with a key to capture `tapes/notebook.jsonl`, commit it alongside the notebook, and every later run (including a headless `sema notebook run`) replays it. ### Force replay for the whole notebook To guarantee a notebook never calls a model — say when you publish it or run it in CI — run it with the environment variable set, no edits required: ```bash SEMA_LLM_CASSETTE=tapes/notebook.jsonl \ SEMA_LLM_CASSETTE_MODE=replay \ sema notebook run my-notebook.sema-nb ``` Any cell that makes a call not on the tape fails with a clear "cassette miss", so a stale notebook can't quietly reach for a live model. > **Tip:** keep tapes next to what they belong to — `tapes/` beside a test, or beside the > `.sema-nb` — and commit them. They're plain text and diff cleanly, so a reviewer can see > exactly how the recorded model output changed when you re-record. ## How it works with the rest of Sema A cassette slots in just above the real model and below everything else, so it composes instead of conflicting: * **Cost & budgets.** A replayed answer keeps its real token counts, so `llm/last-usage`, `llm/session-usage`, and budget limits all behave as if the call really happened. This is different from a [cache](./caching) hit, which reports **zero** usage (no call was made); a replay stands in for a real call, so it reports the real spend. * **Tracing.** A replayed call still produces its [OpenTelemetry](./observability) trace, with the recorded model and token counts — so replayed runs show up in your traces just like live ones. * **The response cache.** `llm/with-cassette` turns the in-memory response [cache](./caching) off for its block, so the cache can't answer before the tape does. You generally want one or the other, not both. * **Retries & fallback.** While *recording*, the normal [retry and fallback](./resilience) logic wraps the real call, so the tape captures the final successful answer. On replay there's nothing to retry. ## What's in the file A tape is plain text — **NDJSON**, one JSON object per line — so it's diffable, appendable, and reviewable in a pull request. There's one line per saved call, and the `kind` field says what it is: ```jsonl {"v":1,"kind":"complete","key":"a1b2…","content":"Hello","model":"gpt-5-mini","prompt_tokens":12,"completion_tokens":1} {"v":1,"kind":"stream","key":"c3d4…","content":"Hi there","model":"gpt-5-mini","chunks":["Hi"," there"],"completion_tokens":2} {"v":1,"kind":"embed","key":"e5f6…","model":"text-embedding-3-small","embeddings":[[0.01,-0.02,0.03]]} ``` Only the **answer** is saved, looked up by a fingerprint (`key`) of the request. The prompt text, your API key, and any headers are **never written to the file** — they simply aren't part of what gets saved, so a tape is safe to commit. The `v` field is a format version, there so old tapes can be migrated if the shape ever changes. ### What counts as "the same call" Two calls match if their meaningful inputs are the same — the model, the system prompt, the temperature, and the messages. Change any of those and it's a different call: in `:replay` mode you get a clear "not recorded" error, which is exactly what flags a prompt or model change. Things that don't affect the answer — request IDs, timing, your API key — are not part of the fingerprint. ## Recipes * **Record once, replay in CI.** Run the suite locally with a key and `:mode :auto` (or `:record`) to capture tapes, commit them, then run CI with `SEMA_LLM_CASSETTE_MODE=replay`. New or changed calls fail loudly. * **Update a tape after a prompt change.** Delete the tape (or the affected line) and re-run in `:auto`, or run that block in `:record` once. Commit the new tape; the diff shows how the model's answer changed. * **A reproducible demo.** Wrap the demo's LLM calls in `llm/with-cassette … {:mode :replay}` and ship the tape, so it runs for anyone with no key. ## Good to know * **Re-record after changes.** Change a prompt, model, or temperature and the old tape no longer matches — re-record it (`:record`, or delete the file and run `:auto`). * **One answer per call.** The first recorded answer for a given call is the one replayed. * **Replay needs no provider.** In `:replay` mode nothing calls a model, so a cassette works with no API key configured at all. * **Cassette miss?** A "cassette miss in :replay mode" error means this exact call wasn't recorded. Either the request changed (re-record it) or you're replaying a call you never captured — switch that block to `:auto` to record it, then commit the updated tape. --- --- url: 'https://sema-lang.com/docs/llm/observability.md' --- # Tracing & Metrics Sema can record what happens inside every LLM and agent run — each model call, tool execution, retry, and notebook cell — as [OpenTelemetry](https://opentelemetry.io/) traces and metrics, and send them to a tool where you can browse them. You don't write any instrumentation: switch it on with one environment variable — or one [`otel/configure`](#configuring-from-sema-code) call — and `llm/complete`, `agent/run`, `llm/embed`, and the rest are recorded automatically. If OpenTelemetry is new to you, the terms used below: * **OpenTelemetry (OTel)** is an open, vendor-neutral standard for traces and metrics. * A **trace** is one run. It is made of **spans** — individual timed operations such as a single LLM call or a tool execution. Spans nest, so an agent run appears as a tree. * **OTLP** is the network protocol OTel uses. Sema speaks OTLP, so it works with any tool that accepts it — a free local viewer like [Jaeger](https://www.jaegertracing.io/), or a hosted service like [Langfuse](https://langfuse.com/), Grafana, or Datadog. * Sema follows the OTel [GenAI semantic conventions](https://github.com/open-telemetry/semantic-conventions-genai) — the agreed attribute names for LLM telemetry (token counts, model, cost, …) — so these tools understand the data with no per-tool glue. Grafana, Jaeger, SigNoz, OpenObserve, Datadog, Honeycomb, Logfire, MLflow, and others read it as-is; a few LLM-specific tools (Arize Phoenix, Langfuse, …) need one extra setting — see [Backend Compatibility](./otel-compat). Tracing is **off by default** — if you don't point Sema at a backend or a file, it records nothing. And once it's on, a slow or unreachable backend can never block, delay, or crash your script: telemetry is sent in the background, out of the way of your run. ## How to turn it on The usual way is **environment variables** — values you set in your shell. (To turn tracing on from inside a script instead, see [Configuring from Sema code](#configuring-from-sema-code) below.) You can set them inline for a single command, or `export` them for the whole session: ```bash # Inline — applies to this one run only: OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 sema myscript.sema # Or exported — applies to every command in this shell session: export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 sema myscript.sema ``` Two variables decide *where* the data goes, and **setting either one turns tracing on**: * `OTEL_EXPORTER_OTLP_ENDPOINT` — send to a backend over the network (Jaeger, Langfuse, …). * `SEMA_OTEL_FILE` — write to a local file instead (handy with no backend at all). Set neither and tracing stays off. The full list of variables is in [Configuration reference](#configuration-reference) below. ### Configuring from Sema code Environment variables aren't the only way. `otel/configure` turns tracing on from inside a script, so a program can point itself at a backend without any shell setup: ```sema ;; Hosted backend with an API key — :key becomes an Authorization: Bearer header. (otel/configure {:endpoint "https://cloud.langfuse.com/api/public/otel" :key "sk_prod_123" :service-name "my-agent"}) (llm/complete "say hi" {:max-tokens 16}) ; recorded against that backend ``` Call it once, early — before any `llm/*` or `agent/*` work. It installs a provider on the first call and returns `#t` when this call turned tracing on, or `#f` when nothing was configured or telemetry was already active (from the environment at startup, or an earlier `otel/configure`). One provider is installed per process, so environment configuration — if present — wins, and a later `otel/configure` is a no-op. The config map accepts: | Key | Maps to | What it does | | --- | --- | --- | | `:endpoint` | `OTEL_EXPORTER_OTLP_ENDPOINT` | Backend address. **Setting it turns tracing on.** | | `:file` | `SEMA_OTEL_FILE` | Write JSONL spans to a path instead of the network. Also turns tracing on. | | `:protocol` | `OTEL_EXPORTER_OTLP_PROTOCOL` | `"http/protobuf"` (default) · `"http/json"` · `"grpc"` (release builds; see below). | | `:key` | `OTEL_EXPORTER_OTLP_HEADERS` | An API key, sent as `Authorization: Bearer `. | | `:headers` | `OTEL_EXPORTER_OTLP_HEADERS` | Extra headers — a map (`{:x-project "app"}`) or a pre-formatted `"name=value,..."` string. | | `:service-name` | `OTEL_SERVICE_NAME` | The name runs appear under. | | `:environment` | `SEMA_OTEL_ENVIRONMENT` | Deployment label (`prod`, `staging`, …). | | `:release` | `SEMA_OTEL_RELEASE` | Release/version stamp. | | `:capture-content` | `SEMA_OTEL_CAPTURE_CONTENT` | `#t` to record prompt/response text (off by default — see [Privacy](#privacy)). | Each key maps to the environment variable of the same role, so a script that configures itself behaves exactly like one driven by the environment. Extra headers as a map: ```sema (otel/configure {:endpoint "https://otlp.example.com" :headers {:x-project "checkout" :x-tenant "acme"}}) ``` Or capture to a local file with no backend at all: ```sema (otel/configure {:file "/tmp/sema-trace.jsonl"}) ``` ## Quick start: see a trace in one minute [Jaeger](https://www.jaegertracing.io/) is a free trace viewer that runs in a single container — a good way to see your first trace. ```bash # 1. Start Jaeger. The UI is on port 16686; it accepts traces on 4318. docker run --rm -d --name jaeger -p 4318:4318 -p 16686:16686 \ -e COLLECTOR_OTLP_ENABLED=true jaegertracing/all-in-one # 2. Point Sema at it and run something. No model is pinned here, so this uses # your default provider and its default model — just make sure an API key is # set (ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY / …). OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \ sema -e '(llm/complete "say hi" {:max-tokens 16})' ``` Open `http://localhost:16686` in your browser, pick the **sema** service, and you'll see one trace whose `chat` span carries the provider, model, input/output token counts, cost, and finish reason. > **Choosing a specific model.** The example uses whichever provider is active. To pick > one, select it first: `(llm/set-default :openai)` then `{:model "gpt-5-mini"}`, or > `(llm/set-default :anthropic)` then `{:model "claude-haiku-4-5-20251001"}`. A model id > only works with the provider that offers it — sending an OpenAI model id to Anthropic > returns a 404. ## Configuration reference Every setting is an environment variable (see [How to turn it on](#how-to-turn-it-on) for how to set them) — or the matching `otel/configure` key (see [Configuring from Sema code](#configuring-from-sema-code)). The `OTEL_*` names come from OpenTelemetry itself; the `SEMA_OTEL_*` names are Sema conveniences. | Variable | What it does | | --- | --- | | `OTEL_EXPORTER_OTLP_ENDPOINT` | The address of your tracing backend, e.g. `http://localhost:4318`. **Setting this turns tracing on.** | | `OTEL_EXPORTER_OTLP_PROTOCOL` | How to talk to it: `http/protobuf` (default) · `http/json` · `grpc`. Keep the default unless your backend only accepts gRPC. gRPC support is compiled into release binaries (installer, Homebrew, GitHub archives); a plain `cargo install sema-lang` or source build needs `--features otel-grpc`, otherwise `grpc` warns once and falls back to `http/protobuf`. | | `OTEL_EXPORTER_OTLP_HEADERS` | Extra HTTP headers, usually authentication — e.g. `Authorization=Bearer `. Comma-separated `name=value` pairs; see [Authentication headers](#authentication-headers). | | `OTEL_EXPORTER_OTLP_TIMEOUT` | Per-export timeout in milliseconds. Keep it short (e.g. `3000`) so a dead backend never holds things up. | | `OTEL_SERVICE_NAME` | The name your runs appear under in the backend (default `sema`). | | `SEMA_OTEL_FILE` | Write traces to this file path, one JSON object per line, instead of sending them over the network. Also turns tracing on. | | `SEMA_OTEL_ENVIRONMENT` | A label such as `prod` or `staging` for filtering (recorded as `deployment.environment.name`). | | `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` | Set to `true` to also record the **prompt and response text** (off by default — see [Privacy](#privacy)). Sema also accepts the shorter alias `SEMA_OTEL_CAPTURE_CONTENT`. | | `OTEL_BSP_MAX_QUEUE_SIZE`, `OTEL_BSP_MAX_EXPORT_BATCH_SIZE`, `OTEL_BSP_SCHEDULE_DELAY` | Advanced: tune the background export batching. The defaults are fine for most uses. | Sema can send over HTTP or gRPC; choose with `OTEL_EXPORTER_OTLP_PROTOCOL`. HTTP (the default) is what most backends accept — only switch to gRPC if yours requires it. ### Writing to a file instead of a backend No backend running? Set `SEMA_OTEL_FILE` and Sema writes each finished span to a file as one JSON object per line: ```bash SEMA_OTEL_FILE=/tmp/sema-trace.jsonl \ sema -e '(llm/complete "ping" {:max-tokens 16})' cat /tmp/sema-trace.jsonl | jq . ``` The file is written synchronously, so even a one-line script captures its spans. ## Authentication headers Almost every **hosted** backend needs an API key, and you pass it as an HTTP header through `OTEL_EXPORTER_OTLP_HEADERS`. (This is separate from `SEMA_OTEL_COMPAT`, which only relabels attribute names — see [Backend Compatibility](./otel-compat).) The header **name** and the key are dictated by the backend, not by Sema; always check the tool's own OTLP page for the exact names. ### The format `OTEL_EXPORTER_OTLP_HEADERS` is a comma-separated list of `name=value` pairs — the [W3C Baggage](https://www.w3.org/TR/baggage/) format the OpenTelemetry spec mandates: ```bash # one header OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer sk-abc123" # two headers — separate with a comma OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer sk-abc123,x-project=my-app" ``` Rules worth knowing: * **Separate multiple headers with commas**, not semicolons — semicolons are not supported. * **The first `=` splits the name from the value**, so the value itself may contain `=`. base64 strings with `=` padding (common in Basic auth) work fine. * **Avoid literal commas or spaces inside a value** — a comma starts a new header. If a token genuinely needs one, percent-encode it (`,` → `%2C`). Bearer tokens and base64 never contain commas, so this rarely comes up. * **Quote the whole value in your shell** so `$(...)` substitutions and special characters survive. ### Common patterns | Auth style | `OTEL_EXPORTER_OTLP_HEADERS` value | Example tools | | --- | --- | --- | | Bearer token | `Authorization=Bearer ` | Braintrust, Lunary, LangSmith | | Basic auth | `Authorization=Basic ` | Langfuse, W\&B Weave | | Vendor key header | `x-portkey-api-key=` · `dd-api-key=` | Portkey, Datadog | ### Building a Basic-auth header Basic auth wants base64 of `id:secret`. Build it with `base64` and read the keys from environment variables rather than hard-coding them. For [Langfuse](https://langfuse.com/): ```bash export OTEL_EXPORTER_OTLP_ENDPOINT="https://cloud.langfuse.com/api/public/otel" export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic $(echo -n "$LANGFUSE_PUBLIC_KEY:$LANGFUSE_SECRET_KEY" | base64)" sema myagent.sema ``` ### Tools that need more than one header Some backends need a second (or third) header to route the trace to the right project or workspace — the auth key alone isn't enough. The exact names come from each tool's OTLP docs: | Tool | `OTEL_EXPORTER_OTLP_HEADERS` value | | --- | --- | | HoneyHive | `Authorization=Bearer ,x-honeyhive=project:` | | W\&B Weave | `Authorization=Basic ,project_id=/` | | Maxim | `x-maxim-api-key=,x-maxim-repo-id=` | | Opik | `Authorization=,projectName=,Comet-Workspace=` | Several LLM-focused backends can also show **richer** detail with one extra compatibility setting on top of the auth header — see [Backend Compatibility](./otel-compat). ## What gets traced | Span | Kind | Name | When | | --- | --- | --- | --- | | LLM call | `CLIENT` | `chat {model}` | every non-streaming completion (including cache hits) | | Embeddings | `CLIENT` | `embeddings {model}` | every `llm/embed` | | Tool call | `INTERNAL` | `execute_tool {name}` | every tool dispatch in an agent loop | | Agent run | `INTERNAL` | `invoke_agent {name}` | every `agent/run` / tools-enabled completion | | Notebook run | `INTERNAL` | `notebook.run_all` → `notebook.cell {id}` | a notebook "Run All" (one trace, one child span per cell) | | Retry | `INTERNAL` | `llm.retry_attempt` | each HTTP retry (429 / 5xx / network), nested under the LLM span | Each LLM span carries the standard GenAI attributes: `gen_ai.operation.name`, `gen_ai.provider.name`, `gen_ai.request.model` / `gen_ai.response.model`, `gen_ai.usage.input_tokens` / `output_tokens`, prompt-cache token counts, `gen_ai.response.finish_reasons`, and the computed cost (`gen_ai.usage.cost`, plus `gen_ai.usage.cost_usd`). Cache hits are flagged with `sema.gen_ai.cache.hit`. Tool spans carry `gen_ai.tool.name` / `gen_ai.tool.call.id` / `gen_ai.tool.type`. ### Sessions and users (grouping multi-turn runs) Every span carries a `gen_ai.conversation.id`, generated per run or supplied by you. For tools that group by session (such as Langfuse), Sema also emits `session.id` and `user.id`, so the turns of one conversation appear together: ```sema (agent/run bot "what is 2 + 3?" {:session-id "chat-42" :user-id "alice"}) (agent/run bot "now add 10" {:session-id "chat-42" :user-id "alice"}) ;; both runs appear under one session "chat-42", attributed to alice ``` `agent/run`, `llm/chat`, and `llm/complete` accept `:conversation-id`, `:session-id`, and `:user-id`. If you omit `:session-id` it defaults to the conversation id; a standalone completion gets a fresh conversation id automatically. ### Metrics When you export over a network endpoint, Sema also records two standard GenAI metric histograms: * `gen_ai.client.token.usage` — token counts (dimension `gen_ai.token.type` = `input` or `output`). * `gen_ai.client.operation.duration` — call latency in seconds. > Cache hits report zero usage by design (no provider call was made), so token metrics > undercount real spend when caching is in play. ## Adding your own spans The `llm/*` and `agent/*` calls are traced for you. When you build your *own* abstraction — a custom RAG loop, a batch job, a provider Sema doesn't ship — these builtins let it emit first-class spans too. Every one is a **no-op when tracing is off**, so they are safe to leave in, and they never change your program's return value. ### Generic spans ```sema ;; with-span runs the body inside a named span carrying an attribute map, ends it on exit ;; (Error status if the body throws), and returns the body's value. Use {} for no attrs. (with-span "ingest-batch" {:batch.size 100} (otel/event "started" {}) (process-batch)) ``` The underlying builtin is `(otel/span name thunk attrs)`; `with-span` is the ergonomic macro over it. Any LLM/tool spans created inside nest beneath it. ### Annotate the current span ```sema (otel/set-attribute :http.status 200) ; one attribute on the innermost span (otel/set-attributes {:rows 42 :cache.hit true}) (otel/set-status :ok) ; or (otel/set-status :error "upstream timeout") (otel/event "cache-miss" {:key "user:42"}) ; a point-in-time event ``` Attribute values keep their type — integers, floats, and booleans render as numbers/bools in the backend, not strings. ### Typed spans (render like the built-ins) For work that *is* an LLM call, tool, or retrieval — but that you implement yourself — use the typed helpers. They set `gen_ai.operation.name` and, when `SEMA_OTEL_COMPAT` is set, the backend-native span-kind, so a custom pipeline classifies in Phoenix/Traceloop/Langfuse exactly like the built-in `llm/*` spans. ```sema ;; A custom LLM/generation call (a provider Sema doesn't natively support): (otel/llm-span {:model "custom-model" :provider "myco" :operation "chat"} (lambda () (let ((resp (my-http-llm-call prompt))) ;; Account tokens + cost on the span — same gen_ai.usage.* keys as the built-ins. (otel/llm-usage {:input-tokens 120 :output-tokens 30 :cost-usd 0.001}) resp))) ;; A user-built retrieval step (first-class RETRIEVER span): (otel/retrieval-span "vector-search" (lambda () (search index query)) {:top-k 5}) ;; A user tool: (otel/tool-span "lookup-weather" (lambda () (weather city))) ``` ### Grouping into sessions `with-session` groups every span started in its body under a session id (and optional user), filling Langfuse **Sessions/Users** for non-agent code: ```sema (with-session "chat-42" {:user "alice"} (llm/complete "...") ; inherits session chat-42, user alice (my-custom-pipeline)) ``` | Form | What it does | | --- | --- | | `(with-span name attrs body…)` / `(otel/span name thunk attrs)` | Generic span around a block. | | `(otel/set-attribute key value)` / `(otel/set-attributes map)` | Set attribute(s) on the innermost active span. | | `(otel/set-status :ok)` / `(otel/set-status :error msg)` | Set the innermost span's status. | | `(otel/event name attrs-map)` | Point-in-time event on the current span. | | `(otel/llm-span config thunk)` + `(otel/llm-usage usage-map)` | Typed LLM/generation span + token/cost accounting. | | `(otel/tool-span name thunk [attrs])` | Typed TOOL span. | | `(otel/retrieval-span name thunk [attrs])` | Typed RETRIEVER span. | | `(with-session id config body…)` / `(otel/with-session id [config] thunk)` | Group spans into a session/user. | ## Privacy Prompt and response **text** is never recorded unless you explicitly set `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true`. Token counts, model names, cost, and timing carry no message text and are always exported. When content capture is on, long messages are truncated to keep span sizes reasonable. ## Embedding Sema in a Rust application When Sema runs as a library inside your own program, it **never installs a global tracer provider on its own** — that is the host application's job. You choose how it connects to telemetry with `InterpreterBuilder::with_telemetry(mode)`: ```rust use sema::InterpreterBuilder; use sema_otel::TelemetryMode; // Emit against the provider your application already installed in `opentelemetry::global`. let interp = InterpreterBuilder::new() .with_telemetry(TelemetryMode::UseHostGlobal) .build(); ``` | `TelemetryMode` | Behavior | | --- | --- | | `Off` (default) | No telemetry; never touches any global state. | | `UseHostGlobal` | Emit against the global provider your app already installed (silent no-op if there is none). | | `OwnProvider(p)` | Emit against a provider you hand to Sema; installs **no** global provider. | | `FromEnv` | Self-install from the `OTEL_*` / `SEMA_OTEL_FILE` variables. The provider is owned by the built `Interpreter` and flushes when it is dropped. If your app already runs OpenTelemetry, prefer `UseHostGlobal` or `OwnProvider`. | Sema's spans automatically nest under whatever span is current (`opentelemetry::Context::current()`), so a host request span becomes the parent of Sema's `invoke_agent → chat / execute_tool` tree. `Interpreter::new()` and `build()` with the default `Off` never touch global OpenTelemetry state. --- --- url: 'https://sema-lang.com/docs/llm/otel-compat.md' --- # Backend Compatibility By default Sema labels its telemetry with the [OpenTelemetry GenAI semantic conventions](https://github.com/open-telemetry/semantic-conventions-genai) — the standard `gen_ai.*` attribute names. Tools that follow that standard understand Sema's traces with no extra configuration. A handful of popular LLM-observability tools don't read `gen_ai.*` — they look for their own attribute names instead, so a Sema span can show up in them as "unknown" or with blank fields. For those tools, set the `SEMA_OTEL_COMPAT` environment variable to a **compatibility mode** — a short name such as `openinference` or `langfuse` that tells Sema which extra attribute names to write *alongside* the standard ones. Nothing about your program changes — it's still the same automatic tracing, just labelled so more tools can read it. This is purely additive: the standard `gen_ai.*` attributes are always present; `SEMA_OTEL_COMPAT` only adds extra copies under other names. Read the [Tracing & Metrics](./observability) page first for how tracing works and how to point Sema at a backend — this page only covers the per-tool labelling. ## Which tools need a compatibility mode This section covers the tools that can **receive** OpenTelemetry traces over OTLP. Most of them read the standard `gen_ai.*` attributes and need **no** compatibility mode; only a few key off their own attribute names and need a `SEMA_OTEL_COMPAT` mode. (Tools that ingest only through their own SDK, sit in front of your calls as a proxy, or run offline evaluations can't receive an OTLP push at all — see [Tools you can't send traces to](#tools-you-can-t-send-traces-to).) ::: tip "No compatibility mode" is not the same as "no setup." Almost every **hosted** service still needs its own authentication header — an API key passed through `OTEL_EXPORTER_OTLP_HEADERS`, exactly as shown for Langfuse on the [Tracing & Metrics](./observability#sending-to-hosted-langfuse) page. That auth header is a property of the *backend*, not a Sema compatibility mode. The tables below say only whether a tool needs a `SEMA_OTEL_COMPAT` mode to *understand* Sema's attributes — the column for "does it need an API key" is "almost always, if it's hosted". ::: ### Reads `gen_ai.*` — no compatibility mode needed **General trace viewers and APM platforms.** These store and display `gen_ai.*` as ordinary span attributes (the LLM-specific ones also build GenAI dashboards from them): | Tool | Self-hostable? | Notes | | --- | --- | --- | | Grafana / Tempo, [Jaeger](https://www.jaegertracing.io/) | yes | plain OpenTelemetry trace viewers | | [SigNoz](https://signoz.io/) | yes | OTLP on 4317 / 4318 | | [OpenObserve](https://openobserve.ai/) | yes | OTLP `/api/{org}/v1/traces` *(verified live)* | | Honeycomb, Elastic | partly | general OTel APM | | [Logfire](https://pydantic.dev/logfire) | no | Pydantic's OTel platform | | [Datadog](https://www.datadoghq.com/) LLM Observability | no | maps `gen_ai.*` (semconv 1.37+) natively; needs a Datadog API-key header | | [Dynatrace](https://www.dynatrace.com/) | no | maps `gen_ai.*` natively; needs a Grail (DPS) licence + an ingest token | | [Coralogix](https://coralogix.com/) AI Center | no | maps `gen_ai.*`, but needs account-side setup (S3-archive routing + the experimental-semconv opt-in) | | [New Relic](https://newrelic.com/) | no | accepts OTLP and stores `gen_ai.*` as raw attributes; native GenAI dashboards are not documented | **LLM-native platforms.** These parse `gen_ai.*` into structured LLM records on their own OTLP endpoint (all hosted ones need an API key/header): | Tool | Self-hostable? | OTLP endpoint / notes | | --- | --- | --- | | [OpenLIT](https://openlit.io/) | yes | OTel-native; `docker compose up -d`; OTLP on 4318, no auth by default | | [MLflow](https://mlflow.org/) | yes | tracking server exposes an OTLP `/v1/traces` endpoint | | [Braintrust](https://www.braintrust.dev/) | no | maps `gen_ai.*` to structured fields; API key required (see the optional `braintrust` mode below) | | [W\&B Weave](https://wandb.ai/) | no | `…/otel/v1/traces`; parses `gen_ai.*`; Basic-auth + `project_id` header *(verified in docs)* | | [Portkey](https://portkey.ai/) | no | `/v1/otel/v1/traces`; reads `gen_ai.*`; `x-portkey-api-key` header | | [HoneyHive](https://honeyhive.ai/) | no | `/v1/traces`; reads `gen_ai.*`; Bearer + `x-honeyhive` project header | | [Opik](https://www.comet.com/opik) (Comet) | yes | `/api/v1/private/otel` (HTTP only); API key + project/workspace headers | | [Lunary](https://lunary.ai/) | yes | `/v1/otel`; reads `gen_ai.*`; Bearer token | | [Maxim AI](https://www.getmaxim.ai/) | no | `/v1/otel`; reads `gen_ai.*` / `llm.*` / `ai.*`; `x-maxim-*` headers | | [PostHog](https://posthog.com/) | yes | `/i/v0/ai/otel`; maps `gen_ai.*` → `$ai_*` events; project token | | [FutureAGI](https://futureagi.com/) | no | native convention is `gen_ai.*` (+ `fi.span.kind`); OpenInference is only an optional output mode | | [Laminar](https://www.lmnr.ai/) | yes | parses `gen_ai.*` (+ its own `lmnr.*`); HTTP + gRPC; API key | | [Agenta](https://agenta.ai/) | yes | translates `gen_ai.*` into its own `ag.*`; HTTP/protobuf only; API key | | [Confident AI](https://www.confident-ai.com/) | no | Observatory endpoint reads `gen_ai.*` (+ `confident.*`); API key — this is DeepEval's backend | | [Patronus AI](https://docs.patronus.ai/) | no | OTLP gRPC; ingests standard OTel spans; `x-api-key` header | | [Promptfoo](https://www.promptfoo.dev/) | local | built-in OTLP receiver (port 4318) **while `promptfoo eval` runs**; no token | ### Needs a compatibility mode These ingest OTLP but key off their **own** attribute names, so without the matching `SEMA_OTEL_COMPAT` mode a Sema span shows up with blank or "unknown" fields: | Tool | `SEMA_OTEL_COMPAT` mode | What it adds | | --- | --- | --- | | [Arize Phoenix](https://phoenix.arize.com/), [Arize AX](https://arize.com/) | `openinference` | span types, model/provider, tokens, cost, message I/O, tool args + schemas | | [Langfuse](https://langfuse.com/) | `langfuse` | observation type/model, usage + cost detail, trace-level input/output, tags | | [Traceloop](https://www.traceloop.com/) / OpenLLMetry | `traceloop` | span types, entity input/output, indexed message keys, tool functions | | [LangSmith](https://www.langchain.com/langsmith) | `langsmith` | run types, session threading, tags/metadata | | [Braintrust](https://www.braintrust.dev/) | `braintrust` *(optional)* | adds the richer `braintrust.*` tags/metadata/scores (it already reads `gen_ai.*` without it) | > **Often grouped with OpenLLMetry, but actually `gen_ai.*`-native:** Laminar, LangWatch, > Agenta and FutureAGI are sometimes listed as "Traceloop-compatible". In practice they read > `gen_ai.*` directly (Agenta and FutureAGI translate it into their own namespace), so they > need **no** compatibility mode — they're in the table above. The OpenLLMetry SDK works with > them because *it too* emits `gen_ai.*`, not because they parse the `traceloop.*` namespace. > **Advertise OTel but unconfirmed:** Galileo, PromptLayer, Keywords AI, Arthur AI, and > [LangWatch](https://langwatch.ai/) accept OTLP or claim OpenTelemetry support, but their > docs don't pin down which attributes they surface from a raw push. They may well work — > send a trace with the standard setup and check whether your spans appear. ## Setting `SEMA_OTEL_COMPAT` It's an environment variable like the others (see [How to turn it on](./observability#how-to-turn-it-on)). Its value is a comma-separated list of compatibility modes — the lower-case names from the table above: ```bash # Just Phoenix: SEMA_OTEL_COMPAT=openinference sema myagent.sema # Phoenix and Langfuse at once: SEMA_OTEL_COMPAT=openinference,langfuse sema myagent.sema # Every mode at once — useful if you're not sure which backend you'll use: SEMA_OTEL_COMPAT=all sema myagent.sema ``` Accepted modes: `openinference` (also `phoenix`, `arize`), `traceloop` (also `openllmetry`), `langsmith`, `langfuse`, `braintrust`, and `all`. Names you don't recognise are ignored, so a typo won't break anything. Some of the added detail — message text, tool arguments and results, and the trace-level input/output summary — is **content**, so it only appears when you also turn on content capture with `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true` (see [Privacy](./observability#privacy)). Token counts, models, cost, and span types are always added. When `SEMA_OTEL_COMPAT` is unset, no extra attributes are written — the traces are exactly what you get on the [Tracing & Metrics](./observability) page. ## Per-tool setup ### Arize Phoenix (OpenInference) Phoenix is an open-source LLM trace viewer that runs in one container: ```bash # Start Phoenix. UI on 6006; it accepts traces on 6006 (HTTP) and 4317 (gRPC). docker run -d --name phoenix -p 6006:6006 -p 4317:4317 arizephoenix/phoenix:latest SEMA_OTEL_COMPAT=openinference \ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:6006 \ OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true \ sema -e '(llm/complete "say hi" {:max-tokens 16})' ``` Open `http://localhost:6006`. Each Sema span is typed (`LLM` / `TOOL` / `AGENT` / `EMBEDDING`) and shows the model, provider, token counts, cost, the message I/O, and — for agent runs — tool arguments, results, and the tool schemas offered to the model. ### Langfuse Langfuse already reads several of Sema's standard attributes (cost and message I/O). The `langfuse` value fills in the rest — the observation type and model, the usage/cost detail objects, and the trace-level input/output summary: ```bash SEMA_OTEL_COMPAT=langfuse \ OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:3000/api/public/otel" \ OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic " \ OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true \ sema myagent.sema ``` (See the [Langfuse example](./observability#sending-to-hosted-langfuse) for how to build the auth header.) Multi-turn runs group into [Sessions](./observability#sessions-and-users-grouping-multi-turn-runs) via the `:session-id` and `:user-id` options. ### Traceloop (OpenLLMetry) Traceloop is mainly a hosted product, but it reads plain OTLP, so you can also view the output in any OTLP backend (such as SigNoz). `SEMA_OTEL_COMPAT=traceloop` adds the `traceloop.span.kind` and `traceloop.entity.*` attributes, the indexed per-message keys, and the advertised tool functions. (You only need this for Traceloop's own platform — the look-alikes Laminar, LangWatch and Agenta read `gen_ai.*` directly.) ### LangSmith Point Sema at LangSmith's OTLP endpoint with your API key and `SEMA_OTEL_COMPAT=langsmith`; this adds LangSmith's run types, session threading, and tags/metadata, which are needed for those features (`gen_ai.*` alone can't populate them). LangSmith is primarily hosted, but Enterprise self-hosted deployments expose their own OTLP endpoint too. ### Braintrust Braintrust reads the standard attributes, so it works with no value set. Add `braintrust` only if you want its native `braintrust.tags` and `braintrust.metadata` fields. ## Span-type mapping How each Sema span is labelled for each tool when its compat value is on: | Sema span | OpenInference | Traceloop | LangSmith | Langfuse | | --- | --- | --- | --- | --- | | `chat` | `LLM` | `task` | `llm` | `generation` | | `embeddings` | `EMBEDDING` | `task` | `embedding` | `generation` | | `execute_tool` | `TOOL` | `tool` | `tool` | `span` | | `invoke_agent` | `AGENT` | `agent` | `chain` | `span` | | `retrieve` (vector search) | `RETRIEVER` | `workflow` | `retriever` | `span` | | `rerank` | `RERANKER` | `workflow` | `chain` | `span` | | notebook cell / retry | `CHAIN` | `workflow` | `chain` | `span` | ## Tags, metadata & streaming TTFT With a compat mode on, three more things are filled in automatically — all behind the same `SEMA_OTEL_COMPAT` switch, so a plain OTel backend stays lean. **Auto-tags.** Every LLM span is tagged with its `operation:…`, `provider:…`, and `model:…`, plus `cache-hit` on a cache-served response. These land on `langfuse.trace.tags`, `braintrust.tags`, and `langsmith.span.tags`. **Your own tags & metadata.** Pass `:tags` (a list) and `:metadata` (a map) to `llm/complete`, `llm/chat`, `llm/stream`, or `agent/run`. Your tags are merged with the auto-tags; metadata fans out to each backend's native field (`langfuse.trace.metadata.*`, `langsmith.metadata.*`, `traceloop.association.properties.*`, `braintrust.metadata`). ```sema (llm/complete "Summarize this." {:max-tokens 100 :tags ["prod" "summarizer"] :metadata {:env "prod" :feature "digest"}}) ``` **Streaming time-to-first-token.** A streamed call records how long the first token took. It's always on the span as `sema.gen_ai.server.time_to_first_token` (seconds) + `sema.gen_ai.is_streaming`, and with compat on it also fills Langfuse's `completion_start_time` (which drives its TTFT column). Traceloop/OpenLLMetry gets the boolean `gen_ai.is_streaming` (OpenLLMetry has no per-span TTFT attribute — it tracks streaming latency as a histogram metric instead). Two more identity fields ride along when their backend is active: LangSmith's `langsmith.trace.session_id` (it ignores the standard `session.id`), and Langfuse's `langfuse.release` from `SEMA_OTEL_RELEASE`. **Per-direction cost split (OpenInference).** Alongside the combined `llm.cost.total`, chat spans also carry `llm.cost.prompt` and `llm.cost.completion`, so Phoenix/Arize show the prompt-vs-completion cost breakdown. Derived from Sema's in-SDK cost computation. **Embedding detail (OpenInference).** Embeddings spans carry `embedding.model_name`, and — when [content capture](#limitations) is enabled — the input texts at `embedding.embeddings.{i}.embedding.text` (capped per call). Raw vectors are never emitted. ## Tools you can't send traces to Some LLM tools collect data a different way — through their own client SDK, by sitting in front of your API calls as a proxy, or by running offline evaluations — rather than by receiving OpenTelemetry traces. Sema's OTLP export can't feed those; to use one, follow its own integration guide instead. The main categories: * **Proxies / gateways** — capture by routing your model calls through them, not by accepting traces: [Helicone](https://www.helicone.ai/), [LiteLLM](https://litellm.ai/), [Pezzo](https://pezzo.ai/). (Portkey is *not* here — its observability endpoint accepts OTLP and reads `gen_ai.*`; see the table above.) * **SDK-only platforms** — ingest only through their own Python/JS library, with no OTLP trace endpoint: [Vellum](https://www.vellum.ai/), [Athina AI](https://athina.ai/), [Parea AI](https://www.parea.ai/), [Nebuly](https://www.nebuly.com/). * **Evaluation-only** — offline scoring/testing, not a runtime trace receiver: [RAGAS](https://docs.ragas.io/), [UpTrain](https://uptrain.ai/), [Evidently AI](https://www.evidentlyai.com/), [Giskard](https://www.giskard.ai/), [TruLens](https://www.trulens.org/). * **Guardrails libraries** that *emit* telemetry rather than receive it: [NVIDIA NeMo Guardrails](https://github.com/NVIDIA/NeMo-Guardrails), [Guardrails AI](https://www.guardrailsai.com/). * **Has an OTLP endpoint, but needs attributes Sema doesn't emit** — [Fiddler AI](https://www.fiddler.ai/) accepts OTLP/HTTP, but requires its own `fiddler.span.type` and `application.id` on every span; without them spans are dropped, and Sema has no Fiddler compatibility mode to add them. > Several tools that *used* to be SDK-only or eval-only now run an OTLP endpoint — Opik, > Lunary, PostHog, Maxim, Promptfoo, Patronus and Confident AI are all in the supported > tables above. [Humanloop](https://humanloop.com/) is gone the other way: its team joined > Anthropic and the platform was sunset in September 2025, so it's no longer an integration > target. If a tool below later adds an OTLP endpoint that reads the GenAI conventions, Sema > works with it the same as the others — no change needed on Sema's side. ## Limitations * **Message content requires the opt-in flag.** The message I/O, tool arguments and results, and the trace-level input/output only appear when `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true`. Token counts, models, cost, and span types are always added. * **OpenInference has no separate tool-result field** — the result appears in the tool span's `output.value` rather than a dedicated attribute. * **A backend may re-derive cost** from the token counts on its side rather than reading Sema's `gen_ai.usage.cost`, so the figure it shows can differ from Sema's exact per-call cost (which accounts for cache pricing). * **Proxies and gateways can't receive traces.** Helicone, LiteLLM and Pezzo capture data by routing your model calls through them, not by accepting an OTLP push — use their own integration instead. * **Not yet implemented:** the per-message *indexed* attribute form some older Traceloop/LangSmith parsers expect (Sema emits the structured and entity forms today). * **More attributes per span.** Compat adds extra copies of each value. If you only use a plain OTel backend, leave `SEMA_OTEL_COMPAT` unset to keep spans lean. --- --- url: 'https://sema-lang.com/docs/llm/workflows.md' --- # Workflows Sema's workflow runtime lets you define multi-phase agentic workflows as ordinary Sema code. Every phase, step call, checkpoint, and budget charge is journaled to a frozen JSONL run directory. Crash, edit, and resume — the runtime skips leaves that already completed and only re-runs what changed. ## Quick start ```bash # Run a workflow file sema workflow run my-workflow.sema --args '{"topic":"rust"}' --view # Resume after a crash or edit sema workflow run my-workflow.sema --resume wf_1719494_12345 # Statically validate without calling any LLM sema workflow check my-workflow.sema # Open the web viewer for past runs sema workflow view --run-dir .sema/runs ``` ## The DSL ### `defworkflow` A prelude macro that expands to `workflow/run`. The body is a thunk (an implicit `lambda`); the form **is** the run — when Sema evaluates it, the runtime opens a journal, emits events, and returns a `{:status …}` envelope. ```sema (defworkflow name "doc string" meta-map body-form-1 body-form-2 ...) ``` The `meta-map` supports: | Key | Type | Description | |-----|------|-------------| | `:phases` | `[:string …]` | Declared phase plan — the dashboard shows all phases up front | | `:budget` | `{:tokens N :usd M}` | Spend caps (see [Budget Enforcement](#budget-enforcement)) | | `:permissions` | string | Sandbox restrictions for `sema workflow run`, using the same syntax as `--sandbox` | | `:policy` | policy | Model, tool, subject, and content guards for this workflow (see [Model and tool policies](#model-and-tool-policies)) | | `:args` | map | Argument schema (informational; the actual args come from `--args`) | Any other key in the meta map is recorded in `metadata.json` and is available to a policy `:metadata :require` rule (see [Run-wide evidence](#run-wide-evidence)). The body is ordinary Sema code. `phase` markers interleave with `def`, `step`, `checkpoint`, `parallel`, `pipeline`, and any other Sema forms. The last value is the return envelope — if it's already a `{:status …}` map it passes through; otherwise the runtime wraps it as `{:status :success :value …}`. ### `phase` A **marker**, not a wrapper. `(phase "Audit")` closes the previously-open phase and opens "Audit". Every `step`, `checkpoint`, and `budget` event that follows attributes to it until the next `(phase …)` or the run end (which closes the last open phase). Returns `nil`. ```sema (phase "Inventory") ;; forms here belong to "Inventory" (checkpoint :files (list "a.php" "b.php")) (phase "Audit") ;; forms here belong to "Audit" (define findings (step "Audit each file" {:name "auditor"})) ``` ::: tip `phase` takes exactly one argument — the label. It is NOT a wrapper like `let` or `when`. A common mistake is `(phase "Audit" (do-stuff))` — the correct form is `(phase "Audit")` followed by the body forms. ::: ### `step` A journaled LLM leaf — the workflow's atomic orchestration unit. The `step` macro wraps `workflow/step` and handles prompt resolution, schema validation, tool dispatch, and `:agent` routing. ```sema ;; Without schema — returns the completion text (step "Summarize the changelog.") ;; With schema — returns typed data (validated via llm/extract) (step "List auth-relevant files." {:name "scout" :schema [:list :string]}) ;; With tools — runs the real tool loop (llm/chat) (step "Find TODOs in src/" {:name "coder" :tools [read-file run-command]}) ;; With :agent — runs a configured defagent as this step (step "Review this file" {:agent code-reviewer :schema verdict}) ``` The opts map supports: | Key | Type | Description | |-----|------|-------------| | `:name` | `:string` | Role label shown in the dashboard (default `"step"`) | | `:schema` | schema spec | Typed extraction — the step returns a validated map, not text | | `:tools` | `[tool …]` | Tool-calling loop — the step runs `llm/chat` with tool dispatch | | `:agent` | `defagent` | Run a configured `defagent` as this step via `agent/run` | | `:policy` | policy | Additional restrictions for this step; it cannot loosen the workflow policy | When `:agent` is present, the defagent owns its own tools and model — inline `:tools`/`:model` are ignored (the static checker warns if both are given). The runtime emits `agent.started` before the leaf and `agent.result` after, plus a per-step `budget` event with token/cost attribution. (The `agent.*` event names are the frozen journal contract — they predate the `step` rename.) ### `checkpoint` Records a keyed step value and returns it. With one argument, reads the previously-stored value back. ```sema ;; Write: store the files list under :files, return it (checkpoint :files (list "a.php" "b.php")) ;; Read: get the value back (nil if never set) (let ((files (checkpoint :files))) (count files)) ``` Checkpoints double as the run-scoped state bag — values stored in one phase are readable in a later phase. Each checkpoint emits a `checkpoint` event with a `content_key`, an opaque value digest, and a capped display value; the memo sidecar stores the canonical value for resume. ### `approval` Stops before a sensitive action until a host records an approve or reject decision. The subject is used only to bind the decision; the request sidecar stores its SHA-256 digest, not the raw value. Put only operator-safe text in `:preview`. ```sema (approval :release-signoff {:reason "Publish the release" :subject {:kind :external-action :target "pkg.sema-lang.com" :digest package-digest} :preview "Publish sema-policies@1.0.0"}) ;; This does not run until the request is approved. (publish-package) ``` The default `auto` mode prompts when stdin and stderr are terminals, `CI` is unset, and no durable approval authority was supplied. Passing an approval public or signing key selects durable `pause` behavior instead. The prompt accepts approve, reject with a reason, or quit-pending. Ctrl-C exits with code 130 and leaves the durable request pending; it never turns an interrupted prompt into a rejection. Terminal prompts use an ephemeral Ed25519 authority kept in host memory. The CLI records the signed decision, creates a fresh interpreter, and resumes the same run immediately. If you quit an ephemeral prompt without deciding, start a fresh run: its private authority intentionally cannot be recovered from the workflow or its sidecars. For CI, automation, or a decision made from another terminal, create a durable authority and pass only its public key to the workflow process: ```bash # Create the authority once. The private file is created with mode 0600. mkdir -p .sema sema workflow approval-keygen \ --private-key-file .sema/approval.private \ --public-key-file .sema/approval.public # Pause at a gate and return exit code 3. Keep the exact file, args, run dir, # and public-key file for the later resume command. sema workflow run release.sema \ --args '{"package":"sema-policies","version":"1.0.0"}' \ --approval-mode pause \ --approval-public-key-file .sema/approval.public # The command prints the concrete run id and approval id. You can also list # pending requests as text or JSON. sema workflow approvals wf_… sema workflow approvals wf_… --json # Approve from a separate trusted process. The private key is never passed to # `workflow run` and is never visible to Sema code. sema workflow approve wf_… apr_… \ --signing-key-file .sema/approval.private \ --actor release-manager \ --comment 'verified package and version' # Or reject; a reason is required. sema workflow reject wf_… apr_… \ --signing-key-file .sema/approval.private \ --actor release-manager \ --reason 'release checks failed' # Apply the recorded decision. Reuse the exact original inputs. sema workflow run release.sema \ --args '{"package":"sema-policies","version":"1.0.0"}' \ --resume wf_… \ --approval-mode pause \ --approval-public-key-file .sema/approval.public ``` The loopback workflow viewer can record the same signed decision. Give the private key only to the host process that serves the viewer: ```bash # Inspect existing runs and enable controls for requests from this authority. sema workflow view \ --run-dir .sema/runs \ --approval-signing-key-file .sema/approval.private \ --approval-actor release-manager # Or start the viewer with the workflow. A signing key implies a durable pause # when the default mode is auto. sema workflow run release.sema \ --args '{"package":"sema-policies","version":"1.0.0"}' \ --view \ --approval-signing-key-file .sema/approval.private \ --approval-actor release-manager ``` The private key stays in the host viewer state. It is not placed in the Sema environment, returned by the API, or written to the run directory. The viewer shows validated request summaries. A pending request is inspect-only when the viewer has no signing key or its key does not match the request authority. After approve or reject, resume the same run with the exact original file, arguments, run directory, and authority. The viewer displays the durable winner when another process wins the compare-and-set race. | Mode | Behavior | |------|----------| | `auto` | Prompt on a real terminal outside CI when no durable authority is configured; otherwise use `pause` behavior | | `prompt` | Require terminal stdin/stderr and ask approve, reject, or leave pending | | `pause` | Publish a request and exit 3; a durable gate requires `--approval-public-key-file` | | `deny` | Refuse the gate without recording an approval decision and exit 1 | An explicit interactive `prompt` cannot use a public-key file by itself because the terminal process cannot sign the decision. Omit the public key to use an ephemeral terminal authority, or run with `--view` and the matching `--approval-signing-key-file`. The request and decision JSON files under the run's `approvals/` directory are the protocol authority; journal events are audit evidence. Decisions use Ed25519 signatures and compare-and-set publication, so the first approve or reject wins and a conflicting decision cannot overwrite it. A decision binds the run, complete static import/package dependency closure, arguments, phase, gate key and occurrence, canonical subject digest, request timestamp, request revision, and public authority. Editing a binding invalidates the decision. The evaluator reads imports and loads from those exact snapshotted bytes; runtime-selected or macro-generated files outside the preflight closure fail closed instead of escaping the approval revision. Files introduced by macro expansion at eval time (not parse time) are not traced during preflight and will fail at runtime with a missing-dependency error. Approval subjects must be canonical immutable data: scalars, lists, vectors, maps, bytevectors, or typed numeric arrays. Mutable cells, records, functions, promises, channels, and other runtime objects are rejected instead of being hashed through an ambiguous display string. The raw subject is never stored; only its digest is. Treat `:preview`, `:reason`, comments, and actor names as operator-visible text. An approval is a sequential gate in the owning workflow task. Call `approval` directly; `workflow/approval` cannot be aliased, stored, or passed as a first-class value. Put the gate before, not inside, `parallel`, `pipeline`, async task combinators, steps, retry/timeout forms, resource-cleanup forms, or a nested `workflow/run`. `sema workflow check` and `sema workflow run` reject those placements before execution. Pending, rejected, malformed, cancelled, and authority-invalid gates are uncatchable by Sema `try`/`catch`, so later protected forms cannot run. A workflow-level completion policy can require proof that an approved gate was crossed before the run reports success: ```sema (defpolicy human-reviewed {:completion {:require-events [:approval.applied]}}) ``` This is run-wide evidence. It proves that an approval was applied during the successful invocation, but it does not associate that approval with an arbitrary later tool call. Keep the gate immediately before the protected action and bind its `:subject` to that action's stable identity. Durable approval storage and approval key generation support Unix permission modes and protected Windows ACLs. Other targets fail closed if Sema cannot enforce private approval files. ### `parallel` Runs a list of zero-arg thunks concurrently with bounded concurrency (default 8\). A **barrier** — waits for all thunks before returning. Results come back in input order. A thunk that throws yields `nil` in its slot (the batch never aborts). ```sema ;; Fetch two URLs concurrently (parallel (list (fn () (http/get url-a)) (fn () (http/get url-b)))) ;; Override the concurrency cap (parallel thunks 4) ``` ### `pipeline` Each item flows through all stage functions independently — **no barrier between stages**. Item A can be in stage 3 while item B is still in stage 1. A stage that throws drops that item to `nil` and skips its remaining stages. Results align to `items` (nils for dropped). ```sema ;; Each file → audit → verify (pipeline files (fn (f) (step (str "Audit " f) {:name "auditor"})) (fn (x) (step (str "Verify " (:claim x)) {:name "verifier"}))) ``` ## The run directory Every `sema workflow run` creates a run directory under `.sema/runs//`: ``` .sema/runs/wf_1719494_12345/ events.jsonl # the system of record (append-only) events.resume-1.jsonl # one per --resume continuation memo/ # per-leaf resume cache 3f13d37d3df7b337_0.json # content-key → memoized value 7b03b1d77c616601_0.json approvals/ # authoritative human approval protocol apr_….request.json apr_….decision.json metadata.json # workflow name, code version, budget, permissions result.json # the final {:status …} envelope ``` ### Event vocabulary Existing event shapes are **frozen** — add fields only as append-only, optional/skippable fields, and add new event kinds without changing old ones. Old runs stay readable forever. | Event | Key fields | Description | |-------|-----------|-------------| | `run.started` | `workflow`, `run_id`, `code_version`, `args_json`, `phases` | First line of every run | | `phase.started` | `phase` | A phase opened | | `phase.ended` | `phase`, `status`, `dur_ms` | A phase closed (paired with `phase.started`) | | `agent.started` | `agent_id`, `agent_name`, `model` | An agent leaf began | | `agent.result` | `agent_id`, `status`, `output`, `dur_ms`, `model` | An agent leaf produced a result | | `agent.tool_call` | `agent_id`, `tool_name`, `args_json` | An agent invoked a tool | | `agent.tool_result` | `agent_id`, `tool_name` | An agent tool call completed successfully | | `policy.checked` | `policy`, `boundary`, `subject`, `rule`, `source` | A policy layer allowed a protected boundary | | `policy.violation` | `policy`, `boundary`, `subject`, `rule`, `action`, `source` | A policy layer denied a protected boundary | | `policy.flagged` | `policy`, `boundary`, `subject`, `rule`, `label`, `count`, `action`, `source` | A content detector matched under the `:audit` action; the value was not changed | | `policy.redacted` | `policy`, `boundary`, `subject`, `rule`, `label`, `count`, `source` | A content detector matched under the `:redact` action; the matched spans were replaced | | `policy.bypassed` | `policy`, `boundary`, `subject`, `reason`, `source` | A lexical `policy/without` scope bypassed a protected boundary | | `approval.requested` | `approval_id`, `request_digest`, `key`, `reason`, `subject_digest` | A durable request stopped the run | | `approval.granted` | `approval_id`, `decision_id`, `actor`, `provenance` | An approved decision was observed on resume | | `approval.rejected` | `approval_id`, `decision_id`, `actor`, `reason` | A rejected decision was observed on resume | | `approval.applied` | `approval_id`, `decision_id` | Execution crossed an approved gate | | `checkpoint` | `key`, `content_key`, `value_digest`, `value` | A checkpoint was recorded | | `budget` | `agent_id`, `input_tokens`, `output_tokens`, `cost_usd`, `budget_limit` | A per-leaf budget observation | | `run.ended` | `status`, `reason`, `dur_ms` | Last line of every run | Each event carries a monotonic `seq` (0-based) and a `ts` (RFC3339 UTC instant). The journal is flushed per event, so a crash mid-run leaves a valid JSONL prefix. ### Evidence export Export a run into a machine-readable ledger, Markdown summary, and integrity manifest: ```bash sema workflow export ``` The exporter reads approval requests and decisions through the same digest and Ed25519 signature validation used by the CLI and viewer. `evidence.json` and `evidence.md` include approval summaries. `manifest.json` includes SHA-256 entries for the authoritative request and decision sidecars as well as the run journals, metadata, result, and generated evidence files. Export fails if a listed approval request or decision is invalid. ## Resume `--resume ` reuses the run directory and short-circuits any leaf whose content-key is in the prior run's `memo/` dir. The model is **not called** for memoized leaves — they replay for free. ### How content keys work Each step leaf's content key is a hash of `(kind, code-version, args, phase, step-name, prompt, schema, effective-policy)`. Checkpoints use `(kind, code-version, args, phase, key)`. Same inputs → same key → memo hit → no re-call. An occurrence ordinal distinguishes identical repeats in source order. Tightening or otherwise changing the effective step policy invalidates that step's memo. ### Automatic invalidation Edit the workflow or change `--args` → content keys change → no memo hits → full re-run. No guard files to maintain; the invalidation is automatic. ### Per-leaf granularity Delete one memo file → that leaf re-runs while others still replay. A missing memo always re-runs conservatively (never resumes wrong). ### Resume segment A `--resume` run writes a fresh `events.resume-N.jsonl` segment (not appended to `events.jsonl`) so each file keeps the frozen invariants (first line is `run.started`, `seq` monotonic from 0). The viewer merges segments. ### Resume doesn't double-charge A `--resume` run starts spend at zero. Memoized leaves don't re-call the model and don't recharge the budget. Only leaves that actually run count against the cap. ## Budget enforcement Declare `:budget {:tokens N :usd M}` in the `defworkflow` metadata. The runtime charges each step leaf and latches a sticky `over_budget` flag when a cap is exceeded — further step leaves are **refused** and the run ends `{:status :failed :reason "budget exceeded"}`. ```sema (defworkflow audit "Audit with a 5000-token cap." {:phases ["Scan" "Report"] :budget {:tokens 5000}} (phase "Scan") (def a (step "Find files." {})) ;; a burns 5200 tokens → cap trips after its Budget event (phase "Report") (def b (step "Summarize." {})) ;; b refused: over_budget latch is sticky {:status :success :a a :b b}) ;; → {:status :failed :reason "budget exceeded"} ``` * **Token caps are deterministic.** `:tokens N` counts actual usage tokens. * **USD caps are best-effort.** `:usd M` depends on the pricing table being available for the model. * **Per-leaf attribution.** Each `budget` event records the `agent_id`, token counts, and cost — the dashboard shows per-leaf spend. * **Sticky latch.** Once tripped, the latch stays set for the rest of the run. No step leaf launches after it, even under concurrent `parallel` fan-out. ## Permission enforcement Declare `:permissions` in the `defworkflow` metadata to tighten the sandbox for `sema workflow run`. The value uses the same syntax as the CLI `--sandbox` flag: `"strict"`, `"all"`, `"none"`, or comma-separated capabilities such as `"no-fs-write,no-network"`. Capability names may be written with or without the `no-` prefix (`"fs-write"` and `"no-fs-write"` are equivalent), but workflow docs use the `no-*` form because it reads as a denial list. | Value | Denies | |-------|--------| | `none` | Nothing; useful only when you want the metadata to say there is no workflow-specific tightening | | `strict` | `shell`, `fs-write`, `network`, `env-write`, `process`, `llm`, `serial` | | `all` | Every capability listed below | | `no-fs-read` | File, directory, import, PDF, stream-input, `http/file`, and read-side DB access | | `no-fs-write` | File writes/deletes/renames, output streams, KV writes, and write-side DB access | | `no-shell` | Calls to `shell` | | `no-network` | HTTP client/server operations | | `no-env-read` | Environment and host information reads | | `no-env-write` | Environment variable writes | | `no-process` | Process operations such as `exit`, `sys/args`, `sys/which`, and `shell` | | `no-llm` | LLM calls | | `no-serial` | Serial port operations | ```sema (defworkflow readonly-audit "Audit without writing files or using the network." {:phases ["Audit"] :permissions "no-fs-write,no-network"} (phase "Audit") (def files (file/list "src")) {:status :success :files files}) ``` Workflow permissions can only remove capabilities from the caller's sandbox; they cannot loosen a stricter `--sandbox` or `--allowed-paths` setting. ## Model and tool policies A policy is a compiled, immutable map that guards a workflow at its boundaries. Define one with `defpolicy`, then attach it to a workflow or a step with `:policy`. A policy map accepts seven sections, and every section is optional: | Section | Guards | |---------|--------| | `:models` | Which `provider/model` a call may resolve to | | `:tools` | Which tool names a model may call, and the values of their named arguments | | `:subjects` | The file, network, command, or external action a tool actually performs (see [Semantic subjects](#semantic-subjects)) | | `:input` | Text sent to a model (see [Content guards](#content-guards)) | | `:output` | Text returned by a model (see [Content guards](#content-guards)) | | `:metadata` | Workflow metadata keys the run must declare (see [Run-wide evidence](#run-wide-evidence)) | | `:completion` | Journal events the run must produce before it reports success (see [Run-wide evidence](#run-wide-evidence)) | Any other key is rejected when the policy compiles, with the closest valid key as a hint. ```sema (defpolicy safe-agent {:models {:default :deny :allow ["openai/gpt-5" "anthropic/*"] :deny ["anthropic/deprecated-model"] :on-deny :fail} :tools {:default :deny :allow {"read-file" {:paths ["src/**" "Cargo.toml"]} "fetch-url" {:domains {:allow ["api.example.com" "*.example.com"] :schemes ["https"] :ports [443]}} "run-command" {:commands ["cargo test" "cargo check"]}} :deny ["delete-file"] :on-deny :tool-error}}) (defworkflow guarded-audit "Audit with a least-privilege model and tool envelope." {:phases ["Audit"] :permissions "no-fs-write" :policy safe-agent} (phase "Audit") (def result (step "Inspect the Rust sources." {:name "auditor" :tools [read-file fetch-url run-command]})) {:status :success :result result}) ``` Model rules use an exact `provider/model` identity. The only wildcard form is `provider/*`; provider wildcards and partial model globs are rejected. Deny rules win over allow rules. When `:models` or `:tools` is present, `:default` defaults to `:deny`. Tool allow entries may be unconstrained (`{}`) or constrain named JSON arguments: | Constraint | Shorthand argument | Match | |------------|--------------------|-------| | `:paths` | `"path"` | Workspace-relative literal, `*`, and `**` patterns; absolute paths and root/symlink escapes are denied | | `:domains` | `"url"` | Parsed HTTP(S) URLs matched by normalized hostname, scheme, and optional effective port | | `:commands` | `"command"` | Exact command strings only; no wildcard or shell-prefix matching | A leading `*.` matches subdomains only, so list both `"example.com"` and `"*.example.com"` when both the apex and its subdomains are allowed. URLs containing credentials are always denied. `:domains` defaults `:schemes` to `["https"]`; add `:schemes ["http" "https"]` to accept plain HTTP. The only accepted schemes are `"http"` and `"https"`. Use explicit selectors when a tool uses different argument names or has multiple path-like arguments: ```sema {:tools {:allow {"copy-file" {:paths [{:arg :source :allow ["src/**"]} {:arg :destination :allow ["generated/**"] :deny ["generated/private/**"]}]}}}} ``` ### Semantic subjects A `:tools` rule matches a tool by name and inspects the arguments that rule names. A `:subjects` rule matches what the tool actually does, so one rule covers every tool that performs that action. The tool declares its own subjects; the policy matches them. Declare subjects in the optional options map of `deftool`, between the parameter schema and the handler: ```sema (deftool read-source "Read a source file." {:path {:type :string}} {:policy-subjects [{:kind :file-read :path-arg :path}]} (fn (path) (file/read path))) (tool/policy-subjects read-source) ; => [{:kind :file-read :path-arg :path}] ``` `:policy-subjects` is the only option `deftool` accepts. A tool without an options map declares no subjects. | `:kind` | Required keys | Optional keys | |---------|---------------|---------------| | `:file-read` | `:path-arg` | — | | `:file-write` | `:path-arg` | — | | `:file-delete` | `:path-arg` | — | | `:network-request` | `:url-arg` | `:method` | | `:command` | `:command-arg` | — | | `:external-action` | `:action` | `:target-arg` | `:path-arg`, `:url-arg`, `:command-arg`, and `:target-arg` name a parameter of that tool. The runtime reads the model-supplied value of that argument and matches it against the policy. A `:subjects` section holds `:default`, `:allow`, and `:deny` lists of rules. Each rule needs a `:kind` and may add one constraint: ```sema (defpolicy sandboxed-actions {:subjects {:default :deny :allow [{:kind :file-read :paths ["src/**" "tests/**"]} {:kind :file-write :paths ["generated/**"]} {:kind :network-request :domains {:allow ["docs.rs" "*.docs.rs"]} :methods [:get]} {:kind :command :commands ["cargo test"]} {:kind :external-action :actions ["publish-draft"]}] :deny [{:kind :file-write :paths ["src/secrets/**"]}]}}) ``` * `:paths`, `:domains`, and `:commands` take the same selector shapes as the `:tools` constraints above, without the `:arg` key — the argument is already named by the tool's subject declaration. * `:methods` is valid only on `:network-request`. An empty or absent list matches any method. * `:actions` is valid only on `:external-action`. An empty or absent list matches any action. * `:default` defaults to `:deny`, like `:models` and `:tools`. * Under `:default :deny`, a tool that declares no subjects is denied with the rule `subjects.missing`. Declare subjects on every tool such a policy allows. Deny rules fail closed. A deny rule denies any value it cannot evaluate: a non-string argument, an unparsable URL, a URL whose scheme or port is outside the rule's selector, a path that escapes the workspace, or an absent request method. `:domains` defaults `:schemes` to `["https"]`, so a `:network-request` deny rule denies every `http://` URL, not only the hosts in its `:allow` list. Allow rules fail closed the same way: a value the rule cannot evaluate is never allowlisted. A value that *was* compared and is simply not covered stays a non-match for both, so a deny rule naming one host does not deny every other host. ### Content guards `:input` scans every system prompt, message, embedding input, and rerank document before the provider call. `:output` scans the assistant response, including a response replayed from the cache or a cassette. Both take a `:detect` list and an optional `:actions` map: ```sema (defpolicy no-pii {:input {:detect [:secret :email :phone :ipv4 :payment-card] :actions {:email :redact :phone :redact}} :output {:detect [:secret] :max-length 4000 :require [:summary] :schema {:summary :string :confidence {:type :number :optional true}} :forbid [{:id "no-placeholder" :contains "TODO"} {:id "no-lorem" :regex "(?i)lorem ipsum"}] :action :block}}) ``` The detectors are `:secret`, `:email`, `:phone`, `:ipv4`, and `:payment-card`. Each detector's action defaults to `:block`; `:actions` overrides it with `:audit`, `:redact`, or `:block`. An `:actions` key that is not listed in `:detect` is a compile error, and `:allow` is not a valid detector action. | Action | Effect | |--------|--------| | `:audit` | The value passes through unchanged and a `policy.flagged` event records the rule, label, and match count | | `:redact` | Each matched span is replaced with `«redacted: