Сообщений 0    Оценка 330        Оценить  
Система Orphus

Описание языка описания расширяемых парсеров «Nitra»


Поддерживаемые платформы
Принципы разработки
Пример строчного калькулятора
Пример расширения правила из импортируемого синтаксического модуля
Файл .nitra
Тело пространства имен (NamespaceBody)
Директива «using» (Using)
Член пространства имен (NamespaceMember)
Пространство имен (Namespace)
SyntaxModule
SyntaxModuleMember
Marker
RegexRule
SimpleRule
TokenRule
VoidRule
RuleAlias
TokenLiteral
SpanClass
KeywordRegex
ExtensibleRule
ExtensibleRuleBody
ExtensibleRuleBodyMember
ExtensionRule
RuleMethod, RuleMethodOverride и RuleMethodMissing
RuleMethodAttributes, RuleMethodAttributeList и RuleMethodAttribute
RuleMethodsParam
RuleMethodBody
ExtendToken
ExtendSyntax
RuleExpression
RuleExpression.Sequence
RuleExpression.And
RuleExpression.Not
RuleExpression.Optional
RuleExpression.FieldName
RuleExpression.Char
RuleExpression.String
RuleExpression.Rounds
RuleExpression.Call
RuleExpression.ZeroOrMany
RuleExpression.OneOrMany
RuleExpression.ZeroOrManyWithSeparator
RuleExpression.ZeroOrManyWithHangingSeparator
OneOrManyWithSeparator
OneOrManyWithHangingSeparator
RegexExpression
RegexExpression.Choice
RegexExpression.Sequence
RegexExpression.Optional
RegexExpression.Invert
RegexExpression.Subtract
RegexExpression.Char
RegexExpression.String
RegexExpression.Call
RegexExpression.Rounds
RegexExpression.Range
RegexExpression.InvertedRange
RegexExpression.ZeroOrMany
RegexExpression.OneOrMany
RegexExpression.ZeroOrManyWithSeparator
RegexExpression.OneOrManyWithSeparator
Range
SingleChar
CharRange
UnicodeRange
CharLiteral
StringLiteral
Объект AST
Поля AST
Библиотека Nitra.Runtime.dll
NSpan
Location
SourceSnapshot
Ast
Located
Стандартная библиотека Nitra.Core.dll
Модуль Whitespaces
Модуль TokenNames
Модуль Identifiers
Модуль StandardSpanClasses
Модуль PrettyPrint
Модуль Outline
Модуль CStyleComments

Nitra – это новое имя для продукта, ранее носившем рабочее название N2.

Nitra – это интегрированный набор средств, цель которого – облегчить создание языков программирования и DSL.

Работы над проектом Nitra ведутся в JetBrains около года. За это время нами создана первая подсистема проекта Nitra – подсистема генерации парсеров. От привычных многим генераторов парсеров наш продукт отличается главным образом следующим:

Достоинствами Nitra являются также:

  1. Простой и информативный язык описания грамматик.
  2. Отсутствие необходимости заниматься подгонкой грамматик под ограничения генератора.
  3. Безлексерный парсер – позволяет легко разбирать языки с несколькими лексическими контекстами, такие как XML.
  4. Поддержка постфиксных, инфиксных (бинарных), префиксных операторов и приоритетов операторов.
  5. Достаточно высокая скорость парсинга (достаточная для использования в IDE).
  6. Линейная зависимость времени парсинга от размера разбираемой строки.
  7. Модульность. Модули могут находиться как в той же сборке, так и во внешних сборках.
  8. Информативные сообщения об ошибках.
  9. Возможность получить полную информацию об ошибке, а не только текстовое описание.
  10. Автоматическое восстановление после обнаружения ошибки парсинга.
  11. Удобный способ анализа AST. Код анализа AST выглядит как методы, объявленные в AST, непосредственно оперирующие именами подправил.
  12. Поддержка неоднозначных грамматик (когда две ветви спарсили одинаковый набор токенов).
  13. Автоматическое разрешение неоднозначностей.
  14. Поддержка синтаксических предикатов.
  15. Простой API парсера.
  16. Средство тестирования и визуализации парсеров.
  17. Полная поддержка Unicode и Unicode-категорий.
  18. Интеграция с другими подсистемами Nitra (пока в разработке). Например, подсистемой символов и метаинформации.
  19. Простая установка и легкость использования Nitra в своих проектах.

Поддерживаемые платформы

На сегодня Nemerle поддерживает разработку генераторов парсеров для платформы .Net. Однако в дизайн системы заложена относительно легкая возможность создания парсеров для других платформ. В том числе потенциально возможно генерировать код парсеров на чистом C или под виртуальную машину вроде LLVM или Java.

Принципы разработки

Разработка Nitra-парсера начинается с создания синтаксического модуля. Синтаксический модуль является единицей трансляции и инкапсуляции. Конечный парсер состоит из одного или нескольких синтаксических модулей.

Пример строчного калькулятора

В Nitra создание нового языка или расширения к существующему начинается с объявления нового синтаксического модуля (см. SyntaxModule). Модуль объявляется в .nitra-файле (см. ниже). Если вы используете VS, то можно воспользоваться шаблоном синтаксического модуля или шаблоном проекта Nitra (которые можно найти в пакете диалога создания проекта Nemerle).

Для начала объявим пустой синтаксический модуль Calc.

syntax module Calc
{
}

Строчный калькулятор разбирает выражения, состоящие из арифметических операторов («+», «-», «*», «/», «()», «^») и чисел. Nitra позволяет описать разбор операторов одним правилом. Кроме того, потребуется описать стартовое правило (с которого будет начитаться разбор) и правила разбора чисел. Вот как выглядит грамматика строчного калькулятора на Nitra:

syntax module Calc
{
  using Whitespaces;

  [StartRule, ExplicitSpaces]
  syntax Start = s Expr !Any;

  syntax Expr
  {
    | Number
      {
        regex Digitd = ['0'..'9'];
        regex Number = Digitd+ ('.' Digitd+)?;
      }

    | Number;
    | Parentheses = '(' Expr ')';
    | Add         = Expr '+' Expr precedence 10;
    | Sub         = Expr '-' Expr precedence 10;
    | Mul         = Expr '*' Expr precedence 20;
    | Div         = Expr '/' Expr precedence 20;
    | Pow         = Expr '^' Expr precedence 30 
                                  right-associative;
    | Neg         = '-' Expr      precedence 100;
  }
}

Конструкция (аналогичная C#):

        using Whitespaces;

открывает синтаксический модуль Whitespaces из стандартной библиотеки Nitra.Core.dll, позволяя обращаться к объявленным в ней сущностям по не квалифицированным именам.

Первое правило - это стартовое правило:

[StartRule, ExplicitSpaces]
syntax Start = s Expr !Any;

Оно помечено атрибутом StartRule. Это приводит к тому, что для него генерируется специальная функция, позволяющая упростить его парсинг (Nitra создает расширяемые правила, что приводит к тому, что работать с ними нужно через рефлексию).

Атрибут ExplicitSpaces подавляет автоматическую расстановку пробельных правил (по умолчанию Nitra автоматически расставляет пробельные правила в нужных местах). Это требуется потому, что в начале стартового правила необходимо добавить пробельное правило (чтобы можно было начинать выражение с пробелов), а алгоритм автоматической расстановки пробельных правил вставляет их после правил.

Конструкция !Any – это отрицательный предикат (см. раздел RuleExpression.Not) проверяющий, что после успешно разобранного выражения не идет каких бы то ни было символов. Any – это правило, разбирающее любой (один) символ (см. раздел Модуль Whitespaces).

Конструкция:

syntax Expr
{
  ...
}

объявляет расширяемое правило Expr (см. раздел ExtensibleRule), описывающее арифметическое выражение. Арифметическое выражение состоит из бинарных операторов «+», «-», «*», «/», «()», «^», унарного оператора «-» и круглых скобок. У операторов есть приоритет и ассоциативность. Например, выражение: 2 + 3 * 4 при вычислении даст значение 14, так как приоритет оператора «*» выше чем у «+».

Разбор числа описывается правилами:

regex Digit = ['0'..'9'];
regex Number = Digit+ ('.' Digit+)?;

Более подробно о regex-правилах можно узнать из раздела RegexRule. Данные правила вложены в тело расширяющего правила (см. ExtensionRule) Number. Regex-правила удобны для разбора внутренностей токенов. Они быстры и экономны, так как переводятся в ДКА и не порождают AST.

Правило Digit разбирает цифры: 0, 1, 2, 3, 4, 5, 6, 7, 8 и 9. А правило Number разбирает число с необязательным значением после точки, например 123.987 или 42.

Nitra позволяет выражать понятие приоритета и ассоциативности операторов напрямую (а не эмулировать их с помощью набора правил). Nitra также позволяет выражать бинарные операторы через левую рекурсию, что более естественно для восприятия человека.

В Nitra введена специальная поддержка для описания операторов. Все операторы, которые будут использоваться совместно, должны быть описаны как расширения (см. ExtensionRule) одного расширяемого правила (см. ExtensibleRule). Для описания бинарного (или более арного) инфиксного оператора нужно описать правило, в котором оператор (т.е. некий литерал или другое правило описывающее оператор) будет находиться между двумя рекурсивными обращениями к текущему расширяемому правилу. Например, для оператора сложения нужно описать следующее расширяющее правило:

| Add = Expr '+' Expr precedence 10;

Приоритет оператора, при этом задается при помощи конструкции «precedence n». В данное время приоритет задается целочисленной константой. Чем выше ее значение тем выше приоритет. В дальнейшем мы заменим константы на задаваемые декларативно уровни приоритета. Их так же можно будет расширять динамически.

Если требуется право ассоциативный оператор, то к декларации приоритета нужно добавить конструкцию right-associative:

| Pow         = Expr '^' Expr precedence 30 right-associative;

Ассоциативность определяет, как будут рассматриваться несколько идущих подряд операторов. Например, для левоассоциативного оператора «/» выражение:

100 / 5 / 2

будет рассматриваться как:

(100 / 5) / 2

Если бы он был правоассоциативным, то это же выражение рассматривалось бы как:

100 / (5 / 2)

Приведенная выше грамматика позволяет разобрать выражение. Полученный парсер можно загрузить в Nitra.Visializer или использовать из своего приложения. Однако он делает мало полезного. Давайте заставим наш парсер вычислять значения разбираемых выражений.

Это можно сделать разными путями. Можно получить AST и проанализировать его в коде C#-программы. Но для подобных задач в Nitra есть встроенное решение – методы объявляемы непосредственно на AST.

Ниже приведен вариант грамматики калькулятора, расширенный AST-методами:

using N2.Runtime;
using System;

syntax module Calc
{
  // синтаксические модули из стандартной библиотеки
  using PrettyPrint;
  using TokenNames;
  using StandardSpanClasses;
  using Whitespaces;


  [StartRule, ExplicitSpaces]
  syntax Start = s Expr !Any
  {
    Value() : double = Expr.Value();
  }

  syntax Expr
  {
    Value() : double;
    missing Value = double.NaN; 

regex regex     | [SpanClass(Number)] Number
      {
        regex Digit = ['0'..'9'];
        regex Number = Digit+ ('.' Digit+)?;

        override Value = double.Parse(GetText(Number));
      }

    | ParenthesesParentheses = '(' Expr ')'
      {
        override Value = Expr.Value();
      }
    | Add         = Expr '+' Expr precedence 10
      {
        override Value = Expr1.Value() + Expr2.Value();
      }
    | Sub         = Expr '-' Expr precedence 10
      {
        override Value = Expr1.Value() - Expr2.Value();
      }
    | Mul         = Expr '*' Expr precedence 20
      {
        override Value = Expr1.Value() * Expr2.Value();
      }
    | Div         = Expr '/' Expr precedence 20
      {
        override Value = Expr1.Value() / Expr2.Value();
      }
    | Pow      = Expr '^' Expr precedence 30 right-associative
            {
        override Value = 
          Math.Pow(Expr1.Value(), Expr2.Value());
      }
    | Neg         = '-' Expr            precedence 100
      {
        override Value = -Expr.Value();
      }
  }
}

Как видите, методы очень похожи на методы классов, но вместо классов они объявляются непосредственно в правилах. Более подробно о них можно узнать из раздела «RuleMethod, RuleMethodOverride и RuleMethodMissing».

Строка:

Value() : double = Expr.Value();

описывает метод Value, не имеющий параметров и возвращающий значение типа double (тип System.Double из .Net). Тело метода состоит из единственного выражения:

Expr.Value()

В нем Expr – это обращение к полю AST, созданному на основании обращения к правилу Expr в теле правила. Так как у правила Expr тоже объявлен метод Value(), его можно вызвать. Результат его вычисления будет результатом вычисления метода Value у правила Start.

В AST-методах Nitra используется синтаксис Nemerle. Он похож на синтаксис C#, но имеет ряд отличий. Более подробно его можно изучить здесь. В будущем будет возможно описывать AST-методы на C#. Для понимания данных примеров достаточно знать, что в методах не требуется указывать ключевое слово return. Возвращаемым значением метода является результат последнего (или единственного) выражения. Кроме того, метод можно записывать в сокращенной форме (без фигурных скобок):

Value() : double = Expr.Value();

или в полной форме:

Value() : double { Expr.Value(); }

В первом случае после «=» должно идти единственное выражение. Во втором может идти несколько выражений, разделенных «;».

Строка:

Value() : double;

объявленная в теле расширяемого правила Expr, описывает абстрактный метод. Абстрактные и виртуальный методы можно описывать только в расширяемых правилах. Метод без тела (как в нашем примере) считается автоматически абстрактным. Если у метода определено тело, то он становится виртуальным.

Абстрактный метод должен быть в обязательном порядке переопределен в расширяющих правилах:

        override Value = double.Parse(GetText(Number));

при переопределении не нужно описывать сигнатуру метода. Так как AST-методы в Nitra не поддерживают перегрузку, Nitra сможет найти описание сигнатуры в расширяемом правиле. Зато требуется написать ключевое слово «override». Это предохраняет от случайного перекрытия метода и устраняет неоднозначность между определением метода и вложенного правила.

В данном примере сначала происходит обращение к методу GetText. На вход ему передается имя поля Number. Это поле было сформировано, так как в теле правила было обращение к regex-правилу Number. Тип этого поля – NSpan. NSpan описывает диапазон текста. В данном случае это диапазон текста, соответствующего разобранному числовому значению. Метод GetText объявлен в одном из базовых классов AST (см. описание класса Located). Он возвращает текст, соответствующий переданному NSpan. Полученный текст передается в .Net-функцию double.Parse, которая преобразует текст в число с плавающей точкой (double). Таким образом, данный метод преобразует разобранное число в double.

Переопределение метода Value, объявленного в правиле, разбирающем оператор «+»:

        override Value = Expr1.Value() + Expr2.Value();

получает значение обоих своих подвыражений и возвращает их сумму.

Так как в теле данного правила имеется два обращения к правилу Expr, имена полей, формируемые для них, получают целочисленный индекс (имена этих полей можно задать и явно, см. раздел RuleExpression.FieldName).

Аналогичным образом описаны переопределения и для остальных правил. Различия здесь только в выполняемых вычислениях, так что смысла их описывать нет.

Единственное, о чем осталось упомянуть – это о конструкции:

        missing Value = double.NaN; 

Эта конструкция задает значение, используемое, если в исходном коде (в данном случае в коде выражения) присутствует ошибка, вследствие которой парсер создал AST, в котором отсутствует узел, соответствующий подвыражению. Скажем, если написать выражение «2 + ».

После добавления AST-методов калькулятор можно использовать на практике. Вот так выглядит код его использования из приложения на C#:

using N2;
using System;

class Program
{
  static void Main()
  {
    var parserHost = new ParserHost();
    for(;;)
    {
      Console.Write("input>");
      var input = Console.ReadLine();

      // выходим, если пользователь ввел пустую строку
      if (string.IsNullOrWhiteSpace(input))
        return;

      // Парсим строку с помощью стартового правила
      var parseResult = Calc.Start(SourceSnapshot(input), parserHost);
      // Создаем материализованный AST
      var ast = CalcAstWalkers.Start(parseResult);

      Console.WriteLine("Pretty print: " + ast);
      
      // Выводим сообщения об ошибках, если таковые имеются
      foreach(var error in parseResult.GetErrors())
      {
        var lineCol = error.Location.StartLineColumn;
        Console.WriteLine("(" + lineCol.Line + "," + lineCol.Column + "): " + error.Message);
      }

      // Вычисляем результат выражения путем вызова 
      // AST-метода и выводим значение на консоль.
      Console.WriteLine("Result: " + ast.Value());
    }
  }
}

Обратите внимание, что AST для выражения строится даже если произошла ошибка. О том, были ли ошибки парсинга, можно узнать обратившись к свойству parseResult.IsSuccess или проверив, что parseResult возвращает пустой список.

Теперь можно скомпилировать, запустить и потестировать калькулятор:

input>2 +    3*4
Pretty print: 2+3*4
Result: 14
input>2+
Pretty print: 2+
(1,3): Expected: Expr.
Result: NaN
input>$3
Pretty print: 3
(1,1): Unexpected token
Result: 3
input>

Пример расширения правила из импортируемого синтаксического модуля

Вы может расширять расширяемые правила, объявленные как в своем проекте, так и во внешних сборках. Ниже приведен пример из JsonParser, добавляющий разбор комментариев в стиле C к стандартному пробельному правилу, объявленному в библиотеке Nitra.Core.dll.

syntax module JsonParser
{
  ...
  using Whitespaces;
  using CStyleComments;

  extend token IgnoreToken
  {
    | SingleLineComment
    | MultiLineComment
  }

Здесь правило IgnoreToken объявлено в модуле Whitespaces, входящем в стандартную библиотеку:

...

token IgnoreToken
{
  | Spaces
}

void s = IgnoreToken*; // optional spacer

Конструкция «extend token» позволяет расширить (добавить альтернативы) в правило, объявленное в другом модуле. Таким образом, к игнорируемым пробельным символам добавляются комментарии C и C++. Правила, описывающие комментарии, находятся в другом модуле из стандартной библиотеки – CStyleComments.

Описание языка Nitra

Файл .nitra

[StartRule, ExplicitSpaces]
syntax Start = s NamespaceBody !Any;

Nitra имеет свой файловый формат с расширением .nitra.

Содержимое .nitra-файла рассматривается как тело (NamespaceBody) неявного описанного (безымянного) глобального пространства имен. Пространства имен Nitra аналогичны пространствам имен C#.

Тело пространства имен (NamespaceBody)

      syntax NamespaceBody = Using* NamespaceMember*;

Тело пространства имен Nitra может содержать ноль или более директив «using» (Using) и идущие за ними ноль или более деклараций верхнего уровня (NamespaceMember).

Директива «using» (Using)

syntax Using
{
  | UsingOpen  = "using" NamespaceOrType ";";
  | UsingAlias = "using" Name "=" NamespaceOrType ";";
}

Директивы using практически аналогичны директивам using в C# (см. § 9.4. спецификации C#). Они служат для сокращения имен.

UsingOpen позволяет «открыть» пространство имен, синтаксический модуль или тип. После этого к открытым символам можно будет обращаться с помощью короткого имени. Например:

using Nitra;
...
NSpan(1, 2)

UsingAlias позволяет задать псевдоним (alias) для типа или синтаксического модуля. Например:

using Token = Nitra.NSpan;
...
Token(1, 2)

Член пространства имен (NamespaceMember)

syntax NamespaceMember
{
  | Namespace    = "namespace" QualifiedName
                   "{"  NamespaceBody  "}";
  | SyntaxModule = "syntax" "module" Name 
                   "{" Using* SyntaxModuleMember*  "}";
}

Пространства имен могут содержать объявления других пространств имен или синтаксических модулей.

Пример:

namespace Nitra.Tests
{
  syntax module JsonParser
  {
    using PrettyPrint;
    using Outline;
    
  }
}

Пространство имен (Namespace)

| Namespace    = "namespace" QualifiedName "{"  NamespaceBody  "}";

Пример:

namespace Nitra.Tests
{
}

Пространство имен аналогично пространству имен C# (см. §9.2. спецификации C#).

SyntaxModule

| SyntaxModule = "syntax" "module" Name "{" Using* SyntaxModuleMember*  "}";

Синтаксический модуль является единицей расширения. Парсер может состоять из одного или нескольких синтаксических модулей. Синтаксические модули могут:

Синтаксические модули, к которым происходит обращение, могут быть расположены как в том же проекте, так и во внешней библиотеке.

syntax module JsonParser
{
}

SyntaxModuleMember

syntax SyntaxModuleMember
{
  | RegexRule
  | TokenRule
  | SimpleRule
  | VoidRule
  | ExtensibleRule
  | Marker       = "marker" Name ";";
  | ExtendSyntax = "extend" "syntax" (Name "=")? BaseName 
                   "{" ExtentionRule* "}";
  | ExtendToken  = "extend" "token"  (Name "=")? BaseName 
                   "{" ExtensionRule* "}";
  | RuleAlias
  | TokenLiteral   = "literal" Name "=" (StringLiteral; ",")+ ";";
  | SpanClass    = "span" "class" Name 
                   MatchTokens=("=" RegexExpression)? ";";
  | KeywordRegex = "keyword" "regex" MatchTokens=RegexExpression 
                   "rule" QualifiedName ";";
}

Marker

| Marker       = "marker"  Name ";";

Маркер определяет фиктивное правило, имя которого может использоваться для задания дополнительной метаинформации в грамматике. Эта метаинформация может использоваться генераторами кода и подсистемами Nitra.

Пример:

        marker outline_begin;

Стандартные маркеры

Маркеры из модуля PrettyPrint указывают pretty print-еру, что в месте их применения нужно:

Маркеры из модуля Outline указывают, какие конструкции и как можно сворачивать в редакторе кода:

RegexRule

        syntax TokenRule = RuleAttributes "regex" Name "=" RegexExpression TokenRuleBody;

Именованные правила, разбирающие регулярную грамматику (регулярные выражения). Не поддерживают рекурсию ни в каком виде. Зато в них поддерживаются циклы (* и +).

Примеры:

regex Digits   = ['0'..'9']+;
regex Integer  = '0' | ['1'..'9'] ['0'..'9']*;
regex Exponent = ("E" | "e") ("-" | "+")? Digits;
regex Fraction = "." Digits ;
[SpanClass(Number)]
regex Number = "-"? Integer Fraction? Exponent?;

Такие правила не порождают AST. Вместо этого для них вычисляется соответствующий отрезок текста. В материализованном AST им соответствуют значения типа NSpan.

При восстановлении после обнаружения ошибки восстановление внутри TokenRule не производится, так как они считаются монолитными значениями.

ПРЕДУПРЕЖДЕНИЕ

Не применяйте этот вид правил для определения сложных токенов вроде строк и комментариев. Это приведет к плохому автоматическому восстановлению после ошибок. Описывайте строки и комментарии как token-правила, содержимое которых может быть описано regex-правилами.

Тело (TokenRuleBody) может содержать вложенные токен-правила (TokenRule):

regex EscChar = '\\' | '/' | 'b' | 'f' | 'n' | 'r''t' 
              | 'u' HexDigit HexDigit HexDigit HexDigit
{
  regex HexDigit = ['0'..'9''a'..'f''A'..'F'];
}

SimpleRule

        syntax SimpleRule = RuleAttributes "syntax" Name "=" RuleExpression SimpleRuleBody;

Именованное правило, разбирающее контекстно-свободную грамматику. Поддерживает прямую и косвенную правую рекурсию. Левая рекурсия не поддерживается. Также поддерживаются циклы (* и +).

Каждое объявление SimpleRule описывает также ветку AST (класс). Класс AST может содержать одно или несколько полей. Поля вычисляются по грамматике (задаваемой правилом RuleExpression). Для каждого подправила (в RuleExpression) создается по одному полю. Имена полей выводятся автоматически или задаются автором грамматики. Подробнее см. раздел "Поля AST".

Пример:

        syntax Property = Key ":" Value; 

Данное правило описывает свойство JSON. Key и Value - это ссылки на другие правила, которые должны быть объявлены в том же синтаксическом модуле или импортированы из других модулей.

В теле (SimpleRuleBody) могут содержаться (подробнее см. SimpleRuleBodyMember):

TokenRule

        syntax TokenRule = RuleAttributes "token" Name "=" RuleExpression SimpleRuleBody;

TokenRule аналогичен SimpleRule за исключением того, что:

TokenRule особым образом обрабатываются при восстановлении после обнаружения ошибок. Кроме того, при формировании сообщения об ошибке, парсер будет указывать на такое правило, а не на его содержимое.

Токен так же может быть описан регулярным правилом (RegexRule). Но при этом вам не будет доступна структура таких правил. Это может негативно повлиять на подсветку кода и на восстановление после ошибок. Например, комментарии и строки следует описывать с помощью TokenRule, так как в ином случае парсер не может обнаружить незакрытую строку или комментарий.

Пример:

token Identifier = !Keyword IdentifierBody
{
  regex KeywordToken = "true" | "false" | "null";
  token Keyword = KeywordToken !IdentifierPartCharacters;
}

VoidRule

        syntax VoidRule = RuleAttributes "void" Name "=" RuleExpression SimpleRuleBody;

Void-правила аналогичны TokenRule, но для них не строится материализованного AST, и они несколько иначе рассматриваются парсерами в некоторых ситуациях.

Основное предназначение void-правил – описание правил, разбирающих незначимые элементы грамматики, такие как пробельные символы (пробелы, табуляции, концы строк) и комментарии.

Так как по умолчанию Nitra производит автоматическую расстановку пробелов, обычно требуется определить два void-правила: s и S. Правило «s» должно разбирать незначимые символы (пробельные и комментарии). Правило «S» должно быть аналогично правилу «s», но дополнительно содержать предикат, проверяющий, что входной поток символов не содержит символы, соответствующие символам идентификатора разбираемого языка.

S ставится после ключевых слов, описанных в грамматике по месту, с помощью литералов.

void S = !IdentifierPartCharacters s;
void s = IgnoreToken*;

RuleAlias

        syntax RuleAlias = "alias" Name "=" RuleExpression ";";

Позволяет задать псевдоним для правила. Псевдоним решает две задачи:

  1. Подставляет свое содержимое по месту, тем самым не создавая нового правила.
  2. Определяет имя для поля в месте подстановки.
        alias Name = Identifier;

Данный пример задает псевдоним Name для правила Identifier. Теперь, если где-то в грамматике будет обращение к данному псевдониму:

| UsingAlias = "using" Name "=" NamespaceOrType ";"

то вместо Name будет подставлено обращение к правилу Identifier, но созданное в материализованном AST поле будет иметь имя «Name».

TokenLiteral

  | TokenLiteral   = "literal"  Name  "="  (StringLiteral; ",")+  ";";

TokenLiteral позволяет описать имена для токенов, задаваемых непосредственно в грамматике с помощью литералов (в приведенном выше литералами являются: "literal", "=", "," и ";").

Если перед определением правила, в котором используется литералы, задать для этих литералов имена с помощью TokenLiteral, то для этих литералов будет создано поле с заданным именем.

Например, если задать

        literal Assignment = "=";

то для литерала "=" будет создано поле с именем Assignment.

Имена можно переопределять перед следующим использованием. Определение будет использовано с места, где оно определено, и до следующего определения или до конца области видимости, в которой оно определено.

SpanClass

| SpanClass = "span" "class" Name MatchTokens=("=" RegexExpression)? ";";

SpanClass позволяет задать класс подсветки. Это позволяет описать подсветку для своего языка или расширения к другому языку.

Описанный класс подсветки может быть задан правилу посредством атрибута SpanClass.

Если при описании класса подсветки указать регулярное выражение MatchTokens, то этот класс подсветки будет автоматически назначаться всем литералам, которые соответствуют (matched) регулярному выражению.

Например, следующие описание приводит к тому, что все литералы, состоящие из двух или более букв латинского алфавита, будут помечаться классом подсветки Keyword:

        span class Keyword = ['a'..'z', '_']['a'..'z', '_']+;

Пример применения задания подсветки для строк:

span class String;

[SpanClass(String)]
token RegularStringLiteral = "\"" RegularStringLiteralPart* "\"";

KeywordRegex

| KeywordRegex = "keyword" "regex"  MatchTokens=RegexExpression 
                 "rule" QualifiedName ";";

KeywordRegex позволяет задать регулярное выражение, описывающее ключевые слова языка, и имя правила, которое будет вставляться вместо пробельного правила при автоматической расстановке пробельных правил.

Вставляемое правило должно контролировать, что за ключевым словом не идет символов, входящих в состав идентификаторов. В противном случаем парсер будет ошибочно разбирать префиксы идентификаторов, совпадающие с ключевыми словами (например, «do» в doStaff, при условии, что «do» – ключевое слово). Для этого в состав указанного правила должен входить соответствующий предикат.

Например, следующее определение описывает, что после литералов, состоящих из строчных букв латинского алфавита, будет ставиться правило S:

        keyword regex ['a'..'z']+ rule S;

Такое правило определено в модуле Whitespaces следующим образом:

        void S = !IdentifierPartCharacters s; // identifier break spacer

Это правило проверяет, что текущий символ не является символом, допустимым внутри идентификатора, и затем распознает стандартное пробельное правило «s», объявленное в том же модуле.

Если разбор пробельных символов в вашем языке отличается от стандартных правил, вы можете объявить собственные версии правил s и S.

ExtensibleRule

        syntax ExtensibleRule = RuleAttributes "syntax" Name ExtensibleRuleBody;

Описывает расширяемое правило.

Расширяемое правило позволяет ввести альтернативы.

Например, следующее правило вводит правило Key, состоящее из трех альтернатив: StringLiteral1, StringLiteral2 и Identifier:

syntax Key
{
  | StringLiteral1
  | StringLiteral2
  | Identifier
}

Это правило аналогично следующему BNF-правилу:

Key = StringLiteral1 | StringLiteral2 | Identifier;

ExtensibleRule позволяет описывать тела расширений по месту:

syntax Value
{
  | Object = "{" (Property; ",")* "}";
  | Array  = "[" (Value; ",")* "]";
  | Identifier
  | StringLiteral1
  | StringLiteral2
  | Number
  | "true"
  | "false"
  | "null"
}

Расширяемое правило может вообще не содержать расширений:

syntax Value
{
}

Расширения можно добавить позднее. Например, в другом синтаксическом модуле, объявленном в отдельной сборке.

Расширяемое правило называется расширяемым, так как является точкой расширения грамматики. Любое расширяемое правило можно расширить (см. ExtendSyntax) в другом синтаксическом модуле. Причем синтаксический модуль расширяющий правило из другого модуля, может быть описан как в одной сборке с расширяемым, так и в отдельной сборке.

Расширение правил может производиться динамически. Расширение можно произвести как во время загрузки грамматики, так и непосредственно в процессе парсинга. Это позволяет динамически изменять грамматику языков, созданных на основе Nitra. Это полезно для создания языков с изменяемым синтаксисом (например, Nemerle), или для введения расширений в классические языки и DSL.

ExtensibleRuleBody

syntax ExtensibleRuleBody
{
  | Empty = ";";
  | Block = "{" ExtensibleRuleBodyMember* "}";
}

Тело ExtensibleRule. Как видно из грамматики, может состоять из точки с запятой (т.е. тело опущено) или блока, содержащего члены ExtensibleRule.

ExtensibleRuleBodyMember

syntax ExtensibleRuleBodyMember
{
  | RuleAlias
  | RegexRule
  | TokenRule
  | VoidRule
  | SimpleRule
  | ExtensibleRule
  | ExtensibleTokenRule
  | ExtensionRule
  | RuleMethod
  | RuleMethodOverride
  | RuleMethodMissing
}

Члены, допустимые в ExtensibleRule (см. соответствующие описания). Как и в теле синтаксического модуля, в теле ExtensibleRule могут присутствовать описания правил (RuleAlias, RegexRule, TokenRule, VoidRule, SimpleRule, ExtensibleRule и ExtensibleTokenRule), а также объявление собственных расширений – ExtensionRule, и методы правил (RuleMethod, RuleMethodOverride и RuleMethodMissing).

ExtensionRule

        syntax ExtensionRule = "|" RuleAttributes (Name "=")? RuleExpression Precedence? ExtensionRuleBody?;

ExtensionRule позволяют задать набор альтернатив расширяемого правила прямо при его объявлении или с помощью конструкции ExtendSyntax.

Пример:

| Object = "{" (Property; ",")* "}";

RuleMethod, RuleMethodOverride и RuleMethodMissing

syntax RuleMethod = 
  RuleMethodAttributes "private"? Name "(" (RuleMethodsParam; ",")* ")" ":" 
  Type RuleMethodBody;
syntax RuleMethodOverride = RuleMethodAttributes "override" 
  Name RuleMethodBody;
syntax RuleMethodMissing  = RuleMethodAttributes "missing"
  Name RuleMethodBody;

RuleMethod – описывает методы, определяемые непосредственно на правиле. Такие методы могут получать информацию о правиле, на котором они определены, и о его подправилах. Методы могут вызывать другие методы, определенные как на том же правиле, так и на других правилах. Кроме того, результат вычисления таких методов может быть кэширован непосредственно в AST (создаваемом для правил).

RuleMethod могут манипулировать именами подправил для получения информации о них и вызова их методов.

Если RuleMethod объявлен на расширяемом правиле ExtensionRule, то порождается аналог виртуального метода (в терминах ООП). Если такой метод не имеет тела, то он автоматически считается абстрактным. Если тело есть – виртуальным.

Виртуальные методы могут (а абстрактные должны) быть переопределены в расширениях расширяемого правила. Это позволяет автору расширяемого правила абстрагировать метод от конкретных расширений, а авторам расширений – создавать конкретные реализацию метода для своих расширений.

При объявлении расширения нужно указать ключевое слово override (см. RuleMethodOverride). При этом сигнатуру переопределяемого метода указывать не нужно, так как для методов не поддерживается перегрузка.

RuleMethod позволяют осуществлять любые вычисления над результатом парсинга (AST). С их помощью можно осуществлять многопроходную трансляцию. Кэширование результатов методов (помеченных атрибутом Cached) позволяет разделить обработку на стадии, так как значения сохраняются непосредственно в AST.

Пример:

token Identifier = !(Keyword !IdentifierPartCharacters) IdentifierBody
{
  Value() : string = GetText(IdentifierBody);
}

syntax QualifiedIdentifier = Names=(Identifier; ".")+
{
  Parts() : list[string] = Names[0].Map(n => n.Value());
}

Здесь Value возвращает текст, разбираемый подправилом IdentifierBody правила Identifier, а метод Parts возвращает список частей QualifiedIdentifier. QualifiedIdentifier состоит из одного или нескольких идентификаторов, разделенных точкой. Результат разбора цикла доступен через поле Names. Так как это цикл с разделителем, его результатом является кортеж (Tuple) из двух элементов. В первом элементе кортежа содержится список элементов (их AST), а во втором список разделителей (опять же их AST). Конструкция Names[0] позволяет обратиться к первому элементу кортежа. Далее стандартная функция Map (аналогичная функции Select из Linq) преобразует список, содержащий элементы AST в список строк. Значение для каждого элемента вычисляется посредством обращения к методу Value, описанному в правиле Identifier.

Зачастую разбираемый код может быть некорректным. При восстановлении после обнаружения ошибок парсер может создать суррогатные ветки AST для отсутствующих значений. При обращении к методам суррогатного AST будет выдано исключение. Чтобы предотвратить это, можно объявить специальную форму метода – RuleMethodMissing. У этой формы не может быть параметров. Она может только лишь возвратить некоторое значение по умолчанию. Именно это значение будет использоваться в случае отсутствия значения.

Пример виртуального метода:

token CharPart
{
  Value() : char;

  | Simple = !ReservedCharChar Char
    {
      override Value = FirstChar(this.Char);
    }
  | UnicodeEscapeSequence = "\\u" HexDigit HexDigit HexDigit HexDigit
    {
      override Value = 
        HexToChar(this, HexDigit1.StartPos, HexDigit4.EndPos);
    }
  | EscapeSequence = "\\" Char
    {
      override Value = EscapeSequence(FirstChar(this.Char));
    }
}

И его использование:

        token CharLiteral = "\'" CharPart "\'" { Value() : char = CharPart.Value(); }

Метод FirstChar – это встроенный метод. Он возвращает первый символ, разобранный правилом, переданным в качестве параметра.

Методы HexToChar и EscapeSequence – это обычные методы, описанные в том же проекте (в отдельном Nemerle-модуле):

public HexToChar(ast : Nitra.Ast, startPos : int, endPos : int) : char
{
  unchecked HexToInt(ast, startPos, endPos) :> char
}

public HexToInt(ast : Nitra.Ast, startPos : int, endPos : int) : int
{
  assert2(startPos < endPos);

  def text = ast.Location.Source.OriginalText;
  mutable result = HexDigit(text[startPos]);

  for (mutable i = startPos + 1; i < endPos; i++)
    unchecked result = (result << 4) + HexDigit(text[i]);

  result
}

public static EscapeSequence(c : char) : char
{
  | '\'' => '\'' | '\"' => '\"' | '\\' => '\\' | '0'  => '\0'
  | 'a'  => '\a' | 'b'  => '\b' | 'f'  => '\f' | 'n'  => '\n'
  | 'r'  => '\r' | 't'  => '\t' | 'v'  => '\v' | c    => c
}

RuleMethodAttributes, RuleMethodAttributeList и RuleMethodAttribute

syntax RuleMethodAttributes = RuleMethodAttributeList*
syntax RuleMethodAttributeList = "[" (RuleMethodAttribute; ",")+ "]"
syntax RuleMethodAttribute
{
  | Cached = AttributeName="Cached"
}

RuleMethod-ы поддерживают один атрибут «Cached». Если он задан, то после первого выполнения метода его возвращаемое значение кэшируется (мемоизируется) в AST и становится доступно через свойство. Свойство будет иметь имя метода за вычетом стандартных префиксов:

RuleMethodsParam

        syntax RuleMethodsParam = Name ":" Type;

Описывает параметр. В качестве имени может быть использован любой идентификатор. В качестве типа любой тип .Net (в том числе AST или тип символа Nitra).

RuleMethodBody

Код на Nemerle. В дальнейшем, возможно, будет можно писать код на C#.

В коде методов правил можно обращаться к подправилам по их имени, так, будто для каждого подправила создано свойство. Так же можно обращаться к свойствам узла AST, соответствующего правилу, в котором объявлен метод. Доступ к членам узла AST можно осуществлять напрямую или через «this». Более подробную информацию об AST и его членах можно узнать в разделе Объект AST.

ExtendToken

| ExtendToken  = "extend" "token"  (Name "=")? BaseName "{" InlineExtensionRule* "}";

Данная конструкция применяется, если нужно расширить расширяемый токен, определенный в другом синтаксическом модуле. BaseName задает имя правила, подлежащего расширению. Это имя может быть полностью квалифицированным, или простым, или частично квалифицированным, если синтаксический модуль, в котором определено расширяемое правило, открыт с помощью директивы using.

extend token IgnoreToken
{
  ...
}

Данный пример расширяет IgnoreToken (игнорируемые правила) правилами комментариев в стиле C и C++.

Если имя расширяемого правила конфликтует с именами из текущего синтаксического модуля, то можно задать локальный псевдоним – Name.

extend token LocalAlias = Whitespaces.IgnoreToken
{
  ...
}

Здесь Whitespaces.IgnoreToken – полностью квалифицированное обращение к правилу IgnoreToken из модуля Whitespaces, а LocalAlias – локальное имя. Это имя используется в качестве имени AST.

ExtendSyntax

| ExtendSyntax = "extend" "syntax" (Name "=")? BaseName "{" ExtentionRule* "}";

Данная конструкция аналогична конструкции ExtendToken за тем исключением, что позволяет расширить расширяемое синтаксическое правило, а не токен.

RuleExpression

syntax RuleExpression
{
  | Sequence                       = LeftRule=RuleExpression ^ 10 RightRules=(RuleExpression ^ 10)+
  | Not                            = "!" RuleExpression ^ 20
  | And                            = "&" RuleExpression ^ 20
  | Optional                       = RuleExpression "?" precedence 30
  | ZeroOrMany                     = RuleExpression "*" precedence 30
  | OneOrMany                      = RuleExpression "+" precedence 30
  | FieldName                     = Name "=" RuleExpression ^ 11
  | Char                           = CharLiteral
  | String                         = StringLiteral
  | Rounds                         = "(" RuleExpression ")"
  | Call                      = QualifiedName BindingPower=("^" Number)?
  | ZeroOrManyWithSeparator        = "(" RuleExpression ";" 
                                         Separator ")" "*"
  | ZeroOrManyWithHangingSeparator = "(" RuleExpression ";" 
                                         Separator ";" "?" ")" "*"
  | OneOrManyWithSeparator         = "(" RuleExpression ";" 
                                         Separator ")" "+"
  | OneOrManyWithHangingSeparator  = "(" RuleExpression ";" 
                                         Separator ";" "?" ")" "+"
}

RuleExpression описывает тело syntax- или token-правила.

RuleExpression.Sequence

| Sequence = LeftRule=RuleExpression ^ 10 RightRules=(RuleExpression ^ 10)+

Описывает последовательность подправил. Последовательность может содержать два или более подправил.

Пример:

A B (C; ",")+

В данном примере содержится три подправила:

RuleExpression.And

| And = "&" RuleExpression ^ 20

Утверждающий предикат. Предикат не приводит к разбору текста. Он всего лишь проверяет, может ли быть разобрано указанное в нем правило в данном месте. В случае успеха разбора подправила RuleExpression парсинг правила, в котором встретился предикат, продолжается дальше так, будто предиката не было. В случае неудачи происходит откат всего разбираемого правила.

Данное правило не формирует элементов AST.

Пример:

syntax IndentedEmbeddedStatement
{
  | Statement = !"{" EmbeddedStatement; // not a block statment
  | Block     = &"{" Stmt=Block;
}

В приведенном примере правило Block будет разобрано, только если в текущей позиции разбора находится символ '{'.

RuleExpression.Not

| Not = "!" RuleExpression ^ 20

Отрицательный предикат. Он делает то же, что и положительный предикат, но инвертирует значение. В случае неудачи разбора подправила RuleExpression парсинг правила, в котором встретился предикат, продолжается дальше так, будто предиката не было. В случае успеха происходит откат всего разбираемого правила.

syntax IndentedEmbeddedStatement
{
  | Statement = !"{" EmbeddedStatement; // not a block statment
  | Block     = &"{" Stmt=Block;
}

В приведенном примере правило Statement будет разобрано, только если в текущей позиции разбора не находится символ '{'.

RuleExpression.Optional

| Optional = RuleExpression "?" precedence 30

Разбирает необязательное правило.

При формировании AST, результат такого правила помещается в тип-обертку option[T]. В случае успеха разбора подправила RuleExpression полученное значение помещается в option[T].Some, в случае неудачи возвращается option[T].None. При этом T замещается на тип, соответствующий результату подправила RuleExpression.

Пример:

| If = "if" "(" BooleanExpression ")" IndentedEmbeddedStatement 
        ("else" IndentedEmbeddedStatement)?;

RuleExpression.FieldName

| FieldName = Name "=" RuleExpression ^ 11

Задает имя для подправила. На основе этого имени формируется имя поля AST.

Пример:

LeftRule=RuleExpression

Здесь RuleExpression подправило, а LeftRule имя формируемого для него поля.

RuleExpression.Char

| Char = CharLiteral

См. CharLiteral.

RuleExpression.String

| String = StringLiteral

См. StringLiteral.

RuleExpression.Rounds

| Rounds = "(" RuleExpression ")"

Позволяет описать последовательность правил, сделав их одним подправилом.

RuleExpression.Call

| Call = QualifiedName BindingPower=("^" Number)?

Вызывает другое правило (обращается к другому правилу). Типом вызова является тип вызываемого правила:

Пример:

| Property = Name "=" ComplexInitializer;

Здесь Name и ComplexInitializer – это вызовы других правил.

RuleExpression.ZeroOrMany

| ZeroOrMany = RuleExpression "*" precedence 30

Цикл, разбирающий ноль или более элементов.

Имя поля, формируемое для цикла, приводится к множественному числу (см. Поля AST). Типом такого поля будет list[T], где T – это тип, соответствующий типу подправила RuleExpression.

Пример:

        token StringLiteral = '\"' StringPart* '\"';

Для цикла «StringPart*» будет создано поле

StringParts : list[StringPart];

RuleExpression.OneOrMany

| OneOrMany = RuleExpression "+" precedence 30

То же самое, что ZeroOrMany, но позволяет разбирать одно или более правило (т.е. не допускает пустых списков).

Пример:

        syntax SwitchSection = SwitchLabel+ Statement+;

RuleExpression.ZeroOrManyWithSeparator

| ZeroOrManyWithSeparator = "(" RuleExpression ";" Separator ")" "*"

Цикл с разделителем, разбирающий ноль или более элементов. Отличается от RuleExpression.ZeroOrMany тем, что элементы разбираемого списка разделяются разделителем, задаваемым подправилом Separator.

Пример:

| Array  = "[" (Value; ",")* "]";

Данное правило разбирает текст вроде: «[ 1, a, "aaa" ]». При этом для цикла «(Value; ",")*» формируется поле:

Values : list[Value] * list[NSpan];

то есть кортеж, состоящий из двух списков. В первый список будет помещены элементы разбираемого списка, а во второй – позиции (так как при разборе литералов возвращаются позиции их вхождения в коде) разделителей.

RuleExpression.ZeroOrManyWithHangingSeparator

| ZeroOrManyWithHangingSeparator = "(" RuleExpression ";" Separator ";" "?" ")" "*"

То же самое, что ZeroOrManyWithSeparator, но допускает «висячий» разделитель, то есть разделитель в конце списка (за которым не идет элемента).

Пример:

        syntax EnumMemberDeclarations = (EnumMemberDeclaration; ","; ?)*;

Данный пример разбирает код вроде:

UriEscaped = 1,
Unescaped,
SafeUnescaped,

или:

UriEscaped = 1,
Unescaped,
SafeUnescaped

то есть как содержащий запятую в конце списка, так и не содержащий ее.

Если бы использовалось правило ZeroOrManyWithSeparator, то наличие запятой в конце списка было бы недопустимо.

OneOrManyWithSeparator

| OneOrManyWithSeparator = "(" RuleExpression ";" Separator ")" "+"

Цикл, аналогичный ZeroOrManyWithSeparator, но не допускающий пустые списки.

Пример:

        syntax VariableDeclarators = (VariableDeclarator; ",")+;

Данный пример разберет:

x
x = 1, y

Но не разберет:

x,

или пустой список.

OneOrManyWithHangingSeparator

| OneOrManyWithHangingSeparator = "(" RuleExpression ";" 
                                      Separator ";" "?" ")" "+"

Цикл, аналогичный ZeroOrManyWithHangingSeparator, но не допускающий пустые списки.

Пример:

        syntax NotEmptyArguments = "(" (Expression; ","; ?)+ ")";

Данный пример разберет:

(42)
(42,)
(42, 5)
(42, 5,)

Но не разберет:

()
(,)

RegexExpression

syntax RegexExpression
{
  | Choice                  = LeftRule=RegexExpression ^ 10 
                              RightRules=("|" RegexExpression ^ 10)+
  | Sequence                = LeftRule=RegexExpression ^ 20 
                              RightRules=(RegexExpression ^ 20)+
  | Optional                = RegexExpression "?" precedence 30
  | ZeroOrMany              = RegexExpression "*" precedence 30
  | OneOrMany               = RegexExpression "+" precedence 30
  | Invert                  = "~" RegexExpression precedence 25
  | Subtract                = Rule1=RegexExpression 
                              "-" Rule2=RegexExpression precedence 22
  | Char                    = CharLiteral
  | String                  = StringLiteral
  | Call                    = QualifiedName
  | Rounds                  = "(" RegexExpression ")"
  | Range                   = "[" (Range; ",")+ "]"
  | InvertedRange           = "[" "^" (Range; ",")+ "]"
  | ZeroOrManyWithSeparator = "(" RegexExpression ";" Separator ")" "*"
  | OneOrManyWithSeparator  = "(" RegexExpression ";" Separator ")" "+"
}

RegexExpression описывает тело regex-правила, а также используется в некоторых местах, где требуется задать регулярное выражение.

Результатом разбора RegexExpression является тип NSpan, описывающий отрезок текста (см. NToken). Узлов AST для RegexExpression не создается.

RegexExpression.Choice

| Choice = LeftRule=RegexExpression ^ 10 RightRules=("|" RegexExpression ^ 10)+

Задает набор альтернатив RegexExpression.

Пример:

        '\n' | '\r' | "\r\n";

RegexExpression.Sequence

| Sequence = LeftRule=RegexExpression ^ 20 
             RightRules=(RegexExpression ^ 20)+

Задает последовательность RegexExpression.

RegexExpression.Optional

| Optional = RegexExpression "?" precedence 30

Задает необязательное правило. Если разбор подправила RegexExpression терпит неудачу, результатом правила становится пустой диапазон (т.е. NToken с Length равным нулю).

Пример:

        "-"? Integer Fraction? Exponent?;

Данный пример разберет:

-42
36.6
5

RegexExpression.Invert

| Invert = "~" RegexExpression precedence 25

Инвертирует RegexExpression.

RegexExpression.Subtract

| Subtract = Rule1=RegexExpression "-" Rule2=RegexExpression 
             precedence 22

Вычитает один RegexExpression (Rule1) из другого (Rule2).

Пример:

        regex NotQuot = ~'\"';

Данный пример разберет любой символ кроме двойной кавычки.

RegexExpression.Char

| Char = CharLiteral

См. CharLiteral.

RegexExpression.String

| String = StringLiteral

См. StringLiteral.

RegexExpression.Call

| Call = QualifiedName

Вызывает другое правило (обращается к другому правилу). По эффекту аналогично подстановке тела правила, к которому происходит обращение, в место вызова.

RegexExpression.Rounds

| Rounds = "(" RegexExpression ")"

Позволяет описать последовательность правил, сделав их одним подправилом.

RegexExpression.Range

| Range = "[" (Range; ",")+ "]"

Описывает диапазон (см. RegexExpression.ZeroOrManyWithSeparator) символов.

Пример:

        regex HexDigit = ['0'..'9', 'a'..'f', 'A'..'F'];

данный пример разбирает шестнадцатеричные числа.

RegexExpression.InvertedRange

| InvertedRange = "[" "^" (Range; ",")+ "]"

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

Пример:

        regex NotQuot = [^'\"'];

Данный пример разберет любой символ кроме двойной кавычки.

RegexExpression.ZeroOrMany

| ZeroOrMany = RegexExpression "*" precedence 30

Цикл, разбирающий ноль или более RegexExpression-элементов. В отличие от аналога из RuleExpression является «уступчивым». Это означает, что если имеется следующая грамматика:

        'a'* 'a'

и следующая строка:

aaa

то цикл «'a'*» разберет первые две буквы «a» и оставит последнюю букву «a» следующему правилу (литералу), в то время как цикл RuleExpression.ZeroOrMany «съест» все буквы «a», и следующее правило гарантированно завершится неудачей.

Такая разница в поведении обусловлена тем, что для разбора RuleExpression используется метод рекурсивного спуска, а для RegexExpression – преобразование к ДКА.

RegexExpression.OneOrMany

| OneOrMany = RegexExpression "+" precedence 30

Цикл, разбирающий один или более RegexExpression-элементов.

        regex Digits   = ['0'..'9']+;

Данный пример разберет:

1
42
1234567892345678934567

но не пустую строку.

RegexExpression.ZeroOrManyWithSeparator

| ZeroOrManyWithSeparator = "(" RegexExpression ";" Separator ")" "*"

Цикл с разделителем, разбирающий ноль или более элементов. Отличается от RegexExpression.ZeroOrMany тем, что элементы разбираемого списка разделяются разделителем, задаваемым подправилом Separator.

RegexExpression.OneOrManyWithSeparator

| OneOrManyWithSeparator = "(" RegexExpression ";" Separator ")" "+"

Цикл, аналогичный RegexExpression.ZeroOrManyWithSeparator, но не допускающий пустые списки.

Range

syntax Range
{
  | SingleChar   = CharLiteral
  | CharRange    = CharLiteral ".." CharLiteral
  | UnicodeRange = Name
}

SingleChar

| SingleChar = CharLiteral

См. CharLiteral.

CharRange

| CharRange    = CharLiteral ".." CharLiteral

Описывает диапазон символов с последовательно идущими кодами, начиная от первого CharLiteral и до второго CharLiteral.

Пример:

        regex Digit = ['0'..'9'];

UnicodeRange

| UnicodeRange = Identifier

Позволяет задать диапазоны символов с помощью сокращенных имен диапазонов символов UNICODE.

Примеры:

regex LetterCharacter       = [Lu, Ll, Lt, Lm, Lo, Nl];
regex CombiningCharacter    = [Mn, Mc];
regex DecimalDigitCharacter = [Nd];
regex ConnectingCharacter   = [Pc];
regex FormattingCharacter   = [Cf];

CharLiteral

      token CharLiteral = "\'" CharPart "\'";

Разбирает один символ, заданный символьным литералом в стиле «C».

StringLiteral

      token StringLiteral = "\"" StringPart* "\"";

Разбирает последовательность символов, заданную строковым литералом в стиле «C».

Объект AST

Nitra создает не только парсер, но и автоматически генерирует AST для syntax- и token-правил. Это предъявляет дополнительные требования к правилам Nitra. Они должны не только описывать грамматику, но и предоставлять информацию для формирования классов AST. Для каждого syntax- и token-правила, не помеченного атрибутом NoAst, создается ветка AST с именем, соответствующим имени правила (при этом имя приводится к формату Pascal Case).

Поля AST

Полями AST-классов становятся подправила.

Имена полей можно задать явно. Если этого не сделать, Nitra попытается сформировать имя автоматически из содержимого подправила.

Задать имя для подправила явно можно одним из следующих способов:

Если имя поля не задано явно, Nitra пытается вычислить его автоматически. Ниже приведено описание алгоритма вычисления имен:

Библиотека Nitra.Runtime.dll

Библиотека Nitra.Runtime.dll содержит базовые классы, необходимые для работы парсеров, генерируемых Nitra. Ниже описаны основные типы этой библиотеки.

NSpan

Структура, хранящая отрезок текста (его начальную и конечную позицию). Конечная позиция указывает на символ, идущий непосредственно за отрезком. Если начальная позиция равна конечной, то отрезок является пустым.

namespace Nitra
{
  [Record, StructuralEquality]
  public struct NSpan
  {
    public StartPos : int { get; }
    public EndPos   : int { get; }
    public Length   : int { get; }
    public IsEmpty  : bool { get; }

    public IntersectsWith(start : int, end : int) : bool;
    public IntersectsWith(other : NSpan)         : bool;
    public IntersectsWith(other : Location)       : bool;
    public Intersection(start : int, end : int)   : ValueOption[NSpan];
    public override ToString() : string;
  }
}

Location

Класс Nitra.Location описывает местоположение в исходном файле. Он хранит информацию об отрезке текста и ссылку на исходный код (в виде SourceSnapshot).

public struct Location : IEquatable[string]
{
  public Source   : SourceSnapshot { get; }
  public StartPos : int            { get; }
  public EndPos   : int            { get; }
  public NSpan    : NSpan          { get; }
  public Length   : int            { get; }

  public this(source : SourceSnapshot, startPos : int, endPos : int);
  public this(source : SourceSnapshot, tok : NSpan);
  public this(source : SourceSnapshot, a : NSpan, b : NSpan);

  public static Default : Location { get; }

  public IsEmpty         : bool { get; }
  public IsTextAvailable : bool { get; }

  public GetText() : string;

  public StartLineColumn : LineColumn { get; }
  public EndLineColumn   : LineColumn { get; }

  public IntersectsWith(start : int, end : int) : bool;
  public IntersectsWith(other : Location)       : bool;
  public IntersectsWith(other : NSpan)         : bool;
  public Intersection(start : int, end : int)   : ValueOption[Location];

  public override ToString() : string;

  public Equals(str : string) : bool
    implements IEquatable[string].Equals;

  public Combine(other : Location) : Location;

  public static @+ (a : Location, b : Location) : Location;
  public static @==(a : Location, b : string)   : bool;
  public static @==(a : string,   b : Location) : bool;
  public static @!=(a : Location, b : string)   : bool;
  public static @!=(a : string,   b : Location) : bool;
}

SourceSnapshot

public class SourceSnapshot
{
  public OriginalText : string { get; }
  public Text         : string { get; }
  public FileIndex    : int    { get; }
  public FileName     : string { get; }

  public this(original : SourceSnapshot);    

  public this(originalText : string, 
              text         : string,
              fileIndex    : int, 
              fileName     : string,
              lineIndexes  : array[int], 
              textOffset   : int);

  public this([NotNull] text : string, fileIndex : int = 0, 
              fileName : string = "");

  public static Default : SourceSnapshot { get; }

  public PositionToLineColumn(pos : int) : LineColumn;
  public GetSourceLine(pos : int) : Location;

  public WithText([NotNull] text : string) : SourceSnapshot;
  public WithText([NotNull] text : string, textOffset : int) : SourceSnapshot;

  public override ToString() : string;
}

Ast

public abstract class Ast : Located
{
  public this(location : Location);
  public override ToString() : string;
  public ToString(options : PrettyPrintOptions) : string;
  public virtual PrettyPrint(writer : PrettyPrintWriter, 
                             callerBindingPower : int) : void;
}

Located

public class Located
{
  public static @:(located : Located) : Location;

  public this(location : Location);

  public Location : Location { get; }

  public GetText() : string;
  public GetText(token : NSpan) : string;
  public FirstChar(token : NSpan) : char;
  public IntersectsWith(start : int, end : int) : bool;
  public IntersectsWith(other : NSpan)         : bool;
  public Intersection(start : int, end : int)   : ValueOption[Location];
}

Стандартная библиотека Nitra.Core.dll

Ниже перечислены синтаксические модули, входящие в стандартную библиотеку Nitra.Core.dll. Их использование позволяет сократить количество правил в своих грамматиках.

Модуль Whitespaces

syntax module Whitespaces
{
  using Identifiers;

  regex Any = ['\u0000'..'\uFFFF'];

  regex UnicodeLineSeparator = '\u2028';
  regex UnicodeParagraphSeparator = '\u2029';
  regex NewLineCharacter = '\n' | '\r' | UnicodeLineSeparator 
                           | UnicodeParagraphSeparator;
  regex NewLine = "\r\n" | NewLineCharacter;

  regex Whitespace = [Zs] | '\t' | '\v' | '\f' | '\uFEFF';
  regex Space = Whitespace | NewLineCharacter;
  regex Spaces = Space+;

  token IgnoreToken
  {
    | SpaceToken = Spaces;
  }

  void s = IgnoreToken*;                // optional spacer
  void S = !IdentifierPartCharacters s; // identifier break spacer
}

Модуль TokenNames

syntax module TokenNames
{
  literal OpenBrace        = "(", "{";
  literal CloseBrace       = ")", "}";
  literal OpenSquareBrace  = "[";
  literal CloseSquareBrace = "]";
  literal SingleQuote      = "\'";
  literal Quote            = "\"";
  literal Semicolon        = ";";
  literal Comma            = ",";
  literal Dot              = ".";
  literal Colon            = ":";
  literal Question         = "?";
  literal At               = "@";
  literal Assign           = "=";
  literal Prefix           = "\\u";
  literal Op               = "=>", "*=", "/=", "%=", "+=", "-=", "<<=",
                             ">>=", "&=", "^=", "|=", "==",
                             "??", "||", "&&", "=", "|", "&", "!=", 
                             "<=", ">=", "<<", ">>", "<", ">", "^",  
                             "+", "-", "!", "~", "++", "--", "*", "/", 
                             "%", "->";
  literal Backslash        = "\\";
  literal Epsilon          = "";
}

Модуль Identifiers

syntax module Identifiers
{
  regex LetterCharacter           = [Lu, Ll, Lt, Lm, Lo, Nl];
  regex CombiningCharacter        = [Mn, Mc];
  regex DecimalDigitCharacter     = [Nd];
  regex ConnectingCharacter       = [Pc];
  regex FormattingCharacter       = [Cf];

  regex IdentifierStartCharacter  = LetterCharacter | "_";
  regex IdentifierPartCharacters  = LetterCharacter 
                                  | DecimalDigitCharacter
                                  | ConnectingCharacter 
                                  | CombiningCharacter
                                  | FormattingCharacter;
  regex IdentifierBody            = IdentifierStartCharacter 
                                    IdentifierPartCharacters*;
}

Модуль StandardSpanClasses

syntax module StandardSpanClasses
{
  using Identifiers;

  span class Comment;
  span class Char;
  span class String;
  span class Number;
  span class Keyword      = ['a'..'z', '_']['a'..'z', '_']+;
  span class Operator     = ['+', '-', '/', '*', '^', '!', '?', ':', 
                             '<', '=', '>', '|', '&', '~', '%']+;
  span class OpenBrace    = "(" | "{" | "[";
  span class CloseBrace   = ")" | "}" | "]";
}

Модуль PrettyPrint

syntax module PrettyPrint
{
  marker i;   // увеличить отступ
  marker d;   // уменьшить отступ
  marker nl;  // новая строка
  marker inl; // i + nl
  marker sm;  // напечатать пробел
}

Модуль Outline

syntax module Outline
{
  marker outline_impl;
  marker outline_begin;
  marker outline_end;
  marker outline_begin_before;
  marker outline_end_before;
  marker outline_hiden;
}

Модуль CStyleComments

syntax module CStyleComments
{
  using Whitespaces;

  token SingleLineComment = Start="//" Body=(!NewLine Any)* 
                            End=NewLine?;
  token MultiLineComment  = Start="/*" Body=(!"*/" Any)*    End="*/";
}


Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.
    Сообщений 0    Оценка 330        Оценить