Runtime
clause_runtime är ett LLVM-fritt C++23-bibliotek. Det äger kontexter,
processheapar, atomer, laddade moduler och livscykelbokföring, och kör
Erlang-processer på schemaläggarens arbetstrådar (processer).
Alla API:er är interna för projektet; värdanrop måste serialiseras per
runtime och får inte överlappa en programkörning, vars arbetstrådar
synkroniserar sinsemellan (trådar).
Länkning
Länka exakt en runtime byggd för målet genom Clause::generated_program:
add_executable(harness harness.cpp)
target_link_libraries(harness PRIVATE Clause::generated_program)
Det tar med arkivet, ABI-/runtime-headers och C++23, men inte LLVM. link_consumer.cpp visar en fullständig livscykel.
Livscykel
Runtime::start(options)→std::expected<std::unique_ptr<Runtime>, Status>. Standardvärden: aktuell ABI-version och native-termbredd,max_atoms2^20 (högst 2^26; program sätter det med--max-atoms, runtime-alternativ). Antalet kontexter är inte begränsat.process_heapochprocess_stackär alternativen för kontexter som skapas avcreate_context();memory_limit_bytesär den valfria gränsen för hela runtimen, som rapporteras avmemory_bytes(). Alla saknar tak som standard.create_context()ellercreate_context(heap_options, stack_options)→ lånadProcessContext*, stabil tills den förstörs. Standardvärden för heapen: minimiheap på 233 ord (min_heap_words) och inget minnestak (limit_bytes=UNLIMITED_HEAP_BYTES; en satt budget är en multipel av ordstorleken som är minst minimiheapen). Stacken saknar också tak om inteStackOptions::limit_wordsär satt.destroy_context(ctx),shutdown(): shutdown returnerarbusymedan kontexter finns kvar; därefter lyckas den idempotent och senare anrop returnerarstopped. Destruktorn städar upp återstående kontexter.- Identiteter för kontexter och runtimes återanvänds inte; när de tar slut
blir det fel. En kontexts identitet bär dess pid-nummer
(pid:ar). En kontextpekare är ett lån, inte
en identitet.
lifetime()ger en svag token som rapporteraralive() == falseinnan heapen rivs. - Livscykelanrop kastar aldrig undantag och är tysta. Statusvärden: abi.md.
Ordning vid nedrivning: stäng schemaläggarposter → förstör kontexter → släpp kodregistreringar → förstör schemaläggartjänsten → atomtabellen sist. Upplösta funktionshandtag håller kod och atomstavningar vid liv efter att runtimen rivits, men aldrig en process.
Processminne
Varje kontext äger en ProcessHeap: ett enda heapblock som skapas vid dess
första allokering med storleken max(min_heap_words, request), plus en kedja
av heapfragment som ägs av samma process. Ord flyttas endast när värden anropar
collect() vid en safepoint.
Heapen följer den klassiska ERTS-designen; runtime-heap.md är dess kontrakt (layout, områden, dimensionering, antagning, rötter, skräpsamling).
allocate(words)returnerar nollställt ordutrymme.reserveger en reservation som endast kan flyttas, med explicit bekräftelse (commit) och automatisk återställning (rollback). En reservation åt gången per heap: bygg barnen först och reservera föräldern sist.- Bump-allokering fyller heapblocket; en begäran som inte får plats går till
det nyaste fragmentet, annars till ett nytt fragment med storleken
max(min_heap_words, request)(begränsat av den återstående budgeten). Ord är endast ordjusterade. Återställning återställer områdets topp, släpper ett fragment (eller heapblocket) som reservationen skapat och återställer bokföringen exakt. - Avvisar noll, överflöd och uttömd budget innan något publiceras. Fel:
out_of_memory(allokering) ellerlimit_exceeded(budget); genererad kod får den exakta statusen. - Binaries över 64 bytes finns i delade buffertar utanför heapen. Varje
heapcell som refererar till en sådan håller en
std::shared_ptroch ansluts till processens lista över objekt utanför heapen när den publiceras; nedrivningen går igenom listan och släpper dessa referenser (binaries utanför heapen). add(value)/copy_tokopierar en graf från en annan process i samma runtime, med bevarad delning och delade buffertar utanför heapen; en misslyckad kopiering ändrar ingenting (kopiering mellan heapar).used_wordsräknar allokerade ord;capacity_wordsräknar heapblock och fragment;off_heap_wordsräknar de buffertar som denna process refererar till, var och en en gång. Underliggande ord plus ord utanför heapen delar den valfria budgetenlimit_bytes; en skräpsamling behåller hälften av den budget som återstår efter att de överlevande frigjorts, så att uttömma den innebär att de levande data inte längre får plats (felbeteende).- Varje använt ord tolkas som ett objekt som inleds av ett huvud, en
cons-cell eller utfyllnad (ordlayout);
reserverade ord börjar nollställda.
Råa
allocate()-ord måste förbli noll eller innehålla kompletta objekt.verify()går igenom heapblocket och varje fragment och kontrollerar att varje termplats pekar på början av ett objekt i samma process (tester och felsökning; annarscorrupt_heap). collect(roots)kopierar allt som kan nås från processrötterna och värdens rotord till ett nytt heapblock, frigör det gamla blocket och fragmenten, släpper döda binaries utanför heapen och skriver om rötterna (skräpsamling). Genererad kod samlar skräp vid funktionsingångar och loophuvuden i comprehensions närwants_collection()(skräpsamling i genererad kod). Den körs endast vid en safepoint (ingen genererad kod som körs utanför ettSafePoint-omfång, ingen öppen reservation), annarsunsafe_point; rotinventeringen listar vad den skriver om; misslyckande att allokera det nya blocket gerout_of_memoryutan att något ändras.
Kodserver och inbyggda funktioner
Varje runtime äger en 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});
- En
ModuleRegistryavbildar exakt namn/aritet (≤ 255) på ett anropbart objekt där allt ärTerm.loadfryser och publicerar det; dubbletter eller fel publicerar ingenting. resolveskiljer mellan saknad modul och saknad export.ResolvedFunctionfäster modulavbildningen;callkontrollerar aritet, argument och resultat och gör om värdundantag till fel.- Native-kroppar måste vara synkrona, icke-blockerande och får inte behålla kontexten eller argumentspannet.
- En begränsad katalog över kända uppskjutna BIF:ar (
self/0,length/1,spawn/3,spawn_link/3,send/2,make_ref/0,garbage_collect/0,apply/3(genererad kod anropar i ställetapply/2,3genom tjänsterna för dynamiska anrop, funs),tuple_size/1,+/2) rapporterarnot_implemented; andra oregistrerade namn returnerarunknown_builtintyst. Denna värdväg når inte produktionens inbyggda funktioner. - Produktionens inbyggda funktioner finns i serverns
BuiltinRegistry, registrerade vid runtimens uppstart; genererad kod, dynamiska anrop och funs når dem genom bryggan för inbyggda funktioner. CodeServer::export_framehittarFrameDescriptorför en export utifrån modul, funktionsatom och aritet;function_framelägger till de inbyggda funktionerna för dynamiska anrop;external_funinternerar definitionerna av externa funs som byggs under körning.CodeServer::unloadär uppskjuten; dynamisk laddning stöds inte.
Schemaläggarens bokföring
SchedulerService (scheduler.hpp)
registrerar endast processernas livscykel; den kör ingen kod.
register_process(context)en gång per kontext (samma runtime); dubbletter returneraralready_registered.remove_process(id)pensionerar en post som inte körs.begin_dispatch→ körs;finish_dispatch→ körbar, väntande eller avslutad (med orsak).set_suspendedväxlar en flagga på körbara/väntande poster. Andra övergångar returnerarinvalid_transition.request_shutdown()stänger registrering, utdelning och suspensionsstyrning; inspektion och returer förblir tillgängliga för dränering.runochexecuterapporterarnot_implemented.
Designskisser för arbetstrådar och processer finns i
runtime/design/ och runtime/include/{scheduler,process}.hpp.
Meddelanden är implementerade (processer): en
sändning kopierar meddelandet in i mottagarens heap och lägger till det sist i
dess signalinkorg (runtime/include/mailbox.hpp).
Trådar
Schemaläggarens arbetstrådar (plan step 56, arbetstrådar) kör processer på flera trådar i en runtime. Tjänster som de delar är synkroniserade; allt annat förblir begränsat till tråden som kör dess process eller skyddas av exekutorns mutex.
- Atomer (step 54):
AtomStorageskyddar båda indexen med en delad mutex. Uppslagningar (lookup,boolean,size) delar den;internslår upp under det delade låset och endast en ny stavning tar det exklusiva låset, kontrollerar igen och publicerar posten, så att kapplöpande internering av samma stavning får ett och samma ord. Orden förblir stabila: en post ändras eller tas aldrig bort före nedrivningen. - Kod (step 55):
CodeServerskyddar sina moduler och definitioner av externa funs med en delad mutex. Uppslagningar (find_module,resolve,atom_word,record_definition,fun_definition,export_frame,function_frame,owns) delar den;loadoch byggandet av en ny definition av en extern fun tar den exklusivt, så att samtidiga registreringar av samma namn publicerar en modul (de andra fårduplicate_module) och kapplöpandeexternal_fun-anrop får en definition. Registret för inbyggda funktioner fylls vid runtimens uppstart, innan någon arbetstråd körs, och läses endast därefter. - Fästen: moduler tas aldrig bort medan runtimen lever (urladdning är
uppskjuten) och servern förstörs efter varje kontext, så definitioner,
ramar och atomplatser som den returnerat förblir giltiga för varje anrop och
varje fun-cell. Handtag från
ResolvedFunctionochfind_modulebehåller också sin modul efter att runtimen är borta. - Pid-nummer (step 56):
ProcessNumbersutfärdar nummer under ett exklusivt lås och godtar pid-ord under ett delat. - Minne (step 56): redovisningen för hela runtimen (
RuntimeMemory) debiterar och frigör med atomära operationer; en debitering tar aldrig redovisningen förbi en valfri gräns. - Port-I/O (steps 57C–57F): I/O-tjänstens läs-, skriv- och bevakningstrådar samt sockettråden (portar) rör processtillstånd endast genom exekutorns mutex.
- Länkning: på Linux lägger runtimen till
-pthreadför sina trådar; på Windows länkar den Winsock (ws2_32,mswsock) för socketar.
Standard-utdata
RuntimeOptions::standard_output (output.hpp)
tar emot erlang:display/1 och senare bytes för standard_io.
Standardimplementationen skriver till processens stdout genom C:s stdio
(buffrat); en värdmottagare returnerar false för att rapportera en
misslyckad skrivning.
erlang:display/1 återger sitt argument i
visningsstil, skriver texten och en radbrytning i en
enda skrivning och returnerar true. Återgivningsgränser och avvisade
skrivningar blir infrastrukturstatusar (resource_limit, output_failure,
...) i den kontrollerade kanalen, aldrig Erlang-undantag.
Programuppstart
CLAUSE_main_v1 (startup.hpp,
runtime/src/startup/) kör ett helt program åt den genererade main: den
kontrollerar varje deskriptors ABI, startar en standardruntime, registrerar
alla moduler före all ingångskod, bygger argv i ingångskontexten, anropar
ingångspunkten och översätter resultatet till avslutsstatusen för
körbara filer. Rapporter går till stderr efter
att stdout har tömts; kontexten och runtimen rivs i ordning på varje väg.
CLAUSE_halt_v1 implementerar erlang:halt/0,1 (abort anropar
std::abort).
Uppskjutna tjänster
Dessa rapporterar en rad [feature] notimpl (funktioner) och
ändrar inget tillstånd:
| Gräns | Fel |
|---|---|
TermFactory::port (inga portar ännu, plan step 53), reference(ReferenceIdentity) och function(FunctionIdentity) | TermError::not_implemented |
SchedulerService::run / execute | SchedulerError::not_implemented |
CodeServer::unload | CodeError::not_implemented |
dispatch_builtin på en katalogiserad BIF | Status::not_implemented |
Clause