Clause
← All documentation

Runtime

clause_runtime is an LLVM-free C++23 library. It owns contexts, process heaps, atoms, loaded modules and lifecycle bookkeeping, and runs Erlang processes on scheduler workers (processes). All APIs are project-internal; host calls must be serialized per runtime and not overlap a program run, whose workers synchronize among themselves (threads).

Linking

Link exactly one runtime built for the target through Clause::generated_program:

add_executable(harness harness.cpp)
target_link_libraries(harness PRIVATE Clause::generated_program)

It brings the archive, ABI/runtime headers and C++23, but not LLVM. link_consumer.cpp shows a full lifecycle.

Lifecycle

runtime.hpp:

Teardown order: close scheduler records → destroy contexts → release code registrations → destroy scheduler service → atom table last. Resolved function handles keep code and atom spellings alive after runtime teardown but never a process.

Process memory

Each context owns one ProcessHeap: a single heap block created by its first allocation and sized max(min_heap_words, request), plus a chain of heap fragments owned by the same process. Words move only when the host calls collect() at a safe point.

The heap follows the classic ERTS design; runtime-heap.md is its contract (layout, areas, sizing, admission, roots, collection).

Code server and builtins

Each runtime owns one CodeServer (code_server.hpp, callable.hpp):

auto functions = std::make_unique<ModuleRegistry>();
auto added = functions->add("identity", 1,
    [](ProcessContext &, std::span<const Term> args) -> CallResult<Term> { return args.front(); });
auto loaded = context.code_server().load({"native_demo", CodeImage::linked(), std::move(functions)});
auto fn = context.code_server().resolve({.module = "native_demo", .function = "identity", .arity = 1});

Scheduler bookkeeping

SchedulerService (scheduler.hpp) records process lifecycle only; it runs no code.

Design sketches for workers and processes live in runtime/design/ and runtime/include/{scheduler,process}.hpp. Messages are implemented (processes): a send copies the message into the receiver's heap and appends it to its signal inbox (runtime/include/mailbox.hpp).

Threads

Scheduler workers (plan step 56, workers) run processes on several threads of one runtime. Services they share are synchronized; everything else stays confined to the thread running its process or guarded by the executor's mutex.

Standard output

RuntimeOptions::standard_output (output.hpp) receives erlang:display/1 and later standard_io bytes. The default writes to process stdout through C stdio (buffered); a host sink returns false to report a failed write.

erlang:display/1 renders its argument in display style, writes the text and a newline in one write and returns true. Rendering limits and rejected writes become infrastructure statuses (resource_limit, output_failure, ...) in the checked channel, never Erlang exceptions.

Program startup

CLAUSE_main_v1 (startup.hpp, runtime/src/startup/) runs a whole program for the generated main: it checks every descriptor's ABI, starts a default runtime, registers all modules before any entry code, builds argv in the entry context, calls the entry and maps the result to the exit status of executables. Reports go to stderr after stdout is flushed; the context and runtime are torn down in order on every path. CLAUSE_halt_v1 implements erlang:halt/0,1 (abort calls std::abort).

Deferred services

These report one [feature] notimpl line (features) and change no state:

BoundaryError
TermFactory::port (no ports yet, plan step 53), reference(ReferenceIdentity) and function(FunctionIdentity)TermError::not_implemented
SchedulerService::run / executeSchedulerError::not_implemented
CodeServer::unloadCodeError::not_implemented
dispatch_builtin on a catalogued BIFStatus::not_implemented