Семантичний аналіз
Виконується після синтаксичного аналізу в типовій компіляції та в
--print-types; дії, що працюють лише із синтаксисом, його пропускають. Помилки
зупиняють уражений пакет до LLVM; діагностика зберігає походження з макросів і
включень, а наступні вхідні дані все одно діагностуються.
Перевірки модулів і викликів
- Оголошення модуля обов'язкове й унікальне; арність функцій 0..255; без повторних визначень; кожен експорт існує (повторний експорт дає попередження, як в OTP). Імена в лапках і Unicode-імена зберігають свою точну ідентичність.
- Перевіряється кожна функція, включно з невикористаним і недосяжним кодом.
- Виклики розв'язуються в межах пакета за модулем/іменем/арністю. Віддалені виклики (включно з самокваліфікованими) потребують експортів. Відсутні/приватні викликані функції та повторні модулі є помилками.
fun F/Aмає називати функцію модуля (function F/A undefined);fun M:F/Aі виклики значень (F(Args)) не розв'язуються під час компіляції (funs). Кожне окреме значення fun отримує записModule::funsпісля аналізу зв'язувань.- Самостійна, взаємна та міжмодульна рекурсія приймаються. Граф викликів розбивається на сильно зв'язні компоненти (ітеративний алгоритм Тар'яна) у порядку «викликана функція перед викликачем»; компонента рекурсивна, коли має кілька членів або член викликає сам себе.
Поведінки
Слідує
поведінкам (behaviours)
OTP, erl_internal:add_predefined_functions і check_behaviour з erl_lint.
- Модуль, що оголошує
-callback, отримує попередньо визначену в OTP експортовану функціюbehaviour_info/1:callbacksдає оголошені callbacks у порядку вихідного коду,optional_callbacks— ті, що є в коректно сформованих списках-optional_callbacks(callbacks, кваліфіковані модулем, пропускаються). Фронтенд додає її як функцію на рядку-module(driver/predefined). Як і в OTP, власний вихідний код модуля не може її назвати:-export, локальний виклик,fun behaviour_info/1чи-specдля неї є невизначеною функцією; віддалені виклики (також самокваліфіковані) до неї доходять.-callbackпоряд із написаною вручнуbehaviour_info/1є помилкою (cannot define callback attibute for F/A when behaviour_info is defined, написання OTP). -behaviour(M)і-behavior(M)можуть повторюватися.Mрозв'язується в пакеті; ім'я бібліотечного модуля (library.md) додає цей модуль так само, як літеральний виклик. Діагностика використовує текст OTP на імені атрибута:undefined callback function F/A (behaviour 'M')для кожного обов'язкового, не необов'язкового callback, який модуль не експортує (відсортовано);conflicting behaviours -- callback F/A required by both 'M2' and 'M1' (line L, column C), коли попередня поведінка також його вимагає (необов'язковий callback враховується, коли експортований);behaviour M undefined, колиMнемає в пакеті або він не маєbehaviour_info/1(поведінка OTP, якої бібліотека Clause ще не надає, як-отapplication), а також помилки OTP щодо імен модулів для імен, які не можуть бути модулями (the module name must not be empty,... must be an atom, ...).
- Попередження не спричиняють збою компіляції. Параметри
-compilenowarn_behaviours(уся перевірка, включно з помилками імен модулів),nowarn_undefined_behaviour_func,nowarn_undefined_behaviourіnowarn_conflicting_behavioursїх вимикають. - Не обчислюється: написана вручну експортована
behaviour_info/1модуля поведінки, тож callbacks його користувачів не перевіряються, а попередження OTP про погано визначені та застарілі callbacks ніколи не виникають (відмінності). - Докази:
tests/fixtures/lint(діагностика, згенерована OTP, CTestlint_diagnostics),executables_behaviours, випадки CLIsemantic.
Імпорти
Слідує правилам імпорту з erl_lint.
-import(Module, [F/A, ...])робить локальний викликF(...)віддаленим викликомModule:F(...): модуль має бути в пакеті (бібліотечний модуль приєднується до нього, як при літеральному виклику) й експортувати функцію або бути модулем вбудованих функцій (-import(io, [format/2]),-import(erlang, [display/1])). Імпорт має пріоритет над автоімпортованим BIF з тим самим іменем.- Помилки з текстом OTP:
function F/A already imported from M(тоді весь атрибут ігнорується),defining imported function F/A,creating a fun from imported name F/A is not allowed. Імпортована функція в guard є недопустимим викликом у guard (крім guard BIF з erlang). - Попередження
import directive overrides auto-imported BIF F/A -- ..., якщо його не названо вno_auto_importабо не заданоnowarn_bif_clash; Clause знає автоімпортовані BIF, які реалізує. - Докази:
executables_imports, випадки lintimports_conflicts,imports_fun,imports_bif, семантичні випадки CLI.
Попередньо визначені функції
Слідує модулям OTP і
erl_internal:add_predefined_functions. Фронтенд додає module_info/0,1 до
кожного скомпільованого модуля (після згенерованої behaviour_info/1) як
функції Erlang, що повертають літеральні дані (driver/predefined,
semantic/module_info); Module::predefined_ рахує ці кінцеві форми, які
завжди експортовані й не потрапляють у --print-types.
module_info()дає[{module,_},{exports,_},{attributes,_},{compile,_},{md5,_}];module_info(Key)також відповідає наfunctions,nifs([]) іnative(false), а для будь-якого іншого ключа генеруєbadarg.exports: експортовані функції в порядку визначення (escripts додаютьmain/1), потімbehaviour_info/1, якщо згенерована,module_info/0,module_info/1.functions: кожна функція в порядку визначення, потім ті самі попередньо визначені.attributes: кожен атрибут, крімmodule, експортів та імпортів, форм type/spec/callback, records,export_type,optional_callbacks,export_record,compile,file, документації таfeature, у порядку вихідного коду; значення, що не є списком, загортається в список (-tags(a)дає{tags,[a]}). Без-vsnпершим іде{vsn,[N]}, деN— цеmd5, прочитаний як беззнакове ціле число big-endian, як його виводить OTP.md5: MD5 надрукованого вихідного коду модуля (print_source: макроси розгорнуто, включення вбудовано, коментарі й форматування відкинуто).compile:[{version,ClauseVersion},{options,[]},{source,AbsolutePath}].- На відміну від
behaviour_info/1, власний вихідний код модуля їх бачить: локальні виклики таfun module_info/1працюють. Визначення будь-якої з них є помилкою (function module_info/0 already defined); її експорт, як і будь-який повторний експорт, дає лише попередження (function module_info/0 already exported). - Атрибути є даними; лише
on_load,nifsі неінертні параметри-compileвідхиляються як такі, що змінюють поведінку. - Згенеровані функції не мають рядка вихідного коду (OTP їм його не дає): ні
налагоджувальних розташувань, ні коментарів із вихідним кодом в IR, ні записів
--print-types.module_info/0містить ті самі літерали, що йmodule_info/1, тож не робить викликів. - Докази:
executables_module_info(stdout OTP; похідні значення — за формою), випадкиtests/fixtures/lintmodule_info_defined,exports_repeated.
Зв'язування
Кожне зв'язування має ідентичність відносно функції clause[N].local[M].
Входження — це визначення, читання або перевірки точної рівності, позначені
контекстом голови/guard/тіла. Аналіз детермінований.
- Кожна клауза починає з порожнього середовища. Кандидати в голові збирають попередні визначення; guards їх читають; тіло бачить їх лише в разі успіху.
_нічого не зв'язує;_Name— звичайна змінна. Повторене ім'я є обмеженням точної рівності.- Зіставлення в тілі відвідують праву частину перед лівою; ланцюжки
обробляються справа наліво. Зразок у дужках
P1 = P2є псевдонімом, а не послідовністю. - Сусідні вирази читають ті самі вхідні імена; їхні визначення експортуються
після всього виразу.
{X = 1, X}— читання незв'язаної змінної;{X = 4, X = 3}допустимий і завершується збоєм під час виконання. - Визначення в правій частині
andalso/orelseабо всерединіcatch Exprпісля цього небезпечні (OTPvtunsafe); імена, зв'язані доcatch, залишаються придатними. Кожне ім'я, зв'язане всерединіtry, після нього небезпечне; клаузиofбачать імена тіла, клаузи catch бачать їх як небезпечні, а тіло after бачить кожне ім'я, зв'язане раніше в try, як небезпечне. Змінна стека клаузи catch має бути новою (OTPstacktrace_bound), а її guard не повинен її читати (stacktrace_guard).maybeнічого не експортує: кожен?=зв'язує для наступних виразів тіла, клаузиelseбачать імена тіла як небезпечні, і кожне ім'я, зв'язане всередині, після нього небезпечне. Guards ніколи не публікують зв'язувань; зіставлення в guards є помилками навіть тоді, коли недосяжні. - Ключі map читають лише вхідні зв'язування; розміри binary також читають попередні сегменти того самого binary (див. зразки).
- Аналізований вираз
caseзв'язує в охопній області видимості. Кожна клауза починає з цієї області; визначення в її зразку попередні до guard, який лише читає. Імена, зв'язані кожною клаузою, експортуються з однією ідентичністю (пізніші клаузи повторно використовують ідентичність, яку попередня клауза дала цьому імені); імена, зв'язані лише деякими клаузами або небезпечні в будь-якій, після цього небезпечні. Експорти об'єднуються консервативно, як вerl_lintз OTP (icrt_export); попередження OTP, коли пізніший зразок зіставляє експортоване ім'я, не видається. Клаузиifслідують тим самим правилам із guard і без зразка. begin/end— послідовність в охопній області видимості.- Читання незв'язаних, небезпечних і шаблонних (
_) змінних є помилками з розташуванням. Повідомлення зберігають формулювання компілятора (unbound variable X,unsafe variable X); корпус зв'язувань перевіряє кожне щодо класуunbound_var/unsafe_varз OTP. - Comprehension обчислює кожен кваліфікатор по черзі в області видимості, залишеній попередніми. Зразки генераторів зв'язують нові імена, що затінюють зовнішні (читання всередині зразка надають перевагу іменам, які він уже зв'язав); zip-група зв'язує всі свої зразки разом. Шаблони читають як сусідні вирази. Область видимості після comprehension — та сама, що була до неї: її імена там незв'язані.
- Кожна клауза анонімного fun починає з області видимості в місці fun: імена в
голові нові й затінюють зовнішні, guards їх читають, і ніщо зв'язане
всередині не видно після fun. Кожне зовнішнє визначення, прочитане всередині,
записується як захоплення (
Function::captures, у порядку визначення). Ім'я іменованого fun — це ще одне визначення, з якого починає кожна клауза (Function::fun_names); воно ніколи не захоплюється. Змінніfun M:F/Aє читаннями виразу fun (Function::fun_operands).
Обходи ітеративні з бюджетом модуля в 1 000 000 одиниць роботи або 32 на
вираз, якщо це більше (згенерованим парсерам, як-от erl_parse з OTP, потрібно
близько 10 на вираз). Розв'язання служб має бюджет того самого розміру; воно
переглядає форми -compile і -import модуля щодо стану автоімпорту імені один
раз на ім'я, а не один раз на виклик (кожна з тисяч клауз erl_parse викликає
hd/1). Вичерпання або будь-яка семантична помилка очищує таблиці зв'язувань і
нормалізації модуля.
Типи та специфікації
Приватний граф типів представляє всі розібрані форми типів незалежно від
розкладки в runtime: одиничні значення (singletons), діапазони, контейнери, ролі
полів map, добутки функцій і нерозв'язані застосування. Об'єднання сплощуються
й позбуваються дублікатів; term() — верхній елемент, а none() — нижній.
Значення за замовчуванням: 1 048 576 вузлів, 256 членів об'єднання, 200 000
елементів роботи на обхід оголошеного типу (розв'язання, переклад, розгортання)
і 8 000 000 на прохід виведення по всій програмі; налагоджувальне збирання
clau (без NDEBUG) дозволяє 16 000 і 1 000 000, щоб великі програми все одно
швидко компілювалися, і може розширювати більше. Вичерпання розширює до
term() з видимим прапорцем і ніколи не звужує представлення.
Оголошені метадані (-type, -opaque, -nominal, -export_type, -spec,
-callback, -optional_callbacks) слідують довіднику з typespec OTP і
зафіксованій поведінці erl_lint/erl_internal/erl_types:
- Локальні псевдоніми можуть затінювати імена вбудованих типів. Віддалені типи
пакета мають бути експортовані. Відсутні типи, дублікати, некоректні
метадані, одиничні змінні типів, некоректні межі та специфікації для
відсутніх функцій є помилками. Недоступні зовнішні типи дають попередження й
стають
term(). - Кожен псевдонім або перевантаження має власну область видимості змінних; повторені формальні параметри слідують підстановці OTP за останнім аргументом.
- Рекурсивні псевдоніми залишаються скінченними іменованими посиланнями; непрозорі (opaque) тіла розгортаються лише у своєму модулі; номінальні імена зберігаються всюди, і лише перевірка специфікацій читає визначення номінального типу, всередині його модуля.
- Обчислення констант точне, обмежене 10 000 десяткових цифр на значення.
Виведення типів
Виведення типів (inference) відокремлене від оголошених типів і ніколи не довіряє специфікаціям.
-
Вхідні дані експортованих функцій і функцій, які називає
fun F/A, є довільними термами. Функція, в яку входять лише прямими викликами з пакета (не експортована й не названа fun), бере як вхідні дані об'єднання фактів про аргументи в місцях її виклику (semantic/types/inference_inputs), що зіставляються зі зразками голови кожної клаузи; рекурсивні виклики також враховуються. Вхідні дані починаються зnone()(функція, яку ніщо не викликає, ніколи не виконується й виводиться якnone()) і зростають упродовж проходів пакета. Кожне вхідне значення підіймається сходинками залежно від кількості своїх змін: перші 4 зміни об'єднують, наступні 2 розширюють, як рекурсивні результати, наступні 2 також замінюють контейнери всередині контейнера наterm(), а подальша зміна даєterm()(з повідомленням про розширення). Вхідні дані, що не усталилися після 63 проходів, стаютьterm()(з повідомленням про розширення). Рекурсивна компонента вважається витратною, щойно одне розв'язання витратить понад 2^17 об'єднань ґратки (враховуючи вкладені) і понад 1/16 найбільшої кількості об'єднань, витрачених одним проходом дотепер; коли вхідні дані її членів знову змінюються, вони одразу стаютьterm()(з повідомленням про розширення), тож подальші розв'язання більше не слідують за коливаннями вхідних даних її викликачів. -
Проходи після першого обходять лише компоненти, для яких змінилося щось із прочитаного ними (
semantic/types/inference_incremental): компонента зберігає свої підсумки й факти виразів, поки вхідні дані її членів, кожен прочитаний підсумок (виклики,fun F/A,apply) і вхідні дані викликаних функцій, які вона обчислювала повторно, залишаються незмінними, а бюджети проходу й повторного аналізу дали б їй ті самі відповіді (бюджет повторного аналізу витрачено так само, як на початку її останнього обходу, або він знову дозволив би чи відхилив би все); її робота все одно враховується в бюджеті проходу. Результати збігаються з обходом кожної компоненти. Завершальний прохід із вхідними данимиterm()обходить їх усі. Після проходу лише вхідні дані функцій, обійдених у ньому або викликаних функцією, обійденою в ньому, знову об'єднуються з їхніх викликів; факти аргументів і вхідні дані решти залишаються такими, як після останнього об'єднання. -
Літерали мають точні факти: цілі числа будь-якого розміру й символи є одиничними значеннями, атоми — одиничними атомами, числа з плаваючою комою —
float(), рядки — непорожніми списками своїх символів ([]для""), а-/+від літерального числа зберігають його факт. Кортежі зберігають факти своїх елементів; maps, побудовані зі сталими ключами (факт одного значення: ціле число, атом,[]або кортеж чи map із таких), зберігають факт значення кожного ключа, ключ, заданий двічі, зберігає своє останнє значення, а будь-який інший ключ даєmap(). Побудова bitstring рахує свій розмір: літеральні, розмірені та UTF-кодовані літеральні символи — точно, UTF-сегмент іншого значення — за його кодуванням (UTF-8: від 8 до 32 бітів байтами), а сегментbinary/bitstring— за фактом свого значення або будь-яким кратним своєї одиниці (<<1, Rest/binary>>даєnonempty_binary()). Операнд, що ніколи не дає значення (none()), робить побудовуnone(). -
Оператори та вбудовані функції (
semantic/types/inference_operators) обчислюють свій результат із фактів своїх операндів. Одиничні цілі числа згортаються точно до 4 096 бітів на операнд і результат (включно з bignums; більше даєinteger()); інші цілі числа використовують інтервальну арифметику для+,-,*невід'ємних діапазонів,bandз невід'ємним операндом (X band 15дає0..15),remна обмежений дільник (X rem 10дає-9..9),bnot, унарного-іabs/1; операнд float даєfloat(), невідомий —number(), а/завжди даєfloat(). Порівняння згортаються, коли значення операндів можуть порівнюватися лише одним чином (одиничні атоми чи цілі числа, діапазони цілих чисел, що не перетинаються, числа проти не-чисел), і даютьboolean()в іншому разі;and/or/xor/notіandalso/orelseкомбінують істиннісні значення, які можуть мати їхні операнди (правий операндandalsoє результатом, коли лівий —true). Таблиця дає кожній вбудованій функції мосту її результат:pid()дляself/0іspawn/1,3,reference(),port(),non_neg_integer()для розмірів,string(),nonempty_string(),binary(),list(),tuple(),atom(),boolean()для перевірок типів,true/okдля вбудованих функцій із побічними ефектами, повідомлення для!іsend/2;trunc/1і подібні зберігають цілочислові операнди; невідомі вбудовані функції даютьterm(). Операція, що завжди генерує виняток (1 + a,1 div 0,error/1,exit/1,throw/1,halt/0,1), даєnone(), аerlang:raise/3повертає лишеbadarg. Згортання ніколи не змінює згенерований код: переповнення та збої залишаються результатами часу виконання. -
Тіло, чий вираз ніколи не завершується (
none()), даєnone(); так само й виклик із таким аргументом. Шляхи, що генерують винятки, нічого не додають до об'єднання, тож функція, яка завжди генерує виняток, виводиться якnone(). -
Функції тотожності/проєкції зберігають точні відношення з аргументами, які поширюються через вкладені локальні та віддалені виклики зі свіжими змінними для кожного виклику.
-
Контейнери (
semantic/types/inference_containers): список[E1, ..., En | T]об'єднує свої елементи перед фактом хвоста (правильний список, коли хвіст є правильним списком,nonempty_improper_list(H, T), коли хвіст не є списком,nonempty_maybe_improper_list(), коли він невідомий або може бути неправильним: комірка ніколи не порожня, також у зразку на кшталт[H | _]);++,--,hd/1,tl/1,element/2,setelement/3,tuple_to_list/1іmap_get/2читають і перебудовують факти елементів член за членом об'єднання, причому член, що згенерував би виняток, нічого не додає. Оновлення map зберігає точні ключі (:=для відсутнього ключа відкидає цей член) і даєmap()для невідомого map. Records на кортежах є кортежами: побудова заповнює значення за замовчуванням (undefined, якщо його немає), доступ і оновлення читають і встановлюють поле відповідних кортежів, а#r.f— це індекс поля. Спискова comprehension — це, можливо, порожній список фактів її шаблонів, бінарна comprehension — будь-яка кількість копій розміру її шаблону, map comprehension —map(). -
Funs (
semantic/types/inference_funs):fun F/A— fun своєї арності, що повертає виведений результат функції (вбудованої функції, або, зі змінною арністю,fun()),fun M:F/A— fun, що повертаєterm(), а анонімний fun — fun, що повертає об'єднані результати своїх клауз, з урахуванням захоплених значень. Виклик значення об'єднує результати його funs цієї арності, які вибирають його аргументи (term()для невідомого fun,none(), коли жоден член не можна так викликати). Анонімний fun, цілком зв'язаний зі змінною й викликаний через неї, обчислюється знову для цього виклику, а його зразки зіставляються з фактами аргументів, щонайбільше на 4 таких виклики вглиб, і факти його першого обчислення потім відновлюються (Double = fun(Y) -> Y * 2 end, Double(3)дає 6).apply(F, Args)(такожerlang:apply/2) — це викликFз аргументами, які міститьArgs: елементами літерального правильного списку, інакше — елементами його факту ([], список відомих позицій або стільки копій елемента правильного списку, скільки приймає єдина арністьF); зв'язаний анонімний fun обчислюється знову, як вище (apply(fun kind/1, [1])вибираєkind(1)).apply(M, F, Args), деMіF— відомі атоми, що називають експортовану функцію пакета, читає функціональні типи цієї функції, якfun F/A. Аргументи невідомої довжини, інші цілі таM:F(Args)зі змінними іменами залишаютьсяterm(). Оскількиfun F/Aіapply/3можуть називати функцію, виведену пізніше, проходи по пакету також повторюються, доки кожне таке читання не побачить кінцевий результат своєї функції; інакше останній прохід дає їм результатиterm(). -
Присвоєння цілих значень у тілі та псевдоніми копіюють факт правої частини (
Y = 42, Z = Y, id(Z)виводить 42); зразки кортежів, списків, maps і records на кортежах дають своїм змінним факти частин, з якими вони зіставляються, у зіставленнях у тілі, клаузахcase(з аналізованого виразу), клаузахofвиразуtry(зі значення його тіла) і генераторах (з елементів їхніх вхідних даних або ключів і значень map). Недоведені значення залишаютьсяterm()без відношень. -
Звуження (
semantic/types/inference_narrowing,inference_scopes,meet): усередині клаузи функції,case,receiveабо fun кожен зразок перетинається (meet) зі значенням, яке зіставляє (форма літерала, кортежу, списку, record на кортежі, map або bitstring; факт зв'язаної змінної), змінна аналізованого виразуcaseзвужується разом із ним, а guard звужує змінні, які перевіряє. Перевірки типів (is_atom/1доatom(),is_boolean/1,is_integer/1,is_float/1,is_number/1,is_binary/1,is_bitstring/1,is_list/1доmaybe_improper_list(),is_tuple/1,is_map/1,is_function/1,2,is_pid/1,is_port/1,is_reference/1,is_record/2,3до кортежу record,is_map_key/2— свій map доmap(), і старі імена guards) перетинаються з фактом свого аргументу; порівняння з цілочисловими константами (<,=<,>,>=,==,=:=, з будь-якого боку, включно зі згорнутими константами) звужують значення, про яке вже доведено, що воно ціле, до діапазону, причому межі однієї перевірки накопичуються (10 >= X, X >= 0дає0..10); два доведені цілі числа, що порівнюються, звужують одне одного за своїми межами (X > YзYу0..5робитьXщонайменше 1), а/=,=/=з константою зсувають рівну їй межу діапазону всередину. Значення, що може бути float або іншим термом, не звужується. Кон'юнкція застосовує кожну перевірку по черзі, диз'юнкція об'єднує те, що доводить кожна альтернатива,notі хибні перевірки нічого не доводять. Клауза після тієї, чиї зразки — усі прості змінні, а весь guard був однією перевіркою типу, бачить це значення без перевіреної категорії; після одного порівняння з цілочисловою константою те саме значення (проста змінна цієї клаузи), доведене як ціле, бачить порівняння хибним (f(N) when N >= 0 -> ...; f(N) when is_integer(N) -> ...: друга клауза бачитьneg_integer()). Правий операндandalso, клаузаtrueвиразуcase Test ofі те, що йде після фільтра comprehension, бачать перевірку істинною. Порожній перетин робить клаузу неможливою: вона нічого не додає до результату. Звужені факти діють лише всередині своєї клаузи чи операнда;catch, правий операндorelseі кожна клауза відновлюють факти, що були до них. Післяcase,if,receive,tryабоmaybeфакт змінної — це об'єднання її фактів у кінці кожної клаузи, що завершується, тожcase X of forever -> ...; N when is_integer(N), N >= 0 -> ... endзалишаєXякnon_neg_integer() | forever. -
tryіmaybe:try— це об'єднання значень його клаузof(значення його тіла безof) і значень його клауз catch; тілоafterнічого не додає. Клаузиofпочинають з фактів у кінці тіла й зіставляють його значення, як клаузиcase(неможлива клауза нічого не додає); клаузи catch і тілоafterпочинають з фактів доtry, причому зразок класу зіставляється зerror | exit | throw.maybe— це об'єднання значення його тіла, значень його клаузelseі, безelse, значень, на яких можуть не вдатися його зіставлення?=: факт зіставленого значення без форми зразка, коли зразок зіставляє всю свою форму (нові змінні, використані один раз, літеральні атоми й цілі числа,[], кортежі з них), інакше весь факт. Кожен зразок?=перетинається зі своїм значенням і публікує свої змінні для решти тіла;?=, що ніколи не може зіставитися, зупиняє тіло. Клаузиelseпочинають з фактів доmaybeі зіставляють об'єднані невдалі значення, як клаузиcase. Факти післяtryоб'єднують факти в кінці його тіла (безof) і кожної клаузи, що завершується; післяmaybe— факти в кінці його тіла, кожної клаузиelse, що завершується, і, безelse, факти до нього. -
Використання (
semantic/types/inference_uses): операція, що генерує виняток, якщо операнд не має певного типу, доводить цей тип для змінної, яку вона прочитала, після повернення з операції: арифметика й унарні-/+—number(),div,rem, бітові оператори йbnot—integer(),and/or/xor,notі лівий операндandalso/orelse—boolean(),++і--—list(), викликане значення — fun цієї арності, змінне ім'я модуля чи функції —atom(), оновлення map —map(), доступ до record на кортежі чи його оновлення — кортеж record, сегмент binary — свій тип (<<X:8>>:integer()), а його розмір —non_neg_integer(), і по одному рядку на кожну перевірку аргументу вбудованої функції мосту (hd/1,tl/1—nonempty_maybe_improper_list(),length/1—list(),element/2—pos_integer()іtuple(),map_get/2іis_map_key/2—map(),atom_to_list/1—atom(), ...). Операція зберігає свою перевірку під час виконання. Імена, зв'язані з тим самим значенням (Y = X, змінний зразокcaseна змінному аналізованому виразі), звужуються разом. -
Домени входу й успіху: домен входу кожного аргументу — це об'єднання по можливих клаузах функції його факту після голови й guard (звужений факт простої змінної, інакше факт зразка); його домен успіху — об'єднання по клаузах, що завершуються, його факту в місці їхнього нормального повернення.
--print-typesпоказує домен успіху як вхідні дані (bounded(1..10) -> 1..10,inc(X) -> X + 1якinc(number()) -> number()) або домен входу функції, яка ніколи не повертається. Виклик, що повертається, звужує свої змінні аргументи до домену викликаної функції, крім випадку всередині рекурсивної компоненти, яка ще розв'язується. -
Результати клауз об'єднуються консервативно: проєкція зберігається, лише якщо кожна клауза повертає той самий аргумент.
caseабоifоб'єднує результати своїх клауз так само. -
Функціональні типи (
semantic/types/function_types): окрім підсумку-об'єднання вище, функція зберігає по одному функціональному типу на кожну можливу клаузу, як перевантаження-spec: факти аргументів після голови й guard (звужений факт простої змінної, інакше факт зразка) і результат клаузи (none()для клаузи, що завжди генерує виняток; неможливі клаузи не додають жодного). Клауза, чий результат є залежним фактом від її аргументів (caseабоif, що її завершує, або змінна, зв'язана з таким, вкладені на будь-якій глибині), розділяється на один функціональний тип на кожну клаузу цього факту, а факти її параметрів перетинаються з аргументами, які вони називають. Функціональні типи з рівними вхідними даними зливаються (їхні результати об'єднуються); понад 32 останні зливаються в один з об'єднаними вхідними даними й результатами. Рекурсивні компоненти ітерують їх разом із результатами, кожен раунд об'єднуючи (потім розширюючи) результат кожного типу; компонента, що не збігається, зберігає лише свої підсумки-об'єднання. Факт анонімного fun зберігає по одному функціональному типу на кожну можливу клаузу (її зразки й guard над будь-яким аргументом),fun F/A— функціональні типиF/A; funs, що об'єднуються, зберігають свої функціональні типи, лише коли вони рівні, інакше вони об'єднуються в один fun з їхніх об'єднаних результатів із будь-якими вхідними даними (перетин із fun з будь-якими вхідними даними, як це робить використанняF(A), їх зберігає). Спеціалізація, домени й перевірки специфікацій читають підсумок-об'єднання. -
Виклики вибирають функціональні типи: виклик функції з функціональними типами читає по черзі кожен тип, з вхідними даними якого перетинається кожен факт аргументу, і зупиняється після точного (зразки з нових змінних, використаних один раз, літеральних атомів і цілих чисел,
[]і кортежів із них; без guard або лишеtrueі перевірки типів простих змінних-аргументів; для розгалуження — аргумент як аналізований виразcase), вхідні дані якого містять аргументи: жодна пізніша клауза не може бути вибрана. Його результат — об'єднання результатів вибраних типів, причому результат, рівний аргументу, є фактом цього аргументу в межах результату типу; якщо нічого не вибрано, виклик даєnone()(він може лише згенеруватиfunction_clause). Невідомі аргументи вибирають кожен тип, тобто об'єднання; функція без функціональних типів (бюджет, компонента, що не збіглася) використовує свій підсумок-об'єднання. Після повернення з виклику його змінні аргументи звужуються до домену успіху та до об'єднаних вхідних даних вибраних типів. Виклики значень fun так само вибирають функціональні типи факту fun, а анонімний fun, обчислений знову для виклику (див. Funs), входить у свої клаузи, перевіряє їхні guards і виходить із них, якcase, тож клауза, з якою його аргументи не можуть зіставитися, нічого не додає (F = fun(1) -> one; (_) -> other end, F(2)даєother). Рекурсивні компоненти збігаються за результатами й функціональними типами. -
Виклики обчислюють викликану функцію знову: коли факти аргументів виклику лежать у межах вхідних даних викликаної функції й вужчі в одному з них, тіло викликаної функції обчислюється знову з ними як вхідними даними, як зв'язаний анонімний fun, а результат виклику — це результат вибраних функціональних типів, перетнутий з результатом цього обчислення (
two_callers() -> {add_one(10), add_one(20)}дає{11, 21}); аргументи виклику також звужуються до домену успіху цього обчислення. Підсумок викликаної функції, її функціональні типи й записані факти виразів ніколи не змінюються (спеціалізація читає лише їх). Бюджети: 4 вкладені обчислення, викликані функції щонайбільше з 256 виразів, 4 096 одиниць роботи на виклик із пулу в 262 144 на прохід по пакету (окремо від власного бюджету пакета); рекурсивні компоненти ніколи не обчислюються знову. Після вичерпання бюджету виклик зберігає результат вибраних функціональних типів. -
receive— це об'єднання його клауз і тілаafter, яке з тайм-аутомinfinityніколи не виконується. -
Рекурсивна компонента починає результат кожного члена з
none()у своєму першому розв'язанні, а в пізніших проходах — із підсумків, якими завершилося її останнє розв'язання: тоді перший обхід кожного члена замінює його підсумок тим, що знайшов обхід (попередній підсумок до цього слугує лише припущенням для викликів), тож застарілі функціональні типи ніколи не залишаються. Вона повторно виводить члени, доки жоден результат не змінюється; кожен раунд об'єднує новий результат із попереднім (розширює його після перших 8 раундів, домен виведення), а незавершений рекурсивний виклик нічого не додає до об'єднання. Функція, яка ніколи не може повернутися, залишаєтьсяnone(). Компонента, що не збіглася після 8 раундів плюс 4 на кожного члена, або чиї раунди витратили понад восьму частину бюджету проходу, розширює кожного члена доterm()(з повідомленням про розширення, як при вичерпанні бюджету), а завершальний раунд повторно обчислює факти виразів; факти виразів попередніх раундів відкидаються, тож залишаються лише факти з кінцевих припущень. Така компонента запам'ятовується: пізніші проходи одразу дають її членамterm()і виконують лише той завершальний раунд, замість того щоб знову ітерувати до межі. Прохід, що розширює всі вхідні дані доterm(), забуває запам'ятовані компоненти, тож кожна отримує ще одне розв'язання з цими вхідними даними. -
Раунди ніколи не зменшують підсумок члена: його функціональні типи об'єднуються з типами попереднього раунду тип за типом (результати об'єднуються, змінені вхідні дані об'єднуються, і тип більше не точний), або, коли кількість типів змінилася, типи обох раундів зливаються. Як запобіжник, кожен член зберігає свої останні 4 підсумки, і член, що повертається до будь-якого з них, стає
term()без функціональних типів до кінця розв'язання, що дає змогу іншим членам збігтися. -
Раунд обходить лише членів, які читають підсумок, змінений з часу їхнього останнього обходу: кожен обхід відзначає підсумки, які він читає (виклики, результати викликів,
fun F/A), змінений підсумок позначає своїх читачів, а непозначений член зберігає свої факти виразів. Перший раунд обходить кожного члена. Член зберігає свій останній обхід між розв'язаннями: розв'язання повторно бере результат того обходу разом із фактами виразів і роботою, поки прочитані ним члени мають ті самі результати й функціональні типи, а решта прочитаного ним не змінилася й дала б ті самі відповіді, як для збереженої компоненти в інкрементних проходах. Результати збігаються з його обходом. -
Спільний бюджет роботи обмежує виведення; вичерпання втрачає точність і повертається до загального коду, ніколи не відхиляючи програму.
-
Специфікації, що суперечать виведенню, діагностуються (
semantic/types/contracts), але ніколи не стають фактами, тож програма в будь-якому разі поводиться так, як каже її код. Оголошений тип стає фактами, які він містить (або більше): вбудовані типи — за іменем (byte()— це0..255,timeout()—non_neg_integer() | infinity,iodata()іiolist()— списки будь-якої форми), псевдоніми — за своїм визначенням, непрозорі й номінальні типи — за своїм визначенням усередині свого модуля і як будь-який терм поза ним, віддалені типи — через пакет, змінні типів — через свої межіwhen(без обмежень: будь-який терм), maps і records — за своєю категорією. Суперечить лише пара, що не перетинається, оскільки виведені факти можуть містити більше значень, ніж створює код:- домен входу аргументу, що не має спільних значень зі специфікацією, є помилкою;
- виведений результат без спільних значень (якщо він не невідомий і не
none(), що підходить до будь-якого результату) або виклик, факти аргументів якого відхиляє кожне перевантаження, є попередженням (виклик може бути на шляху, який ніколи не виконується); none()/no_return()допускає лише функцію, яка ніколи не повертається.
Діагностика називає функцію, оголошений тип так, як його написано, і виведений. Специфікації
-callbackне перевіряються. Компілятор OTP не перевіряє специфікацій (відмінності).
Залежні факти
semantic/types/dependent. case, if або try ... of поводиться як fun від
змінних, які звужують його клаузи, застосований до них: його значення є
залежним фактом, по одному функціональному типу на кожну можливу клаузу.
- Параметри: змінні, зв'язані до конструкції, які читають її аналізований вираз
(останній вираз тіла
try) і guards, у порядку вихідного коду (змінні, які зв'язують зразки її клауз, нові в кожній клаузі й не є параметрами); щонайбільше 4, звуження пізніших забувається. Кожна можлива клауза зберігає факти параметрів після свого зразка й guard та своє значення (none()для клаузи, що ніколи не завершується); вона точна, як функціональний тип під час вибору виклику, колиcaseабоtryзіставляє параметр або кортеж параметрів із точним зразком і guard, або колиifмає точний guard. - Аналізований вираз, що є кортежем змінних, звужує кожну змінну до її елемента
зіставленого значення, а змінна зразка-кортежу зв'язується з тим самим
значенням, що й елемент аналізованого виразу на її позиції
(
case {X, Y} of {A, b} when is_integer(A) -> ...звужуєXіY). try ... ofзалежить від своїх клаузof; його клаузи catch можуть іти за будь-яким значенням його тіла, тож їхні об'єднані значення об'єднуються зі значенням кожної клаузи (try X of 1 -> one; _ -> other catch _:_ -> error endдає$try_of_operator(X :: 1) -> error | one; (X :: _) -> error | other).receiveіmaybe ... elseзалишаються об'єднанням своїх клауз: їхні клаузи зіставляють повідомлення або невдале значення?=, яке ніщо поза ними не може назвати, тож жодне читання не могло б вибрати серед них.- Клауза, чиє значення саме є залежним (вкладений
caseабоif, або читання залежної змінної), вносить по одному функціональному типу на кожну свою клаузу над об'єднанням обох списків параметрів, з перетнутими вхідними даними. Типи, у які ніколи не можна ввійти (вхідні даніnone()або в межах вхідних даних попереднього точного типу), відкидаються, так само як і параметри, чиї факти однакові в кожному типі; рівні вхідні дані зливаються, і понад 32 типи останні зливаються, як для функціональних типів. Конструкція, чиї решта типів дають те саме значення або яка не звужує жодного параметра, є своїм простим об'єднанням. - Кожен інший споживач читає стертий факт — об'єднання значень клауз. Факти, що виходять за межі обійденої функції (результати, функціональні типи, факти funs), стираються; об'єднання зберігають залежність, лише коли обидві сторони мають однакову.
- Змінна, зв'язана із залежним значенням, його зберігає. Читання змінної
вибирає клаузи, у які входять поточні факти її параметрів (у порядку вибору
виклику: точна клауза, вхідні дані якої їх містять, зупиняє вибір) і чиї
значення перетинаються з власним звуженим фактом змінної, а факт читання — це
об'єднання їхніх значень
(
Y = case X of 1 -> one; _ -> other end, case X of 1 -> Y endчитаєone). - Змінна, яку зв'язує кожна клауза
caseабоif, є об'єднанням її фактів у кінці клауз, що завершуються, залежним від параметрів конструкції так само, як її значення:case X of 1 -> Y = 5; _ -> Y = 6 end, Yвиводить(1) -> 5; (_) -> 6. Те саме об'єднання застосовується післяreceive,tryіmaybe, без залежності. - Оператори, вбудовані функції, побудова кортежів, списків, maps і records та
виклики функцій пакета (їхні функціональні типи вибираються, як під час
вибору виклику, аргументи повторно не звужуються) із залежними операндами
обчислюються один раз для кожної комбінації клауз операндів, зі значеннями
цих клауз замість фактів операндів і перетнутими вхідними даними: читання
однієї змінної беруть ту саму клаузу, інші операнди комбінуються,
щонайбільше 16 комбінацій (понад це використання є простим об'єднанням).
R + 1зR = case X of a -> 1; b -> 2 endвиводить(a) -> 2; (b) -> 3. - Звуження залежної змінної (зразок, guard, використання) також звужує її
параметри до об'єднання вхідних даних тих клауз, у які ввійдено і чиї
значення перетинаються зі звуженим фактом, а також їхні параметри по черзі, на
4 рівні вглиб: після
R = case X of a -> 1; b -> 2 endвиразcase R of 1 -> X endчитаєXякa. - Залежний результат клаузи функції розділяє її (див. Функціональні типи):
параметри, що називають аргумент, або ім'я, зв'язане з його значенням,
перетинаються з його вхідними даними; інші відкидаються (такий тип точний,
лише якщо їхні вхідні дані — будь-який терм).
nested_case(X, L)зcase X of 1 -> case L of spanish -> uno; _ -> one end; _ -> other endвиводить(1, spanish) -> uno; (1, _) -> one; (_, _) -> other. --print-typesпозначає залежне значення як функціональний тип, названий за його конструкцією, з кожним вхідним значенням після імені його параметра:case X of 1 -> one; _ -> other endзакінчується наend. % $case_of_operator(X :: 1) -> one; (X :: _) -> other;ifдрукує$if_operator,try ... of—$try_of_operator.
Домен виведення
semantic/types/lattice. Факт — це множина значень, які може мати змінна чи
результат. Факти об'єднуються там, де сходиться потік керування (клаузи,
гілки), і розширюються між раундами рекурсивної компоненти; кожен бюджет нижче
коректно розширюється до більшої множини й ніколи не відхиляє програму. Факти
друкуються як типи Erlang, категорії — за своїми вбудованими іменами.
| Факт | Друк | Об'єднання | Бюджет і розширення |
|---|---|---|---|
| Ніщо | none() | Нейтральний елемент | Функція, яка ніколи не повертається, залишається none() |
| Будь-що | term() | Поглинає кожен факт | dynamic() і any() — це term() |
| Цілі числа | 42, 1 | 3 | 7 | Об'єднання одиничних значень | Понад 8 одиничних значень стають своїм діапазоном |
| Діапазон цілих чисел | 1..10, 0..255 | Найменший діапазон, що містить обидва | Межа, що зсунулася між раундами, переходить до наступного порогу: нижня — до 1, потім 0, потім без обмеження; верхня — до -1, потім без обмеження. Межі всередині контейнерів також розширюються там, де попередній факт має кортеж (того самого розміру й тега), map (із тими самими ключами) або список у тому самому місці |
| Необмежені цілі числа | pos_integer() (1 і більше), non_neg_integer() (0 і більше), neg_integer() (-1 і менше), integer() | Найменший інтервал, що містить обидва, друкується своєю категорією | — |
| Числа з плаваючою комою | float() | — | — |
| Числа | number() | Діапазон або категорія цілих чисел, об'єднані з float() | Одиничні цілі числа з float() залишаються 1 | float() |
| Атоми | ok, error | ok, boolean() | Об'єднання одиничних значень; рівно false і true друкуються як boolean() | Понад 8 одиничних значень стають atom() |
| Ідентифікатори | pid(), port(), reference() | — | — |
| Кортежі | {ok, 1}, tuple(), #point{x :: 0, y :: _} | Кортежі однакового розміру, чиї перші елементи не є двома різними атомами (їхнім тегом), об'єднуються поелементно; інші залишаються окремими членами | Понад 16 елементів стають tuple(), якщо не кожен елемент відомий; понад 8 окремих форм стають tuple(). Кортеж з іменем і розміром видимого record на кортежі друкується як record |
| Списки | [], [T], [T, ...], nonempty_improper_list(H, T), [1, a], [a, b | T] | Елементи об'єднуються; [] з непорожнім списком дає, можливо, порожній; неправильні списки об'єднують голови й хвости. Списки з двох або більше відомих елементів зберігають свої позиції (нотація Clause, мова типів такої не має, елементи друкуються через | у дужках): позиційні списки однієї довжини об'єднуються позиція за позицією, інакше вони об'єднуються як прості списки | Список із 0..1114111 (char()) друкується як string() або nonempty_string(); можливо, порожній список із _ друкується як list() |
| Maps | #{}, #{a := 1}, #{1..17 => a}, map() | Maps з однаковими ключами об'єднуються значення за значенням; maps з іншими ключами об'єднуються в одну асоціацію своїх об'єднаних ключів і значень (=>: будь-який ключ може бути відсутнім) | Понад 16 ключів об'єднуються в одну асоціацію |
| Funs | fun((term()) -> 1), fun() | Funs однієї арності об'єднують свої результати; інші арності дають fun() | — |
| Bitstrings | <<_:16>>, <<_:3, _:_*2>>, binary() | Менший розмір плюс кожна різниця розмірів як одиниця | Основа й одиниця 0/8, 8/8, 0/1, 1/1 друкуються як binary(), nonempty_binary(), bitstring(), nonempty_bitstring() |
- Об'єднання зберігає по одному члену на кожну об'єднану форму, у порядку термів
Erlang їхніх значень: числа, атоми,
reference(), funs,port(),pid(), кортежі, maps,[], списки, bitstrings, потім оголошені іменовані типи; понад 8 членів стаютьterm(). - Контейнери вкладаються щонайбільше на 4 рівні; факт, глибший за це, стає
term(). - Об'єднання відкидає точний член-кортеж, який містить інший точний кортеж того самого розміру (з двох, що містять один одного, залишається попередній), тож одна множина значень має одне об'єднання, а рекурсії по деревах усталюються.
- Рекурсивні компоненти об'єднуються впродовж 8 раундів (цикли до 8 функцій збігаються точно), потім розширюються (виведення типів).
- Звуження (
Lattice::meet) — це перетин фактів, значення, які містять обидва (або більше, алеnone()лише тоді, коли спільних немає). Порожній перетин означає, що шлях не може виконатися. - Факти — це вузли графа, які ніколи не змінюються, тож граф зберігає відповідь
кожного об'єднання двох фактів, перетину й перевірки включення (
within) до кінця пакета: майже всі вони повторюються. Збережена відповідь усе одно враховує виконані нею об'єднання для відмови з урахуванням вартості, тож результати від неї не залежать; відповідь, обчислена, коли бюджет вузлів розширив факт, не зберігається.
На -O2 зі спеціалізацією типів пониження використовує ці факти для
вилучення перевірок (спеціалізація); специфікації
ніколи не стають фактами.
Кеш виведення
Модулі в каталогах кешу виведення можуть почати
компіляцію з результатами, збереженими попередньою компіляцією: підсумками,
локальними вхідними даними та фактами виразів своїх функцій. infer тримає їх
незмінними в кожному проході, ніколи не обходить компоненту, всі члени якої їх
повторно використовують, і обходить кожну іншу функцію як зазвичай, читаючи
збережені підсумки, як і будь-які інші.
- Від чого залежать факти модуля. Експортовані функції та функції, які
називає
fun F/A, починають зterm(); вхідні дані кожної іншої функції надходять від її викликачів, які перебувають у тому самому модулі; факт називає функцію іншого модуля лише через атом: віддалений виклик,fun M:F/Aабоapply/2,3на факті-атомі (і читає розкладку record іншого модуля лише через native record, який він кваліфікує або імпортує), а кожен факт-атом походить з атома, який модуль записує, або з факту модуля, якого він досягає. Отже, факти модуля залежать лише від його власного коду й коду модулів пакета, які називають його атоми, транзитивно. Ключ його збережених результатів покриває саме це: ідентичністьclau, відбиток модуля (його вихідний код, кожен файл, який він включає, параметри препроцесингу та відповідь parse transform), попередньо визначені форми, статус escript і бібліотеки, а також те саме для кожного модуля пакета, якого досягають його атоми. Модуль, досягнутий у такий спосіб, що не має відбитка, робить модуль некешованим. - Живість. Живість експортів лише вилучає функції, а вилучений викликач лише
звужує вхідні дані функцій, які він викликав: факти, обчислені з більшою
кількістю живих функцій, залишаються дійсними. Збережені результати повторно
використовуються, коли вони містять кожну функцію, живу зараз
(
results do not fitв іншому разі); пропущені функції нічого не отримують. - Змішування. Збережені та свіжі результати можуть зустрітися в одному пакеті: факти кожного модуля дійсні для кожного запуску програми, хоч би що казали факти іншого модуля, тож свіжий модуль, що читає збережений підсумок (або навпаки), залишається коректним. Рекурсивна компонента з членами обох видів обходиться повністю.
- Точність. Повторно використані результати — це результати компіляції, яка їх зберегла. Результати свіжого модуля все ж можуть відрізнятися від компіляції без кешу: бюджет проходу більше не платить за повторно використані модулі, а ідентичності графа, за якими впорядковано кілька членів об'єднання, нумеруються інакше.
--print-types
Друкує кожен модуль пакета (у порядку вхідних даних/цілей, бібліотечні модулі після них) як вихідний код Erlang (друк вихідного коду) з тим, що знайшло виведення типів. Вивід іде в stdout і призначений для читання людиною, це не Erlang і не формат обміну. Попередження залишаються в stderr.
%% module "branches" source="branches.erl" target="" declared=complete inferred=complete
-module(branches).
-export([mixed/1]).
%% inferred: mixed(1) -> 1; (_) -> 2
mixed(X) ->
case X of
1 ->
1;
_ ->
2
end. % $case_of_operator(X :: 1) -> 1; (X :: _) -> 2
- Рядок
%% moduleназиває модуль, його вихідний код, ціль проєкту та чи завершилися оголошені й виведені типи, чи їх було розширено через ліміт. - Оголошення (
-type,-spec,-callback, records) з'являються так, як написано. - Над кожною функцією
%% declared: f(Inputs) -> Resultдає кожне перевантаження її-specу розв'язаному вигляді (з обмеженнямиwhen), а%% inferred: f(Inputs) -> Result— те, що знайшло виведення, тож їх можна порівняти. Вхідні дані — це домен успіху кожного аргументу (відterm()для експортованих функцій і тих, які називаєfun F/A, від об'єднаних аргументів викликачів для інших функцій). Функція з кількома функціональними типами друкує по одній сигнатурі на тип, кожну зі своїми вхідними даними:f(integer()) -> integer(); (atom()) -> string()(semantic::types::function_source).-specзалишається з функцією одразу після нього, відокремлений від інших форм порожнім рядком. - Найзовнішніший вираз кожного рядка (вираз тіла або аналізований вираз
case), чий факт каже більше, ніжterm(), позначається завершальним коментарем% Typeпісля пунктуації рядка (відомий тип приховує відношення з аргументом, яке показується лише для значення, про яке більше нічого не відомо): щонайбільше одна позначка на рядок, жодної для вкладених у нього виразів, тож вивід залишається коректним Erlang. Вираз тіла на кількох рядках несе свою позначку на останньому рядку (end, % ...). Літеральні терми (літерали, а також кортежі, списки, побудовані maps і bitstrings з літералів) не позначаються; зіставлення показує своє значення. Залежне значення друкується як функціональний тип своєї конструкції (залежні факти). - Значення, про яке виведення довело, що воно дорівнює одному з аргументів
функції, і про яке більше нічого не відомо, друкується як ім'я цього
аргументу, змінна типу: змінна, яку дає йому перша клауза, що зв'язує весь
аргумент, інакше
_argumentN(з 1, також коли попередній аргумент узяв це ім'я). Вхідні дані аргументу показують те саме ім'я, коли це будь-який терм:second(_, Y) -> Y,keep(Acc, number()) -> Acc. Змінна показує лише свій тип, її ім'я вже каже, яким аргументом вона є.
Очікування виведення
tests/fixtures/inference/*.erl записують, що виведення має знайти для кожної
функції і що воно знаходить сьогодні. Кожен модуль стає CTest
inference_<module> (tests/compiler/inference/expectations.py):
%% expect: sum() -> 3
%% today: sum() -> _
sum() -> 1 + 2.
expect:— це сигнатура, яку--print-typesмає надрукувати у своєму рядку%% inferred:;today:, присутній, поки виведення не дотягує, — та, яку він друкує зараз.- Перевірка порівнює вивід із
today, якщо він є, інакше зexpect; кожна функція модуля потребує рядкаexpect. Рядокtoday, який виведення вже наздогнало, провалює перевірку, доки його не вилучено. expectations.py <clau> <fixture> --recordпереписує рядкиtodayз поточного виводу (дія супровідника: перегляньте diff).values.erlпокриває літеральні та побудовані значення, результати операторів і вбудованих функцій, контейнери та їхні частини, funs та їхні виклики, локальні вхідні дані від викликачів, об'єднання й діапазони цілих чисел, відношення з аргументами та значенняtry/maybe;narrowing.erl— перевірки типів, guards, зразки, діапазони порівнянь, диз'юнкції та звуження, які не повинні просочуватися;base_types.erl— по одній функції на кожен базовий і вбудований тип мови типів, чия-specмає розв'язатися і чиє тіло створює таке значення;clauses.erl— функціональні типи, вибір виклику й обчислення для кожного виклику в межах і за межами їхніх бюджетів;dependent.erl— залежні факти;trees.erlіparsers.erl— рекурсії по деревах і форми згенерованих парсерів;widening.erl— лічильники всередині контейнерів, які розширюються й усталюються.tests/fixtures/inference/contracts/містить перевірки специфікацій (CTestinference_contracts).tests/fixtures/inference/budgets/walkers.erl— прохід компілятора у звичній для Erlang формі (великий record стану, що передається між взаємно рекурсивними функціями обходу, результати яких — об'єднання багатьох record). CTestinference_walkers(tests/compiler/inference/walkers.py) утримує кількість його проходів, кроків роботи, об'єднань ґратки та розв'язань, раундів і обходів членів групи функцій обходу в межах бюджетів приблизно на чверть вище за поточні показники, а також запускає програму.
Друк типів
semantic::types::type_source(graph, type) (semantic/types/printing) подає
тип із графа типів у синтаксисі типів Erlang: _ для будь-якого терму
(term(), записується через TERM_SOURCE для стислості; синтаксис типів
читає _ як any()), none(), атоми й цілі числа, 1..5, {ok, T},
tuple(), [T], [T, ...], #{K => V, K := V}, #r{f :: T},
<<_:B, _:_*U>>, fun((A) -> R), A | B. Fun з кількома функціональними
типами друкує їх у нотації Clause, fun((1) -> one; (_) -> other): синтаксис типів Erlang не має перевантаженого типу fun, а
об'єднання типів fun означає інше. Цілі числа об'єднання друкуються в порядку
значень там, де стоїть його перше ціле число, послідовні — як діапазон
(1 | 2 | 3 | 5 друкується як 1..3 | 5; факт зберігає одиничні значення).
Попередньо визначені типи erlang втрачають свій модуль; посилання на
оголошені типи залишаються іменованими. Бюджет вузлів обмежує текст; понад
нього, а також глибше за 32 рівні вкладеності, замість тексту стоїть ....
clau --print-types answer.erl client.erl
clau --print-types --project project.toml --target demo --verbose
Clause