code / tomo

Lines40.1K C23.5K Markdown9.5K YAML4.9K Tomo1.6K
7 others 735
Shell232 make207 Python205 INI48 Text21 SVG16 Lua6
(217 lines)

Tomo Library/Module Design

There are two ways to "import" code that is defined elsewhere: local files from the same project and shared library objects from another project. The first type of import (local files) is necessary for splitting large projects into smaller components for ease of understanding and compilation speed. The second type of import (shared libraries) is to allow you to install third party libraries or frameworks that can be used across many projects.

Local Imports

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!

Shared Library Imports

In Tomo, a shared library is built out of a directory that contains multiple .tm files. Each .tm file in the directory (excluding those that start with an underscore) will be compiled and linked together to produce a single libwhatever.so file (or libwhatever.dylib on Mac) and whatever.h file that can be used by other Tomo projects. You can build a library by running tomo -L /path/to/dir or tomo -L in the current directory.

Installing

If you additionally add the -I flag, Tomo will copy the entire directory (excluding files and directories that begin with . such as .git) into ~/.local/lib/tomo@vTOMO_VERSION/LIBRARY_NAME@LIBRARY_VERSION.

Using Shared Libraries

To use a shared library, write a statement like use foo with an unqualified name (i.e. not an absolute or relative path like /foo or ./foo). When a program uses a shared library, that shared library gets dynamically linked to the executable when compiling, and all of the necessary symbol information is read from the source files during compilation.

Versioning

When you build and install a library, its version is determined from a CHANGES.md file at the top level of the library directory (see: Versions). The library's version number is added to the file path where the library is installed, so if the library mylib has version v1.2, then it will be installed to ~/.local/lib/tomo@TOMO_VERSION/mylib@v1.2/. When using a library, you must explicitly supply either the exact version in the use statement like this: use mylib@v1.2, or provide a modules.ini file that lists version information and other details about modules being used. For each module, you should provide a [modulename] section with a version= field.

# File: foo.tm
use mylib
...

And the accompanying modules.ini:

[mylib]
version=v1.2

The modules.ini file must be in the same directory as the source files that use its aliases, so if you want to share a modules.ini file across multiple subdirectories, use a symbolic link. If you need to include per-file overrides for a directory's modules.ini file, you can use foo.tm:modules.ini.

Module Downloading

If you want, you can also provide the following options for a module:

  • git: a Git URL to clone the repository
  • revision: if a Git URL is provided, use this revision
  • url: a URL to download an archive of the library (.zip, .tar, .tar.gz)
  • path: if the library is provided in a subdirectory of the repository or archive, list the subdirectory here.

For example, this is what it would look like to use the colorful library that is distributed with the Tomo compiler in the examples/colorful subdirectory:

[colorful]
version=v1.0
git=git@github.com:bruce-hill/tomo
path=examples/colorful

If this extra information is provided, Tomo will prompt the user to ask if they want to download and install this module automatically when they run a program and don't have the necessary module installed.

1 # Tomo Library/Module Design
3 There are two ways to "import" code that is defined elsewhere: local files from
4 the same project and shared library objects from another project. The first
5 type of import (local files) is necessary for splitting large projects into
6 smaller components for ease of understanding and compilation speed. The second
7 type of import (shared libraries) is to allow you to install third party
8 libraries or frameworks that can be used across many projects.
10 ## Local Imports
12 To see how local imports work, let's look at a simple file:
14 ```
15 // File: foo.tm
16 my_variable := "hello"
17 ```
19 When this file is compiled to a static object file by `tomo -c foo.tm`, it
20 produces the following C header file and C source file:
22 ```c
23 // File: foo.tm.h
24 #pragma once
25 #include <tomo/tomo.h>
27 extern Text_t my_variable$foo_C3zxCsha;
28 void $initialize$foo_C3zxCsha(void);
29 ```
31 ```c
32 // File: foo.tm.c
33 #include <tomo/tomo.h>
34 #include "foo.tm.h"
36 public Text_t my_variable$foo_C3zxCsha = Text("hello");
37 public void $initialize$foo_C3zxCsha(void) {
38 static bool initialized = false;
39 if (initialized) return;
40 initialized = true;
42 ```
44 Notice that the symbols defined here (`my_variable$foo_C3zxCsha`) use a
45 filename-based suffix with a random bit at the end that includes a dollar sign.
46 C compilers support an extension that allows dollar signs in identifiers, and
47 this allows us to use guaranteed-unique prefixes so symbols from one file don't
48 have naming collisions with symbols in another file.
50 The C file is compiled by invoking the C compiler with something like: `cc
51 <flags...> -c foo.tm.c -o foo.tm.o`
53 Now, what happens if we want to _use_ the compiled object file?
55 ```
56 // File: baz.tm
57 foo := use ./foo.tm
59 func say_stuff()
60 say("I got $(foo.my_variable) from foo")
62 func main()
63 say_stuff()
64 ```
66 If I want to run `baz.tm` with `tomo baz.tm` then this transpiles to:
68 ```c
69 // File: baz.tm.h
70 #pragma once
71 #include <tomo/tomo.h>
72 #include "./foo.tm.h"
74 void say_stuff$baz_VEDjfzDs();
75 void main$baz_VEDjfzDs();
76 void $initialize$baz_VEDjfzDs(void);
77 ```
79 ```c
80 // File: baz.tm.c
81 #include <tomo/tomo.h>
82 #include "baz.tm.h"
84 public void say_stuff$baz_VEDjfzDs() {
85 say(Texts(Text("I got "), my_variable$foo_C3zxCsha, Text(" from foo")), yes);
88 public void main$foo_VEDjfzDs() {
89 say_stuff$foo_VEDjfzDs();
92 public void $initialize$foo_VEDjfzDs(void) {
93 static bool initialized = false;
94 if (initialized) return;
95 initialized = true;
97 $initialize$foo_C3zxCsha();
98 ...
101 int main$baz_VEDjfzDs$parse_and_run(int argc, char *argv[]) {
102 tomo_init();
103 $initialize$baz_VEDjfzDs();
105 Text_t usage = Texts(Text("Usage: "), Text$from_str(argv[0]), Text(" [--help]"));
106 tomo_parse_args(argc, argv, usage, usage);
107 main$baz_VEDjfzDs();
108 return 0;
110 ```
112 The automatically generated function `main$baz_VEDjfzDs$parse_and_run` is in
113 charge of parsing the command line arguments to `main()` (in this case there
114 aren't any) and printing out any help/usage errors, then calling `main()`.
116 Then `baz.tm.o` is compiled to a static object with `cc <flags...> -c baz.tm.c
117 -o baz.tm.o`.
119 Next, we need to create an actual executable file that will invoke
120 `main$baz_VEDjfzDs$parse_and_run()` (with any command line arguments). To do
121 that, we create a small wrapper program:
123 ```c
124 // File: /tmp/program.c
125 #include <tomo/tomo.h>
126 #include "baz.tm.h"
128 int main(int argc, char *argv[])
130 return main$baz_VEDjfzDs$parse_and_run(argc, argv);
132 ```
134 This program is compiled with the already-built object files to produce an
135 executable binary called `foo` like this: `cc <flags...> /tmp/program.c
136 foo.tm.o baz.tm.o -o baz`
138 Finally, the resulting binary can be executed to actually run the program!
141 ## Shared Library Imports
143 In Tomo, a shared library is built out of a *directory* that contains multiple
144 `.tm` files. Each `.tm` file in the directory (excluding those that start with
145 an underscore) will be compiled and linked together to produce a single
146 `libwhatever.so` file (or `libwhatever.dylib` on Mac) and `whatever.h` file
147 that can be used by other Tomo projects. You can build a library by running
148 `tomo -L /path/to/dir` or `tomo -L` in the current directory.
150 ### Installing
152 If you additionally add the `-I` flag, Tomo will copy the entire directory
153 (excluding files and directories that begin with `.` such as `.git`) into
154 `~/.local/lib/tomo@vTOMO_VERSION/LIBRARY_NAME@LIBRARY_VERSION`.
156 ### Using Shared Libraries
158 To use a shared library, write a statement like `use foo` with an unqualified
159 name (i.e. not an absolute or relative path like `/foo` or `./foo`). When a
160 program uses a shared library, that shared library gets dynamically linked to
161 the executable when compiling, and all of the necessary symbol information is
162 read from the source files during compilation.
164 ### Versioning
166 When you build and install a library, its version is determined from a
167 `CHANGES.md` file at the top level of the library directory (see:
168 [Versions](versions.md)). The library's version number is added to the file
169 path where the library is installed, so if the library `mylib` has version
170 `v1.2`, then it will be installed to
171 `~/.local/lib/tomo@TOMO_VERSION/mylib@v1.2/`. When using a library, you must
172 explicitly supply either the exact version in the `use` statement like this:
173 `use mylib@v1.2`, or provide a `modules.ini` file that lists version
174 information and other details about modules being used. For each module, you
175 should provide a `[modulename]` section with a `version=` field.
177 ```tomo
178 # File: foo.tm
179 use mylib
180 ...
181 ```
183 And the accompanying `modules.ini`:
185 ```ini
186 [mylib]
187 version=v1.2
188 ```
190 The `modules.ini` file must be in the same directory as the source files that
191 use its aliases, so if you want to share a `modules.ini` file across multiple
192 subdirectories, use a symbolic link. If you need to include per-file overrides
193 for a directory's `modules.ini` file, you can use `foo.tm:modules.ini`.
195 ### Module Downloading
197 If you want, you can also provide the following options for a module:
199 - `git`: a Git URL to clone the repository
200 - `revision`: if a Git URL is provided, use this revision
201 - `url`: a URL to download an archive of the library (`.zip`, `.tar`, `.tar.gz`)
202 - `path`: if the library is provided in a subdirectory of the repository or
203 archive, list the subdirectory here.
205 For example, this is what it would look like to use the `colorful` library that
206 is distributed with the Tomo compiler in the `examples/colorful` subdirectory:
208 ```ini
209 [colorful]
210 version=v1.0
211 git=git@github.com:bruce-hill/tomo
212 path=examples/colorful
213 ```
215 If this extra information is provided, Tomo will prompt the user to ask if they
216 want to download and install this module automatically when they run a program
217 and don't have the necessary module installed.