AFlat aims to trust that developers know what they are doing. It allows and encourages things that many other modern languages do not, such as pointer asthmatic, memory management, and so on. While aflat does allow some developers to do some dangerous things, it also provides optional safety features that can be used to prevent some of the more dangerous things. AFlat is not a language for everyone, but it is a language for careful developers who want to have more control over their code and their programs.
A 4 byte signed int
int i = 0;
A 4 byte floating point value
float f = 0.3;
An 8 byte signed integer
long l = 12;
A 2 byte integer
short s = 1;
A 1 byte bool
bool b = false;
An 8 bit memory address. In aflat, pointers point to any type so you can have a pointer to an int, a pointer to a float, and so on. This is different from C where pointers point to a specific type. This is done to allow for more flexibility in the language. Because of this, pointers in aflat are a bit dangerous. It is suggested to use boxing instead of pointers wherever possible.
adr a = NULL;
The generic type is essentially the same as the adr type, but it is used to indicate that the pointer is meant to point to some object. This is meant to be used with a function or class that needs to preform operations on any sort of reference type object. The generic type also suppresses implicit casting, which is useful when you want to make sure that the pointer is pointing to the correct type.
generic g = NULL;
Aflat supports type inference at declaration with the let
keyword. This should not be used when declaring a numeric type such as short, int, or long. Because aflat returns type number
from an int literal. The number type is a union of all numeric types and will break your code if it is tied to a variable. You should also take note that implicit casting will not happen when using type inference.
let i = "hello"; // this will assign an adr to i
Any Type Defined by a user will be an 8 bit memory reference to the given type
Type a = new Type();
A user defined type can be cast to and from an adr or generic... be careful! This is not type safe!
adr b = NULL;
Type a = b;
You will see more about this in the class section.
Functions in aflat are defined with the following syntax:
<access> <return Type> <function name> : <decorator name> (<arguments>){
<function body>
};
- Functions can be defined with a return type of any of the above types. Note that there is no return type for void functions.
- If a function does not return a value with the
return
keyword, a return statement with no value is implied. *- Keep in mind that this is unsafe as the function will 'return' whatever value is currently in the EAX register.
- For now functions can only have up to 6 arguments. This is due to not passing arguments on the stack and only using the registers.
Example:
int add(int a, int b){
return a + b;
};
- A function can have an optional argument by using the * opporator before. If the argument is not given, it will be passed as
NULL
Example:
int add(int a, * int b){
return a + b; // b will have the value 0 if no argument is passed there
};
The main function is the entry point for aflat. It is the first function called when aflat is run. It can take optional arguments int argc and adr argv for command line arguments.
int main(int argc, adr argv){
// do stuff
return 0;
};
It can also be implemented as follows if you do not need to pass command line arguments:
int main(){
// do stuff
return 0;
};
The name of a function without parenthesis returns a pointer to the function. This is useful for passing functions as arguments to other functions.
int add(int a, int b){
return a + b;
};
int main(){
adr foo = add;
int i = foo(1, 2);
return 0;
};
Functions can be called with the following syntax:
<function name>(<arguments>);
- The aflat compiler will check the number and type of arguments for functions when called, but not for function pointers.
- Function Pointers can be called with the same syntax.
example:
int add(int a, int b){
return a + b;
};
int main(){
int i = add(1, 2); // Be careful with function pointers! they are not type safe!
return 0;
};
Functions can be defined without a name. Antonymous functions.If the function body only has one statement, the curly braces are not required. Because aflat does not know the return type for anonymous functions, type inference cannot be used to determine the type. The compiler will assume that whatever you know what you are doing and will not check the return type. Be careful!
[<parameters>]=>{
<function body>
};
example:
int main(){
adr add = [int a, int b]=>{
return a + b;
};
int i = add(1, 2); // aflat assumes that the return type is int
return 0;
};
Functions under the global scope can be public(default), private, or export. - Public functions are globally accessible to the linker. This is necessary if using a header file. - Private functions are only accessible within the Module to avoid naming conflicts with other modules. - Export can be explicitly imported for use in another module and avoid naming conflicts during linking.
Functions can be decorated with functions. Functions that are decorated with a function must have a single refrence argument with the name _arg. This can point to an object with multiple properties if needed. The decorator function must take two arguments, adr foo and adr _arg.
adr decorator(adr foo, adr _arg) {
io.print("Hello from Decorator");
return foo(_arg);
};
int decorated(adr _arg) : decorator {
io.print("Hello from Decorated");
return 0;
};
int main() {
decorated();
};
The above example will print "Hello from Decorator" and then "Hello from Decorated" when the decorated function is called.
Class Decorators are also supported. And will be documented in the class section.
Declarations are used to define variables. They are used to define variables and functions.
<declaration> ::= <type> <identifier>;
<declaration> ::= <type or let> <identifier> = <expression>;
<assignment> ::= <identifier> = <expression>;
<load> ::= <identifier> =: <expression>;
Stores the value of an expression into the address pointed to by an identifier.
<return> ::= return <expression>;
<reference> ::= ?<identifier>;
Returns the address of the variable
<import> ::= import {<function>, <function>} from "path" under <ident>
| import <Class>, <Class> from "path";
| import * from "path" under <ident>; // try to avoid this
Import functions or classes from modules;
<int literal> ::= *<int>
eg: 123
<float literal> ::= *<int>.*<int>
eg: 123.456
<string literal> ::= "*<char>"
eg: "hello"
The string literal returns an adr. like a char * in C.
<not Expr> ::= !<expr>
eg: !(a == b)
<char literal> ::= '<char>'
eg: 'a'
<identifier> ::= *<char>
eg: foo
<compound expression> ::= <expression> <operator> <expression>
eg: 1 + 2 * 3
<parenthetical expression> ::= ( <expression> )
eg: (1 + 2) * 3
This can be used to override the order of operations.
<function call> ::= <identifier> ( <arguments> )
eg: foo(1, 2)
<lambda expression> ::= [<parameters>]=>{<function body>}
eg: [int a, int b]=>{return a + b;}
<reference> ::= ?<identifier>
eg: ?foo
Returns the address of the identifier.
<dereference> ::= <identifier> as <type>
eg: foo as int
Returns the value stored at the address. Casts the value to the type specified.
<variable> ::= <identifier>
eg: foo
<new> ::= new <type> ( <arguments> )
eg: new OBJECT(1, 2)
Dynamically allocates memory for an object of the specified type and calls the constructor with the arguments passed. The parenthesis are optional. The type must be a class. The constructor must be a function with the name init
. The value returned is the address of the object.
Note that currly braces are optional when the body is a singal statment;
If statements are used to execute code based on a condition. The syntax is:
if <expression> {
<code to execute if condition is true>
};
-
The condition is evaluated before the code is executed. If the condition is true, the code is executed. Condition must be a bool
-
Boolean conditional operators are
==
!=
>
>=
<
<=
-
Logical operators are. all logical operators are handled operators aflat does not have logical operators, the bitwise operators are used instead.
- | or
- & and
if statements can be used with else statements. The syntax is:
if <expr> {
<code to execute if condition is true>
}else{
<code to execute if condition is false>
};
While loops are used to execute code while a condition is true. The syntax is:
while <expr> {
<code to execute>
};
For loops are used to execute code a set number of times. The syntax is:
for <initializer> <boolean expr> <iterator>{
<body>
};
example:
.needs <io>
int main(){
for int i = 0; i < 10 i = i + 1
printInt(i);
return 0;
};
Classes in aflat are effectively structs that can implement functions and support encapsulation and rudimentary inheritance. The syntax is:
class <class name> signs <parent class>{
<contract>
<class variable declarations>
<class functions>
};
Class functions are functions that are declared with the following syntax:
class <class name>{
<return type> <function name>([<paramiters>]); // this is the function declaration it can be defined
//with the regular
// function syntax. Or it can be defined outside of the class via function
// scoping.
};
- Function scoping allows functions to be defined outside of the class. This is useful for separating implementation from declaration. syntax:
// scoped function
<return type> <function name>@<class name>([<paramiters>]){
<function body>
};
- A class function automaticly creates a pointer to the object that called it. It is stored in the my variable. This is useful for functions that need to access the class variables.
the constructor is a special function that is called when an object is created. The constructor must have the name init
. The syntax is:
class <class name>{
<class name> init([<paramiters>]) {
<function body>
return my; // if my is not returned, the compiler wil assume that the constructor returns the address of the object (my).
};
};
Class fields are variables that are declared in the class definition. They can be accessed from within the class by using the my
reference. The syntax is:
class <class name>{
<mutability> <acess modifier> <type> <field name> = <initial value>;
};
If an initial value is specified, an assignment is added to the top of the constructor.
The access modifier is used to determine the visibility of the field. The following are valid access modifiers:
- public
: the field is visible to all classes.
- private
: the field is visible only to the class that defines it.
Class decorators are used to turn a function into an instance of a class. It is syntactic sugar for passing a function pointer to the constructor. The syntax is:
class Decorator {
const adr foo = foo;
Decorator init(adr foo){
return my;
};
int runFoo(){
adr foo = my.foo;
return foo();
};
};
class HasDecoratedFunction {
int decorated() : Decorator {
io.print("decorated");
};
HasDecoratedFunction init(){
return my;
};
};
int main(){
HasDecoratedFunction hdf = new HasDecoratedFunction();
hdf.decorated.runFoo(); // decorated is not a function, it is an instance of the Decorator class.
return 0;
}
Contracts are used to create OO interfaces. The allows classes that sign them to behave as the parent class. The syntax is:
contract {
<contract var declarations>
};
the contract veriables are automatically included in any class that signs the contract. signing a contract is done with the following syntax:
class <class name> signs <contract name>{
<contract var declarations>
<class functions>
};
Here is an example of the intended use of a contract:
.needs <std>
import {print} from "io" under io;
class IWorker{
contract {
adr work = [] => io.print("Generic worker working...\n");
};
};
class Plumber signs IWorker{
// the init function should be the first function called to bootstrap the class
int init(){
my.work = []=>{ // this is the implementation of the work function as defined in the contract. It can also be done using
// function pointer
io.print("Plumber: I am tightening the pipes\n");
};
};
int getClients(){
io.print("Plumber: I am getting clients\n");
};
};
class Carpenter signs IWorker{
// the init function should be the first function called to bootstrap the class
int init(){
my.work = []=>{ // this is the implementation of the work function as defined in the contract. It can also be done using
// function pointer
io.print("Carpenter: I am building a house\n");
};
};
int buyTools(){
io.print("Carpenter: I am buying tools\n");
};
};
int doWork(IWorker worker){
worker.work();
};
int main(){
Plumber plumber = Plumber();
Carpenter carpenter = Carpenter();
IWorker generic = IWorker();
plumber.getClients();
carpenter.buyTools();
plumber.work();
carpenter.work();
generic.work();
doWork(plumber);
doWork(carpenter);
doWork(generic);
return 0;
};
output:
Plumber: I am getting clients
Carpenter: I am buying tools
Plumber: I am tightening the pipes
Carpenter: I am building a house
Generic worker working...
Plumber: I am tightening the pipes
Carpenter: I am building a house
Generic worker working...
- Keep in mind that when calling function pointer that are a part of a class, the first parameter is the pointer to the object. The function can be created with a variable named my or self.
There are two class modifiers currently implemented: safe
and dynamic
A safe class cannot be passed as an argument to a function or returned from a function as an l value. This is useful for keeping track of object ownership. The syntax is:
safe class <class name> signs <parent class>{
<contract>
<class variable declarations>
<class functions>
};
if there is a get() method defined in the class, that method will be implicitly called when attempting to access a safe object.
A dynamic class MUST be instantiated on the heap with the new keyword. If implicit casting is used, it will default to declaring on the heap. The syntax is:
dynamic class <class name> signs <parent class>{
<contract>
<class variable declarations>
<class functions>
};
When an object is created, the following steps are taken:
- The object is allocated on the heap or the stack depending on if new was used.
- The init function is called if present. (All initial value assignments are prepended onto the init function)
When an object goes out of scope, the following steps are taken:
- The endScope function is called if present.
When an object is deleted, the following steps are taken:
- The destructor function is called if present.
- The memory for the function is freed.
Much like in c or c++, aflat supports a header and source file interface; The header file contains the function and class definitions. The source file contains the implementation of the functions and classes. Header files should have the extension '.gs' and source files should have the extension '.af'.
Modules from the aflat standard library are included with the following syntax:
.needs <module name>
All other modules must be included with the following syntax:
.needs "path/to/module/header.gs"
A root directory for header files can be specified with the following syntax:
```c
.root "path/to/root/directory"
If a root directory is not specified, the current directory is used.
The list of standard headers are as follows:
- math
- Handles math functions
- std
- Deals with memory managment and time. Also carries the assert function
- asm
- a repository of syscall wrappers
example of using the std header file:
.needs <std> // the std module is included
int main(){
assert(1==1, "failed common assert"); // the std module contains the assert function
return 0;
};
Modules are the prefered method of sharing code over header files.
A module is only a .af source file with no header. The module can export functions and classes that can be used by other modules.
A function is exported using the export
modifier once a function is exported it cannot be accessed via header file.
Example:
export int add(int a, int b){
return a + b;
};
Classes are automaticaly exported by a module.
Importing is done with the import statment. If there is no "./" in the path given, aflat searches in the standard library directory for the statment. Otherwise the aflat searches the working directory. .af is automaticly appened to modual names if it is not there.
Functions are imported inside of curly braces. All functions can be imported using the * oporator. Functions are imported under a namespace defined using the under
keyword. The namespace must be used to access the function.
example:
import {add, sub} from "./src/Mod" under mod;
int foo(){
int a = mod.sub(5, 6);
return mod.add(a, 3);
};
Classes are imported outside of curly braces. No namespace is needed for classes. example:
import Player from "./src/GameEngin";
int game(){
Player p1 = Player();
};
note -- if a class signs a contract, the base class must be imported before the child.
The list of standard modules is as follows:
- Collections
- Handles arrays and list and the ICollection interface
- concurrency
- Handles threads and pipes defines the Process and MProcess classes
- DateTime
- Provides the DateTime class
- files
- Handles file IO and defines the File class
- io
- Handles input and output to the console
- strings
- Functions to deal with strings and convert between other types and strings
- String
- provides the Standard string object
- ATest
- Provides the testing framework for Aflat
Example:
.needs <std>
import {print} from "io" under io;
int main(){
io.print("Hello World!\n");
};
The Aflat package manager is built into the compiler.
The package manager is used to create a project. The syntax is:
aflat make <project name>
The project name is will be the name of the directory that will be created. It will create a head, src, and bin directory. The head directory will contain the header files for the project. The src directory will contain the source files for the project. The bin directory will contain the compiled object files for the project. It will also create an aflat.cfg file which will contain settings for the compiler.
The project can be built with the following syntax:
aflat build
This will compile all of the source files in the src directory and call gcc to link them into an executable. The executable will be placed in the bin directory.
The project can be run with the following syntax:
aflat run
This will compile all of the source files in the src directory and call gcc to link them into an executable. The executable will be placed in the bin directory. The executable will then be run.
The package manager is used to create a src/header pair. The syntax is:
aflat add <src/header name>
This will create a header and source file in their respective directories. It will also create and entry in the aflat.cfg file telling the compiler to compile the new source file.
The package manager is used to create a module. The syntax is:
aflat module <module name>
This will create a source file in the src directory. It will also create and entry in the aflat.cfg file telling the compiler to compile the new source file.