Local Imports
How to split a Tomo project across multiple files and import code between them using local import statements.
In Tomo, you can split a project into multiple files and access the code in
other files by doing use ./file.tm. There are a few major
benefits to doing this:
- Files can be split apart at conceptual boundaries to make the codebase easier to read and navigate.
- Each file has its own namespace and can define local variables not visible outside the file. This makes it easy to avoid namespace collisions.
- Tomo compiles each file in parallel with lazy recompliation. This means that compile times can be much faster if a project is intelligently split into multiple files.
To see how local imports work, let’s look at a simple file:
// File: foo.tm
my_variable := "hello"When this file is compiled to a static object file by
tomo -c foo.tm, it produces the following C header file and C
source file:
// File: foo.tm.h
#pragma once
#include <tomo/tomo.h>
extern Text_t my_variable$foo_C3zxCsha;
void $initialize$foo_C3zxCsha(void);// File: foo.tm.c
#include <tomo/tomo.h>
#include "foo.tm.h"
public Text_t my_variable$foo_C3zxCsha = Text("hello");
public void $initialize$foo_C3zxCsha(void) {
static bool initialized = false;
if (initialized) return;
initialized = true;
}Notice that the symbols defined here
(my_variable$foo_C3zxCsha) use a filename-based suffix with a
random bit at the end that includes a dollar sign. C compilers support an
extension that allows dollar signs in identifiers, and this allows us to use
guaranteed-unique prefixes so symbols from one file don’t have naming
collisions with symbols in another file.
The C file is compiled by invoking the C compiler with something like:
cc <flags...> -c foo.tm.c -o foo.tm.o
Now, what happens if we want to use the compiled object file?
// File: baz.tm
foo := use ./foo.tm
func say_stuff()
say("I got $(foo.my_variable) from foo")
func main()
say_stuff()If I want to run baz.tm with tomo baz.tm then
this transpiles to:
// File: baz.tm.h
#pragma once
#include <tomo/tomo.h>
#include "./foo.tm.h"
void say_stuff$baz_VEDjfzDs();
void main$baz_VEDjfzDs();
void $initialize$baz_VEDjfzDs(void);// File: baz.tm.c
#include <tomo/tomo.h>
#include "baz.tm.h"
public void say_stuff$baz_VEDjfzDs() {
say(Texts(Text("I got "), my_variable$foo_C3zxCsha, Text(" from foo")), yes);
}
public void main$foo_VEDjfzDs() {
say_stuff$foo_VEDjfzDs();
}
public void $initialize$foo_VEDjfzDs(void) {
static bool initialized = false;
if (initialized) return;
initialized = true;
$initialize$foo_C3zxCsha();
...
}
int main$baz_VEDjfzDs$parse_and_run(int argc, char *argv[]) {
tomo_init();
$initialize$baz_VEDjfzDs();
Text_t usage = Texts(Text("Usage: "), Text$from_str(argv[0]), Text(" [--help]"));
tomo_parse_args(argc, argv, usage, usage);
main$baz_VEDjfzDs();
return 0;
}The automatically generated function
main$baz_VEDjfzDs$parse_and_run is in charge of parsing the
command line arguments to main() (in this case there aren’t any)
and printing out any help/usage errors, then calling main().
Then baz.tm.o is compiled to a static object with
cc <flags...> -c baz.tm.c -o baz.tm.o.
Next, we need to create an actual executable file that will invoke
main$baz_VEDjfzDs$parse_and_run() (with any command line
arguments). To do that, we create a small wrapper program:
// File: /tmp/program.c
#include <tomo/tomo.h>
#include "baz.tm.h"
int main(int argc, char *argv[])
{
return main$baz_VEDjfzDs$parse_and_run(argc, argv);
}This program is compiled with the already-built object files to produce an
executable binary called foo like this:
cc <flags...> /tmp/program.c foo.tm.o baz.tm.o -o baz
Finally, the resulting binary can be executed to actually run the program!