Clause
← Уся документація

Англійський оригінал змінився після перекладу, тож частина тексту може бути застарілою. Читати англійською

Перекладено з англійського оригіналу · 1e3a39c8-dirty · 2026-10-26

ABI згенерованого коду (ревізія 21)

Приватний контракт між виходом компілятора та runtime. Це внутрішній для проєкту C++23, несумісний із BEAM і не загальний FFI. Об'єктні файли та runtime мають походити з одного збирання; старіші ревізії дескрипторів відхиляються до використання.

Заголовки: v1.hpp (типи термів/контексту/функцій), term.hpp (кодек безпосередніх цілих чисел), status.hpp, builtins.hpp, startup.hpp (запуск програми).

Терми

Терм — це одне беззнакове слово завширшки з вказівник цільової платформи (32 або 64 біти). Розміщення визначаються з налаштованої цілі LLVM, тож код для іншої цілі використовує її ширини.

Функції та символи

Сигнатура точки входу (нативна угода про виклики 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, на яку посилаються інші модулі пакета, дескриптори fun у приватній таблиці <prefix>.funs (fun), закодовані літерали модуля та кеш літералів (області), записувані кеші атомів і fun, які згенерований код читає inline, і code_convention своїх тіл. Реєстрація викликає CLAUSE_register_module_v4(Runtime*), яка перевіряє версію/ширину та всі експорти, інтернує атоми, будує заморожений реєстр і публікує його разом з образом коду в одній транзакції. Дублікати модулів ніколи не замінюють код; будь-який збій нічого не публікує (уже інтерновані атоми залишаються в обмеженій таблиці).

Канал збоїв (ревізія 2)

Помилки не кодуються в бітах терма. Після кожного нехвостового згенерованого виклику викликач перевіряє канал перед використанням результату чи обчисленням наступного аргументу: він читає слово failed у FrameState контексту, яке runtime підтримує рівним 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} із власним корисним навантаженням
Comprehensionerror:{bad_generator, Tail}, error:{bad_filter, Value}, error:{bad_generators, Inputs} (ErrorReason 16-18); відхилення строгого генератора — {badmatch, Element}
Доступ до record, хибні аргументи, арифметика, mapbadrecord, badarg, badarith, badmap/badkey
Відсутнє поле native recordErrorReason::badfield (20), корисне навантаження {{Module, Name}, Field}
Виклик значення (F(Args))ErrorReason::badfun (22, корисне навантаження — саме значення), badarity (23, корисне навантаження {Fun, Args}), undef (24), записуються CLAUSE_apply_v1 (fun)
Динамічні виклики (M:F(Args), apply/2,3, fun M:F/A зі змінними)badarg для модуля чи функції, що не є атомом, неправильного списку аргументів або недійсної арності; undef, коли жоден модуль програми не експортує функцію; badfun/badarity, як вище (fun)
Зовнішнє створення 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/3raised_error/raised_exit/raised_throw: клас визначається ідентифікатором, власне корисне навантаження — уся причина
erlang:halt/0,1CallError::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 звичайними зразками та guard; пропущений клас відповідає 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), коли жодна не підходить.

Comprehension використовують наявні сервіси з кількома операціями: накопичені елементи обертаються конструкцією reverse (ContainerConstruction::reverse = 2, значення {List, Tail}), яка будує результат за одне резервування купи; генератори списків перевіряють і читають cons-комірки inline; binary comprehension об'єднує свої частини за допомогою BitOperation::concat ({List}), а map comprehension будує свій map за допомогою MapOperation::from_list ({Pairs}, пізніші ключі перемагають). Генератори map один раз беруть MapOperation::entries (11, {Map}): комірку з ключами та значеннями в словах 1 + 2i і 2 + 2i у канонічному порядку ключів (сам класичний map або копію записів map поза купою у формі кортежу, яку читає лише згенерований код) і читають кожен запис inline; key_at/value_at ({Map, Position}) лишаються для інших викликачів. Залишок вхідних даних zip показується за допомогою MapOperation::iterator — ланцюжка OTP {K, V, Next}, що закінчується none. MapOperation::update_owned (10) приймає операнди update для оновлення map, джерело якого компілятор довів як більше не прочитане: map-джерело поза купою оновлюється на місці, а його заглушка лишається спожитою (порожньою). Генератори 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, усі видимі лише в термі стека:

Фрейми та передавання керування

Згенеровані функції виконуються на явних фреймах (модель виконання; сервіси в frames.hpp). Кожна функція має дескриптор <symbol>.frame (FrameDescriptor: дескриптор модуля, слоти атомів модуля та функції, арність, код тіла, кількість слотів, кількість слотів термів; зовнішній для експортованих функцій) і внутрішнє <symbol>.body типу abi::v1::Code, void(void *context, TermWord x0, ...) з abi::v1::register_arguments параметрами-термами; з runtime, зібраним Clang, на x86-64 і AArch64 тіла та кожне передавання керування використовують угоду preserve_none, як там робить abi::v1::Code: тіла ніколи не повертаються звичайним шляхом, тож не зберігають регістри, що зберігаються викликаною функцією (callee-saved); реєстрація відхиляє модуль, чия code_convention (0 немає, 1 C, 2 preserve_none) не збігається з угодою runtime. Експортований <symbol> зберігає сигнатуру TermWord(Context *, const TermWord *) як хостова точка входу, що викликає CLAUSE_invoke_v1(context, frame, arguments).

Сервіси runtime

Згенерований код викликає перевірені сервіси C++: CLAUSE_exact_v1 (точна рівність), CLAUSE_immediate_v1 (предикати/запити безпосередніх значень), CLAUSE_construct_v1, CLAUSE_inspect_v1 (для інших споживачів; згенерований код перевіряє й читає комірки кортежів і cons inline), 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 для цільової платформи.

Inline-форми (codegen/lowering_inline.cpp): перевірки типів (is_atom, is_integer, is_number, is_boolean, is_tuple, is_list, is_bitstring, is_float, is_map, is_pid, is_port, is_reference, is_function/1, нативний is_record/1, is_binary/1 за словом бітової довжини) читають теги, а після перевірки boxed-тега — вид заголовка (abi::v1::HeaderKind); вони ніколи не викликають сервіс. Порівняння двох безпосередніх значень (==, /=) або двох малих цілих (<, =<, >, >=), element/2, tuple_size/1, bit_size/1 і byte_size/1 (зі слова бітової довжини, поки розмір є малим цілим), hd/1, tl/1, булеві оператори, band, bor, bxor, bsr, bsl (поки результат залишається малим) і малі div/rem виконуються inline; self/0 читає FrameState::self. Вони виконуються inline, коли операнди це дозволяють, інакше викликають CLAUSE_immediate_v1, що зберігає всі помилки й випадки великих цілих. setelement/3 з малим індексом у межах boxed-кортежу копіює кортеж у вільні слова блоку купи й замінює слот inline; інакше або за браку місця входить у вбудовану функцію мосту.

CLAUSE_builtin_frame_v1(context, builtin) (builtins.hpp) повертає FrameDescriptor робочої вбудованої функції з індексом builtin у abi::v1::bridge_builtins (лише доповнюваний); згенерований код входить у нього як у функцію з аргументами в регістрах, а помилки та збої йдуть у перевірений канал (вбудовані функції). FrameDescriptor з null-тілом є вбудованою функцією: вхід у нього виконує вбудовану функцію на регістрах і повертається до викликача або призупиняє процес на продовженні вбудованої функції, коли вона виконала trap (порції).

Пряма вбудована функція (BuiltinCall::direct: чисті перетворення, як-от integer_to_list/1, atom_to_list/1, list_to_tuple/1, setelement/3, phash2/1) ніколи не призупиняється, не очікує й не діє на інший процес. Згенерований код викликає її через CLAUSE_builtin_call_v1(context, builtin, arguments), що одразу виконує тіло й повертає результат без передачі фрейму, точки відновлення та вхідної точки безпеки; витрачається одна редукція, а помилка йде до перевіреного каналу, як помилка сервісу. Швидка вбудована функція (BuiltinCall::quick: length/1, binary_to_list/1, list_to_binary/1, iolist_to_binary/1) спершу викликається так само; її обмежений швидкий шлях повертає 0, коли відмовляється (довші вхідні дані, інші форми, будь-яка помилка й завжди під час трасування викликів), після чого згенерований код входить у її фрейм, як для будь-якої вбудованої функції мосту.

abi::v1::dispatch_builtin викликає зареєстровані хостом вбудовані функції за байтами модуля/функції, масивом аргументів і арністю, повертаючи Status; вихідні дані записуються лише в разі успіху.

StatusЗначенняОпис
ok0Успіх
not_implemented1Відома відкладена можливість, повідомляється один раз
invalid_argument2Недійсні вхідні дані
diagnostic_failure3Не вдалося доставити звіт
out_of_memory4Виділення пам'яті не вдалося, стан відкочено
busy5Живі контексти або диспетчеризація, що виконується
wrong_owner6Об'єкт належить іншому runtime/контексту
resource_limit7Обмеження або бюджет вичерпано
stopped8Власника вимкнено
abi_mismatch9Відрізняється версія або ширина терма
internal_error10Неочікуваний збій локалізовано
unknown_builtin11Сигнатуру не зареєстровано і немає в каталозі
erlang_error12Записано структуровану помилку Erlang
output_failure13Стандартне виведення відхилило запис

Ревізії

РевізіяЗміна
1Початкові дескриптори та точки входу
2Перевірений канал збоїв
3Написання атомів і слоти в дескрипторах
4Обов'язкові згенеровані кореневі області
5Явні фрейми процесів і передавання керування
6Дескриптори native records у дескрипторах модулів, CLAUSE_record_v1
7Дескриптори fun у дескрипторах модулів, 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
9Кожен контекст починається з abi::v1::FrameState; inline-передавання керування
10Початкові регістри X передаються як параметри коду
11Фіксовані слова false і true
12Inline-поля купи heap_top/heap_limit у FrameState
13Фіксовані undefined, ok, error; ModuleDescriptor::atom_cache і FrameState::runtime
14Області літералів модулів: ModuleDescriptor::literals, literal_words
15FrameState::self, слово pid контексту
16Code використовує preserve_none на x86-64 і AArch64 з runtime, зібраним Clang; ModuleDescriptor::code_convention
17Шість регістрів X як параметри Code
18Комірки sub_binary (HeaderKind::sub_binary, sub_*_word) і inline-видобування Rest
19Слова кінця та межі дописування в комірках refc_binary (refc_append_*_word, 8 слів корисного навантаження) і inline-дописування
20MapOperation::entries та inline-генератори map
21ModuleDescriptor::fun_cache та inline-замикання
22Прямі вбудовані функції: BuiltinName::direct, CLAUSE_builtin_call_v1
23Швидкі вбудовані функції: BuiltinName::call (BuiltinCall), 0 від CLAUSE_builtin_call_v1 означає відмову
24RecordDescriptor::definition (список полів як байти External Term Format, для records:get_definition/2) і ModuleDescriptor::features (слоти атомів увімкнених можливостей, для erl_features:used/1)
25Закодовані літерали модуля та купа літералів: ModuleDescriptor::literals містить закодовані байти (literal_bytes), literal_cache, literal_count; CLAUSE_literal_v1