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.
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 |
|
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
andnil
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")
}
See also: Interrupting a Loop
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
See also: Interrupting a Loop
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
See also: Interrupting a Loop
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.
See also: Interrupting a Loop
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 ornil
at the first iteration and returns an iterator to the next item of the sequence ornil
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.
Method | Signatures |
---|---|
Simple method |
|
Getter |
|
Setter |
|
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
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.