суббота, 24 марта 2012 г.

Создание собственного языка на основе DLR


Данная статья является переводом английской версии. Она была написана Lionel Laské как руководство к его проекту на CodePlex MyJScript. На момент публикации записи английская версия была не доступна, поиск в сети тоже не помог мне найти оригинал статье( возможно плохо искал). Статью перевел давно, что-то руки не доходили опубликовать. Гарантировать качество перевода не могу, как говорится чем богаты…


Введение

Иногда возникает необходимость снабдить приложение собственным языком программирования. Например дать пользователям возможность конфигурировать их собственные правила в движке программы, или разрешить им вносить изменения в вычислительные опрации. В этих случаях, как разработчику, Вам придётся создать собственный интерпретатор или компилятор и включить его в Ваш .NET код. То есть, Вам потребуются не только глубокие знания о написании сканера и парсера, у Вас также должно быть очень хорошее понимание  кодогенерации для выполнения операторов Вашего нового языка.

Dynamic Language Runtime (DLR)  является слоем поверх.NET Framework 3.5, предназначеным  для того, чтобы помочь Вам создавать динамические языки для платформы .NET. Язык, созданный с DLR, может быть встроенным в приложение (как прежде), или новым языком  платформы.NET, как IronPython или IronRuby, предоставленные Microsoft.

Вот главные цели  DLR:

  • Более легко создавать динамические языки,
  • Взаимодействовать между динамическими языками и CLR,
  • Взаимодействовать между динамическими языками, построенными на DLR.

В этой статье мы изучим шаг за шагом процесс создания нового языка на платформе DLR.

О DLR

DLR - это DLL , которая называется "Microsoft.Scripting.dll". В настоящее время DLR поставляется с SilverLight и в виде исходных кодов, со всеми динамическими языками от Microsoft (IronPython и Iron Ruby). DLR распространяется по open source лицензии Microsoft Public License.

В DLR определены пространство имён "Microsoft.Scripting" и несколько вложенных пространств имён. Вот список пространств имён, описанных в этой статье:

  • Microsoft.Scripting.Ast для построения абстрактных синтаксических деревьев,
  • Microsoft.Scripting.Hosting для хостинга DLR в Вашем приложении,
  • Microsoft.Scripting.Shell для построения собственного интерпретатора командной строки.

DLR была построена (и продолжает строиться) с каждым релизом динамических языков от Microsoft. Так что структура исходных кодов в настоящее время не вполне стабильна. Версия DLR, используемая здесь - v1.0.0.1000 поставляемая IronPython 2.0 beta 1. Я обновлю примеры, включённые в эту статью, когда будет доступна последняя версия DLR.

MyJScript

В этой статье мы воспользуемся языком MyJScript, производным от JavaScript. JavaScript очень интересен, потому что он достаточно далёк от C# и VB.NET, но не слишком сложен для написания компилятора. Вот главные особенности  MyJScript,

  • Синтаксис, основанный на JavaScript: точка с запятой, квадратные скобки, ключевые слова "var" и "function",
  • Типы данных, основанные на JavaScript: int и string поддерживаются в MyJScript. Разрешены оба способа написания строковых констант: одинарные и двойные кавычки. MyJScript строки поддерживают методы: length, substr, bold and blink,
  • Необязательное объявление переменных: для объявления переменных используется ключевое слово "var". Не декларированные переменные считаются глобальными,
  • Смешивание операторов и деклараций:  компилятор MyJScript запускает операторы в том порядке, в котором они встречаются в файле. Поэтому функции должны быть объявлены раньше, чем они будут вызваны впервые,
  • Объектно-ориентированный: вы можете создавать собственный объект, вызвав конструктор, используя ключевое слово “new. Вы можете получать и присваивать значения членам (методам и свойствам). Ключевое слово "this" позволяет обратиться к текущему экземпляру.  

Этих особенностей достаточно для того, чтобы дать нам возможность изучить основные особенности DLR. Заметьте, MyJScript – это только пример. MyJScript никак не связан с будущим "Managed JScript" от Microsoft.

Давайте посмотрим два примера MyJScript:  

  1:     function fact(n) {
  2:         if (n==0) return 1;
  3:             return n*fact(n-1);
  4:     }
  5:      write("!4 = " + fact(4));

В этом первом примере объявляется функция factorial, затем я вызываю её рекурсивно. Обратите внимание, что в последней строке значения типа int (целое число) при необходимости автоматически конвертируются в строку. Такое преобразование не разрешено в C#.

  1:     function Cat(name) {
  2:         this.name = name;
  3:         this.miaow = function () { write(this.name+' said Miaow'); };
  4:     }
  5:         a = new Cat('Felix');
  6:     a.miaow();
  7:     Cat("this");
  8:     this.miaow();

Во втором примере, я показываю объектно-ориентированные возможности MyJScript. Сначала объявляю конструктор класса "Cat" , затем создаю экземпляр этого класса. Ключевое слово "this" так же используется: в первый раз для ссылки на текущий экземпляр в конструкторе "Cat", затем для указания глобального контекста. Наконец, обратите внимание на оба способа описания строк (одинарные и двойные кавычки).


Контекст языка


Первое, что нужно сделать для создания собственного языка на DLR – это описать его «контекст». Этот контекст предоставит свойства языка (имя, id, версию, …) и определит входную точку для DLR. Далее представлен контекст языка MyJScript. Это класс производный от "Microsoft.Scripting.LanguageContext":

  1:     class MJSLanguageContext : LanguageContext
  2:     {
  3:         // Constructor: initialize "binder"
  4:         public MJSLanguageContext(ScriptDomainManager);
  5:                 // Language description
  6:         public override Guid LanguageGuid;
  7:         public override string DisplayName;
  8:         public override Version LanguageVersion;
  9:                 // Parser entry point
 10:         public override LambdaExpression ParseSourceCode(CompilerContext);
 11:                 // Look for a global symbol
 12:         public override bool TryLookupGlobal(CodeContext, SymbolId, out object );
 13:                 // Command line customization
 14:         public override ServiceType GetService<ServiceType>(params object[]);
 15:         public override string FormatException(Exception exception);
 16:     }

Конструктор класса MJSLanguageContext – наиболее важный метод. Он содержит инициализацию встроенных функций (“write” например) и создаёт языковые привязки, которые определяют все правила нашего языка.


Другой важный метод языкового контекста - ParseSourceCode. Вот метод ParseSourceCode. Для MyJScript.

  1:         public override LambdaExpression ParseSourceCode(CompilerContext context)
  2:         {
  3:             // Call MyJScript parser
  4:             Parser parser = new Parser(context.SourceUnit);
  5:             parser.Parse();
  6:              // Return the generated AST
  7:             return parser.Result;
  8:         }

ParseSourceCodeэто настоящая входная точка для интерпретатора MyJScript. ParseSourceCode принимает как параметр контекст, содержащий исходный код для исполнения и должен возвратить соответствующее ему синтаксическое дерево в виде экземпляра класса LambdaExpression. Синтаксическое дерево будет запущено в DLR.


Создание синтаксического дерева с DLR


Синтаксическое дерево – это способ иерархического представления программы на нашем языке. Каждый узел является элементом: оператором(statement) , оператором(operator), или значением. Возьмём к примеру оператор:

    res = n * (n-1);

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


dlr2_1



Пространство имён "Microsoft.Script.Ast" включает узлы AST(абстрактное синтаксическое дерево) для каждого элемента, используемого в языке, построенном на CLS (Common Language Specification). Итак, наш предыдущий пример мог бы быть построен как AST в коде следующим образом:

  1:     Ast.Statement(span,
  2:         Ast.Assign(
  3:             res,
  4:             Ast.Action.Operator(
  5:                 Operators.Multiply,
  6:                 typeof(object),
  7:                 Ast.Read(n),
  8:                 Ast.Action.Operator(
  9:                     Operators.Subtract,
 10:                     typeof(object),
 11:                     Ast.Read(n),
 12:                     Ast.Constant(1)
 13:                 )
 14:             )
 15:         )
 16:     );

Конечно, это несколько многословно. Некоторые могут заметить определённое сходство с CodeDOM API. Оба API предоставляют способ представления программ: AST в DLR – это API для генерации IL кода, в то время как CodeDOM API - это API для генерации исходного кода на in C# или VB.NET .


Построение синтаксического дерева на основе грамматики


Те из вас, кто свободно ориентируется в теории компиляторов, знают, что для построения синтаксического дерева из текста нужно выполнить следующие шаги:



  • Сканирование: задача сканера – разделить текст на известные слова. Известные слова языка MyJScript – это например: "var", "function", "if" или "+",
  • Парсинг: задача парсера – убедиться в том, что все известные слова стоят в правильном порядке,
  • Построение синтаксического дерева: синтаксическое дерево строится в памяти одновременно с парсингом.

Подробное обсуждение сканирования и парсинга выходит за рамки данной статьи. Тем не менее, если Вы интересуетесь этими вопросами, Вы можете прочитать хорошую статью Джоэла Побара (Joel Pobar) в февральском номере журнала MSDN за 2008 год. Для языка MyJScript парсер сгенерирован программой Jay, клоном Yacc. Вот интерфейс нашего парсера:

  1:     class Parser
  2:     {
  3:         public Parser (SourceUnit source)
  4:         {            ...        }
  5:                 public bool Parse()
  6:         {            ...        }
  7:         public LambdaExpression Result
  8:         {
  9:             get { return generator.Result; }
 10:         }
 11:     }

Объект парсера инициируется с помощью SourceUnit. SourceUnit – это объект DLR, используемый для представления исходного кода, полученного из командной строки или потока символов файла-исходника. В то же время оба источника могут обрабатываться по-разному. Парсинг MyJScript запускается при вызове метода Parse. Построение AST завершается вместе с парсингом. Для максимального упрощения построение AST в MyJScript выполнено в специализированном классе “Generator”.


Давайте рассмотрим пример: грамматическое правило языка MyJScript для оператора IF. Вот как это правило описано в Jay/Yacc.

    if_else: IF LPAR cond_expr RPAR block_or_statement ELSE block_or_statement 
            { $$ = generator.IfElse($3 as Expression, $5 as Expression, $7 as Expression); }
        | IF LPAR cond_expr RPAR block_or_statement 
            { $$ = generator.IfElse($3 as Expression, $5 as Expression, null); }

Вот код, сгенерированный для этого правила MyJScript генератором:

  1:      public Expression IfElse(Expression condition, Expression ifTrue, Expression ifFalse)
  2:         {
  3:             if (ifFalse == null)
  4:                 return Ast.IfThen(
  5:                     Ast.Convert(condition, typeof(bool)),
  6:                     ifTrue
  7:                 );
  8:              return Ast.IfThenElse(
  9:                 Ast.Convert(condition, typeof(bool)),
 10:                 ifTrue,
 11:                 ifFalse
 12:             );
 13:         }

Он довольно прост, поскольку мне нужно только построить соответствующие узлы для IfThen и IfThenElse. Другие операторы MyJScript создают AST точно таким же способом.


Генерация переменных


Теперь мне хотелось бы остановиться на обработке переменных в DLR. Область видимости переменных сильно зависит от используемого языка программирования. В разных языках переменные могут быть: глобальными, локальными по отношению к функции, локальными по отношению к блоку, могут также быть членами класса или экземпляра, ... Как бы то ни было, область видимости переменных всегда связана с грамматикой языка. Итак, в C# например, при использовании переменной внутри метода, область её видимости должна вычисляться из синтаксиса. Сначала я должен проверить, объявлена ли переменная внутри блока, затем является ли она параметром и наконец является ли она переменной экземпляра (или класса, в случае статического метода).


Теперь мне хотелось бы более подробно остановиться на обработке переменных в DLR. Область видимости переменных – сложная часть языка. От одного языка программирования к другому, переменные могут быть: глобальными, локальными по отношению к функции, локальными по отношению к блоку, быть членами класса или экземпляра... Как бы то ни было, области видимости переменных всегда связаны с грамматикой. Так в C# например, при использовании переменной внутри метода, область её видимости может быть выведена из синтаксиса. Вначале нужно проверить, является ли переменная локальной по отношению к блоку, затем – является ли она параметром и наконец переменной экземпляра(или класса для статических методов).


DLR позволяет декларировать переменные в объекте LambdaBuilder. LambdaBuilder – объект для построения лямбда-выражений, а так же блоков кода функций и встроенных блоков. Метод LambdaBuilder.CreateLocalVariable создаёт переменную для текущего блока. Метод LambdaBuilder.CreateParameter создаёт параметр для текущего блока.


В MyJScript переменные должны: быть объявленными локально по отношению к функции, быть параметром, или по умолчанию (из-за того что объявление переменной необязательно) быть глобальной переменной. Итак, следующий код на языке MyJScript...

  1:     a = 100;
  2:     function foo(n) {
  3:         var b = n + a;
  4:         return b;
  5:     }

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


DLR2_2



Компилятор MyJScript сохраняет области видимости переменных, используя словарь. Один словарь используется для глобальных переменных, один для локальных. Параметры в MyJScript объявляются налету при объявлении функции. Следующий код вызывается парсером когда обнаруживается начало функции.

  1:         public void BeginFunction(String name, List<String> arguments)
  2:         {
  3:             if (name != null && globalVariables.ContainsKey(name))
  4:             {
  5:                 report.Error(…); // function already exist
  6:                 return;
  7:             }
  8:              previousMethod = currentMethod;
  9:             currentMethod = Ast.Lambda(name ?? "<member function>", typeof(object));
 10:              localVariables = new Dictionary<string, Variable>();
 11:              currentMethod.CreateParameter(SymbolTable.StringToId("this"), typeof(object));
 12:                        if (arguments.Count > 0)
 13:             {
 14:                 foreach (string parameter in arguments)
 15:                     currentMethod.CreateParameter(SymbolTable.StringToId(parameter), typeof(object));
 16:             }
 17:         }

Заметьте следующее:



  • Словарь globalVariables сохраняет все глобальные переменные. Когда обнаруживается новое объявление функции, я должен убедиться, что переменной с таким же именем не существует в пределах данной области видимости. В DLR функция – это всего лишь вызываемая переменная!
  • Словарь localVariables инициируется вначале каждой функции, но не вначале каждого блока. В MyJScript, Так же как и в JavaScript переменные должны быть локальными только по отношению к функции. Для более сложных языков (вроде C#) переменные могут быть локальными по отношению к блоку. В этом случае для каждого блока кода должен использоваться собственный словарь.
  • Каждый параметр создаётся посредством вызова метода LambdaBuilder.CreateParameter. "this" добавляется как первый параметр каждой функции. Мы обсудим это далее в этой статье.
  • Наконец заметьте, что имена переменных не обрабатываются напрямую, вместо этого используются идентификаторы DLR. Метод SymbolTable.StringToId используется для преобразования имён в идентификаторы.

Использование переменных


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

  1:         public Expression Variable(string name)
  2:         {
  3:             Variable variable = null;
  4:             if (localVariables.ContainsKey(name))
  5:                 variable = localVariables[name]; // Local variable
  6:              else
  7:              {
  8:                 if (currentMethod != null)
  9:                 {
 10:                     foreach (Variable param in currentMethod.Parameters)
 11:                         if (SymbolTable.IdToString(param.Name).Equals(name))
 12:                             variable = param;
 13:                      if (variable != null)
 14:                         return Ast.Read(variable); // Parameter
 15:                 }
 16:                  if (variable == null)
 17:                 {
 18:                     if (!globalVariables.ContainsKey(name))
 19:                         return Ast.Read(SymbolTable.StringToId(name));  // Unknown variable
 20:                     variable = globalVariables[name]; // Global variable
 21:                 }
 22:             }
 23:              return Ast.Read(variable);  // Known variable
 24:         }

Сначала я смотрю словарь с локальными переменными, затем с параметрами, и наконец, с глобальными переменными. В случае глобальной переменной:



  • Если переменная найдена, возвращается узел Ast.Read с объектом переменной. Это выражение называется "BoundExpression" из-за того, что переменная связана с прочитанным выражением. Так DLR напрямую узнаёт, откуда можно получить значение переменной.
  • Если переменная не найдена, возвращается узел Ast.Read с идентификатором (идентификатор – это имя переменной в таблице символов). Это выражение называется "UnboundExpression" потому что DLR потребуется просмотр во время исполнения, когда значения переменных будут сохранены. Заметьте, что несвязанные выражения ("UnboundExpression") могут встретиться, например, когда переменные объявляются в другом языке (смотрите ниже).

Типы данных


Jim Hugunin в своём блоге сказал, что обработка типов данных была одним из основных приоритетов в архитектуре DLR. Проблема в том, что каждый язык использует свой собственный способ представления типов данных. Итак, строка символов в CLR – это объект String, но она может быть PyString в Python’е и MJSString в MyJScript.


dlr2_3



Каждый из упомянутых классов String, PyString или MJSString включает методы и свойства, в которых заключена мощь каждого из языков. Но как они могут взаимодействовать, если ни языки не предоставляет одинаковых способов для обработки этих типов данных?


Стратегия DLR состоит в использовании базовых типов. Так что строка символов должна быть объектом String, независимо от используемого языка. Допустим, DLR использует методы-расширения из .Net 3.5. Для языка MyJScript я хочу добавить к типу String кое-какие методы, такие как blink, bold и substr из JavaScript. Вот код, делающий это:

  1:     [assembly: ExtensionType(typeof(string), typeof(MyJScript.Runtime.StringExtensions))]
  2:     public static class StringExtensions
  3:     {
  4:         public static string blink(string @this)
  5:         {
  6:             StringBuilder res = new StringBuilder("<blink>");
  7:             res.Append(@this);
  8:             res.Append("</blink>");
  9:             return res.ToString();
 10:         }
 11:          public static string bold(string @this)
 12:         {
 13:             StringBuilder res = new StringBuilder("<bold>");
 14:             res.Append(@this);
 15:             res.Append("</bold>");
 16:             return res.ToString();
 17:         }
 18:          public static string substr(string @this, int index)
 19:         {
 20:             return @this.Substring(index);
 21:         }
 22:          public static string substr(string @this, int index, int length)
 23:         {
 24:             return @this.Substring(index, length);
 25:         }
 26:     }

Всего несколько строк кода и String можно обрабатывать как JavaScript String. Заметьте, что методы-расширения позволяют добавлять только методы, но не свойства. Если Вам интересно, как добавить свойства, вместо этого придётся использовать правила (смотрите ниже).


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


Подведём итог того, что мы изучили. Для запуска операторов нашего языка MyJScript, скажем  «1+1» мне всего лишь надо возвратить синтаксическое дерево из входной точки языкового контекста ParseSourceCode. Теперь я должен возвратить:

  1:     Ast.Action.Operator(
  2:         Operators.Add,
  3:         typeof(object),
  4:         Ast.Constant(1),
  5:         Ast.Constant(1)
  6:     )

Что мне нужно, чтобы запустить этот код? Ничего! Когда это синтаксическое дерево передаётся DLR, она автоматически переводит его в IL-код и запускает. В данном случае DLR просто вызовет суммирование двух целых чисел.


Это работает потому, что DLR уже известны стандартные правила для большинства типов данных. Поэтому мне не требуется писать что-то ещё.

Теперь давайте изменим мой пример на «1 + 'A'». Этот код является валидным MyJScript оператором потому что, как и JavaScript, MyJScript обеспечивает неявное преобразование между целыми числами и строками. Вот синтаксическое дерево: 
  1:     Ast.Action.Operator(
  2:         Operators.Add,
  3:         typeof(object),
  4:         Ast.Constant(1),
  5:         Ast.Constant("A")
  6:     )

Ну вот, это не работает! DLR не знает как сложить строку с целым числом. DLR не известно это правило, так что мы научим её ему.


Правила – это часть объекта Binder, создаваемого во время инициализации контекста языка. Ниже извлечение из класса MJSBinder:

  1:    public class MJSBinder : ActionBinder
  2:     {
  3:         public MJSBinder(CodeContext context) : base(context)
  4:         {
  5:         }
  6:          protected override StandardRule<T> MakeRule<T>(CodeContext callerContext, DynamicAction action, object[] args)
  7:         {
  8:             if (operation.Operation == Operators.Add
  9:               && args[0] is int
 10:               && args[1] is string)
 11:             {
 12:                 // Rule to add string and int in MyJScript
 13:                 return MakeAddStringRule<T>(callerContext, action, args);
 14:             }
 15:              // Leave DLR find a rule
 16:             return base.MakeRule<T>(callerContext, action, args);
 17:          }
 18:      ...
 19:     }

Сердце MJSBinder’а – метод MakeRule. MakeRule вызывается всякий раз, когда DLR находит выражение, которое она не может понять как запускать. Метод MakeRule  должен возвращать правило: условие запуска правила и код, который следует запустить. Обе части правила являются AST как мы видели ранее. Вот код метода MakeAddStringRule:

  1:         private StandardRule<T> MakeAddStringRule<T>(CodeContext callerContext, DynamicAction action, object[] args)
  2:         {
  3:             StandardRule<T> rule = new StandardRule<T>();
  4:              // Same as: (p0 is int) && (p1 is string)
  5:             rule.Test =
  6:                 Ast.AndAlso(
  7:                     Ast.TypeIs(rule.Parameters[0], typeof(int)),
  8:                     Ast.TypeIs(rule.Parameters[1], typeof(string))
  9:                 );
 10:              // Same as: string.Contat(p0.ToString(), p1)
 11:             rule.Target =
 12:                 rule.MakeReturn(this,
 13:                     Ast.Call(
 14:                         typeof(string).GetMethod("Concat", new Type[] { typeof(string), typeof(string) }),
 15:                         Ast.Call(
 16:                             Ast.Convert(rule.Parameters[0], typeof(object)),
 17:                             typeof(object).GetMethod("ToString", new Type[0])
 18:                         ),
 19:                         Ast.Convert(rule.Parameters[1], typeof(string))
 20:                     );
 21:            return rule;
 22:       }

Правило состоит из теста и цели. Тест (устанавливается через свойство StandardRule.Test) проверяет, является ли первый параметр целым числом, а второй строкой. Цель (устанавливается через свойство StandardRule.Target) – дерево, которое надо запустить для получения результата. Цель – это первый параметр, преобразованный в строку и объединённый со вторым параметром.


Теперь правило создано, DLR имеет всё, чтобы запустить наше выражение «1 + 'A'» . Более того: если DLR найдёт в другой раз одно целое число плюс одну строку, правило будет добавлено без нового вызова нашего метода MJSBinder.MakeRule.


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

    '10' < 2

Сравнивая два члена этого выражения и используя преобразование в строку, мы получим неверный результат. Причина в том, что число 1 находится перед числом 2 в лексикографическом порядке. Чтобы получить правильный результат, левый операнд должен быть конвертирован перед выполнением каких бы то ни было сравнений. Это то, что делается интерпретатором JavaScript. Этот процесс требует того, чтобы сначала попробовать преобразовать первый операнд, затем выполнить сравнение как строк или как чисел, в зависимости от результата преобразования. Это сложно сделать средствами AST, но вместо этого мы используем метод. Метод MJSLibrary.Compare выполняет эту работу за нас. Теперь у нас есть правило для сравнения чисел со строками:

  1:        private StandardRule<T> MakeCompareStringRule<T>(CodeContext callerContext, DynamicAction action, object[] args)
  2:         {
  3:             StandardRule<T> rule = new StandardRule<T>();
  4:              // Same as: (p0 is string) && (p1 is int)
  5:             rule.Test =
  6:                 Ast.AndAlso(
  7:                    Ast.TypeIs(rule.Parameters[0], typeof(string)),
  8:                    Ast.TypeIs(rule.Parameters[1], typeof(int))
  9:                 );
 10:              // Same as: MJSLibrary.CompareTo(p0, p1) < 0
 11:             rule.Target =
 12:                 rule.MakeReturn(this,
 13:                     Ast.LessThan(
 14:                         Ast.Call(
 15:                            typeof(MJSLibrary).GetMethod("CompareTo"),
 16:                            rule.Parameters[0],
 17:                            rule.Parameters[1]
 18:                         ),
 19:                     Ast.Constant(0)
 20:                     );
 21:            return rule;
 22:       }

Объектно-ориентированное программирование с MyJScript


DLR можно использовать не только для обработки значений базовых типов, но так же для обработки экземпляров объектов в объектно-ориентированном языке.  Мы убедимся в этом через несколько минут. Но для начала давайте взглянем, как объекты обрабатываются в JavaScript.


В JavaScript объекты – это всего-навсего коллекции пар имя/значение. Хороший способ убедиться в этом – исследовать JavaScript Object Notation (JSON). Итак, к примеру наш объект Cat уже объявлен следующим образом:

  1:     function Cat(name) {
  2:         this.name = name;
  3:         this.miaow = function () { alert(this.name+' said Miaow'); };
  4:     }
  5:      x = new Cat('Felix');

но в JSON это можно сделать так:

  1:     x = { "name":"Felix", "miaow": function() { alert(this.name+' said Miaow'); } }

Обе декларации делают точь-в-точь одно и то же. В JavaScript не требуется сначала объявлять каждый класс со всеми свойствами и методами. Объекты JavaScript строятся по мере того, как в них добавляются новые члены. Синтаксис JavaScript "new Constructor(...)" – это всего лишь синтаксический сахар для создания пустого объекта, затем вызывается функция-инициализатор. Итак, третий путь инициализации объектов – следующий:

  1:     x = {};
  2:     Cat.call(x, 'Felix');

Заметьте, что в JavaScript функция – это только объект, предоставляющий специфический метод "call", принимающий как параметр значение для this и параметры конструктора. Из-за того, что функция является всего лишь значением, добавление нового метода к экземпляру становится настолько же простым как установка нового значения(как в myaow в нашем примере).


Работа объектов MyJScript намного проще чем в JavaScript. Следующий фрагмент предоставляет некоторые подробности относительно исходного кода MJSObject. Класс MJSObject используется для каждого экземпляра MyJScript:

  1:    public class MJSObject
  2:     {
  3:         Dictionary<string, object> members;
  4:          public MJSObject()
  5:         {
  6:             this.members = new Dictionary<string, object>();
  7:         }
  8:          public bool HasMember(string name)
  9:         {
 10:             return members.ContainsKey(name);
 11:         }
 12:          public virtual void Set(string name, object value)
 13:         {
 14:             if (members.ContainsKey(name))
 15:                 members.Remove(name);
 16:              members.Add(name, value);
 17:         }
 18:          public virtual object Get(string name)
 19:         {
 20:             if (!members.ContainsKey(name))
 21:                 return null;
 22:              return members[name];
 23:         }
 24:     }

Класс MJSObject - словарь имя/значение. Каждое имя размещает переменные экземпляра: свойства или методы (как "name" или "miaow" в нашем предыдущем примере).


Установка и получение свойств объекта


Компилятор MyJScript позволяет создавать пустой объект с помощью следующего оператора:

  1:     x = {};

Парсер MyJScript переводит этот оператор в дерево:

  1:     Ast.Statement(span,
  2:         Ast.Assign(
  3:             x,
  4:             Ast.New(
  5:                 typeof(MJSObject).GetConstructor(new Type[0])
  6:             )
  7:         )
  8:    );

Это означает: результат вызова конструктора MJSObject без параметра присваивается переменной "x".


Теперь для того, чтобы установить или возвратить члены нового объекта, мне сначала потребуется обучить DLR этой операции. Итак, добавим два новых правила в класс MJSBinder

  1: :
  2: 
  3:         private StandardRule<T> MakeSetMemberRule<T>(CodeContext callerContext, DynamicAction action, object[] args)
  4:         {
  5:             SetMemberAction setmember = (SetMemberAction)action;
  6:             StandardRule<T> rule = new StandardRule<T>();
  7:              // Same as: (p0 is MJSObject)
  8:             rule.Test = Ast.TypeIs(rule.Parameters[0], typeof(MJSObject));
  9:              // Same as: (p0 as MJSObject).Set(name, p1)
 10:             rule.Target =
 11:                 rule.MakeReturn(this,
 12:                     Ast.Call(
 13:                         Ast.Convert(rule.Parameters[0], typeof(MJSObject)),
 14:                         typeof(MJSObject).GetMethod("Set", new Type[] { typeof(string), typeof(object) }),
 15:                         Ast.Constant(SymbolTable.IdToString(setmember.Name)),
 16:                         Ast.Convert(rule.Parameters[1], typeof(object))
 17:                     )
 18:                 );
 19:              return rule;
 20:         }
 21:           private StandardRule<T> MakeGetMemberObjectRule<T>(CodeContext callerContext, DynamicAction action, object[] args)
 22:         {
 23:             GetMemberAction getmember = (GetMemberAction)action;
 24:             StandardRule<T> rule = new StandardRule<T>();
 25:              // Same as: (p0 is MJSObject)
 26:             rule.Test = Ast.TypeIs(rule.Parameters[0], typeof(MJSObject));
 27:              // Same as: (p0 as MJSObject).Get(name)
 28:             rule.Target =
 29:                 rule.MakeReturn(this,
 30:                     Ast.Call(
 31:                         Ast.Convert(rule.Parameters[0], typeof(MJSObject)),
 32:                         typeof(MJSObject).GetMethod("Get", new Type[] { typeof(string) }),
 33:                         Ast.Constant(SymbolTable.IdToString(getmember.Name))
 34:                     )
 35:                 );
 36:              return rule;
 37:         }


Первое правило вызывается, когда MJSBinder.MakeRule сталкивается с операцией "SetMember". Правило будет запущено, в случае если типом объекта будет "MJSObject". Чтобы запустить код, вызывается MJSObject.Set, которому передаются имя свойства и его новое значение как параметры. Второе правило вызывается, когда MJSBinder.MakeRule встречает операцию "GetMember". Условие код запуска подобны первому правилу, только используется вызов "MJSObject.Get".


Благодаря этим правилам, компилятор MyJScript теперь может запускать операторы:

  1:     x.name = "Hello";
  2:     write(x.name);

Построение и вызов методов


Теперь давайте посмотрим, как вызываются методы. Вот операторы MyJScript, декларирующие и вызывающие экземпляр метода:

  1:     x.foo = function(n) { write(n); }
  2:     x.foo("Hello");

Мы видели ранее, что парсер переводит функцию в объект LambdaExpression. Вызвать LambdaExpression - это просто, поскольку за кулисами DLR переводит LambdaExpression в .Net-делегат. Итак, для вызова LambdaExpression мне нужно всего лишь сгенерировать вызов. Вот как вызов метода обрабатывается в  MyJScript:

  1:         public Expression MethodCall(Expression instance, String function, List<Expression> values)
  2:         {
  3:             int length = values.Count;
  4:             Expression[] array = new Expression[length+1];
  5:              array[0] = instance;
  6:             for (int i = 0; i < length; i++)
  7:                 array[i+1] = values[i];
  8:              return Ast.Action.InvokeMember(
  9:                 SymbolTable.StringToId(function),
 10:                 typeof(object),
 11:                 InvokeMemberActionFlags.None,
 12:                 new CallSignature(values.Count),
 13:                 array
 14:                 );
 15:         }

MyJScript строит новый массив для параметров, затем начинается вызов члена, с помощью действия InvokeMember.


Благодаря тому, что MyJScript может возвращать экземпляр параметра "this", мне нужно добавить новое правило в MJSBinder. Это правило будет вызывать метод "InvokeMember" типа MJSObject:

  1:         private StandardRule<T> MakeInvokeMemberRule<T>(CodeContext callerContext, DynamicAction action, object[] args)
  2:         {
  3:             StandardRule<T> rule = new StandardRule<T>();
  4:             InvokeMemberAction invokeMember = (InvokeMemberAction)action;
  5:              // Same as: (p0 is MJSObject)
  6:             rule.Test = Ast.TypeIs(rule.Parameters[0], typeof(MJSObject));
  7:              Expression method = Ast.Action.GetMember(
  8:                 invokeMember.Name,
  9:                 typeof(object),
 10:                 rule.Parameters[0]
 11:              );
 12:              Expression[] newparam = new Expression[rule.Parameters.Length + 1];
 13:             newparam[0] = method;
 14:             for (int i = 0; i < rule.Parameters.Length; i++)
 15:                 newparam[i + 1] = rule.Parameters[i];
 16:              // Same as: p0.name(p0, p1, … pn)
 17:             rule.Target =
 18:                 rule.MakeReturn(this,
 19:                     Ast.Action.Call(
 20:                         typeof(object),
 21:                         newparam
 22:                     )
 23:                 );
 24:              return rule;
 25:         }

Это правило исполняет свою роль в три шага: сначала получает значение члена, затем добавляет текущий экземпляр как параметр (это параметр «this»), и наконец, вызывает метод, используя эти новые параметры.


Глобальный контекст и встроенные функции


Глобальный контекст – это последний пункт, который следует рассмотреть для завершения обзора MyJScript. В JavaScript, все, что определено глобально является членом глобального объекта. Итак, глобальные переменные или глобальные функции привязаны к этому глобальному объекту. Давайте посмотрим пример:

  1:     x = 'Hello';
  2:     alert(this.x);     // Print Hello

Более того: я могу преобразовать наш глобальный объект в «Cat» (этого не следует делать в реальной жизни). Чтобы сделать это я всего-навсего вызываю конструктор класса «Cat»:

  1:     this.Cat('Felix');    alert(name);      // Print Felix
  2:     miaow();          // Print Felix said Miaow

В MyJScript этим глобальным контекстом является единственный экземпляр класса MJSObject. Вот код, представляющий этот объект:

  1:         public class MJSContext
  2:         {
  3:             static MJSObject @this = null;
  4:                 public static MJSObject GetContext()
  5:             {
  6:                 if (@this == null)
  7:                     @this = new MJSObject();
  8:                 return @this;
  9:             }
 10:         }

Каждая операция, выполненная над глобальными переменными глобальными функциями, так же должны выполняться в глобальном контексте. Итак, присвоение значения глобальной переменной должно порождать два дерева операторов:

  1:         public Expression AssignGlobalVariable(Variable variable, Expression value)
  2:         {
  3:             List<Expression> statements = new List<Expression>();
  4:              // variable = value
  5:             statements.Add(
  6:                     Ast.Assign(
  7:                         variable,
  8:                         Ast.Convert(value,  typeof(object))
  9:                     )
 10:              );
 11:              // MJSContext.GetContext().Set(variable, value)
 12:             statements.Add(
 13:                     Ast.Action.SetMember(
 14:                         SymbolTable.StringToId(variable.Name),
 15:                         typeof(object),
 16:                         Ast.Call( typeof(MJSContext).GetMethod("GetContext", new Type[0]))
 17:                     ),
 18:                     Ast.Read(variable)
 19:             );
 20:              return Ast.Block(statements);
 21:         }

Первый оператор присваивает значение переменной, второй – вызывает MJSContext.GetContext().Set(), чтобы присвоить значение соответствующему члену глобального контекста.


Глобальный контекст так же используется как хост MyJScript для встроенных функций, таких как «write»:

  1:     public class MJSLibrary
  2:     {
  3:         internal static void Initialize()
  4:         {
  5:             MJSObject mjscontext = MJSContext.GetContext();
  6:             mjscontext.Set(
  7:                 "write",
  8:                 ReflectionUtils.CreateDelegate(
  9:                     typeof(MJSLibrary).GetMethod("Write"),
 10:                     typeof(MyJScriptCallTarget)
 11:                 )
 12:             );
 13:            ...
 14:         }
 15:          public static object Write(object @this, object value)
 16:         {
 17:             Console.WriteLine(value != null ? value.ToString() : "<null>");
 18:             return null;
 19:         }
 20:     }

Теперь давайте вернемся к методу MJSContext.TryLookupGlobal, который мы видели в первом абзаце статьи. Этот метод вызывается DLR для разрешения неизвестных имен переменных. Далее представлен код этого метода для MyJscript:

  1:         public override bool TryLookupGlobal(CodeContext context, SymbolId name, out object value)
  2:         {
  3:             MJSObject mjscontext = MJSContext.GetContext();
  4:             string memberName = SymbolTable.IdToString(name);
  5:             if (mjscontext.HasMember(memberName))
  6:             {
  7:                 value = mjscontext.Get(memberName);
  8:                 return true;
  9:             }
 10:              return base.TryLookupGlobal(context, name, out value);
 11:         }

Взаимодействие с CLR


Как уже говорилось ранее, основные типы, такие как «string» или «int» доступны для всех DLR-языков. Благодаря этому, можно свободно использовать свойства и методы, определенные в этих типах. Вот несколько примеров:

  1:         a='Hello';
  2:         write(a.Length);
  3:         write(a.ToUpper() + ' WORLD!');

Заметьте, что вызов свойства «Length» и метода «ToUpper» не описан в правилах языка MyJScript. Это из-за того, что правила языка MyJScript предназначены только для MJSObjects. Тем не менее, результат сложения «ToUpper» и текстовой константы в самом деле приходит из правила MyJScript.


DLR так же предоставляет способ импортировать объекты из других сборок. Благодаря этой особенности, отсутствующей в JavaScript, я добавляю в MyJScript ключевое слово «using», как в C#. Ниже приведен пример использования этого нового ключевого слова.

  1:         a='Hello';
  2:         write(a.Length);
  3:         write(a.ToUpper() + ' WORLD!');

Бурное обсуждение вопроса о том, как реализовать «using» на самом деле не представляет интереса. Достаточно вспомнить ситуацию, когда мне понадобилось вызвать метод DLR, именуемый "LanguageContext.DomainManager.Globals.TryGetName(name)". Этот метод загружает переменную в текущий контекст со всеми членами каждого класса и объектами в контексте.


Интерпретатор командной строки


Компилятор MyJScript наконец-то завершен. Теперь неплохо было бы написать интерпретатор командной строки для запуска команд MyJScript или сценариев. DLR предоставляет стандартные функции для того, чтобы написание консольного приложения было легким. Вот весь необходимый код. Класс должен быть унаследован от Microsoft.Script.Hosting.ConsoleHost:

  1:     public class MJSConsole : ConsoleHost
  2:     {
  3:         protected override void Initialize()
  4:         {
  5:             base.Initialize();
  6:             this.Options.ScriptEngine = ScriptEnvironment.GetEnvironment().GetEngine(typeof(MJSLanguageContext));
  7:             Environment.LoadAssembly(typeof(string).Assembly)
  8:         }
  9:          [STAThread]
 10:         static int Main(string[] args)
 11:         {
 12:             return new MJSConsole().Run(args);
 13:         }
 14:     }

Немного изменим эту командную строку, включив logo и специфический запрос. Чтобы сделать это, нам следует класс MJSCommandLine, ссылающийся на класс MJSLanguageContext.

  1:     class MJSCommandLine : CommandLine
  2:     {
  3:         protected override string Logo
  4:         {
  5:             get
  6:             {
  7:                 return "MyJScript Command line\r\nLGPL Copyright (c) Lionel Laské 2008\r\nType CTRL-Z and RETURN to quit\r\n\r\n";
  8:             }
  9:         }
 10:          protected override string Prompt
 11:         {
 12:             get
 13:             {
 14:                 return "mjs> ";
 15:             }
 16:         }
 17:     }

Это все! С помощью этих нескольких строчек, интерпретатор MyJScript может:



  • Запускать операторы из командной строки,
  • Запускать операторы из файла,
  • Предоставлять множество опций DLR (смотрите ниже).

 


dlr2_4



Взаимодействие с другими языками DLR


Мы увидели как MyJScript может взаимодействовать с типами CLR. Теперь давайте посмотрим как MyJScript может взаимодействовать с другими языками DLR, а точнее, с наиболее известным из них: IronPython.


Предыдущий параграф показал, как можно создать консольное приложение, исполняющее код на DLR-языке. Так же существует возможность запустить его в собственной среде исполнения. Это можно сделать, сначала создав новый объект ScriptRuntime. Следующим образом:

  1:     // Create a new DLR runtime
  2:     ScriptRuntime runtime = ScriptRuntime.Create();
  3:      // Create a global scope
  4:     ScriptScope globals = runtime.CreateScope();

Затем нам надо загрузить среду исполнения языка из его контекста. Это объект Microsoft.Script.Hosting.ScriptEngine :

  1:     // Load MyJScript engine
  2:     ScriptEngine myjscript = runtime.GetEngine(typeof(MyJScript.DLR.MJSLanguageContext));
  3:      // Run a command on MyJScript engine
  4:     ScriptSource mjssrc = myjscript.CreateScriptSourceFromString("write('Hello world!');");
  5:     mjssrc.Execute(globals);

Я делаю то же самое для движка языка IronPython:

  1:     // Load IronPython engine
  2:     ScriptEngine python = runtime.GetEngine(typeof(IronPython.Runtime.PythonContext));
  3:      // Run a command on IronPython engine
  4:     SriptSource pysrc = python.CreateScriptSourceFromString("print 'Hello world!';", SourceCodeKind.Statements);
  5:     pysrc.Execute(globals);

Теперь давайте напишем вспомогательную функцию для более легкого вызова в следующих примерах.

  1:     static private void RunProgram(ScriptEngine engine, string command)
  2:     {
  3:         ScriptSource src = engine.CreateScriptSourceFromString(command, SourceCodeKind.Statements);
  4:         src.Execute(globals);
  5:     }

Теперь я могу скомбинировать вызов обоих компиляторов: IronPython и MyJScript.

  1:     // Call a MyJScript variable from IronPython
  2:     RunProgram(myjscript, "a='MyJScript';");
  3:     RunProgram(python, "print 'Hello',a;");
  4:         // Call a IronPython variable from MyJScript
  5:     RunProgram(python, "b='IronPython';");
  6:     RunProgram(myjscript, "write('Hello '+b);");

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


Было бы неплохо создать объект MyJScript, а потом использовать его в IronPython или наоборот. Тем не менее, это не просто. Фактически, каждый язык использует собственные правила. IronPython не знает как обрабатывается MJSObject, и MyJScript не знает как обрабатывается PyObject. Так что смешивание объектов в многоязыковой среде – более сложный процесс.


Узнать больше


О DLR


В настоящее время существует несколько ресурсов, посвященных DLR. Блог Jim Hugunin это библия DLR, но, к несчастью, там всего несколько постов и блог давно не обновлялся. Тем не менее, этот блог очень интересно читать, чтобы узнать больше о концепциях внутри DLR.


Блог John Lam - так же интересный способ получения информации о DLR. Джон работает в команде IronRuby. Его блог обновляется очень часто, но не рассказывает ничего эксклюзивного о DLR.


Martin Maly, один из авторов DLR начал недавно блог, посвященный DLR. На сегодня этот блог – действительно лучший путь для понимания того, как работает DLR. Множество особенностей DLR объясняются в каждом посте с самого начала, на сегодня опубликовано более десяти постов.


Хорошим способом изучения DLR было бы также изучение исходных кодов DLR. Это возможно благодаря тому, что DLR является open source проектом. К сожалению, исходные коды включают в себя всего несколько комментариев и документация – просто сборник всех комментариев… Весьма интересно взглянуть, как реализованы языки DLR. Toyscript – маленький базовый, загружаемый с DLR. Он был создан для использования в качестве руководства, так что это хорошее введение в DLR. Ну и конечно же вы можете так же изучить исходные коды IronPython или IronRuby, но из-за большого количества строк кода, они намного сложнее для понимания.


Несколько веб-трансляций интересны для углубленного изучения DLR. Если у вас есть доступ к видео TechEd 2007's, посмотрите например отличную сессию WEB404 от Мартина Мейли. В последнее время, множество «необходимых для просмотра» видеороликов для тех, кто интересуется компиляторами появилось на Lang.Net symposium.


О MyJScript


Исходные коды MyJScript можно загрузить на CodePlex (http://www.codeplex.com/MyJScript). Код включает множестов комментариев и тестов для всех основных особенностей. MyJScript был написан как руководство для изучения технологии компиляторов и DLR. Итак, MyJScript – это не настоящий компилятор JavaScript.


Две основные особенности JavaScript были упрощены в MyJScript:



  • Функции не могут быть использованы до их декларации. Из-за того, что MyJScript генерирует AST "на лету", он не может увидеть функцию, декларируемую ниже. Для того, чтобы избежать этого, большинство языков, построенный на DLR используют промежуточное дерево вместо AST.
  • Объекты MyJScript нельзя производить от других объектов, используя свойство “prototype” функции (смотрите отличную статью от Ray Djajadinata на MSDN чтобы узнать об этом больше). Наследование можно добавить в MyJScript, внеся некоторые изменения в реализацию MJSObject.

Заключение


Эта статья рассказывает о наиболее важных особенностях DLR:



  • AST: позволяет вам строить набор операторов и генерировать из них IL код.
  • Rules: предоставляет ненавязчивый путь для изучения всех специфических особенностей DLR (conversion, member function, extended types, ...) вашего языка.
  • Hosting: позволяет вам очень легко генерировать консоль для вашего языка или включить DLR язык в ваше приложение.

Благодаря этим трем особенностям, DLR предоставляет основные инструменты для написания собственного языка со встроенным CLR взаимодействием и с другими DLR языками. Это несомненно реальная мощь DLR.


Благодарности


Many thanks to Sami Jaber for his thorough reading of this article. Thanks also to Bill Chiles to suggest me to translate this article from french.