tape run
Usage
tape run <file.tape> [-- args...]Examples
tape run hello.tape
tape run server.tape -- --port 8080
tape run app.tape -I libs/ -- arg1 arg2How it works
- Split source into regions
- Parse
#codeand#viewregions (with rt0 preamble if no imports) - If imports present: re-parse without preamble, resolve modules
- Execute
@tapeblocks (compile-time metaprogramming) - Type check
- Lower to TAC IR
- Leave out what nothing reaches, as
tape builddoes (What goes into the output); not in a hot run - Run it in the VM: each function is decoded on its first call, its small callees inlined and its TAC optimized as a native build’s is (
-O0: as lowered)
The VM finds main() and executes it. Extern functions are resolved at runtime through FFI (LoadLibrary/dlopen).
Options
| Flag | Description |
|---|---|
-- | Separator between tape options and program arguments |
-I <path> | Add module search path (then ./stdlib, then stdlib/ next to the tape binary — see tape build) |
--target <t> | Target platform (affects rt0 preamble selection) |
--human | Human-readable error output |
--max-errors <N> | Stop after N errors |
-O0 | Run the TAC as lowered: no inlining or optimization of the program’s functions. The program behaves the same, only slower (a hot run is always like this) |
--hot | A hot run: edit a component’s code while the program runs (see below) |
--no-dce | Keep every function, used or not, as tape build --no-dce |
--dce-report | Print, on stderr, how many functions of each module were kept and left out |
--hot-slack <n> | Bytes of room after each component instance’s vars in a hot run |
--fuel <n> | Stop the program after n VM instructions: trap: instruction limit exceeded at file:line:col (exit 101) and a note naming the flag. For a script that may loop forever. Default: no limit. Instructions are only counted when a limit is given, so a run without --fuel costs nothing extra |
--tape-fuel <n> | The compile-time budget of each @tape block, as for tape build |
--set NAME=VALUE | A build setting, as for tape build; without it each setting is its default (Build settings) |
Hot reload
tape run --hot app.tape watches the program’s own source files (not the
standard library’s). When one is saved, the program is rebuilt and, at the
next hot.poll(), the components and functions whose code changed take
their new code, with their state kept:
import hot from "hot";
component Greeter {
var count: i64 = 0;
fn greet() {
count = count + 1;
io.println("hello {count}"); // edit this line while it runs
}
}
fn main() -> i32 {
var g: Greeter = Greeter {};
while (true) {
hot.poll(); // the safe point: rebuilt code comes in here
g.greet();
time.sleep(time.seconds(1));
}
return 0;
}A window made with ui.run needs nothing more: it polls between events,
and paints itself again after a reload, so what it shows follows the new
code without waiting for input.
What a reload takes, and what needs a restart:
- Taken: a component method’s new body, wherever it is called from (calls, dispatches, handlers bound to events); methods added or removed; vars added, removed or retyped (below).
- Taken: a plain function’s new body, in any of the program’s modules,
when its parameters, result and attributes stay the same: every call
after the reload runs it, direct calls, function pointers and handlers
alike (
hot: reloaded fn shape.draw). New functions are taken too. A call already running when the reload comes finishes in the old code: an edit ofmainitself, or of the loop that callshot.poll(), shows at the next run (hot: fn main is running: the call in progress keeps its old code). The same holds on worker threads: a task already running keeps its code until it returns. - Taken in views: what a view’s bindings compute (
Shown { text: "total {n}"; }): the children stay, with their state, and show the new values. - Taken where only components use it: a module
const(their code reads the new value), a prop’s default (instances made after the reload start with it; the ones alive keep the values they have), a struct’s fields (their code uses the new layout; a component var of that struct is retyped, below). - Restart: a component’s props themselves (added, removed, renamed or
retyped), events, slots or dispatch signatures; a view that changes
shape (a child added, removed or of another component type; for now); a
component added or removed; a global or its initial value; a plain
function’s parameters or result changed, or a function removed (
hot: fn twice changed its signature: restart to apply); a thread’s own functions (what they call is taken); a plainco fn; and a struct, enum or union whose layout changed while a plain function changes too (hot: struct Pt changed: restart to apply). The reloader names what changed.
Vars. Every instance keeps the values of the vars that keep their name
and type. A var the reload adds starts at its declared default. A var it
removes, or gives another type, is dropped (a string’s memory is freed),
and the reloader names it (hot: Note.text was dropped). A hot run gives
every instance some room after its vars for this: as much again as its
vars take, at least 64 bytes, or --hot-slack <n>. New vars that don’t fit
are refused (hot: Box's vars need 24 bytes, its instances have 16: restart to apply).
Coroutines. A component’s suspended coroutine frame survives a reload
when its coroutine still keeps the same values in the same places at its
yields (changing what it prints, say): it resumes in the new code where it
was. A frame suspended at a yield @label(name) that the new code also
has, saving the same locals there by name and type, continues at that
label in the new code, its locals carried over, even when the rest of the
coroutine changed (hot: Guard.route continues at step). Any other frame
restarts: its defers run and it reads started() == false, so the
component can start it again (coroutines).
Give the points a reload should keep a label.
A change that needs a restart is not applied: the old code keeps running,
and the reloader says why on stderr (hot: fn twice changed its signature: restart to apply). A rebuild that doesn’t compile shows its
errors and changes nothing. Each reload says what it took (hot: reloaded Greeter fn twice).
hot.poll() called from inside a component’s own code traps: that code
would return into code that was just replaced. Call it from the main loop.
The TAPE variable
tape run sets the environment variable TAPE to the path of the tape
binary that runs the program (tape test and tape repl do too). A script
uses it to start the same compiler, with the process
module:
var build: process.Command = process.command(os.env("TAPE")!);
build.args(["build", "app.tape", "-o", "app"]);
let s: process.Status = build.run() or { return 1; };Program arguments
Everything after -- is passed to the running program. Read the arguments
with os.args(), os.arg_count() and os.arg(i). As in a native build,
argument 0 is the program itself (the .tape file under tape run, the
executable when built) and the user’s arguments start at 1:
import io from "io";
import os from "os";
pub fn main() -> i32 {
for (i in 1..os.arg_count()) {
io.println(os.arg(i)!);
}
return 0;
}FFI library search
The VM searches for @link libraries in this order.
On Linux and macOS (a name without a dot gets the platform suffix, tapert100 → tapert100.so / tapert100.dylib):
- The source file’s directory
- The current directory
- The directory of the tape binary — where tape’s own runtime libraries are (the release package, or
/usr/lib/tapefor the.deb) - The system search path (
LD_LIBRARY_PATH, the loader cache; on macOS,/usr/libfor names likelibSystem.B.dylib)
On Windows:
- The standard DLL search order (the directory of
tape.exe, system directories,PATH) - The source file’s directory
If a library is found but fails to load (wrong architecture, missing dependency, code-signing refusal), that error is reported.
Exit codes
The VM returns whatever main() returns:
fn main() -> i64 {
return 0;
}| Exit code | Meaning |
|---|---|
| 0+ | Value returned by main() |
| 101 | A runtime trap (trap: <what> at file:line:col on stderr): out-of-bounds index, call through a null function pointer, unwrap of null or error, division by zero, duplicate view key, @trap("...") — the same line and code a native build gives — a stack overflow (recursion past the VM’s call stack), instruction limit exceeded (the --fuel budget spent), or trap: out of memory (a string or other implicit allocation got no memory; no location) |
| -1 (255 on Linux/macOS) | No main function found, or VM error (vm error: ... on stderr, e.g. an unresolved library symbol) |
tape run will accept pre-compiled .tb bytecode bundles, allowing distribution and execution without source or re-compilation. This requires the bytecode encoding format (see ).Last modified: