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, and an SVG plotting library. 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 dgemm (αAB + βC)
[ 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. 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. - 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). - DGEMM —
dgemm-nn/tn/nt/tt(αAB + βC) for all four transpose variants, each with a loop order that keeps the inner loop unit-stride; a single-column B takes a matrix-vector path instead. - 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 —
correlation-pearson,correlation-spearman,correlation-kendall(tau-b);correlate-withbootstraps a confidence interval for any of them,cordoes it with kendall in one word, andqnormis the standard normal quantile. - Regression trees —
fit-treegrows a CART regression tree over a features frame and a numeric response, taking numeric and categorical columns and missing values as they come;predictapplies one,feature-importanceranks the features,pruneandprune-cvcost-complexity-prune it,draw-treeprints it, andlib/plot.telic'splot-treedraws it. - 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,sin,cos,tan,tanh,asin,acos,atan,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 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).
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. - 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.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,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.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 —
svdandfit-linearon LAPACK through the FFI; loading the library also rebindsdgemm-*to BLAS and addsdgemv-n/dgemv-tfor matrix-vector products. - 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/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,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, claude.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, regression
PLAN.md — future work
See LICENSE.