1. Modules
When creating a large project, it is very important to organize your code. Ymir offers a system of modules, which is used to manage different parts of the code that have different purposes. Each source file in Ymir is a module.
1.1. File hierarchy
Lets have a look at the following file hierarchy :
.
├── main.yr
└── extern_modules
├── bar.yr
└── foo.yr
1 directory, 3 files
In this file hierarchy there are three files, which contain modules, the
first module in the file main.yr
will be named main
. The second
one in the extern_modules/bar.yr
file will be named extern_modules::bar
,
and the third one in the extern_modules/foo.yr
file will be named
extern_modules::foo
.
To be properly importable, the module must be defined from the
relative path of the compilation, i.e. if the file is located in
$(pwd)/relative/path/to/file
, its module name must be
relative::path::to::file
.
The name of a module is defined by the first line of the source code,
by keyword mod
. If this line is not given by the user, the path of
the module will only be the file name, so you will not always be able
to import the module, depending on its relative path. You can consider
this line mandatory for the moment.
For example, in the file foo.yr
, the first line must look like :
mod extern_modules::foo
And, it will therefore be importable everywhere, for example in the
main
module, when writing the import declaration :
import extern_modules::foo
The syntax of the import statement is the following :
import_statement := 'import' path (',' path)* (';')?
path := Identifier ('::' Identifier)* ('::' '_')?
1.2. Sub modules
Sub modules are local modules, declared inside a global modules, are
inside another sub module. Unlike global module, the access to the
symbols defined inside them is not implicit. For that reason they have
to be explicitely mentionned when trying to access to their
symbols. This mention is done with the double colon binary operator
::
, where the first operand is the name of the module, and the
second the name of the symbol to access.
mod main
import std::io;
mod InnerModule {
pub def foo () {
println ("Foo");
}
}
def main () {
InnerModule::foo (); // access of the function declared in InnerModule
}
The access operator ::
, can also be used to access to symbols
declared inside global modules. This will be discussed after talking
about privacy of symbols.
1.3. Privacy
All symbols defined in a module are private by default. The privacy of a given symbol s refer to the possibility for foreign modules, and symbols to access to this given symbol s. When a symbol s is declared private in a module s, then only the other symbols of the module m have access to it. Module privacy can be seen as a tree, where a global module is a root, and module symbols are the branches and leaves of the tree. In such a tree, symbols have access to their parent, siblings, and the siblings of their parents.
In the following figure an example of a module tree is presented, where a global module named A, has three symbols, 2 sub modules A::X and A::Y, and a function A::foo. In this tree, we assume that every symbols are declared private. For that reason, the function A::foo has access to A, A::X, A::Y, but not to A::X::bar, nor A::Y::baz. The symbol A::X::bar, has access to every symbols (A, A::X, A::Y, A::foo), except A::Y::baz.
Global modules are always tree roots, for that reason they don't have
parents. For example, the module extern_modules::foo
, does not
have access to the symbols declared inside the module
extern_modules
, (if they are privates).
The keyword pub
flag a symbol as public, and accessible by
foreign modules. This keyword can be used as a block, or for only one
symbol. Its syntax grammar is presented in the following code block.
pub := 'pub' '{' symbol* '}'
| 'pub' symbol
1.3.1. Example
1) Module extern_modules/foo.yr
mod extern_modules::foo;
/**
* foo is public, it can be accessed from foreign modules
*/
pub def foo () {}
/**
* The bar function is private by default
* Thus only usable in this module
*/
def bar () {}
2) Module main.yr
/**
* This importation will give access to all the symbols in the module
* 'extern_modules::foo' that have been declared 'public'
*/
import extern_modules::foo
def main () {
foo (); // foo is public we can call it
bar (); // however, bar is private thus not accessible
}
Errors:
Error : undefined symbol bar
--> main.yr:(7,5)
7 ┃ bar (); // however, bar is private thus not accessible
╋ ^^^
┃ Note : bar --> extern_modules/foo.yr:(8,5) : extern_modules::foo::bar is private within this context
┗━━━━━━
ymir1: fatal error:
compilation terminated.
1.4. Symbol conflict resolution
When two external global modules declare two symbols with the same
name, it may be impossible to know which symbol the user is refereing
to. In this case, the double colon operator ::
can be used with
the name of the module declaring the symbol to resolve the
ambiguity. To give an example of symbol conflict, let's say that we
have two module extern_modules::foo
and extern_modules::bar
declaring a function with the same signature foo
.
1) Module extern_modules/bar.yr
mod extern_modules::bar
import std::io
pub def foo () {
println ("Bar");
}
2) Module extern_modules/foo.yr
mod extern_modules::foo
import std::io
pub def foo () {
println ("Foo");
}
In the main
module, both modules extern_modules::bar
and
extern_modules::foo
, are imported. The main function presented below
refers to the symbol foo
. In that case, there is no way to tell
which function will be used, extern_modules::foo::foo
or
extern_modules::bar::foo
. The compiler returns an error. One can
note that this errors occurs only because the signature of the two
function foo
are the same (taking no parameters), and they are both
public. If there was a difference in their prototypes, for example if
the function in the module extern_modules::bar
would take a value of
type i32
as parameter, the conflict would be resolved by itself, as
the call expression will be different.
3) Module main.yr
import extern_modules::bar, extern_modules::foo
def main () {
foo ();
}
Errors:
Error : {extern_modules::bar::foo ()-> void, extern_modules::foo::foo ()-> void, mod extern_modules::foo} x 3 called with {} work with both
--> main.yr:(4,6)
4 ┃ foo ();
╋ ^
┃ Note : candidate foo --> extern_modules/bar.yr:(4,9) : extern_modules::bar::foo ()-> void
┃ Note : candidate foo --> extern_modules/foo.yr:(4,9) : extern_modules::foo::foo ()-> void
┗━━━━━━
ymir1: fatal error:
compilation terminated.
.page-inner {
width: 95%;
}
In the above error, we can see that three modules are presented. The
two functions foo
— in extern_modules::bar
, and
extern_modules::foo
— and the extern_modules::foo
module
itself. Obviously, it is not possible to use the call operator
()
on a module, that is why it is not presented as a possible
canditate in the notes of the error.
The conflict problem can be resolved by changing the calling
expression, and using the double colon operator ::
. In the
following example, the full name of the module is used. This is not
always necessary, as bar::foo
is sufficient to refer to
extern_modules::bar::foo
, and foo::foo
for function
foo
in extern_modules::foo
.
import extern_modules::bar, extern_modules::foo
def main () {
extern_modules::bar::foo ();
extern_modules::foo::foo ();
foo::foo ();
bar::foo ();
}
Results:
Bar
Foo
Foo
Bar
1.5. Public importation
As for all declaration, importation are private. It means that the
importation is not recursive. For example, if the module
extern_modules::foo
imports the module extern_modules::bar
, and the
module main
import the module extern_modules::foo
, all the public
symbols declared in extern_modules::bar
will not be accessible in the
module main
.
You can of course, make a pub
importation, to make the symbols of
the module extern_modules::bar
visible for the module main
.
1) Module extern_modules/bar.yr
mod extern_modules::bar
import std::io
pub def bar () {
println ("Bar");
}
2) Module extern_modules/foo.yr
mod extern_modules::foo
pub import extern_modules::bar
3) Module main.yr
mod main
import extern_modules::foo;
def main () {
bar ();
}
In the example above, the function bar
defined in the module
extern_modules::bar
, is imported (because the function is public
is public) by the module extern_modules::foo
. This importation
is public, thus when the module main
imports the module
extern_modules::foo
, it also imports the module
extern_modules::bar
, and has access to the function bar
.
1.5.1. Best practice
Public importation must be used with caution, to avoid polluting other
modules. A good practice, is to define some modules only to make
public importations. These modules should be named _
. For
example, with our previous file hierarchy, a file
extern_modules/_.yr
would be added, and no public imports made
in the modules extern_modules::foo
, nor in the module
extern_modules::bar
.
mod extern_modules::_;
pub import extern_modules::foo;
pub import extern_modules::bar;
These modules are not automatically generated by Ymir — even if it seems trivial —, to allow importing only a subset of the modules contained in a sub directory. These importation modules are optional and left to the choice of the user.
1.6. Include directory
You can use the -I
option, to add a path to the include
directory. This path will be used as if it was the current $(pwd)
. In
other words, if you add the I -path/to/modules
option, and you have a
file in path/to/modules/relative/to/my/file
, the name of the module
must be relative::to::my::file
.
gyc -I ~/libs/ main.yr
This is how the standard library is included in the build, and how you
can access modules in std::
that are not located in $(pwd)/std/
.
1.7. Compilation of modules
All modules must be compiled, the import
declaration is just a
directive of for symbols access, but does not compile the imported
symbols. For example, in the following example, there are two modules,
one declaring a function foo
, and the other importing it and
calling it.
1) Module main.yr
mod main
import extern_modules::foo;
def main () {
foo ();
}
2) Module extern_modules/foo.yr
mod extern_modules::foo
pub def foo () {}
By compiling only the main
function, the compiler returns a link
error. This error means that the symbol foo
declared in the module
extern_modules::foo
was not found during the symbol linkage.
$ gyc main.yr
/tmp/ccCOeXDq.o: In function `_Y4mainFZv':
main.yr:(.text+0x3e): undefined reference to `_Y14extern_modules3foo3fooFZv'
collect2: error: ld returned 1 exit status
To avoid this error, and create a valid executable, where all symbols
can be found, the module extern_modules::foo
has to be compiled
as well. GYC is able to manage object files (containing pre compiled
symbols), and compiled libraries. The way GYC manage these kind of
objects is similar to all compiler of the GCC suite, and is not
presented in this documentation (cf. GCC options for linking).
$ gyc main.yr extern_modules/foo.yr