Кастомізуємо компілятор F#
У кожного експерта з мов програмування свої розваги. Одні розробляють нові езотеричні мови, інші натомість підлаштовують уже наявну мову під власні потреби. Зробимо вигляд що ми із другого табору й створімо українську варіацію компілятора F#, якою можна користуватися, адже всі люблять вивчати програмування мовою, яку вже знають, і не морочитися з якоюсь непотрібною англійською.
Більшість діалектів мов, що підтримують кастомізацію синтаксису, зазвичай спираються на наявні можливості мови, наприклад «Іржа». Цей діалект визначає правила переписування мови, і завдяки цьому компілятор працює як належить після опису правил нового сінтаксиса.
Для F# усе було б не так просто. На жаль, компілятор недостатньо гнучкий «з коробки», проте його легко хакнути. Якщо піти на dotnet/fsharp і запустити ./build на вашій улюбленій ОС, ви отримаєте власноруч зібраний компілятор F#, мовну службу, бібліотеку часу виконання та інтеграцію з IDE. Безболісний досвід, дуже рекомендую.
Хакінг
Отже, оскільки ми вирішили піти хардкорним шляхом, треба знайти місця, де ми застосуємо правку. Найприродніший спосіб — це, очевидно, лексер, де ми можемо змінити те, як розпізнається токен. У F# це src/Compiler/SyntaxTree/LexHelpers.fs. Якщо придивитися, він визначений як відображення (map) між рядком та ідентифікатором токена. Це супер круто, бо якщо ми додамо більше відображень токенів, то зможемо підтримувати водночас і українську, і англійську, що дуже зручно. Тож зробімо це.
FSHARP, "абстрактний", ABSTRACT ALWAYS, "та", AND ALWAYS, "як", AS ALWAYS, "ствердити", ASSERT ALWAYS, "база", BASE ALWAYS, "початок", BEGIN ALWAYS, "клас", CLASS FSHARP, "конст", CONST FSHARP, "замовчання", DEFAULT FSHARP, "делегат", DELEGATE ALWAYS, "зробити", DO ALWAYS, "зроблено", DONE FSHARP, "інякщо", ELIF ALWAYS, "інакше", ELSE ALWAYS, "кінець", END ALWAYS, "виключення", EXCEPTION FSHARP, "зовнішній", EXTERN ALWAYS, "ложь", FALSE ALWAYS, "востаннє", FINALLY FSHARP, "фіксовано", FIXED ALWAYS, "для", FOR ALWAYS, "фун", FUN ALWAYS, "функція", FUNCTION FSHARP, "глобальний", GLOBAL ALWAYS, "якщо", IF ALWAYS, "у", IN ALWAYS, "успадкує", INHERIT FSHARP, "інлайн", INLINE FSHARP, "інтерфейс", INTERFACE FSHARP, "внутрішній", INTERNAL ALWAYS, "ледачий", LAZY ALWAYS, "нехай", LET(false) ALWAYS, "співстав", MATCH ALWAYS, "відповідає", MATCH FSHARP, "член", MEMBER ALWAYS, "мод", INFIX_STAR_DIV_MOD_OP "мод" ALWAYS, "модуль", MODULE ALWAYS, "змінливий", MUTABLE FSHARP, "простір", NAMESPACE ALWAYS, "новий", NEW FSHARP, "нуль", NULL ALWAYS, "з", OF ALWAYS, "відкрити", OPEN ALWAYS, "або", OR FSHARP, "перевизначити", OVERRIDE ALWAYS, "приватний", PRIVATE FSHARP, "відкритий", PUBLIC ALWAYS, "рек", REC FSHARP, "повернути", YIELD(false) ALWAYS, "сіг", SIG FSHARP, "статичний", STATIC ALWAYS, "структ", STRUCT ALWAYS, "тоді", THEN ALWAYS, "до", TO ALWAYS, "істина", TRUE ALWAYS, "спробувати", TRY ALWAYS, "тип", TYPE FSHARP, "вживати", LET(true) ALWAYS, "знач", VAL FSHARP, "пусто", VOID ALWAYS, "коли", WHEN ALWAYS, "доки", WHILE ALWAYS, "із", WITH FSHARP, "поступатися", YIELD(true)
Як бачите, це навіть не лише токени, а якась DSL для визначення токенів, що супер круто.
То що ще? Нічого! От і все. Ми пропатчили компілятор. Тепер можна зібрати продукт F# і замінити FSharp.Compiler.Service.dll у вашій локальній інсталяції, що приблизно відповідає поточній гілці. Наприклад, якщо ви користуєтеся .NET 10, застосовуйте зміни до гілки release/dev18.0, а якщо ви користуєтеся найновішим прев’ю .NET 11 — використовуйте гілку main.
Використання зібраних продуктів складним шляхом
Після збірки ви можете користуватися своїм компілятором з командного рядка. Просто запустіть artifacts/bin/fsc/Release/net10.0/fsc.exe --help, щоб побачити перелік опцій. Також, щоб одразу почати гратися, ви можете запустити F# interactive командою artifacts/bin/fsi/Release/net10.0/fsi.exe. Запустіть якийсь скрипт
нехай ім'я = "Андрій" printfn "Привіт %s" ім'я;;
Натисніть ENTER — і вуаля, ви бачите застосовані зміни. Це магія, ви офіційно хакер компілятора.
Пакування
Очевидно, це круто лише для того, щоб почати. Звісно, захопливо вигинати продукт, яким ви користуєтеся, у незвичний спосіб, і це чудово, але що, якщо ви хочете похизуватися своїм крутим проєктом перед другом? Друзі не надсилають друзям EXE! Тож що ми робимо? Друзі влаштовують повноцінну атаку на ланцюг постачання і створюють гарненькі Nuget-пакети. Оскільки F# — це продукт, пакування є частиною збірки, і нам лишається тільки підкрутити дещо тут і там.
Щоб полегшити собі життя, ми хочемо зробити дві речі: змінити ім’я DLL для FCS (F# Compiler Service) і змінити Nuget-дистрибуцію продукту-компілятора F#. Причина, з якої я хочу змінити ім’я FCS, — уникнути плутанини під час запуску цієї варіації у Visual Studio, адже збірки мають однакове ім’я, але різну версію, і це було б болісно розв’язувати. Перейменування набагато простіше.
Служба компілятора живе у src/Compiler/FSharp.Compiler.Service.fsproj.
Просто змініть AssemblyName і PackageId на FSharp.Compiler.Service.Ukrainian. Це звичайний проєкт MSBuild, тож уся кастомізація, яку ви знаєте з нудного корпоративного життя, тут спрацює.
Проєкт F# використовує файли .nuspec для подальшої кастомізації процесу пакування, тож вам слід підправити файл-шаблон src/Compiler/FSharp.Compiler.Service.nuspec. Просто змініть FSharp.Compiler.Service\$Configuration$\netstandard2.0\FSharp.Compiler.Service.dll на FSharp.Compiler.Service\$Configuration$\netstandard2.0\FSharp.Compiler.Service.Ukrainian.dll. Це переважно рутинні зміни, і вам треба знати, що саме змінювати, але це нормально.
Друга частина — зміна пакування для всього набору компілятора F# разом з інструментами тощо. Він живе в іншому наборі файлів. src/Microsoft.FSharp.Compiler/Microsoft.FSharp.Compiler.fsproj, src/Microsoft.FSharp.Compiler/Microsoft.FSharp.Compiler.nuspec та setup/Swix/Microsoft.FSharp.Compiler.MSBuild/Microsoft.FSharp.Compiler.MSBuild.csproj. Єдина відмінність тут у тому, що вам слід явно додати властивість AssemblyName у src/Microsoft.FSharp.Compiler/Microsoft.FSharp.Compiler.fsproj. Я додаю з іменем AndriiKurdiumov.FSharp.Compiler.Ukrainian. Трохи хвастощів завжди допомагає.
Будьте дуже уважні й змініть усі місця, але не бійтеся щось пропустити — збірка радо зламається, якщо ви забудете перейменувати.
Використання зібраних продуктів легким шляхом
Ми просто створюємо новий консольний застосунок F# командою dotnet new console --lang F#. Це буде основою для нашої програми.
Додаймо також dotnet new nugetconfig, щоб можна було використовувати локально зібраний пакет.
Відкрийте nuget.config і додайте такий рядок
Не хвилюйтеся й використовуйте повний шлях, усе буде добре.
Після цього відкрийте файл fsproj і додайте такі блоки
10 $(PkgAndriiKurdiumov_FSharp_Compiler_Ukrainian)/lib/net10.0/fsi.dll $(PkgAndriiKurdiumov_FSharp_Compiler_Ukrainian)/lib/net10.0/fsc.dll
Зміна LangVersion важлива лише якщо ви збираєте з гілки release/dev18.0, оскільки якщо у вас встановлено .NET 11 Preview, компілятор з версії .NET 10 не знав про версію 11. В іншому разі все має бути гаразд.
Змініть вміст Program.fs на цю програму
нехай повідомлення = "F#" printfn "Привіт %s" повідомлення
Після цього зберіть і запустіть
dotnet build --packages pkg dotnet run --no-build
Що далі
Це найпростіша форма хакінгу, і ми можемо, наприклад, створювати доповнення до примітивів FSharp.Core, а можемо доповнити весь ASP.NET, якщо стане сміливості. Щоб спростити такий хакінг, я створив невеликий інструмент, який показує, як код можна побачити іншими мовами. Також, очевидно, є ліниві, які хочуть спробувати щось у своєму браузері, бо запускати термінал так складно. Тож ви можете глянути на Fable REPL українською. Приклади додаються! Якщо ви любите IDE, ви можете встановити VSIX розширення в студію із файла artifacts\VSSetup\Release\VisualFSharpFull.vsix і насолоджуватися любимою IDE. Якщо у вас інша любима IDE, приходьте в коменти, щось придумаємо.
Висновки
Якщо у вас сильна воля та хакерський дух, ніщо не зупинить вас у цьому світі програмного забезпечення. Ви можете переставляти речі як вам заманеться. Не соромтеся бути дивакуватими.
3 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарівДякую. Люблю читати історії коли йдуть
Це велика рідкість, коли думають нестандартно і ще й діляться цим
Я завжди вважаю що якщо є можливість легко розібрати щось на частини, то це треба робити. Останні років 10 мало хто про такі експерименти пише в .NET середовищі, тому звісно писати треба.
Гарна робота! Дякую Андрій