Fevereiro de 2016

Volume 31 – Número 2

C# – Script personalizável em C#

Por Vassili Kaplan

Neste artigo, mostrarei como criar uma linguagem de script personalizável usando C# – sem usar bibliotecas externas. A linguagem de script é baseada no algoritmo de divisão e mesclagem para análise de expressões matemáticas em C# que eu apresentei da edição de outubro de 2015 da MSDN Magazine (msdn.com/magazine/mt573716).

Usando funções personalizadas, eu posso ampliar o algoritmo de divisão e mesclagem para analisar não só uma expressão matemática, como também para analisar uma linguagem de script personalizável. As instruções de fluxo de controle de linguagem “padrão” (if, else, while, continue, break e assim por diante) podem ser adicionadas como funções personalizadas, assim como podem outras funcionalidades de linguagem de script típicas (comandos de SO, manipulação de cadeia de caracteres, pesquisa de arquivos e assim por diante).

Vou chamar minha linguagem de script personalizável em C# (Customizable Scripting in C#) ou CSCS. Por que eu ainda quero criar outra linguagem de script? Porque é uma linguagem fácil de personalizar. Adicionar uma nova função ou uma nova instrução de fluxo de controle que usa um número arbitrário de parâmetros só requer algumas linhas de código. Além disso, também mostrarei neste artigo que os nomes de função e as instruções de fluxo de controle podem ser usados em qualquer cenário de linguagem não escrita em inglês com apenas algumas mudanças de configuração. Vendo como a linguagem CSCS é implementada, você será capaz de criar sua própria linguagem de script personalizada.

Escopo da CSCS

É bem simples implementar uma linguagem de script básica, mas extremamente difícil implementar uma linguagem de cinco estrelas. Aqui, vou limitar o escopo da CSCS para que você saiba o que esperar dela:

  • A linguagem CSCS possui as instruções de fluxo de controle if, else if, else, while, continue e break. As instruções aninhadas também são suportadas. Você aprenderá como adicionar mais instruções de controle dinamicamente.
  • Não existem valores boolianos. Em vez de escrever “if (a),” você tem que escrever “if (a == 1)”.
  • Os operadores lógicos não são suportados. Em vez de escrever “if (a ==1 and b == 2),” você escreve ifs aninhados: “if (a == 1) { if (b == 2) { … } }”.
  • As funções e métodos não são suportados na CSCS, mas elas podem ser escritas em C# e registradas com o Analisador de divisão e mesclagem para serem usadas com a CSCS.
  • Somente comentários de estilo “//” são suportados.
  • As variáveis de matrizes unidimensionais são suportadas e são todas definidas em nível global. Uma variável pode conter um número, uma cadeia de caracteres ou uma tupla (implementada como lista) de outras variáveis. Matrizes multidimensionais não são suportadas.

A Figura 1 mostra o programa “Olá, mundo!” em CSCS. Devido ao erro de digitação de “print” (imprimir), o programa exibe um erro no fim: “Couldn’t parse token [pint]” (não foi possível analisar o token [pint]). Observe que todas as instruções anteriores foram executadas com sucesso, ou seja, a CSCS é uma intérprete.

“Olá, mundo!” em CSCS
Figura 1 “Olá, mundo!” em CSCS

Modificações do algoritmo de divisão e mesclagem

Eu fiz duas alterações na parte da divisão do algoritmo de divisão e mesclagem. (A parte da mesclagem permanece igual).

A primeira mudança é que o resultado da análise de uma expressão agora pode ser um número, uma cadeia de caracteres ou uma tupla de valores (podendo cada uma delas ser uma cadeia ou um número), em vez de apenas um número. Eu criei a seguinte classe Parser.Result para armazenar o resultado do uso de um algoritmo de divisão e mesclagem:

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; }
}

Agora, a segunda modificação acontece quando a parte da divisão é realizada não só até um caractere de parada de análise —) ou \n— ser encontrado, mas até qualquer caractere em uma matriz transmitida de caracteres de parada de análise ser encontrado. Isso é necessário, por exemplo, ao analisar o primeiro argumento de uma instrução If no qual o separador pode ser qualquer caractere <, >, ou =.

Você pode dar uma olhada no algoritmo de divisão e mesclagem modificado no download do código-fonte que o acompanha.

O Intérprete

A classe responsável pela interpretação do código CSCS é chamada de Intérprete. Ele é implementado como um singleton, ou seja, uma definição de classe na qual só pode haver uma instância da classe. Neste método Init, o Analisador (consulte o artigo original mencionado anteriormente) é inicializado com todas as funções usadas pelo Intérprete:

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());
...
}

No arquivo Constants.cs, são definidos os nomes reais usados na 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";

Todas as funções registradas com o Analisador devem ser implementadas como uma classe derivada da classe ParserFunction e devem substituir seu método Evaluate.

A primeira coisa que o Intérprete faz ao iniciar o trabalho sobre um script é simplificá-lo removendo todos os espaços em branco (a menos que ele esteja dentro de uma cadeia de caracteres) e todos os comentários. Portanto, espaços ou linhas novas não podem ser usados como separadores de operador. O caractere de separador de operador e a cadeia de comentário são definidos em Constants.cs, assim como:

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

Variáveis e matrizes

O CSCS dá suporte a números (tipo duplo), cadeias de caracteres ou tuplas (matrizes de variáveis implementadas como uma lista C#). Cada elemento de uma tupla pode ser uma cadeia de caracteres ou um número, mas não outra tupla. Por isso, não há suporte a matrizes multidimensionais. Para definir uma variável, a função CSCS “set” é usada. A classe C# SetVarFunction implementa a funcionalidade de configuração de uma valor de variável, como mostrado na Figura 2.

Figura 2 Implementação da função variável set

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);
    // Check if the variable to be set has the form of x(i),
    // meaning that this is an array element.
    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);
  }
}

Aqui estão alguns exemplos de definição de uma variável em CSCS:

set(a, "2 + 3");  // a will be equal to the string "2 + 3"
set(b, 2 + 3);    // b will be equal to the number 5
set(c(2), "xyz"); // c will be initialized as a tuple of size 3 with c(0) = c(1) = ""

Observe que não existe uma declaração especial de uma matriz: basta definir uma variável com um índice e a matriz iniciará, se já não tiver sido iniciada, e adicionar elementos vazios a ela, se necessário. No exemplo anterior, os elementos c(0) e c(1) foram adicionados, ambos inicializados em cadeias de caracteres vazias. Isso elimina, na minha opinião, a etapa desnecessária que é exigida na maioria das linguagens de script quando declaram primeiro uma matriz.

Todas as variáveis e matrizes CSCS são criadas usando funções CSCS (como set ou append). Elas são todas definidas com abrangência global e podem ser usadas mais tarde apenas chamando a o nome da variável ou uma variável com um índice. Em C#, isso é implementado na GetVarFunction mostrada na Figura 3.

Figura 3 Implementação da função Get Variable

class GetVarFunction : ParserFunction
{
  internal GetVarFunction(Parser.Result value)
  {
    m_value = value;
  }
  protected override Parser.Result Evaluate(string data, ref int from)
  {
    // First check if this element is part of an array:
    if (from < data.Length && data[from - 1] == Constants.START_ARG)
    {
      // There is an index given - it may be for an element of the tuple.
      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];
    }
    // This is the case for a simple variable, not an array:
    return m_value;
  }
  private Parser.Result m_value;
}

Somente a função variável set deve ser registrada com o Analisador:

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

A função variável get é registrada dentro da função variável set do código C# (consulte a penúltima instrução na Figura 2):

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

Alguns exemplos de obtenção de variáveis na CSCS são:

append(a, "+ 5"); // a will be equal to the string "2 + 3 + 5"
set(b, b * 2);    // b will be equal to the number 10 (if it was 5 before)

Fluxo de controle: If, Else If, Else

As instruções de fluxo de controle If, Else If e Else também são implementadas internamente como as funções do Analisador. Elas são registradas pelo Analisador como qualquer outra função:

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

Somente a palavra-chave IF deve ser registrada com o Analisador. As instruções ELSE_IF e ELSE serão processadas dentro da implementação 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;
}

A implementação real da instrução If está na classe Intérprete, como mostrado na Figura 4.

Figura 4 Implementação da instrução 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)
    {
      // Got here from the middle of the if-block. Skip it.
      m_currentChar = startIfCondition;
      SkipBlock();
    }
    SkipRestBlocks();
    return result;
  }
  // We are in Else. Skip everything in the If statement.
  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();
}

Está explicitamente declarado que a condição If tem a forma: argumento 1, sinal de comparação, argumento 2:

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

É aqui que as instruções opcionais AND, OR ou NOT são adicionadas.

A função EvalCondition apenas compara os tokens de acordo com o sinal de comparação:

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;
}

Aqui está a implementação de uma comparação numérica:

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);
  }
}

A comparação da cadeia de caracteres é semelhante e está disponível no acompanhamento do download de código, tal como a implementação simples da função GetNextIfToken.

Quando uma condição is, else if ou else é verdadeira, todas as instruções dentro do bloco são processadas. Isso é implementado na Figura 5 no método ProcessBlock. Se a condição não for verdadeira, todas as instruções serão ignoradas. Isso é implementado no método SkipBlock (veja o código-fonte de acompanhamento).

Figura 5 Implementação do método 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;
    }
  }
}

Observe como as instruções “Continue” e “Break” são usadas dentro de todo o loop. Estas instruções também são implementadas como funções. Eis a continuação:

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

A implementação da instrução Break é análoga. Ambas estão registradas com o Analisador como qualquer outra função:

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

Você pode usar a função Break para sair dos blocos If aninhados ou para sair de um loop while.

Fluxo de controle: O loop While

O loop while também é implementado e registrado com o Analisador como função:

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

Sempre que a palavra-chave é analisada, o método Evaluate do objeto WhileStatement é chamado:

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;
}

Por isso, a implementação real da do loop while está na classe Intérprete, como mostrado na Figura 6.

Figura 6 Implementação do loop while

internal void ProcessWhile()
{
  int startWhileCondition = m_currentChar;
  // A heuristic check against an infinite loop.
  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;
    }
    // Check for an infinite loop if same values are compared.
    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;
    }
  }
  // The while condition is not true anymore: must skip the whole while
  // block before continuing with next statements.
  SkipBlock();
}

Observe que o loop while verifica de forma proativa um loop infinito após um certo número de iterações, definidos nas definições de configuração pela constante CHECK_AFTER_LOOPS. A heurística aqui é: se exatamente os mesmos valores na condição while são comparados sobre diversos loops, isso poderia indicar um loop infinito. A Figura 7 mostra um loop while no qual eu me esqueci de incrementar a variável de cycle i dentro do loop while.

Detectando um loop while infinito na CSCS
Figura 7 Detectando um loop while infinito na CSCS

Funções, funções, funções

Para que a CSCS faça mais coisas úteis, é necessário adicionar mais conteúdo, ou seja, mais funções devem ser implementadas. Adicionar uma nova função à CSCS é simples: Primeiro, implemente uma classe derivada da classe ParserFunction (substituindo o método Evaluate) e registre-a com o Analisador. Aqui está a implementação da função 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;
}

A função imprime qualquer número de argumentos separados por vírgulas transmitidos a ela. A leitura real dos argumentos é realizada na função auxiliar GetFunctionArgs, que retorna todos os argumentos transmitidos como uma lista de cadeira de caracteres. Você pode observar a função que acompanha o código-fonte.

A segunda, e última etapa, consiste em registrar a função Print com o Analisador na parte de inicialização do programa:

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

A constante Constants.PRINT é definida como “print.”

A Figura 8 mostra a implementação de uma função que começa um novo processo.

Figura 8 Implementação de função que executa um processo

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;
}

Aqui está como você pode encontrar arquivos, iniciar e finalizar um processo e imprimir alguns valores na 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);
}

Figura 9 lista as funções que são implementadas no código-fonte transferível, juntamente com uma descrição resumida. A maioria das funções são wrappers por cima de funções C# correspondentes.

Figura 9 Funções CSCS

abs obtém o valor absoluto de uma expressão
append acrescenta uma cadeia de caracteres ou um número (convertidos a uma cadeia de caracteres) a uma cadeia de caracteres.
cd altera um diretório
cd.. altera o diretório um nível para cima
dir mostra os conteúdos do diretório atual
enc obtém os conteúdos de uma variável de ambiente
exp função exponencial
findfiles encontra arquivos com um determinado padrão
findstr encontra arquivos contendo uma cadeia de caracteres com um padrão específico
indexof retorna o índice de uma subsequência ou -1 se não encontrado
kill finaliza um processo com um determinado número de id de processo
pi retorna uma aproximação do pi constante
pow retorna o primeiro argumento para o poder do segundo argumento
imprimir imprime uma determinada lista de argumentos (números e listas são convertidos em cadeias de caracteres)
psinfo retorna informações do processo para um determinado nome de processo
pstime retorna o tempo total do processador para este processo; útil para tempos de medição
pwd exibe o nome de caminho do diretório atual
run inicia um processo com uma determinada lista de argumento e retorna a id do processo
setenv define o valor de uma variável de ambiente
set define o valor de uma variável ou de um elemento de matriz
sin retorna o valor do seno de um determinado argumento
size retorna o comprimento da cadeia de caracteres ou do tamanho da lista
sqrt retorna a raiz quadrada do número fornecido
substr retorna a subsequência da cadeia de caracteres começando de um determinado índice
tolower converte uma cadeia de caracteres em minúsculas
toupper converte uma cadeia de caracteres em maiúsculas

Internacionalização

Observe que você pode registrar vários rótulos (nomes de função) correspondendo à mesma função com o Parser. Neste sentido, é possível adicionar qualquer número de linguagens diferentes.

Adicionar uma tradução consiste em registrar outra cadeia de caracteres com o mesmo objeto C#. O código C# correspondente acompanha:

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);
...
}

O método AddTranslation adiciona um sinônimo para uma função já existente:

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

Graças ao suporte C# de Unicode, a maioria das linguagens podem ser adicionadas desta forma. Observe que o nome das variáveis também pode ser em Unicode.

Todas as traduções são especificadas no arquivo de configuração. É assim que o arquivo de configuração se parece em espanhol:

<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>

Aqui está um exemplo do código CSCS em espanhol:

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);
}

Observe que o Parser agora pode processar estados de controle e funções tanto em inglês quando em espanhol. Não há limite para o número de linguagens que você pode adicionar.

Conclusão

Todos os elementos CSCS – instruções de fluxo de controle, variáveis, matrizes e funções – são implementados definindo uma classe C# derivada de uma classe de base ParserFunction e substituindo seu método Evaluate. Em seguida, você registra um objeto desta classe com o Parser. Essa abordagem apresenta as seguintes vantagens:

  • Modularidade: Cada função CSCS e instrução de fluxo de controle reside em sua própria classe, por isso, é fácil definir uma nova função ou uma instrução de fluxo de controle ou modificar um existente.
  • Flexibilidade: É possível ter palavras-chave e nomes de função de CSCS em qualquer linguagem. Somente o arquivo de configuração precisa ser modificado. Ao contrário da maioria das linguagens, no CSCS as funções e os nomes de variáveis da instrução de fluxo de controle não precisam estar em caracteres ASCII.

Mas, é claro, nesta fase a linguagem CSCS ainda está longe de estar concluída. Aqui estão algumas maneiras de tornar isso mais útil:

  • Criando matrizes multidimensionais. É possível usar a mesma estrutura de dados C# que a das matrizes dimensionais: a List<Result>. No entanto, mais funcionalidades de análise devem ser adicionadas ao obter e configurar um elemento da matriz multidimensional.
  • Possibilitando que tuplas sejam inicializadas em uma linha.
  • Adicionando operadores lógicos (E, OU, NÃO e assim por diante), o que seria muito útil para instruções if e while.
  • Adicionando a capacidade de escrever funções e métodos em CSCS. Atualmente, apenas funções previamente escritas e compiladas em C# podem ser usadas.
  • Adicionando a capacidade de incluir código-fonte CSCS de outras unidades.
  • Adicionando mais funções que realizam tarefas típicas relacionadas com SO. Devido ao fato de muitas tarefas poderem ser facilmente implementadas em C#, a maioria seria apenas um wrapper fino por cima de suas contrapartes C#.
  • Criando um atalho para a função set(a, b) como “a = b”.

Espero que você tenha gostado desta apresentação breve da linguagem CSCS e de ver como você pode criar sua própria linguagem de script.


Vassili Kaplané ex-desenvolvedor do Microsoft Lync. Ele é apaixonado pela programação em C# e C++. Atualmente, ele mora em Zurique, Suíça, e trabalha como autônomo para vários bancos Você pode encontrá-lo em iLanguage.ch.

Agradecemos ao seguinte especialista técnico da Microsoft pela revisão deste artigo: James McCaffrey