ABI згенерованого коду (ревізія 21)
Приватний контракт між виходом компілятора та 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, корисне навантаження не перевикористовується і діє в межах усього процесу ОС. Ідентифікатори ніколи не серіалізуються і не використовуються для впорядкування.false,true,undefined,okіerrorфіксовані: корисні навантаження 0–4 у кожному runtime. - Порожній кортеж — точно
0x2b, nil — точно0x3b; інші біти корисного навантаження недійсні. - Локальні pid: молодші чотири біти
0x3, корисне навантаження — номер процесу; допускаються лише тоді, коли їх видав runtime (pid і посилання). - Упаковані (boxed) слова та слова списків вказують у купу процесу-власника й допускаються лише після того, як runtime доведе володіння (див. терми).
- Первинні теги: список
0x1, упакований0x2. Заголовок зберігає свій вид (kind) з біта 2 і кількість наступних слів з біта 7 (header_count_shift); вид кортежу — 0. Згенерований код читає елементи кортежів і cons-комірки inline лише після того, як виведення типів або домінантна перевірка форми довела комірку (доведення); плани зіставлення перевіряють форми кортежів і cons inline на кожному рівні оптимізації (тег boxed, потім заголовок), а точна рівність спершу порівнює слова, викликаючиCLAUSE_exact_v1лише для двох різних слів списків або упакованих слів. 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, на яку посилаються інші модулі пакета, дескриптори fun у
приватній таблиці <prefix>.funs (fun), закодовані
літерали модуля та кеш літералів (області), записувані кеші
атомів і fun, які згенерований код читає inline, і code_convention своїх
тіл. Реєстрація викликає CLAUSE_register_module_v4(Runtime*), яка перевіряє
версію/ширину та всі експорти, інтернує атоми, будує заморожений реєстр і
публікує його разом з образом коду в одній транзакції. Дублікати модулів
ніколи не замінюють код; будь-який збій нічого не публікує (уже інтерновані
атоми залишаються в обмеженій таблиці).
.registerслід викликати явно до розв'язання експортів. Глобальних конструкторів немає; користувачі статичних архівів мають посилатися на точки входу реєстрації.llvm.usedутримує дескриптори та точки входу; компонування без runtime завершується збоєм через відсутній сервісний символ.- Адреса дескриптора є ключем прив'язки атомів; її образ має залишатися відображеним у пам'ять протягом життя модуля. Кожен runtime має власні прив'язки для того самого образу.
- Вирази-атоми, окрім фіксованих атомів, читають
atom_cache, колиFrameState::runtimeконтексту володіє кешем, інакшеCLAUSE_atom_v3; вони ніколи не інтернують (атоми). - Складені літерали — це
literal_countтермів уliterals(literal_bytesбайтів,abi/literals.hpp): компактне кодування термів BEAM (базові тегиu,i,a: кількості, цілі числа довільного розміру в доповняльному коді з порядком байтів big-endian, атоми як слот атома модуля + 1, де 0 означає[]) і розширені теги Clause для списків (довжина, елементи, хвіст), кортежів, чисел із рухомою комою (binary64), bitstring (кількість бітів, байти), map і байтових рядків. Реєстрація декодує їх у купу літералів runtime; runtime, що отримав право на кеш, заповнюєliteral_cache(одне слово на літерал), який згенерований код читає після перевірки цього права, а код інших runtime викликаєCLAUSE_literal_v1(context, index, descriptor)(області). - Стартовий об'єктний файл (
clausev1_start) перелічує кожен дескриптор уStartupDescriptor, а його нативнаmainвикликаєCLAUSE_main_v1(argc, argv, descriptor), яка реєструє всі модулі та запускає точку входу (виконувані файли).
Канал збоїв (ревізія 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} із власним корисним навантаженням |
| Comprehension | error:{bad_generator, Tail}, error:{bad_filter, Value}, error:{bad_generators, Inputs} (ErrorReason 16-18); відхилення строгого генератора — {badmatch, Element} |
| Доступ до record, хибні аргументи, арифметика, map | 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 (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/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 звичайними зразками та 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, усі видимі лише в термі стека:
- Розташування завжди
[](OTP додає{file, F},{line, L}), а параметриerror/3не додаютьerror_info. - Верхній фрейм
function_clauseпоказує арність, а не список аргументів. - Жоден фрейм не називає BIF чи оператор, що зазнав збою (OTP додає
{erlang, '+', Args, [{error_info, ...}]}), і нічого нижче функції точки входу не з'являється. - Хвостовий виклик звільняє фрейм викликача, тож, як і в OTP, викликач, що завершився хвостовим викликом, відсутній у трасуванні. OTP також перетворює виклики функцій, які ніколи не повертаються, на хвостові виклики; Clause цього не робить.
- Елементи стека
{Fun, Args}дляerlang:raise/3відхиляються.
Фрейми та передавання керування
Згенеровані функції виконуються на явних фреймах
(модель виконання; сервіси в
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).
- Кожен контекст починається з
abi::v1::FrameState: блок стека, вершина, межа заштовхування, зміщення поточного фрейму, залишок редукцій, вказівник на регістри, кількість живих регістрів, повільні біти, слово failed, вільні слова блоку купи[heap_top, heap_limit), ідентичність runtime, за яким закріплено кеші атомів, слово pid контексту (self), по одному слову цілі кожне. Тіло при вході читає з нього заголовок свого фрейму (stack + frame) і регістри та робить switch за словом відновлення в заголовку. - Двійкові зразки: перевірка початку, перевірка кінця і цілочислові сегменти
сталої ширини до розміру слова мінус 8 бітів (56 на 64-бітних цілях, 24 на
32-бітних) читають комірку inline (
abi::v1::heap_binary_*_word,refc_*_word:data_представлення поза купою — це перший байт його буфера) на будь-якій бітовій позиції. Сегмент із цілих байтів на вирівняній за байтом позиції потребує одного завантаження; для інших читаються байти, яких торкається поле, і поле виділяється зсувом. Ширини, що не становлять цілих байтів, обробляються inline лише для big-endian. Інші сегменти викликаютьCLAUSE_bits_v1. Суб'єктsub_binary(sub_*_word) читається через свій оригінал. ЗавершальнийRest/binaryабоRest/bitsз одиницею 1 виділяється inline зсувом вказівника як 4-слівна коміркаsub_binaryоригіналу. - Двійкові цикли: коли жоден заголовок клаузи функції не порівнює один із
параметрів зі зв'язаним значенням, а якась клауза передає його залишок
назад як цей параметр власного хвостового виклику, функція отримує
приватний варіант
<symbol>.loop. Власний крок функції створює комірку залишку, як описано вище, і входить із нею у варіант (залишок, створений сервісом, який може бути спільним, натомість знову входить у функцію). Варіант не створює комірки залишку: кожен власний хвостовий виклик записує нове зміщення й довжину в бітах у комірку параметра, на яку посилається лише цей цикл, і передає ту саму комірку далі, тож цикл над binary виділяє пам'ять один раз. Варіант читає представлення комірки один раз на крок, перед своїми клаузами, і пропускає перевірку параметра на bitstring. Завантаження й записи стану процесу, фреймів, слівsub_binaryі слів binary у купі та поза нею мають неперетинні теги LLVM TBAA; варіант, що не викликає інших функцій Erlang, позначає завантаження оригіналу своєї комірки та заголовка, зміщення й адреси даних оригіналу як!invariant.loadі отримує ще один GVN після конвеєра (атрибут функціїclause-binary-loop), тож представлення виноситься за межі циклу, а зміщення й довжина, записані на одній ітерації, стають регістровими значеннями наступної. Будь-яке інше читання залишку в тілі або всього параметра у варіанті, а також кожен fun, що захоплює одне з них, отримує нову коміркуsub_binaryз тими самими бітами (сервіс створює її навіть без місця в купі), тож ніщо інше не утримує комірку циклу; guard читають саме значення. У трасуваннях стека варіант має ім'я та арність функції. - Цикли з числами з рухомою комою (64-бітні цілі): параметр серед перших
шести (які передаються в регістрах) є параметром із рухомою комою, якщо
кожна клауза зв'язує його зі змінною (або ігнорує), якийсь власний хвостовий
виклик передає йому вираз лише з результатом із рухомою комою (літерал
такого числа,
/, викликmathабо+ - *з таким операндом), а його значення читається лише як операнд арифметики, порівнянь чи викликівmath, як той самий параметр власного хвостового виклику або в результаті чи хвостовому виклику іншої функції (ніколи у fun). Функція отримує приватний варіант<symbol>.float, що приймає кожен такий параметр як біти його double у слові аргументу (атрибути функціїclause-float-parameters, бітова маска, іclause-float-generic, функція). Власний хвостовий виклик будь-якої з них, у якому нове значення кожного такого параметра є double у регістрі, входить у варіант із цими бітами, тож цикл не створює комірки на кожному кроці; варіант створює комірку параметра лише там, де читає саме слово. Сирі біти ніколи не потрапляють туди, де читає збирач сміття: інші власні хвостові виклики варіанта знову входять у функцію, а передавання керування у варіант через сервісний шлях (запит на збирання, профілювання, вичерпані редукції, розширення стека) пакує double за допомогою функції модуляclause.float.boxі натомість входить у дескриптор функції. У трасуваннях стека варіант має ім'я та арність функції. - Побудова binary, кожен байт якої відомий під час компіляції (малі цілі константи і рядкові літерали сталої ширини в цілих байтах, щонайбільше 64 байти), є inline-коміркою binary в області літералів модуля, як і інші складені літерали.
Left ++ Right, деLeftзаписано як правильний список (рядковий літерал або список без хвоста), будується як[E1, ..., En | Right], як це робить компілятор OTP, без викликуerlang:'++'/2. Будь-який іншийLeft ++ RightобходитьLeftinline (codegen/lowering_append): правильний список щонайбільше з 32 комірок копіюється наRightу вільні слова блоку купи; довший чи неправильнийLeftабо блок без місця входить у вбудовану функцію, яка зберігає кожну помилку й порцію.- Чиста вбудована функція з літеральними аргументами обчислюється під час
компіляції, як це робить
sys_core_foldв OTP:atom_to_list(Atom)для атомного літерала — це рядковий літерал його імені, без виклику. integer_to_list(N)для малого цілого числа записує комірки його цифр у вільні слова блоку купи, аlist_to_integer(L)для правильного списку цифр після необов'язкового знака мінус, досить короткого для малого цілого числа (17 цифр на 64-бітних цілях, 8 на 32-бітних), читає його на місці (codegen/lowering_integer_text). Будь-який інший аргумент або блок без місця входить у вбудовану функцію, яка генерує всі помилки. Як лівий операнд++,integer_to_list(N)перевіряєNна своєму місці, а дописування записує цифри безпосередньо наRight, без копіювання.erlang:make_tuple(N, V)з малимNу межах 1..64 виділяє кортеж зсувом вказівника і заповнює його черезllvm.experimental.memset.pattern, де в купі є місце; інакше виконується вбудована функція.- Побудови binary (64-бітні цілі), усі сегменти яких — цілі числа сталої
ширини в цілих байтах до 64 бітів, загалом щонайбільше 64 байти,
виділяються inline зсувом вказівника як комірки
heap_binary(заголовок, довжина в бітах, обнулені слова даних, потім байти кожного сегмента), коли кожне значення — мале ціле і в купі є місце; інакше їх будуєCLAUSE_bits_v1. Ті самі сегменти після накопичувача з цілого binary (<<Acc/binary, ...>>) записуються в буфер накопичувача, коли він єrefc_binary(абоsub_binaryтакого), що закінчується, вирівняно за байтом, наrefc_append_end_wordкомірки з місцем нижчеrefc_append_limit_word: байти йдуть за кінець, кінець просувається, а результат — 4-слівнийsub_binaryцієї комірки. + - * /(64-бітні цілі) з операндом-float і/для будь-яких двох чисел читають кожен операнд як double (мале ціле перетворюється, значення комірки float читається через select її комірки або сталої комірки-замінника, тож жодне завантаження не залежить від розгалуження), обчислюють у регістрах і виділяють зсувом вказівника 2-слівну комірку float, коли результат скінченний, дільник ненульовий і в купі є місце; інакшеCLAUSE_immediate_v1генеруєbadarithабо підвищує тип. Літерал числа з рухомою комою, а також результат/, inline-обчислення math або операції з таким операндом (завжди число з рухомою комою за успішного виконання) також зберігає свій double у регістрі: операції, що читають його, беруть регістр, а комірка створюється лише там, де щось читає саме слово (CLAUSE_float_v1, коли в купі немає місця); резервний сервісний шлях, що читає його, отримує власну комірку. Порівняння двох таких чисел порівнює double, точна рівність — їхні біти (тож0.0 =:= -0.0лишається false).- Вирази map без бази, ключі яких — атомні літерали або літерали малих цілих
(щонайбільше 32 різні), записуються inline як класичні комірки map, з
ключами, відсортованими під час компіляції в точному порядку термів, і
останнім значенням повтореного ключа; інакше їх будує
CLAUSE_map_v1. - Шаблони map (
#{K := V},#{}),map_get/2,is_map_key/2,map_size/1та оновлення (M#{K := V},M#{K => V}, щонайбільше 8 полів) читають комірку класичної map inline, коли кожен ключ — атом або мале ціле число, для яких рівність термів є рівністю слів: сканують слова ключів, потім слово значення або копіюють комірку з новими значеннями, коли вона вже містить усі ключі й у купі є місце. Відсутній ключ одразу відхиляє зразок; дерева map, інші ключі, відсутній ключ в інших випадках і помилки обробляєCLAUSE_map_v1(пошук у малих map). - Кожен такий пошук літерального ключа (атома або малого цілого числа,
записаного у вихідному коді) має власний inline-кеш, внутрішню глобальну
змінну
[2 x word], спільну для всіх процесів: індекс слова останнього знайденого ключа (спочатку 1) і кількість перезаписів, що залишилися (8). Пошук спершу порівнює слово ключа за кешованим індексом, тож map зі звичною для цього місця структурою потребує одного порівняння; інакше він сканує і, поки залишаються перезаписи, зберігає знайдений індекс. Обидва слова — атомарні зі слабким упорядкуванням; місце, що вичерпало перезаписи, лише читає, тож місця з багатьма структурами в багатьох процесах не спричиняють перекидання рядка кешу. Змінні ключі (map як словники) лише скануються (inline-кеші структури map). - Кортежі та списки щонайбільше з 64 слів виділяються inline зсувом
вказівника в
[heap_top, heap_limit)(заголовок або cons-комірки записуються на місці,heap_topпросувається), інакше їх будуєCLAUSE_construct_v1; результат вкорінюється, як вихід сервісу. Runtime враховує inline-слова перед кожною операцією з купою і скидає обидва поля після неї; вони null, поки блоку купи немає або діє бюджет купи на процес, тож тоді кожне виділення доходить до сервісу (купа).CLAUSE_frame_v1іCLAUSE_registers_v1лишаються для інших споживачів. - Тіло виходить лише через виклики
musttail. Виклики, хвостові виклики і повернення виконуються inline, коли можуть, інакше код даютьCLAUSE_enter_v1(виклик),CLAUSE_tail_v1(хвостовий виклик) абоCLAUSE_return_v1(повернення), які підтримують стан актуальним (модель виконання). - Кожне передавання керування передає перші
abi::v1::register_arguments(6) регістрів X як параметри C після контексту: початкові аргументи виклику, результат повернення (якx0тіла викликача), нуль для невикористаних. Подальші аргументи йдуть у масиві регістрів. Навколо кожного сервісу передавання керування згенерований код зберігає параметри в масив (сервіси та збирач сміття читають їх там) і завантажує їх назад після нього. Runtime викликає код так само (ProcessStack::run_code), а тіло його нижнього фрейму зберігаєx0у перший регістр для хоста. - Вхід обнуляє фрейм і копіює у слоти лише аргументи після параметрів; тіло
тримає аргумент-параметр у слоті лише там, де
lower_framesвивантажує його через виклик або точку безпеки (safepoint). Обчислені значення не зберігаються завчасно: пониження позначає обчислені терми (метаданіclause.term), аlower_framesдає слот терма лише значенню, що перетинає виклик або точку безпеки; масиви аргументів сервісів досі проходять через кореневі слоти. Невдалі кандидати очищають свої слоти. Значення, потрібні тілу після виклику або точки безпеки на початку циклу, вивантажуються: терми — у слоти термів (враховуються вrootsдескриптора), інші слова — у сирі слоти після них; сирі слоти не є коренями. - Результат передається як
x0; корисні навантаження помилок є кореневими словами каналу (BEAMfvalue). Збій повертається до викликача як результат, і кожен викликач перевіряє канал після виклику; слово обробника в заголовку залишається 0. - Стек за замовчуванням не має обмеження; заштовхування, для якого хост не
може виділити пам'ять, записує
out_of_memory, а заштовхування понад необов'язкове обмеження на процесStackOptions::limit_wordsзаписуєresource_limit(інфраструктурні збої). - Виклик функціонального значення передає його аргументи, а після них
захоплені значення fun, у регістрах;
CLAUSE_apply_v1повертаєFrameDescriptor, у який входить передавання керування (fun). - Точки безпеки:
CLAUSE_enter_v1/CLAUSE_tail_v1виконують збирання перед заштовхуванням фрейму викликаної функції (її аргументи є кореневими регістрами); купа встановлює бітslow_collectщоразу, коли потребує збирання, тож inline-входи ніколи його не пропускають. АCLAUSE_safepoint_v1(context)на початку кожного циклу comprehension виконує збирання на місці; початок циклу викликає її лише тоді, колиFrameState::slowмаєslow_collect, потім витрачає одну редукцію і, коли їх не лишилося, входить уerlang:self/0(вхід призупиняє процес), тож довгі цикли без викликів ділять планувальник. Кожен інший сервіс є критичною секцією, яка ніколи не переміщує купу (збирання в згенерованому коді).
Сервіси 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 | Значення | Опис |
|---|---|---|
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 | Дескриптори 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 |
| 12 | Inline-поля купи heap_top/heap_limit у FrameState |
| 13 | Фіксовані undefined, ok, error; ModuleDescriptor::atom_cache і FrameState::runtime |
| 14 | Області літералів модулів: ModuleDescriptor::literals, literal_words |
| 15 | FrameState::self, слово pid контексту |
| 16 | Code використовує 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-дописування |
| 20 | MapOperation::entries та inline-генератори map |
| 21 | ModuleDescriptor::fun_cache та inline-замикання |
| 22 | Прямі вбудовані функції: BuiltinName::direct, CLAUSE_builtin_call_v1 |
| 23 | Швидкі вбудовані функції: BuiltinName::call (BuiltinCall), 0 від CLAUSE_builtin_call_v1 означає відмову |
| 24 | RecordDescriptor::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 |
Clause