AGL Language Specification
1. Lexical Elements
Comments
Line comments start with // and extend to the end of the line.
// This is a comment
let x = 42 // inline comment
There are no block comments.
Identifiers
Identifiers start with a letter or underscore, followed by letters, digits, or underscores.
identifier = [a-zA-Z_][a-zA-Z0-9_]*
Keywords
break continue else err false fn for
if import in let match ok return
struct true var while
Literals
| Type | Examples |
|---|---|
| Integer | 0, 42, 1000 |
| Float | 3.14, 0.5 |
| String | "hello", "line\nbreak" |
| Boolean | true, false |
Integer literals are decimal digits. Float literals require a digit on both sides of the decimal point (0.5, not .5).
String literals are enclosed in double quotes. Escape sequences are supported with backslash (e.g., \n, \\, \"). Strings cannot span multiple lines.
Operators and Delimiters
Operators:
+ - * / %
= == != < > <= >=
&& || !
-> .
Delimiters:
( ) { } [ ] , :
Auto-Semicolon Insertion
Agl uses Go-style automatic semicolon insertion. A newline acts as a statement terminator when the preceding token is one of:
- Identifier, literal (
int,float,string,true,false) break,continue,return),],}
Newlines inside parentheses (), brackets [], or braces {} at nonzero nesting depth are suppressed (do not act as terminators). This allows multi-line expressions and argument lists.
let result = foo(
arg1,
arg2
) // newline terminates here, after )
2. Types
| Type | Description | Truthy when |
|---|---|---|
int | 64-bit signed integer | nonzero |
float | 64-bit double-precision float | nonzero |
bool | true or false | true |
string | Immutable byte sequence | non-empty |
array | Ordered collection of values | non-empty |
struct | Named record with typed fields | always truthy |
result | Tagged union: ok(val) or err(val) | is_ok |
fn | Function value (named or lambda) | always truthy |
nil | Absence of value | always falsy |
Numeric Promotion
When an int and float are operands to an arithmetic or comparison operator, the int is promoted to float and the result is float.
3. Variables
Immutable Binding (let)
let name = "Ago"
let pi = 3.14
let bindings cannot be reassigned. Attempting to assign to a let variable produces an error.
Mutable Binding (var)
var count = 0
count = count + 1
var bindings can be reassigned with =.
Optional Type Annotation
let x: int = 42
var name: string = "hello"
Type annotations are syntactically accepted but not enforced at compile time. Types are checked dynamically at runtime.
4. Expressions
Arithmetic
1 + 2 // 3
10 - 3 // 7
4 * 5 // 20
10 / 3 // 3 (integer division)
10 % 3 // 1
2.0 * 3.0 // 6.0
1 + 2.5 // 3.5 (int promoted to float)
Integer division by zero is a runtime error. Float division by zero produces infinity.
Comparison
1 == 1 // true
1 != 2 // true
3 < 5 // true
5 > 3 // true
3 <= 3 // true
4 >= 5 // false
Comparison operators work on int, float, and string values. String comparisons are lexicographic (byte-by-byte).
Logical
true && false // false
true || false // true
!true // false
&& and || operate on bool values only. ! is a unary prefix operator.
String Operations
"hello" + " " + "world" // concatenation
"abc" == "abc" // equality
"abc" < "abd" // lexicographic comparison
"abc" <= "abc" // true
len("hello") // 5
The + operator concatenates strings. All six comparison operators (==, !=, <, >, <=, >=) work on strings.
Unary
-42 // negation (int or float)
!true // logical not (bool only)
Array Literal and Index
let nums = [1, 2, 3, 4, 5]
nums[0] // 1
nums[4] // 5
Array indices must be integers. Out-of-bounds access is a runtime error.
Struct Literal and Field Access
struct Point {
x: int
y: int
}
let p = Point { x: 10, y: 20 }
p.x // 10
p.y // 20
Field access uses dot notation. Accessing a nonexistent field is a runtime error.
Grouped Expressions
Parentheses override precedence:
(1 + 2) * 3 // 9
5. Statements
Variable Declaration
let x = 42
var y = 0
Assignment
y = y + 1
Only var variables can be assigned. Only simple identifiers are valid assignment targets.
if / else
if x > 0 {
print("positive")
} else if x == 0 {
print("zero")
} else {
print("negative")
}
Braces are required. There is no ternary operator.
while
var i = 0
while i < 10 {
print(i)
i = i + 1
}
for-in
Iterates over array elements:
let items = [10, 20, 30]
for item in items {
print(item)
}
The loop variable is scoped to the loop body and is not mutable.
return
Returns a value from the enclosing function:
fn add(a: int, b: int) -> int {
return a + b
}
Bare return (without a value) returns nil. return at the top level is permitted.
break / continue
Reserved keywords recognized by the lexer. (Loop control within while and for.)
6. Functions
Declaration
fn greet(name: string) {
print("Hello, " + name)
}
fn add(a: int, b: int) -> int {
return a + b
}
Parameters require type annotations: name: type. The return type follows -> and is optional (defaults to nil/void).
Functions are first-class values and are bound with let (immutable).
Calling
greet("world")
let sum = add(1, 2)
Argument count must match parameter count at runtime. Maximum 64 arguments per call.
Recursion
Functions can call themselves. Maximum call depth is 512.
7. Lambdas / Closures
Anonymous functions use the fn keyword without a name:
let double = fn(x: int) -> int {
return x * 2
}
double(5) // 10
Closures
Lambdas capture the enclosing environment by value (snapshot at creation time):
fn make_counter(start: int) -> fn {
var n = start
return fn() -> int {
return n
}
}
let count = make_counter(10)
count() // 10
The return type for functions returning a closure is fn.
As Arguments
Lambdas are commonly passed to higher-order functions:
let doubled = map([1, 2, 3], fn(x: int) -> int { return x * 2 })
// [2, 4, 6]
8. Result Type
The result type represents success or failure.
Constructors
ok(42) // success with value 42
err("not found") // error with message
Match Expression
Pattern match on a result value:
fn safe_div(a: int, b: int) -> result {
if b == 0 {
return err("division by zero")
}
return ok(a / b)
}
match safe_div(10, 3) {
ok(v) -> print(v)
err(e) -> print(e)
}
Both ok and err arms are required. Each arm binds the inner value to a name. The arms can appear in any order. The match body is an expression (not a block).
Truthiness
A result value is truthy if it is ok, falsy if err.
9. Module System
Import
import "math" // imports math.agl from same directory
import "lib/utils" // imports lib/utils.agl relative to current file
The import path is a string literal (without the .agl extension). Paths are resolved relative to the importing file’s directory.
Restrictions:
- Path traversal (
..) is rejected. - The resolved path must stay within the base directory.
- Circular imports are prevented (each module loaded once).
- Maximum 64 modules.
Imported modules execute in the same environment – their top-level fn and variable declarations become available to the importer.
10. Operator Precedence
From lowest to highest:
| Precedence | Operators | Associativity | Description |
|---|---|---|---|
| 1 | = | right | Assignment |
| 2 | \|\| | left | Logical OR |
| 3 | && | left | Logical AND |
| 4 | == != | left | Equality |
| 5 | < > <= >= | left | Comparison |
| 6 | + - | left | Addition |
| 7 | * / % | left | Multiplication |
| 8 | ! - (prefix) | right | Unary |
| 9 | () . [] | left | Call / Access |
Assignment is parsed as a statement (not an expression), so it does not participate in the Pratt precedence table. All binary operators are left-associative.
11. Built-in Functions
| Function | Signature | Description |
|---|---|---|
print(args...) | variadic | Print values to stdout, one per call |
len(x) | (string\|array) -> int | Length of string or array |
type(x) | (any) -> string | Type name as string |
str(x) | (any) -> string | Convert to string |
int(x) | (string\|float\|int) -> int | Convert to integer |
float(x) | (string\|int\|float) -> float | Convert to float |
push(arr, val) | (array, any) -> array | Return new array with val appended |
map(arr, fn) | (array, fn) -> array | Apply fn to each element |
filter(arr, fn) | (array, fn) -> array | Keep elements where fn returns true |
abs(n) | (int\|float) -> int\|float | Absolute value |
min(a, b) | (number, number) -> number | Minimum of two same-typed numbers |
max(a, b) | (number, number) -> number | Maximum of two same-typed numbers |
read_file(path) | (string) -> result | Read file contents (max 10MB) |
write_file(path, content) | (string, string) -> result | Write string to file |
file_exists(path) | (string) -> bool | Check if file exists |
push, map, and filter return new arrays (immutable semantics).
12. Grammar Summary (EBNF)
program = { statement } ;
statement = let_stmt | var_stmt | assign_stmt | return_stmt
| if_stmt | while_stmt | for_stmt
| fn_decl | struct_decl | import_stmt
| expr_stmt ;
let_stmt = "let" IDENT [ ":" type ] "=" expression NEWLINE ;
var_stmt = "var" IDENT [ ":" type ] "=" expression NEWLINE ;
assign_stmt = IDENT "=" expression NEWLINE ;
return_stmt = "return" [ expression ] NEWLINE ;
if_stmt = "if" expression block [ "else" ( if_stmt | block ) ] ;
while_stmt = "while" expression block ;
for_stmt = "for" IDENT "in" expression block ;
fn_decl = "fn" IDENT "(" params ")" [ "->" type ] block ;
struct_decl = "struct" IDENT "{" { IDENT ":" type NEWLINE } "}" ;
import_stmt = "import" STRING NEWLINE ;
expr_stmt = expression NEWLINE ;
block = "{" { statement } "}" ;
expression = unary | binary | call | index | field_access
| match_expr | lambda | ok_expr | err_expr
| literal | IDENT | "(" expression ")" ;
literal = INT | FLOAT | STRING | "true" | "false"
| array_lit | struct_lit ;
array_lit = "[" [ expression { "," expression } ] "]" ;
struct_lit = IDENT "{" [ IDENT ":" expression { "," IDENT ":" expression } ] "}" ;
lambda = "fn" "(" params ")" [ "->" type ] block ;
ok_expr = "ok" "(" expression ")" ;
err_expr = "err" "(" expression ")" ;
match_expr = "match" expression "{" ok_arm err_arm "}" ;
ok_arm = "ok" "(" IDENT ")" "->" expression NEWLINE ;
err_arm = "err" "(" IDENT ")" "->" expression NEWLINE ;
params = [ IDENT ":" type { "," IDENT ":" type } ] ;
type = IDENT | "fn" ;