Imports

Koru’s import system enables modular code organization through directory-based namespacing. Unlike traditional module systems, Koru uses directory structure to create automatic namespaces.

The Two Import Systems

Koru programs use two kinds of imports working together:

Koru Imports (import)

Import Koru events and modules:

import std/io          // Standard library
import lib/raylib      // Your own modules

What you get: Access to the public events those modules declare.

Why no ~ here: this is a .k file — pure Koru, no host language in it. The ~ is a switch that tells a host-language parser where Koru begins, so in a file with no host language there is nothing to switch away from and the character does not appear at all. Every example on this page that starts with a bare import is a .k; every one that starts with ~import is a .kz, which is the same language with Zig interleaved.

Module naming: The import path is the module qualifier — kept verbatim, with / separators. There is no last-segment shortening.

Event syntax: alias/path:event()

  • After import std/io: use std/io:print.ln(text: "Hello")
  • After import lib/raylib: use lib/raylib:init()

Zig Imports (@import)

Import Zig code and the standard library:

const std = @import("std");           // Zig stdlib
const raylib = @import("raylib");     // C libraries

What you get: Access to Zig functions, types, and constants

Syntax: Regular Zig: std.debug.print()

Working Together

Both import systems work seamlessly in the same host-embedded .kz file — and this is where the ~ comes from. The file below has Zig in it, so the parser starts in Zig and every Koru line has to say where it begins. Reach for .kz when you genuinely need a host body; a program that only imports, flows and invokes is a .k and carries no ~ at all.

// main.kz — host-embedded, so Koru lines wear ~
const std = @import("std");  // Zig import for logging
~import std/io            // Koru import for I/O events

~tor greet { name: string }

~proc greet|zig {
    std.debug.print("Greeting {s}\n", .{name});  // Zig
}

~greet(name: "World") |> std/io:print.ln(text: "Hello!")  // Koru (explicit path!)

Basic Import Syntax

Single Module Import

import std/io

std/io:print.ln(text: "Import works!")

Path aliases make imports consistent. The first path segment is an alias — std and app are built in, and a program declares its own with std/compiler:paths { alias: path }:

  • std/* - Standard library (e.g., std/io, std/profiler)
  • app/* - Your own files, relative to the entry file
  • Custom aliases (e.g., vendor/*, internal/*) via std/compiler:paths

Module naming rules:

  • The import path is the module qualifier verbatim: std/iostd/io:event, lib/rayliblib/raylib:event
  • Qualifiers always use /, never .. is member access (e.g. print.ln)
  • This ensures consistent naming: std/io:print.ln(...) is the same everywhere

See: 404_import_lib_root

Single File Imports

Import a sibling .kz file through the app alias, by its name (without the .kz extension):

import std/io
import app/helper

// Access events from helper.kz
app/helper:greet(name: "World"): g |> std/io:print.ln("{{ g:s }}")

Naming: The module name is the filename without .kz

  • app/helper imports helper.kz → module app/helper
  • app/utils imports utils.kz → module app/utils

Directory Imports

Import entire directories of Koru modules:

import app/test_lib

// Access events from test_lib/graphics.kz
app/test_lib/graphics:init()

// Access events from test_lib/audio.kz
app/test_lib/audio:play(sound: "explosion.wav")

Namespace Hierarchy: Directory imports create a two-level namespace under the alias:

Directory structure:

test_lib/
  graphics.kz  → app/test_lib/graphics:*
  audio.kz     → app/test_lib/audio:*
  physics.kz   → app/test_lib/physics:*

Usage (always explicit!):

import app/test_lib

app/test_lib/graphics:render(frame: 42)
app/test_lib/audio:init()
app/test_lib/physics:update(dt: 0.016)

Format: package/module:event()

  • Package: Directory name (test_lib)
  • Module: Filename without .kz (graphics, audio, physics)
  • Event: Public event name (render, init, update)

Public vs Private Events

Only public events can be imported. Use ~pub to make events available:

// In test_lib/graphics.kz (host-embedded, so declarations wear ~)
~pub tor init {}

~pub tor render { frame: i32 }

// This event is private (no pub)
~tor internal-helper { x: i32 }
// In main.k
import app/test_lib

app/test_lib/graphics:init()       // ✅ OK - public
app/test_lib/graphics:render(...)  // ✅ OK - public
app/test_lib/graphics:internal-helper(...)  // ❌ ERROR - not public

Why explicit pub? Prevents accidental API exposure and makes public APIs self-documenting.


Parent + Submodule Pattern

Status: Implemented — see tests 110_012 (auto-import parent) and 110_016 (import depth limit)

Koru supports hierarchical module organization where parent modules provide package-level utilities and submodules provide specialized functionality.

The Design

When importing a submodule path, you automatically get the parent module (if it exists):

// Import selective submodule
import std/io/file

// You get:
// - io.kz (parent module - package utilities)
// - io/file.kz (submodule - file operations)

// You can use BOTH:
std/io:print.ln(text: "From parent")       // Parent utility
std/io/file:open(path: "test.txt")        // Submodule

// You CANNOT use (not imported):
std/io/console:write(...)  // Error: not imported

Full Package Import

Import the entire package to get parent + all submodules:

import std/io

// Gets: io.kz + io/file.kz + io/console.kz + io/network.kz + ...

std/io:print.ln(text: "Parent")
std/io/file:open(...)
std/io/console:write(...)
std/io/network:connect(...)

Optional Parent

If only a directory exists (no parent .kz file), imports still work:

net/              → No net.kz file (pure organizational directory)
  tcp.kz
  udp.kz
import std/net/tcp
// Only gets tcp.kz (no parent exists, that's fine!)

std/net/tcp:connect(...)

Why This Design?

Hierarchical thinking:

  • Parent provides package-level abstractions
  • Submodules provide specialization
  • Import path mirrors namespace usage

Selective imports:

  • Import only what you need: import std/io/file
  • Reduces compilation time and dependencies

No disambiguation needed:

  • import std/io → full package
  • import std/io/file → parent + file submodule
  • Clear, unambiguous semantics

See design doc: IMPORT_DESIGN.md


Ambient Behavior: Imports Register Taps

Importing a module automatically registers any taps defined in it. This is what makes features like profiling work with a single import line.

How It Works

When you import a module:

  1. Events become available (the obvious part)
  2. Taps defined in the module are registered (the ambient part)
  3. Those taps start intercepting events immediately

This is how [profile]import std/profiler instruments your entire program!

Example: Automatic Logging

// In test_lib/logger.kz (host-embedded)
const std = @import("std");
~import std/taps

~pub tor log { event_name: string }

~proc log|zig {
    std.debug.print("[TAP] Intercepted event: {s}\n", .{event_name});
}

// Tap defined in the module: compute is a bare-return event,
// so the tap binds the produced value at the call site
~tap(main:compute -> *): _ |> log(event_name: "compute")
// In main.kz (host-embedded)
~import std/io
~import app/test_lib/logger  // This registers the tap automatically!

~tor compute { x: i32 } -> i32

~proc compute|zig {
    return x * 2;
}

// When you call compute, logger's tap fires automatically!
~compute(x: 42): r |> std/io:print.ln("Result: {{ r:d }}")

Output:

[TAP] Intercepted event: compute
Result: 84

The tap fires without you writing it in main.kz. Importing made it ambient!

Real-World: Profiler

The profiler module defines universal taps:

// In profiler.kz (verbatim from the standard library)
~[opaque]tap(* -> *)
| Profile p |> write-event(p.source, p.timestamp_ns)

When you import the profiler:

[profile]import std/profiler

Every event in your program is now being profiled! The import:

  1. Adds profiler events to your namespace
  2. Registers the universal tap tap(* -> *)
  3. The tap intercepts ALL events, collecting timing data

One line = full program instrumentation. This is the power of ambient taps!


Conditional Imports

Import modules only when specific compiler flags are set:

[profile]import std/profiler
[test]import std/testing
[debug]import std/debug

How It Works

With flag:

koruc --profile my_app.kz  # Profiler imported

Without flag:

koruc my_app.kz  # Profiler NOT imported (no overhead!)

Real-World Example

// main.kz — host-embedded, so Koru lines wear ~
const std = @import("std");

// Only import profiler in profile builds
~[profile]import std/profiler

// Only import testing framework in test builds
~[test]import std/testing

~tor hello {}

~proc hello|zig {
    std.debug.print("Hello from conditional import test!\n", .{});
}

~hello()

Development build: No profiling overhead

Profile build: Full instrumentation with universal taps

See: 310_027_conditional_imports

Flag Control

# Without flag - import skipped
koruc my_app.kz

# With flag - import processed
koruc --profile my_app.kz
koruc --test --debug my_app.kz  # Multiple flags

Zero-cost abstractions: Code paths not needed don’t get compiled!

See:


Mixing Koru and Zig Imports

The real power comes from using both import systems together:

// Zig imports (for stdlib, C libraries, types)
const std = @import("std");
const raylib = @import("raylib");

// Koru imports (for events and flows)
~import std/io
~import lib/game_engine

~tor render-frame {}

~proc render-frame|zig {
    // Use Zig code
    const screen_width = raylib.GetScreenWidth();
    std.debug.print("Rendering at {d}px width\n", .{screen_width});
}

// Combine Zig + Koru in flows
~lib/game_engine/graphics:init()
| ready |> render-frame() |> std/io:print.ln(text: "Frame rendered!")  // Explicit path!

Why Both?

Koru imports give you:

  • Event-driven flows
  • Pattern matching on outcomes
  • Compiler-checked branch coverage
  • Universal taps for profiling/logging

Zig imports give you:

  • Access to Zig’s stdlib (std.debug, std.mem, etc.)
  • C library bindings (raylib, SDL, etc.)
  • Type definitions for event signatures
  • Low-level control when needed

Together: High-level event flow + low-level implementation power!

Import Availability Across Phases

Important: Zig imports (like const std = @import("std")) are available in both compilation phases (comptime and runtime).

This means your [comptime] and [runtime] procs can both use:

const std = @import("std");

~[comptime|runtime] proc profiler|zig {
    std.debug.print("Profile: {s}\n", .{source});  // Works in both phases!
}

Why: Imports have no side effects - they just make symbols available. The phase filtering system special-cases imports to ensure they’re always accessible.

See the comptime architecture blog post for details on how phase filtering works.


Namespace Collision Prevention

Multiple modules can have events with the same name without colliding:

engine/
  graphics.kz  → ~pub tor init {}
  audio.kz     → ~pub tor init {}
  physics.kz   → ~pub tor init {}
import app/engine

// All three "init" events coexist peacefully!
app/engine/graphics:init()
app/engine/audio:init()
app/engine/physics:init()

Why this works: The module name provides automatic namespacing.


Configuring Paths with koru.zon

The koru.zon file configures import path resolution:

.{
    .name = "my-project",
    .version = "0.1.0",

    .paths = .{
        .std = "./koru_std",      // std/* resolves here
        .lib = "./lib",           // lib/* resolves here
        .vendor = "./vendor",     // vendor/* (custom)
        .internal = "./internal", // internal/* (custom)
    },
}

Using Custom Paths

import std/io           // Resolves to ./koru_std/io/
import lib/utils        // Resolves to ./lib/utils/
import vendor/raylib    // Resolves to ./vendor/raylib/
import internal/core    // Resolves to ./internal/core/

Project Structure Example

my-project/
├── koru.zon              # Path configuration
├── src/
│   └── main.kz           # Your code
├── lib/                  # Your modules
│   ├── utils/
│   │   └── helpers.kz
│   └── common/
│       └── types.kz
├── vendor/               # Third-party modules
│   └── raylib/
│       └── bindings.kz
└── koru_std/             # Standard library
    ├── io.kz
    ├── io/
    │   ├── file.kz
    │   └── console.kz
    ├── profiler.kz
    └── debug/

Benefits:

  • Consistent imports: std/io works regardless of file location
  • Easy refactoring: Move files without changing imports
  • Clear intent: the alias prefix signals where code resolves from
  • Project organization: Separate user code from libraries

See: koru.zon documentation


Security: No Parent Directory Access

Koru forbids .. in import paths for security:

import ../secret_data  // ❌ PARSE ERROR
import ../../etc       // ❌ PARSE ERROR

Why: Prevents accidental or malicious access to files outside the project.

Instead, use:

  • Alias paths: import internal/data
  • Configure paths in koru.zon to make internal modules accessible

See: 402_import_dotdot_forbidden


Advanced Patterns

Multi-Module Flows

Complex flows can coordinate multiple modules:

import lib/net

lib/net/tcp:listen(port: 8080)
| listening l |> lib/net/tcp:accept(server_id: l.server_id)
    | connection c |> lib/net/tcp:read(conn_id: c.conn_id)
        | data d |> lib/net/http:parse(bytes: d.bytes)
            | request r |> lib/net/http:build(status: 200, body: "OK")
                | response resp |> lib/net/tcp:write(conn_id: c.conn_id, data: resp.bytes)
                    | sent |> _
                    | failed |> _
        | closed |> _
    | failed |> _

Pattern: Related functionality grouped in a package, accessed through different modules.

See: 110_010_dir_import_flow

Cross-Module Type References

Reference Zig types from imported modules:

// In test_lib/user.kz (host-embedded)
pub const User = struct {
    name: []const u8,
    age: u32,
};

~pub tor create { name: string, age: u32 } -> User
// In main.kz (host-embedded)
~import app/test_lib

~pub tor register-user {
    user_data: app/test_lib/user:User,  // module/submodule:Type
} -> bool

Syntax: module/submodule:TypeName

The : operator separates module reference from type name, consistent with event syntax.


Design Rationale

Why Explicit Paths Always?

Traditional approach: Aliasing for convenience

// JavaScript
import { println } from "std/io";
println("Hello");  // Which println? Where from?

Koru approach: Explicit everywhere

import std/io
std/io:print.ln(text: "Hello")  // Crystal clear!

Benefits:

  • AI-first: std/io:print.ln appears identically everywhere in the codebase
  • Easy to grep/search: Find all usages instantly
  • No aliasing confusion: Never ask “did they alias it?”
  • The path IS documentation: You know where every event comes from

Why Directory-Based Namespaces?

Koru approach: Filesystem IS the namespace

lib/
  raylib/
    graphics.kz  → raylib/graphics:*
    audio.kz     → raylib/audio:*

Benefits:

  • Natural organization (filesystem structure = code structure)
  • Automatic collision prevention
  • Scales to large codebases
  • No manual namespace declarations
  • Refactoring is moving files

Why Two-Level Hierarchy?

One level: Everything in flat namespace (collision-prone)

Three+ levels: Deeply nested names (verbose, hard to read)

Two levels: Sweet spot!

  • Package groups related functionality (raylib)
  • Module provides fine-grained organization (graphics, audio)
  • Event is the actual functionality (render, play)

Result: raylib/graphics:render() - clear, concise, collision-free

Why Explicit pub Markers?

Without pub: Everything is public (accidental API exposure)

With pub: Explicit public boundary

pub tor api-function {}      // Public API
tor internal-helper {}       // Private implementation

Benefits:

  • Prevents accidental API exposure
  • Clear API boundaries
  • Enables refactoring without breaking imports
  • Self-documenting code

Why Mix Koru and Zig Imports?

Zig imports: Access the ecosystem (stdlib, C libraries, existing code)

Koru imports: Event-driven flow control with pattern matching

Together: Best of both worlds!

  • Use Zig for implementation (fast, proven, ecosystem access)
  • Use Koru for orchestration (clear flow, compiler-checked coverage)
  • Seamless interop (Koru procs contain Zig code, return to Koru flow)

Quick Reference

Import Syntax

// Pure Koru (.k)
import std/io                 // Standard library
import lib/raylib             // Directory import
import std/io/file            // Selective submodule (parent included)
[profile]import std/profiler  // Conditional import
// Host-embedded (.kz) — Zig lives in the same file, so Koru lines wear ~
const std = @import("std");
const raylib = @import("raylib");

~import std/io
~[profile]import std/profiler

Event Access (Always Explicit!)

// After: import std/io
std/io:print.ln(text: "Hello")         // ✅ Explicit path
io:print.ln(text: "Hello")             // ❌ WRONG - no aliasing!

// After: import app/test_lib
app/test_lib/graphics:render(frame: 1)    // ✅ Explicit path
graphics:render(frame: 1)                 // ❌ WRONG - no aliasing!

Ambient Taps

// Importing registers taps defined in the module
[profile]import std/profiler  // Registers universal taps automatically!

// Now ALL your events are being profiled (ambient behavior)
compute(x: 42)  // Profiler tap intercepts this
| result r |> format(value: r.value)  // And this
    | formatted |> _  // And this!

Key insight: One import line = full program instrumentation

Type References

import app/test_lib

tor process {
    user: app/test_lib/user:User,    // Cross-module type
}

Path Resolution

  • std/* - Standard library (configured in koru.zon)
  • lib/* - Project libraries (configured in koru.zon)
  • Any custom alias - configured in koru.zon
  • helper - Sibling .kz file, relative to current file
  • No .. allowed (security)


All examples verified by regression tests.