Local Imports
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!