Здравствуйте, Marty, Вы писали:
M>Это интересный вопрос, на самом деле: имхо было бы удобно описывать правила отдельно, а действия, которые производятся при матче правил — отдельно. Тогда грамматику языка можно описать один раз, а построение AST можно пилить для target языков отдельно, и биндить к правилам при выводе в целевой язык.
Это имеет смысл в ограниченном количестве случаев. Ситуации, когда одна и та же грамматика применяется для разных целевых языков, лично мне в жизни не встречались и ожиданий таковых нет.
И даже если такое встретится, мне, скорее всего, будет проще просто склонировать грамматику и переписать правила, чем мучиться с выписыванием отдельного решения.
Впрочем, если хочется попрактиковаться — именно так устроен Ohm-js. В реальности получается не особо удобно, несмотря на офигенную гибкость TypeScript. Основная проблема — безымянные части правил.
В Lingo, к примеру, я могу писать семантическия действия прямо по месту, ещё и выбирая для байндинга только нужные мне части правил:
...
Add = Term:l "+" Add:r { Sum(:l, :r}} | Term
...
В Ohm у меня справа от = стоит безымянная альтернатива; чтобы забиндить к ней какое-то действие, нужно дать ей имя. И аргументами для действия будут все ноды, независимо от их полезности, а тип результата будет каким-то общим супертипом для всех узлов. Средства для всего этого в библиотеке есть, но выглядит костыльно:
...
Add =
Term "+" Add:r --two
| Term
...
const actions = {
...
Add_two: (l, _plus, r) => Add.create(l.getAst(), r.getAst()),
...
} satisfies ArithmeticSemantics<AstNode>
Примерно то же самое придётся делать в более-менее любом подходе, где действия отделены от семантики. Причём ценность этого равна примерно нулю, потому что, повторюсь, целевой язык компилятора обычно известен к тому моменту, когда пишется грамматика. И лучше помочь тем, кто пилит в этом направлении, чем портить жизнь остальным 99.9% пользователей. Авторы Ohm.js приводят вырожденные примеры — типа "а давайте мы забабахаем калькулятор на семантических правилах; а теперь давайте на той же грамматике забабахаем претти-принтер". Ну вот в реальной жизни наиболее практичный способ — это не вычислять грамматикой значение выражения, а породить AST, из которого уже легко, непринуждённо, и типобезопасно получается и "вычисление значения", и pretty print, и вообще примерно всё остальное.
Особенно с учётом того, что реальные языки (в отличие от школьных примеров типа целочисленной арифметики) используют
разные типы узлов в AST, и крайне полезно статически проверять корректность сборки родителей из детей.
M>Криворукий плагин не роняет фар, а просто выгружается. Хотя в фаре плагины inproc. По-моему, это тривиально делается. В винде для этого есть SEH, в других системах наверняка должно быть что-то подобное.
По-моему, вы фантазируете. Никакой SEH не поможет вам от плагина, который просто срёт в общее адресное пространство потому, что кто-то не освоил адресную арифметику, или отлаживался в дебаг-режиме, а в релизе у него неинициализированный указатель показывает в космос, а не в null.
M>Спс, я не интересовался настолько, чтобы что-то копать. LSP это конечно интересно, но вообще для VSCode есть куча плагинов на все случаи жизни, а не только для подсветки кода. Или LSP настолько всеобъемлющ, что позволяет реализовать любые типы плагинов для VSCode?
Нет, LSP — только для языковых инструментов. Плагины, которые расширяют саму студию — всякие альтернативные редакторы, интеграция юнит-тестирования или системы контроля версий пишутся на тайпскрипте и, в некотором смысле, работают in-proc. Но вас-то интересуют не они. Ну, по крайней мере мне как раз интересно, как с минимально возможными усилиями завести IDE для
нового языка, а не новый пункт меню в одной случайно выбранной IDE.
M>Но да, в контексте раскраски синтаксиса и интеллисенса от LSP наверное никуда не деться.
Да, вся навигация по коду, автодополнение, signature hint, рефакторинги и всё прочее — это LSP.