ФЕВРАЛЯ 2016

ТОМ 31 НОМЕР 2

C# - Собственный язык Customizable Scripting на C#

Василий Каплан

Продукты и технологии:

C#

В статье рассматриваются:

  • модификация алгоритма разделения-слияния (Split-and-Merge algorithm);
  • класс Interpreter;
  • переменные и массивы;
  • управляющие выражения;
  • функции;
  • интернационализация.

В этой статье я покажу, как создать собственный скриптовый язык, используя C# безо всяких внешних библиотек. Этот скриптовый язык основан на алгоритме разделения-слияния (Split-and-Merge) для разбора математических выражений на C#, который я представил в номере «MSDN Magazine» за октябрь 2015 года (msdn.com/magazine/mt573716).

Применяя пользовательские функции, я могу расширить алгоритм разделения-слияния для разбора не только математического выражения, но и собственного скриптового языка. «Стандартные» управляющие выражения в языке (if, else, while, continue, break и т. д.) можно добавить как пользовательские функции, равно как и другую функциональность типичного скриптового языка (команды ОС, манипуляции со строками, поиск файлов и т. п.).

Я намерен назвать свой язык так: «Customizable Scripting на C#», или CSCS. Зачем мне понадобилось создавать еще один скриптовый язык? Потому что он прост в настройке. Добавление новой функции или управляющего выражения, которое принимает произвольное количество параметров, требует написания всего нескольких строк кода. Более того, имена функций и управляющих выражений можно использовать вне рамок английского языка, и для этого достаточно внести некоторые изменения в конфигурацию, о которых я тоже расскажу в этой статье. Увидев, как реализуется язык CSCS, вы сможете создать собственный скриптовый язык.

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

Область CSCS

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

  • Язык CSCS имеет управляющие выражения if, else if, else, while, continue и break. Вложенные выражения также поддерживаются. Вы узнаете, как «на лету» добавлять дополнительные управляющие выражения.
  • Логических значений в этом языке нет. Вместо «if (a)» вы должны писать «if (a == 1)».
  • Логические операторы не поддерживаются. Вместо «if (a ==1 and b == 2)» вы должны писать вложенные if: «if (a == 1) { if (b == 2) { … } }».
  • Функции и методы в CSCS не поддерживаются, но их можно писать на C# и регистрировать в Split-and-Merge Parser, чтобы использовать с CSCS.
  • Поддерживаются комментарии только в стиле «//».
  • Поддерживаются переменные и одномерные массивы, все они определяются на глобальном уровне. Переменная может содержать число, строку или кортеж (tuple) других переменных (реализованный как список). Многомерные массивы не поддерживаются.

На рис. 1 представлена программа «Hello, World!» на CSCS. Из-за опечатки в «print», программа сообщает об ошибке в конце: «Couldn’t parse token [pint]» (не удалось разобрать лексему [pint]). Заметьте, что все предыдущие выражения были выполнены успешно, т. е. CSCS является интерпретатором.

“Hello, World!” in CSCS
Рис. 1. «Hello, World!» на CSCS

Модификации в алгоритме разделения-слияния

Я внес два изменения в Split-часть алгоритма Split-and-Merge. (Merge-часть осталась прежней.)

Первое изменение в том, что результат разбора выражения теперь может быть числом, строкой или кортежем значений (каждое из которых является либо строкой, либо числом), а не только числом. Я создал класс Parser.Result для хранения результата после применения алгоритма разделения-слияния:

public class Result
{
  public Result(double dRes = Double.NaN, 
    string sRes = null, 
    List<Result> tRes = null)
  {
    Value  = dResult;
    String = sResult;
    Tuple  = tResult;
  }
  public double
       Value  { get; set; }
  public string
       String { get; set; }
  public List<Result> Tuple  { get; set; }
}

Второе изменение заключается в том, что теперь часть, отвечающая за разделение, выполняется не просто до нахождения символа прекращения разбора — ) или \n, а пока не будет найден любой символ в переданном массиве символов прекращения разбора. Это необходимо, например, при разборе первого аргумента выражения if, где разделителем может быть любой символ: < , > или =.

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

Интерпретатор

Класс, отвечающий за интерпретацию CSCS-кода, называется Interpreter. Он реализован как singleton, т. е. в его определении указано, что можно создать только один экземпляр этого класса. В его методе Init средство разбора (Parser) (см. ранее упомянутую предыдущую статью) инициализируются все функции, используемые Interpreter:

public void Init()
{
  ParserFunction.AddFunction(Constants.IF,
        new IfStatement(this));
  ParserFunction.AddFunction(Constants.WHILE,
     new WhileStatement(this));
  ParserFunction.AddFunction(Constants.CONTINUE,
  new ContinueStatement());
  ParserFunction.AddFunction(Constants.BREAK,
     new BreakStatement());
  ParserFunction.AddFunction(Constants.SET,
       new SetVarFunction());
...
}

В файле Constants.cs определены реальные имена, используемые в CSCS:

public const string IF          = "if";
public const string ELSE        = "else";
public const string ELSE_IF     = "elif";
public const string WHILE       = "while";
public const string CONTINUE    = "continue";
public const string BREAK       = "break";
public const string SET         = "set";
...

Любая функция, зарегистрированная в Parser, должна быть реализована как класс, производный от класса ParserFunction, и должна переопределять его метод Evaluate.

Начиная работу над скриптом, Interpreter первым делом упрощает скрипт, удаляя все пробельные символы (если только они не находятся внутри строки) и все комментарии. Следовательно, пробелы или новые строки нельзя использовать в качестве разделителей операторов. Символ разделителя оператора и строка — признак комментария тоже определяются в Constants.cs:

public const char END_STATEMENT = ';';
public const string COMMENT     = "//";

Переменные и массивы

CSCS supports numbers (type double), strings or tuples (arrays of variables implemented as a C# list). Each element of a tuple can be either a string or a number, but not another tuple. Therefore, multidimensional arrays are not supported. To define a variable, the CSCS function “set” is used. The C# class SetVarFunction implements the functionality of setting a variable value, as shown inCSCS поддерживает числа (типа double), строки и кортежи (массивы переменных, реализованных как C#-список). Каждый элемент кортежа может быть или строкой, или числом, но не другим кортежем. Поэтому многомерные массивы не поддерживаются. Чтобы определить переменную, применяется CSCS-функция set. C#-класс SetVarFunction реализует функциональность присваивания значения переменной (рис. 2).

Рис. 2. Реализация функции, присваивающей переменной значение

class SetVarFunction : ParserFunction
{
  protected override Parser.Result Evaluate(string data, ref int from)
  {
    string varName = Utils.GetToken(data, ref from, Constants.NEXT_ARG_ARRAY);
    if (from >= data.Length)
    {
      throw new ArgumentException("Couldn't set variable before end of line");
    }
    Parser.Result varValue = Utils.GetItem(data, ref from);
    // Проверяем, имеет ли присваиваемая переменная форму x(i),
    // означающую, что это элемент массива
    int arrayIndex = Utils.ExtractArrayElement(ref varName);
    if (arrayIndex >= 0)
    {
      bool exists = ParserFunction.FunctionExists(varName);
      Parser.Result  currentValue = exists ?
            ParserFunction.GetFunction(varName).GetValue(data, ref from) :
            new Parser.Result();
      List<Parser.Result> tuple = currentValue.Tuple == null ?
                                  new List<Parser.Result>() :
                                  currentValue.Tuple;
      if (tuple.Count > arrayIndex)
      {
        tuple[arrayIndex] = varValue;
      }
      else
      {
        for (int i = tuple.Count; i < arrayIndex; i++)
        {
          tuple.Add(new Parser.Result(Double.NaN, string.Empty));
        }
        tuple.Add(varValue);
      }
      varValue = new Parser.Result(Double.NaN, null, tuple);
    }
    ParserFunction.AddFunction(varName, new GetVarFunction(varName, varValue));
    return new Parser.Result(Double.NaN, varName);
  }
}

Вот несколько примеров определения переменной в CSCS:

set(a, "2 + 3");  // a будет равна строке "2 + 3"
set(b, 2 + 3);    // b будет равна числу 5
set(c(2), "xyz"); // c будет инициализирована как кортеж размером 3 с c(0) = c(1) = ""

Обратите внимание на то, что специального объявления массива нет: простое определение переменной с индексом будет инициализировать массив, если он еще не инициализирован, и добавлять в него пустые элементы при необходимости. В предыдущем примере были добавлены элементы c(0) и c(1), оба инициализированные пустыми строками. Это исключает необязательный на мой взгляд шаг предварительного объявления массива, который требуется в большинстве скриптовых языков.

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

Все переменные и массивы в CSCS создаются с помощью CSCS-функций (вроде set или append). Все они определяются на глобальном уровне и могут использоваться позднее простым обращением к имени переменной или к переменной с индексом. В C# это реализуется в GetVarFunction (рис. 3).

Рис. 3. Реализация функции, получающей значение переменной

class GetVarFunction : ParserFunction
{
  internal GetVarFunction(Parser.Result value)
  {
    m_value = value;
  }
  protected override Parser.Result Evaluate(string data, ref int from)
  {
    // Сначала проверяем, является ли этот элемент частью массива:
    if (from < data.Length && data[from - 1] == Constants.START_ARG)
    {
      // Указан индекс – это может обращение к элементу кортежа
      if (m_value.Tuple == null || m_value.Tuple.Count == 0)
      {
        throw new ArgumentException("No tuple exists for the index");
      }
      Parser.Result index = Utils.GetItem(data, ref from, true /* expectInt */);
      if (index.Value < 0 || index.Value >= m_value.Tuple.Count)
      {
        throw new ArgumentException("Incorrect index [" + index.Value +
          "] for tuple of size " + m_value.Tuple.Count);
      }
      return m_value.Tuple[(int)index.Value];
    }
    // Это случай для простой переменной, а не массива:
    return m_value;

  }
  private Parser.Result m_value;
}

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

ParserFunction.AddFunction(Constants.SET, new SetVarFunction());

Функция получения переменной (get) регистрируется внутри C#-кода функции присваивания (см. предпоследнее выражение на рис. 2):

ParserFunction.AddFunction(varName, new GetVarFunction(varName, varValue));

Некоторые примеры получения переменных в CSCS:

append(a, "+ 5"); // a будет равна строке "2 + 3 + 5"
set(b, b * 2);    // b будет равна числу 10 (если до этого она была равна 5)

Управляющие выражения: If, Else If, Else

Управляющие выражения If, Else If и Else тоже реализуются на внутреннем уровне как функции Parser. Они регистрируются Parser так же, как и любые другие функции:

ParserFunction.AddFunction(Constants.IF, new IfStatement(this));

В Parser должно быть зарегистрировано только ключевое слово IF. Выражения ELSE_IF и ELSE будут обрабатываться внутри реализации IfStatement:

class IfStatement : ParserFunction
{
  protected override Parser.Result Evaluate(string data, ref int from)
  {
    m_interpreter.CurrentChar = from;
    Parser.Result result = m_interpreter.ProcessIf();
    return result;
  }
  private Interpreter m_interpreter;
}

Настоящая реализация выражения If находится в классе Interpreter (рис. 4).

Рис. 4. Реализация выражения If

internal Parser.Result ProcessIf()
{
  int startIfCondition = m_currentChar;
  Parser.Result result = null;
  Parser.Result arg1 = GetNextIfToken();
  string comparison  = Utils.GetComparison(m_data, ref m_currentChar);
  Parser.Result arg2 = GetNextIfToken();
  bool isTrue = EvalCondition(arg1, comparison, arg2);
  if (isTrue)
  {
    result = ProcessBlock();
    if (result is Continue || result is Break)
    {
      // Попали сюда из середины блока if. Пропускаем его.
      m_currentChar = startIfCondition;
      SkipBlock();
    }
    SkipRestBlocks();
    return result;
  }
  // Мы находимся в Else. Пропускаем все в выражении If.
  SkipBlock();
  int endOfToken = m_currentChar;
  string nextToken = Utils.GetNextToken(m_data, ref endOfToken);
  if (ELSE_IF_LIST.Contains(nextToken))
  {
    m_currentChar = endOfToken + 1;
    result = ProcessIf();
  }
  else if (ELSE_LIST.Contains(nextToken))
  {
    m_currentChar = endOfToken + 1;
    result = ProcessBlock();
  }
  return result != null ? result : new Parser.Result();
}

Явно объявляется, что условие If имеет следующую форму: аргумент 1, знак сравнения, аргумент 2:

Parser.Result arg1 = GetNextIfToken();
string comparison  = Utils.GetComparison(m_data, ref m_currentChar);
Parser.Result arg2 = GetNextIfToken();
bool isTrue = EvalCondition(arg1, comparison, arg2);

Именно здесь можно добавлять необязательные операторы AND, OR или NOT.

Каждый элемент кортежа может быть или строкой, или числом, но не другим кортежем.

Функция EvalCondition просто сравнивает лексемы согласно знаку сравнения:

internal bool EvalCondition(Parser.Result arg1, string comparison, Parser.Result arg2)
{
  bool compare = arg1.String != null ? CompareStrings(arg1.String, comparison, arg2.String) :
                                       CompareNumbers(arg1.Value, comparison, arg2.Value);
  return compare;
}

Вот реализация числового сравнения:

internal bool CompareNumbers(double num1, string comparison, double num2)
{
  switch (comparison) {
    case "==": return num1 == num2;
    case "<>": return num1 != num2;
    case "<=": return num1 <= num2;
    case ">=": return num1 >= num2;
    case "<" : return num1 <  num2;
    case ">" : return num1 >  num2;
    default: throw new ArgumentException("Unknown comparison: " + comparison);
  }
}

Сравнение строк аналогично, и его можно увидеть в исходном коде, сопутствующем этой статье, в реализации функции GetNextIfToken.

Когда условие if, else if или else равно true, обрабатываются все выражения в блоке. Это реализовано в методе ProcessBlock на рис. 5. Если условие не равно true, все выражения в блоке пропускаются. Это реализовано в методе SkipBlock (см. сопутствующий исходный код).

Рис. 5. Реализация метода ProcessBlock

internal Parser.Result ProcessBlock()
{
  int blockStart = m_currentChar;
  Parser.Result result = null;
  while(true)
  {
    int endGroupRead = Utils.GoToNextStatement(m_data, ref m_currentChar);
    if (endGroupRead > 0)
    {
      return result != null ? result : new Parser.Result();
    }
    if (m_currentChar >= m_data.Length)
    {
      throw new ArgumentException("Couldn't process block [" +
                                   m_data.Substring(blockStart) + "]");
    }
    result = Parser.LoadAndCalculate(m_data, ref m_currentChar,
      Constants.END_PARSE_ARRAY);
    if (result is Continue || result is Break)
    {
      return result;
    }
  }
}

Обратите внимание на то, как внутри цикла while используются операторы Continue и Break. Эти операторы тоже реализованы как функции. Вот что представляет собой Continue:

class Continue : Parser.Result  { }
class ContinueStatement : ParserFunction
{
  protected override Parser.Result
    Evaluate(string data, ref int from)
  {
    return new Continue();
  }
}

Реализация Break аналогична. Оба регистрируются в Parser, как и любая другая функция:

ParserFunction.AddFunction(Constants.CONTINUE,  new ContinueStatement());
ParserFunction.AddFunction(Constants.BREAK,     new BreakStatement());

Вы можете использовать функцию Break для выхода из вложенных блоков If или из цикла while.

Управляющие выражения: цикл while

Цикл while вновь реализуется и регистрируется в Parser как функция:

ParserFunction.AddFunction(Constants.WHILE,     new WhileStatement(this));

Всякий раз, когда происходит разбор ключевого слова while, вызывается метод Evaluate объекта WhileStatement:

class WhileStatement : ParserFunction
{
  protected override Parser.Result Evaluate(string data, ref int from)
  {
    string parsing = data.Substring(from);
    m_interpreter.CurrentChar = from;
    m_interpreter.ProcessWhile();
    return new Parser.Result();
  }
  private Interpreter m_interpreter;
}

Так что настоящая реализация цикла while находится в классе Interpreter (рис. 6).

Рис. 6. Реализация цикла while

internal void ProcessWhile()
{
  int startWhileCondition = m_currentChar;
  // Эвристическая проверка на бесконечный цикл
  int cycles = 0;
  int START_CHECK_INF_LOOP = CHECK_AFTER_LOOPS / 2;
  Parser.Result argCache1 = null;
  Parser.Result argCache2 = null;
  bool stillValid = true;
  while (stillValid)
  {
    m_currentChar = startWhileCondition;
    Parser.Result arg1 = GetNextIfToken();
    string comparison = Utils.GetComparison(m_data, ref m_currentChar);
    Parser.Result arg2 = GetNextIfToken();
    stillValid = EvalCondition(arg1, comparison, arg2);
    int startSkipOnBreakChar = m_currentChar;
    if (!stillValid)
    {
      break;
    }
    // Проверка на бесконечный цикл,
// если сравниваются одни и те же значения
    if (++cycles % START_CHECK_INF_LOOP == 0)
    {
      if (cycles >= MAX_LOOPS || (arg1.IsEqual(argCache1) &&
        arg2.IsEqual(argCache2)))
      {
        throw new ArgumentException("Looks like an infinite loop after " +
          cycles + " cycles.");
      }
      argCache1 = arg1;
      argCache2 = arg2;
    }
    Parser.Result result = ProcessBlock();
    if (result is Break)
    {
      m_currentChar = startSkipOnBreakChar;
      break;
    }
  }
  // Условие while больше не является true: нужно пропустить
  // весь блок while, прежде чем продолжать
  // со следующих выражений

  SkipBlock();
}

Заметьте, что цикл while на упреждение проверяет вхождение в бесконечный цикл после определенного количества итераций, определенного в конфигурационных параметрах константой CHECK_AFTER_LOOPS. Эвристика здесь в том, что если в течение нескольких циклов сравниваются точно те же значения, это может указывать на бесконечный цикл. На рис. 7 показан цикл while, где я забыл об увеличении переменной цикла i внутри while.

Detecting an Infinite While Loop in CSCS
Рис. 7. Обнаружение бесконечного цикла while в CSCS

Функции, функции, функции

Чтобы CSCS делал более полезные вещи, нужно нарастить его функционал, т. е. следует реализовать больше функций. Добавление новой функции в CSCS достаточно прямолинейно: сначала реализуется класс, производный от класса ParserFunction (с переопределением метода Evaluate), а затем он регистрируется в Parser. Вот реализация функции Print:

class PrintFunction : ParserFunction
{
  protected override Parser.Result Evaluate(string data, ref int from)
  {
    List<string> args = Utils.GetFunctionArgs(data, ref from);
    m_interpreter.AppendOutput(string.Join("", args.ToArray()));
    return new Parser.Result();
  }
  private Interpreter m_interpreter;
}

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

Второй и последний шаг — регистрация функции Print в Parser в инициализирующей части программы:

ParserFunction.AddFunction(Constants.PRINT,     new PrintFunction(this));

Константа Constants.PRINT определена как print.

На рис. 8 показана реализация функции, которая начинает новый процесс.

Рис. 8. Реализация функции запуска процесса

class RunFunction : ParserFunction
{
  internal RunFunction(Interpreter interpreter)
  {
    m_interpreter = interpreter;
  }
  protected override Parser.Result Evaluate(string data, ref int from)
  {
    string processName = Utils.GetItem(data, ref from).String;
    if (string.IsNullOrWhiteSpace(processName))
    {
      throw new ArgumentException("Couldn't extract process name");
    }
    List<string> args = Utils.GetFunctionArgs(data, ref from);
    int processId = -1;
    try
    {
      Process pr = Process.Start(processName, string.Join("", args.ToArray()));
      processId = pr.Id;
    }
    catch (System.ComponentModel.Win32Exception exc)
    {
      throw new ArgumentException("Couldn't start [" + processName + "]:
        " + exc.Message);
    }
    m_interpreter.AppendOutput("Process " + processName + " started, id:
      " + processId);
    return new Parser.Result(processId);
  }
  private Interpreter m_interpreter;
}

Чтобы CSCS делал более полезные вещи, нужно нарастить его функционал, т. е. следует реализовать больше функций.

Вот как можно найти файлы, запустить и уничтожить процесс, а также вывести некоторые значения в CSCS: 

set(b, findfiles("*.cpp", "*.cs"));
set(i, 0);
while(i < size(b)) {
  print("File ", i, ": ", b(i));
  set(id, run("notepad", b(i)));
  kill(id);
  set(i, i+ 1);
}

В табл. 1 перечислены функции, реализованные в пакете сопутствующего исходного кода, и дано краткое описание. Большинство функций является оболочками соответствующих C#-функций.

Табл. 1. CSCS-функции

Функция Описание
abs Получает абсолютное значение выражения
append Дописывает строку или число (преобразуемое потом в строку) к строке
cd Выполняет смену каталога
cd.. Выполняет смену каталога на один уровень выше
dir Показывает содержимое текущего каталога
enc Получает содержимое переменной окружения
exp Экспоненциальная функция
findfiles Находит файлы по заданному шаблону
findstr Находит файлы, содержащие строку, которая отвечает указанному шаблону
indexof Возвращает индекс подстроки или –1, если она не найдена
kill Уничтожает процесс с заданным идентификатором
pi Возвращает аппроксимированное значение константы pi
pow Возвращает первый аргумент, возведенный в степень, указанную во втором аргументе
print Выводит данный список аргументов (числа и списки, преобразованные в строки)
psinfo Возвращает информацию о процессе с указанным именем
pstime Возвращает общее процессорное время для данного процесса; полезна для замера показателей времени
pwd dОтображает полный путь к текущему каталогу
run Запускает процесс с заданным списком аргументов и возвращает идентификатор этого процесса
setenv Задает значение переменной окружения
set Задает значение переменной или элемента массива
sin Возвращает значение синуса данного аргумента
size Возвращает длину строки или размер списка
sqrt Возвращает квадратный корень данного числа
substr Возвращает подстроку строки, начинающуюся с указанного индекса
tolower Преобразует строку в буквы нижнего регистра
toupper Преобразует строку в буквы верхнего регистра

Интернационализация

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

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

Добавление трансляции (перевода) заключается в регистрации другой строки в том же C#-объекте. Соответствующий код на C# выглядит так:

var languagesSection =
  ConfigurationManager.GetSection("Languages") as NameValueCollection;
string languages = languagesSection["languages"];
foreach(string language in languages.Split(",".ToCharArray());)
{
  var languageSection =
    ConfigurationManager.GetSection(language) as NameValueCollection;
  AddTranslation(languageSection, Constants.IF);
  AddTranslation(languageSection, Constants.WHILE);
...
}

Метод AddTranslation добавляет синоним для уже существующей функции:

public void AddTranslation(NameValueCollection languageDictionary, string originalName)
{
  string translation = languageDictionary[originalName];
  ParserFunction originalFunction =
    ParserFunction.GetFunction(originalName);
  ParserFunction.AddFunction(translation, originalFunction);
}

Благодаря поддержке Unicode в C# большинство языков можно добавить этим способом. Заметьте, что имена переменных тоже могут быть в Unicode.

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

<Languages>
  <add key="languages" value="Spanish" />
</Languages>
<Spanish>
    <add key="if"    value ="si" />
    <add key="else"  value ="sino" />
    <add key="elif"  value ="sinosi" />
    <add key="while" value ="mientras" />
    <add key="set"   value ="asignar" />
    <add key="print" value ="imprimir" />
 ...
</Spanish>

А вот пример CSCS-кода на испанском:

asignar(a, 5);
mientras(a > 0) {
  asignar(expr, 2*(10 – a*3));
  si (expr > 0) {
    imprimir(expr, " es mayor que cero");
  }
  sino {
    imprimir(expr, " es cero o menor que cero");
  }
  asignar(a, a - 1);
}

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

Заключение

Все элементы CSCS — управляющие выражения, переменные, массивы и функции — реализуются определением C#-класса, производного от базового класса ParserFunction, и переопределением его метода Evaluate. Затем вы регистрируете объект этого класса в Parser. Такой подход обеспечивает следующие преимущества.

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

Конечно, на этом этапе язык CSCS далек от какой-либо завершенности. Вот несколько способов сделать его более полезным.

  • Создание многомерных массивов. Можно использовать ту же C#-структуру данных, что и для одномерных массивов: List<Result>. Однако для получения и задания элемента многомерного массива потребуется добавить более обширную функциональность разбора.
  • Поддержка инициализации кортежей в одной строке кода.
  • Добавление логических операторов (AND, OR, NOT и т. д.), которые были бы очень полезны для выражений if и while.
  • Добавление возможности писать функции и методы в CSCS. На данный момент можно использовать только функции, ранее написанные и скомпилированные в C#.
  • Добавление возможности включать исходный код CSCS из других блоков (units).
  • Добавление большего количества функций, выполняющих типичные задачи, связанные с ОС. Поскольку большинство таких задач можно легко реализовать на C#, эти функции зачастую будут тонкими оболочками своих C#-эквивалентов.
  • Создание сокращения для функции set(a, b) в виде a = b.

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


Василий Каплан (Vassili Kaplan) — бывший разработчик Microsoft Lync. Увлекается программированием на C# и C++. В настоящее время живет в Цюрихе (Швейцарии) и работает как фрилансер на различные банки. С ним можно связаться по адресу iLanguage.ch.

Выражаю благодарность за рецензирование статьи экспертуMicrosoft Джеймсу Маккаффри (JamesMcCaffrey).