Skip to content

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 use statements.

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.


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)”
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)
end

The 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.

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.

You almost always have a workaround: type inference.

var sl = skiplist.init[u32, u32](42) // type inferred — always worked

The annotation form is needed when:

  • The initializer is .undefined and 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)”
// In mymod.jan
pub struct Result[T, E] {
ok: ?T,
err: ?E,
}
// In your file
use 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",
}
end

The 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.

Two-file fix:

  • The parser’s postfix-{ branch already recognised struct literals after a bare-name generic_instantiation (Foo[K, V] { ... }). It now also recognises them after a generic_instantiation whose left-hand side is a field_expr — that is, mymod.Foo[K, V] { ... }.
  • The lowerer’s lowerStructLiteral previously extracted the first token of the callee — for mymod.Foo that is mymod, the qualifier, not the type. It now extracts the right-hand identifier of the field_expr callee (Foo).

Bare-name struct literals (Foo[K, V] { ... } after use mymod.Foo) were never affected.

result_demo.jan
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")
end
end

Step 3: .undefined Initializers Now Work Too (5 min)

Section titled “Step 3: .undefined Initializers Now Work Too (5 min)”

Both of these compile end-to-end:

var v: mymod.Result[u32, str] = .undefined
v.ok = 42 // emits cleanly
var p: Pair[u32, u32] = .undefined
p.a = 7 // emits cleanly

If 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.

You now have three ways to bring a generic struct into existence, all production-ready:

// 1. Constructor literal — initialises every field at the call site
var v = mymod.Result[u32, str] { ok: 42, err: .none }
// 2. Type inference via constructor function
var v = mymod.make_ok[u32, str](42)
// 3. Annotation + .undefined + per-field assignment
var v: mymod.Result[u32, str] = .undefined
v.ok = 42
v.err = .none

Pick 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.


  • var v: mod.Type[K, V] = ... — module-qualified type annotation
  • mod.Type[K, V] { field: val } — module-qualified struct literal
  • mod.func[K, V](args) — generic function call (this always worked)
  • Type inference — first choice when the initializer carries enough information
  • Annotation — required for .undefined initializers, 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.