tape run

Usage

bash
tape run <file.tape> [-- args...]

Examples

bash
tape run hello.tape
tape run server.tape -- --port 8080
tape run app.tape -I libs/ -- arg1 arg2

How it works

  1. Split source into regions
  2. Parse #code and #view regions (with rt0 preamble if no imports)
  3. If imports present: re-parse without preamble, resolve modules
  4. Execute @tape blocks (compile-time metaprogramming)
  5. Type check
  6. Lower to TAC IR
  7. Leave out what nothing reaches, as tape build does (What goes into the output); not in a hot run
  8. 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

FlagDescription
--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)
--humanHuman-readable error output
--max-errors <N>Stop after N errors
-O0Run 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)
--hotA hot run: edit a component’s code while the program runs (see below)
--no-dceKeep every function, used or not, as tape build --no-dce
--dce-reportPrint, 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=VALUEA 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:

tape
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 of main itself, or of the loop that calls hot.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 plain co 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:

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

tape
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;
}

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):

  1. The source file’s directory
  2. The current directory
  3. The directory of the tape binary — where tape’s own runtime libraries are (the release package, or /usr/lib/tape for the .deb)
  4. The system search path (LD_LIBRARY_PATH, the loader cache; on macOS, /usr/lib for names like libSystem.B.dylib)

On Windows:

  1. The standard DLL search order (the directory of tape.exe, system directories, PATH)
  2. 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:

tape
fn main() -> i64 {
    return 0;
}
Exit codeMeaning
0+Value returned by main()
101A 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)
Planned — 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: