A Forth-styled language for numeric, statistical, and symbolic work, and for general scripting: matrices and linear algebra, statistics and regression, dimensioned quantities and calendar arithmetic, sets/arrays/frames and columnar datasets, strings and regex, subprocesses and pipes, logic programming with backtracking, and multi-core data parallelism — with embedded SQLite, a runtime C FFI, an SVG plotting library, and an MCP server. A compact, self-contained C interpreter: NaN-boxed tagged values, direct-threaded code with compile-time fusion, mark-and-sweep GC, and a WASI build from the same source.
The language is designed to be LLM-friendly from the start.
make pack concatenates the whole documentation set — the reference, the library reference, and the idioms — into one file sized to fit a single LLM prompt.
Dedicated to Chuck Peddle and Tony Wilkinson.
make # builds ./telic
make test # runs the golden-output test suite
make bench # runs the benchmark suite (Telic vs CPython)
./telic # REPL
./telic prog.telic # run program files and exit (repeatable, in order; -i to drop into the REPL after)
./telic -e '3 4 + .' # run a code string and exit (repeatable, in argument order with files; implies -b)
Self-contained: its vendored dependencies — PCRE2 (regex), isocline (REPL line
editing), and SQLite (embedded SQL) — live under external/ and are built from
source into the binary, so make needs only a C compiler and the system
libffi. Refresh them with make vendor-pcre2, make vendor-sqlite, and
make vendor-isocline (see each directory's PROVENANCE).
make also builds liblapacke_telic.so, a thin shared library that wraps
the platform BLAS/LAPACK (Accelerate on macOS, OpenBLAS on Linux) behind
the LAPACKE C interface. The statistics library dlopens it through the
FFI and requires it — the stats module is native-only; the wasm build
excludes the FFI. Re-vendor with make vendor-lapacke.
make wasm # cross-builds telic.wasm (needs wasi-sdk in ~/wasi-sdk, or set WASI_SDK)
make test-wasm # runs the golden suite against telic.wasm under wasmtime
The wasm build targets WASI (a-shell, standalone runtimes, the browser via a
WASI shim): PCRE2 compiles without JIT, SQLite single-threaded, and the
platform layer stubs what WASI lacks — no isocline line editing, FFI,
subprocesses, or threads; the loadable statistics library is native-only.
make test-wasm finds wasmtime on PATH or ~/.wasmtime/bin, or set
WASMTIME=<path>; tests exercising the stubbed words are skipped via
tests/wasm-skip.txt.
\ Arithmetic
3 4 + . \ 7
\ Exact rationals — and they take units (money)
1/3 1/6 + . \ 1/2
1/2 $ 50/1 ¢ + . \ 1 $
\ Matrices: * is element-wise; matrix multiply is matmul
[ 1 2 3 4 ] 2 2 matrix dup transpose * \ element-wise product of M and Mᵀ
\ Dimensioned quantities: units propagate, combine, and collapse
10 m 2 s / . \ 5 m.s^-1
1 kg 1 m * 1 s / 1 s / . \ 1 newton (interns to the named unit)
\ Complex numbers — and they take units (phasors)
-1+0i sqrt . \ 0+1i
3+4i volt 1-1i volt + dup . abs . \ 4+3i volt 5 volt
\ Dates: instants are quantities in s, so units do the date arithmetic
wall-now 2 week + time>iso . \ the ISO timestamp two weeks from now
"2026-01-31T09:00:00Z" iso>time { :months 1 } date-shift time>iso . \ clamps to Feb 28
\ Sets and set algebra
[< 1 2 3 >] [< 2 3 4 >] + . \ [< 1 2 3 4 >] (union via polymorphic +)
\ Set-builder { x² | x ∈ 1..10, even x } — literal + filter/map + spread
[< 1 10 range [: 2 mod 0= :] filter ' fsq map spread >] . \ [< 4 16 36 64 100 >]
\ Frames — symbol-keyed nested maps
{ :a 1 :b { :c 2 } } /b/c @ . \ 2
\ Path queries — * (any child), // (any depth), [pred] filters
{ :a { :n 1 } :b { :n 2 } } /*/n select-values . \ [ 1 2 ]
{ :ann { :age 34 } :bo { :age 25 } } /*[age>30]/age select-values . \ [ 34 ]
\ JSON: parse to frames/arrays, serialize back
"[1, 2, 3]" json>frame frame>json . \ [1, 2, 3]
\ Higher-order operations
[ 1 2 3 4 5 ] [: dup * :] map . \ [ 1 4 9 16 25 ]
[ "bb" "a" "ccc" ] [: size :] sort-by . \ [ "a" "bb" "ccc" ]
\ Strings and regex (PCRE2)
"x=42" "(\w+)=(\d+)" match . \ [ "x=42" "x" "42" ]
"hello world" "o" "0" replace . \ hell0 w0rld
\ format fills {n} with the stack entry n deep from the top ({0} = top, {1} under
\ it), with a printf-style {n:spec}; {tab} and {nl} emit the control characters
1.5 250 "{0:d} ms{tab}{1:04.1f} s" format . \ 250 ms 01.5 s
"{bold}{red}alert{plain} ok" format . \ colored on a tty; the ink escapes vanish when piped
\ Exceptions
[: "missing" throw :]
[: "got " . . cr :] try-catch \ prints "got missing"
\ Generators — coroutines on the delimited-continuation primitives
: primes 2 yield 3 yield 5 yield 7 yield ;
' primes 4 gen-take . \ [ 2 3 5 7 ]
\ Subprocesses over pipes
"echo hi" run read-out . \ hi
\ Logic: unify binds variables; amb keeps the first branch that succeeds
lvar to X lvar to Y lvar to Z
[ 1 2 3 ] [ X Y Z ] ~ drop X ? . Y ? . Z ? . cr \ 1 2 3
[: fail :] [: "fallback" :] amb . \ fallback
\ Multi-core: run a quotation across the array on every core
[ 1 2 3 4 5 6 7 8 ] [: dup * :] pmap . \ [ 1 4 9 16 25 36 49 64 ]
\ Datasets: column-oriented tables with verbs
[ [ "name" "age" ] [ "ann" 34 ] [ "bo" 25 ] [ "cy" 61 ] ] true rows>dataset
dup :age @ mean . \ 40 (a numeric column is already a vector)
[: :age @ 30 > :] filter :name @ . \ [ "ann" "cy" ]
\ Count distinct values, most frequent first; masks alter as well as select
[ :b :a :b :c :b ] count first . \ [ :b 3 ]
[ 1 2 999 4 ] vector dup 999 eq null mesh matrix>array . \ [ 1 2 null 4 ]
\ Statistics over a matrix column: mean and the median (0.5 quantile)
[ 2 4 4 4 5 5 7 9 ] 8 1 matrix dup mean . 0.5 quantile . \ 5 4.5
\ A fit over a real table: income on three columns of 32,561 rows. The statistics
\ library reaches LAPACK through the FFI, so this part is native-only.
"statistics" load-library
"data/adult.tsv" read-tsv to adult
adult [ :age :education-num :hours-per-week ] dataset>matrix with-intercept
adult@income 50 1e-8 1 fit-logistic-ridge transpose .
\ 1x4: -8.523 0.04691 0.3453 0.04283 (intercept, age, education, hours)
\ SQLite, in-memory: create, insert a bound param, query back
":memory:" db-open
dup "create table t(x)" [ ] db-exec drop
dup "insert into t values (?)" [ 42 ] db-exec drop
"select x from t" [ ] db-query :rows @ 0 @i :x @ . \ 42make bench runs bench/run-benchmarks.sh, which builds the binary, runs each
port five times against three CPython runs, and reports medians with a
verification table pairing every result against its reference. The -matrix
rows are vectorized and answer to numpy, the -parallel rows to a process pool
of the same width, and both time pool creation as telic times spawning its
threads. mandelbrot-matrix and spectral-norm-matrix take their matrix
multiply from the platform BLAS through the statistics library, so those two
rows put Accelerate against numpy rather than telic's own kernels. Refresh the
table below from a report with
python3 tools/update-readme-bench.py <report.md>; never edit a cell by hand.
Medians of 5 telic reps against 3 CPython reps (2026-08-21): Apple M4 Max, 16 cores (12P + 4E), Mac16,5, 128 GB memory, Darwin 25.5.0, clang -O3 -march=native -Wall -Wextra, CPython 3.14.6, numpy 2.5.1.
| benchmark | size | telic | python | py / telic |
|---|---|---|---|---|
| leibniz | 1000000000 iterations | 8.373 s | 42.258 s | 5.05× |
| leibniz-matrix | 1000000000, vectorized vs numpy | 0.7167 s | 1.786 s | 2.49× |
| leibniz-matrix | 1000000000, vectorized vs R 4.5.2 sum(4 / seq.int(...)) |
0.7167 s | 1.720 s | 2.40× |
| leibniz-parallel | 1000000000, pmap vs pool of 16 | 1.221 s | 3.335 s | 2.73× |
| nqueens | N = 8 ×45 | 0.5167 s | 1.841 s | 3.56× |
| nqueens-iter | N = 8 ×45 | 1.115 s | 1.841 s | 1.65× |
| nbody | 500000 steps | 0.5178 s | 1.236 s | 2.39× |
| raytrace | 40× 100×100 | 0.5713 s | 5.350 s | 9.37× |
| raytrace-parallel | 420× 100×100, pmap vs pool | 0.5149 s | 5.170 s | ~10× |
| float | 100000 pts × 60 | 0.7669 s | 1.895 s | 2.47× |
| crypto-pyaes | 23000 B, 70× enc+dec | 0.5119 s | 2.673 s | 5.22× |
| fannkuch | N = 9 ×6 | 0.6113 s | 1.099 s | 1.80× |
| binary-trees | depth 16 ×2 | 0.6387 s | 1.397 s | 2.19× |
| mandelbrot | N = 1000 ×2 | 0.6956 s | 2.628 s | 3.78× |
| mandelbrot-matrix | N = 1000 ×7, vectorized vs numpy | 0.5685 s | 0.6907 s | 1.21× |
| mandelbrot-parallel | N = 1000 ×16, pmap vs numpy pool | 0.5438 s | 2.604 s | 4.79× |
| spectral-norm | N = 130, 50× | 0.6281 s | 2.573 s | 4.10× |
| spectral-norm-matrix | N = 260, 7000× vs numpy | 0.6062 s | 0.5687 s | 0.94× |
| scimark-lu | N=100, 200× | 0.8276 s | 11.383 s | ~14× |
| scimark-sparse | N=1000, 1000× | 0.5684 s | 2.196 s | 3.86× |
| scimark-fft | N=1024, 5×150 | 0.5529 s | 2.130 s | 3.85× |
| barnes-hut | 200 bodies, 6×50 | 0.5211 s | 1.373 s | 2.64× |
| scimark-sor | N=100, 10 cyc × 200 | 0.6641 s | 11.446 s | ~17× |
| scimark-montecarlo | 1000000 × 6 | 0.7160 s | 1.883 s | 2.63× |
| montecarlo-parallel | 20000000 samples × 13, pmap 10w vs pool 10w | 0.6124 s | 2.600 s | 4.25× |
| meteor | 30 solves | 0.5609 s | 1.615 s | 2.88× |
| hexiom | level 25, 250 solves | 0.5553 s | 0.8110 s | 1.46× |
| regex-dna | 100K → 1M ×17 | 0.5799 s | 0.0003 s | 0.00× |
| regex-compile | 239 patterns, cold | 0.0010 s | 0.0071 s | 7.24× |
| regex-effbot | 21 pat × 0..10k | 2.721 s | 15.767 s | 5.79× |
| regex-v8 | 12 blocks ×200, browser trace | 0.7663 s | 2.143 s | 2.80× |
| deepcopy | N=100000, 60 copies/N | 0.5783 s | 11.436 s | ~20× |
| json-loads | 222k parses | 0.5321 s | 0.9706 s | 1.82× |
| json-dumps | EMPTY/SIMPLE/NESTED/HUGE ×500 | 0.7272 s | 2.521 s | 3.47× |
The ports live in bench/pyperformance/ beside the CPython sources they answer
to, and bench/variants/ holds the vectorized and parallel forms. Where a port
departs from its pyperformance original the file's header says so.
- Tagged Vals — the none value, floats, strings, symbols, sets, arrays, cons pairs, frames, matrices, quantities, segments, execution tokens, curried tokens, dictionary addresses, continuations, logic variables and the unbound/wildcard sentinel, process streams, database handles, C pointers, internal marks. A single 8-byte NaN-boxed representation; the tag determines interpretation.
- Direct-threaded inner interpreter — each dictionary cell is a handler function pointer, dispatched by an indirect tail call (
musttail); a colon call, literal, or branch carries its operand in the cell(s) right after the handler. The dictionary is the threaded code. - Compile-time instruction fusion — a float op collapses with its operands and its store into one instruction, whether they are globals (
vvf+ a b), locals (zr zr f* to zr2), a literal, or a stack slot read by depth (2 pick f+), so a quotation reading values parked below a combinator's operands costs the same as one reading locals. Also fused:f*+/f*-multiply-add, a comparison before a branch (= if,> while), an array read-modify-write (arr i arr i @i f1- !i), and++ name/f++ name.see-compiledshows the fused ops. - Program image and execution state separated — the dictionary, symbol pool, and object heap are global (
Vocabulary,Compiler,Arena); the three stacks, instruction pointer, locals, and GC roots live in a per-runInterpreter. Several execution contexts share one image, which is how the parallel words give each worker its own stacks over the shared heap — and why a worker xt must not mutate shared inputs or print. - Three stacks — data, return, and a side stack for values that mustn't sit on either:
>side,side>,side-drop,side-peek,side-depth. - Colon definitions —
: name body ;. The body is captured as source text forseeand the text-formsave. - Anonymous quotations —
[: ... :]pushes a fresh xt. Works at top level and inside colon defs. recurse— compiles a call to the innermost definition being compiled (the enclosing quotation, else the colon word), so an anonymous quotation can self-call.- Tail-call elimination — a call in tail position compiles to a frame-reusing jump, so a self-recursive or
recurseloop runs in constant return-stack space. Disabled where it would be unsafe (a body using>r/reset/shift/fail, or locals plus a quotation). - Partial application —
curry( value xt -- xt' ) binds a value into a curried token, a heap value accepted wherever an xt is; the token travels through other words' frames intact, works inside parallel regions, and is garbage-collected. - Control flow —
if/else/then, thebegin/until/againandbegin/while/repeatloops withleave/continuefor early exit, the countedstart limit delta do k … loopover a named index local,case/of/endof/endcasedispatching by unification (clauses are patterns — ground values, open-record frames,_, logic vars that bind for the body), countedtimes/i-times,exit, and>r/r>/r@for return-stack access. - Delimited continuations — four primitives, the substrate for generators, exceptions, and restarts (
docs/continuations.md):resetinstalls a delimiter,shiftcaptures the slice up to it,shift-withcaptures and then runs a handler in the outer context,resumere-enters a captured continuation, multi-shot. - Tick and execute —
' word executefor first-class invocation by name. - Forward declaration —
defer namedeclares a word with no target, for mutual recursion or late binding;embodiesinstalls one and stays retargetable,embodies!finalizes it and rewrites the existing call sites to call direct. forget— truncate the dictionary back to a named word; symbol identities survive.- Variables and symbols —
variable foodeclares a global, read by bare name and assigned with42 to foo; at top leveltocreates the global on first assignment.symbol bardefines a symbol,:foois a symbol literal,string>symbolinterns a computed string. - Word-local variables — a head at the start of a colon definition or quotation names what the body receives from the stack, rightmost from the top:
| x y |, orx y |with the opening bar left off. Everything else is declared wheretofirst assigns it, and the compiler collects those names to the head, so a name means one thing throughout the body. In the head,^nameis an enclosing global the body assigns and?namea fresh logic variable per call.++ name/-- nameincrement/decrement in place (f++/f--the unsafe float-only forms). - A quotation's locals are its own — it reads the slots it declares and the data stack, never the enclosing body's, which is a compile error. Values reach it three ways: its own head,
pickfrom the stack below the combinator's operands, orcurry. - Mark-and-sweep GC — walks the three stacks, the C-level roots, and the dictionary (each global's value cell and every compiled literal). Triggers on object-table and live-byte pressure, at a safepoint between words.
Coroutines on the continuation primitives, in generators.telic:
yield— emit a value to the driver and suspend until resumed.start-generator— run a producer to its firstyield, leaving the yielded value and a resumable continuation.gen-take— collect the first N values a producer yields into an array;gen-each— run a consumer on each yielded value until the producer is exhausted.
Built in exceptions.telic on top of the continuation primitives:
throw— non-local exit with a value.catch— wraps an xt, answering(result 0)or(exc 1). Interpreter errors are caught too — division by zero, out of bounds, a type mismatch — arriving as a{ :message :trace }frame.try-catch— run an xt, and on either kind of failure run a handler with the exception.ensure— cleanup on both the normal and the failing path;with-db/with-streamscope a resource that way.
An uncaught error prints its message with a backtrace of the call chain,
innermost first (in inner ← mid ← outer), and an unknown word suggests the
nearest name in scope (unknown word: filtr (did you mean filter?)). A
shift-with handler can resume the captured continuation, giving the Common
Lisp restart pattern.
A logic variable stands for a value not yet known. unify makes two terms equal
by binding the variables inside them, and amb tries alternatives, undoing
those bindings when one fails — on the same delimited-continuation substrate as
exceptions.
- Variables —
lvarpushes a fresh one;| ?x |in a locals head declares one that is fresh on every call.?reads a variable, answering what it is bound to. unify(~) — makes two terms equal, binding variables on either side: values by comparison, arrays and cons pairs element-wise, frames as open records where shared keys must agree and extra keys are ignored. A mismatch fails._matches anything and binds nothing.- Search —
ambruns the first of two quotations; if it fails — a mismatch, or an explicitfail— its bindings are undone and the second runs.choosedoes the same across a cons list. The first branch that succeeds is the one kept;solutionsandtake-solutionscollect every success instead of the first. - Tests —
matches?answers whether two terms could unify and leaves nothing bound;unify?keeps the bindings when they do.case/ofdispatches throughunify?, so a clause pattern may hold variables that bind for its body. - Lists —
[( a b c )]builds cons pairs and[( H T )]is Prolog's[H|T]underunify, withcons,head-tail, andarray↔consconversions. - Fact database —
relation/assert/query/retract/count-matches/inner-join. A relation is a frame of a row-set plus per-column indexes (declared symbol columns); rows are column-keyed frames that dedup;querymatches a pattern by unification, narrowing through the index.inner-joinmerges two relations on a shared column, andbulk-loadbuilds a whole relation in one sorted pass. The same row-frame shape is what a SQLite query returns.
- Polymorphic arithmetic —
+/-/*//dispatch on operand tags: floats compute, strings concatenate (+), sets union/difference/intersection, matrices element-wise, a scalar broadcasts over a matrix, and arrays concatenate (+). - Integer division —
%truncating divmod, withmod(sign follows the dividend) andquotient(toward zero) on top; all three broadcast element-wise like the arithmetic words. min2/max2— pairwise minimum and maximum, element-wise with scalar broadcast.- In-place matrix ops —
+!/-!/*!//!mutate the left matrix in place. Float-only fast paths (f+,f-,f*,f/,f^, …) skip the type dispatch when both operands are known floats. - Matrix construction —
R C 0-matrix(zeros),[ ... ] R C matrix,[ ... ] vector(an n×1 column, length inferred),V N diagonal-matrix(N×N with V on the diagonal),N identity-matrix,start end step matrix-range(a 1×N row over a stepped range). matmul— the matrix product (*is element-wise); the statistics library adds a size dispatch, keeping the built-in loop for small operands and handing larger ones to the platform BLAS.- Indexing —
@i/@j/@i,jto read rows, columns, or single cells;@ereads by flat row-major index (whatargmax/where/argsortproduce);!i,jand!estore a single element in place. - Shape —
dim,reshape,flatten,transpose,diagonal,matrix>array(the elements as an array in row-major order; a dimensioned matrix yields per-element quantities, NaN becomesnull). - Selection —
augment/hstack(concatenate two matrices column-wise),vstack(row-wise),submatrix(copy a half-open row×column block),select-rows(gather rows named by a float index array or an index vector; a dataset operand gathers every column by the same indices). - Reductions —
sum,row-sums,column-sums,max,min,argmax,argmin(flat row-major index of the extreme element),row-maxes,row-mins,column-maxes,column-mins,cumulative-sum(row-major prefix sums, shape preserved). Librarymean,row-means,column-meanson top. - Norms —
normandfrobenius-norm, both √(Σ elements²);dotis the inner product. - Descriptive statistics —
var,quantile, andks-distancein C, withstd,se,median,percentile,quantiles,iqr,ci,summary,histogram-table,ecdf,binomial-deviance,cross-validate, and thebootstrapfamily in the embedded library — LAPACK-free, so wasm-capable. NaN elements are missing values: the statistics skip them, and the correlations and regressions use complete cases. - Correlations —
covariance,correlation-pearson,correlation-spearman,correlation-kendall(tau-b);correlate-withbootstraps a confidence interval for any of them,cordoes it with kendall in one word.qnormandpnormare the standard normal quantile and CDF,random-normala standard normal deviate. - SVG plotting (
lib/plot.telic) — scatter, line series, histograms, bar charts, and Tukey boxplots over a deferred-rendering figure: marks accumulate with the style in effect and nothing maps to pixels until render, so draw order is free and the domain may be set after the data.save-figurewrites a version,show-figureopens a browser view that later versions appear in. - Element-wise math —
abs,sqrt,exp,log,ln,ln1+,log2,sin,cos,tan,sinh,cosh,tanh,asin,acos,atan,atan2,erf,erfc,round,truncate,round-up,round-down. Polymorphic over floats and matrices. - Comparison —
=is structural, so matrices work as set members;</>/eqon a matrix or array operand mask element-wise into a 1/0 matrix, sonames "ann" eq wherefilters a text column. On scalars and strings all of them are structural. - Sorting and masks —
sort(an ascending copy, NaNs last),argsort(the sorting permutation),where(the flat indices of a mask's nonzero elements),nan?(the missing-value mask), andmesh(masked substitution). Masks serve selection and alteration alike:dup 0 @j 0 < where select-rowskeeps the rows whose first column is negative,dup nan? 0 meshfills a column's NaNs,dup -1 eq null meshturns a sentinel into missing.
Fractions of arbitrarily large integers, always reduced; an integer is the case
with denominator 1. 1/3 is a literal, and an integer literal, JSON integer, or
SQLite INTEGER too large for a float reads as an exact rather than rounding
silently. The arithmetic, comparison, and rounding words compute exactly;
comparison crosses exact and float, arithmetic between them errors, and
float>exact / exact>float convert. A quantity's magnitude may be an exact,
so currency arithmetic is exact (1/2 $ 50/1 ¢ + is 1 $). rationalize
answers the simplest fraction that reads back as the same float. Matrices and
the ⚠ words take only floats.
A pair of floats with literals 3+4i / 4i. + - * / ^ take two complexes or a complex and a float (floats promote losslessly); sqrt, exp, ln, and the trigonometric words answer principal values; negate keeps the type, abs answers the modulus; complex / real-part / imaginary-part / conjugate / arg construct and destructure. Ordering is by real then imaginary part, and a complex equals a float of its value. A complex takes a unit (3+4i ohm), so impedance arithmetic is quantity arithmetic.
A magnitude (float, matrix, exact, or complex) carrying a unit; arithmetic propagates and checks units, rescaling same-dimension operands. Units are rational-exponent vectors over user-declared base dimensions, each with a rational scale.
base/unit— declare dimensions and units.base unit m;1 kg 1 m * 1 s / 1 s / unit newton(derived);1 $ 100 / unit ¢(scaled sub-unit). A unit word is postfix —10 m,3 newton.- Arithmetic —
*//combine unit exponents and scales (a dimensionless result collapses back to a bare float/matrix);+/-require the same dimension and rescale across scales;^/sqrtscale the exponents;= < >compare by value, normalizing scale within a dimension. Named units print by name, unnamed compounds in base form. - Statistics keep the unit — the matrix reductions and statistics accept a dimensioned matrix:
sum/mean/max/min/quantile/median/iqr/cianswer in the operand's unit,varin the unit squared (std/sereturn throughsqrt), index/count words and the correlations answer bare;magnitudestrips a quantity to its payload,unit-ofanswers its unit as the quantity1in that unit. - Standard set (
units.telic) — SIm s kg ampere kelvin mol, derivedhertz newton pascal joule watt coulomb volt,minute/hour/day/week/km, and currencies$/¢,£/penny,€/eurocent. - Constants (
constants.telic) — capitalized:PIETAUPHI, and the physical set as dimensioned quantities (CGHHBARKBNAQE, SI-2019 exact values) —C 2 ^ 1 kg *is E=mc², and prints in joules.
Integer bitwise operators over the float representation: a value is read as a two's-complement integer (exact within the double's 53-bit range), the operation runs, and the result is pushed back as a float — byte- and bit-level work such as block ciphers, codecs, and bit-stream packing.
bit-and/bit-or/bit-xor/bit-not— bitwise logic, named apart from the truthiness wordsand/or/not.lshift/rshift— left shift and arithmetic right shift (= floor(a / 2ⁿ));lowest-bit— 0-indexed position of the lowest set bit (−1 when zero).- Hex literals —
0xffreads as 255;{0:x}underformatrenders hex back out.
A thread-local xoshiro256** stream. Each worker thread derives its own stream from the shared base seed, so parallel draws are deterministic per worker.
seedsets the base seed;randomdraws a uniform float in[0, 1)andrandom-inta uniform integer below a bound.sampleandresample-indicesdraw on this stream.
- String literals are raw,
""being the one escape;formatfills{n}placeholders from the stack —"got {0} of {1}" format— and on a terminal colors text with ink directives ({red}…{plain}) that vanish when piped;+concatenates. - Regex on PCRE2, JIT-compiled:
match,match-all,replace(all matches, with&and\1–\9backrefs), andhas?to test one. Patterns are plain"..."literals, read by PCRE2 itself. - Slicing and building —
substring,char-at,spliton a pattern,joinwith a separator,trim,upper-case/lower-case(ASCII). - Unicode — strings are UTF-8 and the bare words work in codepoints:
size,substring,char-at,codepoint-at, withbyte-sizeandbyte-substringfor the raw layer and for regex byte offsets.string>chars/string>codepointsdecompose,codepoint>char/codepoints>stringrebuild,emitencodes one. Regex is Unicode-aware, and invalid bytes fail to match rather than erroring. edit-distance— over codepoints, counting insertions, deletions, substitutions, and adjacent transpositions.
- Set literals —
[< 1 2 3 >], set operations,member?,size, in-placeset-add!/set-remove!, andarray>set(sort-and-dedup an array into a set in one pass). group-by—array :col group-bygroups frames by a symbol field into a frame from each value to a set of rows.- Array literals —
[ 1 2 3 ],arrayto gather N from the stack,array-ofto fill,rangeandiotafor integer sequences,@iand!ito read and store by index. - Array operations —
sort,reverse,take,concat,flatten-array,sample(with or without replacement),shuffle,resample(the bootstrap draw), andfirst/second. - Growing at the end —
add-last!andremove-last!over a doubling buffer, both amortized O(1) with indexing still O(1). - Map, fold, zip-map, filter —
mapfor a single source,reducefor a left fold over a collection,nmapfor N-ary zip,filterto select by predicate, with anonymous quotations as the higher-order argument. - Counted map-fold —
fold-timesfolds over an index range with no collection, the accumulator staying off the data stack;sum-timesandproduct-timesare the common defaults andpmap-reducethe parallel form. - Search, traversal, and reshaping —
find-first(short-circuits),any?/all?,eachfor side effects,flat-map,sort-byon an extracted key,partition, andgroup-withfor grouping by a computed key. - Destructuring —
spreadpushes a set/array/frame's elements onto the stack (a frame as alternating symbol/value); a locals head,unify, or acasepattern receives the pieces by name. - In-place slicing —
slice!copies a strided run from one array into another (a negative step with source and target aligned reverses in place),to-slice!stores values from the stack into a range.
Symbol-keyed nested maps — the associative type, and the compound term the logic layer builds on. The three bracket families are distinct: [ ] arrays, { } frames, [< >] sets. [ ] { } and ; are self-delimiting — [1 2 3] and {:a 1} parse without inner spaces; [< >] still need theirs.
- Literals —
{ :a 1 :b 2 }; values may be any Val, including nested frames, arrays, and sets. - Builders —
framefrom parallel key and value collections,array>framefrom an alternating key/value array,frame>arrayback again. - Path literals —
/a/b/cis a symbol array[ :a :b :c ], built once at compile time, used to address into the tree — and usable as a key when constructing a frame ({ /a/b/c v }/array>frame), where it vivifies nested frames. A path may also be a search pattern:*matches any child at that level,//matches at any depth (descendant-or-self), and[…]filters by predicate ([city=:NYC],[age>30],[.>0]on the node itself,[addr/zip]on a sub-path). - Access —
@gets,!sets and vivifies intermediates,has?tests,delete-atremoves,update-atapplies a quotation to a leaf,mergecombines two frames, andkeys/values/sizereport. All buthas?take a single key or locator, not a search pattern. - Key tokens —
row@pricejoins a frame reference to a key in one token, the left part being a local or a word that supplies the frame. Gets chain —row@address@city—row!pricesets, and an empty left part takes the frame from the stack, so@priceis the postfix form. A defined word always wins, so@iand@orkeep their meanings. - Path queries —
select-valuesreturns every value a search pattern matches, in document order;select-keysreturns the path to each match. - Representation — parallel key/value arrays in symbol-id order, which is interning order rather than alphabetical, so
keys,values,spreadand printing are stable for a program but not name-sorted. Mutable in place, reference semantics, structurally comparable.
Flat, fixed-length typed numeric buffers stored off the arena (one allocation, freed by GC), for dense numeric data without per-element boxing and as FFI scratch.
int-segment/double-segment—( n -- seg )an n-element zero-filled buffer; both store doubles internally, so@ireads and!iwrites a float, sharing the array indexing words.segment>pointer— intern the backing buffer as aT_PTRfor an FFI:ptrargument, no copy.
An instant is epoch seconds as a quantity in s, so the units machinery is the
date arithmetic: wall-now 2 hour + is an instant, instant − instant is a
duration, … 1 day / counts days. Unsuffixed words are UTC and pure Gregorian
arithmetic, identical on every platform; -local twins use the process
timezone (TZ re-read per call).
wall-now— the absolute wall clock;nowis the monotonic interval clock.epoch>date/date>epoch— an instant to and from a date frame{ :year :month :day :hour :minute :second :weekday :yearday }, composition accepting a partial frame and carrying out-of-range fields mktime-style. Plus-localvariants.format-time/parse-time— strftime and strptime;time>iso/iso>timefor the ISO 8601 Z form.date-shift— calendar shifts by:years:months:weeks:days:hours:minutes:seconds, the day clamped to the target month.days-in-monthis leap-aware.
Worker threads over one shared object heap: a quotation runs across the collection on several cores, results joining back by handle with no copy. Allocation inside a region is per-worker.
pmap,pfilter(order preserved), andpmap-reduce, whose combiner must be associative.-extforms take an explicit worker count and items-per-claim; the bare forms usenum-coresworkers.
json>frame/frame>json— parse and serialize: objects ↔ frames with interned symbol keys, arrays ↔ arrays, strings ↔ strings, numbers ↔ floats,true/false↔ the reserved:1/:0symbols,null↔ the none value. An integer too large for a float reads as an exact and writes back without loss.
value>bytes/bytes>value— a whole value graph as bytes and back, sharing and cycles preserved; compiled code and OS handles are refused by type.save-value/load-valuedo the same through a file.
- Interactive REPL on isocline: theme-adaptive syntax highlighting, matching-brace highlighting, inline hints, Tab completion (dictionary words, filenames inside string literals), persistent history, and multi-line editing. Each entry answers
okwith the stack depth and top value, or the error message and its trace; a failed entry leaves the data stack as it was. loadruns a source file as if typed.savewrites the user's vocabulary as a re-loadable.telicsource file.reloadtruncates user state and re-runs every fileloaded this session, in order.read-file/write-file/append-file— read a whole file as one (byte-safe) string; write or append a string's bytes to a path.open-file— a file as a read-only stream, for the stream words below.args— the command-line arguments after the program file, as a string array; a leading#!line is skipped.file-exists?— whether a path exists (access,F_OK); follows symlinks, any file type.- Files and directories —
list-directory,file-info,make-directory,delete-file,delete-directory,rename-file,copy-file,touch-file, withlsmkdirrmrmdirmvpwdcatcptouchas shell names. find-executable—( name -- path/none )the absolute path ofnameon$PATH, or the none value if not found.load-library—"plot" load-libraryloadslib/plot.telicfrom beside the telic binary (binary-dir, symlinks resolved), from any cwd; the statistics library locates its LAPACK shared library the same way.env/env!— read an environment variable as a string (the none value if unset) and set one (process-wide, sostart-processchildren inherit it).stdin/stdout/stderr— the standard streams asT_STREAMvalues (fds 0/1/2), composing withread/write/close—s stdout writeemits,stdin readreads input whole.
Drive external programs over pipes (fork/execv/pipe/waitpid, with a manual PATH search; binary-safe, no shell):
start-process— launch from an argv array, answering{ :pid :in :out :err }with the three streams.write/read/read-line/read-available/wait-readable/close— stream I/O, blocking or not.running?/wait/stop— liveness, block-until-exit, signal-and-reap.subprocess.telicconveniences:run,read-out,read-err,write-in,run-result,end-process.parallel-run— a batch of commands at a bounded width, collecting each one's output and status in input order.
Embedded relational storage via the vendored SQLite amalgamation — built into the binary, no external dependency. A database is a T_DB handle.
db-open/db-close— open a file, or":memory:", and close it.db-exec— run a statement with no result set, answering the affected-row count.db-query— run a query, answering a fact-database relation of row frames keyed by column name, so the result drops straight intoquery/inner-join.db-query>datasetanswers the same query as a column-oriented dataset with typed columns.tsv>db— import a TSV into a new table, inferring each column's type.- Bound parameters — every query and statement takes an array bound to its
?placeholders, so values need no hand-escaping. create-index— index a query result so the fact-databasequerycan use it.
TSV is the one tabular file format (convert other formats to TSV before loading).
read-tsv/write-tsv— a header TSV to a column-oriented dataset with typed columns, and back.load-tsv/save-tsv— the same file as an array of row-arrays, untyped and in file column order.- Conversions —
rows>datasettypes the columns of an array of row-arrays,rows>relationbuilds an indexed fact-database relation,dataset>rowsinvertsrows>dataset, anddataset>matrixbuilds an observations×columns matrix from named columns. - Dataset verbs —
select-rows,select-columns,sort-rows,filter,map,dim,column-type, andcountwork on a dataset directly,filterandmapseeing each row as a frame keyed by column name and every column keeping its representation.column>arrayreads any column as an array,column>setits distinct values,column-typeits type (:numeric:datetime:quantity:text), andgroup-indicesmaps each distinct value to its row positions in one sort. frames>dataset— an array of row frames, asqueryanddb-queryreturn, into a dataset with inferred column types.aggregate— split-apply-combine: group rows by a column, reduce each group to a row frame, reassemble as a dataset.head/headn— print the first rows as an aligned table,headntaking the row count and the columns to lead with.replace-where!— edit one column in place where a predicate holds.resample-indices— indices drawn with replacement, for bootstrap resampling.
The statistics library (lib/statistics.telic, loaded on demand) builds on the matrix and FFI layers:
- Descriptive —
std,se,median,percentile,quantiles,iqr,ci(percentile confidence interval). - Resampling —
bootstrap/pbootstrap(parallel) over a fit quotation. - Linear algebra — matrix multiply (
dgemm-nn/tn/nt/tt) and matrix-vector products (dgemv-n/dgemv-t) on the platform BLAS,svdandfit-linearon LAPACK, all through the FFI. - Regression —
linear-regressionandlogistic-regression(Firth-penalized IRLS), each answering a model frame of per-coefficient estimates with bootstrap confidence intervals, the predictor names, and the complete-case data it fitted.fit-logistic-ridgeis the L2-penalized fit, withcv-logistic-ridge/pcv-logistic-ridgechoosinglambdaby cross-validation. - Generalized linear models —
fit-glmtakes a family as three quotations, withgaussian-identity,poisson-log,gamma-log,binomial-logit, andnegative-binomial-logprovided andfit-poisson/fit-gammawrapping the log-link fits.fit-negative-binomialestimates the dispersion alongside the coefficients;fit-multinomial,fit-multinomial-ridge, andpredict-multinomialhandle several classes. - Gradient boosting —
fit-xgbtrains an XGBoost booster on a feature matrix and response through the systemlibxgboost, taking a params frame keyed by XGBoost parameter names;xgb-predictscores,xgb-importanceranks the features,xgb-freereleases the booster, andxgb-save/xgb-loaduse XGBoost's own model format, readable by Python and R.
Call C functions in any shared library at runtime via libffi — no per-library glue. An opaque C pointer is a T_PTR handle (a registry index, since a 64-bit pointer doesn't fit a Val).
ffi-open—dlopena library;""opens the running process for already-linked symbols.ffi-function— resolve a symbol and define a word that calls it. Types are symbols::void :int :long :double :ptr :string.ffi-variadicdoes the same for a variadic function, with the variadic types fixed per binding.ffi-free—freea C buffer held as aT_PTR.matrix>pointer/segment>pointer— pass a matrix's or segment's buffer to a:ptrparameter, no copy.- FFI is unsafe: a wrong signature corrupts or crashes; argument count is checked, types are the caller's responsibility.
lib/statistics.telicdrives LAPACK'sdgesvd/dgelsdthis way, andffi-openonlibcurlmakes an HTTPS request in-process without a subprocess.
lib/http.telic — http-get, http-post, and the general http-request
({ :status :body }) over a curl subprocess; lib/claude.telic calls the
Anthropic API through it.
lib/mcp.telic serves the Model Context Protocol over stdio — telic -e '"mcp" load-library mcp-serve' — at revision 2026-07-28. Two tools: telic-eval runs
source in a named session, a child interpreter that keeps its definitions, data,
database handles and fitted models between calls, and telic-help answers a
word's reference entry. Sessions compute concurrently. For remote access, put
the stdio server behind a stdio-to-Streamable-HTTP gateway such as mcp-proxy.
dup,drop,swap,over,nip,rot,depth,pick,roll,clear— stack-manipulation primitives;pickcopies the nth item androllmoves it, both counting from the top.copy/reify— deep copy of a value (strings, arrays, sets, frames, matrices);reifyadditionally renames unbound logic vars to canonical:_0/:_1/… for a ground, storable, comparable snapshot.type-of—( a -- sym )the value's type as a symbol (:float,:frame,:lvar, …), with a lib predicate per type (float?…lvar?); a bound logic var answers as its value.now— monotonic seconds as a float, for timing intervals (wall-now, under Time and dates, is the absolute clock).timed—( xt -- … )runs xt, prints its elapsednowseconds, and passes its results through.see— prints a word's source definition;see-compileddisassembles its threaded body.man— a word's reference entry as a frame;help nameprints it.words— the dictionary grouped by reference section;apropos— every word whose name or summary matches a pattern.variables— the current globals as{ :name :value :type }frames;varsprints them.forget,bye,halt,gc,clear,.s,.a— interpreter utilities.
See PLAN.md.
src/c/telic.h — types, global program structs (Vocabulary/Arena/Compiler), per-run Interpreter, prototypes
src/c/core.c — engine: interpreter, dictionary, symbol table, GC, arena, value printing, tokenizer/reader, see, text save
src/c/words.c — arithmetic, stack ops, printing words, delimited continuations, format, math, RNG
src/c/time.c — clocks and calendar: wall-now, epoch↔date, strftime/strptime
src/c/compiler.c — compile-time words: colon/quotation definition, control flow, locals, to/constant/variable/symbol, forget
src/c/io.c — file, TSV, stream, and environment I/O
src/c/collections.c — sets, arrays, and frames
src/c/indexing.c — polymorphic element access: @i/!i and their fused forms, over arrays/segments/matrices
src/c/matrix.c — matrix words and numeric kernels
src/c/statistics.c — statistics kernels: var, quantile, kendall's tau-b
src/c/dimension.c — dimensioned quantities: base dimensions, units, quantity arithmetic
src/c/functional.c — higher-order operations (map, nmap, …) and multi-core parallelism
src/c/superwords.c — compile-time instruction fusion (superwords)
src/c/strings.c — string and PCRE2 regex operations
src/c/logic.c — logic variables, unification, amb, fact database
src/c/database.c — SQLite integration
src/c/foreign.c — FFI (libffi), pointer registry, matrix/segment bridges
src/c/platform_posix.c — POSIX platform: arena mmap, isocline REPL, subprocesses
src/c/platform_wasi.c — WASI platform: allocator + erroring stubs for FFI/subprocess
src/c/help_table.c — generated help/man text (from docs/reference.md)
src/forth/*.telic — standard library (concatenated in Makefile order, embedded)
lib/ — loadable libraries: statistics.telic, plot.telic, http.telic, claude.telic, mcp.telic
external/ — vendored deps: pcre2, sqlite, isocline, lapacke
tests/ — golden-output test files
bench/ — benchmark suite (Telic vs CPython) and inventory
docs/ — the word reference (reference.md, reference-libraries.md), idioms.md,
and the primers: continuations, logic
PLAN.md — future work
See LICENSE.