Comments 56
Вообще говоря с Razor-ом можно было подружиться посредством верификации перечня подключенных к сборке типов по белому списку. Есть подозрение, что это было бы более простой задачей.
В любом случае, в разоре прилично всего напрягало, когда обсуждали решения. Во-первых, когда при написании вьюх возникает ошибка, мне иногда приходится ковыряться в исходниках MVC, чтобы понять, что имеется в виду. Во-вторых, если не изменяет память — он рендерил что-то с использованием то ли временного каталога на диске, то ли еще какой-то грязи. В-третьих — он всё-таки для программистов совсем.
По итогу просто было ощущение, что будем пытаться использовать инструмент для задачи, под которую он не заточен.
Берёте на выбор Mono.Cecil/dnlib/System.Reflection.Metadata
и загружаете полученную от Razor-а сборку. Дальше просто анализируете набор импортированых токенов типов и методов, ибо чтобы вызвать хоть что-то, нужно на него сначала сослаться, даже для работы dynamic нужен специальный набор типов и методов, которые можно в белый список не включать.
Временный каталог Razor-у (старому, который не в ASP .NET Core) нужен для сохранения шарповых исходников, которые тот скармливает csc.exe
и полученных на выходе dll-ок. Они-то вам и нужны.
Я правильно понимаю, что каждому шаблону соответствует отдельная dll?
В принципе, звучит работоспособно, но:
- слегка на костылях, всё-таки. Неочевидный код, ошибка в котором — серьезная security-уязвимость
- у нас одновременно в работе бывает несколько тысяч шаблонов, каждую минуту некоторые из них меняются другими. Сложно оценить перформанс этой системы со сборками и рефлексией
А нового разора, когда анализировали существующие варианты, по-моему, вообще ещё не существовало.
В общем, не топлю за то, что однозначно Razor плохо, просто вот по совокупности много всем стало казаться, что слишком много "но".
Нет, впервые вижу такой. Возможно, изучили бы плотнее, хотя выглядит так, что многие проблемы Razor он в себя включает.
Писал в других комментариях: чуть ли не ключевой в итоге критерий, по которому решили писать своё — возможность обнаружения всех используемых параметров на этапе компиляции шаблона. Мы их валидируем на наличие в системе, соответствие типов и всё такое, и для нас это очень полезно.
Но если знаете какие-то движки, которые подходят под наши требования, скиньте обязательно, мне интересо
Liquid рассматривали очень плотно, не понравился тем, что почти не поддерживается (видно по гитхабу) и, насколько помню, туго расширяется в тех местах, где нам надо.
Статья всё же про ANTLR и парсеры, а не про очередной шаблонизатор.
Смысл статьи — показать процесс разработки крупной фичи от этапа сбора требований до выпуска в продакшн, немного познакомить с ANTLR и просто поделиться опытом. Думаю, кому-то точно может быть интересно.
Вы придумали PHP?)
Нет, серьезно, не рассматривали такую возможность? PHP или PHP+Twig вполне бы подошел. Его тут даже не надо наружу выставлять, просто запускать консольное приложение с параметрами. И к базе он умеет коннектиться.
Не совсем понятно, как это должно работать. Схема с межпроцессным взамодействием .NET-стека с PHP в условиях десятков тысяч операций в минуту кажется очень странной. В отличие от других альтернатив, которые тут озвучивали, эту мы реально даже не рассматривали.
Ну что-нибудь типа такого:
sendOrderCompleteEmail(email, orderId)
{
html = executeCommand("php orderComplete.html orderId=" + orderId);
sendEmail(email, html);
// или так
order = findOrder(orderId);
stdinContent = jsonEncode(order);
html = executeCommand("php orderComplete.html", stdinContent);
sendEmail(email, html);
}
Или демона поднять и данные через порт слать.
Понимаю идею, но всё равно не могу пожалеть, что мы так не сделали. Впрочем, возможно, для каких-то задач это подходит, почему нет.
Ок. Подскажите еще пожалуйста, как в шаблоне выглядит вывод словоформ (1 товар, 2 товара)? И можно ли задавать, экранировать или нет вывод тега ${}
?
Словоформы — через функцию-расширение: ${ forms(ProductCount, 'Товар', 'Товара', 'Товаров') }
Настройкой экранирования не заморочились пока, поскольку
- крайне редкие последовательности в любом из известных видов текста (не случайно выбрано два символа, а не один)
- основной медиум — html, где доступно энкодирование через амперсанд-значения, если очень приспичит.
Ну да, я про второе. То есть у вас при выводе значения переменной знаки "< >" автоматически заменяются на "< >
"? Или надо вручную функцию вызывать?
Прочитал, интересно, и боль знакомая местами прямо сквозит. Нам повезло в том плане, что сами придумывали грамматику и сразу в очень формальном виде, это свело неприятности на этапе общения с ANTLR к минимуму.
Читал вашу статью. К сожалению, у вас грамматика была под ANTLR 3. Мы в Positive Technologies написали собственную под ANTLR 4 с более лаконичным синтаксисом, поддержкой островных конструкций, интерполяции строк, левой рекурсией. Хотя все равно не обошлось без вставок кода, т.к. язык сложен для обработки.
Мы рассмотрели несколько доступных шаблонных движков
А StringTemplate рассматривали? Он тоже на ANTLR 4 построен, правда развивается не так активно, как хотелось бы.
При близком знакомстве Antlr оказался java-утилитой, которую нужно запускать из командной строки, пытаясь подобрать правильные аргументы из противоречащих друг другу статей документации, чтобы она сгенерировала C#-код, на который будет ругаться StyleCop.
Регистронезависимость выглядит страшновато, но в документации резонно написано, что лексический разбор – штука точная и подразумевающая максимально явное описание
Я бы рекомендовал все же использовать фрагментные лексемы, чтобы грамматика не выглядела так ужасно. Если язык полностью регистронезависимый, то лучше использовать свой регистронезависимый поток. Это описано в соответствующей секции в моей статей "Теория и практика парсинга исходников с помощью ANTLR и Roslyn". В ней описаны и другие полезные советы.
Конечно, есть вариант сделать один гигантский визитор, в котором будет реализован обход всех наших правил, которых вроде и не так много, а вроде как почти 50.
Можно использовать partial классы, и тогда гиганский визитор будет разделен на несколько файлов.
Но гораздо удобнее оказалось описать несколько разных визиторов, задача каждого из которых – сконструировать одну вершину нашего результирующего дерева.
А мы в Swiftify наоборот отказались от такого разбиения и пришли как раз к гиганскому визитору. Код упростился, а также избавился от лишних аллокаций других визиторов как у вас:
public override ConditionBlock VisitIfCondition(QuokkaParser.IfConditionContext context)
{
return new ConditionBlock(
context.ifInstruction().booleanExpression().Accept(
new BooleanExpressionVisitor(visitingContext)),
context.templateBlock()?.Accept(
new TemplateVisitor(visitingContext)));
}
Кстати, один визитор получается и более детерминированным. У нас реализован интерфейс IParserVisitor
вместо абстрактного класса ParserBaseVisitor
, а поэтому в нем переопределены все методы Visit
. Если известно, что метод никогда не вызывается, то внутри прописывается что-то типа throw new ShouldNotBeVisitedException(context)
. А у вас, например, используется реализация по-умолчанию VisitChildren
для непереопределенных методов. Об этом можно почитать в другой моей статье.
А так вообще в целом статья понравилась. Welcome в наш репозиторий грамматик grammars-v4 :)
Спасибо за комментарий, приятно обсудить всё это с кем-то предметно.
Я бы рекомендовал все же использовать фрагментные лексемы, чтобы грамматика не выглядела так ужасно. Если язык полностью регистронезависимый, то лучше использовать свой регистронезависимый поток
Регистронезависимый поток не нравится тем, что когда захочется вывести ошибку с именем переменной или, тем более, использовать строковую константу — они будут не в том виде, в котором написал автор шаблона. Собственно, как у вас и написано — сложно сделать выборочную регистронезависимость.
Насчет фрагментных лексических правил — тут, наверное, дело вкуса. Мне это кажется сильным усложнением, и на диаграммах выглядит страшненько, и вообще. Возможно, если бы было очень много буквенных лексем, стоило бы задуматься.
Код упростился, а также избавился от лишних аллокаций других визиторов как у вас
Ну, насчет аллокаций мы особо не паримся, потому что каждый инстанс визитора очень легковесный, а время его жини очень короткое, и даже при сложном дереве шаблона это всё абсолютно теряется на фоне кусков "payload" — собственно html-вёрстки, которая внутри блоков.
Да и в принципе вопрос перформанса на этапе компиляции шаблона для нас пока не стоит, потому что у нас режим работы "одна компиляция, миллионы рендеров".
А мы в Swiftify наоборот отказались от такого разбиения и пришли как раз к гиганскому визитору. Код упростился
Интересно! Но попробую отстоять свой подход:
- наши визиторы больше про single responsibility principle — согласитесь, что partial class это не совсем то
- один визитор не даёт возможности по-разному разбирать одно синтаксическое правило в разных контекстах (без введения какого-то состояния или обращения к parent разбираемого нода)
- пожалуй, самое важное — все методы визитора возвращают один тип. Это значит, что результат придётся рано или поздно кастить, а это, мы же понимаем, потенциальная ошибка в рантайме. В вашей статье так и написано —
var first = (Expression)VisitExpression(context.expression(0));
. А у нас визитор, разбирающий выражения, возвращаетIExpression
, визитор про вызову функций —FunctionCallExprsession
. При этом никаких кастов не требуется, и неправильное использование визиторов просто не компилируется. Это, пожалуй, и есть ключевой принцип, по которому я решаю, отдельный это визитор или нет: какой тип он должен в итоге возвращать.
Про опасность VisitChildren
(равно как и дефолтного AggregateResult
) мы в курсе, это, пожалуй, самое неприятное поведение в базовой реализации. У меня есть желание в нашем базовом визиторе переопределить их, чтобы бросали исключение — тогда мы гарантируем, что всё. что есть в грамматике, покрыто набором визиторов. Пока руки не дошли.
Жалко, что вашей статьи не было на момент, когда писалась львиная доля описанного в статье, очень бы пригодилась при разработке. Про внутренний кэш antlr runtime, кстати, узнал только сейчас — очень любопытно. Попробую замерить эффект от него у нас на продакшне.
Регистронезависимый поток не нравится тем, что когда захочется вывести ошибку с именем переменной или, тем более, использовать строковую константу — они будут не в том виде, в котором написал автор шаблона.
В ошибке то корректный регистр будет отображаться, не нормализованный. Смотрите комментарий в гисте CaseInsensitiveInputStream.java: a case-insensitive lookahead stream causes the ANTLR lexer to behave as though the input contained only lowercase letters, but messaging and other getText() methods return data from the original input string
.
Насчет фрагментных лексических правил — тут, наверное, дело вкуса. Мне это кажется сильным усложнением, и на диаграммах выглядит страшненько, и вообще.
На какой диаграмме? Почему усложнение? По-моему наоборот упрощение: нужно просто описать алфавит из 26 букв и использовать как-то так: ABC: A B C
. Фрагменты в лексере преобразуются в литералы, так что на уровне обработки вообще разницы не будет.
Да и в принципе вопрос перформанса на этапе компиляции шаблона для нас пока не стоит, потому что у нас режим работы "одна компиляция, миллионы рендеров".
Ну ок, просто у нас такой вопрос стоит — файлы могут быть очень большими, с глубокими рекурсивными выражениями (например, конкатенация). Как раз недавно оптимизировали как скорость парсера (обновили до ANTLR 4.6, упростили грамматики), так и скорость конвертера (убрали лишние аллокации, сделали мемоизацию).
наши визиторы больше про single responsibility principle — согласитесь, что partial class это не совсем то
Соглашусь. Однако мы от этого не страдаем)
один визитор не даёт возможности по-разному разбирать одно синтаксическое правило в разных контекстах (без введения какого-то состояния или обращения к parent разбираемого нода)
Тут я не очень понял. Да, можно использовать состояние, а можно передавать контекст у визиторов, в котором и будет состояние. Приведите пример, в котором в вашем случае состояние не будет использоваться, а в случае большого визитора — будет. И чем обращение к parent плохо?
пожалуй, самое важное — все методы визитора возвращают один тип. Это значит, что результат придётся рано или поздно кастить, а это, мы же понимаем, потенциальная ошибка в рантайме.
Да, этого иногда не хватает при одном визиторе. Хотя можно наверное как-то с помощью аннотаций или xml комментов сделать проверки. Ну т.е. если известно, что какой-то визитор возвращает всегда определенный тип, то обозначать это в комменте или аннотации, чтобы в других использующих его методах эта информация учитывалась.
Хотя в Swiftify данная проверка вообще оказалась не актуальной, потому в итоге для всех визиторов используется дефолтный визитор по-умолчанию Visitor
, в котором есть try catch
блок для обработки потенциальных ошибок.
У меня есть желание в нашем базовом визиторе переопределить их, чтобы бросали исключение — тогда мы гарантируем, что всё. что есть в грамматике, покрыто набором визиторов.
Правильно — если реализовать интерфейс, то так или иначе придется реализовать все методы Visit
.
Жалко, что вашей статьи не было на момент, когда писалась львиная доля описанного в статье, очень бы пригодилась при разработке.
Как это не было? Они уже примерно год существуют. Или статья писалась два года? :)
Разработка движка — осень 2015-го, потом первые 90% статьи случились зимой 2016-го, потом "я включил Алдан и немножко поработал", и вот мы в весне 2017-го.
Тогда еще не было Antlr.Runtime как nuget-пакета, и приходилось его как dll подкладывать, книжка не совсем соответствовала реализации ANTLR 4.5, ну и по мелочи радости. И статей вроде вашей вот находилось гораздо меньше.
Вроде NuGet версия существует с середины 2013 года: Antlr4.Runtime. А стабильная — с середины 2014. Это стандартный рантайм появился в конце 2016. Последний быстрее обновляется, но менее производительный.
Вроде, NuGet тогда сильно запаздывал: antlr был уже 4.6, а пакет еще 4.5.x или что-то такое. Не самое важное, в общем, но точно помню, что не могли использовать пакет.
Про регистронезависимый стрим осознал, записал себе попробовать в пресловутовый список доработок, спасибо, полезно.
Тут я не очень понял. Да, можно использовать состояние, а можно передавать контекст у визиторов, в котором и будет состояние. Приведите пример, в котором в вашем случае состояние не будет использоваться, а в случае большого визитора — будет. И чем обращение к parent плохо?
Я называю этот паттерн parent.parent.parent.parent
, думаю, он каждому разработчику на WinForms/javascript знаком, как и удовольствие от поддержки такого кода. Это не совсем тот случай, но мне кажется идеологически правильным, чтобы разбор правила был контекстно-независимым, как и грамматика.
Сложно привести пример, не углубляясь в нашу специфику — скорее всего, не для каждой грамматики это актуально. Предположим, у нас есть правило, описывающее доступ к переменной, что-то вроде variable { Identifier }
. В зависимости от контекста это может быть логическим выражением (в конструкции A and B
) или арифметическим выражением (A + B
). Удобно в ArithmeticExpressionVisitor
и BooleanExpressionVisitor
иметь по правилу, VisitVariable
, которые обернут использование переменных в соответствующие классы для работы с выражениями нужного типа.
Тут, наверное, всё упирается в специфику грамматики и использований результата разбора. Как минимум, наш подход тоже отлично работает и имеет плюсы. По ощущениям — очень понятно, атомарно и логично так работать (плюс за типизацию я сильно топлю, и generic-проверки на этапе компиляции котирую выше обработок исключений). Думаю, кому-то может эта идея пригодиться.
На какой диаграмме? Почему усложнение? По-моему наоборот упрощение: нужно просто описать алфавит из 26 букв и использовать как-то так: ABC: A B C. Фрагменты в лексере преобразуются в литералы, так что на уровне обработки вообще разницы не будет.
Мы генерируем синтаксические диаграммы как дополнительную документацию о грамматике, получается наглядно. Надо проверить, как это отразится на них, но почти уверен, что как-нибудь эти лишние кусочные псевдоправила и лишний уровень сложности в описании лексем будет виден. Если нет — прекрасно, но в любом случае, подход со стримом больше понравился.
Про внутренний кэш antlr runtime, кстати, узнал только сейчас — очень любопытно. Попробую замерить эффект от него у нас на продакшне.
Кстати, по этому поводу рекомендую также обратить внимание и на подход, предложенный Sam Harwell (см.
issue на гитхабе). Я пока что не пробовал.
И в целом под C# существует два рантайма: Antlr4.Runtime и Antlr4.Runtime.Standard. Рекомендую использовать первый — он быстрее и автор (как раз Sam Harwell) вызывает больше доверия.
Мы используем Standard, и вообще-то у него же владелец — сам создатель Antlr (https://www.nuget.org/profiles/parrt), куда еще больше доверия-то? Да и Харвеллд в авторах там тоже есть.
Я так понимаю, Харвелловский рантайм подходит, если использовать alternate C# target от него же, а мы исторически на стандартном.
Есть где-то почитать про сравнение таргетов? Это всегда интересовало, хотя сейчас уже, конечно, сложно представить мотивацию перейти с одного на другой.
Он просто владелец, а не разработчик. Ну и к тому же он больше по Java рантайму. Разработчиком является ericvergnaud и с ним не всегда приятно общаться, а в рантайме было много ошибок. При переходе скорее всего ничего не изменится — мы в Swiftify перешли без проблем (используется для открытой грамматики Objective-C).
Про сравнение видимо пока нигде не почитать. :) Хотя можно попробовать поискать. Скорее всего если у вас небольшие файлы, то разницы не будет.
StringTemplate прошёл под радарами, кажется, до начала собственной разработке. Но вообще, мы пришли к выводу, что статического разбора переменных из шаблона вообще нигде нет, и нужно своё делать. Сложность-то не в том, чтобы в строку параметры подставить.
Я пока использую Razor, но то что вы написали мне понравилось, хотелось бы по использовать.
Спасибо
Исходники на гитхабе, просто в приватном репозитории!
Если коротко — выход в опен-сорс это дополнительные затраты, и разовые, и фоновые. Вроде и клёво, а вроде и пока не готовы. Но смотрим в этом направлении.
Если будем опенсорсить — разумеется, напишем здесь
Где-то синтаксис такой, что мы, будучи программистами, не смогли понять, как написать условие.
это про mustashe что ли? Ну попросили бы разобраться кого то из непрограмистов.
А если серьезно — формирую письма этим шаблонизатором — не вижу ни малейшей проблемы с ним.
Да, про него! На самом деле, конечно, потом я разобрался. Но есть ощущение, что не зная хорошо синтаксиса и фиолософию, ничего в шаблоне по мелочи подправить/дополнить простому смертному нельзя.
как то ж смертные разбираются в таких декларативных штуках как например sql
Смертные, когда нужно сделать что-то минимально сложное и из реальной жизни, в sql пишут if exists () begin...
Я понимаю мысль, да. Но клиенты приходят и говорят "мы хотим иметь в письмах условия Если-Иначе, и циклы". Наш язык даёт им это в таких терминах, а Mustashe требует форматирования мозга для начала.
Но в любом случае, красота, понятность и прочие хараектеристики синтаксиса бесконечно далеки от объективности, так что тут спора интересного не выйдет.
А вы F# не рассматривали для этой задачи?
Проектирование и разработка шаблонного движка на C# и ANTLR