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 alive after runtime teardown, but their atom slot words pin no spellings. Atom storage retires its global slots before freeing records; spelling access through a surviving non-fixed atom Term reports expired_context.

Process memory

Each context owns one ProcessHeap (process_heap.hpp); its layout, areas, sizing, admission, roots and collection are specified in runtime-heap.md. The host API:

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});

Persistent terms

One table per runtime (memory/persistent_terms), owned by Runtime::Impl and reached through ProcessContext::persistent_terms(), keeps the terms of persistent_term (builtins).

ETS tables

One registry per runtime (ets/tables), owned by Runtime::Impl and reached through ProcessContext::ets_tables(), keeps the tables of ets (builtins).

Match specifications

runtime/src/match_spec/ compiles and runs match specifications as OTP's erl_db_util.c does, for ets and erlang:match_spec_test/3 (builtins).

Scheduler bookkeeping

SchedulerService (scheduler.hpp) is a host-facing record of process lifecycle; it runs no code. Programs run on the executor (processes), which does not use it.

Threads

Scheduler workers (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 io output to standard output (io). 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 (executables): it checks every descriptor's ABI, starts a runtime, registers all modules before any entry code, builds argv in the entry context, runs the entry on the executor and maps the result to the exit status. 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::function(FunctionIdentity)TermError::not_implemented
SchedulerService::run / executeSchedulerError::not_implemented
CodeServer::unloadCodeError::not_implemented
dispatch_builtin on a catalogued BIFStatus::not_implemented