Programming Macrocoder

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.

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:

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:

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.

Rules and target projects

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.

Macrocoder Language

This chapter explains all the programming concepts of the Macrocoder Language. For the detailed formal grammar definition of FCL see chapter "Macrocoder grammar".

Classes

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

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)];
}

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

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

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:

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]];
}

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;
}

Public, protected and private visibility

Methods and attributes can be protected against other classes access. Macrocoder supports the usual public/protected/private keywords:

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.

Constructor

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

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 {...}
}

Interfaces and exposition

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

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:

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:

Note that:

Extension

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.

Lifecycle

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.

Lifecycles and lifesets

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:

lifecycle MyLifeCycle {
  
  lifeset MyLifeSet {
    phase myPhase1 = 1;
    phase myPhase2 = 1234;
  }
}

A lifeset L1 can be defined as creating lifeset L2. This means that:

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 implementation

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...
}

Objects

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:

Literals

Literals are constant objects expressed by their value. They can be:

Objects and references

In order to work with standalone objects, Macrocoder provides with means to create and reference them:

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

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:

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

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

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:

Boolean operators, like > or != return either true or false.

Explicit casts

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:

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.
}

Upscan

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

Statements are the smallest imperative instructions that can be used to define the implementation of a method.

Code block

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.

Statement if

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;
}

Statement while

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!

Statement for

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:

The three expressions should be used with the objectives below:

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

Statement switch/case

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:

Statement return

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;
  }
}

Callbase

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

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.

Statement abort

The abort statement interrupts the execution immediately. It can be used to immediately discarded.

if (x < 0) {
  abort
;
}

Statement feed

TBD

Variants

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:

Note that:

Arrays

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):

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.

Links

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:

Note that:

Dependent links

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:

Sets

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):

Lookups

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:

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):

Phase programming

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:

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:

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:

Basic phase model

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.

Declaring phases

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:

Phase scopes

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.

Phase "do" action

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 lset parameter

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.

Phase-protected attributes

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:

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.

Phase-shared attributes

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);
    }
  }
}

Phase-protected methods

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.

Phase-shared methods

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

Cross-lifeset creation

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.

Advanced phase model

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

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:

Execution order

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:

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:

Phase methods

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.

Phase protection

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:

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.

Management mode

First concept is management mode.
Management mode indicates who is in charge to update the attribute. Macrocoder supports the following management modes:

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:

Enable/finalize phases

While the management mode defines who can modify an attribute, enable and finalize phase settings determine when all operations can be performed.

Let us assume that a given attribute a has enable=p1 and finalize=p2, where p1<=p2; it holds that:

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;
      }
    }
  }
}

Phase alignment

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:

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:

Generic evolutionary methods

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.

Multiple lifesets

Macrocoder textual grammars

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 following chapters will cover these topics:

Create the grammar source file

In order to define a grammar, a source file must be defined:

Defining a grammar

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:

Understanding grammars

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:

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...".

Class mapping

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.

Grammar lifeset

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
      }
    }
  }
}

The GBase class

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.

Extending grammar classes

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
      }
    }
  }
}

Terminals

In grammar definitions, terminals are the most atomic items. Terminals can either be fixed or variable:

The terminals available in Macrocoder are the following:

Class mappings

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:

Sequences

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.

Class mappings

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.

Repetitions

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.

Class mappings

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;
}

Choice

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.

Class mappings

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;

Optional

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).

Class mappings

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.

Operator

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:

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.

Macrocoder operators model

Within Macrocoder an operator is a set composed by:

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 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.

Class mappings

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:

Grammar restrictions

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.

Using the graphic editor

This chapter illustrates in details the steps required to use the textual grammars graphic editor.

Editing lifeset and lifecycle

Lifeset and lifecycle can be edited by double clicking on the grammar header:

Adding rules

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:

Editing sequences

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:

Editing choices

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.

Macrocoder graphic grammars

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:

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.

Defining a diagram

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:

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.

Pictograms

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:

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 are formed by one ore more components among:

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.

Pictogram item: polyline

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:

Pictogram item: polygon

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:

Pictogram item: label

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:

Pictogram item: field

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.

Pictogram item: connpoint

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:

Pictogram item: assembly

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.

Pictogram

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.

Pictogram parameters

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:

Pictogram inclusion and extension

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".

Figures

A figure defines the object that users will be actually placing on their diagrams. Figures are defined inside a diagram lifeset.
A figure defines:

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.

Figure fields

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:

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.

Line allows

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:

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

Lines are defined using the following syntax:

line lineName lineDetails lineFields linePictograms;

The various parameters are here described:

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.

Line fields

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

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.

Class mappings

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 mappings

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:

Lines mappings

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:

Coding techniques

Lines validation

TBD

Connpoint discrimination

TBD

Programming rules

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.

Metatypes, types and objects

A metatype is a family of types. Macrocoder supports the following metatypes:

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.

Class metatype

The class metatype:

This metatype includes the internal basic types, like Int or String, plus all classes written by the user or generated by Macrocoder itself.

Inteface metatype

The interface metatype:

This metatype is used to define method interfaces that other classes can expose and implement.

Composed metatype

The composed metatype:

Class

A class, as usual in object-oriented languages, is a construct used to define a distinct type.

Types purpose

Besides its metatype, each type has a purpose that can be data or structural.

Types with data purpose:

Types with structural purpose:

Lifecycles

Lifecycles are classes devoted to the management of the entire parsing and generation process. They:

Lifecycles import

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.

Phases

Phases are the logical steps that orderly bring the evolution towards the final generation.

Instancing deadline

The instancing deadline is a characteristic of structural types: it is the maximum allowed phase when that type can be instanced.

Invokeability phase range

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:

An evolutionary method(f) on a type T with instancing deadline=fd has always invokeability phase range chosen with the following rules:

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).

Invocation phase range

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.

Evolution

The evolution is the process of bringing each object through all the phases.

Evolution state

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.

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.

Lifeset cauldron

Every lifeset has a lifeset cauldron that hosts all the objects that have no other parent. A cauldron:

Lifeset chain

Lifesets can be chained: a lifeset "L1" can, during its evolution, create and setup instances inside lifeset "L2".

Prephasing

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.

Hypertypes

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.

Multiple types

An hypertype can contain one or more types. If the contained types are more than one, they must respect the following rules:

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.

Constant

Hypertypes can contain the const flag that restricts access to read-only operations.

Hypertype status

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:

Hypertypes casting

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:

Initiating status

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.

Evolutionary methods

Evolutionary methods are special methods executed when a phase evolution has to be performed. They are further divided in:

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.

Objects copy

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:

Variants

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:

See also chapter Variants.

Arrays

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:

See also chapter Arrays.

Links

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:

See also chapter Links.

Sets

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

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.

Type casting

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.

Type upscan

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.

Methods and attributes forwarding

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:

Attributes access rules

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:

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 object obj is initiating:

If object obj is phased:

If object obj is pre-phased:

If object obj is transitional:

If object obj is pre-transitional:

Macrocoder grammar

This chapter reports ther formal specification of the Macrocoder Language grammar.

Grammar specification syntax

The formal grammar specification is expressed according to the syntax below:

The OPER keyword begins the definition of an operator:

Grammar specification

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