[Nitra] Пример простого языка вычисляющего выражения
От: VladD2 Российская Империя www.nemerle.org
Дата: 02.12.15 07:31
Оценка: 21 (2)
#Имя: wiki.nitra.SimpleLanguage
В качестве примера опишу создание простого (но тем не менее не тривиального) языка.

Этот язык создан в целях демонстрации того чтобы на простом примере продемонстрировать как использовать AST, декларации, символы, области видимости (Scope's) и зависимые свойства для типизации. Для понимания проходящего сначала рекомендую прочесть Описание подсистемы сбора информации «Nitra»
Автор(ы):
.

Для начала пример кода на этом языке:
var x = y + 2 * z;
var z = y + 3;
var y = 1;


Данный язык состоит из объявлений переменных которые видны глобально. "Глобально" значит что из выражений можно обращаться как к переменным объявленным выше текущей, так и ниже (а возможно и в другом файле).
Переменные можно инициализировать произвольным выражением. Причем выражения вычисляются не в порядке их написания, а в порядке зависимостей. Зацикленным выражения не вычисляются.

Особенность данного примере в том, что он не порождает ничего. Он только демонстрирует расчеты на зависимых свойствах. Аналогом этому примеру является свертка констант (constant folding) в языках программирования общего назначения.

Весь код (без комментариев) этого примера доступен здесь.

Для начала опишем синтаксис:
[cut]
syntax module SampleSyntax
{
  using Nitra.Core; // https://github.com/JetBrains/Nitra/blob/master/Nitra/Nitra.Runtime/Core.nitra
  using Nitra.CStyleComments; // https://github.com/JetBrains/Nitra/blob/master/Nitra/Nitra.Runtime/CStyleComments.nitra

  // директива keyword regex приказывает генератору парсеров вставлять правило S за литералами
  // соответствующими регулярной грамматике ['a'..'z', '_'..'_']+. Это нужно, чтобы парсер не
  // распознавал ключевое слово "var" в конструкции вида "varx= 42", а считал varx единым идентификатором.
  keyword regex ['a'..'z', '_'..'_']+ rule S;

  regex Keyword = "var";

  // это и последующее правило описывает идентификатор языка.
  // В Нитре поддерживается автоматическое связывание имен, по этому идентификатор 
  // разделяется на правило объявляющее имя символа (в данном случае "Name")
  // и правило описывающее ссылку на имя (в данном случае "Reference").
  // Атрибут "Name" производит автоматическое отображение идентификатора на предопределенный
  // в библиотеке Nitra.Runtime.dll ast типа Name: 
  // https://github.com/JetBrains/Nitra/blob/master/Nitra/Nitra.Runtime/Declarations/Name.nitra.
  [Name]
  token Name = !Keyword IdentifierBody;

  // Атрибут "Reference" производит автоматическое отображение идентификатора на предопределенный
  // в библиотеке Nitra.Runtime.dll ast типа Reference:
  // https://github.com/JetBrains/Nitra/blob/master/Nitra/Nitra.Runtime/Declarations/Reference.nitra.
  [Reference]
  token Reference = !Keyword IdentifierBody;

  // Стартовое правило. Разбирает 0 или более переменных (описанных в правиле VariableDeclaration).
  // nl - это маркер указывающий претипринтеру, что переменные нужно разделять переносом строки.
  [StartRule]
  syntax TopRule = (VariableDeclaration nl)*;

  // Правило описывающее переменную. Обратите внимание, что для указания имени мы 
  // использовали правило Name - это нужно для автоматического порождения символа для переменной.
  syntax VariableDeclaration = "var" sm Name sm "=" sm Expression ";";

  // Expression - Выражение нашего языка. В Expression описывается несколько альтернатив.
  // В Nitra единственный способ сделать набор альтернатив (т.е. связать правила по ИЛИ)
  // это описать расширяемое правило. Каждая альтернатива отделяется знаком "|" (шпалой, как мы ее называем).
  syntax Expression
  {
    | [SpanClass(Number)] Num = Digits // альтернатива разбирающая число
      {
        regex Digits = ['0'..'9']+; // вложенное правило типа regex разбирающее последовательность цифр - число
      }

    | Braces = "(" Expression ")"; // очевидно - скобки

    | Variable = Reference; // ссылка на другие переменные. 

  precedence Sum: // группа приоритетов для операторов "+" и "-"
    | Sum = Expression sm Operator="+" sm Expression; // оператор "+". 
    | Sub = Expression sm Operator="-" sm Expression; // sm - это маркер заставляющий претипринтер напечатать пробел

  precedence Mul: // Mul имеет более высокий приоритет нежели Sum но более низкий чем Unary
    | Mul = Expression sm Operator="*" sm Expression; // конструкция "Operator=" задает имя образуемому подправилом полю
    | Div = Expression sm Operator="/" sm Expression;

  precedence Unary:
    | Plus  = Operator="+" Expression
    | Minus = Operator="-" Expression
  }
}

[/cut]
Скормив этот файл nitra и получив dll мы уже получим возможность забирать код на нашем языке, подсветку и претипринт.

Чтобы наш можно было использовать из IDE или из тестовой утилиты нужно написать еще один файл Sample-Language.nitra с описанием языка:
language Sample
{
  syntax module SampleSyntax start rule TopRule;
}

Здесь SampleSyntax — это имя описанного выше синтаксического модуля, а TopRule имя стартового правила. Еще в этом файле может быть множество информации: расширение файла, информация об авторе, информация о стилях подсветки и т.п. Как это выглядит для более серьезного языка можно посмотреть здесь. Описание языка для самой Nitra.

Теперь нам нужно писать AST нашего языка в которых будут располагаться вычисления необходимые нам. AST находится в файле Sample-Ast.nitra:
[cut]
using Nitra;
using Nitra.Declarations;
using Nitra.Runtime.Binding;

// Стартовый AST. Обычно он называется CompileUnit, так 
// как описывает код отдельного файла.
ast Top
{
  // Зависимое свойство хранящее TableScope. TableScope находится в библиотеке 
  // и предоставляет функциональность таблицы имен. По совместительству TableScope
  // является областью видимости, и может использоваться в формировании более 
  // сложных областей видимости.
  // Значение этого свойства задается ивзне. В реализации интерфейса IProjectSupport (см. ниже)
  in ContainingTable : TableScope;

  // Данное присваивание присваевает таблицу имен на которую ссылается ContainingTable
  // всем AST-ам переменных (их свойствам ContainingTable). Variables - это структурная 
  // коллекция, а у них в Nitre автоматически генерируются свойства имеющие тот же тип и имя, 
  // что свойство у элемента коллекции. Подробнее см.
  // http://rsdn.ru/article/nitra/Nitra-Ast-and-Symbols-doc.xml#EAEAC
  Variables.ContainingTable = ContainingTable;

  // Структурное поле Variables хратит список AST-ов переменных.
  // Значение этого поля заполняется путем отображения на него дерева разбора.
  // Как это делается можно будет увидеть ниже.
  Variables : Variable*;
}

// Variable описывает декларацию переменной в нашем языке.
// Декларация - это особый вид AST, который порождает новый символ.
// Для описания декларации используем синтаксис "declaration", а не "ast".
// Декларации неявно унаследованы от Declaration:
// https://github.com/JetBrains/Nitra/blob/master/Nitra/Nitra.Runtime/Declarations/Declaration.n
// В Declaration объявлено структурное свойство Name и зависимое свойство ContainingTable.
// Для каждой декларации автоматически создается соответствующий ей символ.
declaration Variable
{
  // Декларация может содержать секцию symbol, в которой описываются зависимые 
  // свойства символа поражаемого деклараций. Собственно задача типизации вычислить
  // и собрать данные с AST и поместить их в свойства символов. Набор символов и будет
  // детальным описанием нашей программы.
  symbol
  {
    Kind = "var"; // Kind - это предопределенное свойство используемое при визуализации.

  // Директива stage позволяет задать стадию на которой могут быть вычисленные зависимые 
  // свойства объявленные после нее. Стадии нумеруются с нуля.
  stage 1: 
    // зависимое свойство которое будет хранить результат вычисления 
    // инициализирующего выражения.
    in Result : double; 
  }

  // Зависимое свойство ContainingTable унаследовано от неявного предка Declaration.
  // По этому нам не надо объявлять его явно.
  //in ContainingTable : TableScope;
  // Это свойство используется Nitra для размещения символа автоматически создаваемого 
  //по декларации. Символ будет создан как только будет задано значение этого свойства.

  // Передаем область видимости выражению. Она будет использоваться для автоматического
  // связывания имен.
  Expression.Scope = ContainingTable;
  // Помещаем результат вычисления выражения в свойство Result символа порождаемого для переменной.
  Symbol.Result    = Expression.Result;

  // Структурное свойство содержащее AST выражения инициализирующего переменную. 
  // Заполняется при отображении (см. отображение ниже).
  Expression : Expression;
}

// Абстрактный базовый AST описывающий выражение.
abstract ast Expression
{
stage 1:
  // Обратите внимание, на то, что Scope выражения задан на стадии 1. Это приводит к тому, что к
  // моменту начала его использования в Scope попадут все символы переменных имеющиеся в программе.
  // Так же обратите внимание на то, что тип данного свойства "Scope". Scope - это абстрактный тип
  // объявленный в библиотеке. Все области видимости обязаны наследоваться от него. Это поволяет
  // формировать сложную структуру областей видимости. Подробнее см. раздел 
  // "Области видимости, таблицы имен и связывание"
  // http://rsdn.ru/article/nitra/Nitra-Ast-and-Symbols-doc.xml#EPFAE
  in  Scope  : Scope;
  out Result : double;
}

// AST хранящий значение числа (числового литерала) в нашем языке.
// Он унаследован от Expression и не является абстрактным (может быть создан его экземпляр).
ast Number : Expression
{
  // Берем значение числа или 0, если его не удалось разобрать.
  Result = Value.ValueOrDefault;

  // Value - это структурно поле заполняемое при отображении. Его реальный тип ParsedValue<double>.
  // ParsedValue<> оборачивает значения простых типов решая две задачи:
  // 1. Добавляет к значению координаты откуда оно было разобрано.
  // 2. Позволяет хранить информацию о том, что значение не удалось разобрать.
  Value : double;
}

// Так как в нашем языке есть несколько типов выражений с похожей структурой AST,
// объявляем для них абстрактный базовый AST с именем "Binary".
abstract ast Binary : Expression
{
  // назначаем область видимости обоим подвыражениям.
  Expression1.Scope = Scope;
  Expression2.Scope = Scope;

  // структурные свойства описывающие два подвыражения.
  Expression1 : Expression;
  Expression2 : Expression;
}

// теперь в конкретных AST нам остается описать только вычисление зависимого
// свойства Result (описанного в Expression). Структурные свойства Expression1 и Expression2 у нас уже есть.
ast Sum : Binary { Result = Expression1.Result + Expression2.Result; }
ast Sub : Binary { Result = Expression1.Result - Expression2.Result; }
ast Mul : Binary { Result = Expression1.Result * Expression2.Result; }
ast Div : Binary { Result = Expression1.Result / Expression2.Result; }

// аналогично поступаем для выражений с одним подвыражением...
abstract ast Unary : Expression
{
  Expression.Scope = Scope;
  
  Expression : Expression;
}

ast Plus  : Unary { Result = Expression.Result; }
ast Minus : Unary { Result = -Expression.Result; }

// VariableRef - пожалуй это самая интересная часть описания AST. 
// Это описание ссылки на значение другой переменной в выражении.
ast VariableRef : Expression
{
  // Ref<> - это специальный тип позволяющий выразить результат разрешения имен или связывания.
  // Этот типа позволят хранить 
  // 1. Обычную ссылку на символа (при удачном связывании/разрешении имен).
  // 2. Информацию о том, что связывание/разрешение имен прошли ну дачно (символ не определен, Unresolved).
  // 3. При связывании или разрешении имен было найдено более одного символа с тем же именем (неоднозначность, ambigouty).
  // Метод Ref<>.Resolve() позволяет произвести разрешение имер (обычно выражающееся в фильтрации 
  // информации некоторым алгоритмом.
  // Reference.Ref (объявленный в библиотеке) имеет тип Ref[DeclarationSymbol]. См.
  // https://github.com/JetBrains/Nitra/blob/master/Nitra/Nitra.Runtime/Declarations/Reference.nitra 
  // Нам же нужно убедиться, что связывание произошло не с каким-то любым символом, а с символом 
  // типа VariableSymbol (образованного декларацией Variable, см. выше).
  // Метод Resolve производит эту операцию и в случае успеха помещает в зависимое свойство Ref результат.
  out Ref : Ref[VariableSymbol] = Reference.Ref.Resolve();

  // передает область видимости Reference-у. Это приведет к тому, что в нем автоматически произойдет
  // связывание ссылки на имя с символом. При этом результат помещается в свойство Reference.Ref.
  Reference.Scope = Scope;
  // Обращаемся к значению свойства Result символа с которым было связано имя. 
  // Обратите внимание, что этот код выполнится только при успешном разрешении имени
  // (при выполнении Reference.Ref.Resolve()). В ином случае значение свойства Ref.Symbol останется не вычисленным,
  // соответственно не будет вычесано и значение зависимого свойства Result (и так по цепочке вверх).
  Result     = Ref.Symbol.Result;

  // Структурное свойство значение которого получается при отображении.
  // Отображение для AST Reference генерируется автоматически, так как
  // на правило Reference был повешен атрибут "Reference".
  Reference : Reference;
}

[/cut]

AST и расчеты на нем готовы, но AST не появится сам собой. Нам нужно проецировать дерево разбора на него, чтобы Nitra автоматически строила AST для кода на нашем языке.
Отображение (оно же проекция, оно же маппинг) находится в файле Sample-Mapping.nitra.
[cut]
using Nitra;
using Nitra.Runtime;
using Nitra.Declarations;
using Nitra.Runtime.Binding;

// Отображаем дерево разбора правила TopRule из синтаксического модуля SampleSyntax на AST типа Top (см. их объявления выше).
map syntax SampleSyntax.TopRule -> Top
{
  // отображаем поле TopRule.VariableDeclarations (дерева разбора) на структурное свойство Top.Variables
  VariableDeclarations -> Variables;
}

// здесь все аналогично...
map syntax SampleSyntax.VariableDeclaration -> Variable
{
  // в AST Variable нет объявления поле Name. Оно неявно унаследовано от базового типа 
  // Declaration описанного в библиотеке. Но проецировать значения нужно и на свойства
  // базовых типов.
  Name       -> Name;
  Expression -> Expression;
}

// Отображение для расширяемого правила (описывающего ИЛИ в грамматике Nitra).
// Здесь опять же все похоже на самое первое отражение. Сначала указывается тип дерева 
// разбора (имя правил), а затем имя AST-типа.
map syntax SampleSyntax.Expression -> Expression
{
  // описание отображения для каждого расширения правила SampleSyntax.Expression
  // Num - это имя расширяемого правила (SampleSyntax.Expression.Num) 
  // Number - это имя AST-типа в который происходит отображение.
  | Num -> Number
    {
      // для вычисления значение структурного свойства Value используется выражение на Nemrle.
      // Мы это называем ручное или программное отображение. Оно нужно потому что Nitra не умеет
      // отображать распарсенные значения в double.
      // Значение типов не являющихся AST в Nitra должны быть обернуты в тип ParsedValue<>.
      // При этом ему передается позиция значения в коде (поле Digits дерева разбора имеет тип NSpan),
      // а так же самое значение.
      Value = ParsedValue(Digits, double.Parse(GetText(Digits)));
    }

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

  | Braces -> Expression
  | Variable -> VariableRef
    {
      Reference -> Name;
    }
  | Sum
    {
      Expression1 -> Expression1;
      Expression2 -> Expression2;
    }
  | Sub
    {
      Expression1 -> Expression1;
      Expression2 -> Expression2;
    }
  | Mul
    {
      Expression1 -> Expression1;
      Expression2 -> Expression2;
    }
  | Div
    {
      Expression1 -> Expression1;
      Expression2 -> Expression2;
    }
  | Plus
    {
      Expression -> Expression;
    }
  | Minus
    {
      Expression -> Expression;
    }
}

[/cut]

Последний штрих... Для того чтобы написанные нами вычисления работали нам нужно написать небольшой обвязочный код, который запустит их и проинициализирует ContainingTable у корневых элементов AST-а (т.е. у Top). Пока что в Nitra это делается так. Нужно создать паршал-класс корневого элемент AST и реализовать в нем интерфейс IProjectSupport. Вот как это сделано в данном примере (файл Main.n):

[cut]
public partial class Top : AstBase, IProjectSupport
{
  public RefreshProject(project : Project) : void
  {
    // получаем файлы проекта
    def files   = project.Files.ToArray(); 
    // неведанная хфигня :) необходимая для вычисления зависимых свойств
    def context = DependentPropertyEvalContext(); 
    // Таблица имен, она же глобальная область видимости (Scope).
    // В ней будут объявлены все наши ременные. 
    def scope   = TableScope("Variables", null);

    foreach (file in files)
      when (file.Ast is Top as top)
        // задаем глобальную область видимости каждому Top AST нашего проекта.
        top.ContainingTable = scope;

    // Прогоняем первую стадию вычислений. На ней будут сформированы 
    // и помещены в таблицу имен символы переменных.
    AstUtils.EvalProperties(context, files, "Collect variables", 0);
    // Прогоняем вторую стадию. На ней будут произведены вычисления и получен результат.
    AstUtils.EvalProperties(context, files, "Compute variables", 1);
  }
}

[/cut]

Заключение

Данный пример аналогичен свертке констант (constant folding) применяемому в большинстве языков программирования. Он изначально игрушечный и не предполагает компиляции в машинный код и т.п. Его цель показать как производятся расчеты на зависимых свойствах.

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

Посмотреть на результат можно поместив полученную dll в нашу тестовую утилиту (Nitra.Visualizer.exe). Так же пример будет работать и в IDE, но там будет доступна только подсветка и навигация, а результат расчетов увидеть не удастся.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Отредактировано 14.01.2017 23:36 VladD2 . Предыдущая версия . Еще …
Отредактировано 14.01.2017 23:12 VladD2 . Предыдущая версия .
Отредактировано 17.12.2015 20:58 VladD2 . Предыдущая версия .
Отредактировано 04.12.2015 16:03 VladD2 . Предыдущая версия .
Отредактировано 02.12.2015 9:35 VladD2 . Предыдущая версия .
Отредактировано 02.12.2015 9:28 VladD2 . Предыдущая версия .
Отредактировано 02.12.2015 9:21 VladD2 . Предыдущая версия .
Отредактировано 02.12.2015 9:07 VladD2 . Предыдущая версия .
Re: [Nitra] Пример простого языка вычисляющего выражения
От: _NN_ www.nemerleweb.com
Дата: 02.12.15 07:32
Оценка:
Здравствуйте, VladD2, Вы писали:

Вот такие вот статьи надо ставить не только сюда, но и в блог плюс ссылку в твиттер.
http://rsdn.nemerleweb.com
http://nemerleweb.com
Re: [Nitra] Пример простого языка вычисляющего выражения
От: _NN_ www.nemerleweb.com
Дата: 02.12.15 07:35
Оценка:
Здравствуйте, VladD2, Вы писали:

Тут тоже стоит добавить комментарии
// Это понятно
syntax module SampleSyntax
{
  // Это понятно, но неясно что получаем
  // Только ниже по коду догадываемся, что nl , sm оттуда .
  using Nitra.Core;
  // А это зачем в данном примере ?
  using Nitra.CStyleComments;

  // Вот это совсем неясно для чего
  keyword regex ['a'..'z', '_'..'_']+ rule S;
http://rsdn.nemerleweb.com
http://nemerleweb.com
Отредактировано 14.01.2017 23:15 VladD2 . Предыдущая версия .
Re[2]: [Nitra] Пример простого языка вычисляющего выражения
От: hardcase Пират http://nemerle.org
Дата: 02.12.15 08:45
Оценка: 9 (1)
Здравствуйте, _NN_, Вы писали:

_NN>
_NN>  // Вот это совсем неясно для чего
_NN>  keyword regex ['a'..'z', '_'..'_']+ rule S;
_NN>


Так как парсеры у нас безлексерные, мы вынуждены расставлять в пользовательский код так называемые пробельные правила — s и S. s правило опционально съедает пробелы, концы строк и еще ряд специальных символов. S правило вызывает s, но сперва проверяет, что на текущей позиции конец идентификатора (предикат !IdentifierPartCharacters).
keyword regex — это директива генератору парсеров, которая сообщает какие строковые литералы, используемые в правилах идущих ниже по тексту в текущем синтаксическом модуле, являются ключевыми словами. После таких строковых литералов будет вставлено правило S. Вероятно ее нужно назвать как-то понятнее, либо вообще избавиться от нее.
Эти приседания нужны чтобы парсер не принимал текст вида:
varx = 10;

Без этой директивы он благополучно распознает ключевое слово var и идентификатор x.
/* иЗвиНите зА неРовнЫй поЧерК */
Re[2]: [Nitra] Пример простого языка вычисляющего выражения
От: VladD2 Российская Империя www.nemerle.org
Дата: 02.12.15 09:38
Оценка:
Здравствуйте, _NN_, Вы писали:

Нечаянно запостил тему раньше времени (не дописав). Ты как-то очень быстро отреагировал. Посмотри тему еще раз, так как я ее дописал.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[2]: [Nitra] Пример простого языка вычисляющего выражения
От: VladD2 Российская Империя www.nemerle.org
Дата: 02.12.15 09:48
Оценка:
Здравствуйте, _NN_, Вы писали:

_NN> // А это зачем в данном примере ?

_NN> using Nitra.CStyleComments;

Это добавляет поддержку комментариев. В прочем штука для данного примера не особо нужная.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re: [Nitra] Пример простого языка вычисляющего выражения
От: Kolesiki  
Дата: 04.12.15 00:59
Оценка: +4
Здравствуйте, VladD2, Вы писали:

VD>В качестве примера опишу создание простого (но тем не менее не тривиального) языка.


Влад, откровенно, "простой язычок" выливается в весьма непростой набор описаний, выходящих далеко за рамки PEG. Понятно, что каждая строчка там — что-то полезное, без чего волшебство не работает, но не окажется ли так, что уже средней сложности язык утонет в умопомрачительном количестве директив и подсказок Нитре?
Когда Нитра начиналась, мне казалось, что она наоборот — должна быть проще Немерли.
Конструктор-всемогутер (в силу своей идеи) не может не захватывать всё, что только может переварить, но мне кажется работа "в лоб" (с простым деревом разбора) может оказаться проще, чем указывание Нитре в каждый аспект.
АСТ — да, полезная вещь, но если даже имеется просто дерево разбора, можно написать всяких вспомогательных методов, дающих ту же инфу, что и АСТ, но без плясок с мэппингом.
Область компилеростроения и так сложная вещь, так что если её ещё больше усложнять, есть риск никогда не добраться до финиша.

"Близе к делю" (ц) Шакал
Можно ли как-то работать с Нитрой, но без АСТ маппингов? (но чтобы расширять грамматику тоже можно было)
Re: [Nitra] Пример простого языка вычисляющего выражения
От: jazzer Россия Skype: enerjazzer
Дата: 04.12.15 01:12
Оценка:
Здравствуйте, VladD2, Вы писали:

Влад, а напиши, сколько строк реального кода в каждом примере (без комментариев). А то с комментариями выглядит устрашающе, а на самом деле кода там мало.
jazzer (Skype: enerjazzer) Ночная тема для RSDN
Автор: jazzer
Дата: 26.11.09

You will always get what you always got
  If you always do  what you always did
Re[2]: [Nitra] Пример простого языка вычисляющего выражения
От: VladD2 Российская Империя www.nemerle.org
Дата: 04.12.15 12:38
Оценка:
Здравствуйте, jazzer, Вы писали:

J>Здравствуйте, VladD2, Вы писали:


J>Влад, а напиши, сколько строк реального кода в каждом примере (без комментариев). А то с комментариями выглядит устрашающе, а на самом деле кода там мало.


Там же рядом ссылки на файлы в которых комментариев нет.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[2]: [Nitra] Пример простого языка вычисляющего выражения
От: VladD2 Российская Империя www.nemerle.org
Дата: 04.12.15 15:42
Оценка:
Здравствуйте, Kolesiki, Вы писали:

K>Влад, откровенно, "простой язычок" выливается в весьма непростой набор описаний, выходящих далеко за рамки PEG. Понятно, что каждая строчка там — что-то полезное, без чего волшебство не работает, но не окажется ли так, что уже средней сложности язык утонет в умопомрачительном количестве директив и подсказок Нитре?


Боюсь ты совсем не верно оцениваешь Нитру. Главное что тебе нужно понять — Нитра это не Немерл 2.0. Нитра — это средство для создания Нимерл 2.0 (или расширяемого C#).

Ее нельзя сравнивать с Немерлом просто потому, что в Немерле нет средств позволяющих делать то что и на Нитре.

Если разобраться в том как устроен Немерл, то окажется, что буквально все до тел методов и атрибутов в Немерле захардкожено. Немерл поддерживает только символы для типов (классов, структур, интерфейсов), членов типов и локальных переменных. По сути он поддерживает только подотчетные символы. С синтаксисом дела обстаят так. Для выражений мы еще можем объявить макросы (переписывающие выражения в другие), но для каких-то конструкций верхнего уровня синтаксис почти не расширяется (есть костыль, но очень неудобный и кривой).

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

В Немерле вообще невозможно управлять областями видимости. Нитра же предоставляет решение для этого. Причем решение очень декларативное и простое в использовании.

В Немерле почти нет возможностей для описания какие-то собственные стадии компиляции и вмешаться в стадии компилятора. Нитра же рассчитана на эти сценарии изначально.

K>Когда Нитра начиналась, мне казалось, что она наоборот — должна быть проще Немерли.


Они не проще. Она позволяет решать сложные задачи проще чем на Немерле. Написав компилятор Немерла на ней мы получим существенно более простое решение. Его будет в сотни раз проще развивать и поддерживать. При этом из коробки поддерживаются плюшки IDE и снимаются всякие ограничения на расширяемость синтаксиса.

K>Конструктор-всемогутер (в силу своей идеи) не может не захватывать всё, что только может переварить, но мне кажется работа "в лоб" (с простым деревом разбора) может оказаться проще, чем указывание Нитре в каждый аспект.


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

Вот только поверь на слово — это закат солнца вручную. Сложные вещи — это связывание имен и типизация. Их проще делать специальными средствами.

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

K>АСТ — да, полезная вещь, но если даже имеется просто дерево разбора, можно написать всяких вспомогательных методов, дающих ту же инфу, что и АСТ, но без плясок с мэппингом.


Можно. Но чем сложнее задача, тем сложнее будет выглядеть решение на дереве разбора.

За AST есть следующие аргументы:
1. Все что делается с кодом делается ради создания модели кода в виде символов. AST это промежуточное (между деревом разбора и символами) представление позволяющее упростить трансформацию информации. При трансформации прямо из дерева разбора в символы трасформация получается очень нетривиальной. Структура AST-а намного ближе к символам. Это позволяет порождать символы прямо по AST.
2. AST позволяет описать вычисления один раз, а не дублировать их.
3. AST поддерживает полиморфизм, что позволяет произвести нормальную декомпозицию AST/деклараций/символов.
4. AST компактный и его можно легко сериализовать. Это позволит оптимизировать работу плагина к IDE (не парсить весь проект при открытии, а десиреализвоать символы и AST, а так же занимать меньше памяти).
5. В AST все члены именованы. Это позволяет упросить описание вычислений.

K>Область компилеростроения и так сложная вещь, так что если её ещё больше усложнять, есть риск никогда не добраться до финиша.


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

K>Можно ли как-то работать с Нитрой, но без АСТ маппингов? (но чтобы расширять грамматику тоже можно было)


Можно. Но глупо. Это будет в разы сложнее.

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

Неужели маппинг настолько сложно выглядит?

Код вычисления на AST понятен?

ЗЫ

Спасибо за отклик!

2All: не стесняйтесь задавать вопросы. Только в обсуждении можно найти лучшие решения.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[2]: [Nitra] Пример простого языка вычисляющего выражения
От: VladD2 Российская Империя www.nemerle.org
Дата: 04.12.15 15:59
Оценка:
Здравствуйте, Kolesiki, Вы писали:

K>но не окажется ли так, что уже средней сложности язык утонет в умопомрачительном количестве директив и подсказок Нитре?


Опиши, плиз, что тебе кажется излишним? Что не понятно? В общем, по больше деталей.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[2]: [Nitra] Пример простого языка вычисляющего выражения
От: novitk США  
Дата: 04.12.15 16:08
Оценка: :)
Здравствуйте, jazzer, Вы писали:

J>а на самом деле кода там мало.


Мало? Я уверен, что реализация этой задачи на скажем ANTLR/Скале будет не больше.

Влад впрочем скажет, что этот пример такой игрушечный, а вот для Немерле 2 абсолютно необходим весь этот зоопарк ДСЛей. Тут я не уверен, но думаю что ребят занесло.
Re[3]: [Nitra] Пример простого языка вычисляющего выражения
От: VladD2 Российская Империя www.nemerle.org
Дата: 04.12.15 17:50
Оценка:
Здравствуйте, novitk, Вы писали:

N>Мало? Я уверен, что реализация этой задачи на скажем ANTLR/Скале будет не больше.


Я рад за твою уверенность. Сделай, сравним.

N>Влад впрочем скажет, что этот пример такой игрушечный, а вот для Немерле 2 абсолютно необходим весь этот зоопарк ДСЛей. Тут я не уверен, но думаю что ребят занесло.


А ты не думай. Ты сделай аналог и сравни.

У меня четкие критерии. Один и тот же код переписывается с использованием разных технологий и далее сравнивается объем кода и простота его поддержки/развития. При этом вопросов не возникает. У тебя же "уверенность" высосанная из пальца.

И таки — да, чем сложнее язык, тем более явно проявляются преимущества. Калькулятор по фигу на чем писать. Язык со сложными областями видимости уже нет. Язык с выводом типов и расширяемым синтаксисом уже и обсуждать нечего.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re: [Nitra] Пример простого языка вычисляющего выражения
От: Философ Ад http://vk.com/id10256428
Дата: 04.12.15 18:30
Оценка:
Здравствуйте, VladD2, Вы писали:

VD>В качестве примера опишу создание простого (но тем не менее не тривиального) языка.

VD>Для начала пример кода на этом языке:
VD>
VD>var x = y + 2 * z;
VD>var z = y + 3;
VD>var y = 1;
VD>


Код не читал, просто любопытствую:
а вот так:
var x = y + 2 * z;
var z = y + 3;
var y = x*z;


будет работать?

т.е. когд y неоднозначно, и решается через квадратное уравнение.
Всё сказанное выше — личное мнение, если не указано обратное.
Re[2]: [Nitra] Пример простого языка вычисляющего выражения
От: VladD2 Российская Империя www.nemerle.org
Дата: 04.12.15 18:48
Оценка:
Здравствуйте, Философ, Вы писали:

Ф>Код не читал, просто любопытствую:

Ф>а вот так:
Ф>
Ф>var x = y + 2 * z;
Ф>var z = y + 3;
Ф>var y = x*z;
Ф>

Ф>будет работать?

Так все переменные входящие в цикл останутся не вычисленными.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[3]: [Nitra] Пример простого языка вычисляющего выражения
От: Kolesiki  
Дата: 05.12.15 20:33
Оценка:
Здравствуйте, VladD2, Вы писали:

VD>Главное что тебе нужно понять — Нитра это не Немерл 2.0.


Это-то я как раз понимаю! Тут сложно перейти от мышления "реализатора языка" к "юзеру конструктора языка" (улавливаешь?). Вы-то там, в джетбрэйнсах, варитесь во всём этом и всё кажется простым и логичным но тут, по другую сторону джакузи, всё выглядит как-то переусложнённо.

Про АСТ вот чего-то не догнал:

VD> AST это промежуточное (между деревом разбора и символами)


Я думал, АСТ — это и есть сияние чистой программы, где дерево описывает все её сущности и по которым уже можно выдавать код. А символы — это просто узлы АСТ — классы, переменные... т.е. символы не являются чем-то отдельным.

VD> Опиши, плиз, что тебе кажется излишним? Что не понятно?


Ну вот представь, ты слесарь и чётко разбираешься в гайках, поршнях, можешь даже собрать двигатель, знаешь структуру всех деталей, а потом тебе предлагают собрать F1 используя "3Д-печатный конструктор болидов" — умом-то ты понимаешь, что там тоже будут известные тебе детали и узлы, но вся кухня в целом тебе абсолютно чужеродна. Вот так я (умея работать с PEG) смотрю на Нитру — всё знакомо, но ничего непонятно.

Сейчас пойдёт поток несвязных мыслей, так что можно отвечать так же несвязно

1. После ПЕГ пугает введение излишеств вроде "keyword regex". Я боюсь, что подобный "неявный помощник" заложит таймбомбу, что потом не разберёшься, почему один токен работает, а другой — нет.

2. "...по_этому идентификатор разделяется на правило объявляющее имя символа (в данном случае "Name") и правило описывающее ссылку на имя" — когда и зачем это применять? Почему я не могу просто поставить какой-нибудь крючочек и конструктор сам догадается что нужно делать?

3. Каша из keyword, regex, token, syntax — у меня уже голова кругом. Далее идёт отдельно три объявления syntax, а потом внутри syntax Expression ещё куча подправил. Как всё это разруливать? Почему все они не сделаны как syntax? Откуда столько неоднородности?
ПЕГ предлагает простую модель: терминалы и правила. Можно в Нитре оставаться в рамках модели ПЕГ?

4. Ладно, синтаксис описали — зачем ещё один Sample-Language.nitra? (причём в примере с NitraLanguage.nitra там ещё и цвета зачем-то захардкожены) Вроде б мы уже имеем "подсветку и претипринт", так мы ещё раз приседаем для какой-то другой утилиты?

5. Далее АСТ: вообще весь файл ни о чём, кроме знакомых строк, которые я уже как бы описал в SampleSyntax:
"ast Sum : Binary { Result = Expression1.Result + Expression2.Result; }"
Все эти ContainingTable, stage 1, Reference — это, вероятно, какие-то чертовски важные детали, но которые со стороны выглядят как лапша, торчащая из коллайдера. У нас простой язык: список присвоений, переменные, арифм.выражения, числа, операторы, скобки. Они структурированы в дерево. Всё. Но никакого дерева в Sample-Ast.nitra я не вижу — тону в дебрях.

6. Отображение — тут выглядит всё просто, но опять же — из-за простоты и дублирования кажется, будто делаешь одну и ту же работу трижды. Здесь всё понятно, кроме одного — что, все эти пляски с АСТ создавались из-за одной единственной фигни под названием partial class? Повторюсь, мне кажется можно взять простое дерево разбора и запрашивать у него инфу через хелперы (которые, никто не запрещает, могут построить некий внутренний АСТ для ускорения работы).

7. Ну и довершает пасквиль на могиле моего понимания "последний штрих" обвязочного кода, потому что мы и так уже предостаточно наописывали для АСТ, чтоб теперь ещё и приседать вокруг него.

Громоздко, загромождено деталями, непонятно — вот так для меня выглядит ещё простой! язык арифметики. Видимо, Нитра будет инструментом для ооочень избранных головастиков, я такой "упрощающий инструмент" едва ли постигну.
Re[3]: [Nitra] Пример простого языка вычисляющего выражения
От: Kolesiki  
Дата: 05.12.15 21:51
Оценка:
Влад, чисто политический момент: Нитра — не какая-то там саморасширяющаяся Немерля, а ажно целый конструктор таких языков! Не кажется ли тебе, что это прыжок через ступеньку? Может, сначала саму Немерлю-2 стоило начать с чистого листа, реализовать почти так же, но с учётом всего того, что вам нехватало? Ведь я так понимаю, конечная цель — не облака компилеростроения, а реальный практический язык для ДотНЕТа. Так не всё ли равно, как другие компании будут делать другие языки?
И "конструктор" тогда бы не понадобился (со всей обязательной в таких случаях универсализацией подо все языки).
Re[4]: [Nitra] Пример простого языка вычисляющего выражения
От: WolfHound  
Дата: 05.12.15 22:37
Оценка:
Здравствуйте, Kolesiki, Вы писали:

K>Влад, чисто политический момент: Нитра — не какая-то там саморасширяющаяся Немерля, а ажно целый конструктор таких языков! Не кажется ли тебе, что это прыжок через ступеньку? Может, сначала саму Немерлю-2 стоило начать с чистого листа, реализовать почти так же, но с учётом всего того, что вам нехватало?

Там не хватает примерно всей нитры.
... << RSDN@Home 1.2.0 alpha 5 rev. 62>>
Пусть это будет просто:
просто, как только можно,
но не проще.
(C) А. Эйнштейн
Re[4]: [Nitra] Пример простого языка вычисляющего выражения
От: hi_octane Беларусь  
Дата: 06.12.15 02:55
Оценка: +1
K>Влад, чисто политический момент: Нитра — не какая-то там саморасширяющаяся Немерля, а ажно целый конструктор таких языков! Не кажется ли тебе, что это прыжок через ступеньку? Может, сначала саму Немерлю-2 стоило начать с чистого листа, реализовать почти так же, но с учётом всего того, что вам нехватало? Ведь я так понимаю, конечная цель — не облака компилеростроения, а реальный практический язык для ДотНЕТа. Так не всё ли равно, как другие компании будут делать другие языки?
K>И "конструктор" тогда бы не понадобился (со всей обязательной в таких случаях универсализацией подо все языки).

Nitra это не через ступеньку — это удаление из Nemerle всего что было захардкожено, выпрямление всех косяков и ограничений, и т.п. Nemerle — язык который можно раширять под задачу очень-очень сильно, но некоторые ограничения захардкожены и требуют очень серьёзного усилия для поддержки как в языке так и в интеграции с VS. Но как только ты хорошо подумаешь над задачей "хочу язык который можно расширять не просто очень сильно а как угодно", сразу получится что "возможность сделать из исходного языка совсем-совсем другой" — это часть определения "как угодно". Ну и отсюда сразу становится очевидно что раз уровень абстракции решения и самой задачи совпадают — значит чисто политически всё делается правильно.
Nemerle — power of metaprogramming, functional, object-oriented and imperative features in a statically-typed .NET language
Re[4]: [Nitra] Пример простого языка вычисляющего выражения
От: _NN_ www.nemerleweb.com
Дата: 06.12.15 05:37
Оценка:
Здравствуйте, VladD2, Вы писали:

VD>Я рад за твою уверенность. Сделай, сравним.


Мне кажется будет хорошей идеей привести сравнение Нитры с другими решениями.
Это сразу поставит все точки над ё.
И так будет ясно когда лучше поменять код на использование Нитри, а где можно и оставить как есть.
http://rsdn.nemerleweb.com
http://nemerleweb.com
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.