XTCLANG GUIDES
How to structure your Ecstasy language project with modules.
xtclang
modules
Authored by: Abby Sassel, Cameron Purdy
Last edited: @January 11, 2024
About this guide
Learn how to confidently start and structure your own Ecstasy language project with modules.
Topics:
- Introduction to Ecstasy modules and how to create and use them.
- Outline the layout of a typical Ecstasy module.
- Explore the core
ecstasy
module included by default in every type system.
Target Audience
If you would like to learn how to confidently start and structure your own Ecstasy language project with modules, this Ecstasy Modules guide is for you!
It is helpful to have an understanding of Ecstasy’s syntax, structure and execution model prior to using this guide. For a primer, please refer to earlier guides here.
What is an Ecstasy module?
A tool for code organization
An Ecstasy module is a tool for project and code organization.
At its simplest, a module is an .x
file containing the class definitions for a unit of functionality. By convention, Ecstasy modules are organized within a directory structure as follows:
|- <module-name>/ <- module directory
|- src/ <- source code directory
|- main/ <- conventional nested directory
|- x/ <- conventional nested directory
|- <module-name>.x <- module file
All Ecstasy source code is organized in .x
files, which are text files using the ASCII/UTF-8 encoding. (ASCII files are legal UTF-8 files.)
A unit of deployment
An Ecstasy module is also the unit of compilation, the unit of versioning, and the unit of deployment.
The compiled form of an Ecstasy module is an .xtc
file. An .xtc
file is analogous to:
- a .NET assembly
- a Java
.JAR
file - a C or C++
.dll
/.so
executable file
Each .xtc
file contains a intermediate representation (IR) of the source code, which is portable to any supported machine/OS combination. It also contains all of the resources specified by the source code, including templates, HTML, images, data and configuration files, and even entire directory trees.
For ease of deployment and portability, most applications should be deployable as a single .xtc
file.
Declaring a module
A module is declared with the module
keyword and the name of the module:
module MyFirstModule {
// module definition
}
Ecstasy module names can be qualified using the same structure as the Domain Naming System (DNS). The qualified name indicates the organization responsible for the module:
module protocols.eclipse.org {
// module definition
}
The use of the domain name as part of the qualified module name is quite purposeful; here are some intended uses for leveraging the domain name:
- The basis for Internet module repositories, and downloading modules by their names; and
- The basis for private key module signing with public key validation.
It is helpful to think of a module in the way that you would think of a storage drive; the module corresponds to the root of the storage, packages are like directories within that storage, and classes are like files in those directories. The analogy isn’t perfect, but it does provide a basis for visualizing the structure of a module.
Defining a module
Modules consist of packages and classes. Packages and classes can be declared similarly to modules, but with the package
or class
keywords instead of module
.
For example, the following module M
contains the package P1
that contains the package P2
that contains the class C
:
module M {
package P1 {
package P2 {
class C {
}
}
}
}
The majority of syntactic variations, such as generic type parameters or constructor parameters, that can be applied to modules, packages and classes in the same way.
Modules and packages can be annotated, extend an existing class, implement interfaces, and incorporate mixins. For example, the Database
mixin from the oodb.xtclang.org
module is used to indicate that a module is a database schema. However, to use that mixin, the using module must mount the other module into its namespace as package, so that the name Database
can be resolved:
@Database module AddressBookDB {
// mount the library "oodb.xtclang.org" into this module as
// the "oodb" package
package oodb import oodb.xtclang.org;
// import the mixin "Database" from the library "oodb.xtclang.org",
// so that we can refer to it with its simple name "Database"
// (as we do in the module declaration above!)
import oodb.Database;
// ...
}
Using modules
In the above example, the package statement does create a package oodb
within the AddressBookDB
module, but that package is just a reference (or a redirect) to a different module, in the same way that a file name is used as a mount point in UNIX or Linux. In other words, the AddressBookDB
module contains an empty oodb
package that acts as a reference to the oodb.xtclang.org
module. The only two places in the Ecstasy language that allow the use of qualified module names are in the module
declaration statement itself, and in the package
declaration statement when it used to specify a module to import.
The Ecstasy language provides a core "ecstasy" module, which is automatically present in every type system. The way that works is that every module implicitly contains a package named ecstasy
, as if the following line of code were added to each module as part of its compilation:
package ecstasy import ecstasy.xtclang.org;
During compilation, names are resolved by asking the code's compiler context if it knows what the name means, and if it doesn't, then asking the next outer containing context, and so on, all the way out to the context of the module itself. So everywhere within a module, the name "ecstasy" will refer to the ecstasy.xtclang.org
module -- unless of course someone re-uses the name ecstasy
for something else, which would obscure (shadow) the package name.
This approach to name resolution means that you can place constants, functions, type definitions, and even imports onto a package or module, and those names will be available everywhere within that context. It's a powerful way to achieve many of the useful benefits of globally defined names, without the downsides of actually having any "globals".
Growing modules
Optionally, source files within a module can organized hierarchically in a directory structure:
|- lib_ecstasy/
|- src/
| |- main/
| |- x/
| |- ecstasy.x <- module file
| |- ecstasy/ <- module directory
| |- collections/ <- package directory
| | |- Array.x <- class
| | |- List.x
| | |- Set.x
| | |- ...
| |- Boolean.x <- class
| |- Const.x
| |- Enum.x
| |- ...
|- ...
Here's a small portion of the layout of the core "ecstasy" module from the Ecstasy project on GitHub:
|- lib_ecstasy/ <- project
|- src/
| |- main/
| |- resources/ <- static resources
| | |- ...
| |- x/ <- source code directory
| |- ecstasy.x <- module file
| |- ecstasy/
| |- annotations/ <- a package within the module
| | |- Abstract.x <- a class within the package
| | |- Override.x
| | |- Transient.x
| |- collections/
| | |- Array.x
| | |- Collection.x
| | |- List.x
| | |- Set.x
| |- Boolean.x
| |- Const.x
| |- Enum.x
| |- Exception.x
| |- Object.x
| |- Service.x
| |- ...
|- LICENSE
|- README.md
|- build.gradle.kts
In the example above, the "ecstasy" module contains packages such as "annotations" and "collections".
Organization and naming conventions
Like directories in a file system, modules are hierarchical. A module can contain packages and classes, and packages can in turn contain more packages and classes, and classes can contain more classes. Modules and packages are just two special forms of classes!
An entire module and everything that it contains can be organized within a single .x
file -- even if it contains thousands of packages and classes! At the other extreme, the module can be maximally spread out across files, with one file for each module, package, and class, organized in a directory hierarchy that corresponds to the module's own hierarchy. Or the source code organization can fall anywhere between those two extremes, with some packages and classes being split out into their own .x
files and directories, and others defined "inline" within the parent module, package, or class definition.
This allows you to use a single file for a tiny module, such as a "Hello World!" example. It also allows very large modules to be organized in a manner that is conducive to team development and version control systems. And it allows for small classes to be defined within the class' enclosing namespace, without requiring each class to be split out into a tiny file of its own.
Regardless of how that choice is made, and regardless of the number of directories and source files that make up a module, from the language's point of view, a module is treated as single unit, and it is compiled as if it were a single file. In other words, it would be perfectly acceptable for a compiler pre-processor to take all of those files, and merge them together into one giant file (following the syntax rules of the language), and the compilation result would be identical -- other than the file-name and line-number information used for stack traces and debugging.
There are two ways to name the file that contains the module declaration code:
- the default naming rule for the module file is to use the module's name (either unqualified like "
ecstasy
", or qualified like "ecstasy.xtclang.org
"), plus the.x
source file extension. To split out packages or classes, each would be placed in their own file in a sub-directory whose name matches the module file name -- but without the.x
extension. The following example shows the moduleMyApp
, a nested classProfile
and packageutil
, and a classRunner
inside theutil
package: - if the entire module is defined a single
.x
file, then the name of the file does not have to match the name of the module. In this case, it would not be possible to break out contents of the module into their own separate files in a sub-directory without first matching the module file name to the name of the module itself, as illustrated above in theMyApp
example.
|- myapp/
|- src/
| |- main/
| |- x/
| |- MyApp.x <- module "MyApp"
| |- MyApp/
| |- Profile.x <- class "MyApp.Profile"
| |- util.x <- package "MyApp.util"
| |- util/
| |- Runner.x <- class "MyApp.util.Runner"
In the MyApp
example above, note how similarly the nested package structure is to the module structure itself. There is one important difference, though: A package can be represented simply by the presence of a directory. In other words, a sub-directory in a source tree implies the presence of a package of that same name. If the util.x
package source file in the example above were deleted, then the compiler would still infer the existence of the package, as if there were a util.x
file with the following line of code:
package util {}
In other words, the presence -- and just the presence! -- of that sub-directory implies that there is a piece of code defining the package. That's about as simple as it gets.
Packages are name-spaces within a module, just like directories are name-spaces within a file system. Packages can in turn contain more packages and classes. Most of the time, those are organized hierarchically within the package's directory, but -- as is the case with modules -- the contained packages and classes can be textually contained within the package's source code itself. This flexibility allows small class and type definitions to be included directly into the source code of the package, and allows the larger classes within the package to each have their own .x
file.
Interesting details
- Modules and packages are
const
classes. Objects instantiated fromconst
classes are immutable, and Ecstasy automatically provides an implementation of the Comparable, Orderable, Hashable, and Stringable interfaces, as specified by the Const interface. Examples of otherconst
classes include String and Int classes, which should give you some idea of what aconst
is. (We'll cover this topic in detail in a subsequent chapter.) - A module or a package is a singleton class. This means that the runtime will automatically instantiate and hold on to one instance of the class, and no other instance of the class can be created. Furthermore, it means that the singleton instance is always easily available -- by its name!
- In the case of a module, because all the code in the module is nested inside of the module, the name of the module is visible from any of that code. Alternatively, a reference to the current module is always available as
this:module
. Take the example ofMyApp
, introduced previously in this chapter; anywhere inside of that application, one can obtain a reference to the module in either of the following ways: - Ecstasy automatically provides an implementation of the Module interface for each
module
, and an implementation of the Package interface for eachpackage
. TheModule
interface extends thePackage
interface, so any question that a package can answer, a module can also answer (but the converse is not true).
MyApp m1 = MyApp;
MyApp m2 = this:module;
EXPLORE & LEARN
Ready to learn more?
Check out our latest guides, blogs and tutorials!