Source on Codeberg (opens in a new tab)

kern v0.9.0-rc1: the stability candidate

Code-side, every v1.0 ROADMAP gate is closed or has its harness shipped. Generics, async, Secure<T, State> with EU-sovereign cloud KMS, native OCI containers, kern-pkg ported to Kern, and a chain-hashed AI audit log all land here. What v0.9.x exists to wait out is calendar and governance, not code.

At a glance

Tests, 100% pass
901
v1.0 gates closed
16/20
Compiler lines (Kern)
25K
C runtime lines
31K

What “stability candidate” means

v1.0 is a contract: no breaking changes without a v2. The ROADMAP lists 20 explicit v1.0 exit gates. Through this release wave — v0.7 (generics), v0.8 (async, Secure<T, State>), v0.9.0-rc1 (kern-pkg port, cloud KMS, native containers, chain-hashed audit) — 16 of those gates are now closed or have their harness shipped.

The four that remain are not code:

  • Parser-fuzzer 24 h soak. Harness shipped this release (tools/fuzz_parser.sh). The 24-hour clean run is calendar.
  • Real-account exercise of cloud KMS providers. Code lands here; the regulatory check is using a real Scaleway / Azure tenant against the interface.
  • Stichting Kern Foundation. In formation. Incorporation is governance, not engineering.
  • 6 months of zero breaking changes. The point of v0.9.x: soak.

Three of the four are calendar / governance. So v0.9.0-rc1 is the version where, code-side, we’re done.

Why generics before async, and async before kern-pkg. Async codegen heap-allocates an __async_ctx struct that contains the function’s locals; if the generic ABI changes after async ships, every async function’s frame layout changes too. Generics first, then async on a stable ABI. Then the package manager port — because the only way to kill the Python-shim asterisk on “self-hosted” is to port it on top of finished primitives, not parallel to in-flight ones.

Generics — selective monomorphization on a frozen ABI

The strategy committed in v0.6 is now the implementation. Stdlib hot paths get monomorphized typed accessors emitted as linkonce_odr, so multiple compilation units share them without link conflicts. Generic call sites compile to the monomorphized typed wrappers when they exist; the type-erased path is the fallback.

  • Monomorphized: List<T>, Map<K,V>, Result<T>, Option<T>, Decimal
  • Opt-in: @monomorphize annotation, parsed and recorded as __annotation_* TraitInfo entries
  • Frozen for v0.7.0: the ABI decision is locked in before async lands on top of it. See docs/design/generics-strategy.md.

Async — spawn, await, spawn_thread on cooperative routines

The async fallback paths v0.6.0 documented as TODO are now the real codegen. Every direct spawn / await runs through libuv-driven code. Bodies execute on cooperative kern routines — stackful coroutines with 16 KB guard-paged stacks (configurable via KERN_ROUTINE_STACK_SIZE). await from inside a routine parks it on the awaitee; no OS thread is held during suspension.

async fn work(label: str) -> str:
    await sleep_ms(80)
    return label

async fn main():
    # Two cooperative routines — both park on the timer.
    a = spawn work("alpha")
    b = spawn work("beta")

    ra = await a
    rb = await b
    # Both finish in ~82 ms total, not 160.

    # CPU-bound work routes to the libuv worker pool:
    h = spawn_thread crunch_numbers(big_input)
    result = await h

Routine-aware IO is not just sleep_ms: kern_async_file_read / kern_async_file_write / kern_async_dns_resolve / kern_async_tcp_connect / kern_async_tcp_read all branch on routine context — inside a routine they submit with a callback and park; the callback re-enqueues the routine when the IO event fires. Outside a routine, the existing synchronous-pump path is unchanged. Routine-aware libpq and mbedTLS land in the same wave.

Concurrency proof. Two spawned sleep_ms(80) tasks complete in ≈82 ms (not 160). Two parallel DNS resolves in ≈11 ms. Two spawn_thread 80 ms tasks complete in ≈82 ms via the libuv worker pool.

Secure<T, State> — compile-time IO governance with EU cloud KMS

Secure<T, State> is opt-in compile-time governance for sensitive data, with State ∈ { Plain, Encrypted, Hashed }. At runtime a Secure value is just T; the type-state lives in the typechecker’s parallel-marker tracking (__sec_state__<name>) and is propagated on identifier assignment. The compile-time IO-sink check rejects Secure<_, Plain> flowing into db_* or http_* calls with a clear diagnostic.

import std.crypto.kms as kms
import std.crypto.secure as secure

# KERN_KMS_PROVIDER routes to: scaleway | azure | ovh | local
# Default is local AES-256-GCM via mbedTLS.

async fn store_session_token(raw: str) -> Result<str>:
    plain   = secure.wrap(raw)              # Secure<str, Plain>
    encrypted = await kms.encrypt(plain)?  # Secure<str, Encrypted>

    # db_insert(plain, ...) — COMPILE ERROR:
    #   Secure<str, Plain> cannot reach a db_* sink.
    return await db.insert("sessions", encrypted)?

KERN_KMS_PROVIDER selects the backend at runtime. Two EU-sovereign providers ship in this release:

  • Scaleway KMS — EU-native, regions fr-par and nl-ams
  • Azure Key Vault — with AAD bearer-token caching, EU-only deployments
  • OVH — stub interface awaiting real-account exercise
  • Local — AES-256-GCM via mbedTLS, the default. Per-encryption nonces, master key from KERN_KMS_MASTER_KEY.

kern-pkg — the Python shim is gone

The package manager is no longer a transition asterisk on “self-hosted.” Every command runs on Kern:

  • init / build / run / test — local lifecycle
  • add / remove / verify / install — dependency management
  • publish — pack, hash, sign with Ed25519, upload
  • search — query the registry index
  • update — re-resolve manifest deps into kern.lock

Manifest [dependencies] and [dev-dependencies] parse into a typed List<Dependency>. The lockfile is read and written with chain-integrity verification. The Python kern-pkg/ directory remains in-tree as a transition shim only and is on its way out. This closes the ROADMAP toolchain self-hosting gate.

Native OCI containers — no daemon, no shellout

kern_container_run_native() runs a sandboxed process directly: Linux namespaces (PID, mount, net, UTS, IPC), cgroup v2 for CPU / memory / IO limits, overlayfs for the rootfs, and a veth pair for the network. No daemon, no docker / podman / runc shellout.

Pulling images is just as direct. kern_oci_pull() talks to any OCI Distribution Spec v2 registry — Docker Hub, GHCR, Quay, GitLab CR — with SHA-256 verification on every blob and cache-keyed-by-digest. Tampered blobs are detected and unlinked. oci_run_image() composes pull + config + run into a single call.

import sys.container as ctr

async fn main():
    db = await ctr.run(
        image:   "postgres:16-alpine",
        name:    "kern-db",
        ports:   ["5432:5432"],
        env:     {"POSTGRES_DB": "kern"},
        restart: "unless-stopped"
    )?
    await db.ready(timeout: 30)

    # cgroup v2 metrics by name:
    stats = ctr.stats_native("kern-db")
    print(stats.cpu_usage_us, stats.mem_rss_bytes)

Chain-hashed AI audit log — closes the EU AI Act gate

BLAKE2b on the event log was correct for crash safety; it was not enough for tamper-evidence on its own — a determined writer could rewrite history and rehash. v0.9.0-rc1 lifts the AI audit log onto a SHA-256 hash chain:

  • Every record carries prev_hash + record_hash, where record_hash = SHA-256(prev_hash || canonical_body)
  • On open, kern_ai_audit_verify() replays the file and refuses appends if any link mismatches
  • The event-log verifier also recomputes each record’s hash from seq | ts | type | data | prev_hash and rejects tampered payloads even when the chain link is kept consistent

Tamper-evident by construction. Closes the EU AI Act audit-trail gate.

Production primitive resilience — finishing the v0.6 work

v0.6 wrapped the LLM proxy and Qdrant in a circuit breaker + retry layer. v0.9 adds the slow-path armour around it:

  • LLM proxy — 60 s socket timeout (SO_RCVTIMEO / SO_SNDTIMEO) on the shared HTTPS-with-headers path. A hung TLS handshake to a third-party LLM no longer holds a routine indefinitely.
  • Qdrant — 30 s socket timeouts plus kern_qdrant_health() for readiness / liveness probes.
  • WebSocket — stdlib connect_with_backoff() (200 ms base, doubling, capped at 30 s, ±25% jitter) and send_resilient().
  • Event log — fcntl exclusive lock per append plus tunable fsync interval. Multi-process safe, crash-safe.

Compliance exports — SBOM and Art. 30 records

  • std.compliance.cyclonedx_sbom() — CycloneDX 1.5 JSON SBOM listing all runtime dependencies. Compliance-scanner ready for the EU Cyber Resilience Act.
  • std.compliance.render_art30_register() — emits a GDPR Art. 30 record-of-processing-activities document directly from runtime metadata.

Compiler and runtime fixes worth naming

  • println(int|float|bool) segfault — typed args were being passed to the i8* helper instead of the type-specific one. Now routed through compile_print_call_direct, like print always was.
  • Cross-unit struct projection from Ok-arm bindings — bind_variant_args now sets __kt__<bind> on the Ok-payload bitcast, so for x in lf.field chains correctly across compilation units.
  • PersonalData taint leaks — list elements, map values, string interpolation, and trait method dispatch all propagate PersonalData taint to the AI guard.
  • OCI digest verification — every layer + config blob is verified before extraction; a tampered blob is detected and unlinked.
  • test_perf_regex_match_1k — was failing the int→bool narrowing rule; comparison made explicit. Conformance is now 706/706.

What v0.9.x is for

v0.9.0-rc1 is not v1.0. The point of the v0.9.x line is soak time:

  • Parser fuzzer 24 h clean. Harness shipped (commit 4b700d2) — five mutators (bit flip, insert, delete, truncate, token splice), smoke run 521 iters / 0 crashes / 0 timeouts in 120 s. CI gate command for v1.0: tools/fuzz_parser.sh 86400.
  • HSM / PKCS#11 signing. Adds a PKCS#11 binding for hardware-backed Ed25519 release signing.
  • Full DORA Art. 9/10/11 risk mapping. Currently mapped at the operation level; the runtime risk register is the v0.9.x deliverable.
  • Real-account exercise of cloud KMS providers. Code interfaces ship here; running them against real Scaleway / Azure tenants is the regulatory check.
  • External security audit. Scheduled inside the v0.9.x window.
  • 6 months of zero breaking changes. Then v1.0.

Every item above is on the ROADMAP with a checkbox. None are aspirational; all are blockers.

By the numbers

  • 901 tests at 100% pass — 706 conformance, 195 negative; both gates CI-blocking
  • 16 of 20 v1.0 ROADMAP gates closed or harness shipped
  • 84 stdlib modules across std, data, net, db, sys
  • 25,499 lines of Kern in the self-hosted compiler (10 source files)
  • 31,000 lines in the C runtime, ~300 FFI bridge functions
  • 5 libFuzzer targets — JSON, HTTP, string, BLAKE2b, base64 — plus the new structure-aware parser fuzzer
  • Reproducible builds verified byte-for-byte identical with SOURCE_DATE_EPOCH
  • EUPL-1.2, hosted on Codeberg, governed from the Netherlands

Try v0.9.0-rc1

Install on Linux or macOS. Single binary, no runtime dependencies.

curl -fsSL https://kern-lang.eu/install.sh | bash