Skip to content

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.

07-wildcard-prelude.zolo
Playground
// 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(...).

10-use-plugin.zolo
Playground
// 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.

11-qualified-std-no-use.zolo
Playground
// 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

enespt-br