Skip to main content

Types

Cortex does not have its own type system: it reuses the Compute Engine's type language, the same syntax accepted by ce.declare("f", "(real) -> real"). See the Compute Engine type guide for the type language itself. This page covers where a type annotation is written in Cortex source and what it means; the type grammar includes unions, intersections, tuples, records, function signatures, and generic collection types.

Annotation positions

A type annotation follows a : after a declaration target:

x: real
x: real = 5

Type-syntax tokens — <, >, ->, |, & — are only meaningful inside a type annotation. They are never part of the general expression grammar: once the parser sees a leading symbol :, it hands the rest of the type expression to the type subparser and resumes parsing Cortex source exactly where the type subparser stopped. An unrelated : that doesn't follow a declaration target at the start of a statement is not treated as an annotation at all.

Function parameters and return values can also be annotated:

f(x: real, n: integer) -> real = x^n
function g(x: integer) -> integer { x + 1 }
(x: integer) |-> x + 1

Parameter annotations are enforced when a function is called. A return-type annotation is recorded in the function's signature, but the current runtime does not reject a returned value merely because its inferred type differs from the annotation.

MathJSON representation

The parser holds a type annotation as a MathJSON string. A declaration places that string after the declared symbol. An initializer is stored in the declaration's attributes dictionary:

x: real = 5
["Declare", "x", {"str": "real"},
["Dictionary", ["KeyValuePair", "value", 5]]]
xs: list<integer>
["Declare", "xs", {"str": "list<integer>"}]
f: (real) -> real
["Declare", "f", {"str": "(real) -> real"}]

Note that <, >, |, &, and -> inside the type annotation are consumed entirely by the type subparser — for example u: integer | boolean holds the whole "integer | boolean" string, and none of those tokens are visible to (or reinterpreted by) the surrounding expression grammar.

Semantics

An annotation uses the same engine type machinery as ce.declare(). Type checking is not a separate Cortex-side pass — it happens at canonicalization/evaluation time, the same way it does for any other declared symbol. Cortex does not add a second type checker on top of the engine's.

Typed parameters are represented with Typed nodes:

f(x: integer) -> real = x + 1
["Assign", "f",
["Function",
["Typed", ["Add", "x", 1], {"str": "real"}],
["Typed", "x", {"str": "integer"}]]]

Inference

A symbol with no annotation gets its type inferred by the engine from how it is used — the same inference the engine already performs for any undeclared symbol. This includes the engine's existing convention that evaluating a bare symbol as a boolean operand (And/Or/Xor/Not) infers that symbol boolean for the lifetime of the engine; a later numeric use of the same symbol in the same scope will then error. This is engine behavior, not something specific to Cortex.

Diagnostics

An invalid type inside an annotation position surfaces as a type-annotation-error diagnostic, offset-corrected to point at the offending token within the type text (not at the : or the declaration target):

x: notatype

produces a type-annotation-error diagnostic pointing at notatype.