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 public events from .kz files
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 file:
const std = @import("std"); // Zig import for logging
~import std/io // Koru import for I/O events
~proc greet {
std.debug.print("Greeting {s}\n", .{name}); // Zig
return .{ .done = .{} };
}
~greet(name: "World")
| done |> 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 defined in koru.json under "paths":
std/*- Standard library (e.g.,std/io,std/profiler)lib/*- Your project libraries- Custom aliases (e.g.,
vendor/*,internal/*)
Module naming rules:
- The import path is the module qualifier verbatim:
std/ioβstd/io:event,lib/raylibβlib/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 by its name (without the .kz extension):
~import helper
// Access events from helper.kz
~helper:greet(name: "World")
| greeted g |> _ Naming: The module name is the filename without .kz
"helper"importshelper.kzβ modulehelper"utils"importsutils.kzβ moduleutils
Directory Imports
Import entire directories of Koru modules:
~import test_lib
// Access events from test_lib/graphics.kz
~test_lib/graphics:init()
| done |> _
// Access events from test_lib/audio.kz
~test_lib/audio:play(sound: "explosion.wav")
| done |> _ Namespace Hierarchy: Directory imports create a two-level namespace:
Directory structure:
test_lib/
graphics.kz β test_lib.graphics:*
audio.kz β test_lib.audio:*
physics.kz β test_lib.physics:* Usage (always explicit!):
~import test_lib
~test_lib/graphics:render(frame: 42)
~test_lib/audio:init()
~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
~pub event init {}
| done {}
~pub event render { frame: i32 }
| done {}
// This event is private (no ~pub)
~event internal_helper { x: i32 }
| done {} // In main.kz
~import test_lib
~test_lib/graphics:init() // β
OK - public
~test_lib/graphics:render(...) // β
OK - public
~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: Specified in tests 165-167, awaiting implementation
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:
- Events become available (the obvious part)
- Taps defined in the module are registered (the ambient part)
- 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
~pub event log { event_name: []const u8 }
| done {}
// Tap defined in the module
~compute -> result
| result r |> log(event_name: "compute")
| done |> result { result: r } // In main.kz
~import test_lib/logger // This registers the tap automatically!
~event compute { x: i32 }
| result { value: i32 }
~proc compute {
return .{ .result = .{ .value = x * 2 } };
}
// When you call compute, logger's tap fires automatically!
~compute(x: 42)
| result r |> _ // Logger intercepts this transition Output:
[TAP] Intercepted event: compute 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
~* -> *
| Profile p |> write_event(source: p.source, timestamp_ns: p.timestamp_ns)
| done |> _ When you import the profiler:
~[profile]import std/profiler Every event in your program is now being profiled! The import:
- Adds profiler events to your namespace
- Registers the universal tap
~* -> * - 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
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
~event hello {}
| done {}
~proc hello {
std.debug.print("Hello from conditional import test!\n", .{});
return .{ .done = .{} };
}
~hello()
| done |> _ Development build: No profiling overhead
Profile build: Full instrumentation with universal taps
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:
- 627_conditional_import_flag_off (flag disabled)
- 628_conditional_import_flag_on (flag enabled)
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
~event render_frame {}
| rendered {}
~proc render_frame {
// Use Zig code
const screen_width = raylib.GetScreenWidth();
std.debug.print("Rendering at {d}px width\n", .{screen_width});
// Return to Koru flow
return .{ .rendered = .{} };
}
// Combine Zig + Koru in flows
~game_engine/graphics:init()
| ready |> render_frame()
| rendered |> std/io:print.ln(text: "Frame rendered!") // Explicit path!
| done |> _ 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 {
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 event init {}
audio.kz β ~pub event init {}
physics.kz β ~pub event init {} ~import engine
// All three "init" events coexist peacefully!
~engine/graphics:init()
~engine/audio:init()
~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/ioworks 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
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.zonto make internal modules accessible
See: 402_import_dotdot_forbidden
Advanced Patterns
Multi-Module Flows
Complex flows can coordinate multiple modules:
~import lib/net
~net/tcp/server:listen(port: 8080)
| listening l |> net/tcp/server:accept(server_id: l.server_id)
| connection c |> net/tcp/connection:read(conn_id: c.conn_id)
| data d |> net/http/request:parse(bytes: d.bytes)
| request r |> net/http/response:build(status: 200, body: "OK")
| response resp |> net/tcp/connection: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: 160_dir_import_flow
Cross-Module Type References
Reference Zig types from imported modules:
// In test_lib/user.kz
pub const User = struct {
name: []const u8,
age: u32,
};
~pub event create { name: []const u8, age: u32 }
| created { user: User } // In main.kz
~import test_lib
~pub event register_user {
user_data: test_lib.user:User, // module.submodule:Type
}
| registered { success: 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.lnappears 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 event api_function {} // Public API
~event 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
// Koru imports
~import std/io // Standard library
~import lib/raylib // Directory import
~import std/io/file // Selective submodule (parent included)
~[profile]import std/profiler // Conditional import
// Zig imports (in same file!)
const std = @import("std");
const raylib = @import("raylib"); 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 test_lib
~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 test_lib
~event process {
user: test_lib.user:User, // Cross-module type
} Path Resolution
std/*- Standard library (configured inkoru.zon)lib/*- Project libraries (configured inkoru.zon)- Any custom alias - configured in
koru.zon helper- Sibling.kzfile, relative to current file- No
..allowed (security)
Related Documentation
- koru.zon configuration - Configure import paths
- Event Taps - Observe imported events with taps
- Comptime Architecture - How imports work across phases
- Full Import Spec - Technical specification
All examples verified by regression tests.