Eta — Language Guide

Note

This guide is the canonical entry point for learning the Eta language. Each section is intentionally short and links to a deep-dive page — either a tutorial chapter under docs/guide/ or a module / tool reference under docs/guide/reference/.

Note

Looking for the library APIs? See the Standard Library Guide — a per-module reference covering core, collections, math, aad, torch, causal, clp, logic, net, process, and the rest of the std.* packages.


1. Orientation

Eta is a Lisp/Scheme-like language with a hygienic macro system and a stack-based bytecode VM that uses NaN-boxing for value representation. The same VM hosts a wide range of capabilities — the symbolic core, logic programming, constraint logic programming (CLP), automatic adjoint differentiation, statistics, neural networks, causal inference, and actor-style concurrency — delivered as first-class language features or as packaged std.* modules.

Toolchain

ToolRoleReference
etaiInterpreter for .eta source or .etac bytecodeQuick Start
etacAhead-of-time bytecode compilerCompiler, Bytecode Tools
eta_replInteractive REPLREPL
eta_lspLanguage Server (diagnostics, completion, navigation)VS Code, Debugging
eta_dapDebug Adapter (breakpoints, stepping, inspection)Debugging
eta_testTest runner with TAP / JUnit outputTesting
eta_jupyterJupyter kernelJupyter
eta profRuntime profiling (run, report, merge, view)Profiling

Hello, world

(module hello
  (import std.io)
  (begin
    (println "Hello, world!")))
etai hello.eta

Tip

The interactive notebook at cookbook/notebooks/LanguageBasics.ipynb walks through the same material as §§ 2–9 in a runnable form.

How to read this guide

Each chapter is short, with a small example and a Deep dive link. Read the chapters in order for a tour, or jump straight to the deep dive of the topic you need.


2. Syntax & Values

Eta source is S-expressions: lists ( … ), vectors #( … ), strings "…", characters #\a, booleans #t / #f, symbols, fixnums and floats. Comments are ; to end of line, or #| … |# for block comments. Quoting uses ', quasi-quotation `, unquote ,, and splicing ,@.

'(1 2 3)                ; list literal
#(1 2 3)                ; vector literal
`(a ,(+ 1 2) ,@'(c d))  ; => (a 3 c d)

Equality has three flavours: eq? (identity), eqv? (numeric/char equivalence), equal? (structural). The numeric tower is fixnum + double, with NaN-box tagging — see nanboxing.md.

Because code is just S-expression data, eval compiles and executes any expression at runtime against the current lexical environment. This is fundamental: it is how the symbolic engine in cookbook/xva-wwr lowers a differentiated expression onto the active AAD tape, how the REPL evaluates user input, and how macros and quasi-quotation compose with runtime metaprogramming.

(eval '(+ 1 2))                          ; => 3
(let ((x 10)) (eval '(+ x 5)))           ; => 15
(define f (eval '(lambda (a b) (* a b)))) ; build a closure from data
(f 3 4)                                  ; => 12

Deep dives: syntax-and-values.md, eval.


3. Bindings & Scope

Eta is lexically scoped. The binding forms are define, defun (a define + lambda shorthand), let, let*, letrec, named-let, and set!. Internal defines inside a lambda/let are mutually recursive.

(define greeting "hello")

(let* ((a 1)
       (b (+ a 1))
       (c (+ a b)))
  c)                          ; => 3

(let loop ((n 10) (acc 1))    ; named let
  (if (= n 0) acc (loop (- n 1) (* acc n))))

Note

The REPL allows shadowing (re-define of an existing name); modules do not. See repl.md for the REPL-specific rules.

Deep dive: bindings-and-scope.md.


4. Control Flow

FormPurpose
ifTwo-branch conditional
condMulti-branch with optional else
when / unlessOne-armed conditional with implicit begin
caseDispatch on eqv? of a key
and / orShort-circuit, return the deciding value
beginSequence expressions, return the last

Loops are recursive: use named let for tight loops and letrec for mutual recursion. All such forms are tail-call optimised — see §5.

(cond
  ((null? xs)        'empty)
  ((= (length xs) 1) 'singleton)
  (else              'many))

Deep dive: control-flow.md.


5. Functions, Closures & Tail Calls

lambda constructs a closure; defun is shorthand for (define name (lambda …)). Parameter lists support fixed, optional and dotted-rest arguments, and apply calls a function with a list of arguments.

(defun adder (n) (lambda (x) (+ n x)))   ; closure over n
(define add5 (adder 5))
(add5 3)                                 ; => 8

(defun sum (first . rest) (foldl + first rest))
(apply sum '(1 2 3 4))                   ; => 10

Eta guarantees tail-call optimisation in tail position — including the last expression of if, cond, when, unless, case, let*, letrec, and begin. Mutual recursion via letrec runs in constant stack.

Deep dives: functions-and-closures.md, tail-calls.md.


6. Records & Compound Data

define-record-type produces a constructor, a predicate, accessors and optional setters. Records are not pairs; equal? performs structural comparison.

(define-record-type <point>
  (make-point x y)
  point?
  (x point-x)
  (y point-y set-point-y!))

(define p (make-point 3 4))
(point-x p)                ; => 3
(set-point-y! p 7)

Other compound types: pairs / lists, vectors (#( … ), fixed-length, mutable, O(1) indexed), hash maps and hash sets — see §12.


7. Pattern-Style Dispatch

Eta has no built-in match form. Idiomatic dispatch uses cond with predicate guards or, for symbolic data, structural unification from std.logic (§17).

(defun shape-area (s)
  (cond
    ((and (pair? s) (eq? (car s) 'circle))
       (let ((r (cadr s))) (* 3.14159 r r)))
    ((and (pair? s) (eq? (car s) 'rect))
       (* (cadr s) (caddr s)))
    (else (raise 'unknown-shape s))))

For destructuring on relational data, use (== pat term) with logic variables — see logic.md.


8. Macros (syntax-rules)

Macros are hygienic and pattern-based. define-syntax binds an expander; syntax-rules lists (pattern → template) cases with ellipsis (...) for variadic patterns.

(define-syntax swap!
  (syntax-rules ()
    ((_ a b)
     (let ((tmp a))
       (set! a b)
       (set! b tmp)))))

Important

Eta’s macro system is syntax-rules only — there are no procedural macros. This keeps expansion deterministic and serialisable into bytecode. See macros.md for ellipses, literal keywords, and worked examples from the standard library.

Deep dive: macros.md.


9. Modules & Imports

Every source file declares one or more (module name … ) forms with explicit (import …) and (export …) clauses. The module search path is the input file’s directory plus --path arguments and ETA_MODULE_PATH.

ClauseEffect
(import std.math)All exports of a module
(import (only std.math pi e))Only listed names
(import (except std.collections sort))All except listed names
(import (rename std.math (pi PI)))Rename on import
(import (prefix std.math math:))Namespace-style qualified access

Reference: modules.md.


10. Error Handling

raise and catch compile to the Throw and SetupCatch VM opcodes. Tags are symbols; the raised payload can be any value. A tag-less (catch body) is a catch-all that also intercepts runtime.* errors.

(defun safe-div (a b)
  (if (= b 0)
      (raise 'division-by-zero (list a b))
      (/ a b)))

(catch 'division-by-zero (safe-div 10 0))
;; => (10 0)

dynamic-wind runs its after thunk on every exit (normal or exceptional), enabling reliable cleanup.

Deep dive: error-handling.md.


11. Strings, Symbols & Regex

Strings are immutable byte sequences with the standard Scheme operations (string-append, string-length, substring, string-ref, string->list, string->symbol, …). Symbols are interned. Regular expressions live in std.regex.

(import std.regex)
(define re (regex:compile "(\\d+)-(\\d+)"))
(regex:find-all re "10-20 and 30-40")
;; => (("10-20" "10" "20") ("30-40" "30" "40"))

Deep dive: strings.md. Reference: regex.md.


12. Collections

ContainerMutabilityIndexingModule
ListimmutableO(n)builtin
VectormutableO(1)builtin
Hash mapmutableO(1) avgstd.hashmap
Hash setmutableO(1) avgstd.hashset
Fact tablecolumnarindexedstd.fact_table

std.collections provides the higher-order suite (map*, filter, foldl / foldr, reduce, zip, range, take / drop, flatten, sort, any?, every?, …).

(import std.prelude)
(foldl + 0 (map* (lambda (x) (* x x)) (filter even? (range 1 11))))
;; => 220

Deep dive: collections.md.


13. I/O, Filesystem & OS

Built-ins: display, write, newline, write-string, read-char, current-{input,output,error}-port, open-input-file, open-output-file, open-input-string, open-output-string, get-output-string. std.io adds println, eprintln, read-line, display->string, and the with-…-port redirection helpers.

(import std.io)
(with-output-to-port (open-output-string)
  (lambda () (println "captured")))

CSV via std.csv, Datalog via std.db. JSON has its own section (§14), and structured logging has its own section (§15).

Filesystem (std.fs)

std.fs wraps the native std::filesystem-backed builtins for path manipulation, directory enumeration, and file metadata. Paths are plain strings; results round-trip through the platform-preferred separator.

(import std.fs std.io)
(when (fs:directory? "examples")
  (for-each println (fs:list-directory "examples")))

(define cfg (fs:path-join (fs:temp-directory) "eta" "config.json"))
(println (fs:path-normalize cfg))
FunctionPurpose
fs:file-exists?#t iff the path resolves on disk
fs:directory?#t iff the path is a directory
fs:delete-fileRemove a regular file
fs:make-directoryCreate a directory (idempotent)
fs:list-directorySorted list of entry names (no ./..)
fs:path-joinVariadic; joins with the platform separator
fs:path-splitInverse of fs:path-join (root + components)
fs:path-normalizeLexical canonicalisation
fs:temp-fileAllocate a fresh temp-file path
fs:temp-directoryAllocate a fresh temp-directory path
fs:file-modification-timemtime in epoch milliseconds
fs:file-sizeSize in bytes

Operating system (std.os)

std.os exposes process-level concerns: environment variables, the script’s own command line, current working directory, and a clean exit.

(import std.os std.io)
(println (os:command-line-arguments))            ; e.g. ("--verbose" "in.csv")
(println (or (os:getenv "ETA_HOME") "(unset)"))
(os:change-directory! (os:current-directory))
FunctionPurpose
os:getenvLookup; #f if unset
os:setenv! / os:unsetenv!Mutate the process environment
os:environment-variablesSorted alist of ("KEY" . "value")
os:command-line-argumentsList of strings passed to etai / etac
os:current-directoryWorking directory as a string
os:change-directory!chdir equivalent
os:exitTerminate the process with an optional status code

Command-line arguments (std.args)

std.args is an argparse-style parser: declare a tuple spec, hand it argv, and get back a hash map keyed by symbol (with a 'positional entry for non-option arguments). Supports flag, string, int, float, and list value kinds; --name value, --name=value, short flags, and -- to forward the rest as positional. Optional parse / validate lambdas, choices, required?, and a count action cover the awkward cases.

(import std.args std.io)

(define spec
  '((verbose (--verbose -v) flag)
    (out     (--out -o)     string "a.out")
    (jobs    (--jobs -j)    int    1)
    (tag     (--tag)        list)))

(define r (args:parse-command-line spec))
(when (args:get r 'verbose) (println "loud mode"))
(println (args:get r 'positional))

Runnable demo: cookbook/basics/args.eta.

Subprocesses (std.process)

std.process runs and controls external OS processes. Use process:run for the common shell-out (blocking, captures stdout / stderr) and process:spawn for long-running children whose stdio you want to drive as Eta ports.

(import std.process std.io)

;; Blocking — returns (exit-code stdout stderr)
(define r (process:run "git" '("--version")))
(println (cadr r))                       ; "git version 2.45.0\n"

;; Non-blocking — drive the child by its ports
(define p (process:spawn "python" '("-u" "-c" "print(input())")))
(display "hello\n" (process:stdin-port p))
(close-port (process:stdin-port p))
(println (read-line (process:stdout-port p)))   ; "hello"
(process:wait p)                                ; => 0
FunctionPurpose
process:run program args [opts]Block; return (exit-code stdout stderr)
process:spawn program args [opts]Start a child; return a process handle
process:wait / process:kill / process:terminateLifecycle (graceful or hard stop)
process:pid / process:alive? / process:exit-codeStatus queries

Options accepted by both run and spawn: cwd, env, replace-env?, stdin, stdout, stderr, timeout-ms, binary?.

Reference: process.md.

Deep dive: io.md. References: fs.md, os.md, args.md, process.md.


14. JSON

std.json is a thin wrapper over the native JSON codec implemented in eta/core/src/eta/util/json.h (a hand-written, RFC 8259 parser and serialiser with no third-party dependency). Objects decode to hash maps, arrays to vectors, numbers default to flonums (pass 'keep-integers-exact? #t to keep integer-typed numbers as fixnums), true / false to #t / #f, and null to '().

(import std.json std.io)
(define cfg (json:read-string "{\"name\":\"eta\",\"n\":7}"
                              'keep-integers-exact? #t))
(println (hash-map-ref cfg "name"))     ; "eta"
(println (hash-map-ref cfg "n"))        ; 7

(println (json:write-string (hash-map "ok" #t "xs" #(1 2 3))))
;; => {"ok":true,"xs":[1,2,3]}
FunctionSignatureNotes
json:read(port [opts ...]) -> valueReads from any input port.
json:read-string(string [opts ...]) -> valueConvenience for in-memory text.
json:write(value [port]) -> '()Defaults to (current-output-port).
json:write-string(value) -> stringReturns the serialised form.

Options are alternating keyword/value pairs; the only key recognised in v1 is 'keep-integers-exact?.

Reference: json.md.


15. Logging

std.log is the structured-logging façade over the bundled spdlog runtime. Build a sink, wrap it in a named logger, and emit records at one of six severity levels; records carry a free-form message plus an optional payload alist that is rendered either as key=value (human formatter) or as JSON.

(import std.log)

(let* ((sink   (log:make-stdout-sink))
       (logger (log:make-logger "app" (list sink))))
  (log:set-default! logger)
  (log:info "service started" '((port . 8080)))
  (log:warn logger "slow query" '((ms . 412)))
  (log:flush! logger))

Each level wrapper (log:trace, log:debug, log:info, log:warn, log:error, log:critical) accepts (msg), (msg payload), (logger msg), or (logger msg payload).

HelperPurpose
log:make-loggerCreate a named logger fanning out to one or more sinks
log:make-stdout-sink / …-stderr-sinkColoured console sink (toggle with 'color? #f)
log:make-file-sinkPlain file sink ('truncate? #t for fresh files)
log:make-rotating-sinkSize-rotated file (bytes per file, retained file count)
log:make-daily-sinkDaily-rotated file at (hour, minute) local time
log:make-port-sink / …-error-port-sinkRoutes through a Scheme output port (or current-error-port)
log:set-level! / log:set-global-level!Per-logger or process-wide threshold (traceoff)
log:set-pattern!spdlog format string for the line layout
log:set-formatter!'human (default) or 'json
log:flush! / log:flush-on!Manual flush, or auto-flush above a level
log:shutdown!Drain and dispose every registered logger at exit

Reference: log.md.


16. Time, Freeze & Finalizers

std.time exposes time:now-ms, time:monotonic-ms, time:sleep-ms, time:utc-parts, time:format-iso8601-utc, time:elapsed-ms — see time.md.

std.freeze provides two attributed-variable combinators that compose with the logic engine:

FormMeaning
(freeze v thunk)Run thunk when logic var v becomes ground
(dif x y)Structural disequality; succeeds iff x and y cannot unify

See freeze.md and finalizers.md for object-lifetime hooks.


17. Logic Programming & Unification

Logic variables (logic-var), structural unification (==), and the search combinators (findall, run1, succeeds?, naf) are first class. Backtracking is implemented by a trail managed at the VM level — exception handling and CLP propagation compose with it cleanly.

(import std.logic)

(define parent-db
  '((tom bob) (tom liz) (bob ann) (bob pat) (pat jim)))

(defun parento (p c)
  (map* (lambda (f) (lambda () (and (== p (car f)) (== c (cadr f)))))
        parent-db))

(let ((c (logic-var)))
  (findall (lambda () (deref-lvar c)) (parento 'tom c)))
;; => (bob liz)

Reference: logic.md.


18. Constraint Logic Programming

Three CLP domains are bundled:

DomainModuleReference
CLP(FD)std.clpclp.md
CLP(B)std.clpbclpb.md
CLP(R)std.clprclpr.md
(import std.clp)
(let ((vars (list (clp:var) (clp:var) (clp:var))))
  (for-each (lambda (v) (clp:domain v 1 9)) vars)
  (clp:all-different vars)
  (clp:solve vars))

std.clpr exposes interval domains, linear and quadratic minimise/maximise routines backed by the Fourier–Motzkin oracle. See cookbook/numerics/portfolio-lp.eta for a worked LP.


19. Fact Tables

std.db provides Datalog-style relations with defrel, assert!, and tabled evaluation — see db.md. std.fact_table is a columnar store with hash-indexed lookups for analytics workloads — see fact-table.md and cookbook/numerics/fact-table.eta.


20. Causal Inference

std.causal answers questions of the form “what happens to Y if we intervene on X?” from a graph and (optionally) data. A graph is an edge list; from there you can identify an estimand, estimate it from observations, run sensitivity checks, learn structure, or render the graph for a notebook.

A graph is just an edge list — -> is a directed edge, <-> an unobserved-confounder bidirected edge:

(import std.causal std.causal.identify)

(define finance-dag
  '((sector      -> market-beta)
    (sector      -> stock-return)
    (market-beta -> stock-return)))

(do:identify finance-dag 'stock-return 'market-beta)
;; => (adjust (sector) (P stock-return market-beta (sector)) (P (sector)))

(id '((x -> m) (m -> y) (x <-> y)) '(y) '(x))
;; => (sum (m) (prod (P (m x)) (sum (x*) (prod (P (y x* m)) (P (x*))))))

Once an estimand is identified, plug observational data into the estimation backends — every estimator returns a scalar ATE, and do:bootstrap-ci wraps any of them for percentile CIs:

(import std.causal.estimate)

(define obs '(((x . 0) (z . 0) (y . 0.0)) ((x . 1) (z . 0) (y . 2.1))
              ((x . 0) (z . 1) (y . 9.8)) ((x . 1) (z . 1) (y . 12.0))
              ;; … many more rows …
              ))

(do:ate-gformula obs 'y 'x '(z))   ;; plug-in regression
(do:ate-aipw     obs 'y 'x '(z))   ;; doubly-robust
(do:bootstrap-ci (lambda (b) (do:ate-aipw b 'y 'x '(z))) obs 500 0.05)

Render any graph straight to Mermaid or DOT for notebooks and papers:

(import std.causal.render)
(dag:->mermaid finance-dag)        ;; flowchart LR …
(dag:->dot finance-dag '((title . "Finance DAG") (rankdir . "LR")))
ModuleScope
std.causalDAG utilities, do-calculus rules, back-door identification, plug-in adjustment
std.causal.admgADMGs, bidirected edges, latent projection, c-components
std.causal.identifyID / IDC algorithms, hedge detection, estimand simplifier
std.causal.adjustmentGeneralised adjustment, front-door, IV
std.causal.mediationNatural / controlled direct & indirect effects
std.causal.transportSelection diagrams, sBD criterion, transport queries
std.causal.counterfactualTwin networks, ID* / IDC*, effect-of-treatment-on-treated
std.causal.estimateg-formula, IPW, AIPW, TMLE, bootstrap CIs, E-value, Rosenbaum bounds
std.causal.learnPC / FCI / GES / NOTEARS structure learning, Fisher-z & χ² CI tests
std.causal.renderDOT, Mermaid, LaTeX renderers; define-dag macro

Reference: causal.md; counterfactual deep-dive causal-counterfactual.md; end-to-end example cookbook/causal/causal_demo.eta.


21. Automatic Differentiation (AAD)

Reverse-mode AD with a tape recorded directly by the VM — no closure allocation per arithmetic op. grad returns (value gradient-vector) in a single backward sweep over the tape.

(import std.aad)
(grad (lambda (x y) (+ (* x y) (sin x))) '(2 3))
;; => (8.909... #(2.583... 2))

Helpers for AD-safe primitives (ad-abs, softplus, relu, check-grad) and tape introspection live in std.aad.

Reference: aad.md.


22. Statistics & Linear Algebra

std.stats provides descriptive statistics, OLS multi-regression, PCA, and distribution functions, backed by Eigen for dense linear algebra. The Eigen layer is currently exposed only through std.stats and std.torch — there is no separate user-facing module.

Reference: stats.md.


23. libtorch / Neural Networks

std.torch wraps libtorch tensors, autograd, the nn module suite, optimisers, and (when built with CUDA) device transfer.

(import std.torch)
(define x (torch:tensor '((1.0 2.0) (3.0 4.0)) '(:requires-grad #t)))
(define y (torch:matmul x (torch:transpose x 0 1)))
(torch:backward (torch:sum y))
(torch:grad x)

Reference: torch.md; tests: cookbook/tests/torch/.


24. Concurrency & Distribution

Eta’s actor model is built on nng: every actor owns a mailbox socket; messages are arbitrary Eta values serialised by the runtime. The same send! / recv! API works for in-process threads, OS processes, and remote TCP peers.

PrimitiveUse
(spawn module-path)Fork a child process running the named module
(spawn-thread thunk)Run a closure in a fresh in-process VM thread
(current-mailbox)Child-side handle to the parent / spawner
(send! sock v 'wait)Block until message is sent
(recv! sock 'wait)Block until a message arrives
(monitor sock)Receive a (down …) message when the peer dies

High-level patterns provided by std.net: worker-pool, request-reply, survey, PUB/SUB. Supervision trees (one-for-one, one-for-all) live in std.supervisor.

References: message-passing.md, networking.md, network-message-passing.md, supervisor.md.


25. Finance Examples

ExampleTopicWalkthrough
european.etaBlack–Scholes Greeks via AADeuropean.md
sabr.etaSABR vol surface, Hagan approximationsabr.md
xva.etaCVA / FVA sensitivities via AADxva.md
xva-wwr/Wrong-Way Risk via do-interventionsfeatured/xva-wwr.md
portfolio.etaCausal portfolio engine (full pipeline)featured/portfolio.md
portfolio-lp.etaLP variant via std.clprCLP(R)
fact-table.etaColumnar fact tablesfact-table.md

26. Tooling

TopicReference
REPLrepl.md
Compiler (etac)compiler.md, bytecode-and-tools.md
Bytecode VMbytecode-vm.md
LSP / DAP / VS Codevscode.md, debugging.md
Jupyter kerneljupyter.md
Testing (std.test, eta_test)testing.md
Profiling (eta prof, std.prof)profiling.md, std.prof

27. Runtime Internals (overview)

NaN-boxing

All Eta values fit in a 64-bit double-NaN payload: fixnums and small immediates are encoded directly; heap objects (pairs, vectors, strings, closures, records, …) use tagged pointers. See nanboxing.md.

Bytecode VM

A stack-based VM with explicit Call / TailCall, SetupCatch / Throw, and unification opcodes. See bytecode-vm.md and runtime.md.

Garbage collector

Mark-and-sweep over the VM heap with explicit GC roots from the value stack, frame stack, intern table, and registered finalizer set. See runtime.md, finalizers.md.

Optimisations

etac -O runs constant folding, dead-code elimination, peephole opcode fusion, and known-call inlining. See optimisations.md.

For the architectural overview, read architecture.md.


28. Examples Index

A curated walk through everything in cookbook/: beginner programs, symbolic & logic, AAD & finance, concurrency, causal & portfolio engines, plus the notebook collection.

Tour: examples-tour.md.


29. Further Reading