Macrocoder Language (FCL) is specifically designed to develop Domain Specific Languages software tools.
The rules enforced by Macrocoder have been carefully designed to avoid at compile time many of the pitfalls that developers have to face when writing this kind of tools.FCL is an object oriented language supporting classes, interfaces and a new concept called phase programming.
The execution of a DSL tool is a batch process that starts from the users' sources and ends with the production of the expected results.
This process can be divided internally in a sequence of "steps", each one reaching an intermediate goal.
For example, let's define a language that implements a simple calculator:
A = 12
B = 70
C = A * 2 + B - 10
print contents of C
We expect the DSL tool to understand this source and print on its output "84".
Internally the DSL tool executes the following logical steps:
1) parse the source and load all the instructions given by the user
2) register the existence of the variables A, B and C; in this context it can verify that a variable has not been defined twice;
3) resolve the references; in order to solve the operation, in "A * 2 + B - 10" the DSL tool has to find "A" and "B" and retrieve their values; in this context, it can complain if the user used undeclared variables.
4) print the result, now available.
The Macrocoder Language (FCL) natively supports this process by implementing "phase programming". Phase programming is based on the following concepts:
the programmer defines a sequence of phases that are to be executed in a given order;
each phase has a goal: for example registering the variables, resolving the references or doing the final print;
the involved classes have a "pre phase" and/or a "on phase" methods that are invoked when the associated phase is being executed;
the above phase methods are called in a predefined order among objects: given a phase p, a "pre phase p" method on an object o is executed before any other phase method of its children, while its "on phase p" is executed after;
Phase programming includes the concept of phase protection. An attribute a of an object o might be calculated during the "on phase p" method of o. The phase protections allows to:
forbid any access to a before phase p;
allow read-write access to a only to object o during phase p;
allow public read access to a after phase p;
These access rules ensure that never an object can erroneusly access information that is not ready yet.
The FCL compiler checks these rules at compile time, de facto forbidding potentially erroneus programs to be even compiled.
Also note that FCL, with phase protection, provides with object restricted access, which is more restrictive than the class restricted access offered by the usual private or protected keywords: while the private keyword allows any instance of a given class to access private data of any other instance of that class, the phase protection allows only the owning object to access its data.
A "project" is an ensable that keeps together all the files related to a given activity.
Macrocoder supports two kinds of projects:
- the rules project contain the implementation of the DSL tool; they contain the grammar definitions and the code that generates the output;
- the target project contains the sources written by the user.
In order to be able to do anything, Macrocoder needs always a rules and a target project.
This chapter explains all the programming concepts of the Macrocoder Language. For the detailed formal grammar definition of FCL see chapter "Macrocoder grammar".
As usual with object oriented languages, FCL code is based on the definition of classes. A class is the definition of a type that contains the description of both the data, in class items called attributes, and the code required to manipulate it, in items called methods.Classes can be instanced in objects, that use the class definition as a template to contain real data.
The general definition format for a class is:
class className [: baseClass] {
...attributes and methods
}
A class purpose can be data (if defined outside a lifeset) or structural (if defined inside a lifeset). See Types purpose for more details.
Normally objects are instanced during the initial phase. If objects need to be instanced during other phases, an instancing deadline has to be specified. This can be done only on non-deriving classes (derived classes inherit the instancing deadline by their ancestor). This is allowed only on structural classes.
class className phase=phaseName {
...attributes and methods
}
See grammar specification b_class_data for detailed grammar specification.
Attributes are the containers where data is stored within an object. They are declared in the class among their type. The general definition form is:
class className {
[streamable] [shared|phased|prephased] [enable=phase1][finalize=phase2] attrType attrName [= initialValue | init (parameters)];
}
the streamable keyword is used when the object has to be saved using the encodeXml or other streaming functions; only the attributes marked "streamable" will be saved and reloaded;
the shared/phased/prephased keywords determine the phasing access of the attribute. A complete description of the access rules can be found at the chapter Attributes access rules. In short, phased attributes can be written only by a on phase method of the owning instance; prephased attributes can be written on a pre phase method, while shared methods can be written by other objects as well. Default for structural classes is phased; on data classes, these keywords are meaningless and can't be used.
the enable=phase command specifies the phase when the attribute is enabled; before being enabled, access to the attribute is always forbidden. Default is whatever has been specified as finalize. This command is allowed within structural classes only.
the finalize=phase command specifies the phase when the attribute becomes public and read-only; default is initial. This command is allowed within structural classes only.
the initialValue value specifies an initial value other than the zero/empty condition assumed by default by all objects; only types that can be expressed as literal values can have their initial value specified in this form;
the init keyword can be used to pass parameters to the object constructor if required.
Example of structural class with attributes:
class MyClass {
enable=myPhase1 finalize=myPhase2 Int myData;
shared finalize=myPhase3 String myText;
Int startingYear = 1950;
Vector myPoint init (104, 21);
}
Constants are numeric or string values that can be associated to a textual identifier for convenience. Declaration of a constant is done like a normal attribute but prefixed with the const keyword:
class MyClass {
const Int MY_CONSTANT = 9876;
}
Constant values can be defined only of those types supporting literal values.
Methods contain the code needed to manipulate their class data and execute the tasks their class has been designed for.
There are two kinds of methods:
non-static - these methods operate on an object instance; they have to be invoked with the o.m(...) form, where "o" can be implicit; they have an implicit parameter named this which contains a reference to the object "o" initially used to invoke the method;
static - these methods operate without any object instance but only relying on their parameters; static methods are what is normally called a "function" in not object-oriented languages.
General form for static methods:
class className {
static [ref] [retStatus] types methodName ([parameter [,parameter [,...]]]);
}
where parameter has the following format:
[out] [paramStatus] types parameterName [= defaultValue]
Besides special methods like constructors and evolutionary methods, described in their own chapters, this is the general form for non-static methods:
class className {
[ref] [retStatus] types methodName ([parameter [,parameter [,...]]]) [const] [thisStatus] [phase=p1 [,p2]];
}
the ref keyword means that return value is read-write; otherwise, the return value will be a read-only value;
retStatus and paramStatus are one among initiating, transitional or pretransitional; it indicates the hypertype status of the returned object or parameter; if not specified, default is simple;
the types entry is a single class name or one or more interface specifications (see Multiple types); in case of multiple interfaces, they have to be listed within angle brackets: <I1, I2, ..., In>.
the const keyword indicates a non-static method that will not modify the object on which it has invoked; in other words, the implicit this parameter will be const;
the thisStatus entry is one among phased, prephased, initiating, transitional or pretransitional; it indicates the hypertype status of the invoking object, i.e. of the implicit this parameter. Note that phased and prephased make this method an evolutionary method like on phase or pre phase. If not specified, default is simple.
the out keywords indicates, that the parameter is used for both input and output; the related parameter will not be const;
the defaultValue is a value that is taken as a default if parameter is not specified; it is allowed only on simple types, like Int or String; if a default value is specified for a parameter, all the subsequent parameters must have a default value as well.
the phase=p1 or phase=p1,p2 keywords indicate the invokeability phase range of the method;
Here some method declaration examples:
class MyStructuralClass {
Void method1 (Int x = 1234) const;
Void method2 () phased phase=phase1;
ref Int method3 () phase=phase1,phase2;
static Void doubleValue (out Float value) const;
}
Methods and attributes can be protected against other classes access. Macrocoder supports the usual public/protected/private keywords:
public - attributes and methods are accessible by any class.
protected - attributes and methods are accessible by any class of the same type or derived.
private - attributes and methods are accessible by any class of the same exact type.
For example:
class MyClass {
public:
Void method1 () {}
protected:
Void method2 () {}
private:
Void method3 () {}
}
These protections are seldom used in Macrocoder, where usually everything is left public then protected with the more powerful phase protection.
Constructors are special methods that are executed when the object is instanced. Constructors may have zero or more parameters.
General form for constructors is:
class className {
constructor ([parameter [,parameter [,...]]])[: callbase ([parameter [,parameter [,...]]])];
}
The callbase statement allows chaining another constructor from the base class; the invoked base class constructor will be executed before the code related to this constructor.
If a constructor without parameters is specified, it will be executed by default every time an object of that type will be instanced without constructor parameters.
Phase methods are invoked during phase evolution (see phase programming for their usage). This is their general form:
class className {
on phase phaseName {...}
pre phase phaseName {...}
}
Sometimes heterogeneous objects need to go through a common procedure. For example, we could have a set of various objects and with the need of simply print them on the screen. Now, the concept of "print an object" on the screen is strictly related to the nature of the object itself: for example, an object representing a person could print name and age, while another object representing a car should print brand and model.
This problem could be solved if every object had a printOnScreen method: the caller could then call printOnScreen on every object without caring how to print each object.
However, having a common method is not enough: there must be something formal guaranteeing that method is available and designed for that purpose: this is obtained by using interfaces.
An interface is like a class where only methods can be defined. Normally methods are defined without an implementation (called abstract methods), just to be a placeholder for the real method.
Then, the classes interested in providing a given feature can expose the related interface, providing with the required implementation.
The example below shows an interface and an implementation:
interface Printable {
Void printOnScreen () abstract;
}
class TPerson {
String name;
Int age;
expose Printable {
Void printOnScreen () {
system ().msg << "PERSON name=" << name << " age=" << age << endl;
}
}
}
Now, we can pass a TPerson wherever a Printable is expected, since TPerson is exposing all methods required to be a Printable.
Inheritance is the creation of a new class (or interface) B using another class (or interface) A as a starting point. In this case, we say that A is the base class of B or, alternatively, that B derives from A.
A derived class respects the following rules:
the derived class always has at least the same attributes, methods and exposed interfaces of the base class;
the derived class can add further attributes, methods or expositions;
the derived class may redefine the implementation of methods inherited from the base class;
Thanks to the rules above, an object of type B, derived from A, can be used where an object of type A is expected. Whoever is expecting an object of type A will find in an instance of B all the methods, attributes and expositions that it would find on an instance of A.
The operation of passing a derived object instead of base one is called upcast: this operation is done automatically and it is always allowed.
Vice versa, it might happen to have a generic reference to an object of type A that actually contains an instance of B. In order to access the extra methods or attributes that B has, the object needs to be explicitely downcasted using an explicit cast.
class Base {
Int a1;
Void m1 () {...}
Void m2 () {...}
Void m3 () {...}
}
class Derived: Base {
Int a2;
Void m1 () {...new implementation...}
Void m2 () {callbase (); ...extra implementation...}
Void m4 () {...}
}
In the example above, class Derived derives from class Base:
attribute a1 has been inherited by Derived; therefore, although Derived does not contain a "Int a1;" entry, it actually has it implicitely;
attribute a2 has been added only to Derived;
method m1 is inherited and completely overridden by Derived, that added a completely different implementation;
method m2 is inherited but extended with extra code which is tailed to the previous code thanks to the callbase statement;
method m3 is inherited and unchanged; the Derived class does not cite it, but it is automatically inherited;
method m4 is a new method that is added by Derived;
Note that:
the derived class can never override an attribute already defined in the base class;
in order to override a method, this has to be redefined with the same name, parameters and return type of the base one;
the derived class must have the same purpose of the derived class: they must be both either structural in the same lifeset or data;
a class can derive only from a class, while an interface from an interface.
The extension is the addition of new methods, attributes or expositions to an existing class or interface. Adding new items in one of the extension has the same effect of adding in the main definition.
For example:
class MyClass {
Void method1 ();
}
extend class MyClass {
Void method2 ();
}
has the same effect of:
class MyClass {
Void method1 ();
Void method2 ();
}
The extension concept can be used to split a type definition among multiple files divided by their role. For example, a class might have methods devoted to names resolution, Java generation and reporting. These method can be conveniently spread among multiple files for better code clarity. Instead of having a file containing class A with all its methods and another one with class B with all its methods, it is possible to have a file containing the parts of A and B dedicated to "names resolution", another file with the parts of of A and B dedicated to "Java generation" and a third file the parts of A and B dedicated to "reporting". In this way, code can be grouped by function.
Another useful side effect of extension is that enhances resuability: we could take a project that already works and add for instance "PHP code generation" by simply adding a file that extends the involved classes, keeping the original files intact.
The processing of user sources in a DSL tool is a sequence that starts with the analysis of the user sources and terminates with the production of the expected results or some error messages. Since no interaction with the user is expected during this process, this kind of approach can be defined "batch".
In Macrocoder this process is called lifecycle, A lifecycle is a set of classes, types, interfaces, methods and phases that define how data is processed. Normally, a Macrocoder project has one lifecycle.
A lifecycle is an overall class that contains all the procedures to transform the input sources into the final product. Normally, in one application there is one lifecycle; however, multiple lifecycles can be defined if different scenarios have to be supported.
A lifecycle contains one or more lifesets. Lifecycles are signletons, i.e. for each defined lifecycle, there is one class and one single global instance of it.
A lifeset is a special container class in which phase evolution occurs. Lifesets have the following characteristics:
they can be defined only inside a lifecycle;
they are automatically instanced by the system and they can not be instances elsewhere;
they contain phase definitions;
classes defined inside a lifeset are structural;
lifecycle MyLifeCycle {
lifeset MyLifeSet {
phase myPhase1 = 1;
phase myPhase2 = 1234;
}
}
A lifeset L1 can be defined as creating lifeset L2. This means that:
all phases of L1 are executed before any phase of L2;
lifeset L1 will contain an attribute of type L2 named "L2" (same name of its type) that is a link to the L2 single instance; this link allows L1 to feed L2 by creating its initial instances. This creation chain, that must not contain any loop, is the basis of the multiple lifeset operation in phase programming.
lifecycle MyLifeCycle {
lifeset MyLifeSet {
phase myPhase1 = 1 creates MyNextLifeSet;;
on phase myPhase1 {
var MyNextLifeSet::FooBar fb;
fb.value = 1234;
lset.MyNextLifeSet.enroll (fb);
}
}
lifeset MyNextLifeSet {
phase printResults = 100;
class FooBar {
Int value;
on phase printResults {
system ().msg << "FooBar value=" << value << endl;
}
}
}
}
The example above shows how lifeset MyNextLifeSet is fed by lifeset MyLifeSet during phase myPhase1.
If working with the default predefined lifeset MAIN, the syntax above can be shortened and with less indentation:
lifeset MyLifeSet;
phase myPhase1 = 1 creates MyNextLifeSet;
on phase myPhase1 {
var MyNextLifeSet::FooBar fb;
fb.value = 1234;
lset.MyNextLifeSet.enroll (fb);
}
...etc
Methods declarations must be completed with their implementation. A method implementation is a sequence of statements that are to be executed when the method is invoked.
Method implementations can be specified either inline (near the method declaration) or offline (far from the method declaration):
class MyClass {
Void method1 () {...implementation...}
Void method2 ();
}
impl MyClass::method2 {
...implementation...
}
In the example above, method1 is implemented inline, while method2 is implemented offline. Note that offline implementation can be in any file as long as included in the project.
There is no semantic difference between inline and offline implementation: one or the other should be chosen for better code readability.
In case two polymorphic methods are implemented externally, the implement as keyword can be used to avoid ambiguous "impl" names:
class MyClass {
Void method1 ();
Void method1 (Int x) implemented as method1bis;
}
impl MyClass::method1 {
...implementation...
}
impl MyClass::method1bis {
...implementation...
}
While types define methods and data structures, the real data is contained in the computer memory by mean of objects.
Within Macrocoder, all objects belong to one of the following kinds:
constants - they can be either constants, like const Int PI=3.1415, or literals, like 100 or "Hello world"; these objects can not be modified nor owned by a variant or an array;
standalone - standalone objects are created using the var or the new statements; both statements have the same effect and offer a different way to do the same action; these objects stay alive as long as there something referencing them or one of their attributes, then they die. Standalone objects can be associated to variants and arrays: in that case, they become owned standalone objects.
attributes - attributes are objects defined as attributes of other objects; their existence is strictly bound to the existence of the object containing them; these object can not be used as data for variants or arrays.
Literals are constant objects expressed by their value. They can be:
integers, like for example 123 or -99; they are normally of type Int, except when exceeding the 32-bit signed representatation; in that case, they will be of type Huge;
floating point values, like 123.45, -99.12 or 8.2e+4; their type is Float;
strings, included in double quotes, like "this is a string"; their type is String and they support various backslash escaped characters like:
\n LF, ASCII code 10;
\r CR, ASCII code 13;
\t TAB, ASCII code 9;
\0 ZERO, ASCII code 0;
\\ backslash;
\" double quote;
\xnnnn UNICODE value expressed in hex (for example "\x0041" is the same as "A");
For example, the string "This is a 24\" monitor" becomes is translated to This is a 24" monitor. Thanks to the escape character, the string can contain sequences that would be unreadable (like tabs or weird unicode characters), or it would be mistaken for a control character (like the double quote itself).
decostrings, using the special "freetext" editor mode; these strings, of type Decostring, can contain any character and decoration codes (like bold, italic and underscore);
In order to work with standalone objects, Macrocoder provides with means to create and reference them:
creation is the action of creating the object; creation may include constructor parameters, where appliable;
referencing is a way to give names to objects: in this way it is possible to determine on which objects actions have to be performed.
See the examples below:
ref Int i -> new Int;
i = 123;
This example creates a "reference to an Int" called "i"; then associates to "i" a newly created object of type Int; then, in the second line, uses the name "i" to change the value of that object from the initial 0 to 123.
var Int i;
i = 123;
The code above is semantically identical to the previous one: the var statement creates both an object instance and a reference at once.
In both cases constructors can be specified during creation. For example:
ref Vector v1 -> new Vector (10, 20);
var Vector v2 (10, 20);
References are names that can be temporarely assigned to objects in order to access them.
They must be declared in method implementations using the ref statement with the general form below:
ref [const] [initiating|transitional|pretransitional] type name [-> object];ref [const] [initiating|transitional|pretransitional] <interf1, interf2, ..., interfN> name [-> object];
The reference must be defined to be compatible with the objects to be referenced. See also hypertypes. This implies that:
type must be a type to which the target object may be converted; therefore, type must be identical or a base class of the type of object;
interf must be interfaces exposed by object;
const must be specified if object access is constant; however, it can be specified on non-constant objects also to access them as they were constant;
the initiating|transitional|pretransitional must comply with the hypertype status of the object;
ref Int i -> new Int;
ref Int j -> x;
References can be reassigned at runtime using the -> operator; for example, i->x; causes i to become a reference to x:
var Int a;
var Int b;
// Create a reference "i" and have it reference "a"
ref Int i -> a;
// Now we are changing "a"
i = 100;
// Make "i" reference "b"
i->b;
// Now we are changing "b"
i = 200;
References can be null. This condition can be reached by unassigned references or by failed operations. The null condition can be tested using the valid() function, that returns true if the reference is not null or false otherwise.
// Unassigned reference, always null
ref Int x;
// Failed cast: y is null because s is a String and it
// can not be casted to an Int
var String s;
ref Int y = s.cast (Int);
// Test the valid condition
if (valid (y)) {System ().msg << "y is valid" << endl;}
else {System ().msg << "y is null" << endl;}
Expressions are a combination of literals, variables, operators and functions that are interpreted according to rules of precedence and of association, which compute and then produce another value.
Macrocoder supports all the usual mathematical operators with their conventional associativity and grouping.
Expressions have a type.
Boolean expressions are used by conditional statements. They must be of Int type and they are considered false if zero and true if not zero.
Two constant Int values are defined for boolean operations:
false defined as 0;
true defined as 1;
Boolean operators, like > or != return either true or false.
Explicit casts convert a reference to a type to another type. This is the general form:
obj.cast (type)obj.cast (<interf1, interf2, ..., interfN>)
This expression returns a reference of type "type" or <interf1, interf2, ..., interfN>; const settings and hypertype status are the same of obj.
Conversion can be done only at the following conditions:
type of obj must be a base class of type;
in case of interfaces, obj must expose interfaces interf1, interf2, ..., interfN;
If the conversion can not be done, the cast operation returns a null reference.
// Try to convert obj to type MyType
ref MyType mt -> obj.cast (MyType);
// Was it really a "MyType"?
if (valid (mt)) {
// Call methods available to MyType
mt.methodOfMyType ();
}
else {
// obj is not a "MyType" or derived;
// mt is null.
}
The Macrocoder object instances are organized in a proper tree, where the root is the object that han been enrolled in the lifeset cauldron and the various nodes and leaves are attributes, array and variant items.
Therefore, every object, except the root ones, has one and only parent.
The upscan action scans the objects tree seeking for the first object that matches the indicated type or interface; if none is found, it returns NULL.
class MyClassA {
Int x;
MyClassB b;
}
class MyClassB phase=p1 {
Void doAction () const {
system ().msg << upscan (MyClassA).x;
}
}
Statements are the smallest imperative instructions that can be used to define the implementation of a method.
Code blocks are groups of code enclosed in braces {...}. Each implementation has at least one code block. Code blocks are also used, for example, within conditional statements like if or while to identify the conditional part.
Each code block represents also a scope, where local variables, valid only within the block, can be defined.
The if statement conditionally exectues a code block. Its general form is:
if (expr) {statements1}
if (expr) {statements1} else {statements2}
Expression expr must return type Int; if expr value is not zero, then statements1 will be executed. Otherwise, statements2, if specified with the optional else keyword, will be executed.
if (a > 2) {
system ().msg << "greater than two" << endl;
}
else {
system ().msg << "smaller or equal to two" << endl;
}
The while statement repeatedly executes a code block until its expression becomes false. Its general form is:
while (expr) {statements}
Expression expr must return type Int; expr is evaluated; if its value is not zero, then statements will be executed; the expr will be evaluated over and over again, executing each time statements, until expr evaluates to false.
var Int x = 3;
while (x > 0) {
system ().msg << "x=" << x << endl;
x--;
}
system ().msg << "DONE!" << endl;
This produces the following output:
x=3
x=2
x=1
DONE!
The for statement repeatedly executes a code block until one of its expressions becomes false. It is very similar to the while statement, but for is better suited for loops. Its general form is:
for (expr1; expr2; expr3) {statements}
The for loop executes the following algorithm:
evaluate once expression expr1;
[LABEL] evaluate expression expr2;
if expr2 is false, terminate; otherwise, continue;
execute statements;
evaluate expr3;
goto LABEL;
The three expressions should be used with the objectives below:
expr1 does the initial setup of the counting variable;
expr2 check whether the loop in terminated or not;
expr3 executes the counter increment;
var Int i;
for (i=0; i<4; i++) {
System ().msg << "i=" << i << endl;
}
produces the following output:
i=0
i=1
i=2
i=3
The switch statement can be used to execute one among many blocks according to the value of an expression. The general form is:
switch (expr) {
case literal1: {statement1}
case literal2: {statement2}
...
case literalN: {statementN}
default: {statementDef}
}
The switch statement:
requires Int types only; therefore, expr and literals must be Int;
the statements associated to the literal that matches expr will be executed;
all literal values must be unique;
a given block can be associated to multiple literals using "case lit1,lit2,...,litN";
in case no literals are matched, the optional default entry is executed;
if no default is specified and no case are matched, nothing is executed.
The return statement is used to terminate a function and return the calculated value. The general form is:
return expr;
return;
First form is required for functions whose return type is not Void. These functions must always return a value using "return expr". The latter form is used within Void functions when execution has to be interrupted.
See below an example for a non-Void and a Void function:
class TestClass {
Int doubler (Int x) {return x * 2;}
Void printIfPositive (Int x) {
if (x < 0) {return;}
system ().msg << x;
}
}
The callbase statement invokes the base implementation of an overridden method. Thanks to class inheritance, a derived class can redefine a method that was already defined in its base class. See inheritance for further details.
callbase (param1, param2, ..., paramN);
The example below demonstrates a callbase invocation:
class ClassA {
Void method1 (Int x) {
system ().msg << "Hello from ClassA x=" << x << endl;
}
}
class ClassB: ClassA {
Void method1 (Int x) {
system ().msg << "Hello from ClassB x=" << x << " before" << endl;
callbase (x);
system ().msg << "Hello from ClassB x=" << x << " after" << endl;
}
}
Then, executing:
var ClassB b;
b.method1 (10);
produces:
Hello from ClassB x=10 before
Hello from ClassA x=10
Hello from ClassB x=10 after
Expression discard is an implicit statement that is executed when an expression result is not needed. This is needed when a function whose return value is not needed.
For example, let us consider a function print(Int n) that prints the given value and returns the number of character written. That function should be used in this way:
var Int nch;
nch = print (123);
However, if we do not need to know the number of printed characters, we can simply execute:
print (123);
Note that the expression "print (123)" still returns a value, which is atuomatically discarded.
The abort statement interrupts the execution immediately. It can be used to immediately discarded.
if (x < 0) {
abort;
}
TBD
A variant is an owning composed type able to hold and own one instance. A variant can be used when the exact type of an object is can be determined only at runtime according to the input being processed. A variant is defined with a type or a set of interfaces that restricts the objects it can contain.
The general form for a variant type is:
variant of type
variant of interface
variant of <interface1, interface2, ..., interfaceN>
For example:
class TMyClass {
variant of TXyx first;
variant of <IFoo, IBar> second;
}
A variant of T has the following methods object:
T get() const;
returns the referenced object (in a constant context) or null if none;
ref T get();
returns the referenced object (in a non-constant context) or null if none;
Void reset();
deletes the referenced object and clear the variant;
Void set(out T data);
associates an object data to the variant; the associated object must be already referenced to a variant nor to be an object attribute, otherwise a runtime error will occur;
Int valid() const;
returns true if the variant is associated to an object; false otherwise.
Note that:
variants are forwarding objects: this means that if v1 is a variant of T, it is possible to invoke v1.m() where m is a method of T; this is simply a shortcut for v1.get().m();
a variant of Void can contain any type of data;
a variant content can be downcasted to the actual type using the cast operation: v.get().cast(TMyType);
an object, in order to be bound to a variant, must be dynamic: this means that the object is not already bound to another variant or array, nor it is a class attribute. An object can be checked at runtime to see whether it is dynamic or not by calling the dynamic method.
An array is an owning composed type able to hold and own zero or more instances. An array can be used when a variable number of objects have to be owned by a parent object. An array is defined with a type or a set of interfaces that restricts the objects it can contain.
The general form for an array type is:
array of type
array of interface
array of <interface1, interface2, ..., interfaceN>
For example:
class TMyClass {
array of TXyx first;
array of <IFoo, IBar> second;
}
An array of T has the following methods (elements numbering starts from zero):
Void add(out T obj);
add the given object obj to the end of the array; obj must be an unowned standalone object;
ref T add();
create a new instance of T and add it to the array; a reference to the new added object is returned;
Int count() const;
returns the number of objects contained in the array;
Void delete(Int i);
delete element number i in the array;
Void deleteAll();
delete all objects of the array;
T get(Int i) const;
returns a reference to the object stored at position i;
T insert(Int i);
create a new instance of T and insert it before the object currently at position i; after the insertion, the newly added object will be available at position i; a reference to the inserted object is returned to allow editing;
Void move(Int p1, Int p2);
move the object currently at position p1 to position p2;
Void swap(Int p1, Int p2);
swap the objects currently at position p1 and position p2;
An object, in order to be bound to an array, must be dynamic: this means that the object is not already bound to another variant or array, nor it is a class attribute. An object can be checked at runtime to see whether it is dynamic or not by calling the dynamic method.
A link is a linking composed type able to reference one instance. A link is defined with a type or a set of interfaces that restricts the objects it can reference.
link of type
link of interface
link of <interface1, interface2, ..., interfaceN>
For example:
class TMyClass {
link of TXyx first;
link of <IFoo, IBar> second;
}
A link of T has the following methods object:
T get() const;
returns the referenced object or null if none;
Void reset();
clears the link and sets it to empty;
Void set(out T data);
associates an object data to the link;
Int valid() const;
returns true if the link is associated to an object; false otherwise.
Note that:
links are forwarding objects: this means that if v1 is a link of T, it is possible to invoke v1.m() where m is a method of T; this is simply a shortcut for v1.get().m();
a link of Void can link any type of data;
A dependent link is like a link, but it introduces a dependency between the linking and the linked object. This dependency ensures that each phase is executed first on the linked and then on the linking objects, as these were attributes.
This implies that no loops can be formed among dependent links: these loops are checked at runtime.
Dependend links have the same methods of normal links, except for the set method, that has an extra parameter:
T get() const;
returns the referenced object or null if none;
Void reset();
clears the link and sets it to empty;
Void set(out T data, LocString description);
associates an object data to the link; the description field should contain something meaningful to describe this link; this information will be used in case of loop detection, to help the user understand how the loop is formed.
Int valid() const;
returns true if the link is associated to an object; false otherwise.
A set is a linking composed type able to link zero or more instances. A set can be used when a variable number of objects have to be linked by a parent object. A set is defined with a type or a set of interfaces that restricts the objects it can reference.
The general form for a set type is:
set of type
set of interface
set of <interface1, interface2, ..., interfaceN>
For example:
class TMyClass {
set of TXyx first;
set of <IFoo, IBar> second;
}
A set of T has the following methods (elements numbering starts from zero):
Void add(out T obj);
add a link to the given object obj to the end of the set;;
Int count() const;
returns the number of objects contained in the set;
Void delete(Int i);
delete element number i in the set;
T get(Int i) const;
returns a reference to the object stored at position i;
Void reset();
delete all links stored in the set;
A lookup is a linking composed type able to link zero or more instances. A lookup can be used when a variable number of objects have to be linked by a parent object and associated to a lookup key. An lookup is defined with a type or a set of interfaces that restricts the objects it can reference.
There are two kinds of lookups:
lookup_i - associates an object with a given unique Int value, which is its key;
lookup_s - associates an object with a given unique String value, which is its key;
The general form for a set type is:
lookup_i of type
lookup_i of interface
lookup_i of <interface1, interface2, ..., interfaceN>
lookup_s of type
lookup_s of interface
lookup_s of <interface1, interface2, ..., interfaceN>
For example:
class TMyClass {
lookup_i of TXyx first;
lookup_s of <IFoo, IBar> second;
}
A lookup_s/lookup_i of T has the following methods (K is Int in case of lookup_i and lookup_i for lookup_s):
Int count() const;
returns the number of objects contained in the lookup;
Int exists(K key);
returns true if the lookup contains an entry for the given key, false otherwise;
ref T get(K key, String errorTextOnNotFound) const;
ref T get(LocK key) const;
these methods return a reference to the object that has the given key or they provoke an automatic execution abortion with error logging if not found. To automatic abort is to avoided, call first exists. The LocK type is LocInt or LocString (according to K): this type is a source localized version of that data type and it is the type normally returned by the grammar parser.
The errorTextOnNotFound text is printed in case the searched key is missing.
ref T getAt(Int pos) const;
return the object at position pos, where first element is zero; the elements are returned sorted by their key value;
Void reset();
delete all links stored in the lookup;
Void set(K key, out T data, String textOnDupeError) const;
Void set(LocK key, out T data) const;
create a link to data and associate it with the given key. If the key already exists, the execution aborts at the end of the phase with an explicative message. The LocK type is LocInt or LocString (according to K): this type is a source localized version of that data type and it is the type normally returned by the grammar parser.
The textOnDupeError parameter is the text that will be reported in case the key is duplicated. This is not needed in case a Localize version of the key is used.
Void testSet(K key, out T data) const;
attempt to create a link to data using key; if it fails (key is dupe), it returns false without aborting;
Phase programming is the hearth of Macrocoder language. This technique has been designed to solve many of the problems that often arise when developing code generators or other DSL-related softwares.
When writing code generators, we need to identify steps aiming to precise goals: the completion of each step might be required for the following steps.
A very common case is when we have a set of names and a set of items referencing them. We can think of web URLs (names) and refs to them (links); or in a programming language where a function is declared (name) and used (link).
This case requires two steps:
registration - all names are registered in a global table; during this step we can detect dupe names, because once the first instance is registered in the table, the next ones will find their own name already in it;
resolution - all links will search the table above and find their counterpart; if the name is not found in the table, then it means that it has not been declared.
Macrocoder does this by mean of phases. Each phase is a step that relies on the previous phases and produces results to be used by the following ones.
The great advantage of phases is that they can protect the data objects so they can't be accessed if not ready yet. Let's take the lookup table of the examples above:
it should never be accessible before the "registration" phase: at that stage there is no data in it nor we expect to populate it;
during the "registration" phase, it should be accessible read-write by the objects that are supposed to populate it;
after phase "registration" it should be accessible read-only by anyone.
Macrocoder is able to enforce such rules at compile time, avoiding cases where code reads data that is not available yet or it is partial.
In summary, phase programming is based on the the following concepts:
phases - an ordered sequence of steps, each one with a well defined goal to be reached;
phase methods - actions executed by each object to reach the goals of each phase;
phase alignment - at every step all objects are guaranteed to be aligned to a given phase; according to the internal rules, some might be guarateed to be evolved to phase p, while other to phase p-1; objects just intanced will be rapidly moved through their evolution to reach the current evolution level;
phase protection - protection rules on data that ensure that attributes are accessible only when their contents have been correctly set up;
This chapter explains the basic phase model available in Macrocoder. The basic phase model is simply a subset of the advanced phase model: they can be mixed on need. We suggest to employ the simpler basic phase model wherever the advanced features are not strictly needed.
Phases must be declared and associated to a number that determines their sequence. It does not matter what number is given to each phase: all phases with a smaller number will be executed before, the other after. Numbers must be unique among the same lifeset.
lifeset CORE;
phase registerNames = 1;
father-first phase printResults = 100;
children-first phase resolveNames = 1.5;
In the example above we declared three phases. According to their numbers, they will be executed in this order: registerNames, resolveNames and printResults.
The father-first and children-first modificators determine the way actions need to be executed; if no modificator is specified (like in the case of registerNames), the default is "children-first". Their meaning is:
children-first - during phase operations, children must be satisfied first; this technique is used when the father uses data produced by its children and it is called synthesis.
father-first - during phase operations, fathers must be satisfied first; this technique is used when the children use data produced by their fathers and it is called analysis.
Once phases have been declared, we can implement phase related attributes or actions within classes using the in phase scope indicator:
extend class MyClass {
in phase registerNames {
// ...in phase contents
}
}
Everything declared within the in phase declaration will be executed or implemented within the phase rules.
The most important item that goes inside a in phase declaration is the "do" action:
extend class MyClass {
in phase registerNames {
do {
// code executed during 'registerNames'
}
}
}
The "do" action will be executed during the execution of the related phase.
If the phase is children-first, this "do" action will be executed after the "do" actions of the attributes of the hosting class (i.e. its children); otherwise (father-first) it will be executed before.
The "do" action is an alternative name for the on phase action (if children-first) or pre phase action (if father-first) of the advanced phase model. Therefore, a class that has a "do" action defined, it can't also have the related on/pre phase method.
The "do" action can also be used in its short form:
extend class MyClass {
in phase registerNames do {
// code executed during 'registerNames'
}
}
The "do" action has an implicit parameter called "lset". This parameter is a reference to the lifeset object and it can be used to access the lifeset instance.
The lifeset instance holds the global information that need to be shared among all the instances and it is the final owner of all the objects created in a Macrocoder system.
Inside the in phase declaration, phase-protected attributes can be declared as in the following example:
extend class MyClass {
in phase registerNames {
Int abc;
do {
// code executed during 'registerNames'
abc = 12;
}
}
}
The abc attribute defined above is phase-protected; this means that:
it is not accessible by anybody before phase registerNames;
it is accessible read-write during the registerNames phase only by the "do" action of the same instance that contains it;
it is accessible read-only by everybody once the registerNames phase is completed on this instance;
Note that if the phase is declared father-first, the children will able to access read-only their parents' registerName phase protected attributes during the "do" action of their registerName phase: this is because when a children begins a phase, all of its relatives already completed it.
On the other side, if phase is declared children-first, during its registerName phase, each object will be able to access its attributes' phase protected attributes for registerName, because in this case it will be its children to have completed their phases first.
Inside the in phase declaration, phase-shared attributes can be declared as in the following example:
extend class MyClass {
in phase registerNames {
shared Int abc;
}
}
A phase-shared attribute can be accessed read-write by any object that is executing the "do" action of the same phase.
It has to be used when multiple objects have to convey their information into a single container.
In the example below, a phase-shared lookup table "knownPeople" is defined in the lifeset; this lookup table will be used by every Person object to associate its personName to its instance. This action will detect Person with the same name (because the set method will automatically fail in case of duplication) and it will create a list of known people, available to the following phases.
lifeset CORE;
phase registerNames = 1;
in phase registerNames {
shared lookup_s of Person knownPeople;
}
class Person {
LocString personName;
in phase registerNames {
do {
lset.knownPeople.set (personName, this);
}
}
}
Inside a in phase declaration, phase-protected methods can be declared as in the following example:
extend class MyClass {
in phase registerNames {
Void myMethod () {...}
do {
myMethod ();
}
}
}
A phase-protected method runs under the same privilegies of the "do" action. This means that in can do everything the "do" action can.
A phase-protected method can be invoked only from another phase-protected method for the same phase and instance or from the "do" action itself.
As it happens for the "do" action, the phase-protected methods have the implicit "lset" parameter that allows accessing the lifeset.
Inside a in phase declaration, phase-shared methods can be declared as in the following example:
extend class MyClass {
in phase registerNames {
shared Void myMethod () {...}
}
}
A phase-shared method can be invoked by any other instance during the execution of its "do" action for the same phase.
A phase-shared method for phase P sees its instance data as not
Code written inside lifeset L1 can create or write into instances of lifeset L2 only within phases declared as "creating" lifeset L2:
lifeset L1;
phase myPhase = 10 creates L2;
Only methods being executed within phase myPhase of L1 will be able to create objects in L2.
This chapter explains the advanced phase model; this contains all the features of the phasing system and, for this reason, is a bit harder to understand and manage.
Phases are defined inside a lifeset. Each phase has a name and a phase number. The mnemonic phase name should recall the goal of the phase, while the phase number defines the execution order.
lifecycle MyLifeCycle {
lifeset MyLifeSet {
phase registerNames = 1;
phase printResults = 100;
phase resolveNames = 1.5;
}
}
The example above defines a lifecycle called "MyLifeCycle", containing a lifeset "MyLifeSet" and three phases, "registerNames", "resolveNames" and "printResults".
Phase definitions must follow these rules:
each phase must have a name that is unique within its lifeset; different lifesets may have phases with the same name, which have no reciprocal relationships;
phase names INITIAL and FINAL are reserved;
phase numbers are floating point values that simply define the execution order: lower values are executed first;
phase numbers can not be repeated within a lifeset: each phase in a given lifeset must have its own unique phase number.
The Macrocoder engine orderly goes through the list of phases and calls on each involved object the phase methods related to the current phase.
Phase methods are invoked on every structural object that have been enrolled in the lifeset. The order in which these invocations are done is determined by the structural relationship among objects.
Given any two objects a and b, they could be either structurally related or unrelated:
on every object, its "pre phase p" method is executed before its "on phase p" method;
if a and b are structurally unrelated, then their phase methods are executed in an unknown order;
if a and b are structurally related, then there is a dependency relation among them. Let us assume that a depends on b: in this case "pre phase p" on a is executed before "pre phase p" on b; "on phase p" on a is executed after "on phase p" on b.
Objects are normally structurally unrelated. They become related with a relationship "a depends on b" (aka "a is parent of b") in the following cases:
when a is the lifeset: the lifeset is always parent of all the objects enrolled in its cauldron;
when b is an attribute of a;
when b is owned by an owning composite a, like an array or a variant;
when b is referenced by a dependent link of a.
Phase methods those methods called by the Macrocoder engine for each reached phase. They can be implemented in structural classes (i.e. classes defined inside a lifeset) and in the lifeset itself.
The phase methods are defined with the keywords "pre phase" and "on phase":
lifecycle MyLifeCycle {
lifeset MyLifeSet {
phase registerNames = 1;
// Phase methods defined in the lifeset
pre phase registerNames {}
on phase registerNames {}
class MyStructuralClass {
// Phase methods defined in a structural class
pre phase registerNames {}
on phase registerNames {}
}
}
}
Phase methods are not mandatory: if a given class has nothing to do in phase p, it can totally omit phase methods for p; or define only "pre phase" or "on phase".
The phase methods have the this implicit parameter like any other non-static method; however, they have an extra implicit parameter lset declared as "out pretransitional L", where L is the lifeset type. This parameter can be used to access to the information stored in the lifeset itself. For example:
lifecycle MyLifeCycle {
lifeset MyLifeSet {
phase registerNames = 1;
shared finalize=registerNames lookup_s of MyStructuralClass myLookup;
class MyStructuralClass {
// Object name
LocString objectName;
// Register the name
on phase registerNames {
lset.myLookup.set (objectName, this);
}
}
}
}
In this example, method "on phase registerNames" of clas MyStructuralClass is using lset to access the shared attribute myLookup defined inside the lifeset.
The phase system implements phase protection. This concept derives from a long experience in code generators, where many apparently randomic problems arised from objects accessing data that supposedly was up to date, but some times was not.
The phase protection system works on an attribute a of an object o enforcing these concepts:
before a given phase p1, the attribute a is unset: therefore access to it must be forbidden to anyone;
from phase p1 to phase p2 (most of the times p1=p2), the attribute a is being updated; during this process, only an "on phase" or "pre phase" method running on object o must be allowed to update the attribute; all the remaining objects and methods must have access to a forbidden;
after phase p2, the attribute o.a is ready: from now on, it must be available read-only to everyone.
This kind of protection is offered ad compile time: this means that if a method is accessing a given data and the program compiles, it is guaranteed that the accessed data has been correctly prepared. This is a great step ahead compared to runtime checking, that blocks execution in case of rules violation, because it ensures correctness by design.
To implement phase protection, Macrocoder adds some concepts to the attribute declarations.
First concept is management mode.
Management mode indicates who is in charge to update the attribute. Macrocoder supports the following management modes:
phased - the attribute must be modified by a "on phase" method of the object containing it; this is the default in case nothing is specified;
prephased - the attribute must be modified by a "pre phase" method of the object containing it;
shared - the attribute can be modified by any object; this management mode relaxes the phase protection and should be used with caution; however it necessary when shared data must be updated: for example, the lookup table containing the names of various other objects, must be updated by each object owning a name and not by the one that owns the lookup.
It is important to underline the concept of "modifying object": the attribute can be edited by an "on phase" method being executed exactly on the object instance that contains the attribute. This could be mistaken with the usual "private" protection keyword, but they are quite different:
the private keyword is applied on a class; any object of a class can access and modify any private attribute of any object of the same class; for example, an object can modify private attributes of another object as long as they are instances of the same class;
the phased/prephased keywords, instead, limit the access to the object that owns the attribute; another object, even if instance of the same class, will not have any right to access the protected data.
While the management mode defines who can modify an attribute, enable and finalize phase settings determine when all operations can be performed.
enable - the enable phase indicates when an attribute becomes visible; no "pre/on phase p" methods can ever read or write that attribute if p is less than the enable phase. By default the enable phase, if not specificed, is identical to the finalize phase.
finalize - the finalize phase indicates when an attribute becomes public and read-only; all "pre/on phase p" methods having p greater than the finalize phase can access the attribute (unless forbidden by class protection keywords like protected or private. By default, finalize phase is INITIAL.
Let us assume that a given attribute a has enable=p1 and finalize=p2, where p1<=p2; it holds that:
if a is phased, it can be edited only by the "on phase p" method of the same class that contains it as long as p1<=p<=p2;
if a is prephased, it can be edited only by the "pre phase p" method of the same class that contains it as long as p1<=p<=p2;
if a is shared, it can be edited only by the "pre phase p" or "on phase p" method of any class as long as p1<=p<=p2;
Let us see some examples about phase protected attributes:
lifecycle MyLifeCycle {
lifeset MyLifeSet {
phase phase1 = 1;
phase phase2 = 2;
shared finalize=phase2 Int attributeS;
class Person {
finalize=phase1 Int attribute1;
enable=phase1 finalize=phase2 Int attribute2;
prephased finalize=phase1 Int attribute3;
on phase phase1 {
attribute1 = 1234;
attribute2 = 400;
}
on phase phase2 {
attribute2 = 500;
lset.attributeS = 3000;
}
pre phase phase1 {
attribute3 = 1000;
}
}
}
}
The phase system guarantees that during the execution of the "on/pre phase p" functions, every object that is being accessed is evolved at least to phase p or p-1. This means that if an object is guaranteed to be evolved at phase p, it might be evolved to any phase equal o greater than p. The exact set of rules that controls the guaranteed evolution level is formally explained in the access rules definition.
There is only one exception where an object obj, accessed during "on/pre phase p" may have an evolution less than p-1. This happens when the object obj has just been instanced. Such a situation arises when, during a phase evolution, arises the need of creating a new instance that has to be enrolled to the lifeset.
This situation is handled by mean of the initiating state:
the object obj is created either using var or new, as explained in the objects and references chapter;
the hypertype status of the newly created obj is initiating: this allows accessing its attributes whose phase protection is finalize=INITIAL;
then the obj object is enrolled to the lifeset: during this operation, all the "on/pre phase" methods of obj are invoked up to and including at least phase p-1;
every reference to the initiating objects are cleared: in this way, once the transition started, it will be impossible to access again the obj object as it was still initiating;
the lifeset enroll method will return only after all the evolution methods of obj (and of other objects eventually instanced by obj during its evolutions) are completed;
the lifeset enroll method will also return a reference to obj, this time with an hypertype status of simple.
If the finalize phase of an attribute is INITIAL (which is the default if nothing is specified), the attribute can be modified only when the object is in its "initiating" state. The initiating state is a special condition assumed by structural objects that have been instanced but not yet enrolled to a lifeset:
on phase MyPhase {
// Create an instance of MyClass
var MyClass m;
// Now 'm' is initiating; we can access its attributes
// with finalize=INITIAL.
m.myValue = 123;
// Object 'm' is enrolled to the lifeset.
ref MyClass k -> lset.enroll (m);
// Now 'm' has been enrolled. Reference 'm' has
// become NULL, but the object can be accessed as 'k'.
if (k.myValue == 123) ...
}
In the example above we have that:
variable m has been instanced; reference m has hypertype status initiating; this gives access to the attributes that have finalize=INITIAL;
object m is enrolled in the lifeset (lset.enroll); this action triggers all the phase events of m: the object is brougth quicly through all phases up to MyPhase and it is returned evolved as all the other objects; this example saves a reference to the enrolled object in reference k, whose hypertype status is simple.
in the meantime the m reference (and any other previous reference) is cleared, to avoid allowing initiating access to objecs that are not initiating anumore.
Normally, phase evolution is done by the "on phase" and "pre phase" methods. However, it might be useful to split the action of these methods in further method calls without loosing the "access rights" granted to the usual evolution methods.
This can be obtained by declaring the methods "phased phase=p" (to work with the same rights of "on phase p") or "prephased phase=p" (to work as "pre phase p"):
lifecycle MyTestLifeCycle {
lifeset Test {
phase p3 = 3;
// This method has the same rights of "pre phase p3"
Void prePhaseLike () prephased phase=p3 {}
pre phase p3 {
prePhaseLike ();
}
// This method has the same rights of "on phase p3"
Void onPhaseLike () phased phase=p3 {}
on phase ph3 {
onPhaseLike ();
}
}
}
While the "on/pre phase" methods receive the lifeset reference as an implicit parameter named lset, the generic evolutionary methods are normal methods and they do not have this feature. However, the lset parameter can be passed explicitely declaring it as "out pretransitional":
lifecycle MyTestLifeCycle {
lifeset Test {
phase p3 = 3;
class TTest phase=p3 {}
Void onPhaseLike (out pretransitional Test lset) phased phase=p3 {
var TTest t;
lset.enroll (t);
}
on phase p3 {
onPhaseLike (lset);
}
}
}
In the example above, the enrollment of a new instance of "TTest" is done by the explicit parameter lset.
One of the key Macrocoder features is its ability to read and understand users' source files. This is obtained by programming the Macrocoder parser by specifying one or more grammar definitions.
Macrocoder text grammars can be both specified textually and specified graphically: both specifications are equivalent, can be mixed and undergo the same rules.
The grammar parsing process of Macrocoder can be synthesized in the following steps:
the programmer specifies a grammar lifeset; this contains the grammar rules that define the syntax of the language (the default lifeset name for grammars is "GRAMMAR");
by defining a grammar, the programmer is implicetely defining some structural classes within the grammar lifeset;
the user writes text that must comply to the grammar rules defined above; if it does not, the Macrocoder parser will automatically report the syntax errors, both visually while typing (coloring in red the erroneus parts) and by error messages when execution is attempted;
once the parsing process terminated successfully, the Macrocoder engine will create a tree of objects, enrolled to the grammar lifeset, that describes the user input;
when all the input object trees have been created, the phase evolution will start and results will be produced according to the programmers' rules.
The following chapters will cover these topics:
defining a grammar - explains how grammars are to be defined to obtain the desired input syntax;
grammar lifeset mappings - explains how implicit classes are created after each grammar rule and how to handle them;
In order to define a grammar, a source file must be defined:
to create a graphic specification (recommended) of a textual grammar create a "grammar file" (extension .fcg); see "B" in the picture below;
to create a textual specification of a textual grammar create a plain text source file (extension .fcl); see "A" in the picture below;
This chapter explains how grammars are to be defined to obtain the desired input syntax.
A grammar is a special kind of lifeset and, for this reason, it has to be defined within a lifecycle.
Graphic specification
This is an example to show how a grammar definition looks like:
See edit grammar for details on how to create and edit a grammar.
Textual specification
This is the general grammar definition using the textual specification:
lifecycle LifeCycleName {
grammar GrammarName {
parse files "ext1" with rule RootRuleName;
rule RootRuleName ::= ...;
rule Rule1 ::= ...;
rule Rule2 ::= ...;
...
}
If using the default predefined MAIN lifecycle, the definition can be shortened in this way:
grammar GrammarName;
parse files "ext1" with rule RootRuleName;
rule RootRuleName ::= ...;
rule Rule1 ::= ...;
rule Rule2 ::= ...;
A grammar is subject to the following rules:
the grammar keyword is identical to the lifeset keyword, except that accepts also the rule definitions;
the parse files statement indicates the extensions of the files that have to be parsed using RootRuleName as the root rule;
the simple rule definitions contain further grammar rules that, combined together, form the complete grammar.
Before going to formal definition, we will try to intuitively explain what a grammar is and how it works.
Step 1
Let us take this example of user text:
person John is son of Mike
Intuitively we understand that some parts (person and is son of) are fixed, while other (John and Mike) are variable: John and Mike are just there to put something meaningful in the example, but any other name should work as well.
A source like this can be parsed by this grammar:
Or, in textual specification form:
rule MyGrammar ::= (A)"person" (B)personName:ident (C)"is son of" (D)fatherName:ident;
Italic letters between parentheses have been added in this context just to quickly reference the grammar segment while explaining.
The parser works in this way:
reading our grammar definition, it identifies the keywords; in this case, the keywords are "person" and "is son of";
having this information, the parser splits the source string in tokens, i.e. elementary words; in this case, they are four tokens: 1:[person] 2:[John] 3:[is son of] 4:[Mike];
every textual string other than "person" and "is son of" is identified as an ident;
it starts with the first token (1) and the first rule (A); since they match, it moves along with token (2) and the second rule (B) and so on;
since it reached the end of the rules and the end of the tokens without any error, the parsing is succesfull and the syntax of the input file is valid;
for example, a sequence like 1:[person] 2:[John] 3:[Mike] would fail when parsing token 3: rule "C" expects a "is son of", but the input source contains an ident: this will cause a syntax error at token 3;
Step 2
Let us now consider the following example:
person John is son of Mike
person Bill is son of John
person Luke is son of Bill
Obviously our language must be able to define multiple people, so we expect a source file as the one above to be valid. However, the previous grammar does not agree: as specified, it expects a single "person..." definition. So, we add support to expect a sequence of any number of "person" definitions:
Or, in textual specification form:
rule MyGrammar ::= (A)people:{Person}; /* root rule */
rule Person ::= (B)"person" personName:ident "is son of" fatherName:ident (C);
The root rule has been modified: now it states "expect zero or more repetitions of rule Person". The parser, when at (A), expects either a "person" keyword or the end of the file; if "person" is found, it continues from (B). Once it reaches (C), it goes back to (A), where, again, it expects "person" or the end of the file. It repeats this loop over and over until all the people have been read.
Step 3
Let us now consider a further example:
person John exists
person Bill is son of John
In this example there is a new case: after "person name" we can have "is son of..." or "exists".
Or, in textual specification form:
rule MyGrammar ::= (A)people:{Person};
rule Person ::= (B)"person" personName:ident (C) personDetail:choice (IsSonOf|Exists);
rule IsSonOf ::= "is son of" fatherName:ident;
rule Exists ::= "exists";
In this new example, we introduced (C), a "choice": this entry means "expect either a IsSonOf or an Exists rule).
Finally, the common expression "...a grammar rule matches..." means "...a grammar rule expects...".
Once a grammar parser has verified that the user's source complies with the grammar rules, the information contained in the user's source must be made available for further processing. For example, after the parser has established that "person Mike is son of John" is a valid string, there must be a way to know that a person has been defined, that his name is "Mike" and his father's name is "John", so further processing can be accomplished.
Macrocoder provides with an automatic mean of data extraction based on structural classes being created after each grammar rule.
For example, let us take the following grammar rule:
Or, in its equivalent textual specification form:
rule Person ::= "person" personName:ident "is son of" fatherName:ident;
The "Person" grammar rule will create an automatic structural class named "Person" containing two string attributes called "personName" and "fatherName". Every time the parser will match a "Person" rule it will create an instance of the class "Person" and its attributes will be fillen with the parsed data.
The following chapters will discuss each kind of rule, explaining theirs class mappings.
Every defined grammar definition is a lifeset. This means that inside a grammar definition everything that normally goes in a lifeset, is allowed, including phase definitions and classes. The difference between a grammar and a lifeset is that the first one accepts grammar rule definitions (i.e. rule statements), while the latter does not.
The example below shows a grammar containing a rule MyRootRule, a phase called DoSomething and a structural class MyClass:
lifecycle MyLifeCycle {
grammar MyGrammar {
parse files "abc" with rule MyRootRule;
rule MyRootRule ::= a:ident;
phase DoSomething = 1;
class MyClass {
on phase DoSomething {
//...code
}
}
}
}
All the classes generated after grammar rules are derived from a common base class called GBase.
The GBase structural class is automatically created in each grammar lifeset, because structural classes can derive only from other structural casses defined within the same lifeset (see types purpose).
The GBase class is so defined:
class GBase {
GLocator locator;
}
The locator attribute contains the position in the user source file where the rule matched. This information is automatically used when reporting some parsed information to the "Output" window, that displays an hyperlink for objects having a valid locator.
The only way to add methods and attributes to the grammar generated classes is by extension.
By extending the GBase class, added methods, attributes and expositions will be available to all the grammar generated classes.
In the following example, the rule class MyRootRule is extended to add an evolution method for phase DoSomething.
lifecycle MyLifeCycle {
grammar MyGrammar {
parse files "abc" with rule MyRootRule;
rule MyRootRule ::= a:ident;
phase DoSomething = 1;
extend class MyRootRule {
on phase DoSomething {
//...code
}
}
}
}
In grammar definitions, terminals are the most atomic items. Terminals can either be fixed or variable:
fixed - if they match an exact string, like "is son of";
variable - if they match a variable string, like a user defined identifier or a number;
The terminals available in Macrocoder are the following:
keyword - fixed keyword, like "person"; if the keyword contains spaces, they will match any non empty sequence of spaces, tabs or new lines;
ident - matches an upper or lower case letter A...Z or underscore (_) followed by any number of underscores, letters or numbers; so, any sequence of letters/numbers/underscore not starting with a number is a valid identifer. For example abc, A__123 and _99 are all valid identifiers.
numeric - matches a number in various forms; a number always begins with a number in the range 0...9. Numbers can be expressed as decimal (123), floating point (1.23, 0.2E+4, etc.) or exadecimal (0xAF12).
freetext - matches a freetext entry; freetext can be inserted using the Macrocoder editor by pressing the "F9 - freetext" key; freetext sequence can contain any character and formatting details like font size, bold, italic and so on. The editor evidences the freetext sequence with a distinctive background color.
quoted - matches a quoted string, i.e. a string enclosed in double quotes ("). For example, "This is a string". Inside the quoted string, the quoted terminal supports some escape sequences.
empty - matches an empty string; useful only when in a choice there are many options but all optional: (A|B|...|Z|empty); in a choice like that, empty is matched with lower priority, only if all the other choices failed, no matter where empty is placed.
Keyword and empty terminals do not generate any data, so they do not have any mapped type. The remaining terminals are mapped to a class derived from GBase that contains their locator (inherited from GBase) and value.
The generated class for idents is, for example:
class GString: GBase {
// 'locator' is inherited from GBase; it is reported here
// for clarity:
GLocator locator;
// Terminal value
String value;
}
The classes for the various terminals are:
ident - class is named GString and value type is String;
numeric - class is named GNumeric and value type is Numeric;
freetext - class is named GDecoString and value type is DecoString;
quoted - class is named GString and value type is String;
The sequence is the most used non-terminal when defining grammars. A sequence is a series of rules that have to be matched in a predefined sequence.
See Editing sequences for details on how to add or remove items from a sequence.
For example:
Or, in its equivalent textual specification form:
rule Person ::= "person" personName:ident "of age" personAge:numeric;
In the example above, Person is a sequence of four terminals that have to match in that order to obtain a valid sequence match.
Sequences are mapped to a class where each active sequence entry is represented by an attribute.
In a sequence every referenced rule other than keywords must be tagged with a field name. For example, in the Person rule above, the non-keyword entries are tagged with a field name (personName and personAge).
Field names must be unique within a sequence.
For every tagged rule within a sequence, an attribute with the same name will be created in the generated structure. The attribute type is determined by the class mappings of the sub rule itself.
In the case above, the generated structure will be:
class Person: GBase {
// 'locator' is inherited from GBase; it is reported here
// for clarity:
GLocator locator;
GString personName;
GNumeric personAge;
}
In the example above, personName is a GString because it is an ident rule; instead, personAge is a GNumeric because so is defined for numeric rules.
Sequence classes can be derived from other than GBase, as long as the new base class is directly or indirectly derived itself from GBase.
This can be done using the inherits keyword:
class MyPersonBase: GBase {
//...
}
rule Person ::= inherits MyPersonBase "person" personName:ident "exists";
In the example above, class Person will be derived from MyPersonBase instead of being derived from GBase. However, this is allowed because MyPersonBase is derived from GBase itself.
A repetition is a non-terminal rule that matches zero or more time another rule contained in it. It can be used when multiple entries are expected.
For example:
Or, in its equivalent textual specification form:
rule People ::= peopleSet:{Person};rule Person ::= "person" personName:ident "exists";
the People rule matches when zero or more times the Person rule matches.
A repetition of rule R (i.e. {R}) is mapped to an array of R. For example, the example above would generate the following classes:
class Person: GBase {
GLocator locator;
GString personName;
}class People: GBase {
GLocator locator;
array of Person peopleSet;
}
A choice is a set of other rules among which only one can be matched. It is used when, at a given point, there must be multiple choices available.
See Editing choices for more info on how choices can be edited.
For example:
Or, in its equivalent textual specification form:
rule Person ::= "person" personName:ident personDetails:choice (SimplyExists | IsSomeonesSon);
rule SimplyExists := "exists";
rule IsSomeonesSon ::= "is son of" fatherName:ident;
A choice may include an empty branch, making all choices optional. For example, if we want to allow "exists", "is son of" but even nothing else, we can add an empty branch:
In its equivalent textual specification the same is obtained by including the empty keyword:
rule Person ::= "person" personName:ident personDetails:choice (SimplyExists | IsSomeonesSon | empty);
rule CarModel := "has car" fatherName:ident;
rule BikeModel ::= "has bike";
In the case above we could write "person John has car Honda", but it would be valid "person John" as well.
A choice is mapped to a "variant of GBase": in this way, it can contain any possible option. The Person in the example class it would be defined as:
class Person: GBase {
GLocator locator;
GString personName;
variant of GBase personDetails;
}
The variant contained class can be limited to a GBase descendant. For example, a choice could be allowed to contain only rules derived from MyPersonBase:
rule Person ::= "person" personName:ident personDetails:choice MyPersonBase (SimplyExists | IsSomeonesSon);
rule SimplyExists := inherits MyPersonBase "exists";
rule IsSomeonesSon ::= inherits MyPersonBase "is son of" fatherName:ident;
The optional command can be used to represent rules that can be matched optionally.
The graphic specification form does not support the "optional" concept: it can be implemented by using a choice with a single entry and the empty path.
In the textual specification form, an optional entry is represented within square brackets:
rule Person ::= "person" personName:ident car:[OptionalCar];
rule OptionalCar ::= "has car" carType:ident;
The grammar in the example above accepts strings like "person John" or "person John has car sedan" as well.
The optional command is simply a shortcut for a choice with two entries, where one is empty:
[OptionalCar] is same as choice (OptionalCar | empty).
The optional entry is mapped exactly as a choice except that the variant type is always the type contained in the square brackets.
For example, if the optional is [OptionalCar], its mapping will be variant of OptionalCar.
The great majority of grammars used in domain specific languages is based on a ordered sequence of items that, from left to right, contain all the required information. However there is a very common exception to this general rule: the operators.
We encounter operators as soon as we have to represent a simple math expression. Let's take an expression as simple as "a+b+c*d": writing a grammar rule able to read expressions like this is not trivial. Furthermore, these expression need somehow to be reordered, because, due to math precedence, "c*d" is to be executed before the addition even if it is written at the right.
Macrocoder solves this problem by providing with the operator concept.
There are four kinds of operators:
infix_lr - is a binary operator with left-to-right associativity that is placed between two rules called operands; for example, the multplication symbol * is an infix operator: op1 * op2.
Left associativity means that in case of a sequence of multiple operands, they will be executed from left to right; for example "op1 * op2 * op3" will be executed as "(op1 * op2) * op3".
infix_rl - same as infix_lr, but with left-to-right associativity. For example, "op1 = op2 = op3" will be executed as "op1 = (op2 = op3)".
prefix - in an unary operator that is prefixed before an operand; for example the "not" operator (!) is a prefix operator, because it operates on the following operand: !op.
This operator has always right-to-left associativity: "! ! op" is executed "!(!op)".
postfix - is a unary operator that goes after its operand; for example, the C "++" operator can be a postfix: op++.
This operator has alwasy left-to-right associativity: "op ++ ++" is executed as "(op++)++".
These operators can combined in complex expression, where the execution order depends on the precedence each operator has. For example, the expression "op1 + op2 * op2" should be executed as "op1 + (op2 * op2)" even if the addition comes first, because multiplication has an higher precedence.
Within Macrocoder an operator is a set composed by:
a data rule that represents operands;
zero or more operations (infix_rl, infix_lr, prefix or postfix), where another given rule is used as the operator;
each operation has a precedence value that determine its execution order (precedency) among the others; operations with the lower precedence value are executed first.
This is an example syntax for defining an operator:
Or, in its equivalent textual specification form:
rule Expr ::= operator Operator (
operdata numeric
prefix opNOT 3 "!"
postfix opINC 4 "++"
infix_lr opMUL 5 "*"
infix_lr opDIV 5 "/"
infix_lr opADD 10 "+"
infix_lr opSUB 10 "-"
);
The rule definition above defines an operator set that works on operands represented by a numeric terminal:
the operdata keyword is mandatory and it must be specified once; it indicates the grammar rule that has to be used as terminal operands. In the example the terminal numeric has been specified, but any other rule could be used as well (see the examples below).
the prefix/postix/infix_lr/infix_rl keywords define an operation; each of them has a rule name (e.g. OperIncr) which will be used for class mapping;
each operation requires a number ranging from 0 to 32767 which defines its precedency: lower values mean higher precedence;
finally, each operation has a string that defines how the operator has to be represented (e.g. "++").
The example above defines a simple operator set that implements the usual four simple math operations with their usual precedency.
The following example is a bit more complex: the operdata now is a choice that supports numbers, identifiers and subexpressions included in parentheses. Therefore, it is able to parse expressions as complex as 3+2*(!a-foobar)/(4+x). Here it is its code implementation:
Or, in its equivalent textual specification form:
class MyOperatorBase:GBase {}
rule ValueNumeric ::= inherits MyOperatorBase value:numeric;
rule ValueIdent ::= inherits MyOperatorBase value:ident;
rule ValueParentheses ::= inherits MyOperatorBase "(" subExpr:Expr ")";
rule Expr ::= operator Operator inherits MyOperatorBase (
operdata choice MyOperatorBase (ValueNumeric | ValueIdent | ValueParentheses)
prefix opNOT 3 "!"
postfix opINC 4 "++"
infix_lr opMUL 5 "*"
infix_lr opDIV 5 "/"
infix_lr opADD 10 "+"
infix_lr opSUB 10 "-"
);
Note that in the example above we decided to have all the classes involved in operators to derive from MyOperatorBase instead of the default GBase.
An expression based on operators can be rewritten as a sequence of nested functions. For example a+b can be rewritten as add(a,b); a+b+c becomes add(add(a,b),c): it means, first add a and b; then take the resulting value and add it to c. So, a+b*c becomes add(a,mult(b,c)) and so on.
Macrocoder uses this paradigm to create and feed objects after operators parsing.
For every operator, Macrocoder creates a GBase derived structural class with the name given in the operator definition (Operator in the exampel above); this class has two GBase variants, called p1 and p2, and one Int called operatorId for unary operators.
For every operation, Macrocoder generates a const value named with the operation name (e.g. opADD for operation "+" in the example above), whose value is the CRC-32 of the name.
For example, the definition infix_lr OperMult 5 "*" produces the following class:
class Operator: GBase {
GLocator locator;
variant of GBase p1;
variant of GBase p2;
Int operatorId;
const Int opNOT = 0xD79CD1F0;
const Int opINC = 0x481B73F3;
const Int opMUL = 0x769B0D24;
const Int opDIV = 0x625F928C;
const Int opADD = 0x22835F62;
const Int opSUB = 0x879BAE59;
}
When parsing the user formulae, the Macrocoder parser will compose a tree made of instances of these classes using the same "functions" paradigm described above.
For example, the operation 3+2*4 will produce:
an instance of Operator with operatorId set to opADD;
its p1 attribute will contain an instance of GNumeric, containing 3;
its p2 attribute will contain an instance of Operator with operatorId set to opMUL;
the last instance of Operator will contain a GNumeric set to 2 in its p1 attribute and an instance of GNumeric set to 4 in its p2 attribute.
One of the most annoying problems with grammars is the correct-but-unexpected result. This phenomenon arises when users define a grammar that produces unexpected results that, after a long investigation, proves correct. Most of the existing methods of defining grammar rules are very powerful, but subject to this kind of problem.
Macrocoder addresses this issue by enforcing a set of restrictive rules that avoids unexpected behaviours. This set of rules has the drawback of restricting the variety of languages that can be defined with Macrocoder, but it does not limit the variety concepts that can be expressed with it.
The rules restriction is based on a simple elementary rule: when reading a source file, next token must univokely identify one and only one rule.
This rule is enforced by the compiler, that does not allow the definition of grammars that break this rule.
Let us see some examples where this fundamental rule would be broken:
rule Test ::= "abc" k:choice (Alpha | Beta | Delta);
rule Alpha ::= "xyz" a:ident;
rule Beta ::= "xyz" b:numeric;
rule Delta ::= "klm" b:numeric;
In the example above, the parser expects "abc" and this is ok; then it expects an Alpha, a Beta or a Delta. However, Alpha and Beta both begin with "xyz": if the parsers encounters "xyz" after "abc", it would not be able to tell whether it is an Alpha or a Beta. For this reason, the compiler will refuse to compile this grammar.
Here it is another example:
rule Test ::= "abc" a:[Alpha] d:[Delta] b:Beta;
rule Alpha ::= "xyz" a:ident;
rule Beta ::= "xyz" b:numeric;
rule Delta ::= "klm" b:numeric;
In this case, after an "abc" token, it might optionally follow an Alpha, a Delta and then is expected a mandatory Beta. When parsing a string composed of "abc xyz...", when the compiler encounters "xyz", it is not be able to tell whether it is an optional Alpha or it is in the case when Alpha and Delta has been omitted and the "xyz" string announces a Beta.
Also this case is forbidden by the compiler.
After several tests with very complex situations, we verified that with little effort and no loss of clarity, grammars can always be modified to comply with this simple rule.
This chapter illustrates in details the steps required to use the textual grammars graphic editor.
Lifeset and lifecycle can be edited by double clicking on the grammar header:
To add a new rule simply right-click on the background in the position where the new rule has to be inserted and follow instructions:
Sequences are a set of rules that are executed in order. Sequence name and base class can be edited by right-clicking or double clicking the sequence name:
New rules can be added in a sequence by right-clicking on the link where we want the new rule to be inserted:
Rules can be removed from a sequence by right-clicking them and selecting Delete:
To add new branches to the choice, right click on the diamond and select "Add branch":
The optional empty path can be activated or deactivated by the same menu or by double clicking the diamond.
NOTE: this part is preliminary and subject to change.
Another powerful mean of describing concepts is using graphics. Macrocoder supports the definition of mixed graphic/textual formal languages by the diagram concept.
Macrocoder allows the definition of diagrams that can be used as a formal source of information as it for text grammars.
The Macrocoder diagrams are based on two concepts:
figures - a figure is a shape that can be placed anywhere in a diagram; they can be rectangles, triangles, a stylised man or any other picture that is meaningful for the domain being descripted;
lines - a line is a graphic object made of segmens whose goal is to connect two figures; lines are used to form relationships among figures.
A diagram is then composed by figures connected with lines. Lines and figures can be of different types, representing different formal concepts. Inside figures and lines some areas can be fillen with text, which is parsed with a text grammar. We will see these concepts in details thorught the following chapters.
A diagram is a lifeset that extends the grammar lifeset described in the previous chapter.
It is defined with the following syntax:
lifecycle LifeCycleName {
grammar GrammarName {
diagram DiagramName parsing files "ext1", "ext2", ..., "extN" {
// Define and use:
figure fig1 ...
figure fig2 ...
line line1 ...
line line2 ...
// Just use:
use shape fig3;
use shape line3;
}
// Defined externally
figure fig3 ...
line line3 ...
}
Being a grammar, the diagram supports the short definition form which assumes using the default lifeset MAIN:
grammar GrammarName;
diagram DiagramName parsing files "ext1", "ext2", ..., "extN" {
...
}
The parsing files statement indicates the extensions of the files that will be parsed by this diagram.
A diagram is based on these fundamental concepts:
pictogram - is a vectorial picture like a diamond, a square or whatever else; it contains text fields and connection points;
figure - describes a "picture" that can be placed on a diagram; it uses a pictogram to represent its shape and it contains the grammar rules to parse the text fields;
line - describes a "line" that can connect two figures; it uses one or more pictograms to represent decorative elements on the line like arrows, middle diamonds and so on;
Pictograms, figures and lines can be defined anywhere inside a grammar. The diagram {...} area will contain the list of figures and lines that can be used in that specific diagram. In this way, we can have multiple diagrams sharing any number of figures and lines.
In Macrocoder, a figure is a logical element: it does not have its own graphic description. Its graphic description is delegated to a pictogram.
A pictogram is a vectorial picture that describes a shape: the figure simply uses the pictogram to display itself.
In the picture below, there are some examples of pictogram:
Also, pictograms can be used to decorate lines, adding them shapes, arrows and other pictures (see Lines).
The sequence of actions required to define a figure is:
define a pictogram named pic1;
define one or more figures using the pictogram pic1;
As we will see, pictograms can be parametric: for example, their color or line thickness can be passed as a parameter. In this way, different figures can share the same pictogram with different colors or other distinctive details. For example, the red box could be a figure and the green box another figure: they both share the same pictogram but with different colors.
Furthermore, pictograms can be composed: a pictogram could have a base form, while another might be identical but with added details. For example, in the picture above, the man with the hat is the picture of the man without the hat with an addition.
Pictograms are formed by the followed items:
pictures, graphic shapes like polygons, polylines and so on;
fields, named rectangular areas where the user can write;
connpoints, spots on the figure where lines can be connected.
Pictures are formed by one ore more components among:
polylines - a line made of segments; color, thickness and joint curve can be specified;
polygons - a closed polyline; the areas formed by this polyline can be flood-fillen of the given color;
labels - a fixed text;
assembly - a group of other pictures items;
All the items specified inside a pictogram are displayed in the specified order; therefore, if overlaying, objects specified first will be displayed below objects specified after.
A polyline is a line made of segments. Its general form is:
polyline {
thickness = 3;
color = 0x00FF00FF;
joint = 1;
hatching = 0; path = (-15, 60), (0, 40), (15, 60);
}
The parameters are here explained:
thickness - thickness of the line in pixel; if not specified, default is 1 (see picture below);
color - color of the line; colors are specified in hexadecimal format using 0xRRGGBBAA where RR is the red component, GG green and BB blue; AA is the alpha channel, that indicate transparency: AA=0x00 means transparent, while AA=0xFF is totally opaque;
joint - indicates the radius of the curve that is used to join differet segments (see picture below);
hatching - indicates the hatching (see picture bellow); default is 0;
path - sequence of points that form the polyline;
A polygon is a closed line, made of segments. Its general form is:
polygon {
thickness = 0;
joint = 1;
hatching = 0; color = 0x8F505FFF;
path = (-30,-30), (-20, -20), (20, -20), (10, -30);
fill = (0, -25);
fillcolor = 0xFFE0A0FF;
}
Parameters are here explained:
thickness, joint, hatching, color: these parameters have the same meaning as in the polyline command; they refer to the outline of the polygon
path - is the set of points that form the figure; if the last point does not match the first one, a closing segment is generated automatically;
fill - is the position where the polygon will be filled; the algorithm used is a flood-fill; if the fill action is done outside the polygon, a limited external area will be filled;
fillcolor - is the color used to fill the polygon;
A label is a fixed text that is printed at the given position. Its general form is:
label {
area = (-30, -30), (40, 20);
left align;
label = "Abcd";
color = 0xFF0000FF;
}
Parameters are here explained:
area - is the rectangle where the text has to be placed; it is expressed as (left, top), (width, height); the text falling out of this area, is clipped;
left align, right align, center align: horizontal alignment of the text in its assigned area;
label - the text to be displayed; instead of a quoted text ("..."), an unquoted freetext can be used: in that case, font size, bold, italic and so on will be taken in account;
color - color of the text, in 0xRRGGBBAA form;
A field is a text area like the label described above, but its contents can be edited by the user. The field is where textual user syntax can be added to graphical diagrams.
field {
area = (-30, -30), (40, 20);
left align;
label = "Abcd";
color = 0xFF0000FF;
fieldname = "myField";
}
The field pictogram item has the same parameters as a label plus the fieldname entry. The label entry is used as an initial default value for the field; if omitted, the field is generated empty.
See the Figures chapter for details on how fields are used.
Fieldnames must be unique among all the fields defined in a pictogram.
A connpoint is a spot where lines can be connected to a figure. If a figure uses a pictogram without connpoints, no lines can be connected to it.
Connpoints are defined with the following syntax:
connpoint {
position = (0, -30);
connid = 1;
}
The meaning for each field is:
position - the position where the connpoint must be placed;
connid - a numeric id given to this connpoint; this value must be unique among all the connpoints that form a pictogram; it can be used if the connpoint where a line has been connected matters for the meaning of the connection. If not specified, an unused value will be assigned automatically.
An assembly is a composition of other picture items. Its general form is:
assembly {
...
polyline {...}
polygon {...}
}
The picture items inside an assembly are painted in the listed order, with the objects specified first printed below.
A pictogram is an entity that associates a pictogram item to a name. A pictogram can be associated to something as simple as a label or a polyline, but usually it is associated to an assembly.
This is its syntax:
pictogram label MyPictogram1 {
label = "Foobar";
}
pictogram assembly MyPictogram2 {
label {
...
}
polygon {
...
}
field {
...
}
connpoint {...}
}
The example above defines two pictograms, called MyPictogram1 and MyPictogram2. First pictogram is based on a simple label, while the second pictogram defines an assembly that contains several other pictogram items (a label, a polygon, a field and a connpoint).
Pictograms can be defined only inside a diagram lifeset.
Pictograms can be defined with parameters; in this way, the same pictogram can be reused with small changes (like color, size, etc.). So we can define a single "box" pictogram and have, for example, a figure using a red box and another using a blue box.
This is
pictogram assembly PersonPictogram (color brdCol, thickness thk1) {
// Polygon forming the 'head' of the shape
polygon {
// Thickness of the poligon border
thickness = thk1;
joint = 20;
// Sequence of points forming the polygon
path = (-10,-10), (10, -10), (10, 10), (-10, 10);
// Position where the fill is started
fill = (0, 0);
// Colors for fill and border (format is 0xRRGGBBAA, where AA=ff means opaque)
fillcolor = 0xDFEFFFFF;
color = brdCol;
}
}
figure PersonFigure using pictogram PersonPictogram (brdCol=0x000000FF, thk1 = 8) personName:ident;
In the example above, the PersonPictogram pictogram has two parameters: brkCol and thk1. The first one is used to set the shape color, while the latter defines the thickness of the polygon.
Parameters must be declared indicating their use. It can be one among the following:
color - used for color indications;
numeric - used where numbers are involved, like in path, fill and other elements indicating size or position;
textual - simple string used in the label text field of labels and fields;
richtext - freetext formatted string used in the label text field of labels and fields;
thickness - used for line thickness indications;
hatching - used in hatching indications;
A pictogram can include one or more other pictograms and add features to them.
pictogram assembly PersonPictogram (thickness thk1) {
// Include the "PersonPictogramBase" pictogram
PersonPictogramBase (brdCol = 0x000000FF, thk1=thk1)
// Active field: specify its area and label
field {
// Area is (x,y), (w,h)
area = (-35, 35), (70, 20);
fieldname = "personAge";
center align;
}
}
In the example above, pictogram PersonPictogram includes PersonPictogramBase. It supplies the brdCol parameter with a fixed value (0x000000FF) and uses its thk1 parameter to set PersonPictogramBase's thk1 parameter. Then, it adds a new field named "personAge".
A figure defines the object that users will be actually placing on their diagrams. Figures are defined inside a diagram lifeset.
A figure defines:
which is the pictogram that will be used to represent the figure;
which is the textual grammar that will be used to parse the fields contained in the pictogram;
which lines can be connected to it;
The following code is an example of the syntax to be used to define a figure:
figure figureName [inherits baseClass] using pictogramName fields allows;
For example:
figure State using pictogram StatePictogram stateName:ident
allow 1 to unlimited incoming Transition
allow unlimited outgoing Evolution;
The above definition creates a figure named State whose pictogram is StatePictogram. It has only one field named stateName that will be parsed by the simple grammar rule ident (see figure fields).
This figure requires one to infinite incoming lines of type Transition and any number (including zero) outgoing lines of type Evolution.
Figures can have fields whose contents are filled by the user. The contents of these fields are themselves parsed by the grammar parser and their syntax checked.
In the example below, we will define a rectangular figure with two fields. Top field, named boxName, will contain a simple ident describing the name of the box. The bottom field, named boxContents, will support a syntax like "operation name is started at HH:MM":
lifecycle Example {
diagram SampleDiagram {
files "diagram";
// [1]
pictogram assembly BoxPict {
polygon {
thickness = 2;
fillcolor = 0xC0FFC0FF;
fill = (0, 0);
path = (-80, -50), (80, -50), (80, 50), (-80, 50);
}
field {
fieldname = "boxName";
area = (-80, -46), (160, 20);
center align;
}
field {
fieldname = "boxContents";
area = (-78, -24), (156, 70);
center align;
}
}
// [2]
figure Box using pictogram BoxPict boxName:ident boxContents:OperationDef;
// [3]
rule OperationDef ::= "operation" operationName:ident "is started at" hours:numeric ":" minutes:numeric;
}
}
The example:
[1] defines a pictogram called BoxPict with two fields, boxName and boxContents;
[2] then it defines a figure called Box that uses the previously defined pictogram; the figure specifies the syntax for the two fields: a simple ident for boxName and an OperationDef rule for field boxContents;
[3] defines an OperationDef textual grammar rule that will be used to parse the boxContents field.
This is the resulting graphic appearance:
Each figure must list all and only the fields that have been specified in the pictogram it is using. For example, the figure Box specified above must define exactly fields boxName and boxContents because these are the fields defined in the BoxPict pictogram.
A figure accepts lines connected to its connpoints. As described in the Lines chapter, Lines always have a logical direction: a line connected to a figure at its starting point is called outgoing; a line connected at its ending point is called incoming.
By default, a figure accepts any number and type of incoming and outgoing lines. However, in most projects the type and number of lines that can be connected to a figure must be limited. For example, a Man object might have any number of outgoing Fatherhood lines (i.e. any number of children), but it must have a single incoming Fatherhood line (i.e. one and only one father).
These relationships can be controlled using the allow keyword:
allow range (incoming|outgoing) lineType
where:
range can be an exact number (allow 3 ...), a proper range (allow 3 to 8 ...), an open range (allow 3 to unlimited ...) or any number (allow unlimited ...);
incoming or outgoing indicate the allowed direction;
the lineType contains the name of the type of line allowed;
in every figure, a given lineType can be listed at most once incoming and once outgoing.
For example:
figure State using pictogram StatePictogram stateName:ident
allow 1 to unlimited incoming Transition
allow unlimited outgoing Evolution;
This mechanism can be used to validate simple connections; when the connection rules are too complex to be expressed using the allow statement, the lines validation coding techniques must be used.
Lines are defined using the following syntax:
line lineName lineDetails lineFields linePictograms;
The various parameters are here described:
lineName - this is the name of the line and the name of the type that will be created after it; it is mandatory and it must be unique among a given diagram;
lineDetails - these are the same line details (thickness, joint, color, etc.) as those used by the polyline: they describe the appearance of the line;
lineFields - editable fields that can be added to the line; see the line fields chapter for more details;
linePictograms - editable fields that can be added to the line; see the line pictograms chapter for more details;
line Transition color=0x000000FF thickness=2 joint=4
pictogram(90): ConnectionLinePictogram (Col=0x000000FF, Thk=2)
pictogram(50): Diamond
event(100-20): ident;
Lines require both their ends to be connected to a figure; if they are not connected, the compiler will not go on and the disconnected edges will be evidenced by a red square.
Lines can have text fields whose contents can be written by the user and parsed with the textual parser.
A line can have any number of fields; these fields will bound to a given position of the line when the line is moved.
For example, the line below has three fields called field1, field2 and field3:
line Line thickness=3
field1(0+20):freetext field2(50):ident field3(90):MyRule;
rule MyRule ::= myName:ident;
The fields are associated to three different grammar rules: field1 is parsed by the freetext rule, field2 by a simple ident and field3 is parsed by a text rule called MyRule.
The values between parentheses indicate their bind position on the line. The available formats are (P), (P+d) or (P-d). P is the position in percentage on the line, where 0 is the line start and 100 is the line end. Parameter d is a fixed distance from the percentage position.
For example, (50) means "at the center of the line"; (0+20) means "20 pixel after the beginning of the line".
This is how it appears: the edit mode on the left shows where the lines are bound.
Line pictograms are pictograms that are superimposed to the line to create decorated lines.
Let us see the example below:
pictogram polyline Arrow {
path = (-10, 10), (0, 0), (10, 10);
thickness = 3;
}
pictogram assembly LineBox {
polygon {
path = (-30, -8), (30, -8), (30, 8), (-30, 8);
fillcolor = 0xFFFFC0FF;
fill = (0, 0);
}
label {
area = (-30, -7), (60, 16);
center align;
label = "is son of";
}
}
line MyLine thickness=3 pictogram(50) horizontal:LineBox pictogram(100-20):Arrow;
This line is decorated with two pictograms, Arrow, placed at 20 pixel from the end and LineBox, placed in the middle of the line (A). The specified position on the line is where the (0,0) point of the pictogram will be placed (see red dot in A).
The pictograms must be designed for a line going upwards (A): they will be automatically rotated following the line orientation (B).
If a pictogram must not to be rotated (like the "is son of" label in C), it must be declared with the horizontal flag, as exemplified for the MyLine definition above.
As it happens with text grammar, also diagrams are mapped to classes automatically generated by Macrocoder.
Being the diagram lifeset an extension of the grammar lifeset, all the same rules apply. Also in diagrams, all the generated classes are derived directly or indirectly from GBase.
Figures are mapped to a class named as the figure itself. The figure class derives from GFigure, which is derived from GBase.
A figure is implemented as a grammar sequence; the figure fields are implemented exactly as they were grammar sequence fields. Furthermore, the generated figure class will have fields containing links to the connected lines.
Let us take the example below:
figure State using pictogram StatePictogram stateName:ident
allow 1 to unlimited incoming Transition
allow 1 outgoing Evolution;
The generated class will be this one; as usual, attributes written in italic are actually defined in one of the base classes, but reported here for clarity:
class State: GFigure {
// 'locator' is inherited from GBase;
GLocator locator;
// Inherited from GFigure
GNumeric shapeId;
// Inherited from GFigure
set of GLine incomingLines;
set of GLine outgoingLines;
// Attributes generated after 'allow' statements
set of Transition incoming_Transition;
link of Evolution outgoing_Evolution;
// Field 'stateName'
GString stateName;
}
The attributes are here explained:
locator - is the position where the entire figure has been declared; this attribute is available to all grammar and diagram objects and can be used for error reporting;
shapeId - unique numeric id assigned to this instance; it can be used to uniquely identify this instance among all the objects available in a diagram;
incomingLines - contains a link to every line whose end is connected to this figure instance;
outgoingLines - contains a link to every line whose start is connected to this figure instance;
incoming_Transition - this set has been created because of the "allow 1 to unlimited incoming Transition" definition; this set will contain all and only the incoming lines of type Transition; since "1 to unlimited" allows many objects to be connected, Macrocoder created a "set".
The name is formed by adding the "incoming_" to the name of the line type, which is "Transition" in this case.
The lines linked here will also be linked by the incomingLines common set.
outgoing_Evolution - this is the attribute created after the declaration "allow 1 outgoing Evolution;"; in this case, since the maximum number of connected lines is one, Macrocoder created a link. The lines linked here will also be linked by the outgoingLines common set.
stateName - this field has been created after the stateName field; the rules for these fields are the same as the sequence rule used in textual grammars.
Lines are mapped to a class named as the line itself. The line class derives from GLine, which is derived from GBase.
A line is implemented as a grammar sequence; the line fields are implemented exactly as they were grammar sequence fields. Furthermore, the generated link class will have fields containing links to the connected figures.
Let us take the example below:
line MyLine thickness=3
field1(0+20):freetext field2(50):ident;
The generated class will be this one; as usual, attributes written in italic are actually defined in one of the base classes, but reported here for clarity:
class MyLine: GLine {
// 'locator' is inherited from GBase;
GLocator locator;
// Inherited from GLine
GNumeric incomingShapeId;
GNumeric incomingConnId;
link of GFigure incomingShape;
GNumeric outgoingShapeId;
GNumeric outgoingConnId;
link of GFigure outgoingShape;
GDecoString field1;
GString field2;
}
The attributes are here explained:
locator - is the position where the entire line has been declared; this attribute is available to all grammar and diagram objects and can be used for error reporting;
incomingShapeId - shapeId of the shape connected to the start side of the line; this value will match the shapeId field of the figure connected;
incomingConnId - contains the unique identification number of the connpoint; this value can be specified using the connId field when defining the connpoints. This value can be used to discriminate among multiple connpoints when each connpoint has a different meaning (see connpoint discrimination).
incomingShape - link to the figure connected in the starting side;
outgoingShapeId - same as incomingShapeId but related to the figure connected to the line end;
outgoingConnId - same as incomingConnId but related to the figure connected to the line end;
outgoingShape - same as incomingShape but related to the figure connected to the line end;
field1, field2 - attributes generated after the fields named field1 and field2; the rules for these fields are the same as the sequence rule used in textual grammars.
TBD
TBD
The Macrocoder programming language is based on a set of semantic rules that define the entities that can be deployed and their rules.
Macrocoder language has been specifically defined to implement Domain Specific Languages and their processing procedures.
Note: unfortunately, the concepts expressed in this document have looping reference. Therefore, some concepts (written in italic) will be used before having been defined, but the reader will find their definition later on.
A metatype is a family of types. Macrocoder supports the following metatypes:
classes;
interfaces;
composed types.
A type is a definition that describes what kind of data and procedures we will need to describe an entity. For example, Int is a type that describes what we need to describe an integer value. Other types can be more complex, including a set of procedures (methods) needed to manipulate that kind of information.
An object (also often called instance) is a record of data, organized as defined in its type, that contains real information the program is managing. For example, 234 is an object (or instance) of type Int.
The class metatype:
it is the typical class, with methods and attributes;
it can expose interfaces;
it can derive from another class of the same purpose (see below);
This metatype includes the internal basic types, like Int or String, plus all classes written by the user or generated by Macrocoder itself.
The interface metatype:
it is only a set of methods;
it can be exposed by a class, that implements its methods;
it can derive from another interface of the same purpose (see below);
This metatype is used to define method interfaces that other classes can expose and implement.
The composed metatype:
includes all those types that create relationships among objects;
these types are further divided in owning and linking;
owning composed types are containers for their related objects; if the owning object is deleted, their related objects are deleted as well; they are arrays and variants;
linking composed types are just references to their related objects; if a linking object is deleted, nothing happens to the data it is referencing; they are links, sets and lookups.
A class, as usual in object-oriented languages, is a construct used to define a distinct type.
Besides its metatype, each type has a purpose that can be data or structural.
Types with data purpose:
it is defined outside a lifeset
it does not support evolutionary methods
it methods have invokeability phase range=initialize,finalize
it can inherit only from another "data" type
it can expose only "data" interfaces
its methods and attributes can refer only "data" types
a lifecycle class (which purpose is data), can contain lifesets, whose purpose is structural
Types with structural purpose:
it is defined inside a lifeset
it supports evolutionary methods
its methods have variable invokeability phase range
it can inherit only from another "structural" type defined in its same lifeset
it can expose "data" or "structural" interfaces (from the same lifeset) interfaces
its methods and attributes can refer both "data" and "structural" (from the same lifeset) types
it can have links to structural objects of different lifesets, if they are chain created by its lifeset
it can't have explicit constructor
Lifecycles are classes devoted to the management of the entire parsing and generation process. They:
contain a group of related lifesets that form a complete lifecycle
the lifecycle purpose is data
Lifecycles import is a mean to reuse lifecycle implementation. A lifecycle B can import lifecycle A which is taken as a base library for B.
a lifecycle B can import lifecycle A: this means that lifesets and phases defined in A are duplicated in B;
a lifecycle can import any number of other lifecycle as long as there are no conflicting names;
a lifecycle can indirectly import the same lifecycle multiple times, but it will be imported only once;
lifecycle import sequence must not form any loop.
Phases are the logical steps that orderly bring the evolution towards the final generation.
the system contains ordered phases;
phases are proper and confined to the lifeset they belong to;
every lifeset contains two default phases named initial and final;
phases inside a lifeset are properly ordered;
the phases of two separate lifesets can not be compared unless lifesets are chained; in that case, all phases of first lifeset are before all phases of the second lifeset;
phase initial is before every existing phase;
phase final is after every existing phase;
every object is born with phase set to initial; in other words, every object at its birth, looks as it performed an evolution that ended to initial;
The instancing deadline is a characteristic of structural types: it is the maximum allowed phase when that type can be instanced.
if type T has instancing deadline=f, it can be instanced within methods having invocation phase range=(f1,f2), where f2<=f;
structural purpose classes have instancing deadline=initial unless differently specified;
data purpose classes have always instancing deadline=final;
all metatypes other than class have instancing deadline set to final;
lifeset and lifecycle classes have instancing deadline set to iniitial;
a class instancing deadline must be equal to the instancing deadline of its base class;
The invokeability phase range (f1,f2) is a characteristic proper to every method m. It defines the range of phases within which method m can be invoked.
A method m with invokeability phase range=(f1, f2) can be invoked in the following cases:
within another method offering invocation phase range=(Fa,Fb) included in (F1,F2);
if f1=initial, method m can be invoked on initiating instances;
An evolutionary method(f) on a type T with instancing deadline=fd has always invokeability phase range chosen with the following rules:
if fd <= f: invokeability phase range = (f, f);
if fd > f: invokeability phase range = (f, fd);
Constructor methods for type T having instancing deadline=fd always have invokeability phase range=(initial, fd).
If not automatically nor explicitely defined, default invokeability phase range is (initial, final).
The invocation phase range (Fa, Fb) is a static information that a caller provides to a called method m.
A caller can invoke a method m having invokeability phase range=(F1, F2) only if its invocation phase range (Fa, Fb) respects the relationship F1<=Fa<=Fb<=F2.
This rule guarantees that this invocation is originated from an evolutionary method (f) where Fa <= f <= Fb.
When offering invocation phase range (Fa, Fb), all the objects instanced in the lifeset are guaranteed to have their evolution state (es) set as: evolved(Fa-1) <= es <= EVOLVED(Fb).
The offered invocation phase range of a method m matches its invokeability phase range except where otherwise stated.
The evolution is the process of bringing each object through all the phases.
all objects instanced inside a lifeset go through their evolution
an evolution means that they change their evolution state in the order defined by the phase sequence
if a new instance "i2" is dynamically created by "i1" while in phase "f", as soon as "i2" is enrolled to the lifeset cauldron or associated to a variant, it will start its evolution from initial and it will go through all the phases until reaching "f". In this way, newly created objects are quickly evolved and brought to the same evolution level of other objects.
All Macrocoder objects, during their life, reach various evolution states following the phases order. Within one phase f, an object reaches four evolution states: pre-evolving, pre-evolved, evolving and evolved.
Pre-evolving(f) - This evolution state is reached by an object when it is executing its pre phase f function.
Other objects see a pre-evolving object as it was evolved(f-1); in other words, while an object is executing its pre phase, other objects can access to it as it was still at the previous evolution state.
Pre-evolved(f) - This evolution state is reached by an object when it has terminated its pre phase f function but not yet started its on phase f method.
the object has completed its pre evolutionary method(f) (i.e. pre phase f);
all managed prephased children with finalize<=f are finalized;
all managed phased children with finalize<f are finalized;
all managed shared children with finalize<f are finalized;
Evolving(f) - This evolution state is reached by an object when it is executing its on phase f method. Exeternal objects see an evolving(f) object as a pre-evolved(f).
Evolved(f) - The object has completed its post evolutionary method(f), i.e. the on phase f method. The object is therefore fully evolved to phase f.
all managed prephased children with finalize<=f are finalized;
all managed phased children with finalize<=f are finalized;
all managed shared children with finalize<=f are finalized;
Every lifeset has a lifeset cauldron that hosts all the objects that have no other parent. A cauldron:
accepts only unblessed instances;
accepts only types defined in its lifeset;
an object bound to a cauldron can not be deleted; it will be automatically deleted at the end of the entire evolution.
Lifesets can be chained: a lifeset "L1" can, during its evolution, create and setup instances inside lifeset "L2".
lifeset "L1" can access for reading or writing only the attributes of "L2" defined managed shared with enable=initial;
lifeset "L2" can start its evolution only after "L1" has terminated its own;
"L1" is the chain creator of "L2". "L2" is "chain created" by "L1".
there can be multiple chain creators for a lifeset: this means that multiple lifesets can feed the same lifeset before it starts its evolution
for every lifeset "L2" created by the lifeset "L1" a link attribute named "L2" is created within the "L1" lifeset class (shared enable=initial finalize=initial);
the lifeset type is purpose structural;
at any given time, there is only one lifeset evolving; all the other lifesets, according to the lifeset chain, are either at their initial or final state;
two lifesets not related by a creation chain will execute their evolution in an unpredictable order;
Normally, in the Macrocoder environment, children objects are evolved before their parents; when a parent executes its on phase f method it can be sure that all of its children (attributes and other owned objects) already executed their on phase f method.
Prephasing is the action of bringing some managed attributes to phase "f" before their siblings substructures are evolved to "f".
The "prephasing" concept is needed when some container objects need to be evolved before its children and it is obtained with the pre phase method. This allows actions to be executed before and after the execution of children evolution functions.
In Macrocoder a type describes attributes and methods of a given object. However, when objects are to be passed from a method to another, they need more details to be specified. For example, a method might return a constant object, i.e. an object that can be read but not modified. Or another method might not require an exact type but any object that exposes the interfaces I1 and I2.
These kinds of specifications require something that includes types plus other details: this specification is called an hypertype.
An hypertype can contain one or more types. If the contained types are more than one, they must respect the following rules:
all types must be interfaces;
their purpose must be the same (all data or structural);
if they are structural, they must belong all to the same lifeset;
Given the above rules, an hypertype can specify an object of a given type (or derived), or an object that exposes a given set of interfaces. The rules above make sure that an object can actually exposes all the required set of interfaces. For example, a "purpose data" object can expose only "purpose data" interfaces: an hypertype requiring a data and a structural interface would be impossible to satisfy.
Hypertypes can contain the const flag that restricts access to read-only operations.
Macrocoder controls access to data and methods not only using the usual private/protected/public restrictions, but also considering the current phase and calling object.
Note that code execution starts always from a on phase f or pre phase f method; when the descriptions below refer to "phase f", they intend the phase f that was specified on the pre/on phase call that originated this execution.
Hypertypes have one of the following statuses:
simple - assigned to all generic values; they are guaranteed evolved at least to evolved(f). Methods implemented in "purpose data" types, have all their hypertypes (method parameters and return values) set to simple.
initiating - assigned to all newly created objects having purpose structural until they are enrolled into a cauldron; see chapter "initiating status" below.
phased - assigned only to "this" in post-evolutionary methods; an object with phased status has the right of writing into managed phased attributes.
pre-phased - assigned only to "this" in pre-evolutionary methods; an object with pre-phased status has the right of writing into managed pre-phased attributes.
transitional - assigned to evolved attributes within a post-evolutionary method (f); those attributes evolution is "evolved(f)" (because children evolve before their parents), but links are returned as evolved(f-1), because they might refer to objects not yet evolved.
pretransitional - assigned to parents when executing an upscan operation; it means that the object has already executed its pre phase f method but not yet its on phase f method.
Let us take a method m expecting a parameter p1 of hypertype HT1; that method can be fed with a value of hypertype HT2 as long as HT2 is identical to HT1 or less retrictive. An hypertype HT2 is considered "less restrictive" than HT1 when:
given any type T1 in HT1, HT2 provides with a type T2 equal or derived from T1;
if HT1 is const, HT2 can be non const, never vice versa;
any hypertype status, except for initiating, can always be converted to simple;
hypertype status initiating can not be converted to other than initiating unless the type is on a different lifeset than the one where the action is developing.
The initiating status is assigned to structural objects that have been just instanced. Due to the evolution system, an evolutionary method for phase f encounters instances that are evolved at most to f or f-1. The only exception is when the evolutionary method creates a new object obj1.
In that case, the newly created object has evolution initial and it will be quickly evolved to phase f as soon as enrolled to the lifeset cauldron. The hypertype status of the newly created instance is set to initiating: in this way the compiler knows that obj1 is at its initial evolution level and grants all the required rights.
the 'initiating' status is given to an instance and carried by its HYPERTYPE;
the 'initiating' status is assigned to every variable instanced with "var" with PURPOSE STRUCTURAL;
the 'initiating' status is assigned to every insance created "new" with PURPOSE STRUCTURAL;
the 'initiating' status ensures that an instance is exactly evolved(initial);
as soon as an 'initiating' instance is enrolled into its lifeset cauldron or other operation that causes it to evolve, it looses its 'initiating' status; all references to that instances are instantly cleared, so the instance can't be accessed anymore as 'initiating'. The "enroll" function returns the enrolled object with the new hypertype status;
Evolutionary methods are special methods executed when a phase evolution has to be performed. They are further divided in:
pre-evolutionary methods, defined with the syntax "pre phase f", which execute the evolution from evolved(f-1) to pre-evolved (f);
post-evolutionary methods, defined with the syntax "on phase f", which execute the evolution from pre-evolved(f) to evolved (f);
Methods pre/on phase are called automatically by the Macrocoder execution engine; however, these methods can split their execution on other user defined evolutionary methods; they are like normal methods but defined with phased or pre-phased this hypertype.
Standard evolutionary methods pre/on phase, besides the normal implicit this parameter, have another implicit parameter called lset. This parameter is a pretransitional reference to the lifeset object.
The assignment a=b of an object b of type T2 to object a of type T1 means that the contents of b are copied on a. This is allowed if the following conditions hold true:
type "T1" is identical or derives from "T2";
type "T2" does not contain attributes managed phased nor managed prephased;
type "T2" does not contain attributes managed shared with enable!=INITIAL nor finalize!=FINAL;
type "T2" does not contain composite types (arrays, links, variants, etc.).
Variants are owning composed types. Scope of a variant is to contain one object whose type is decided at runtime. According to the variant declaration, the object type must derive from a given base class or expose the given interfaces.
Variants must conform to the following rules:
variant can host any non-composed class type;
variant can host interfaces only if structural or native;
if variant type is structural, the instance accepted by "set" method must be initiating;
a variant containing structural items can be instanced only as an attribute of a structural class;
variant's purpose is structural or data according to the type it contains;
See also chapter Variants.
Arrays are owning composed types. Scope of an array is to contain zero or more objects whose type is decided at runtime. According to the array declaration, the object type must derive from a given base class or expose the given interfaces.
Arrays must conform to the following rules:
array items have an order;
array can host any non-composed class type;
array can host interfaces only if structural;
if array type is structural, the instance accepted by "set" must be initiating;
a array containing structural items can be instanced only as an attribute of a structural class;
a array can not host composed types;
array's purpose is structural or data according to the type it contains;
See also chapter Arrays.
Links are linking composed types. Scope of an array is to hold a reference to another object that is owned by someone else. According to the link declaration, the linked object type must derive from a given base class or expose the given interfaces.
Links can be dependent or independent.
Independent links are simple links, that unconditionally refer to an object. They have no structural restrictions and they can form looping references.
Dependent links, instead, create a dependency relationships from the linking and the linked objects.
This dependency relationship implies that:
the items pointed by a dependent link are evolved before it;
it can never point to a object within its object structure (i.e. an attribute can not have a dependent link to another attribute of the same object);
dependent links can't be arranged to form a loop, otherwise the Macrocoder engine will stop with an error;
dependent links can be instanced only in structural types;
See also chapter Links.
Sets are linking composed types. Scope of a set is to hold references to zero or more other object that are owned by someone else. According to the set declaration, the linked objects type must derive from a given base class or expose the given interfaces. These composed types are like arrays, except for the fact that sets do not own the data they reference.
See also chapter Sets.
Lookups are linking composed types. Scope of a lookup is to associate a key to references to zero or more other object that are owned by someone else. According to the lookup declaration, the linked object type must derive from a given base class or expose the given interfaces. These composed types are like arrays, except for the fact that sets do not own the data they reference.
See also chapter Lookups.
The r.cast(T) call attempts to convert a reference r to type TT For example, a reference to an object of type A might actually refer to an object of type T, where T derives from A, In this case, in order to access the extra methods and attributes that T adds to those defined in A, th reference must be casted.
If casting can not be executed due to types incompatibility, the casting function will return null.
Cast can not convert hypertype status and flags, which are inherited unchanged. For example, a cast conversion on a const simple will return a const simple.
The r.upscan(T) operation traverses the instances tree upwards starting from r and stops to the first instance that can be casted to T, excluding r itself.
The upscan operation returns an hypertype as described in the access rules chapter.
The returned hypertype will be const if r is const.
Some composed types implement methods and attributes forwarding. This means that accessing the method m (or attribute a) on the composed object has the same effect as doing it on the referenced object itself.
For example, if lnk is a link, calling lnk.m1() is the same as calling lnk.get().m1().
Methods and attributes forwarding undergoes the following rules:
static methods are not forwarded;
evolutionary methods are not forwarded;
methods conflicting with methods already exposed by the composed object are not forwarded.
The Macrocoder phase system is heavily based on rules controlling access to attributes. When accessing obj.attr, the ability to access attr, whether it is returned with or without write access and its hypertype status are calculated starting from the hypertype status of obj.
Now we will describe the hypertype status and access rights (denied, read-only, read-write) of an attribute obj.atr given the status of obj. The conditions are:
access is done inside a method with invocation phase range (f1, f2);
accessed attribute atr has enable=Fe and finalize=Ff;
Note: if obj is read-only, data accessed from it is always read-only even when the rules states read-write.
If object obj is simple:
if atr is shared, returns simple read-write if Fe<=F1<=F2<=Ff; simple read-only if Fe<=F1 && Ff<F2; denied otherwise;
if atr is prephased, returns simple read-only if Ff<F1; it is never returned read-write;
if atr is phased, returns simple read-only if Ff<F1; it is never returned read-write;
if atr is a link, atr.get() returns always simple read-write;
if atr is a dependent link, atr.get() returns always simple read-write;
obj.upscan returns always simple read-write.
If object obj is initiating:
if atr is shared, if Fe=inital, it returns initiating read-write; otherwise, access is denied;
if atr is prephased, access is denied;
if atr is phased, access is denied;
if atr is a link, atr.get() is denied;
if atr is a dependent link, atr.get() is denied;
obj.upscan returns initiating read-write.
If object obj is phased:
if atr is shared, if Fe<=F1<=F2<=Ff it returns transitional read-write; if Fe<=F1 && Ff<F2 it returns simple read-only; otherwise, access is denied;
if atr is prephased, if Ff<=F1 it returns transitional read-only; otherwise, access is denied;
if atr is phased, if Fe<=F1<=Ff it returns transitional read-write; if Ff<F1 it returns transitional read-only; otherwise, access is denied;
if atr is a link, atr.get() returns simple read-write;
if atr is a dependent link, atr.get() returns transitional read-write;
obj.upscan returns pretransitional read-write.
If object obj is pre-phased:
if atr is shared, if Fe<=F1<=F2<=Ff it returns pretransitional read-write; if Fe<=F1 && Ff<F2 it returns simple read-only; otherwise, access is denied;
if atr is prephased, if Fe<=F1<=Ff it returns simple read-write; if Ff<F1, it returns simple read-only; otherwise, access is denied;
if atr is phased, if Ff<F1 it returns simple read-only; otherwise, access is denied;
if atr is a link, atr.get() returns simple read-write;
if atr is a dependent link, atr.get() returns simple read-write;
obj.upscan returns pretransitional read-write.
If object obj is transitional:
if atr is shared, if Fe<=F1<=F2<=Ff it returns simple read-write; if Fe<=F1 && Ff<F2 it returns simple read-only; otherwise, access is denied;
if atr is prephased, if Ff<=F1 it returns transitional read-only; otherwise, access is denied;
if atr is phased, if Ff<=F1 it returns transitional read-only; otherwise, access is denied;
if atr is a link, atr.get() returns transitional read-write;
if atr is a dependent link, atr.get() returns simple read-write;
obj.upscan returns returns pre-transitional read-write.
If object obj is pre-transitional:
if atr is shared, if Fe<=F1<=F2<=Ff it returns simple read-write; if Fe<=F1 && Ff<F2 returns simple read-only; otherwise, access is denied;
if atr is prephased, if Ff<=F1 it returns simple read-only; otherwise, access is denied;
if atr is phased, if Ff<F1 it returns simple read-only; otherwise, access is denied;
if atr is a link, atr.get() returns simple read-write;
if atr is a dependent link, atr.get() returns simple read-write;
obj.upscan returns pretransitional read-write.
This chapter reports ther formal specification of the Macrocoder Language grammar.
The formal grammar specification is expressed according to the syntax below:
the name ::= ... syntax defines a rule named 'name';
processing starts from the root rule, which is the first defined in the list of rules;
the curly brackets "{r}" mean "repeat zero or more times rule r";
the IDENT keyword matches a string formed by a letter among A...Z, a...z or underscore (_) followed by zero or more letters of the same kinds or numbers; note that an IDENT is not allowed to begin with a number;
the EMPTY keyword matches always; it is used mainly in angle brackets sequences (see below);
the QUOTED keyword matches a double quote (") followed by any string and terminated by another quote ("); internally, the backslash (\) is the escape character following the same rules of the "C" language strings;
the VALUE keyword matches a number either decimal (eg. 1234), hexadecimal (eg. 0xABCD) or floating point (eg. 1.234);
the FREETEXT keyword expect a freetext activated by the related command in the Macrocoder editor;
the angle brackets "<a | b | ... | z >" mean "match one and only one among a, b, ..., z";
the angle brackets with EMPTY "<a | b | ... | z | EMPTY>" mean "match one among a, b, ..., z or nothing";
the square brackets "[r]" mean "match r optionally"; it is the same as "< r | EMPTY >";
a quoted string "text" is a keyword and matches exactly the given string;
The OPER keyword begins the definition of an operator:
the DATA keyword specify the rule for operands;
the INFIX-LR p defines an infix operator with left-to-right precedence, like "+" in "x+y+z"; value p indicates the precedence, where lower is executed first;
the INFIX-RL p defines an infix operator with right-to-left, like "=" in "x=y=z"; value p indicates the precedence;
the PREFIX p defines a prefixed operator with like "!" in "!x"; value p indicates the precedence;
the POSTFIX p defines a postfixed operator with like "++" in "x++"; value p indicates the precedence;
b_program ::= {b_meta_namespace}
b_meta_namespace ::= <b_declare_type | b_lifecycle | b_topic_auto>
b_namespace ::= "namespace" IDENT "{" {b_meta_namespace} "}"
b_declare_type ::= <b_namespace | b_class_data | b_interface_data | b_impl | b_topic>
b_visibility ::= <"public" | "protected" | "private"> ":"
b_envtype ::= <"noenv" | "env" b_hypertype_without_flags_no_composed | EMPTY>
b_class_data ::=
<"class" IDENT b_envtype [":" b_type_simple] | "extend class" IDENT>
"{" {b_class_data_contents} "}"
b_class_structural ::=
<
"class" IDENT b_envtype <":" b_type_simple | "phase" "=" b_phase_ident | EMPTY>
|
"extend class" IDENT
>
"{" {b_class_structural_contents} "}"
b_interface_structural ::=
<
"interface" IDENT b_envtype [":" b_type_simple]
|
"extend interface" IDENT
>
"{" {b_method_in_structural_interface} "}"
b_interface_data ::=
<
"interface" IDENT b_envtype [":" b_type_simple]
|
"extend interface" IDENT
>
"{" {b_method_in_data_interface} "}"
b_class_data_contents ::= b_class_data_base_contents
b_class_structural_contents ::= b_class_structural_base_contents
b_class_structural_base_contents ::= <
b_topic_auto |
b_class_structural |
b_interface_structural |
b_method_or_attribute_structural |
b_impl |
b_visibility |
b_expose_structural |
b_pre_phase |
b_on_phase
>
b_class_data_base_contents ::= <
b_topic_auto |
b_constructor |
b_class_data |
b_interface_data |
b_method_or_attribute_data |
b_impl |
b_visibility |
b_expose_data
>
b_phase_ident ::= IDENT
b_expose_data ::= "expose" b_type_simple ["delegating" b_expr] <"{" {b_method_data_only/} "}"|";">
b_expose_structural ::= "expose" b_type_simple ["delegating" b_expr] <"{" {b_method_structural_only} "}"|";">
b_impl ::= b_impl_keyword b_qualified_name b_block
b_impl_keyword ::= "impl"
b_lifecycle ::= <"extend lifecycle" | "lifecycle"> IDENT "{" {<b_lifeset|b_constant_alone|b_lifecycle_import|b_topic_auto>} "}"
b_lifecycle_import ::= "import" b_lifecycle_import_ident ";"
b_lifecycle_import_ident ::= IDENT
b_lifeset ::=
<
"extend lifeset" b_lifeset_body |
"lifeset" b_lifeset_body |
"extend grammar" b_lifeset_body_w_grammar |
"grammar" b_lifeset_body_w_grammar |
"extend diagram" b_lifeset_body_w_diagram |
"diagram" b_lifeset_body_w_diagram>
b_lifeset_body ::= IDENT "{" {<b_lifeset_basic_contents>} "}"
b_lifeset_body_w_grammar ::= IDENT "{" {<b_lifeset_basic_contents | b_gram_rule_def>} "}"
b_lifeset_body_w_diagram ::= IDENT "{" {<b_lifeset_basic_contents | b_gram_rule_def_wo_root | b_diag_pictogram | b_diag_shape>} "}"
b_lifeset_basic_contents ::= <b_file_ext | b_phase | b_class_structural_base_contents>
b_phase ::= "phase" b_phase_name "=" VALUE ";"
b_phase_name ::= IDENT
b_file_ext ::= "files" b_file_ext_name {"," b_file_ext_name} ";"
b_file_ext_name ::= <IDENT | QUOTED>
b_method_or_attribute_data ::=
<
"static" b_method_data_in_class
|
b_constant
|
"streamable" b_type_auto b_method_or_attribute_name b_var_initializer ";"
|
b_method_ref_return_type b_method_or_attribute_name b_method_data_setup_in_class
|
b_type_auto b_method_or_attribute_name <b_var_initializer ";" | b_method_data_setup_in_class_with_return_type>
>
b_constant ::= "const" b_type_auto b_method_or_attribute_name b_var_initializer ";"
b_constant_alone ::= b_constant
b_method_or_attribute_structural ::=
<
"static" b_method_structural_in_class
|
"const" b_type_auto b_method_or_attribute_name b_var_initializer ";"
|
"streamable" [b_attr_managed] b_type_auto b_method_or_attribute_name b_var_initializer ";"
|
b_attr_managed b_type_auto b_method_or_attribute_name b_var_initializer ";"
|
b_method_ref_return_type b_method_or_attribute_name b_method_structural_setup_in_class
|
b_type_auto b_method_or_attribute_name <b_var_initializer ";" | b_method_structural_setup_in_class_with_return_type>
>
b_method_data_in_class ::= <
b_method_ref_return_type b_method_or_attribute_name b_method_data_setup_in_class
|
b_type_auto b_method_or_attribute_name b_method_data_setup_in_class_with_return_type
>
b_method_structural_in_class ::= <
b_method_ref_return_type b_method_or_attribute_name b_method_structural_setup_in_class
|
b_type_auto b_method_or_attribute_name b_method_structural_setup_in_class_with_return_type
>
b_attr_phased ::= "phased"
b_attr_prephased ::= "prephased"
b_attr_shared ::= "shared"
b_attr_enable ::= "enable" "=" b_phase_ident
b_attr_finalize ::= "finalize" "=" b_phase_ident
b_attr_managed ::= <
b_attr_phased [b_attr_enable] [b_attr_finalize]
|
b_attr_prephased [b_attr_enable] [b_attr_finalize]
|
b_attr_shared [b_attr_enable] [b_attr_finalize]
|
b_attr_enable [b_attr_finalize]
|
b_attr_finalize
>
b_method_data_setup_in_class_with_return_type ::= b_method_data_setup_in_class
b_method_structural_setup_in_class_with_return_type ::= b_method_structural_setup_in_class
b_method_in_data_interface ::=
["static"]
<
b_method_ref_return_type b_method_or_attribute_name b_method_data_setup_in_class
|
b_type_auto b_method_or_attribute_name b_method_data_setup_in_class_with_return_type
>
b_method_in_structural_interface ::=
["static"]
<
b_method_ref_return_type b_method_or_attribute_name b_method_structural_setup_in_class
|
b_type_auto b_method_or_attribute_name b_method_structural_setup_in_class_with_return_type
>
b_method_data_setup_in_class ::= b_method_data_setup
b_method_structural_setup_in_class ::= b_method_structural_setup
b_method_ref_return_type ::= "ref" b_hypertype_contents
b_method_data_only ::= <
b_method_ref_return_type b_method_or_attribute_name b_method_data_setup
|
b_type_auto b_method_or_attribute_name b_method_data_setup
>
b_method_structural_only ::= <
b_method_ref_return_type b_method_or_attribute_name b_method_structural_setup
|
b_type_auto b_method_or_attribute_name b_method_structural_setup
>
b_method_data_setup ::=
b_envtype
"(" [b_method_param_list] ")" b_hypertype_const
<
b_method_inline_abstract
|
b_method_inline_external
|
b_method_inline_implemented_as
|
b_method_inline_block
>
b_method_structural_setup ::=
b_envtype
"(" [b_method_param_list] ")" b_hypertype_flags_for_structural_method b_method_structural_invokeability_phase_range
<
b_method_inline_abstract
|
b_method_inline_external
|
b_method_inline_implemented_as
|
b_method_inline_block
>
b_method_structural_invokeability_phase_range ::=
<
EMPTY | "phase" "=" b_phase_ident <EMPTY | "," b_phase_ident>
>
b_constructor ::=
"constructor"
b_envtype
"(" [b_method_param_list] ")"
[":" "callbase" "(" b_expr_list ")"]
<
b_method_inline_external
|
b_method_inline_implemented_as
|
b_method_inline_block
>
b_method_param_list ::= b_method_param {"," b_method_param}
b_method_param ::= ["out"] b_hypertype_without_const b_method_param_name ["=" b_expr]
b_method_param_name ::= IDENT
b_method_or_attribute_name ::= IDENT
b_on_phase ::= "on phase" b_phase_ident
<
b_method_inline_external
|
b_method_inline_implemented_as
|
b_method_inline_block
>
b_pre_phase ::= "pre phase" b_phase_ident
<
b_method_inline_external
|
b_method_inline_implemented_as
|
b_method_inline_block
>
b_method_inline_implemented_as ::= "implemented as" b_method_inline_implemented_as_name ";"
b_method_inline_implemented_as_name ::= IDENT
b_method_inline_block ::= b_block
b_method_inline_external ::= ";"
b_method_inline_abstract ::= "abstract" ";"
b_type_simple ::= [b_type_separator] b_type_qualified_part {b_type_separator b_type_qualified_part}
b_type_separator ::= "::"
b_type_qualified_part ::= IDENT
b_type_auto ::= <
"array of" b_hypertype_without_flags_no_composed|
"variant of" b_hypertype_without_flags_no_composed|
"group of" b_hypertype_with_const_no_composed|
"link of" b_hypertype_with_const_no_composed|
"dependent link of" b_hypertype_with_const_no_composed|
"lookup_s of" b_hypertype_with_const_no_composed|
"lookup_i of" b_hypertype_with_const_no_composed|
"set of" b_hypertype_with_const_no_composed|
b_type_simple
>
b_hypertype_contents ::= <b_type_auto | b_hypertype_multi>
b_hypertype_contents_no_composed ::= <b_type_simple | b_hypertype_multi>
b_hypertype_multi ::= "<" b_hypertype_multi_part { "," b_hypertype_multi_part} ">"
b_hypertype_multi_part ::= b_type_simple
b_hypertype_with_const ::=
b_hypertype_flags_with_const
b_hypertype_contents
b_hypertype_with_const_no_composed ::=
b_hypertype_flags_with_const
b_hypertype_contents_no_composed
b_hypertype_flags_with_const ::= b_hypertype_const b_hypertype_flags
b_hypertype_const ::= ["const"]
b_hypertype_flags_for_structural_method ::= b_hypertype_const b_hypertype_phased_flags
b_hypertype_flags ::= <
EMPTY |
"initiating"|
"transitional"|
"pretransitional"
>
b_hypertype_phased_flags ::= <
b_hypertype_flags|
"phased"|
"prephased"
>
b_hypertype_without_const ::= b_hypertype_flags b_hypertype_contents
b_hypertype_without_flags_no_composed ::= b_hypertype_contents_no_composed
b_qualified_name ::= [b_qualified_name_separator] b_qualified_name_part {b_qualified_name_separator b_qualified_name_part}
b_qualified_name_part ::= IDENT
b_qualified_name_separator ::= "::"
b_var_initializer ::= <EMPTY | "=" b_expr | "init" "(" b_expr_list ")">
b_var_initializer_var ::= <EMPTY | "=" b_expr | "(" b_expr_list ")">
b_var_initializer_construct ::= <EMPTY | "(" b_expr_list ")">
b_statement ::= <b_vardecl | b_refdecl | b_discard | b_if | b_while | b_for | b_switch | b_return | b_feed | b_block | b_abort>
b_discard ::= b_expr ";"
b_block ::= b_block_start { b_statement} b_block_end
b_block_start ::= "{"
b_block_end ::= "}"
b_vardecl ::= "var" b_type_auto b_vardecl_name b_var_initializer_var ";"
b_vardecl_name ::= IDENT
b_refdecl ::= "ref" b_hypertype_with_const b_refdecl_name <"->" b_expr| EMPTY> ";"
b_refdecl_name ::= IDENT
b_if ::= "if" "(" b_expr ")" b_block ["else" b_block]
b_while ::= "while" "(" b_expr ")" b_block
b_for ::= "for" "(" b_expr_list ";" b_expr ";" b_expr_list ")" b_block
b_switch ::= "switch" "(" b_expr ")" "{" {b_switch_case} [b_switch_default] "}"
b_switch_case ::= "case" b_expr {"," b_expr} ":" b_block
b_switch_default ::= "default" ":" b_block
b_return ::= "return" [b_expr] ";"
b_abort ::= "abort" ";"
b_feed ::=
"feed" b_type_simple b_var_initializer_var
<
"via" b_qualified_name ["(" b_expr_list ")"]
|
"param" b_expr_list
>
["link" b_expr_list]
<
";"
|
"do" b_block
>
b_expr ::= <b_expr_oper | "callbase" "(" b_expr_list ")">
b_expr_data ::= <
"new" b_type_auto b_var_initializer_construct
|
"cast" "(" b_hypertype_contents ")"
|
"upscan" "(" b_hypertype_contents ")"
|
b_qualified_name ["(" b_expr_list ")"]
|
QUOTED
|
FREETEXT
|
VALUE
|
"(" b_expr ")"
>
b_expr_list ::= [b_expr {"," b_expr}]
b_expr_oper ::= OPER
DATA b_expr_data
INFIX-RL 13 "->"
INFIX-LR 2 "."
INFIX-LR 12 "«"
INFIX-LR 12 "»"
INFIX-LR 11 "+"
INFIX-RL 20 "+="
INFIX-RL 20 "="
INFIX-LR 15 "&"
INFIX-RL 20 "&="
PREFIX 5 "~"
INFIX-LR 16 "^"
INFIX-RL 20 "^="
INFIX-LR 17 "|"
INFIX-RL 20 "|="
INFIX-LR 13 "<=>"
INFIX-LR 10 "/"
INFIX-RL 20 "/="
INFIX-LR 14 "=="
INFIX-LR 13 ">"
INFIX-LR 13 ">="
INFIX-LR 14 "!="
INFIX-LR 12 "<<"
INFIX-RL 20 "<<="
INFIX-LR 13 "<"
INFIX-LR 13 "<="
INFIX-LR 18 "&&"
PREFIX 6 "!"
INFIX-LR 19 "||"
INFIX-LR 10 "%%%%%%%%%%%%%%%%"
INFIX-RL 20 "%=%%%%%%%%%%%%%%%"
INFIX-LR 10 "*"
INFIX-RL 20 "*="
POSTFIX 3 "--"
POSTFIX 3 "++"
PREFIX 4 "--"
PREFIX 4 "++"
INFIX-LR 12 ">>"
INFIX-RL 20 ">>="
INFIX-LR 11 "-"
INFIX-RL 20 "-="
PREFIX 7 "-"
PREFIX 7 "+"
b_gram_rule_def ::= <"extend rule" | "rule">
b_gram_rule_def_name "::=" b_gram_rule_with_auto ";"
b_gram_rule_def_wo_root ::= <"extend rule" | "rule">
b_gram_rule_def_name "::=" b_gram_rule_with_auto ";"
b_gram_rule_def_name ::= IDENT
b_gram_rule_base ::= <
b_gram_rule_base_without_ref
|
IDENT
>
b_gram_rule_base_without_ref ::= <
"empty"
|
"quoted" ["(" "quote_open" "=" QUOTED "quote_close" "=" QUOTED ")"]
|
"freetext"
|
"ident"
|
"numeric"
|
QUOTED
|
"{" b_gram_rule "}"
|
b_gram_rule_operator
|
b_gram_rule_choice_variant
|
b_gram_rule_optional
>
b_gram_rule_typed_sequence_auto_wo_parenthesis ::= EMPTY ["inherits" b_type_simple] b_gram_rule_typed_sequence_field {b_gram_rule_typed_sequence_field}
b_gram_rule_typed_sequence_auto ::= "(" b_gram_rule_typed_sequence_field {b_gram_rule_typed_sequence_field} ")"
b_gram_rule_typed_sequence_field ::= <
b_gram_rule_base_without_ref
|
IDENT <EMPTY | ":" b_gram_rule_base EMPTY>
>
b_gram_rule_typed_sequence_named_field_for_shape ::= b_gram_rule_typed_sequence_field_name ":" b_gram_rule_base
b_gram_rule_typed_sequence_named_field_for_line ::= b_gram_rule_typed_sequence_field_name b_diag_shape_position_on_line ":" b_gram_rule_base
b_gram_rule_typed_sequence_field_name ::= IDENT
b_gram_rule_choice_variant ::= "choice" b_gram_variant "(" <b_gram_rule_base {"|" b_gram_rule_base} | EMPTY> ")"
b_gram_rule_optional ::= "[" b_gram_rule_base "]"
b_gram_rule_operator ::= "operator" b_gram_variant "(" "operdata" b_gram_rule_base {b_gram_rule_operator_op} ")"
b_gram_rule_operator_op ::=
<
"infix_rl" b_gram_rule_operator_op_contents
|
"infix_lr" b_gram_rule_operator_op_contents
|
"prefix" b_gram_rule_operator_op_contents
|
"postfix" b_gram_rule_operator_op_contents
>
b_gram_rule_operator_op_contents ::= b_gram_type VALUE b_gram_rule
b_gram_type ::= b_gram_type_name ["inherits" b_type_simple]
b_gram_type_name ::= IDENT
b_gram_rule ::= b_gram_rule_base
b_gram_rule_with_auto ::= <b_gram_rule_typed_sequence_auto|b_gram_rule_typed_sequence_auto_wo_parenthesis>
b_gram_variant ::= <EMPTY | b_hypertype_without_flags_no_composed>
b_topic ::= < "extend topic" b_qualified_name "." IDENT | "topic" b_qualified_name> "{" {b_topic_content} "}"
b_topic_content ::= <b_topic_title | b_topic_body_freetext | b_topic_link | b_topic_body_quot | b_topic>
b_topic_title ::= "title" <FREETEXT | QUOTED>
b_topic_body_freetext ::= FREETEXT
b_topic_body_quot ::= QUOTED
b_topic_link ::= "hyperlink" b_qualified_name <FREETEXT | QUOTED>
b_topic_auto ::= <b_topic_body_freetext | b_topic_body_quot | b_topic_link>
b_diag_pictogram ::= "pictogram"
<
"assembly" b_diag_pictogram_offline_decl b_diag_pictogram_body_assembly |
"polyline" b_diag_pictogram_offline_decl b_diag_pictogram_body_polyline |
"polygon" b_diag_pictogram_offline_decl b_diag_pictogram_body_polygon |
"label" b_diag_pictogram_offline_decl b_diag_pictogram_body_label |
"field" b_diag_pictogram_offline_decl b_diag_pictogram_body_field |
"connpoint" b_diag_pictogram_offline_decl b_diag_pictogram_body_connpoint
>
b_diag_pictogram_offline_decl ::= IDENT ["(" b_diag_pictogram_paramdefs ")"]
b_diag_pictogram_paramtype ::= <"color"| "numeric"| "textual"| "richtext"| "thickness"| "hatching">
b_diag_pictogram_value ::= <["-"] VALUE | QUOTED | FREETEXT>
b_diag_pictogram_paramdef ::= b_diag_pictogram_paramtype IDENT ["=" b_diag_pictogram_value]
b_diag_pictogram_paramdefs ::= b_diag_pictogram_paramdef {"," b_diag_pictogram_paramdef}
b_diag_parval_numeric ::= <["-"] VALUE | IDENT>
b_diag_parval_string ::= <QUOTED | IDENT>
b_diag_parval_decostring ::= <QUOTED | FREETEXT | IDENT>
b_diag_pictogram_inline ::= <
b_diag_pictogram_inline_assembly|
b_diag_pictogram_inline_polyline|
b_diag_pictogram_inline_polygon|
b_diag_pictogram_inline_label|
b_diag_pictogram_inline_connpoint|
b_diag_pictogram_inline_field
>
b_diag_pictogram_inline_assembly ::= "assembly" b_diag_pictogram_body_assembly
b_diag_pictogram_inline_polyline ::= "polyline" b_diag_pictogram_body_polyline
b_diag_pictogram_inline_polygon ::= "polygon" b_diag_pictogram_body_polygon
b_diag_pictogram_inline_label ::= "label" b_diag_pictogram_body_label
b_diag_pictogram_inline_field ::= "field" b_diag_pictogram_body_field
b_diag_pictogram_inline_connpoint ::= "connpoint" b_diag_pictogram_body_connpoint
b_diag_pictogram_body_assembly ::= "{" { b_diag_pictogram_invocation} "}"
b_diag_pictogram_body_polyline ::= "{"
{<
b_diag_pictogram_detail_thickness |
b_diag_pictogram_detail_joint |
b_diag_pictogram_detail_color |
b_diag_pictogram_detail_hatching |
b_diag_pictogram_detail_path
>}
"}"
b_diag_pictogram_body_polygon ::= "{"
{<
b_diag_pictogram_detail_thickness |
b_diag_pictogram_detail_joint |
b_diag_pictogram_detail_color |
b_diag_pictogram_detail_path |
b_diag_pictogram_detail_fill |
b_diag_pictogram_detail_fillcolor
>}
"}"
b_diag_pictogram_body_label ::= "{"
{<
b_diag_pictogram_detail_area |
b_diag_pictogram_detail_label |
b_diag_pictogram_detail_align_left |
b_diag_pictogram_detail_align_right |
b_diag_pictogram_detail_align_center |
b_diag_pictogram_detail_textcolor
>}
"}"
b_diag_pictogram_body_field ::= "{"
{<
b_diag_pictogram_detail_area |
b_diag_pictogram_detail_label |
b_diag_pictogram_detail_align_left |
b_diag_pictogram_detail_align_right |
b_diag_pictogram_detail_align_center |
b_diag_pictogram_detail_fieldname |
b_diag_pictogram_detail_textcolor
>}
"}"
b_diag_pictogram_body_connpoint ::= "{"
{<
b_diag_pictogram_detail_position |
b_diag_pictogram_detail_connid
>}
"}"
b_diag_pictogram_detail_thickness ::= "thickness" "=" b_diag_parval_numeric ";"
b_diag_pictogram_detail_joint ::= "joint" "=" b_diag_parval_numeric ";"
b_diag_pictogram_detail_color ::= "color" "=" b_diag_parval_numeric ";"
b_diag_pictogram_detail_textcolor ::= "color" "=" b_diag_parval_numeric ";"
b_diag_pictogram_detail_fillcolor ::= "fillcolor" "=" b_diag_parval_numeric ";"
b_diag_pictogram_detail_hatching ::= "hatching" "=" b_diag_parval_numeric ";"
b_diag_pictogram_detail_path ::= "path" "=" b_diag_pictogram_detail_point {"," b_diag_pictogram_detail_point} ";"
b_diag_pictogram_detail_fill ::= "fill" "=" b_diag_pictogram_detail_point {"," b_diag_pictogram_detail_point} ";"
b_diag_pictogram_detail_fieldname ::= "fieldname" "=" b_diag_parval_string ";"
b_diag_pictogram_detail_area ::= "area" "=" b_diag_pictogram_detail_point "," b_diag_pictogram_detail_size ";"
b_diag_pictogram_detail_label ::= "label" "=" b_diag_parval_decostring ";"
b_diag_pictogram_detail_align_left ::= "left align" ";"
b_diag_pictogram_detail_align_right ::= "right align" ";"
b_diag_pictogram_detail_align_center ::= "center align" ";"
b_diag_pictogram_detail_connid ::= "connid" "=" b_diag_parval_numeric";"
b_diag_pictogram_detail_position ::= "position" "=" b_diag_pictogram_detail_point ";"
b_diag_pictogram_detail_point ::= "(" b_diag_parval_numeric "," b_diag_parval_numeric ")"
b_diag_pictogram_detail_size ::= "(" b_diag_parval_numeric "," b_diag_parval_numeric ")"
b_diag_pictogram_invocation ::= <b_diag_pictogram_invocation_inline | b_diag_pictogram_invocation_offline>
b_diag_pictogram_invocation_offline ::= IDENT [b_diag_pictogram_invocation_params]
b_diag_pictogram_invocation_inline ::= b_diag_pictogram_inline
b_diag_pictogram_invocation_param ::= IDENT "=" <b_diag_pictogram_value | IDENT>
b_diag_pictogram_invocation_params ::= "(" b_diag_pictogram_invocation_param {"," b_diag_pictogram_invocation_param} ")"
b_diag_pictogram_invocation_from_shape ::= <b_diag_pictogram_invocation_from_shape_inline | b_diag_pictogram_invocation_from_shape_offline>
b_diag_pictogram_invocation_from_shape_offline ::= IDENT [b_diag_pictogram_invocation_from_shape_params]
b_diag_pictogram_invocation_from_shape_inline ::= b_diag_pictogram_inline
b_diag_pictogram_invocation_from_shape_param ::= IDENT "=" b_diag_pictogram_value
b_diag_pictogram_invocation_from_shape_params ::= "(" b_diag_pictogram_invocation_from_shape_param {"," b_diag_pictogram_invocation_from_shape_param} ")"
b_diag_shape ::= <b_diag_shape_figure | b_diag_shape_line>
b_gram_rule_typed_sequence_field_for_shapes ::= b_gram_rule_typed_sequence_named_field_for_shape
b_gram_rule_typed_sequence_field_for_lines ::= b_gram_rule_typed_sequence_named_field_for_line
b_diag_shape_figure ::= "figure" IDENT EMPTY ["inherits" b_type_simple] "using pictogram" b_diag_pictogram_invocation_from_shape {b_gram_rule_typed_sequence_field_for_shapes} ";"
b_diag_shape_line ::= "line" IDENT EMPTY ["inherits" b_type_simple] {<b_gram_rule_typed_sequence_field_for_lines | b_diag_shape_line_pictograms | b_diag_shape_line_detail>}";"
b_diag_shape_line_pictograms ::=
"pictogram" IDENT b_diag_shape_position_on_line "=" b_diag_pictogram_invocation_from_shape
b_diag_shape_position_on_line ::= "(" VALUE <EMPTY | <"-" | "+"> VALUE>")" ["horizontal"]
b_diag_shape_line_detail ::= <b_diag_shape_line_detail_thickness | b_diag_shape_line_detail_joint | b_diag_shape_line_detail_color | b_diag_shape_line_detail_hatching>
b_diag_shape_line_detail_thickness ::= "thickness" "=" VALUE
b_diag_shape_line_detail_joint ::= "joint" "=" VALUE
b_diag_shape_line_detail_color ::= "color" "=" VALUE
b_diag_shape_line_detail_hatching ::= "hatching" "=" VALUE