Keyword Events: When Libraries Want to Speak Like Language
Keyword Events: When Libraries Want to Speak Like Language
Every language has its reserved words. if, while, return — the vocabulary baked into the grammar. But what about libraries? Can they earn a place in that vocabulary without becoming first-class language features?
In Koru, we just shipped an answer: keyword events.
// In a library: mark an event as a keyword
~[keyword]pub event greet { name: []const u8 }
// In user code: invoke without qualification
~greet(name: "World") No lib:greet. No import prefix. Just ~greet.
The Problem: Module Prefixes Are Noisy
Koru’s module system is explicit. Every event lives in a module, and normally you invoke it with its full path:
~lib:greet(name: "Alice")
~other_lib:log(message: "Greeted Alice") This is honest. You see exactly where each event comes from. But for foundational operations — the building blocks you use constantly — those prefixes become visual noise.
Compare to most languages: common operations are just there. No std::greet. No System.greet.
We wanted that ergonomics without losing Koru’s explicit module semantics.
The Solution: [keyword] Annotation
Any public event can opt into keyword status:
// In lib.kz
~[keyword]pub event greet { name: []const u8 }
~proc greet {
const std = @import("std");
std.debug.print("Hello, {s}!
", .{name});
} The [keyword] annotation says: “When imported, this event can be invoked without module qualification.”
Users who import this module can now write:
~import "$lib"
~greet(name: "World") The compiler resolves ~greet to lib:greet automatically.
The Tricky Part: Collisions
What happens when two imported modules both define ~[keyword]pub event process?
Some languages would:
- First-write-wins: First import shadows later ones
- Error at import: Can’t import two modules with same keyword
- Silent shadowing: Last import wins (dangerous!)
Koru takes a different approach: error at usage, not at import.
~import "$lib_a" // defines ~[keyword]pub event process
~import "$lib_b" // also defines ~[keyword]pub event process
// This compiles fine! Both imports succeed.
~process() // ERROR: Ambiguous keyword 'process'
// Defined in: lib_a, lib_b
// Use explicit qualification: ~lib_a:process() or ~lib_b:process() Why Error at Usage?
Import-time errors are frustrating. You might import two libraries that both happen to define process as a keyword, even if you never intend to use the unqualified form. Blocking the import punishes you for a collision you might not care about.
Usage-time errors are actionable. When you write ~process(), you’ve made a choice. The compiler can say: “That’s ambiguous. Here are your options.”
The Escape Hatch: Explicit Qualification
Even when a keyword is defined, you can always bypass it with explicit qualification:
~lib_a:process() // Always works
~lib_b:process() // Always works
~process() // Only works if unambiguous This means keywords are purely ergonomic sugar. They never remove capability.
Why Public-Only?
Keywords must be public (~[keyword]pub event). Private events can’t be keywords:
~[keyword] event internal_helper {} // ERROR: [keyword] requires 'pub' Why? Keywords are about API surface. They’re the vocabulary a library offers to the world. Private events are implementation details — they shouldn’t be part of any module’s public vocabulary.
Also, private events aren’t accessible outside their module anyway. Making them keywords would be meaningless.
How It Works: The Compiler Pipeline
Keywords are resolved in a dedicated pass after module canonicalization:
Parser → Canonicalization → Keyword Resolution → Analysis → Emission
↓
KeywordRegistry Canonicalization: All event paths get module qualifiers.
~greetbecomesinput:greet(qualified to the main module).Keyword Registry: Built by scanning all imported modules for
[keyword]annotated public events. Maps keyword names to their canonical paths.Resolution: Paths qualified to the main module (like
input:greet) are checked against the registry. Ifgreetis a registered keyword, the path is rewritten to the canonical form (e.g.,lib:greet).Collision Detection: If multiple modules register the same keyword, the registry tracks all sources. Resolution fails with actionable error message.
This happens at compile time. No runtime overhead.
Design Philosophy: Earned Vocabulary
Most languages have a fixed vocabulary. Keywords are defined by the language spec, and that’s that. You can’t add unless even if your entire codebase would benefit.
Some languages go the other direction — macros, syntax extensions, arbitrary DSLs. Power, but at the cost of readability and tooling.
Koru’s keywords are a middle path:
- Library authors can propose vocabulary
- Users opt in via imports
- Collisions are explicit and resolvable
- Tooling still understands everything (it’s just event invocation)
Keywords feel like language features, but they’re just events with sugar. The compiler knows exactly what ~greet means — it’s lib:greet, which is just an event, which has a signature, which can be type-checked and traced.
A Real Example
Here’s the actual test case that validates this feature:
lib.kz - A library with a keyword event:
~[keyword]pub event greet { name: []const u8 }
~proc greet {
const std = @import("std");
std.debug.print("Hello, {s}!
", .{name});
} input.kz - User code that imports and uses the keyword:
~import "$lib"
// Using ~greet instead of ~lib:greet because lib defines [keyword]pub event greet
~greet(name: "World") Output:
Hello, World! The ~greet invocation is resolved to lib:greet at compile time, with zero runtime overhead.
What Keywords Could Enable
While the basic mechanism is implemented, keywords open the door to interesting possibilities:
- Standard library ergonomics: Common I/O, string, and math operations as unqualified keywords
- Domain-specific vocabularies: Game engines with
~spawn,~collide; test frameworks with~assert,~expect - Control flow abstractions: Event-based
~if,~while,~matchthat integrate with Koru’s continuation system
The beauty is that these would be library code, not language changes. Any library can propose vocabulary; users opt in by importing.
The Trade-offs
Pros:
- Natural vocabulary for common operations
- No loss of explicitness (always can qualify)
- Collision handling is honest and actionable
- Zero runtime cost
- Tooling still works (just events)
Cons:
- Mental overhead: “Is
~fooa keyword or local event?” - Potential for keyword proliferation (libraries racing to claim names)
- Requires discipline from library authors
We think the trade-offs favor keywords for foundational operations. A standard library with common operations as keywords will feel more natural than requiring explicit qualification everywhere.
Try It
Keywords are available now. The feature is covered by regression tests in:
330_001_keyword_basic- Basic keyword resolution330_002_keyword_collision- Collision detection330_003_keyword_explicit- Explicit qualification bypass330_004_keyword_requires_pub- Public-only enforcement
Define your own keyword events and see how they feel. The syntax is simple: just add [keyword] before pub event.
Koru: Where libraries earn the right to speak like language.
Published November 28, 2025