v2026.5.2 – Generics Across Modules
v2026.5.2 – Generics Across Modules
Section titled “v2026.5.2 – Generics Across Modules”Release date: 2026-05-02
Profiles affected: :core (compiler), :core stdlib (std.db.lsm)
Status: Patch release (compiler bugfix + stdlib unification)
This release closes the generics-across-modules story. Three compiler fixes that landed together let you annotate, construct, and pass generic types from imported modules with the same fluency you have always had within a single file. std.db.lsm capitalises on the new compiler surface by collapsing two concrete MemTable* types into a single generic MemTable[K, V].
The four landings together are the dogfooding loop in action: writing the LSM stdlib in idiomatic Janus surfaced four compiler gaps; closing the gaps unblocked the stdlib unification; the unified stdlib proves the gap closures end-to-end.
What’s new at the surface
Section titled “What’s new at the surface”Both of these now compile:
use std.collections.skiplistuse mymod // declares pub struct Result[T, E] { ok: ?T, err: ?E }
func main() do // Module-qualified generic-type annotation — was E2010 before var sl: skiplist.SkipList[u32, u32] = skiplist.init[u32, u32](42) skiplist.insert[u32, u32](&sl, 7, 99)
// Cross-module generic struct literal — silently broken before const r = mymod.Result[u32, str] { ok: 42, err: .none, }endIf you tried either pattern on v2026.5.1 or earlier you got E2010: Unresolved type identifier 'skiplist' or a struct-literal parse rejection, with no honest workaround for the annotation case other than type inference. The doors are open now.
The four landings
Section titled “The four landings”1. Cross-module generic struct literals
Section titled “1. Cross-module generic struct literals”mymod.Type[K, V] { field: val, ... } now parses and lowers correctly. The bare-name form (Type[K, V] { ... } after a direct use mymod.Type) was never affected.
Two-file fix: the parser’s postfix-{ branch now recognises a struct literal after a generic_instantiation whose left-hand side is a field_expr (mirrors the existing bare-name path). The lowerer’s lowerStructLiteral extracts the right-hand identifier of the field_expr callee — the type name — instead of the qualifier.
2. Module-qualified generic-type annotations
Section titled “2. Module-qualified generic-type annotations”var v: othermod.Container[T, U] = ... now passes type validation. Previously the validator inspected the first token of the type expression (othermod, the module qualifier) against the type universe and emitted E2010 because module names are not types.
The fix detects three module-qualified shapes the parser produces — expression-position field_expr callees, type-position dotted paths, and multi-token .named_type ranges — and skips first-token base-name validation for all three. Cross-module resolution defers to the lowerer’s existing module-import logic.
3. Slicing sentinel-pointer parameters
Section titled “3. Slicing sentinel-pointer parameters”func first_n_bytes(s: [*:0]u8, n: usize) -> []u8 do return s[0..<n] // previously segfaulted in the LLVM emitterendThe LLVM emitter’s emitSlice walked LLVMGetTypeKind(pointee) to determine the element type. For sentinel-pointer parameters the pointee was either unregistered at function prologue or returned garbage under LLVM 15+ opaque pointers (LLVMGetElementType is undefined behaviour there).
The fix mirrors the pattern already used by emitIndex: when the pointer-pointee map misses, consult the IR node’s semantic type. For .ptr and .str_ptr semantic kinds the element type is i8. Same behaviour, no segfault.
4. std.db.lsm — generic MemTable[K, V]
Section titled “4. std.db.lsm — generic MemTable[K, V]”The stdlib payoff. std.db.lsm now exposes a single generic in-memory tier:
use std.db.lsm
func main() do // u32-keyed monomorph var mt32 = lsm.mt_init[u32, u32](42) lsm.mt_put[u32, u32](&mt32, 7, 99) const v32 = lsm.mt_get[u32, u32](&mt32, 7) lsm.mt_deinit[u32, u32](&mt32)
// byte-keyed monomorph (same six functions) var mtb = lsm.mt_init[[]const u8, []const u8](42) lsm.mt_put[[]const u8, []const u8](&mtb, "alpha", "first") const vb = lsm.mt_get[[]const u8, []const u8](&mtb, "alpha") lsm.mt_deinit[[]const u8, []const u8](&mtb)endThe previous concrete forms — MemTableU32U32 (Phase B v0) and MemTableBytes (Phase B v1) — are retired. Both monomorphs share the same six functions: mt_init, mt_deinit, mt_len, mt_put, mt_get, mt_remove. Internally they share one wrapper over skiplist.SkipList[K, V].
The GrainStoreU32U32 Phase C facade, the WAL-replay-into-MemTable recovery path, and the crash-recovery semantics are unchanged from v2026.5.1.
Known issues (filed for separate sprints)
Section titled “Known issues (filed for separate sprints)”Var-decl generic-struct annotation, downstream emit
Section titled “Var-decl generic-struct annotation, downstream emit”var v: Result[u32, u32] = .undefinedv.ok = 42 // trips MissingOperand at LLVM emitType validation now passes for both bare-name and module-qualified annotation forms. A separate downstream LLVM emitter path that handles .undefined initializers for generic structs has not yet been updated and emits MissingOperand against the IR builder. The defect predates this release and affects both forms equally.
Workaround: use the type-inferred form or initialise via constructor literal.
var v = make_result() // worksvar v = Result[u32, u32] { ok: 42, err: .none } // worksCross-module test_decl import filter
Section titled “Cross-module test_decl import filter”test_decl-derived graphs from imported modules (e.g. the 14 test functions in std/mem/std_mem.jan) currently get emitted in non-test builds, tripping emitOptionalUnwrap on half-formed test bodies. The import path needs to filter test_decl graphs at the module boundary so they are not transferred to the consumer’s emission graph list.
Workaround: none currently visible if you use std.mem.std_mem — the import pulls test bodies through unconditionally. Other std.* imports are unaffected.
Migration
Section titled “Migration”If you depend on MemTableU32U32 or MemTableBytes directly, migrate to the generic MemTable[K, V] surface. Function names lose their suffixes:
Before (v2026.5.1) | After (v2026.5.2) |
|---|---|
mt_put_u32(&mt, k, v) | mt_put[u32, u32](&mt, k, v) |
mt_get_u32(&mt, k) | mt_get[u32, u32](&mt, k) |
mt_put_bytes(&mt, k, v) | mt_put[[]const u8, []const u8](&mt, k, v) |
mt_get_bytes(&mt, k) | mt_get[[]const u8, []const u8](&mt, k) |
There is no compatibility shim. Per Janus dogfooding doctrine, breaking is preferred over carrying parallel surfaces.
.jan files that don’t touch std.db.lsm are unaffected. Existing :script, :core, and :service code compiles unchanged.
What’s NOT in this release
Section titled “What’s NOT in this release”- Var-decl generic-struct emit fix — separate sprint; the type-inferred form is the canonical workaround.
- Cross-module
test_declfilter — separate sprint. - Phase D (LSM SSTable, flush, compaction) — Phase B v2 unifies the in-memory tier; on-disk SSTables and flush thresholds remain Phase D scope, deferred.
Read more
Section titled “Read more”- Tutorial: Generics Across Module Boundaries – three patterns, when to use each, the one sharp edge to know about.
- Source:
std/db/lsm.jan– the unifiedMemTable[K, V]surface, the WAL frame format, and theGrainStoreU32U32Phase C facade.
Binary version: 2026.5.2
Git hash: 86d848c0
Built on: 2026-05-02