Native plugins and stdlib
Zolo's native plugins (written in Rust and compiled as cdylib) have dedicated
syntax: use plugin foo. This creates a namespace table under the name foo,
keeping the global scope clean.
The wildcard use plugin foo::* opts into registering everything as a global —
idiomatic for plugins like binary where you want every symbol accessible
directly:
use plugin binary::* exposes BinaryReader and BinaryWriter as globals.
Useful when you want the entire plugin API without a prefix.
// Feature: wildcard `use ...::*` — imports all public names
// Syntax: `use plugin foo::*` (native plugins) or `use foo::*` (modules)
// When to use: the canonical case is importing every symbol from a
// native Zolo plugin (`binary`, `winit`, `wgpu`, etc.) when you want
// the entire API available without prefix. For typical user-land
// `.zolo` modules, prefer a named list `{a, b, c}` to avoid polluting
// the scope.
use std::string
// The `binary` plugin registers BinaryReader, BinaryWriter and helpers
// as classes. `use plugin binary::*` opts in to "register everything
// as globals" — the same shape the plugin's runtime publishes.
use plugin binary::*
fn main() {
// 4 little-endian bytes = u32 = 7
let raw = string::char(0x07, 0x00, 0x00, 0x00)
// BinaryReader came from the prelude — no need to qualify.
let r = BinaryReader.from_bytes(raw)
print(r.read_u32_le())
// expected: 7
}
The form use plugin foo creates a namespace (foo.Window, foo.EventLoop,
...). Adding {self, member} to the list binds the namespace and a direct
member at the same time:
use plugin winit creates winit.Window; use plugin crypto::{self, hash}
creates both crypto.hash(...) and the direct shortcut hash(...).
// Feature: `use plugin <name>` — explicit native-plugin loader
//
// Five supported forms:
//
// use plugin foo → namespaced: load + move every
// symbol the plugin registers
// into a `foo` namespace table.
// Globals are kept clean.
// use plugin foo::* → opt-in glob: every symbol the
// plugin registers becomes a global.
// use plugin foo::Bar → load + `local Bar = foo.Bar`
// use plugin foo::{a, b} → load + per-member locals
// use plugin foo::{self, a} → also bind the plugin namespace itself
//
// Bare `use plugin foo` works the same way regardless of whether the
// plugin's runtime registers `<name>` as a unified namespace or as
// individual classes (Window, EventLoop, …) — the loader bridges both
// into the namespace table.
use std::crypto
use plugin winit // namespace — winit.Window, winit.EventLoop
use plugin wgpu // namespace — wgpu.*
use plugin crypto::{self, hash} // namespace + direct hash() binding
fn main() {
// Bare `use plugin foo` keeps the namespace; nothing leaks to
// global scope. Members live under the plugin's name.
print("winit.Window in scope: {winit.Window != nil}") // true
print("winit.EventLoop in scope: {winit.EventLoop != nil}") // true
print("wgpu in scope: {wgpu != nil}") // true
// `crypto` is bound by the `self` entry; `hash` by the explicit
// member entry. Both forms reach the same underlying function.
let digest_a = hash("sha256", "zolo") // direct binding
let digest_b = crypto.hash("sha256", "zolo") // via namespace
print("hash(\"sha256\", \"zolo\") direct = {digest_a}")
print("crypto.hash(...) = {digest_b}")
print("digests match: {digest_a == digest_b}")
}
For the standard library, you can use the fully qualified path
std::module::function() without any use — ideal for one-off calls where
importing the whole module would add noise:
std::os::clock() works without use std::os. If you call os.clock()
frequently, it is worth doing use std::os and using the short form.
// Feature: fully-qualified `std::` paths without `use`
// Syntax: `std::os::clock()`, `std::math::floor(x)` — no `use` needed
// When to use: a one-off stdlib call where importing the whole module
// would be noise. Reach for `use std::M` when you call `M.foo` a lot.
// The fully-qualified form resolves on its own — note: NO `use std::os`.
let start = std::os::clock()
var count = 0
for _ in 0..100_000 {
count += 1
}
print("count =", count)
print("elapsed =", std::os::clock() - start)
// Contrast — the SHORT form still requires the import:
//
// use std::os // <- required for the line below
// let t = os.clock() // TE105 without the `use`
//
// So both of these reach the same function:
// std::os::clock() // qualified — works with or without `use`
// os.clock() // short — needs `use std::os`
//
// How it works: a compiler pass (std_path_desugar) rewrites every
// `std::M::...` value path into the same member-access the parser emits
// for `M....`, and injects a synthetic `use std::M`. So typeck (TE105),
// effect-checking, the unused-import lint, and lowering all treat the
// qualified form exactly like the imported short form. The formatter
// keeps `std::os::clock()` verbatim — it round-trips unchanged.
//
// expected:
// count = 100000
// elapsed = <small float>
See also