Обработка на директиви на препроцесора в Objective-C

Swiftify е уеб услуга за конвертиране на Objective-C източници в Swift. В момента услугата поддържа обработка както на отделни файлове, така и на цели проекти. По този начин може да спести време на разработчиците, които искат да научат нов език от Apple.

Codebeat е автоматизирана система за изчисляване на показателите на кода и анализиране на различни езици за програмиране, включително Objective-C.

Съдържание

Директивите на препроцесора се обработват по време на анализиране на кода. Няма да описваме основните понятия за анализиране, но тук ще използваме термините от статията за теорията и анализирането на изходния код с помощта на ANTLR и Roslyn. И двете услуги използват ANTLR като генератор на анализатор, а самите граматики на Objective-C са публикувани в официалното хранилище на граматиката на ANTLR (Objective-C граматика).

Идентифицирахме два начина за обработка на директиви на предпроцесора:

  • едноетапна обработка;
  • двуетапна обработка.

Обработка в една стъпка

Директивните токени обикновено започват със знак за паунд (# или остър) и завършват с прекъсване на реда (\r\n). Следователно, за да се уловят такива токени, има смисъл да има различен режим на разпознаване на токени. ANTLR поддържа такива режими, те са описани по следния начин: режим DIRECTIVE_MODE; . Фрагментът на лексера със секцията за режим за директиви на препроцесора изглежда така:

Коментарите също трябва да бъдат поставени на правилната позиция в получения Swift код. Въпреки това, както вече беше споменато, самите скрити токени липсват в дървото за анализ.

Наистина, скрити токени могат да бъдат включени в граматиката, но поради това тя ще стане твърде сложна и излишна, т.к. Във всеки ще се съдържат токени КОМЕНТАР и ДИРЕКТИВАправило между значимите токени:

Следователно можете веднага да забравите за този подход.

Възниква въпросът: как да извлечем такива токени, когато преминаваме през дървото за анализ?

Както се оказа, има няколко варианта за решаване на такъв проблем, при които скритите токени са свързани с нетерминални или крайни (крайни) възли на дървото за анализ.

Асоцииране на скрити токени с нетерминални възли

Този метод е заимстван от сравнително стара статия от 2012 г. за ANTLR 3.

В този случай всички скрити токени са разделени на групи от следните типове:

  • предхождащи токени (предхождащ );
  • последващи токени (следващи );
  • осиротели токени (сираци ).

За да разберете по-добре какво означават тези типове, разгледайте едно просто правило, в което фигурните скоби са крайни знаци, а операторът може да бъде всеки израз, съдържащ точка и запетая в края, например присвояването a = b; .

Свързване на скрити токени с терминални възли

В този случай скритите токени са свързани с определени значими токени. В същото време скритите токени могат да бъдатводещи (LeadingTrivia) изатварящи (TrailingTrivia). Този метод в момента се използва в анализатора Roslyn (за C# и Visual Basic), а скритите токени в него се наричат ​​любопитни факти (Trivia).

Наборът от завършващи токени включва всички любопитни факти на същия ред от значимия токен до следващия значим токен. Всички други скрити токени попадат във водещия набор и са свързани със следващия значим токен. Първият значим токен съдържа първоначалните любопитни факти на файла. Скритите токени, които завършват файла, са свързани с последния специален токен за край на файла с нулева дължина. В повече детайлитипове дървета за разбор и любопитни факти са написани в официалната документация на Roslyn.

ANTLR има метод за токен с индекс i, който връща всички токени от конкретен канал отляво или отдясно: getHiddenTokensToLeft(int tokenIndex, int channel) , getHiddenTokensToRight(int tokenIndex, int channel) . По този начин е възможно базираният на ANTLR анализатор да генерира валидно дърво за анализ, подобно на дървото за анализ на Roslyn.

Игнорирани макроси

Двуетапна обработка

Двуетапният алгоритъм за обработка може да бъде представен като следната последователност от стъпки:

  1. Токенизация и парсване на код на директиви на препроцесора. Нормалните кодови фрагменти се разпознават като обикновен текст в тази стъпка.
  2. Оценете условните директиви ( #if , #elif , #else ) и определете кои кодови блокове да компилирате.
  3. Изчисляване и заместване на стойностите на директивите #define на подходящите места в компилираните кодови блокове.
  4. Замяна на директиви от изходния код с интервали (за запазване на правилните позиции на токените в изходния код).
  5. Токенизиране и анализиране на получения текст с премахнати директиви.

Третата стъпка може да бъде пропусната и макросите могат да бъдат включени директно в граматиката, поне отчасти. Въпреки това, този метод все още е по-труден за прилагане от едноетапната обработка: в този случай след първата стъпка е необходимо кодът на директивите на препроцесора да се замени с интервали, ако има нужда да се запазят правилните позиции на токените на нормалния изходен код. Независимо от това, този алгоритъм за обработка на директиви на предпроцесора също беше внедрен по едно време и сега се използва в Codebeat. Граматиките са публикувани в GitHub заедно с посетител, който обработва директивите на препроцесора.Допълнително предимство на този метод е представянето на граматиките в по-структурирана форма.

За двуетапна обработка се използват следните компоненти:

  1. препроцесорен лексер;
  2. анализатор на препроцесор;
  3. препроцесор;
  4. лексер;
  5. анализатор.

Спомнете си, челексерът групира символите от изходния код в смислени последователности, които се наричат ​​токени или токени. Ипарсер изгражда кохерентна дървовидна структура от потока от токени, която се нарича дърво за анализ.Посетител (Посетител) е шаблон за проектиране, който ви позволява да поставите логиката на обработка за всеки възел на дървото в отделен метод.

препроцесорен лексър

Лексер, който разделя токените от директивите на препроцесора и обикновения Objective-C код. Редовните кодови токени използват DEFAULT_MODE, а директивният код използва DIRECTIVE_MODE. По-долу са токените от DEFAULT_MODE.

анализатор на препроцесора

Парсер, който разделя Objective-C кодови токени и обработва токени на предпроцесорна директива. След това полученото дърво за анализ се предава на препроцесора.

Препроцесор

Посетител, който изчислява стойностите на директивите на препроцесора. Всеки метод за обхождане на възел връща низ. Ако оценената стойност на директивата се изчисли като true, тогава се връща следният кодов фрагмент на Objective-C. В противен случай кодът на Objective-C се заменя с интервали. Както бе споменато по-рано, това е необходимо, за да се поддържат правилните позиции на основните кодови токени. За по-лесно разбиране нека използваме следния кодов фрагмент на Objective-C като пример:

Този фрагмент ще бъде преобразуван в следния Objective-C код, когато условното DEBUG е дадено при използване на обработка в две стъпки.

Струва си да се отбележи, че всички директиви и некомпилиран код са се превърнали в интервали. Директивите също могат да бъдат вложени една в друга:

Обикновен Objective-C лексер без токени, които разпознават директиви на препроцесора. Ако няма директиви в изходния файл, тогава същият оригинален файл се използва като вход.

Парсер за обикновен Objective-C код. Граматиката на този анализатор е същата като граматиката на анализатора от едноетапна обработка.

Други методи на обработка

Има и други начини за обработка на директиви на предпроцесора, например, можете да използвате парсер без лексер. Теоретично, в такъв парсер ще бъде възможно да се комбинират предимствата както на едноетапна, така и на двуетапна обработка, а именно: анализаторът ще изчисли стойностите на директивите и ще определи некомпилирани кодови блокове и в един проход. Такива парсери обаче имат и недостатъци: те са по-трудни за разбиране и отстраняване на грешки.

Тъй като ANTLR е много свързан с процеса на токенизация, такива решения не бяха взети предвид. Въпреки че възможността за създаване на граматики без лексери вече съществува и ще бъде подобрена в бъдеще (вижте дискусията).

Заключение

В тази статия бяха разгледани подходи за обработка на директиви за препроцесор, които могат да се използват при анализиране на C-подобни езици. Тези подходи вече са приложени за обработка на Objective-C код и се използват в търговски услуги като Swiftify и Codebeat. Анализаторът с двуетапна обработка е тестван на 20 проекта, в които броят на правилно обработените файлове е повече от 95% от общия брой. В допълнение, обработката в една стъпка също е внедрена за анализиране на C# и е достъпна в Open Source: C# граматика.

Hardcore conf в C++. Каним само професионалисти.

Чете сега

Ние прехвърлямепроект от Swift 4.2 до Swift 5.0

Съвместим с Objective-C Swift код

Swift не е необходим?

Коментари 6

И вашият макрос DEGREES_TO_RADIANS е повреден

Благодаря ти! При превода на такива макроси все още остава неразрешеният въпрос как да получите името на типа чрез дедукция (Double в примера в статията). Планираме да направим нещо подобно:

но все още се развива :)

Имах предвид, че самият макрос е имплементиран неправилно, няма скоби около аргумента.

Каква е ползата в този случай от използването на Any вместо Double, ако можете да направите извод за типа на аргумента? Всъщност от израза е необходимо да се изведе типа на аргумента, да се замени и да се изведе вида на израза. Обмисляли ли сте да използвате собствения препроцесор (cpp)? Трансформирането на макроси с параметри във функции е смело решение, би било много по-лесно просто да ги разширите. Как се обработват ситуации, когато макросът не може да бъде преобразуван във функция?

Относно скобите около спора - съгласен съм, благодаря! Вярно е, че "правилният" код не винаги се намира на входа на конвертора. Този конкретен макрос изглежда идва от тук: http://stackoverflow.com/questions/29179692/how-can-i-convert-from-degrees-to-radians

Каква е ползата в този случай от използването на Any вместо Double, ако можете да направите извод за типа на аргумента?

Ако разгледаме преобразуванетона който и да емакрос с параметри, а не конкретен случай, тогава резултатът е нещо подобно: 1) Типовете на параметъра (градуси) и връщаната стойност не са посочени, така че ги декларираме като Any; 2) На етапа на анализиране на M_PI * (градуси), конверторът знае, че M_PI е от тип Double, така че изразът (градуси) трябва да бъде от тип Double; 3) Тъй като степените са от тип Any,използва се типово леене. (За съжаление, Swift е много строг по отношение на съвместимостта на цифровите типове данни). 4) Възможно е да се изведе типът на аргумента и връщаната стойност в горния пример, и в общия случай -многотрудно. В края на краищата, вместо (градуси) може да има извикване на метод (познат ни или не) или израз с всякаква сложност.

Обмисляли ли сте да използвате родния препроцесор (cpp)?

Огромно предимство на използването на собствен препроцесор е, че ви позволява да избегнете грешки при анализиране, когато използвате сложни макроси. Вероятно в бъдеще ще добавим използването на peprocessor като опция.

Благодаря отново за публикацията. Друг въпрос, биха ли използвали стандартния препроцесор, ако имаше повече опции? Например, човек може да избира кои директиви да обработва и кои не.

Някои опции може да са полезни. Например, малко вероятно е да се наложи да обработваме предварително #if, #else, #endif и да включим в резултата само част от кода, в която условието се оценява като вярно. Вместо това ние включваме директиви като тази в кода на Swift, въпреки че тяхната поддръжка в Swift е много ограничена.