Существует так много языков программирования, которые поддерживают включение мини-языков. PHP встроен в HTML. XML может быть встроен в JavaScript. Linq может быть встроен в C #. Регулярные выражения могут быть встроены в Perl.
// JavaScript example
var a = <node><child/></node>
Если задуматься, большинство языков программирования можно моделировать как разные мини-языки. Например, Java можно разбить как минимум на четыре отдельных мини-языка:
- Язык объявления типа (директива пакета, директивы импорта, объявление класса)
- Язык объявления членов (модификаторы доступа, объявления методов, переменные-члены)
- Язык операторов (поток управления, последовательное выполнение)
- Язык выражений (литералы, присваивания, сравнения, арифметика)
Возможность реализовать эти четыре концептуальных языка в виде четырех различных грамматик, безусловно, сократила бы большую часть спагеттизма, который я обычно вижу в сложных реализациях синтаксического анализатора и компилятора.
Ранее я реализовал синтаксические анализаторы для различных языков (с использованием ANTLR, JavaCC и пользовательских синтаксических анализаторов с рекурсивным спуском), и когда язык становится действительно большим и сложным, вы обычно получаете одну грамматику huuuuuuuge и Реализация парсера становится очень уродливой и очень быстрой.
В идеале, при написании синтаксического анализатора для одного из этих языков, было бы неплохо реализовать его как набор компонованных синтаксических анализаторов, передавая управление между ними.
Хитрость заключается в том, что часто содержащий язык (например, Perl) определяет свой собственный конечный страж для содержимого языка (например, регулярные выражения). Вот хороший пример:
my $result ~= m|abc.*xyz|i;
В этом коде основной код perl определяет нестандартный конец "|" для регулярного выражения. Реализация парсера регулярных выражений как полностью отличного от парсера perl была бы действительно трудной, потому что парсер регулярных выражений не знал бы, как найти конец выражения без обращения к родительскому парсеру.
Или, допустим, у меня был язык, который позволял включать выражения Linq, но вместо окончания с точкой с запятой (как это делает C #) я хотел, чтобы выражения Linq отображались в квадратных скобках:
var linq_expression = [from n in numbers where n < 5 select n]
Если бы я определил грамматику Linq в грамматике родительского языка, я мог бы легко написать однозначную продукцию для «LinqExpression», используя синтаксический взгляд в будущее, чтобы найти вложения в скобках. Но тогда моя родительская грамматика должна была бы воспринять всю спецификацию Linq. И это сопротивление. С другой стороны, отдельному дочернему синтаксическому анализатору Linq будет очень сложно определить, где остановиться, потому что для этого потребуется реализовать просмотр внешних типов токенов.
И это в значительной степени исключает использование отдельных фаз лексирования / синтаксического анализа, поскольку синтаксический анализатор Linq будет определять совершенно другой набор правил токенизации, чем родительский анализатор. Если вы сканируете один токен за раз, как вы узнаете, когда передать управление лексическому анализатору родительского языка?
Что вы, ребята, думаете? Каковы наилучшие методы, доступные на сегодняшний день для реализации отдельных, разрозненных и составных языковых грамматик для включения мини-языков в более крупные родные языки?