Epsil for AI Agents
A condensed reference for language models and coding agents writing Epsil.
Every code fence on this page is executed by the test suite and its // ➔
output verified — the examples cannot drift from the implementation. Epsil is
experimental: syntax and semantics may change between releases.
What Epsil Is
Epsil is a programming language for scientific computing built on the Compute
Engine. It is symbolic and exact by default: 1/3 is the rational one
third, not 0.333…, and Ln(2) or Sqrt(2) stay symbolic. Ask for a decimal
explicitly with N(expr). A program is a sequence of statements (separated by
newlines or ;); its result is the value of the last statement. There is
no print — produce the value you want as the final statement. Runtime
problems (a const reassignment, a type mismatch) become ordinary
Error(...) values, not thrown exceptions; malformed source produces
diagnostics with source locations.
Run it: npx epsil file.epsil, npx epsil --eval 'expr', stdin, or a REPL
(npx epsil). Diagnostics go to stderr; exit code 0 = success, 1 = error.
Validate without evaluating: npx epsil check file.epsil --json emits
structured diagnostics (positions, fix-its). Look up the library:
npx epsil doc Mean, or search by concept — npx epsil doc "standard deviation". Add --diagnostics json to a run for machine-readable runtime
diagnostics. Embed via executeEpsil(ce, source) from
@cortex-js/compute-engine/epsil. See CLI.
Naming convention: Capitalized names are library operators (Sin,
Map, Simplify); lowercase names are your variables and functions.
Calling an unknown function is not an error — the call stays symbolic (a
warning diagnostic with a did-you-mean suggestion fires when a close library
name exists, e.g. len → Length).
Core Syntax
let x = 5 // mutable declaration
const tau = 6.28 // immutable; reassigning yields an Error value
x = x + 3 // assignment: a bare `=` assigns only as a STATEMENT
f(x) = x^2 // function definition, math style
square = x => x^2 // anonymous function ("=>" is the lambda arrow)
cube : (x: number) -> number = x^3 // a named function-type annotation binds x
function g(n) { // function definition, block style
let t = n + 1 // blocks are lexically scoped
t * 2 // a block's value is its last expression
}
hold h(e) = Head(e) // hold: arguments arrive UNEVALUATED (h(x + 1) ➔ Add)
hold mySum(body, bind i, n) = Sum(body, (i, 1, n)) // bind: a bound-variable slot; mySum(k^2, k, 3) ➔ 14
function op(a, b) commutative associative -> number { a + b } // algebraic words in the specifier slot
/// A doc comment right before a definition is its description (About, hover)
let parity = "even" if x % 2 == 0 else "odd" // conditional expression; if is also an expression: if c { a } else { b }
g(x) + f(2)
// ➔ 22
- Comments:
// lineand/* block */. NOT#(that starts a pragma). - Statements: one per line, or separated by
;. Two expressions on one line with no separator is a diagnostic. A line ending in an infix operator continues onto the next line. - Whitespace rule: an infix operator has spaces on both sides or neither
(
a + bora+b;a +bis a diagnostic). Prefix-x/!xand postfixn!must touch their operand. - Types (optional) use the Compute Engine type language:
let n: integer = 4,f(x: real) -> real = x^2. Parameter types are enforced at call time. - Collections: list
[1, 2, 3], set{1, 2, 3}, tuple(1, 2), dictionary{one -> 1, two -> 2}, empty dictionary{->}({}is the empty set). Access dictionaries withd["key"]; identifier-shaped keys also have the shorthandd.key. Tuples index like lists (p[1]is the first component); a matrix (list of lists) indexes asm[2, 1]orm[2][1]. - Spread: in a call argument list,
...tsplices a tuple's elements in as positional arguments (f(...p),Max(...t),g(1, ...p, ...q)). Tuples only — spreading a list is anincompatible-typeerror — and...is valid nowhere else. - Destructuring:
let (q, r) = divmod(17, 5)binds a tuple's components (constmakes them constants;_skips a position; patterns nest). Tuples only, ≥ 2 elements, initializer required; a shape mismatch is an Error value. For conditional destructuring usematch. The same pattern assigns to EXISTING bindings with:=, evaluating the right side once before it writes anything, so(a, b) := (b, a)swaps. It must be:=— a statement-leading(a, b) = …is a comparison, and is diagnosed. - Block in expression position:
do { … }(a bare{ … }in expression position is always a set/dictionary literal). - LaTeX islands:
$\frac{1}{2}$splices parsed LaTeX into the expression (available in the CLI and any host that injects a LaTeX parser).
Operator precedence, loosest → tightest: := · => · ?? (coalesce) ·
|> (pipe) · -> (key-value) · a if c else b (conditional) · || · && ·
comparisons
== != < <= > >= === in !in is (chainable: 1 < 2 < 3) · .. (range) ·
+ - · * / % · unary - ! · ^/** (right-associative) · postfix !.
Calls f(x) and indexing xs[i] bind tightest of all. A bare = has no
fixed tier: it binds like := when it assigns and like == when it compares.
If You Know Python or JavaScript
Epsil deliberately diverges from these reflexes. Wrong-by-instinct → what actually happens → write instead:
| Reflex | What happens in Epsil | Write instead |
|---|---|---|
xs[0] for first element | Silently yields NaN — indexing is 1-based | xs[1]; negative indices work: xs[-1] is the last element |
7 // 2 floor division | Silent wrong value: // starts a comment, so this is just 7 | Floor(7 / 2) |
7 / 2 integer division | Exact rational 7/2, not 3 or 3.5 | Floor(7 / 2) for 3; N(7 / 2) for 3.5 |
range(1, 5) excludes end | Inert call + did-you-mean; Range(1, 5) includes 5: [1,2,3,4,5] | Range(1, n) or 1..n for 1…n inclusive |
x = 5 at top level | Assigns — = assigns only as a whole statement with a name on the left | x == 5 for the equation |
# comment | Diagnostic (# introduces pragmas) | // comment or /* … */ |
def f(x): / (x) => … / lambda x: … | Parse diagnostics; (x) -> … is recovered with a did-you-mean-=> fixit | f(x) = expr, x => expr, or function f(x) { … } |
cond ? a : b | Parse diagnostic | a if cond else b, or if cond { a } else { b } — both are expressions |
elif | Parse diagnostic | else if |
return | Reserved word, not implemented | A block's value is its last expression |
break / continue | Work as expected inside a while/for body; the loop context resets at every function and lambda boundary | (nothing to change) |
print(x) | Inert unknown call; nothing prints | The program's value is its last statement |
len(xs) | Inert + did-you-mean | Length(xs) |
s[0] / len(s) on a string | Works — a string is a collection of its characters (grapheme clusters), 1-based | s[1], Length(s) |
"a" + "b" | Error values inside an Add | "\(a) and \(b)" interpolation, or Join(a, b) |
xs[2] = 9 | Runtime error value — no element assignment; collections are immutable values | Rebuild: Map, Join(xs, [v]), Append(xs, v) |
and / or / not | Parse diagnostics (reserved words) | &&, ||, ! |
x**0.5 habits: x^1/2 | Parses as (x^1)/2 — precedence, not a root | Sqrt(x) or x^(1/2) |
math.floor, np.mean | No modules/namespaces | Everything is global: Floor, Mean, Sin, … |
for loop building a value | Loops are for effect; their value is Nothing | Accumulate into a let, or use Map/Filter/Reduce |
| f-strings / template literals | Backtick is the verbatim-symbol quote; ${} invalid | "x is \(x)" works in any string |
Comfortable habits that do transfer: ** is an accepted alias of ^
(both right-associative, 2^3^2 → 512); % is Mod with the sign
convention of Python (-7 % 3 → 2); xs[-1] is the last element; 0.1 + 0.2 == 0.3 is True (decimal arithmetic); chained comparisons 1 < 2 < 3
work; 2 in [1, 2, 3] works; lowercase true/false are accepted
(canonically True/False).
Verified Idioms
Exactness and numeric approximation:
let exact = 1/3 + 1/6 // stays the exact rational 1/2
let sym = Sqrt(2) * Sqrt(2) // symbolic radicals reduce exactly
"\(exact), \(sym), \(N(Pi, 10))"
// ➔ "1/2, 2, 3.141592654"
Functions, recursion (self-reference works in a one-step definition, with
any number of recursive calls — fib(n-1) + fib(n-2) is fine), and closures:
fact(n) = 1 if n <= 1 else n * fact(n - 1)
makeAdder(k) = x => x + k // closures capture lexically
let add10 = makeAdder(10)
add10(fact(5))
// ➔ 130
Collections pipeline — Map/Filter/Reduce for value-producing iteration,
|> to chain; 1..n is an inclusive range:
1..10 |> Filter(_, k => k % 2 == 0) |> Map(k => k^2, _)
// ➔ [4, 16, 36, 64, 100]
Reduce([1, 2, 3, 4], (acc, x) => acc + x, 0) + Sum(1..100)
// ➔ 5060
Loops are for effect — accumulate into a variable declared outside:
let a = 1071
let b = 462
while b != 0 {
let t = a % b
a = b
b = t
}
a
// ➔ 21
Building a list in a loop (each [k] literal snapshots the current value):
let xs = []
for k in 1..3 { xs = Join(xs, [k * k]) }
xs
// ➔ [1, 4, 9]
Structural match (an expression; _ is the wildcard; a bare name binds
— use == expr to compare against a value):
classify(n) = match n {
0 => "zero"
k if k > 0 => "positive"
_ => "negative"
}
classify(-5)
// ➔ "negative"
Symbolic computation:
let poly = Simplify(2 + 3x^3 + 2x^2 + x^3 + 1)
let roots = Solve(x^2 + x - 6 == 0, x)
let deriv = D(x^3 + x, x)
let area = Integrate(Sin(x), (x, 0, Pi))
(poly, roots, deriv, area)
// ➔ (4x^3 + 2x^2 + 3, [2,-3], 3x^2 + 1, 2)
Lists, slices, and common operators (all indexing is 1-based):
let xs = [10, 20, 30, 40]
(xs[1..2], First(xs), Last(xs), Sort([3, 1, 2]), IndexOf(xs, 30))
// ➔ ([10,20], 10, 40, [1,2,3], 3)
Dictionaries (string keys; dot access is shorthand for identifier-shaped keys):
let d = {one -> 1, two -> 2}
(d.two, d["two"], IsMissing(d.missing), Coalesce(d.missing, 0))
// ➔ (2, 2, True, 0)
An absent numeric field evaluates to NaN; an absent nonnumeric field remains
Missing. IsMissing recognizes both forms.
Library Quick Roster
Verified operator names, so you don't have to guess (search for more with
epsil doc <keywords>):
- Numbers:
Abs,Floor,Ceil(notCeiling),Round,Sqrt,Max,Min(each takes a list or varargs),Mod,GCD,LCM,IsPrime,Random(a..b). - Lists:
Length,First,Last,Rest,Take,Drop,Reverse,Sort(optional comparator — see below),IndexOf,Join,Append,Sum,Mean,StandardDeviation(sample, n−1),Map,Filter,Count(xs)/Count(xs, v)/Count(xs, pred),Reduce(list, f, init),Range(a, b)inclusive,Range(a, b, step). - Strings:
Characters,StringSplit(s)(splits on whitespace by default),String(x),Join(a, b)to concatenate strings,StringJoin(xs, sep?)to join ONE collection with an optional separator (a string subject means its characters, soStringJoin("ab", "cd")is"acdb", not"abcd"— useJoinor"\(a)\(b)"to concatenate). Substring search isRangeOf(s, needle)(a span, orNothing),ContainsSequence,StartsWith,EndsWith—c in sis character membership. AlsoStringReplace(s, target, replacement, count?),Trim/TrimStart/TrimEnd,StringRepeat,PadStart/PadEnd,ToUpperCase/ToLowerCase/CaseFold,StringCompare(a, b)(-1/0/1, code-point order) andNumberFrom(s, base?). - Dictionaries:
Keys,Values. - Absence:
Missingpreserves a missing position;Nothingis omitted from arguments and collections;IsMissing,Coalesce. - Symbolic:
Simplify,HoldValues(body)(evaluatebodywith its assigned symbols kept symbolic),Solve(eq == v, x),D(expr, x),Derivative(f),Integrate,N,Type,IsError(x)(true for an error value, or an expression carrying one).
Caution: Head and Tail exist but are structural operators
(Head([1,2,3]) is the operator name "List", not the first element) —
for elements use First/Rest.
Sort([3, 1, 4, 1, 5], (a, b) => a > b)
// ➔ [5,4,3,1,1]
Watch Out For
- Laziness:
Range,Map,Filter,Take,Drop,Joinare generators — they enumerate when materialized (indexed, aggregated, or iterated; e.g. aTake(xs, 3)stored inside a tuple stays an unevaluatedTake(...)), and a deferred mapping function reads variables at materialization time. Collection literals snapshot their element values immediately. To force work now, aggregate or index where you stand. - Output is the engine's textual form: strings and booleans print
quoted (
"True","florb") — that quoted"True"is a boolean, not a string. Derived collections (Range,Map/Filterresults, loop-built lists) preview-elide above 10 elements ([1,2,3,4,5,...,]); the value is complete — the CLI's--jsonoutput materializes the full elements (up to 10,000). Literals print in full. - Arguments are evaluated before a call —
f(a + 1)receives the value — except for aholdfunction (hold f(e) = …), which receives the expression as written and evaluates it wherever the body reads it (call-by-name:hold twice(e) = e + eevaluatesetwice;let v = eonce). Every parameter of a hold function is held; there is no per-parameter form. - Binder variables stay symbolic:
D(expr, x)andIntegrate(expr, x)treatxsymbolically even ifxhas an assigned value; the result then evaluates with the value. Solet x = 2followed byN(D(x^3 + x, x))is13— the derivative is taken first. - Interpolating a collection broadcasts:
"\(expr)"with a list-valuedexprmaps the string over the elements, yielding a list of strings ("n = \([1, 2])"→["n = 1", "n = 2"]), not one string containing the list. Interpolate scalars only. - Only the last statement's value is returned. An error value in an
earlier statement also emits a
runtime-errordiagnostic so it can't vanish silently. - Boolean inference is sticky: using a bare undeclared symbol as a
boolean operand (
&&/||/!) types itbooleanfor the engine's lifetime; a later numeric use of the same symbol errors. 3!^2is a diagnostic — the lexer reads!^as one operator token. Space it:3! ^ 2.matchbinds bare names:match x { Pi => … }does not compare with π — it binds a new variable namedPi. Pin values with==:match x { == Pi => … }.
For the full reference start at Epsil, the complete grammar in Syntax, and ~70 more verified programs in Examples.