Module source file contains three parts (order matters):
- Module dependencies and name import declarations
- Constants, classes and functions definitions
- Main application code (only for starting module)
Dependencies
using moduleA; // Simple dependency declaration
using moduleB { // Dependency with name imports
functionName,
constantName,
ClassName,
MyName = ClassName // alias declaration
}
In this example moduleA
is used without name imports, thus all names of that module can be referred using long names: moduleA_functionfromModuleA
For example: sys_getParent
.
While moduleB
is used with imports. Function, constants, class and enumeration names from moduleB
that were listed in the {}
of the using declaration is accessible to our module by their short names:
using sys { getParent }
...
x = getParent(y);
It is possible to give an alias name to any name from the module: aliasName = actualName
.
Example:
using sys { gp = getParent }
...
x = gp(y);
System module sys
contains predefined runtime data types and core functions. This module don't have to be used
, but if you need to import its names, use the same using
declaration as with all other modules.
Constants, enums, classes and functions definitions
There is no define-before-use requirement.
And thus there are no separate declarations and definitions.
fn myFunction() { myOtherFunction() } // `myOtherFunction` defined later
...
fn myOtherFunction(){ ... }
Constants
const xPi = 3.14;
const xEmptyNode = *Node;
const xConfigName = *"config.cml"
Constants must be of primitive or immutable object types.
If constant is an object and compiler was unable to define its value at compile type, it places the code that creates this constant before the program entry point on the main thread. If the constant object has an onDispose
handler, it will be called after the program ends also on the main thread.
Order of constant handling is well-defined:
- Depended modules initialized after all its dependencies.
- Inside the module constants are initialized in the order of definition.
- Destruction performed in the reverse order.
Function definition
fn name(parameterName parameterType,...) resultType { expressions }
Functions always belong to the module they are defined in, so function name must not include module name, same as constants.
If result type is absent, function returns void
. If function body is absent (';' instead of { expressions }
) , this means that function is defined externally: there must be a linked object file, containing function ag_fn_<moduleName>_<functionName>
(FFI will be explained in a separate post).
Example:
fn abs(x int) int { x < 0 ? -x : x }
Interface definition
interface Name {
method(parameterName parameterType,...) resultType;
...
}
Interface is an abstract type that cannot be instantiated but there may exist variables and values of this type. These variables point to classes, that implement these interfaces.
Unlike Java and Go where interface method invocation is considerably slower than class method invocation, Argentum interface method calls have approximately the same complexity as the invocation of the ordinary class methods.
Argentum has a unique feature named "open classes". This means that any module can "define" the class of another module to populate this class with additional fields or methods. ("open classes" will be explained in the separate post).
Because of open classes, interface and class names may be of "long_form" including module names, in the following example we extend an Actor
interface from the gui
module with a new method:
interface gui_Actor {
getPosition() Point;
}
Interfaces can have base interfaces:
interface Shape {
+Scaleable;
+Positional;
+Sizeable;
isPolygonal() bool;
}
In this example the Shape
interface extends many other interfaces and adds one extra method.
(Prospected: default method implementations)
Class definition
class Name {
+BaseClassOrInterface {
// Methods implementing this interface
// Or overriding the existing implementation of this base
}
field = initializer;
newMethod(parameters) resultType { expressions }
}
Class is an interface with additional features:
- All interfaces have implementation
- Class can have one base class
- Class can have fields
Class fields must have initializers. It's a bad practice in other languages to have uninitialized fields. The performance gain doesn't worth UB. Zero initialized fields are bad too, b/c this gives 0 a special magical meaning. That's why all fields in Argentum are always initialized with expressions, which define field initial values and types :
class Node {
x = 0;
y = 0;
children = Array(Node);
name = "Unnamed";
}
If a class implements an interface, it must include all interface implementation in the curly braces after that interface name. This distinguishes own class methods from interface implementations, as well as implementations of the different interfaces:
class SceneItem {
+Sizeable {
setSize(width double, height double) { ... }
getWidth() double { ... }
getHeight() double { ... }
}
+Positional{
...
}
paint(Canvas c) { ... }
}
This separation plays three roles:
- It prevents name collisions.
- It makes code more organized and improves readability.
- It allows IDE to autocomplete: when cursor is inside the
{}
of a certain+Base
it becomes obvious what methods are suitable.
If a class inherits from a base class, it includes this base the same way as it does with interfaces.
If this class wants to override some method of the base class, these method definitions go in the same curly braces.
If a class inherits an interface multiple times (through base class of through multiple implemented interfaces), it should include only one method implementation, which can be included in any suitable curly braces.
Argentum classes don't have destructors. All objects are freed automatically according to the composition-aggregation-association rules. It is not like Java when you need to unsubscribe all listeners of object-to-be-deleted in order to prevent the object from becoming ever-lasting garbage.
Argentum class don't have constructors, because it contradicts the idea of deserialization. Instead argentum class can be always constructed with one legit initial state (with fields initialized with built-in initializer expressions) and populated with direct field assignments and/or calls of special initializer this-methods or using initializer operator.
Couple of words on this-methods:
class Person {
firstName = "";
lastName = "";
age = ?int;
setName(first String, last String) this { // <-- this-method
firstName := first;
lastName := last
}
setAge(val int) this { age := val } // <-- another this-method
}
p = Person.setName("John", "Doe").setAge(44);
This-method has this
keyword in place of return data type. The body of this-method return nothing (like void). Then called, they return the same type as their this
-parameter.
This-methods have multiple advantages:
- They can act as named "constructors".
- They can be combined on the same object (as in the above example).
- If applied to the instance of the descendant class, they return the type of that class, not base.
- If applied to @Pointer, they return the same @Pointer, but when applied to pin-pointer, they return pin-pointer. In another words, they can be applied to the newly created instance (as constructors) or to some existing objects (as "resetters") and they preserve this information in the resulting type.
- They allow compiler to eliminate multiple retain-release operations.
(Prospected: fields and methods starting with underscore are private. Fields starting with set and get can be used as property setters/getters)
Main application code
It goes after all class-const-function definitions.
Only starting modules can have this section.
It is identical to methods/functions body (but without curly braces).
It consists of expressions separated with ';'.