Profile Import Rules
Profiles
Every module has a profile declared with @profile:
@profile(t0); // freestanding: no implicit heap, no GC
@profile(t1); // application: explicit types, scope-based cleanup
@profile(t2); // scripting: optional types (default profile)If omitted, the default is t2.
Import restrictions
The compiler checks every import against the profiles of both modules:
| Importer | Can import |
|---|---|
| t2 | t2, t1, t0 |
| t1 | t1, t0, and t2 source (compiled into the program) |
| t0 | t0 only |
A t0 module that imports anything else is a compile error, reported at the
import line:
// kernel.tape
@profile(t0);
import io from "io"; // error: t0 module 'kernel' cannot import t1 module 'io':
// t0 modules import only t0 modulesA module without @profile is t2, so a t0 module can’t import it either.
Give a shared helper module @profile(t0); if t0 code needs it.
The rule holds through every path: a t0 library imported by a t0 program is
checked against its own imports, and the error points at the line in the
library that breaks it. Code emitted by a @tape generator reaches only the
modules the generator imports, which were checked when the generator’s
module was compiled.
The rationale: t0 code must have no hidden dependencies on heap allocation or GC, so it cannot call into t1/t2 modules. A kernel can launch higher-profile programs but cannot call their functions directly.
The standard library follows the same rules: its core modules (such as
arena) are t0 and import only t0 modules, so t0 code
can use them.
Imports the compiler adds
Some imports are added by the compiler, not written in the source, and the profile rules don’t apply to them:
- the runtime bridge, linked into t1 and t2 programs whose generated code allocates (see below; t0 code that would allocate is an error of its own)
- the bridge import of the
tape testnative runner, sotape testworks on t0 modules
Runtime module (tapert100)
Every t1/t2 program that uses stdlib transitively links against tapert100 — the platform bridge that provides (compiled into native programs by default; see --runtime):
- Memory allocator (
rt_alloc,rt_free) - I/O primitives (
rt_write,rt_read,rt_puts) - Process control (
rt_exit) - Initialization (
io_init)
Stdlib modules (io, mem, str, os) explicitly import tapert100:
// inside io.tape
import rt from "tapert100";One implementation exists per target platform:
| Target | Runtime file | Mechanism |
|---|---|---|
| Windows | rt/win64/tapert100.tape | @link("kernel32.dll") — HeapAlloc, WriteFile, etc. |
| Linux | rt/linux/tapert100.tape | @link("libc.so.6") — malloc, write, etc. |
| Uusi | rt/uusi/tapert100.tape | Raw syscalls (brk, write); its own heap over brk (rt/uusi/uusi_heap.tape) |
| macOS | rt/macos/tapert100.tape | @link("libSystem.B.dylib") |
The compiler selects the correct tapert100 based on the build target. The DLL output uses the export name tapert100.dll.
Stdlib modules import tapert100 explicitly. User code never imports it directly — it is accessed indirectly through stdlib.
Last modified: