The Agate language is a scripting language.

An Agate file is a UTF-8 plain text file with a .agate extension.

Syntax

The syntax of Agate is inspired by C, but also takes ideas from Ruby and other languages.

Comments

Comments starts with # and run until the end of the line.

# comments are useful for
# programmers, do comment your
# source code!

In particular, it’s possible to put a shebang at the beginning of the file.

#!/usr/local/bin/agate-cli

There is no multi-line comments in Agate.

Keywords

Agate has 25 keywords: as, assert, break, class, construct, continue, def, else, false, for, foreign, if, import, in, is, loop, mixin, nil, once, return, static, super, this, true, while.

Identifiers

Agate identifiers follow the same rules as the C identifiers. An idenfitier must start with a letter or _, and then may contain letters, digits or _. Identifiers are case-sensitive. Identifiers must not be keywords.

variable_name
ClassName
__private_name
CONSTANT_NAME

Newlines

Newlines are meaningfull in Agate. They mark the end of a statement. Each statement or expression must be on a single line. If the statement seems too long to be on a single line, it’s probably because you should refactor your code in order to make it more readable.

Blocks

Blocks are surrounded by { and }. Blocks are used in Control Flow and Functions. They are two kinds of blocks: expression blocks and statement blocks.

An expression block is a block with no newline after the opening { and consisting in a single expression, followed by the closing } on the same line. An expression block evaluates to the expression that is inside the block.

{ "expression" }

It’s useful in the case of simple functions or methods that can be written as a single expression. In this case, it’s equivalent to:

{
  return "expression"
}

A statement block is a block with a newline after the opening { followed by statements, and a closing } on its own line.

{
  IO.println("The cake")
  IO.println("is")
  IO.println("a lie")
}

Values

Values are immutable built-in objects of the language.

Nil

nil represents the absence of value, it is the only instance of the Nil class. It is similar to nullptr in C++.

Boolean

A boolean value represents whether something is true or false. The literals true and false can be used to create the two possible boolean values. Their class is Bool.

Integers

An integer is a 64-bit signed number.

12345      # decimal form
0xDeadBeef # hexadecimal form
0o755      # octal form
0b101010   # binary form

Integers are instances of the Int class.

Floats

A float is a double precision floating-point number. In order to be recognized as floats, float literals must have a decimal part starting with a point . and at least one digit after the point, or an exponent part starting with e.

3.141592
42.0
1e10
1.0e-5

Floats are instances of the Float class.

Characters

A character is a single UTF-32 encoded codepoint. A character is surrounded by single quotes '.

'a'
'木'
'🎮'

Characters are instances of the Char class.

Escape Sequences

Usual escape sequences are available.

'\0' # U+0000 NULL
'\a' # U+0007 BELL
'\b' # U+0008 BACKSPACE
'\t' # U+0009 CHARACTER TABULATION (horizontal tabulation)
'\n' # U+000A LINE FEED (new line)
'\v' # U+000B LINE TABULATION (vertical tabulation)
'\f' # U+000C FORM FEED
'\r' # U+000D CARRIAGE RETURN
'\e' # U+001B ESCAPE
'\"' # U+0022 QUOTATION MARK (double quote)
'\%' # U+0025 PERCENT SIGN
'\'' # U+0027 APOSTROPHE (single quote)
'\\' # U+005C REVERSE SOLIDUS (backslash)

'\u6728'     # '木'
'\U0001F3AE' # '🎮'

Strings

A string is a sequence of Unicode codepoints. They are stored using UTF-8 encoding. A string literal is surrounded by double quotes ". Escape sequences can also be used in strings.

"" # empty string
"The cake is a lie"
"I love \U0001F3AE" # I love 🎮

Strings are instances of the String class.

Interpolation

Interpolation in strings is introduced by % followed by a parenthesized expression. The expression is evaluated and the .to_s method is called on the result and inserted in the string. Interpolations can be nested.

IO.println("The answer is %(6 * 7)") #> The answer is 42
IO.println("The answer is %((2..1).map {|n| 2 * n}.join())") #> The answer is 42

Ranges

A range is a value that represents an interval of integers. A range can be constructed either with the .. operator for inclusive range, or with the ... operator for exclusive range. Ranges are used for iterating over a sequence of numbers, or for array indexing.

2..4 # 2 to 4 (included)
6...9 # 6 to 9 (excluded)

Ranges are instances of the Range class.

Expressions

Array Literals

An array is a growable sequence of contiguous elements, also called dynamic array. Arrays are instances of the Array class. An array can contain any number of elements of any type. In Agate, array indexing is zero-based.

An array literal can be created with elements between [ and ].

[ 1, 'a', 3.14, "good", true ]

Map Literals

A map is a sequence of key-value pairs, also called associative array. Maps are instances of the Map class. A map can contain any number of key-value pairs. Keys must implement a .hash method and preferably are not mutable.

A map literal can be created with key-value pairs between { and }.

{ 1: 'a', true: 3.14, "good": nil }

Tuple Literals

A tuple is an immutable sequence of contiguous components, also called product type. Tuples are instances of the Tuple class. A tuple can contain two or more components of any type. Like array, tuple indexing is zero-based.

A tuple literal can be created with components between ( and ).

(1, 'a', 3.14, "good", true)

Operators

The following table shows the precedence and associativity of the different operators in Agate. Note that, compared to C, the bitwise operators do not have the same precedence.

Table 1. Operators precedence and associativity
Precedence Operators Description Associativity

1

() [] .

Call, subscript, method

Left

2

+, -, !, ~

Unary plus and minus, logical NOT, bitwise NOT

Right

3

*, /, %

Multiplication, division, and remainder

Left

4

+, -

Addition and subtraction

Left

5

.., ...

Inclusive and exclusive range

Left

6

<<, >>, >>>

Arithmetic left shift, arithmetic right shift, logical right shift

Left

7

&

Bitwise AND

Left

8

^

Bitwise XOR

Left

9

|

Bitwise OR

Left

10

<, >, >=, <=

Comparison operators

Left

11

is

Type test

Left

12

==, !=

Equality, inequality

Left

13

&&

Logical AND

Left

14

||

Logical OR

Left

15

?:

Conditional

Right

16

=

Assignment

Right

The truth is out there

In the context of control flow, any expression can be evaluated as a condition. Thus, it is necessary to split the expressions between those that are true and those that are false:

  • false and nil are false

  • everything else is true

In particular, 0 is true whereas it is false in C/C++.

if (!false) {
  IO.println("This is true") #> This is true
}
if (0) {
  IO.println("This is also true") #> This is also true
}

Logical Operators

Logical operators && and || do short-circuit evaluation: the second argument is evaluated only if the first argument is not enough to determine the value of the expression.

Control Flow

All control flow statements in Agate follow the same principles:

  • the condition is between brackets i.e. ( and ) are mandatory

  • the statements are statement blocks i.e. { and } are mandatory

If Statements

The if statement is a conditional statement. The else branch is optional. An else branch may be either a statement block or another if statement.

def x = 42
if (x % 2 == 0) {
  IO.println("%(x) is even") #> 42 is even
} else {
  IO.println("%(x) is odd")
}

Once Statements

The once statement is a degenerate loop statement where the loop is executed exactly once. It is an idiomatic way to create a new scope, with the additional property that it is a loop like any other, so it can be interrupted.

once {
  IO.println("Hello") #> Hello
  break
  IO.println("World")
}

Loop Statements

The loop statement creates an infinite loop.

def i = 0
loop {
  i = i + 1
  if (i > 2) {
    break
  }
  IO.println(i)
}
#> 1
#> 2

While Statements

The while statement is a loop statement that allows to loop over statements while a condition is true.

def n = 27
def steps = 0
while (n != 1) {
  if (n % 2 == 0) {
    n = n / 2
  } else {
    n = 3 * n + 1
  }
  steps = steps + 1
}
IO.println(steps) #> 111

For Statements

The for statement is a loop statement that allows to loop over a sequence. It has three components:

  • a variable name to bind, a new variable with that name with the loop scope is created

  • a sequence to iterate that is evaluated once before the loop starts

  • a body that is a statement block

for (direction in [ "north", "east", "south", "west" ]) {
  IO.println(direction)
}
#> north
#> east
#> south
#> west

The sequence can be an Array, a Range, a Map, a String or any other object that implements the iterator protocol.

Iterator Protocol

The iterator protocol is the protocol used by for statements. A sequence used in a for statement must implement two functions:

  • iterate(iterator): it takes an iterator or nil at the first iteration and returns an iterator to the next item of the sequence or nil if the sequence is finished.

  • iterator_value(iterator): it takes an iterator and returns the value of the item in the sequence that is pointed by the iterator.

The iterator can be any type.

Then the following statement:

for (variable in sequence) {
  statements
}

is translated to the following:

def $sequence = sequence
def $iterator = nil
while ($iterator = $sequence.iterate($iterator)) {
  def variable = $sequence.iterator_value($iterator)
  statements
}

where $sequence and $iterator are internal variables that you cannot use directly.

Interrupting a Loop

There are two statements to interrupt a loop.

The break statement exits the loop early.

for (i in [ 1, 2, 3, 4 ]) {
  IO.println(i)   #> 1
  if (i == 3) {   #> 2
    break         #> 3
  }
}

The continue statement jumps to the next iteration of the loop.

for (i in [ 1, 2, 3, 4 ]) {
  if (i == 2) {   #> 1
    continue      #> 3
  }               #> 4
  IO.println(i)
}

Assert Statements

The assert statement is useful for defensive programming. Assertions can prevent programming errors. In Agate, there are composed of a conditional expression that must be satisfied, and a string explaining the error in case the expression is false. The expression is verified and if false, the message is printed and the script stops with a runtime error.

def n = 3
assert(n is Int, "%(n) should be an Int")

Variables

Definition

New variables are created using the def keyword followed by the name of the variable. By default, they are initialized to nil unless they are explicitely initialized with = followed by an expression.

def anakin
IO.println(anakin) #> nil
def luke = 5.4
IO.println(luke) #> 5.4

Assignment

Variables can be assigned a new value with the = operator. The = operator is right associative. An assignment is an expression whose value is the result of the right side of =.

def a
def b
a = 42
IO.println(a) #> 42
b = a = 69
IO.println(a) #> 69
IO.println(b) #> 69

Scope

A variable life starts at its definition and ends when exiting the scope where it is defined. It is not possible to use a variable outside its lifetime. It is an error to define a variable with the same name in the same scope. It is possible to define a variable in an inner scope, this is called variable shadowing and it is strongly discouraged.

def a = 42
once {
  def a = 69
  IO.println(a) #> 69
}
IO.println(a) #> 42

Classes

Every value is an object, and every object is an instance of a class. For example, true and false are instances of the Bool class.

New classes can be created with the class keyword.

class Entity {
}

Methods

Methods describe the behaviour of the objects of a class. Methods are defined inside the class scope, they may have zero, one or more arguments.

class Entity {
  update(dt) {
    IO.println("Update the state of the entity")
  }
  render(target, state) {
    IO.println("Render to target using state")
  }
}

Several methods can have the same name as long as they do not have the same number of parameters. This means Agate support overload by arity. In fact, two different methods must have different signatures.

class Entity {
  render(target) {
    IO.println("Render to target using default state")
  }
  render(target, state) {
    IO.println("Render to target using state")
  }
}

Signature

The signature of a method is composed of the name of the method with its parameters where the names of the parameters are replaced with _. For example, the signature of the update method in the example above is update(_). The signatures of the render methods are render(_) and render(_,_).

Signatures are at the heart of Agate method call, so it’s important to understand how they work.

Table 2. Signatures of methods
Method Signatures

Simple method

name(), name(_), name(_,_)

Getter

name

Setter

name=(_)

Prefix operator

@

Infix operator

@(_)

Subscript getter

[_], [_,_]

Subscript Setter

[_]=(_), [_,_]=(_)

Call operator

(), (_), (_,_)

Missing method operator

?(_,_)

Getter

A getter is a method without any parameters, and no parentheses. Generally, they are used for getting properties of the object without modify the object, and are named with an adjective, contrary to zero-parameter methods that are verbs (and have parentheses) and modify the object.

class Entity {
  alive { true }
}

The signature of a getter is just the name of the method. For example, alive is the signature of the method in the previous example.

Setter

A setter is a method with a = after its name and exactly one parameter. They are used for setting properties of the object. They are called whenever there is an assignment.

class Entity {
  construct new() { }
  alive=(value) {
    IO.println("setter: %(value)")
  }
}

def a = Entity.new()
a.alive = false #> setter: false

The signature of a setter is the name of the method including the = followed by one parameter. For example, alive=(_) is the signature of the method in the previous example.

Operators

Agate supports operator overloading through operator methods. In fact, expressions with an operator are simply calls to operator methods, even for Int or Float!

A prefix operator method can be defined with the operator name as the method name, like a getter method. The list of the 4 overloadable prefix operators is: +, -, ! and ~. The signature of a prefix operator method is the name of the operator, e.g. -.

class Vec {
  construct new() { }
  - {
    IO.println("Negate a vector")
  }
}
def a = Vec.new()
def b = -a #> Negate a vector

An infix operator method can be defined with the operator name followed by exactly one parameter as the method name. The list of the 20 overloadable infix operators is: +, -, *, /, %, &, |, ^, ==, !=, <, <=, >, >=, <<, >>, >>>, .., ..., is. The signature of an infix operator method is the name of the operator followed by one parameter, e.g. +(_).

class Vec {
  construct new() { }
  +(other) {
    IO.println("Add two vectors")
  }
}
def a = Vec.new()
def b = Vec.new()
def c = a + b #> Add two vectors

.. and ... are range operators and are implemented in Int to create a Range.

is is the type test operator and is implementd in Object to test if an object is an instance of a given class. Overload this operator with caution!

Subscript

It is possible to overload the subscript operator, both as a getter and a setter. The number of parameters inside the brackets is at least one.

The signature of a subscript getter method is [_], [_,_], …​ according to the number of indices in the subscript.

class Matrix {
  construct new() { }
  [x,y] {
    IO.println("Get element at %(x),%(y)")
  }
}

def m = Matrix.new()
m[1, 2] #> Get element at 1,2

The signature of a subscript setter method is [_]=(_), [_,_]=(_), …​ according to the number of indices in the subscript

class Matrix {
  construct new() { }
  [x,y]=(value) {
    IO.println("Set element at %(x),%(y) to %(value)")
  }
}

def m = Matrix.new()
m[1, 2] = 3 #> Set element at 1,2 to 3

Call Operator

It is possible to overload the call operator. A class that overload the call operator is callable.

The signature of a call operator is (), (_), (_,_), …​ according to the number of parameters in the call.

class Callable {
  construct new() { }
  (a) {
    IO.println("Call with %(a)")
  }
}

def c = Callable.new()
c(1) #> Call with 1

Missing Method Operator

It is possible to implement the missing method operator. In case a method is not found, then this special operator is called with the signature of the method and the arguments in an array. The signature of the missing method operator is ?(_,_).

class Entity {
  construct new() { }
  ?(signature, args) {
    IO.println("Method %(signature) missing: %(args)")
  }
}

def e = Entity.new()
e.unknown("hello") #> Method unknown(_) missing: [hello]

Constructor

A constructor is a special method that create an instance of the object. It is introduced with a construct keyword. The method name must have parenthesized parameters and can have any name, even if, by convention, it is generally named new().

class Entity {
  construct new() {
    IO.println("Born to be alive")
  }
}

def e = Entity.new() #> Born to be alive

As any other method, constructors can be overloaded by arity.

You can use return without any argument in a constructor, it’s an error to put an argument.

Static methods

A static method is a method that do not apply to an instance of the class but to the class itself. As a class is an object itself, a static method is a method that applies to the class object. A static method is defined with the static keyword before the name of the method. A static method can be any kind of method.

class Entity {
  static opening() {
    IO.println("All your base are belong to us")
  }
}

Entity.opening() #> All your base are belong to us

Fields

A field stores the state of an object.

An instance field's name starts with @. Contrary to variables, it does not need to be declared explicitely. It can only be used in instance methods. Initially, fields have a value of nil.

class Entity {
  construct new(name) {
    @name = name
  }
  name { @name }
}

def hero = Entity.new("Mario")
IO.println(hero.name) #> Mario

A class field's name starts with @@. A static field is stored in the class itself, not in instances. It can be accessed through static and instance methods. Initially, static fields have a value of nil.

class Entity {
  static count { @@count }
  construct new() {
    if (@@count == nil) {
      @@count = 1
    } else {
      @@count = @@count + 1
    }
  }
}

IO.println(Entity.count) #> nil
def e1 = Entity.new()
IO.println(Entity.count) #> 1
def e2 = Entity.new()
IO.println(Entity.count) #> 2

Encapsulation is very strong in Agate. All fields are private and cannot be accessed from outside the class. You must provide a getter or a setter in order to provide an access to a field.

class Entity {
  health { @health }
  health=(value) { @health = value }
}

An object can access its own instance fields and that’s all. This means an object cannot access another object’s fields, even if they have the same class. It cannot either access its base class fields.

this

this represent the object itself inside methods of the class. It can be used to refer to the current object, especially when calling another method of the class. You can even use this inside a function inside a method as this is in the closure of the function.

class Entity {
  construct new() { }
  name { "Mario" }
  name_twice() {
    (1..2).each {
      IO.println(this.name)
    }
    #> Mario
    #> Mario
  }
}

def e = Entity.new()
e.name_twice()

As it is verbose to put this every time you call another method, it can be omitted, but not the following .!

def name = "variable"

class Entity {
  construct new() { }
  name { "getter" }
  who_am_i() {
    IO.println(name)      #> variable
    IO.println(this.name) #> getter
    IO.println(.name)     #> getter
  }
}

def e = Entity.new()
e.who_am_i()

Inheritance

A class can inherit another class, its superclass, using the is keyword. If a superclass is not present, the superclass is Object. It’s not possible to inherit from builtin classes (Array, Bool, Char, Class, Float, Fn, Int, Map, Nil, Range, String, Tuple).

class Entity {
  # no superclass = superclass is Object
}
IO.println(Entity.supertype) #> Object
class Hero is Entity {
  # superclass is Entity
}
IO.println(Hero.supertype) #> Entity

All instance methods are dispatched dynamically. Static methods are not inherited, as well as constructors.

super

super is used to access a method of the same name in the superclass. super can be used in constructors, in that case the name of the method is not necessary.

class Entity {
  construct new() {
    IO.println("Entity.new")
  }
  print() {
    IO.println("Entity.print")
  }
}

class Hero is Entity {
  construct new() {
    super()
  }
  print() {
    super.print()
  }
}

def h = Hero.new() #> Entity.new
h.print() #> Entity.print

Mixin

A class can include the methods of another class with the mixin keyword. The included class must not have any fields and must not be a builtin class. It is also possible to do a static mixin to include the methods in the class itself rather than its instances.

class Representable {
  string { .to_s }
}

class Entity {
  mixin Representable
  construct new() { }
  to_s { "Entity" }
}

def e = Entity.new()
IO.println(e.string) #> Entity

There may be several mixins. The order in which a mixin appear is important: its methods are included in the class at the point where it is declared. A method defined in a mixin may be overriden in the class if it appears after the mixin declaration.

Functions

A function is a sequence of statements that may have some parameters. In Agate, functions are objects of class Fn.

Definition, parameters and return value

The def keyword is used to create a new function. Contrary to methods, function can not be overloaded and must have a unique name. A function may have parameters, their names are between brackets after the function name. The function may return a value with the return keywork, or use an expression block. By default, nil is returned.

def is_even(n) {
  return n % 2 == 0
}

def is_odd(n) { !is_even(n) }

Alternatively, it is possible to create a function by creating an object of class Fn, thanks to a block argument. The result is exactly the same.

def is_even = Fn.new {|n|
  return n % 2 == 0
}

def is_odd = Fn.new {|n| !is_even(n) }

Call

A call to a function is just the function name followed by its arguments between brackets. The caller can use the return value of the function. If there are more arguments than parameters, the extra arguments are discarded.

def double(n) { 2 * n }

IO.println(double(2))     #> 4
IO.println(double(2, 3))  #> 4

Block Argument

If the last parameter is a function, the argument may be passed with a block argument. A block argument is a special block that can take parameters between | and act like a function. In fact, it is a function. It can be used to create callbacks.

def apply(x, fn) {
  return fn(x)
}

def print(str) {
  IO.println(str)
}

apply("Hello") {|str| IO.println(str) } #> Hello
apply("World", print) #> World

A block argument can also be used when calling methods.

Closure

A function is a closure, it can capture variables outside its scope.

def counter() {
  def i = 0
  return Fn.new { i = i + 1 } # i is captured
}

def f = counter()
IO.println(f()) #> 1
IO.println(f()) #> 2
IO.println(f()) #> 3

Callable

An object is callable if it implements the call operator. It acts like a function, but it is a method call.

def regular(x) {
  IO.println(x)
}

class Callable {
  static (x) {
    IO.println(x)
  }
}

def invoke(fn, arg) { fn(arg) }

invoke(regular, "regular")    #> regular
invoke(Callable, "callable")  #> callable

Units

A unit is a single Agate file that can be imported in another Agate file with the import keyword. Units are useful to break a big program into small (independent) pieces. Each unit has its own scope.

Import

In order to import a unit, the keyword import is used, followed by a string that identifies the unit. Then, the top-level names from that unit are declared and copied from that unit into the current unit. The names can refer to variables, functions or classes.

import "entities" for Hero, Villain

An import statement can be anywhere, not necessarily at the top of the file.

def in_the_forest = true
if (in_the_forest) {
  import "entities" for Orc, Troll
}

You can rename imported names using the as keyword. This is useful if two units export the same name.

import "scenery" for Tree
import "entities" for Tree as Ent

Process

During an import, the code in the unit is executed, pausing the current script. When the code in the import is finished, the script resumes.

IO.println("Before import") #> Before import
import "tools" for Debug    #> Tools are loaded
IO.println("After import")  #> After import

A unit is never executed twice. It is only executed the first time it is imported, even when several units import the same unit.

import "tools" for Debug  #> Tools are loaded
import "tools" for Log    # no output, the unit is alreay loaded

This means you have to be very careful when doing cyclic imports!

Location

The way to map the string after import to a file can be customized by the runtime environment (see Advanced Configuration - Units). The convention is that the string "foo/bar" maps to a file called foo/bar.agate inside a root directory known by the runtime. The string is always relative to the root directory, not relative to the script directory.