Skip to main content

Cortex for AI Agents

A condensed reference for language models and coding agents writing Cortex. Every code fence on this page is executed by the test suite and its // ➔ output verified — the examples cannot drift from the implementation. Cortex is experimental: syntax and semantics may change between releases.

What Cortex Is

Cortex 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 cortex file.cx, npx cortex --eval 'expr', stdin, or a REPL (npx cortex). Diagnostics go to stderr; exit code 0 = success, 1 = error. Validate without evaluating: npx cortex check file.cx --json emits structured diagnostics (positions, fix-its). Look up the library: npx cortex doc Mean, or search by concept — npx cortex doc "standard deviation". Add --diagnostics json to a run for machine-readable runtime diagnostics. Embed via executeCortex(ce, source) from @cortex-js/compute-engine/cortex. 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. lenLength).

Core Syntax

let x = 5 // mutable declaration
const tau = 6.28 // immutable; reassigning yields an Error value
x = x + 3 // assignment (= is Assign, NOT equality)
f(x) = x^2 // function definition, math style
square = x |-> x^2 // anonymous function ("|->" is the lambda arrow)
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
}
let parity = if x % 2 == 0 { "even" } else { "odd" } // if is an EXPRESSION
g(x) + f(2)
// ➔ 22
  • Comments: // line and /* 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 + b or a+b; a +b is a diagnostic). Prefix -x/!x and postfix n! 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 with d["key"], not d.key. Tuples index like lists (p[1] is the first component); a matrix (list of lists) indexes as m[2, 1] or m[2][1].
  • Spread: in a call argument list, ...t splices a tuple's elements in as positional arguments (f(...p), Max(...t), g(1, ...p, ...q)). Tuples only — spreading a list is an incompatible-type error — and ... is valid nowhere else.
  • Destructuring: let (q, r) = divmod(17, 5) binds a tuple's components (const makes them constants; _ skips a position; patterns nest). Tuples only, ≥ 2 elements, initializer required; a shape mismatch is an Error value. For conditional destructuring use match.
  • 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: = · |-> · |> (pipe) · -> (key-value) · || · && · comparisons == != < <= > >= === in !in (chainable: 1 < 2 < 3) · .. (range) · + - · * / % · unary - ! · ^/** (right-associative) · postfix !. Calls f(x) and indexing xs[i] bind tightest of all.

If You Know Python or JavaScript

Cortex deliberately diverges from these reflexes. Wrong-by-instinct → what actually happens → write instead:

ReflexWhat happens in CortexWrite instead
xs[0] for first elementSilently yields NaN — indexing is 1-basedxs[1]; negative indices work: xs[-1] is the last element
7 // 2 floor divisionSilent wrong value: // starts a comment, so this is just 7Floor(7 / 2)
7 / 2 integer divisionExact rational 7/2, not 3 or 3.5Floor(7 / 2) for 3; N(7 / 2) for 3.5
range(1, 5) excludes endInert call + did-you-mean; Range(1, 5) includes 5: [1,2,3,4,5]Range(1, n) or 1..n for 1…n inclusive
Solve(x^2 = 4, x)Silently []= is assignmentSolve(x^2 == 4, x)[2, -2]
# commentDiagnostic (# introduces pragmas)// comment or /* … */
def f(x): / (x) => … / lambda x: …Parse diagnosticsf(x) = expr, x |-> expr, or function f(x) { … }
cond ? a : bParse diagnosticif cond { a } else { b }if is an expression
elifParse diagnosticelse if
return / break / continueReserved words, not implementedwhile true { … break } runs to the iteration limitLast expression is the value; loop on a condition instead of breaking
print(x)Inert unknown call; nothing printsThe program's value is its last statement
len(xs)Inert + did-you-meanLength(xs)
s[i] / len(s) on a stringError value / inert — strings are not collectionsCharacters(s)[i], Length(Characters(s))
"a" + "b"Error values inside an Add"\(a) and \(b)" interpolation, or StringJoin(a, b)
xs[2] = 9Runtime error value — no element assignment; collections are immutable valuesRebuild: Map, Join(xs, [v]), Append(xs, v)
and / or / notParse diagnostics (reserved words)&&, ||, !
x**0.5 habits: x^1/2Parses as (x^1)/2 — precedence, not a rootSqrt(x) or x^(1/2)
math.floor, np.meanNo modules/namespacesEverything is global: Floor, Mean, Sin, …
for loop building a valueLoops are for effect; their value is NothingAccumulate into a let, or use Map/Filter/Reduce
f-strings / template literalsBacktick 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^2512); % is Mod with the sign convention of Python (-7 % 32); 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) = if n <= 1 { 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; a missing key silently yields NaN):

let d = {one -> 1, two -> 2}
(d["two"], Keys(d))
// ➔ (2, ["one","two"])

Library Quick Roster

Verified operator names, so you don't have to guess (search for more with cortex doc <keywords>):

  • Numbers: Abs, Floor, Ceil (not Ceiling), Round, Sqrt, Max, Min (each takes a list or varargs), Mod, GCD, LCM, IsPrime, RandomInteger(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, Reduce(list, f, init), Range(a, b) inclusive, Range(a, b, step).
  • Strings: Characters, StringJoin, StringSplit(s) (splits on whitespace by default), String(x).
  • Dictionaries: Keys, Values.
  • Symbolic: Simplify, HoldValues(body) (evaluate body with its assigned symbols kept symbolic), Solve(eq == v, x), D(expr, x), Derivative(f), Integrate, N, Type.

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, Join are generators — they enumerate when materialized (indexed, aggregated, or iterated; e.g. a Take(xs, 3) stored inside a tuple stays an unevaluated Take(...)), 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", "xetroc") — that quoted "True" is a boolean, not a string. Derived collections (Range, Map/Filter results, loop-built lists) preview-elide above 10 elements ([1,2,3,4,5,...,]); the value is complete — the CLI's --json output materializes the full elements (up to 10,000). Literals print in full.
  • Binder variables stay symbolic: D(expr, x) and Integrate(expr, x) treat x symbolically even if x has an assigned value; the result then evaluates with the value. So let x = 2 followed by N(D(x^3 + x, x)) is 13 — the derivative is taken first.
  • Interpolating a collection broadcasts: "\(expr)" with a list-valued expr maps 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-error diagnostic so it can't vanish silently.
  • Boolean inference is sticky: using a bare undeclared symbol as a boolean operand (&&/||/!) types it boolean for the engine's lifetime; a later numeric use of the same symbol errors.
  • 3!^2 is a diagnostic — the lexer reads !^ as one operator token. Space it: 3! ^ 2.
  • match binds bare names: match x { Pi => … } does not compare with π — it binds a new variable named Pi. Pin values with ==: match x { == Pi => … }.

For the full reference start at Cortex, the complete grammar in Syntax, and ~70 more verified programs in Examples.