Profile Import Rules

Profiles

Every module has a profile declared with @profile:

tape
@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:

ImporterCan import
t2t2, t1, t0
t1t1, t0, and t2 source (compiled into the program)
t0t0 only

A t0 module that imports anything else is a compile error, reported at the import line:

tape
// kernel.tape
@profile(t0);
import io from "io";   // error: t0 module 'kernel' cannot import t1 module 'io':
                       //        t0 modules import only t0 modules

A 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 test native runner, so tape test works 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:

tape
// inside io.tape
import rt from "tapert100";

One implementation exists per target platform:

TargetRuntime fileMechanism
Windowsrt/win64/tapert100.tape@link("kernel32.dll") — HeapAlloc, WriteFile, etc.
Linuxrt/linux/tapert100.tape@link("libc.so.6") — malloc, write, etc.
Uusirt/uusi/tapert100.tapeRaw syscalls (brk, write); its own heap over brk (rt/uusi/uusi_heap.tape)
macOSrt/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: