ABI згенерованого коду (ревізія 4)
Приватний контракт між виходом компілятора та runtime. Це внутрішній для проєкту C++23, несумісний із BEAM і не загальний FFI. Об'єктні файли та runtime мають походити з однієї збірки; старіші ревізії дескрипторів відхиляються до використання.
Заголовки: v1.hpp (типи термів/контексту/ функцій), term.hpp (кодек безпосередніх цілих чисел), status.hpp, builtins.hpp, startup.hpp (запуск програми).
Терми
Терм — це одне беззнакове слово завширшки з вказівник цільової платформи (32 або 64 біти). Розміщення визначаються з налаштованої цілі LLVM, тож код для іншої цілі використовує її ширини.
- Малі цілі числа: молодші чотири біти
0xf; знакове корисне навантаження (payload) розміромword_bits - 4бітів, діапазон[-2^(word_bits-5), 2^(word_bits-5)-1]. Більші значення є bignum у купі. - Атоми: молодші шість бітів
0x0b, корисне навантаження не перевикористовується і діє в межах усього процесу ОС. Ідентифікатори ніколи не серіалізуються і не використовуються для впорядкування. - Порожній кортеж — точно
0x2b, nil — точно0x3b; інші біти корисного навантаження недійсні. - Локальні pid: молодші чотири біти
0x3, корисне навантаження — номер процесу; допускаються лише тоді, коли їх видав runtime (pid і посилання). - Упаковані (boxed) слова та слова списків вказують у купу процесу-власника й допускаються лише після того, як runtime доведе володіння (див. терми).
Функції та символи
Сигнатура точки входу (нативна угода про виклики C, C-компонування не потрібне):
TermWord function(ProcessContext *context, const TermWord *arguments);
Аргументи — позичений, вирівняний за словом масив у порядку вихідного коду (null при арності 0). Контекст живий і передається через виклики без змін. Повернене слово можна використовувати лише після перевірки каналу збоїв.
Символи: clausev1_<hex module>_<hex function>_<arity>, шістнадцятковий код
байтів UTF-8 у нижньому регістрі, канонічна десяткова арність; оборотні та
незалежні від хоста. Експортовані точки входу зовнішні, інші — внутрішні.
Реєстрація модулів
Кожен модуль генерує clausev1_<hex module>__0.descriptor і .register.
Дескриптор містить версію ABI, ширину терма, таблицю експорту (ім'я, арність,
хостова точка входу та FrameDescriptor, у який входять динамічні виклики),
написання атомів (пари вказівник/розмір UTF-8), дескриптори native records
(слоти атомів модуля, імені та полів, прапорець експорту;
native records) у зовнішній таблиці
<prefix>.records, на яку посилаються інші модулі пакета, і дескриптори funs у
приватній таблиці <prefix>.funs (funs). Реєстрація
викликає CLAUSE_register_module_v4(Runtime*), яка перевіряє версію/ширину та
всі експорти, інтернує атоми, будує заморожений реєстр і публікує його разом з
образом коду в одній транзакції. Дублікати модулів ніколи не замінюють код;
будь-який збій нічого не публікує (уже інтерновані атоми залишаються в
обмеженій таблиці).
.registerслід викликати явно до розв'язання експортів. Глобальних конструкторів немає; користувачі статичних архівів мають посилатися на точки входу реєстрації.llvm.usedутримує дескриптори та точки входу; компонування без runtime завершується збоєм через відсутній сервісний символ.- Адреса дескриптора є ключем прив'язки атомів; її образ має залишатися відображеним у пам'ять протягом життя модуля. Кожен runtime має власні прив'язки для того самого образу.
- Вирази-атоми читають слоти через
CLAUSE_atom_v3; вони ніколи не інтернують. - Стартовий об'єктний файл (
clausev1_start) перелічує кожен дескриптор уStartupDescriptor, а його нативнаmainвикликаєCLAUSE_main_v1(argc, argv, descriptor), яка реєструє всі модулі та запускає точку входу (виконувані файли).
Канал збоїв (ревізія 2)
Помилки не кодуються в бітах терма. Після кожного нехвостового згенерованого
виклику викликач перевіряє CLAUSE_call_failed_v2(context) перед
використанням результату чи обчисленням наступного аргументу. У разі збою
викликана функція повертає недійсне нульове слово.
| Результат | Передавання |
|---|---|
| Незбіг зі зразком, відхилення guard | Продовження до наступного кандидата; канал не змінюється |
| Вичерпано клаузи | error:function_clause; error:{case_clause, Value} із власним корисним навантаженням для case; error:if_clause; error:{try_clause, Value} для клауз of у try; error:{else_clause, Value} для клауз else у maybe |
| Збій зіставлення в тілі | error:{badmatch, Value} із власним корисним навантаженням |
| Comprehensions | error:{bad_generator, Tail}, error:{bad_filter, Value}, error:{bad_generators, Inputs} (ErrorReason 16-18); відхилення строгого генератора — {badmatch, Element} |
| Доступ до record, хибні аргументи, арифметика, maps | badrecord, badarg, badarith, badmap/badkey |
| Відсутнє поле native record | ErrorReason::badfield (20), корисне навантаження {{Module, Name}, Field} |
Виклик значення (F(Args)) | ErrorReason::badfun (22, корисне навантаження — саме значення), badarity (23, корисне навантаження {Fun, Args}), undef (24), записуються CLAUSE_apply_v1 (funs) |
Динамічні виклики (M:F(Args), apply/2,3, fun M:F/A зі змінними) | badarg для модуля чи функції, що не є атомом, неправильного списку аргументів або недійсної арності; undef, коли жоден модуль програми не експортує функцію; badfun/badarity, як вище (funs) |
| Зовнішнє створення native record без значення | ErrorReason::novalue (21), корисне навантаження {{Module, Name}, Field} |
| Цілочисловий результат понад обмеження розміру | Результат сервісу ValueOutcome::system_limit (3): guard відхиляє, тіло генерує error:system_limit (ErrorReason 19) |
| Недійсний лівий операнд лінивого оператора | {badarg, Value} |
| Інфраструктура (OOM, обмеження, володіння, внутрішні) | CallError::runtime_failure з точним Status |
erlang:error/1,2,3, exit/1, throw/1, erlang:raise/3 | raised_error/raised_exit/raised_throw: клас визначається ідентифікатором, власне корисне навантаження — уся причина |
erlang:halt/0,1 | CallError::halted з halt_status (і гаслом) |
Причини — це типізовані ідентифікатори, записані CLAUSE_raise_v2; три
ідентифікатори raised_* вибирають клас exit або throw (інакше error) і
несуть будь-який терм як причину. error/2,3 генерують виняток через
CLAUSE_error_v1(context, reason, args), яка також зберігає список args для
верхнього фрейму стека. Перемагає перший збій; вкладені виклики спільно
використовують канал. GeneratedInvocation — це хостова область: вона
перевіряє незавершені збої перед входом і після повернення, копіює результат
або помилку та очищає канал лише на найзовнішньому виході (також при винятках
C++). Жоден виняток не перетинає згенеровані точки входу. Ті, хто викликає
сирі точки входу, мають відкрити GeneratedInvocation; звичайні хости
використовують ResolvedFunction::call.
catch Expr перенаправляє кожну перевірку збою та генерування винятку
всередині Expr до блоку-обробника, який викликає
CLAUSE_catch_v1(context, slot). Для незавершеного винятку Erlang вона
записує значення catch у кореневий слот і очищає канал: кинутий терм,
{'EXIT', Reason} для exit або {'EXIT', {Reason, []}} для error (типізовані
причини стають своїми термами OTP, як-от {badmatch, V}; стек описано
нижче). Зупинки (halt) та інфраструктурні збої залишаються
незавершеними, а власна перевірка обробника переходить до обробника, що
охоплює, або до виходу з функції. Зв'язування, зроблені всередині Expr,
після цього небезпечні, тож точка злиття об'єднує лише значення.
try Body of ... catch ... end так само захищає лише Body. Його обробник
викликає CLAUSE_exception_v2(context, class_slot, reason_slot, stack_slot),
яка записує атом класу (error, exit або throw), причину та терм
трасування стека в кореневі слоти й очищає канал (зупинки та інфраструктурні
збої залишаються незавершеними, як і для catch). Потім клаузи catch
зіставляють Class:Reason звичайними зразками та guards; пропущений клас
відповідає throw, а іменована змінна стека зв'язується з термом стека. Коли
жодна клауза не підходить, CLAUSE_reraise_v2(context, class, reason, stack) знову записує виняток із причиною raised_* і тим самим стеком, що
звітується та перехоплюється точно так само, як оригінал.
Клаузи of вибирають за значенням тіла та генерують {try_clause, Value}
(ErrorReason::try_clause = 14); винятки всередині клауз of та обробників
переходять до обробника, що охоплює.
maybe не потребує сервісу: кожен ?= — це звичайне зіставлення, чия гілка
незбігу виходить із тіла до виходу maybe з незіставленим (уже вкоріненим)
значенням. Без else це значення є результатом; інакше клаузи else
вибирають за ним, як клаузи case, і генерують {else_clause, Value}
(ErrorReason::else_clause = 15), коли жодна не підходить.
Comprehensions використовують наявні сервіси з кількома операціями: накопичені
елементи обертаються конструкцією reverse
(ContainerConstruction::reverse = 2, значення {List, Tail}); binary
comprehension об'єднує свої частини за допомогою BitOperation::concat
({List}), а map comprehension будує свій map за допомогою
MapOperation::from_list ({Pairs}, пізніші ключі перемагають). Генератори
map читають MapOperation::key_at/value_at ({Map, Position} у канонічному
порядку ключів) і показують залишок вхідних даних zip за допомогою
MapOperation::iterator — ланцюжка OTP {K, V, Next}, що закінчується none.
Генератори bitstring використовують звичайне видобування за зразком плюс
фінальний сегмент binary/all для решти.
try ... after A end додає другий захист навколо тіла та всіх клауз of і
catch. На звичайному шляху A виконується після того, як вибране значення
вкорінено, а значення A відкидається. Обробник after отримує виняток через
CLAUSE_exception_v2, виконує другу копію A і повторно генерує виняток через
CLAUSE_reraise_v2; виняток чи збій усередині A натомість виходить через
обробник, що охоплює, замінюючи оригінал. Зупинки та інфраструктурні збої
пропускають A. Кореневі слоти належать фрейму функції, тож кожен шлях
звільняє їх на виході з функції.
Трасування стека
Кожен кореневий фрейм називає свою згенеровану функцію приватним
abi::v1::FrameDescriptor (дескриптор модуля, слоти атомів імені модуля та
функції, арність). Коли записується виняток Erlang, канал копіює 8
найвнутрішніших іменованих фреймів (типове значення BEAM backtrace_depth);
терм [{Module, Function, Arity, []}, ...] будується лише тоді, коли його
запитує обробник, catch або звіт. Верхній фрейм показує список аргументів
error/2,3 замість арності, коли цей аргумент є списком.
erlang:raise(Class, Reason, Stack) (CLAUSE_reraise_v2) приймає стеки, які
приймає BEAM: правильний список із {M, F, A} (доповнених розташуванням [])
або {M, F, A, Location} з атомами M, F і списком Location, обрізаний
до 8 елементів; після цього стек зберігається як заданий, і фрейми більше не
захоплюються. Недійсний клас чи стек нічого не записує, і виклик обчислюється
в badarg, як в OTP. erlang:get_stacktrace/0 відхиляється з текстом lint
OTP 29 про видалення ("removed").
Відмінності від OTP, усі видимі лише в терміні стека:
- Розташування завжди
[](OTP додає{file, F},{line, L}), а параметриerror/3не додаютьerror_info. - Верхній фрейм
function_clauseпоказує арність, а не список аргументів. - Жоден фрейм не називає BIF чи оператор, що зазнав збою (OTP додає
{erlang, '+', Args, [{error_info, ...}]}), і нічого нижче функції точки входу не з'являється. - Хвостовий виклик звільняє фрейм викликача (step 19), тож, як і в OTP, викликач, що завершився хвостовим викликом, відсутній у трасуванні. OTP також перетворює виклики функцій, які ніколи не повертаються, на хвостові виклики; Clause цього не робить.
- Елементи стека
{Fun, Args}дляerlang:raise/3досі відхиляються.
Фрейми та передавання керування
Згенеровані функції виконуються на явних фреймах
(модель виконання, step 19; сервіси в
frames.hpp). Кожна функція має
дескриптор <symbol>.frame (FrameDescriptor: дескриптор модуля, слоти
атомів модуля та функції, арність, код тіла, кількість слотів, кількість
слотів термів; зовнішній для експортованих функцій) і внутрішнє
<symbol>.body типу void(void *context). Експортований <symbol> зберігає
сигнатуру TermWord(Context *, const TermWord *) як хостова точка входу, що
викликає CLAUSE_invoke_v1(context, frame, arguments).
- Тіло читає заголовок свого фрейму (
CLAUSE_frame_v1) і регістри (CLAUSE_registers_v1) при вході та робить switch за словом відновлення в заголовку. Воно виходить лише через викликиmusttailкоду, який повертаютьCLAUSE_enter_v1(виклик),CLAUSE_tail_v1(хвостовий виклик) абоCLAUSE_return_v1(повернення). - Аргументи займають перші слоти фрейму; кожне обчислене значення зберігається
в слоті терма перед наступним виразом чи викликом. Невдалі кандидати
очищають свої слоти. Значення, потрібні тілу після виклику або точки
безпеки (safepoint) на початку циклу, вивантажуються: терми — у слоти термів
(враховуються в
rootsдескриптора), інші слова — у сирі слоти після них; сирі слоти не є коренями. - Результат передається в регістрі 0; корисні навантаження помилок є
кореневими словами каналу (BEAM
fvalue). Збій повертається до викликача як результат, і кожен викликач перевіряє канал після виклику; слово обробника в заголовку залишається 0. - Стек за замовчуванням не має обмеження; заштовхування, для якого хост не
може виділити пам'ять, записує
out_of_memory, а заштовхування понад необов'язкове обмеження на процесStackOptions::limit_wordsзаписуєresource_limit(інфраструктурні збої). - Виклик функціонального значення передає його аргументи, а після них
захоплені значення fun, у регістрах;
CLAUSE_apply_v1повертаєFrameDescriptor, у який входить передавання керування (funs). - Точки безпеки:
CLAUSE_enter_v1/CLAUSE_tail_v1виконують збирання перед заштовхуванням фрейму викликаної функції (її аргументи є кореневими регістрами), аCLAUSE_safepoint_v1(context)на початку кожного циклу comprehension виконує збирання на місці, коли цього вимагає купа. Кожен інший сервіс є критичною секцією, яка ніколи не переміщує купу (збирання в згенерованому коді).
Сервіси runtime
Згенерований код викликає перевірені сервіси C++: CLAUSE_exact_v1 (точна
рівність), CLAUSE_immediate_v1 (предикати/запити безпосередніх значень),
CLAUSE_construct_v1, CLAUSE_inspect_v1, CLAUSE_integer_v1,
CLAUSE_float_v1, CLAUSE_map_v1, CLAUSE_bits_v1,
CLAUSE_record_v1 (створення/читання/оновлення/зіставлення/перевірка native
record під RecordCheck; результати bad_record, bad_field, no_match),
CLAUSE_make_fun_v1 (побудова fun з FunDescriptor). Кожен повертає успіх,
семантичну помилку (badarg/badarith/...) або інфраструктурний збій і
записує вихідні дані лише в разі успіху. CLAUSE_display_v1
(output.hpp) друкує один рядок
erlang:display/1 і дає true; семантичної помилки в нього немає.
CLAUSE_halt_v1 ніколи не завершується успішно: вона записує запит на
зупинку (CallError::halted з кодом завершення) або badarg, тож викликач
розгортає стек. Імена для компонувальника відповідають декоруванню C++
Itanium або Microsoft для цільової платформи.
CLAUSE_builtin_frame_v1(context, builtin)
(builtins.hpp) повертає
FrameDescriptor робочої вбудованої функції з індексом builtin у
abi::v1::bridge_builtins (лише доповнюваний); згенерований код входить у
нього як у функцію з аргументами в регістрах, а помилки та збої йдуть у
перевірений канал (вбудовані функції). FrameDescriptor з
null-тілом є вбудованою функцією: вхід у нього виконує вбудовану функцію на
регістрах і повертається до викликача або призупиняє процес на продовженні
вбудованої функції, коли вона виконала trap (порції).
abi::v1::dispatch_builtin викликає зареєстровані хостом вбудовані функції
за байтами модуля/функції, масивом аргументів і арністю, повертаючи Status;
вихідні дані записуються лише в разі успіху.
| Status | Значення | Опис |
|---|---|---|
ok | 0 | Успіх |
not_implemented | 1 | Відома відкладена можливість, повідомляється один раз |
invalid_argument | 2 | Недійсні вхідні дані |
diagnostic_failure | 3 | Не вдалося доставити звіт |
out_of_memory | 4 | Виділення пам'яті не вдалося, стан відкочено |
busy | 5 | Живі контексти або диспетчеризація, що виконується |
wrong_owner | 6 | Об'єкт належить іншому runtime/контексту |
resource_limit | 7 | Обмеження або бюджет вичерпано |
stopped | 8 | Власника вимкнено |
abi_mismatch | 9 | Відрізняється версія або ширина терма |
internal_error | 10 | Неочікуваний збій локалізовано |
unknown_builtin | 11 | Сигнатуру не зареєстровано і немає в каталозі |
erlang_error | 12 | Записано структуровану помилку Erlang |
output_failure | 13 | Стандартне виведення відхилило запис |
Ревізії
| Ревізія | Зміна |
|---|---|
| 1 | Початкові дескриптори та точки входу |
| 2 | Перевірений канал збоїв |
| 3 | Написання атомів і слоти в дескрипторах |
| 4 | Обов'язкові згенеровані кореневі області |
| 5 | Явні фрейми процесів і передавання керування |
| 6 | Дескриптори native records у дескрипторах модулів, CLAUSE_record_v1 |
| 7 | Дескриптори funs у дескрипторах модулів, CLAUSE_make_fun_v1, CLAUSE_apply_v1 |
| 8 | Дескриптори експорту називають свій FrameDescriptor; сервіси динамічних викликів CLAUSE_call_v1, CLAUSE_apply_list_v1, CLAUSE_call_list_v1, CLAUSE_make_external_fun_v1 |
Clause