This language manual describes all the syntax rules of Axiom.

Blocks

Axiom uses indentation to denote blocks (this is known as "significant whitespace"), which is similar to Python. However, unlike Python, the only valid indentation character is tab. (This is to put an end to the tabs vs spaces debate).

Comments

Comments in Axiom are denoted by # or ##, for line comments and block comments respectively.

x = 1 #this is a line comment

y = 2
##
this is is a block comment
##

z = ## this is also a block comment ## 3

Identifiers

Identifiers in Axiom must start with an alphabetical character or underscore, and can be followed by any number of alphabetical or numeric characters or underscores.

Statements

Statements each occupy a whole line, and may be followed by an indented block. The types of statements are:

Function Statement

A function is declared using a colon (:) followed by an identifier, followed by a list of parameters in parentheses, followed by an indented block which is the function body. The list of parameters is zero or more identifiers separated by commas. It is an error to define two functions with the same name in the same scope.

#this is a function that takes 2 parameters, a and b
:some_function(a,b)
	#this is the function body
	print 'a =',a
	print 'b =',b
	print 'hello from some function!'

#this is how to call the function
some_function(1,'xyz')

If a function called :main() is defined, it is executed automatically at runtime. The :main() function can take a single parameter args, which is an array of arguments passed to the script. The first item in the array is the name of the script.

#example main
:main(args)
	print 'Received arguments:',args
##
If this script is called "main.axm" and we launch it like:
$ axiom main.axm abc 123
The output would be:
Received arguments: [ "main.axm", "abc", "123" ]
##

The :main() function definition only takes effect if it is within the root script (the script passed to the Axiom binary, not any imported scripts). This is so that each script can have its own :main() function and be imported into the same scope without errors. (The :main() function from the root script takes precedence, and any imported :main() functions are not executed.)

Class Statement

A class is declared by using an amphora (@) followed by an identifier, followed by an indented block, which contains methods and static variables. Each method implicitly receives a variable named "this", which is the class instance (object).

If a method is named "new", it is considered to be the constructor, and is implicitly called when an object is instantiated. The constructor implicitly returns "this", but can return another value to indicate an error. Declaring a constructor is optional.

If a method never references the "this" variable, it is considered a static method, and can be called on the whole class rather than a class instance.

It is an error to define two classes with the same name in the same scope.

#this is a class
@some_class

	#this is a static variable
	count = 0
	
	#this is the constructor
	:new()
		this.id = count++
	
	#this is a method
	:print_id()
		print 'id =',this.id
	
	#this is a static method
	:print_count()
		print 'There are',count,'objects total'

#this instantiates the class and calls the constructor
a = some_class()

#this is how you call a method
a.print_id()

#this is how you call a static method
some_class.print_count()

If Statement

An if statement is the keyword "if" followed by an expression, followed by an indented block. The block will be executed if the expression is true.

An if statement may be followed by any number of elif statements. An elif statement is the keyword "elif" followed by an expression, followed by an indented block. The block will be executed if the expression is true, and all previous if/elif expressions were false.

An if or elif statement may be followed by an else statement. An else statement is the keyword "else" followed by an indented block. The block will be executed if all previous if/elif expressions were false.

if age<16
	print 'You cannot do anything.'
elif age<18
	print 'You can drive, but not smoke or drink.'
elif age<21
	print 'You can drive and smoke, but not drink.'
else
	print 'You can drive, smoke, and drink!'

While Statement

A while statement is the keyword "while" followed by an expression, followed by an indented block. The block will run repeatedly as long as the expression is true.

#this will run forever
while true
	print 'going!'

#this will run 5 times
n = 5
while n--
	print 'n =',n

For Statement

A for statement is the keyword "for" followed by an identifier, followed by a comma, followed by an expression, optionally followed by another expression, then followed by an indented block. The first expression is the range, which can be a number or a collection. The second expression is the offset, which is optional but should evaluate to a number. If offset is not specified, it defaults to 0. The offset defines where to begin iteration within range.

The block runs N times, where N is range or range.len() (if collection), minus offset. If offset is negative, it is relative to range or range.len(). On each iteration, the identifier is set to a value depending on the type of range:

#print numbers 0 to 4
for n,5
	print n

#loop over elements in array
a = ['a','b','c']
for x,a
	print x

#loop over pairs in object
o = {
	x: 1,
	y: 2,
	z: 3
}
for k,o
	print k,'=',o[k]

#loop with offset (will only print 'y' and 'z')
for c,'xyz',1
	print c

Return Statement

A return statement is the keyword "return" optionally followed by an expression. This will end the execution of a function using the expression result as the return value. If no expression is specified, the return value is null. Note that a function body without a return statement will simply execute until the last line and return null.

:square(x)
	return x*x

:nothing()
	#will return null
	return

:nothing2()
	#also returns null
	pass

Continue Statement

A continue statement is simply the keyword "continue". This will exit the current loop iteration and move to the next iteration.

n = 5
while n--
	print 'hi'
	continue
	print 'bye' #this will never happen

for n,5
	print 'hi'
	continue
	print 'bye' #this will never happen

Break Statement

A break statement is simply the keyword "break". This will exit the current loop iteration and end the loop.

n = 5
while n--
	print 'hi' #this will only get printed once
	break

for n,5
	print 'hi' #this will only get printed once
	break

Pass Statement

A pass statement is simply the keyword "pass". This is used to declare an empty block.

#an empty class
@nothing
	pass

#an empty function
:do_nothing()
	pass

Print Statement

A print statement is the keyword "print" followed by zero or more expressions, separated by commas. The results of the expressions will be printed to standard output, separated by a space, and terminated by a newline.

#print a blank line
print

#print one thing
print 1

#print some things separated by spaces
print 'hello',2,'u'

Standalone Expression

A standalone expression statement is just an expression by itself on a line. See the next section for more details on expressions.

:hello()
	print 'hello'

#these are all standalone expressions
n = 5
n++
m = n*2
hello()

Expressions

Expressions are nestable instructions, including literals, references, operations, function calls, and parentheticals. Literals are described in the next section. References are simply identifiers. Function calls are identifiers followed by a list of arguments in parentheses and separated by commas. The list of arguments is zero or more expressions separated by commas. Operations are one or more expressions combined with operators. The operators are described in a subsequent section. A parenthetical is simply an expression in parentheses, for nesting and explicitly ordering operations.

Types

There are six types in Axiom, which are the same as the six JSON types: null, boolean, number, string, array, object.

Nulls are always null. They cast to the boolean false or the number 0.

Booleans are either true or false. True casts to the number 1, and false casts to the number 0.

Numbers are double-precision floating-point numbers. Numbers are also used as timestamps within Axiom, where they represent the number of seconds since January 1, 1970 UTC (the Unix epoch). Numbers can store integers with 52-bit precision, meaning that it is suitable for representing file sizes in the petabyte range or timestamps millions of years in the future. (Unlike 32-bit integers, which can only represent file sizes up to 4 GB or timestamps up to the year 2038.)

Strings are arbitrary arrays of bytes. They are displayed and written as Unicode, but are not necessarily Unicode. This means that strings within Axiom play nice with both Unicode and arbitrary byte values. Axiom provides functionality for encoding/decoding a string as Unicode, as well as accessing the underlying byte values. However, as a convenience, string elements appear to be strings. The index operator returns the byte at the given index as a single-character string. The for statement interprets the string as Unicode and yields the corresponding code points as single-code-point strings.

Arrays are arrays of other variables. They are internally represented by an expanding array (like ArrayList in Java or vector in C++), so random access and stack operations are efficient. Although you may have learned about linked lists in school, in reality they are impractical and almost always slower than arrays.

Objects are maps of keys to values (also called dictionaries or associative arrays). Keys are always strings. Objects also represent instances of classes (similar to Javascript). A class instance is simply an object with the "class" key set to the scope of the class. Additionally, key/value pair order is preserved within objects in Axiom (unlike most other programming languages). The key/value pair order is the order in which the key was first added.

Empty strings, arrays, and objects cast to boolean false with conditional expressions.

Because there are only six types in Axiom, this makes inspecting and reading code much simpler than in languages with unlimited types. Also, because the six types are the same as the six JSON types, every Axiom variable can be easily serialized to JSON.

Literals

A literal is a declared value with no identifier. There are literals for each of the six types: null, boolean, number, string, array, and object.

Null literals are just the keyword "null".

null

Boolean literals are either of the keywords "true" or "false".

true
false

Number literals are one or more digits, optionally followed by a dot and one or more digits.

1
3.5

String literals are any characters surrounded by either a single quote (') or back tick (`). Single-quoted strings can only span one line. Back-ticked strings can span multiple lines, in which case the first blank line and any indentation (up to the block level + 1) is ignored. (Double-quoted strings are not supported. This is to put an end to the single vs double quote debate.)

'hello there'
`
	this is a string with no indentation
	and two lines (plus trailing newline)
`

Array literals are zero or more expressions in brackets, separated by commas.

[1,2,'a','b']

Object literals are zero or more pairs in curly braces, separated by commas. A pair is an identifier or string, followed by a colon (:), followed by an expression.

{
	x: 1,
	'y': 23
}

String Escapes

Special characters within strings can be escaped using the backlash (\). These characters are:

Template Strings

Back-ticked strings are also template strings, which means that any dollar sign ($) followed by an identifier within the string is replaced by the value of the variable referenced by the identifier. It is an error to reference an undeclared variable within a template string. Two consecutive dollar signs ($) specifies a single dollar sign within a template string. A dollar sign that is not followed by an identifier has no special effect.

name = 'world'
print `Hello $name!` #Hello world!
print `Hello $$name!` #Hello $name!
print `the fee is $1.23` #the fee is $1.23

Operators

These are all the operators in order of precedence, from high to low. Operators with high precedence will be evaluated before operators with low predecence. Operators with the same precedence will be evaluated from left to right.

Most of these operators should be recognizable to anyone familiar with mathematics. However, to go over the more obscure obscure operators in detail:

All bitwise operations treat the operands as if they are 32-bit unsigned integers.

These operators have special meaning for arrays:

These operators have special meaning for objects:

Except for "+=", the assignment operators of the form "x ?= y" are equivalent to "x = x ? y". This is distinct from some other languages where the "?=" operators are special (and not equivalent) in that they can modify a held reference. In Axiom, the reference is reassigned, except for in the case of "+=", which allows in-place modification for efficiency and convenience.

The multiplication (*) operator has special meaning for strings and arrays. If a number is on the right-hand side, it can be used to allocate a string or array with the memory initalized to the left-hand side.

' '*10 #allocate a blank string of length 10
'abc'*3 #allocate a string like 'abcabcabc'	
	
[0]*10 #allocate an array of zeroes of length 10
[1,2,3]*3 #allocate an array like [1,2,3,1,2,3,1,2,3]

Also the logical operators (&& and ||) exhibit "short-circuit" behavior. This means that if the result can be known from the left-hand side, the right-hand side is not evaluated.

if true || do_something() #do_something() is never called
	print 'true'

if false && do_something() #do_something() is never called
	print 'this never gets printed'