Imports & Modules

Import syntax

tape
import io from "io";
import math from "math";

io.println("pi = ");
io.print_f64(math.PI);

All imports use the form import local_name from "module_path";. The local name is used as a namespace prefix when accessing the module’s exports.

Accessing exports

Imported items are accessed with dot notation:

tape
import str from "str";
import ui from "ui";

let len = str.len(name);
let kind = ui.PointerKind.Down;

Functions, structs, enums, tagged unions, and components from the imported module are available as module.Name.

Selective imports

Import specific symbols directly into your namespace without a module prefix:

tape
import { println, print_i64 } from "io";
import { Vec3, make_vec } from "./veclib";

println("distance:");
print_i64(make_vec(1, 2, 3).x);

Selective imports bring individual pub symbols into scope as if they were declared locally. No prefix needed.

You can use both forms together:

tape
import io from "io";
import { println } from "io";

println("short form");      // selective — no prefix
io.read_line();             // qualified — still works

Rules:

  • Only pub symbols can be selectively imported
  • Name collisions (with local declarations or other selective imports) are a compile error
  • Importing a var binds to the module’s actual storage — mutations are visible to both modules, same as module.varname qualified access

Not supported — glob imports:

tape
import * from "io";  // COMPILE ERROR — not valid syntax

Glob imports will never be supported in tape. The reasons are fundamental to the language’s philosophy:

  1. Readability over convenience. When you encounter println() in code, you should be able to look at the imports and immediately know where it comes from. Glob imports force the reader to check every imported module to find the source of a symbol.

  2. No silent breakage. If module A adds a new pub fn process() and your file already has a local fn process(), a glob import from A would silently collide. With explicit lists, the module author can add exports freely without breaking downstream code.

  3. Predictable over clever. Tape code should mean the same thing regardless of what’s happening in other files. A glob import couples your file’s name resolution to the full public surface of another module — any addition there can change what names mean here.

Always list symbols explicitly. The small upfront cost of typing names pays back every time someone reads the code.

Module identity

Each .tape file is a module. The import path determines which file to load:

  • Bare names ("io", "mem", "str") — resolved from the include search paths (stdlib, project lib dirs)
  • Relative paths ("./utils", "../shared/types") — resolved relative to the importing file

The .tape extension is appended automatically if not present.

Visibility

By default, declarations are module-private. Use pub to export:

tape
pub fn api_function() -> i64 { return 42; }
fn internal_helper() -> i64 { return api_function() * 2; }

pub struct Config {
    name: *u8;
    value: i64;
}

pub enum Direction { North; South; East; West; }

pub component Button {
    prop label: *u8;
    event on_click();
}

What can be pub:

Declarationpub supported
FunctionsYes
StructsYes
EnumsYes
Tagged unionsYes
ComponentsYes
ConstantsYes
Variables (var/let)Yes
Type aliasesNo (always module-private)
Struct fieldsNo (all fields visible if struct is pub)

pub {} grouping block

Group multiple declarations under a single pub to export them all:

tape
pub {
    fn increment() { count = count + 1; }
    fn decrement() { count = count - 1; }
    fn reset() { count = 0; }
    var count: i64 = 0;
}

// Still private:
fn internal_helper() { }

This is pure syntactic sugar — equivalent to writing pub on each declaration individually. Useful when extracting a batch of related functions into a new module.

Rules:

  • Only valid at module top level in #code region
  • Any declaration that supports pub can appear inside the block
  • Cannot contain non-declaration statements (no expressions, no control flow)
  • Nesting pub {} inside pub {} is redundant but not an error

How cross-module linking works

When module A imports module B, the compiler:

  1. Parses B and collects all pub declarations
  2. Injects extern function stubs into A with qualified names (module.fn_name)
  3. Injects struct/enum/component stubs so A can resolve types
  4. At link time, function calls to module.fn_name resolve to B’s implementation

Standard library modules

tape
import io from "io";           // printing, file I/O
import str from "str";         // string utilities
import mem from "mem";         // allocation, copy, compare
import math from "math";       // math functions
import os from "os";           // OS interface
import sys from "sys";         // system/platform info
import gfx from "gfx";        // graphics primitives
import ui from "ui";           // UI framework, layout, events
import widgets from "widgets"; // standard widget components
import colors from "color";    // color utilities

Multiple imports

Each import is a separate statement:

tape
import io from "io";
import str from "str";
import mem from "mem";

Relative imports

Import from files relative to the current module:

tape
import utils from "./utils";
import types from "../shared/types";

Region-specific imports

PlannedRegion-specific imports are designed but not yet implemented in the resolver.

Import from a specific region of another module:

tape
import colors from "theme#style";
import layout from "app#view";

This would allow importing only the #style or #view region of a multi-region file.

Last modified: