Modules
Reuse and combine existing code in Xreate

Xreate offers modules as a way to organize and reuse source code. For simplicity's sake, it is implemented as one file — one module, with one to one correspondence.

Modules often require prior compilation of other modules for correct work. This leads to a problem of resolution where the required module is located, determine exact module's filename, especially as modern software products tend to incorporate a complicated and volatile file structure depending on configuration and platform to build. The common practice is to rely on build configuration tools to provide exact path to each module.

For this reason Xreate interferes as little as possible with the resolution. The language does not allow for a module to specify a path directly, be it a relative or an absolute path in respect to other required modules. Also the compiler does not search for modules in any predefined list of directories, and does not assume anything about project's file structure. It expects the resolution information to have been already fully predefined before the start of the compilation process.

However, the compiler features some optional built-in functionality to facilitate resolution. It is the very type of a problem the transcend level is excellently suited for. It is modeled after a supply and demand approach and lets modules to declare what they provide and what they require, expressed by annotations. Compiler then tries to satisfy the requirements and find a match. Alternatively, external tools can always be used.

Module Headers

SYNTAX: module [:: annotations-list ] (Full form) { module-statement... }
module :: annotations-list . (Simplified form)
module-statement ::= | require ( annotation ). (1) | discover ( path ). (2) | controller (path). (3)
  • annotations-list List of annotations delimited by semicolon
  • annotation Any valid transcend expression
  • path Absolute or relative path to controller

Xreate recognizes a number of module management statements. Those statements should be located in a specific section module {...} of a source code which is called module header. Module can have several headers. All headers gathered from a single file are combined into one before actual processing.

NOTE: Modules' processing happens before compilation. This means that any data produced in course of compilation is inaccessible at this stage

Requesting Modules

Statement require(..) expresses which modules are required for correct compilation.

tests/modules.cpp: Modules.Doc_Requesting_Modules_1
module {
  require(logger).
}

In this example a module in question requires some other module that provides a feature called logger. There is no way to specify the direct location of the external module. Instead, the "main" module expresses a requirement in an abstract form as a propositional expression which is later used by the resolution to find the exact match.

tests/modules.cpp: Modules.Doc_Requesting_Modules_2
module{require(stringslib).}
processString = function(a:: string):: string
{
  someStrFunc(a)
}

module{require(mathlib).}
processNumber = function(a:: num):: num
{
  someMathFunc(a)
}

The above example demonstrates usage of several headers in one file. It is particularly useful in situations where a developer finds it convenient to place requirements beside the actual code that uses them. This way one can easily spot the requirements that are no more needed. After all, code locality improves readability.

Module Annotations

A module can declare an additional information for various uses. This is expressed by annotations located in the header. They are called 'module annotations'. For instance, module annotations can be used by module resolution to find modules that satisfy requirements of other modules.

tests/modules.cpp: Modules.Doc_ModuleAnnotations_1
module:: status(obsolete).

The example shows a module that declares its status. It can be used by resolution to select the most appropriate module out of a number of candidates. One way to view annotations used by resolution is to treat them as something that module provides.

NOTE: There are no predefined module annotations, and developers can place arbitrary information there.

Modules Resolution

Modules resolution is a process to find exact modules' locations that match requests. Compiler does not perform a search of modules in any predefined directories, and it does not assume anything about project's file structure. Compiler, in order to allow a developer to determine it independently, refers to two transcend tables:

SYNTAX: modules_resolution(request, module-resolved). (1) modules_resolution(request, module-resolved, module-context). (2)
  • request annotation used in statement request(...)
  • module-resolved Path or identifier of a module that matches request
  • module-context Path or identifier of a module that requires other module

These tables contain resolved modules for all possible requests. Form (1) contains requests that should always be resolved to the same module regardless of where module is requested. Form (2) contains such requests for which resolution depends on requester module-context. In other words, difference between forms (1) and (2) is that form (2) takes into account not only what(parameter request) is requested but also where from(parameter module-context) it is requested.

tests/modules.cpp: Modules.Doc_ModulesResolution_1
modules_resolution(numlib, "/path/to/numlib").
modules_resolution(strings, "/path/to/ansi-lib", "moduleA").
modules_resolution(strings, "/path/to/utf8-lib", "moduleB").

In the above example, compiler would always resolve a path to numerical lib(numlib) as "/path/to/numlib" (line 1). However, strings library would be resolved as "/path/to/ansi-lib" if requested in moduleA (line 2) and as "/path/to/utf8-lib" if requested in moduleB (line 3).

When compiler encounters a module request, it looks up a modules_resolution table (either the first or the second one) to find the path to the requested module. Tables can be populated by any means, be it transcend reasoning or any external tools.

NOTE: There is no defined order or priority or fall back behavior while looking into tables. If the same request occurs in both tables they are considered to be ill-formed

Advanced Modules' Resolution

Xreate provides an additional layer – an optional helper, to simplify modules management. It introduces two more built-in statements that can be used in the module's header: Discover Statement and Controller Statement.

  • Discover Statement is presented in the form discover (path). It allows to specify a directory within which compiler would recursively search for all Xreate files and extract module header annotations. It gathers information about all found source files.
  • Controller Statement is presented in the form controller (path) and specifies a path to the modules' resolution controller. Controller is a file that contains transcend rules in order to process data gathered by discovery and populate resolution tables as a result of its work.

The below example shows 3 modules:

tests/modules.cpp: Modules.Doc_AdvModRes_1
//First Module
module::
    name(testA);
    provide(superService);
    status(needToTestEvenMore).

//Second Module
module::
    name(testB);
    provide(superService);
    status(needToTest).

//Third Module
module {
    require(superService).
    discover("/modules/path/").
    controller("/path/to/controller").
}

Two of the modules offer the same feature provide(superSerivce). The third module requires it and specifies a directory in which to look for needed files. The controller's task is to populate the resolution table if the necessary module is found, and also to choose the appropriate candidate, if more than one module offer this service.

One way to decide what to choose in this example is to look at the status of both modules. Let a score be assigned to each possible status, e.g. 0 for status(needToTestEvenMore) and 1 for status(needToTest). The controller would then proceed with best scoring module – Second Module in this case.

See Also

Transcend: Modules API