Generics Across Module Boundaries
Generics Across Module Boundaries
Section titled “Generics Across Module Boundaries”Use generic types from imported modules with the same fluency you have within a single file.
Prerequisite: Generics & Traits covers the basics of declaring generic structs and functions. This tutorial picks up where that ends — using them across
usestatements.
Time: 15 minutes Level: Intermediate What you’ll learn: Module-qualified type annotations, cross-module struct literals, and three equivalent ways to initialise generic structs.
Why This Matters
Section titled “Why This Matters”Janus generics are nominal. A generic type like Result[T, E] declared in mymod is a distinct type from one declared in othermod — even if the field shapes match. When you import a generic type, you reference it through its module path: mymod.Result[T, E].
Up to v2026.4.8, you could declare module-qualified generics, but using them at construction or annotation sites tripped the type validator. Two patterns silently failed:
var v: mymod.Result[u32, str]→E2010: Unresolved type identifier 'mymod'mymod.Result[u32, str] { ok: 42, err: .none }→ struct literal not recognised after the generic instantiation
v2026.5.2 closed both at the type-validation layer; v2026.5.3 closed the downstream emit follow-up. The patterns below all compile end-to-end now.
Step 1: Module-Qualified Type Annotations (5 min)
Section titled “Step 1: Module-Qualified Type Annotations (5 min)”The Pattern
Section titled “The Pattern”use std.collections.skiplist
func main() do // The annotation form — explicit type after the variable var sl: skiplist.SkipList[u32, u32] = skiplist.init[u32, u32](42)
skiplist.insert[u32, u32](&sl, 7, 99) const got: ?u32 = skiplist.get[u32, u32](&sl, 7)
if got != .none do println("found") end
skiplist.deinit[u32, u32](&sl)endThe annotation skiplist.SkipList[u32, u32] works the same way SkipList[u32, u32] would inside the std.collections.skiplist module itself. The compiler resolves skiplist to the imported module, looks up SkipList inside it, and substitutes u32 for both type parameters.
What Changed
Section titled “What Changed”Before v2026.5.0 the type validator inspected the first token of the type expression (skiplist) and looked it up against the type universe. It is a module name, not a type name — so the lookup failed with E2010. The fix detects three module-qualified shapes the parser produces — expression-position field-expressions, type-position dotted paths, and multi-token named-type ranges — and skips first-token validation for all three.
When You Need It
Section titled “When You Need It”You almost always have a workaround: type inference.
var sl = skiplist.init[u32, u32](42) // type inferred — always workedThe annotation form is needed when:
- The initializer is
.undefinedand inference has nothing to chew on - You want the type to appear in the source for documentation
- You are storing the value in a struct field whose type must be spelled out
struct Cache { index: skiplist.SkipList[u32, u32], // annotation required here}Step 2: Cross-Module Struct Literals (5 min)
Section titled “Step 2: Cross-Module Struct Literals (5 min)”The Pattern
Section titled “The Pattern”// In mymod.janpub struct Result[T, E] { ok: ?T, err: ?E,}
// In your fileuse mymod
func main() do const ok_branch = mymod.Result[u32, str] { ok: 42, err: .none, }
const err_branch = mymod.Result[u32, str] { ok: .none, err: "bad input", }endThe literal form mymod.Result[u32, str] { field: val, ... } works the same way Result[u32, str] { ... } works inside mymod. Field-by-field initialization, type-parameter substitution, the field types are checked against the substituted shape.
What Changed
Section titled “What Changed”Two-file fix:
- The parser’s postfix-
{branch already recognised struct literals after a bare-namegeneric_instantiation(Foo[K, V] { ... }). It now also recognises them after ageneric_instantiationwhose left-hand side is afield_expr— that is,mymod.Foo[K, V] { ... }. - The lowerer’s
lowerStructLiteralpreviously extracted the first token of the callee — formymod.Foothat ismymod, the qualifier, not the type. It now extracts the right-hand identifier of thefield_exprcallee (Foo).
Bare-name struct literals (Foo[K, V] { ... } after use mymod.Foo) were never affected.
Try It
Section titled “Try It”use std.io
pub struct Result[T, E] { ok: ?T, err: ?E,}
pub func make_ok[T, E](val: T) -> Result[T, E] do return Result[T, E] { ok: val, err: .none }end
func main() do // Bare-name form (Result is in scope here) const r1 = make_ok[u32, str](42)
// From an importing file you would write: // const r2 = result_demo.Result[u32, str] { ok: 99, err: .none } // Both forms work in v2026.5.2 and later.
if r1.ok != .none do println("got value") endendStep 3: .undefined Initializers Now Work Too (5 min)
Section titled “Step 3: .undefined Initializers Now Work Too (5 min)”Closed in v2026.5.3
Section titled “Closed in v2026.5.3”Both of these compile end-to-end:
var v: mymod.Result[u32, str] = .undefinedv.ok = 42 // emits cleanly
var p: Pair[u32, u32] = .undefinedp.a = 7 // emits cleanlyIf you read v2026.5.2’s release notes you may have seen this pattern listed under Known Issues with a MissingOperand LLVM emit failure. That was filed under “Var-decl generic-struct emit” and closed the next day in v2026.5.3 — see v2026.5.3 release notes for the underlying fix.
Three Equivalent Initialization Forms
Section titled “Three Equivalent Initialization Forms”You now have three ways to bring a generic struct into existence, all production-ready:
// 1. Constructor literal — initialises every field at the call sitevar v = mymod.Result[u32, str] { ok: 42, err: .none }
// 2. Type inference via constructor functionvar v = mymod.make_ok[u32, str](42)
// 3. Annotation + .undefined + per-field assignmentvar v: mymod.Result[u32, str] = .undefinedv.ok = 42v.err = .nonePick whichever reads best for the situation. The constructor literal is the most declarative; the inferred form is the lightest; the .undefined form is the most flexible when fields are populated conditionally or in a loop.
What You Learned
Section titled “What You Learned”Patterns
Section titled “Patterns”var v: mod.Type[K, V] = ...— module-qualified type annotationmod.Type[K, V] { field: val }— module-qualified struct literalmod.func[K, V](args)— generic function call (this always worked)
When to Use Each Form
Section titled “When to Use Each Form”- Type inference — first choice when the initializer carries enough information
- Annotation — required for
.undefinedinitializers, struct fields, function parameters - Constructor literal — most declarative; great for one-shot construction with all fields known
.undefined+ per-field assignment — most flexible; useful when fields populate conditionally or in a loop
The unified MemTable[K, V] in std.db.lsm is the load-bearing consumer of cross-module generics — both MemTable[u32, u32] and MemTable[[]const u8, []const u8] are constructed and passed through the same six functions from std.db.lsm consumers. See the v2026.5.2 release notes for the API and a working example.