Skip to content

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.


Both of these now compile:

use std.collections.skiplist
use 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,
}
end

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


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.

func first_n_bytes(s: [*:0]u8, n: usize) -> []u8 do
return s[0..<n] // previously segfaulted in the LLVM emitter
end

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

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

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


Var-decl generic-struct annotation, downstream emit

Section titled “Var-decl generic-struct annotation, downstream emit”
var v: Result[u32, u32] = .undefined
v.ok = 42 // trips MissingOperand at LLVM emit

Type 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() // works
var v = Result[u32, u32] { ok: 42, err: .none } // works

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.


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.


  • Var-decl generic-struct emit fix — separate sprint; the type-inferred form is the canonical workaround.
  • Cross-module test_decl filter — 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.

  • Tutorial: Generics Across Module Boundaries – three patterns, when to use each, the one sharp edge to know about.
  • Source: std/db/lsm.jan – the unified MemTable[K, V] surface, the WAL frame format, and the GrainStoreU32U32 Phase C facade.

Binary version: 2026.5.2 Git hash: 86d848c0 Built on: 2026-05-02