code / tomo

Lines41.3K C23.7K Markdown9.7K YAML5.0K Tomo2.3K
7 others 763
Python231 Shell230 make212 INI47 Text21 SVG16 Lua6
(143 lines)

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:

  1. Files can be split apart at conceptual boundaries to make the codebase easier to read and navigate.
  2. Each file has its own namespace and can define local variables not visible outside the file. This makes it easy to avoid namespace collisions.
  3. 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!

1 # Local Imports
3 In Tomo, you can split a project into multiple files and access the code in
4 other files by doing `use ./file.tm`. There are a few major benefits to doing
5 this:
7 1. Files can be split apart at conceptual boundaries to make the codebase
8 easier to read and navigate.
9 2. Each file has its own namespace and can define local variables not visible
10 outside the file. This makes it easy to avoid namespace collisions.
11 3. Tomo compiles each file in parallel with lazy recompliation. This means that
12 compile times can be much faster if a project is intelligently split into
13 multiple files.
15 To see how local imports work, let's look at a simple file:
17 ```
18 // File: foo.tm
19 my_variable := "hello"
20 ```
22 When this file is compiled to a static object file by `tomo -c foo.tm`, it
23 produces the following C header file and C source file:
25 ```c
26 // File: foo.tm.h
27 #pragma once
28 #include <tomo/tomo.h>
30 extern Text_t my_variable$foo_C3zxCsha;
31 void $initialize$foo_C3zxCsha(void);
32 ```
34 ```c
35 // File: foo.tm.c
36 #include <tomo/tomo.h>
37 #include "foo.tm.h"
39 public Text_t my_variable$foo_C3zxCsha = Text("hello");
40 public void $initialize$foo_C3zxCsha(void) {
41 static bool initialized = false;
42 if (initialized) return;
43 initialized = true;
45 ```
47 Notice that the symbols defined here (`my_variable$foo_C3zxCsha`) use a
48 filename-based suffix with a random bit at the end that includes a dollar sign.
49 C compilers support an extension that allows dollar signs in identifiers, and
50 this allows us to use guaranteed-unique prefixes so symbols from one file don't
51 have naming collisions with symbols in another file.
53 The C file is compiled by invoking the C compiler with something like: `cc
54 <flags...> -c foo.tm.c -o foo.tm.o`
56 Now, what happens if we want to _use_ the compiled object file?
58 ```
59 // File: baz.tm
60 foo := use ./foo.tm
62 func say_stuff()
63 say("I got $(foo.my_variable) from foo")
65 func main()
66 say_stuff()
67 ```
69 If I want to run `baz.tm` with `tomo baz.tm` then this transpiles to:
71 ```c
72 // File: baz.tm.h
73 #pragma once
74 #include <tomo/tomo.h>
75 #include "./foo.tm.h"
77 void say_stuff$baz_VEDjfzDs();
78 void main$baz_VEDjfzDs();
79 void $initialize$baz_VEDjfzDs(void);
80 ```
82 ```c
83 // File: baz.tm.c
84 #include <tomo/tomo.h>
85 #include "baz.tm.h"
87 public void say_stuff$baz_VEDjfzDs() {
88 say(Texts(Text("I got "), my_variable$foo_C3zxCsha, Text(" from foo")), yes);
91 public void main$foo_VEDjfzDs() {
92 say_stuff$foo_VEDjfzDs();
95 public void $initialize$foo_VEDjfzDs(void) {
96 static bool initialized = false;
97 if (initialized) return;
98 initialized = true;
100 $initialize$foo_C3zxCsha();
101 ...
104 int main$baz_VEDjfzDs$parse_and_run(int argc, char *argv[]) {
105 tomo_init();
106 $initialize$baz_VEDjfzDs();
108 Text_t usage = Texts(Text("Usage: "), Text$from_str(argv[0]), Text(" [--help]"));
109 tomo_parse_args(argc, argv, usage, usage);
110 main$baz_VEDjfzDs();
111 return 0;
113 ```
115 The automatically generated function `main$baz_VEDjfzDs$parse_and_run` is in
116 charge of parsing the command line arguments to `main()` (in this case there
117 aren't any) and printing out any help/usage errors, then calling `main()`.
119 Then `baz.tm.o` is compiled to a static object with `cc <flags...> -c baz.tm.c
120 -o baz.tm.o`.
122 Next, we need to create an actual executable file that will invoke
123 `main$baz_VEDjfzDs$parse_and_run()` (with any command line arguments). To do
124 that, we create a small wrapper program:
126 ```c
127 // File: /tmp/program.c
128 #include <tomo/tomo.h>
129 #include "baz.tm.h"
131 int main(int argc, char *argv[])
133 return main$baz_VEDjfzDs$parse_and_run(argc, argv);
135 ```
137 This program is compiled with the already-built object files to produce an
138 executable binary called `foo` like this: `cc <flags...> /tmp/program.c
139 foo.tm.o baz.tm.o -o baz`
141 Finally, the resulting binary can be executed to actually run the program!