- Aki language basics
- Introduction
- Symbols
- Top-level keywords
- Keywords
- Types:
This is a document of Aki syntax and usage.
⚠ This document is incomplete and is being revised continuously.
Expressions are the basic unit of computation in Aki. Each expression returns a value of a specific type. Every statement is itself an expression:
5
is an expression that returns the value 5
. (The type of this value is the default type for numerical values in Aki, which is a signed 32-bit integer.)
print (if x==1 'Yes' else 'No')
Here, the argument to print
is an expression that returns one of two compile-time strings depending on the variable x
. (print
itself is a wrapper for printf
and so returns the number of bytes printed.)
The divisions between expressions are normally deduced automatically by the compiler. You can use parentheses to explicitly set expressions apart:
(x+y)/(a+b)
The whole of the above is also considered a single expression.
You can also use a semicolon to separate expressions:
x+y; a+b;
Function bodies are considered expressions:
def myfunc() 5
This defines a function, myfunc
, which takes no arguments, and returns 5
when invoked by myfunc()
elsewhere in the code.
To group together one or more expressions procedurally, for instance as a clause in an expression or in the body of a function, use curly braces to form a block:
def main(){
print ("Hello!")
var x=invoke()
x+=1
x
}
With any expression block, the last expression is the one returned from the block. To that end, the x
as the last line of the function body here works as an implicit return. You could also say:
def main(){
print ("Hello!")
var x=invoke()
x+=1
return x
}
For the most part, Aki does not care about where you place linebreaks, and is insensitive to indentation. Expressions and strings can span multiple lines. Again, if you want to forcibly separate lines into expressions, you can use semicolons.
However, comments (anything starting with a #
) always end with a linebreak.
Use the def
top-level keyword to define a function:
def f1(x:i32):i32 x
Function definitions need to have:
- a name that does not shadow any existing name or keyword (except for functions with varying type signatures, where the same name can be reused, or where a bare prototype is redefined with the same type signature and an actual function body)
- zero or more explicitly-typed arguments
- a return type
- and a function body.
In the above example:
- the name is
f1
- the single argument is
x
, with an explicit type ofi32
- the return type is
i32
- and the body is simply the expression
x
.
A more complex function body example:
def f1(x:i32):i32 {
var x = (if x>0 1 else -1)
x * 5
}
Functions can also take optional arguments with defaults:
def f1(x:i32, y:i32=1) x+y
Invoking this with f1(0,32)
would return 32
. With f1(1)
, you'd get 2
.
Note that optional arguments must always follow mandatory arguments.
There is no shadowing of variable names permitted anywhere. You cannot have the same name for a variable in both the universal and current scope.
Scalar types -- integers, floats, booleans -- are passed by value. All other objects (classes, strings, etc.) are automatically passed by reference, by way of a pointer to the object.
The following operator symbols are predefined:
var x:i32=5
if x==5 then print("OK") else print ("No)
if x!=5 then print("No") else print ("OK")
if x>=5 then print ("OK") else print ("No")
if x<=5 then print ("OK") else print ("No")
x=x+1
x=x-1
x=x*5
x=x/2
Logical and
, or
, xor
, and not
.
Parentheses are used to set aside clauses in expressions, and to identify the arguments for a function.
Curly braces are used to set aside expression blocks.
The hash symbol is a comment to the end of a line. (This is one of the few cases where linebreaks are honored as syntax.)
def main(){
# This is a comment.
do_something() # Comment after statement.
}
There is no block comment syntax. However, an inline string can span multiple lines, and is discarded at compile time if it isn't assigned to anything. This can be used for multiline comments.
def main(){
'This is a multiline
string that could be a comment.'
do_something()
}
The @
symbol is used to indicate a decorator.
"Top-level" keywords can only appear as the first level of keywords encountered by the compiler in a module. E.g., you can have a def
as a top-level keyword, but you cannot enclose another def
or a const
block inside a def
. (At least, not yet!)
A const
block is used to define compile-time constants for a module.
const {
x=10,
y=20
}
def main(){
print (x+5) # x becomes 10 at compile time, so this is always 15
}
There can be more than one const
block per module.
It's also possible for constants to be defined at compile time based on previous constants:
const {
WIDTH=80,
HEIGHT=32,
FIELD_SIZE = (WIDTH+1)*(HEIGHT+1)
DIVIDER_WIDTH = WIDTH+1
}
In this example, FIELD_SIZE
would be defined as 2673
, and DIVIDER_WIDTH
would be 81
.
Define a function signature and its body.
def add(a,b){
return a+b
}
The default type for function signatures and return types, as with variables, is i32
.
Function signatures can also take explicitly specified types and a return type:
def add(a:u64, b:u64):u64{
return a+b
}
Defines an external function with a C calling interface to be linked in at compile time.
An example, on Win32, that uses the MessageBoxA
system call:
extern MessageBoxA(hwnd:i32, msg:ptr i8, caption:ptr i8, msg_type: i8):i32
def main(){
MessageBoxA(0,c_data('Hi'),c_data('Yo there'),0:byte)
}
A uni
block defines universals, or variables available throughout a module. The syntax is the same as a var
assignment.
There can be more than one uni
block per module, typically after any const
module. This rule is not enforced by the compiler, but it's a good idea, since uni
declarations may depend on previous const
declarations.
uni {
x=32,
# defines an i32, the default variable type
y=64:u64,
# unsigned 64-bit integer
z:byte=1B
# explicitly defined byte variable,
# set to an explicitly defined byte value
}
These keywords are valid within the body of a function.
Exit a loop
manually.
x=1
loop {
x+=1
if x>10 break
}
x
[output:]
11
See match
.
If a given expression yields True
, then yield the value of one expression; if False
, yield the value of another. Each then
clause is an expression.
var y = 0
var t = {if y == 1 2 else 3}
Each branch of an if
must yield the same type. For operations where the types of each decision might mismatch, or where some possible decisions might not yield a result at all, use when/then/else
.
if
constructions can also be used for expressions where the value of the expression is to be discarded.
# FizzBuzz
def main(){
# Loops auto-increment by 1 if no incrementor is specified
loop (var x = 1, x < 101) {
if x % 15 == 0 print ("FizzBuzz")
else if x % 3 == 0 print ("Fizz")
else if x % 5 == 0 print ("Buzz")
else printf_s(c_data('%i\n'),x)
}
0
}
Note that if we didn't have the return 0
at the bottom of main
, the last value yielded by the if
would be the value returned from main
.
Defines a loop operation. The default is a loop that is infinite and needs to be exited manually with a break
.
x=1
loop {
x = x + 1
if x>10: break
}
A loop can also specify a counter variable and a while-true condition:
loop (x = 1, x < 11){
...
}
The default incrementor for a loop is +1, but you can make the increment operation any valid operator for that variable.
loop (x = 100, x > 0, x - 5) {
...
}
If the loop variable is already defined in the current or universal scope, it is re-used. If it doesn't exist, it will be created and added to the current scope, and will continue to be available in the current scope after the loop exits.
If you want to constrain the use of the loop variable to only the loop, use with
:
with x loop (x = 1, x < 11) {
...
} # x is not valid outside of this block
A built-in unary for negating values.
x = 1
y = not x # 0
Exits from a function early and returns a value.
def f1(x):str {
if x == 1 return "Yes"
# else ...
"No"
}
Note that the return value's type must match the function's overall type, and that all returns must have the same type. This is not permitted:
def f1(x) {
if x == 1 return "Yes"
# else ...
5
}
This, however, is okay. Note how the function has no explicit return type, but all the returned values have matching types.
def f1(x) {
if x == 1 return "Yes"
# else ...
"No"
}
Evaluates an expression based on whether or not a value is equal to one of a given set of compile-time constants (not expressions).
The value returned from this expression is the selected value, not any value returned by the expressions themselves.
There is no "fall-through" between cases
The default
keyword indicates which expression to use if no other match can be found.
select t {
case 0: break
case 1: {
t+=1
s=1
}
case 2: {
t=0
s=0
}
default: t-=1
}
Designates an expression or block where direct manipulation of memory or some other potentially dangerous action is performed.
This is not widely used yet.
Defines a variable for use within the scope of a function.
def main(){
var x = 1, y = 2
# implicit i32 variable and value
# multiple variables are separated by commas
var y = 2:byte
# explicitly defined value: byte 2
# variable type is set automatically by value type
var z:u64 = 4:u64
# explicitly defined variable: unsigned 64
# with an explicitly defined value assigned to it
}
For a variable that only is valid within a specific scope in a function, use with
.
Defines a loop condition that continues as long as a given condition is true.
var x=0
while x<100 {
x+=1
}
Provides a context, or closure, for variable assignments.
y=1 # y is valid from here on down
with var x = 32 {
y+x
# but use of x is only valid in this block
}
As with variables generally, a variable name in a with
block cannot "shadow" one outside.
y=1
with var y = 2 { # this is invalid
...
}
If a given expression yields True
, then use the value of one expression; if False
, use the value of another.
Differs from if/then/else
in that the else
clause is optional, and that the value yielded is that of the deciding expression, not the then/else
expressions. This way, the values of the then/else
expressions can be of entirely different types if needed.
when x=1 do_something() # this function returns an u64
else if x=2 do_something_else() # this function returns an i32
else do_yet_another_thing() # this function returns an i8
In all cases the above expression would return the value of whatever x
was, not the value of any of the called functions.
An unsigned true or false value.
Constant representation of 1: 1:bool
An unsigned byte.
Constant representation of 1: 1:byte
Signed integers of 8, 32, or 64 bit widths.
Constant representation of 1: 1:i8, 1:i32, 1:i64
The default variable type is a 32-bit signed integer (1:i32
).
Unsigned integers of 8, 32, or 64 bit widths.
Constant representation of 1: 1:u8, 1:u32, 1:u64
Floats of 32 or 64 bit widths: 3.2:f32
/ 3.2:f64
.
Constant representation of 1: 1.
or 1.0
.
An array of scalar (integer or float) types.
For a one-dimensional array of bytes:
var x:array byte[100]
For a multidimensional array of bytes:
var x:array byte[32,32]
⚠ There is as yet no way to define array members on creation. They have to be assigned individually.
⚠ There is as yet no way to nest different scalars in different array dimensions.
⚠ There is as yet no way to perform array slicing or concatenation.
A string of characters, defined either at compile time or runtime.
hello = "Hello World!"
hello_also = 'Hello world!'
hello_again = 'Hello
world!'
Linebreaks inside strings are permitted. Single or double quotes can be used as long as they match.
String escaping functions are not yet robustly defined, but you can use \n
in a string for a newline, and you can escape quotes as needed with a backslash as well:
hello = "Hello \"world\"! \n"
⚠ There is as yet no way to perform string slicing or concantenation.