Keyword Events: When Libraries Want to Speak Like Language

· 8 min read

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
  1. Canonicalization: All event paths get module qualifiers. ~greet becomes input:greet (qualified to the main module).

  2. Keyword Registry: Built by scanning all imported modules for [keyword] annotated public events. Maps keyword names to their canonical paths.

  3. Resolution: Paths qualified to the main module (like input:greet) are checked against the registry. If greet is a registered keyword, the path is rewritten to the canonical form (e.g., lib:greet).

  4. 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, ~match that 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 ~foo a 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 resolution
  • 330_002_keyword_collision - Collision detection
  • 330_003_keyword_explicit - Explicit qualification bypass
  • 330_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