Axis is the expression-oriented, strongly-typed scripting language used by the Atlas software. This page is the complete language reference, converted from atlas.help.
The atlas program provides access to the Atlas library through an expression-oriented interface. Rather than entering a dialogue to set parameters (as in the older Fokko program), all parameters are values that can be computed, stored in variables, and passed to functions.
The language is called Axis. It is a general-purpose programming language developed within the Atlas project. The Atlas-specific types and functions are built on top of it.
Scripts are loaded with:
atlas> <script.at
This loads the file if not already loaded. Scripts in the standard atlas-scripts directory can be referenced by name alone.
The older Fokko program enters a dialogue whenever a parameter must be set. In atlas, such parameters are constructed as expressions:
atlas> set ic = inner_class("B5.A3.T1", [[1,0,0]/2,[0,1,1]/2], "scc") Identifier ic: InnerClass atlas> block_sizes(ic) Value: | 0, 0, 0, 0, 0, 0, 0, 1 | | 3, 75, 390, 21,525,2730,4026,28182 |
The variable ic is then reusable. One could equally pass the inner class directly without storing it. This flexibility — multiple reusable variables, programming constructs, scriptability — is the fundamental difference from Fokko.
Axis is expression-based: text entered designates values to be computed. The design is close to functional programming:
rec_fun for recursion.Axis is strictly typed: expressions are type-checked before evaluation. Most errors are caught early with specific messages. Types are almost always inferred — you only need to specify types for function arguments.
Names are introduced with set (permanent) or let ... in (local). Assignment uses :=; the type of a variable cannot change after introduction.
quit set let in begin end if then else elif fi and or not next do dont from downto while for od case esac rec_fun true false die break return set_type whattype showall forget
void int rat string bool vec mat ratvec LieType RootDatum WeylElt InnerClass RealForm CartanClass KGBElt Block Split KType KTypePol Param ParamPol
| Type | Represents |
|---|---|
| bool | Truth values: true or false |
| int | Unbounded-precision integers |
| rat | Rational numbers (reduced fraction of two unbounded integers) |
| string | Character string (double-quoted; cannot span lines) |
| vec | Vector of 32-bit integers |
| mat | Matrix of 32-bit integers (column-major) |
| ratvec | Rational vector: 64-bit integer numerator vector, integer denominator |
| LieType | Lie type such as D4.A2.T1 |
| RootDatum | Root datum specifying a connected complex reductive group |
| WeylElt | Element of the Weyl group associated to a root datum |
| InnerClass | Inner class of real forms (root datum with involution) |
| RealForm | Real form within an inner class |
| CartanClass | Class of Cartan subgroups within an inner class |
| KGBElt | Element of \(K \backslash G/B\) for some real form |
| Block | Block for a pair of real forms at dual inner classes |
| Split | Split integer \(a + b \cdot s\) where \(s^2 = 1\) |
| KType | Standard \(K\)-representation or its irreducible quotient |
| KTypePol | Split-linear combination of final \(K\)-types |
| Param | Standard module or its irreducible quotient |
| ParamPol | Virtual module: Param values with Split coefficients |
Type errors are caught before evaluation:
atlas> block_sizes(GL(5)) Type error: found RootDatum while InnerClass was needed.
To inspect overloads of a name:
atlas> whattype Lie_type ? Overloaded instances of Lie_type string->LieType RootDatum->LieType (int,int)->LieType
| Notation | Meaning |
|---|---|
| [t] | Array of elements of type t |
| (t0,t1) | 2-tuple with components of types t0 and t1 |
| (t0|t1) | Union: value is either of type t0 or of type t1 |
| void | 0-tuple (no useful value; result type of print functions) |
| (t0->t1) | Function from t0 to t1 |
| (t0,t1->t2) | Function taking (t0,t1) returning t2 |
| (->t0) | Function with no arguments returning t0 |
Three equivalent syntaxes:
f(x) or f(x,y,z)x.f — useful for chaining: x.f.g.h is h(g(f(x)))x + y*z — infix for operator symbolsMixed forms also work: f(x,y).g and x.f(y) (the latter calls f(x), which must return a function, then calls that with y).
Array displays: [1,-4,18]. Individual element: v[i] (0-indexed). Counting from end: v~[0] (last), v~[1] (second-to-last), etc.
Slices use a colon. An index specifies a boundary between elements, so v[a:b] contains elements at positions a through b-1:
v[:3] { first 3 elements } v[3:] { all except first 3 } v[2~:] { last 2 elements } v[:2~] { all except last 2 } v~[:] { all elements reversed } "reverse!"~[:] { string reversal: "!esrever" }
Matrix entries: A[i,j] (row, column). Matrix column slicing works like vector slicing. For complex matrix operations (transposition, block extraction, etc.) see swiss_matrix_knife in the built-in reference.
ParamPol acts as an associative array: P[q] returns the Split coefficient of the Param value q in P.
Tuple displays use parentheses: (a,b,c). Unlike arrays, components may have different types.
Pattern matching decomposes tuples when binding names:
atlas> set (q,r) = 22 \% 7 { Euclidean division: q=quotient, r=remainder } Identifiers q: int, r: int atlas> q Value: 3
Skip unwanted components with a blank: let (x,) = z in x takes only the first component of a pair. Pattern matching is available in set, let, and function parameter lists.
When types do not match exactly, Axis inserts implicit conversions automatically — but only when the target type is unambiguous from context.
| int | → | rat (denominator 1) |
| vec | ↔ | [int] (32-bit pack/unpack) |
| ratvec | ↔ | [rat] |
| vec | → | ratvec (denominator 1) |
| mat | ↔ | [vec] (columns) |
| mat | ↔ | [[int]] (columns) |
| string | → | LieType |
| int | → | Split (as (k,0)) |
| InnerClass | → | RootDatum (inheritance) |
| RealForm | → | InnerClass (inheritance) |
| RealForm | → | RootDatum (inheritance) |
| Param | → | ParamPol (convert to final params) |
| Param | → | KType (\(\nu\) component forgotten) |
| KType | → | KTypePol |
A cast forces a specific type by prefixing with type::
atlas> ratvec: [1, 1/2, 1/3, 1/4] Value: [ 12, 6, 4, 3 ]/12 atlas> mat: [[1,2,4],[3,5,7]] Value: | 1, 3 | | 2, 5 | | 4, 7 |
Specify a matrix by rows with |:
atlas> [ 1,2,4 | 3,5,7 | 0,-1,6 ]
The empty list [] always needs a cast: [ratvec]:[].
Global identifiers: set name = expr. Local identifiers: let name = expr in body. Assignment: name := expr. Type cannot change.
atlas> set initial (int n) = let i=0 in while i<n do i:=i+1 od Defined initial: (int->[int])
Compound assignment: x +:= y means x := x+y, and similarly for any operator. Component assignments: v[i]:=x, m[i,j]:=x.
Prefix a name with ! to forbid assignment (constant binding):
set !s = Split:(0,1)
Two names never share an object. Assigning to one name never affects another. Function calls are strictly by value. let vi=v[i] in vi:=3 does not modify v[i].
New type abbreviations with field names use set_type:
atlas> set_type Gaussian_int = (int re, int im)
This defines re and im as projector functions and enables field assignment z.re:=4. For union types, tags become injector functions:
atlas> set_type Maybe<T> = (void none | T some)
General type definitions (in square brackets) allow mutually recursive types:
atlas> set_type [ tree=(void nil|tree_node node),
tree_node=(tree left,int key,tree right) ]
Remove a type definition with forget tree.
atlas> set f (LieType lt) = void: : > let rd=simply_connected(lt) L > then ic=inner_class(rd,"s") L > then rf=quasisplit_form(ic), drf=dual_quasisplit_form(ic) L > in print_block(block(rf,drf)) Defined f: (LieType->)
The void: cast clarifies intent but is not required — Axis infers return types. The multi-line prompt changes: : > after =, L > inside let. New operators are defined identically.
All function names are stored in an overload table. Multiple definitions with different argument types coexist; Axis selects among them by argument type at each call site:
atlas> set f(int n) = n+1 Added definition [2] of f: (int->int)
If argument types exactly match an existing definition, it is replaced:
Redefined inner_class: (RootDatum,string->InnerClass)
Overloading is refused when two argument types would create ambiguous calls. Use forget name@type to remove a specific overload before adding the new one.
Write a parenthesised parameter list, optional return type, colon, and body:
atlas> g( (int n): n+1 ) Value: [ 1, 3, 6 ]
To use a function name as a first-class value, append @type:
atlas> g(f@int) { pass the (int->int) overload of f as a value } atlas> +@(int,int) { integer addition as a value }
To store a function value in a global variable (separate from overloads), use name : function:
atlas> f : (int n): 2*n+3 Identifier f: (int->int)
An identifier declared with name : type (no value yet) introduces an uninitialised variable of that type — useful when mutually dependent functions need to reference each other before both are defined.
Semicolons evaluate left-to-right and return the last value. next returns the first value:
let i=0 in i next i:=3 { returns 0 } a:=1; b:=2; a+b { returns 3 }
begin ... end is equivalent to parentheses for grouping long sequences.
if condition then A else B fi
if condition then A elif ... then ... else B fi
if condition else B then A fi { same as first form — puts minor case first }
The else branch may be omitted (defaults to void). Logical connectives and, or, not short-circuit and cannot be overloaded or redefined.
case I
in E_0, E_1, ..., E_{n-1}
else F { optional: selected when I < 0 or I >= n }
esac
Without else, out-of-range values wrap modulo \(n\). With both then and else, negative values select then and too-large values select else. Useful idiom for sign testing:
case k then F_neg in F_zero else F_pos esac
Process a union-type value by handling each variant. Tags come from a set_type definition:
case solve(system)
| none: error("no solution")
| solution(v,d,M): v/d { particular rational solution }
esac
An else branch handles any unspecified variants. Unlike integer case, branches are not separate function bodies, so break and return can cross variant boundaries.
Loops collect body values into a list; if the result is unused it is not constructed (no overhead):
let i=0 in while i<n do i:=i+1 od { [1,2,...,n] } let i=0 in while i<n do i next i:=i+1 od { [0,1,...,n-1] }
Variables from let in the condition stay in scope in the body. Euclid's algorithm elegantly:
while let r=a%b in r!=0 do a:=b; b:=r od; b
A condition true immediately before do may be omitted. Use dont for a branch that always terminates without contributing to the list (equivalent to false do die).
for i : n do i od { [0,1,...,n-1] } for i : n from 1 do i od { [1,2,...,n] } for i : n ~ do i od { [n-1,...,1,0] (~ reverses traversal) } for :n do 1 od { n ones (index omitted) }
~ before do reverses the traversal; ~ before od reverses assembly of results. The two can be combined.
for e in V do ... od { iterate over elements of array/vec/string/mat } for e@i in V do ... od { with 0-based index i available } for c@p in pol do ... od { over nonzero terms of a ParamPol }
For a ParamPol, c is the Split coefficient and p is the Param value at each nonzero term. For matrices, iteration is over columns.
Apply .## after the outer od to flatten nested results into a single list:
for i:4 do for j:i+1 do (i,j) od od.##
{ [(0,0),(1,0),(1,1),(2,0),(2,1),(2,2),(3,0),(3,1),(3,2),(3,3)] }
Syntactic sugar: omit the inner do, or use if ... do ... fi as a Boolean-counted loop:
for i:4 for j:i+1 do (i,j) od od { same as above } for d in list if d%2=1 do d fi od { keep only odd elements } for i:9 for j:10 if gcd(i,j)=1 do (i,j) fi od od { coprime pairs }
break exits the innermost loop without including the current iteration in the result. Repeat for outer loops: break break breaks from two levels.
return expr exits the innermost user-defined function, returning expr.
die and error(message) terminate evaluation with a runtime error. Use error to provide a diagnostic message; die is for branches that should be unreachable.
Use rec_fun before the function name. The return type must be specified explicitly:
atlas> set rec_fun fac (int n) = int: : > if n<=0 then 1 else n*fac(n-1) fi Defined fac: (int->int)
Better practice: keep the recursive function local, with the global function handling preconditions:
set fac (int n) =
if n<0 then error("factorial of negative number ",n) fi;
let rec_fun f(int n) = int: if n<=0 then 1 else n*f(n-1) fi
in f(n)
For maximum efficiency (build the recursive function only once, not on every call):
set fac =
let rec_fun f(int n) = int: if n<=0 then 1 else n*f(n-1) fi
in (int n): if n<0 then error("negative") else f(n) fi
A command continues to the next line automatically when:
(( >)let, if, while, for, begin) has not been closed yet:, :=, ,, ;, or inA backslash \ as the last non-space character forces continuation by joining the next line (even within a string or token).
Redirect output to a file for the duration of one command:
atlas> > output.txt block_sizes(ic) { create/overwrite } atlas> >>output.txt print_KL_list(rf,drf) { append }
Read commands from a file:
atlas> <filename.at { load; skips if already loaded } atlas> <<filename.at { load unconditionally (useful after editing) }
Files can include other files. Any error closes all open input files and returns control to the terminal. Atlas tracks loaded files and suppresses re-loading (use << to force a reload after editing).
The file search path is given by the input_path variable ([string]), settable from the command line with --path=dir.
atlas> set verbose { enable extra diagnostics during type analysis } atlas> set quiet { disable extra diagnostics } atlas> showall { print all defined identifiers and functions } atlas> whattype expr { type-check expr, print type (no evaluation) } atlas> whattype + ? { print all overloads of + } atlas> forget name { remove a global identifier } atlas> forget f@(int,int) { remove a specific overload } atlas> quit { exit the interpreter }
Every construct in Axis is an expression — there is no statement/expression distinction. Expressions yield a value (possibly void) and may have side effects.
| or | Logical or (short-circuit, not overloadable) |
| and | Logical and (short-circuit, not overloadable) |
| not | Logical not (prefix only) |
| = != < > <= >= | Relational operators |
| + - | Addition, subtraction |
| * / \ % \% | Multiplication, division, integer quotient, remainder |
| ^ | Exponentiation (right-associative) |
| # ~ | Length / append; complement / reverse |
Association is left-to-right except ^ (right-to-left). Some parsing examples:
-x+y { (-x)+y } -a%p { -(a%p) } a+-b^c { a+((-b)^c) } a^-1 { a^(-1) }
if * then * fi { abbreviates: if * then * else () fi } if * then * else * fi if * then * elif * then ... else * fi if * else * then * fi { reversed branch order } case * in *,*,... esac { integer case } case * in *,*,... else * esac { with default } case * | tag(p): * | ... esac { union discrimination } case * | (p).tag: * | ... | else * esac { with default branch } while * do * od for pattern in * do * od for pattern@ident in * do * od for ident : * do * od for ident : * from * do * od if * do * fi { Boolean-counted loop } ( * ) { parenthesised expression } begin * end { same as ( * ) } ( *,*,... ) { tuple display } [ *,... ] { row (array) display } [ *,... | *,... | ... ] { matrix by rows }