Post

Dea/L0 2.1.0: a public C runtime interface

Dea/L0 2.1.0: a public C runtime interface

I have released Dea/L0 2.1.0, a focused update to the self-hosted foundation of the Dea systems programming language. This release gives C code a supported interface to the runtime, fixes several runtime correctness issues, and reorganizes both compiler stages into smaller modules. The L0 2.0.0 language and command-line surface stay unchanged.

For readers new to the project, L0 is the small base language in Dea’s staged bootstrap: it has C-family syntax and compiles to C99. Stage 1, written in Python, bootstraps Stage 2, the compiler written in L0 itself and shipped in this release. L1 is the next language level being built on that foundation. The 2.0.0 announcement describes the checked-pointer runtime and editor tooling that this release builds on.

A public interface for C code

L0 has always supported foreign-function declarations with extern func. Version 2.0.0 added the repeatable --c-source option for passing additional C source files to the compiler. Version 2.1.0 gives those files supported access to Dea runtime types and functions, a capability long available in the implementation of the upcoming L1 language level. The interface is source-compatible across the runtime subset shared by both levels.

The new dea_rt.h lets C helpers work with Dea strings and optional values and call the runtime operations that handle them. It exposes public types, value macros, and rt_* function declarations without defining another copy of the runtime. The generated L0 C file supplies the implementation by including l0_runtime.h exactly once. Both headers are included in installation prefixes and release archives.

The release’s C interoperability example uses the header’s shared integer type:

1
2
3
4
5
6
#include "dea_rt.h"

dea_int add_in_c(dea_int left, dea_int right)
{
  return left + right;
}

The matching L0 declaration is:

extern func add_in_c(left: int, right: int) -> int;

Here dea_int represents L0’s 32-bit signed integer type. The compiler checks L0 calls against the extern func declaration; matching that declaration to the C definition remains the programmer’s responsibility.

The complete example links two C files. With l0c available, run this from the example directory:

1
2
3
4
l0c --run \
  --c-source c_add.c \
  --c-source c_multiply.c \
  c_interop

It prints C sum: 42 and C product: 42.

Existing l0_* type names remain supported. The shared dea_* aliases and DEA_* value macros give C code a common vocabulary for the subset implemented by both L0 and L1: booleans, bytes, integers, strings, their optional forms, and runtime functions with matching signatures. This supports compiling equivalent C source against either level. L0 and L1 object files and runtime binaries remain separate, and level-specific records and private runtime helpers are outside that contract. The C interface documentation defines the supported subset in detail.

Runtime correctness

The checked runtime introduced in 2.0.0 keeps freed user allocations in a bounded quarantine. That lets generated L0 code recognize many stale-pointer accesses while the underlying storage is retained.

Direct C accesses can bypass those generated checks. In AddressSanitizer builds, the runtime now marks released user payloads as poisoned while they remain in quarantine, then unpoisons them before returning the storage to the allocator. AddressSanitizer can therefore report stale C accesses during retention as well as when quarantine retention is disabled.

The release also fixes vec_push_bytes self-appends when growth moves the backing allocation. Optional-value hashing now gives equal values equal hashes across the C boundary, although exact results can change with this fix.

Expanded regression coverage checks these fixes across the supported checking, tracing, and sanitizer configurations.

A more maintainable self-hosted compiler

Both compiler stages now separate command-line handling, semantic analysis, C emission, and backend execution into smaller modules. This gives contributors clearer places to trace a diagnostic, change a semantic check, or work on generated code, while keeping closely related algorithms together.

The refactoring preserves language behavior, diagnostic codes and ordering, generated C behavior, and ownership cleanup. It also preserves the bootstrap fixed point: successive self-built compilers still converge to the same generated C.

Editors, documentation, and upgrading

Editor support now follows the 2.0.0 case syntax more closely. Tree-sitter rejects the obsolete case ... else default while retaining valid if ... else parsing, and standalone wildcard arms receive consistent highlighting in Tree-sitter, Vim, and Emacs, with TextMate regression coverage.

Internal compiler and runtime APIs now meet strict documentation coverage, making their contracts easier to find when working on the toolchain. Release and documentation workflows also verify downloaded artifact digests to catch damaged or mismatched build artifacts.

There is no syntax or CLI migration from 2.0.0. For mixed-language projects, include dea_rt.h in foreign C files; including the implementation-bearing l0_runtime.h there would define the runtime again and cause duplicate symbols. Keep C signatures aligned with L0 declarations, stay within the documented common subset when sharing C code with L1, and avoid persisting exact runtime hash results.

The new public header, aliases, macros, and runtime linkage are the backward-compatible additions behind the minor version bump. Existing public L0 types and diagnostic codes are unchanged.

Download the Linux, macOS, and Windows builds, generated API documentation, and SHA256SUMS from the Dea/L0 2.1.0 release. The full comparison and contributor guide are available in the public repository.

I hope you have fun with Dea/L0 2.1.0. I’d love to hear what you build with it!

This post is licensed under CC BY 4.0 by the author.