Febrero de 2016

Volumen 31, número 2

C#: scripting personalizable en C#

Por Vassili Kaplan

En este artículo mostraré cómo compilar un lenguaje de scripting personalizado con C# sin usar ninguna biblioteca externa. El lenguaje de scripting está basado en el algoritmo de división y combinación para analizar expresiones matemáticas en C# que presenté en la edición de octubre de 2015 de MSDN Magazine (msdn.com/magazine/mt573716).

Mediante el uso de funciones personalizadas, puedo extender el algoritmo de división y combinación para que analice no solo una expresión matemática, sino también un lenguaje de scripting personalizado. Las instrucciones "estándares" de flujo de control de un lenguaje (if, else, while, continue, break, etc.) se pueden agregar como funciones personalizadas, como también se pueden añadir otras funcionalidades típicas de lenguaje de scripting (comandos del sistema operativo, manipulación de cadenas, búsqueda de archivos, etc.).

Llamaré a mi lenguaje "Scripting personalizado en C#" (Customizable Scripting in C#, CSCS). ¿Por qué querría crear otro lenguaje de scripting más? Porque es un lenguaje fácil de personalizar. Con solo unas líneas de código es posible agregar una nueva función o una nueva instrucción de flujo de control que acepte un número arbitrario de parámetros. Además, los nombres de funciones y las instrucciones de flujo de control se pueden usar en cualquier escenario de idioma distinto del inglés con tan solo unos cambios de configuración, que también mostraré en este artículo. Y al ver cómo se implementa el lenguaje CSCS, podrá crear su propio lenguaje de scripting personalizado.

El ámbito de CSCS

Es muy sencillo implementar un lenguaje de scripting muy básico, pero enormemente difícil implementar un lenguaje de cinco estrellas. Limitaré el ámbito de CSCS aquí para que sepa exactamente lo que puede esperar:

  • El lenguaje CSCS tiene las instrucciones de control de flujo if, else if, else, while, continue y break. Las instrucciones anidadas también se admiten. Aprenderá cómo agregar instrucciones de control adicionales sobre la marcha.
  • No hay valores booleanos. En lugar de escribir "if (a)", deberá escribir "if (a == 1)".
  • Los operadores lógicos no se admiten. En lugar de escribir "if (a ==1 and b == 2)", deberá escribir instrucciones if anidadas: "if (a == 1) { if (b == 2) { … } }".
  • Las funciones y los métodos no se admiten en CSCS, pero se pueden escribir en C# y registrarse con el analizador de división y combinación para que se puedan utilizar con CSCS.
  • Solo se admiten comentarios con estilo "//".
  • Se admiten matrices unidimensionales y variables, definidas en el nivel global. Una variable puede contener un número, una cadena o una tupla (implementada como una lista) de otras variables. Las matrices multidimensionales no se admiten.

En la Figura 1 se muestra un programa de tipo "Hello, World!" en CSCS. Debido a un error en la escritura de "print", el programa muestra en un error al final: "Couldn’t parse token [pint]". Observe que todas las instrucciones anteriores se ejecutaron correctamente; es decir, que CSCS es un intérprete.

"Hello, World!" en CSCS
Figura 1. "Hello, World!" en CSCS

Modificaciones en el algoritmo de división y combinación

He realizado dos cambios en la parte de división del algoritmo de división y combinación. (La parte de combinación permanece igual).

El primer cambio es que el resultado del análisis de una expresión ahora puede ser un número, una cadena o una tupla de valores (cada uno de los cuales puede ser una cadena o un número), en lugar de solo un número. Creé la siguiente clase Parser.Result para almacenar el resultado de la aplicación del algoritmo de división y combinación:

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

La segunda modificación es que ahora la parte de división se realiza no solo hasta que se encuentre un carácter de detención del análisis, ) o \n, sino hasta que se encuentre cualquier carácter de una matriz de caracteres de detención de análisis que se pase. Esto es necesario, por ejemplo, cuando se analiza el primer argumento de una instrucción If, donde el separador puede ser un carácter <, > o =.

Puede echar un vistazo al algoritmo modificado de división y combinación en la descarga del código fuente que se incluye.

El intérprete

La clase responsable de interpretar el código CSCS se conoce como intérprete. Se implementa como un singleton, es decir, una definición de clase donde solo puede haber una instancia de la clase. En su método Init, el analizador (vea el artículo original que se mencionó anteriormente) se inicializa con todas las funcionas que utiliza el 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());
...
}

En el archivo Constants.cs, se definen los nombres reales que se utilizan en 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";

Cualquier función registrada con el analizador debe implementarse como una clase derivada de la clase ParserFunction y debe reemplazar su método Evaluate.

La primera cosa que hace el intérprete cuando comienza a trabajar en un script es simplificarlo quitando todos los espacios en blanco (salvo que se encuentren dentro de una cadena) y todos los comentarios. Por tanto, los espacios y las nuevas líneas no se pueden usar como separadores de operadores. El carácter de separación de operadores y la cadena de comentarios se definen también en Constants.cs:

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

Variables y matrices

CSCS admite números (de tipo double), cadenas o tuplas (matrices de variables implementadas como una lista de C#). Cada uno de los elementos de una tupla pueden ser una cadena o un número, pero no otra tupla. Por consiguiente, las matrices multidimensionales no se admiten. Para definir una variable, se usa la función de CSCS "set". La clase de C# SetVarFunction implementa la funcionalidad de establecer el valor de una variable, como se muestra en la Figura 2.

Figura 2. Implementación de la función para establecer una variable

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

Aquí se muestran algunos ejemplos de definición de una variable en 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) = ""

Tenga en cuenta que no hay ninguna declaración especial de una matriz: simplemente al definir una variable con un índice, se inicializará la matriz si aún no lo está y se agregarán elementos vacíos a ella, si es necesario. En el ejemplo anterior, se agregaron los elementos c(0) y c(1), que se inicializaron como cadenas vacías. Esto elimina, en mi opinión, el paso innecesario de declarar primero una matriz que se requiere en la mayoría de lenguajes de scripting.

Todas las variables y matrices de CSCS se crean mediante funciones de CSCS (como set o append). Todas se definen con ámbito global y se pueden usar posteriormente simplemente con una llamada al nombre de la variable o a la variable con un índice. En C#, esto se implementa en la función GetVarFunction que se muestra en la Figura 3.

Figura 3. Implementación de la función para obtener una 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;
}

Solo se debe registrar con el analizador la función para establecer una variable:

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

La función para obtener una variable se registra dentro del código C# de la función para establecer una variable (consulte la penúltima instrucción de la Figura 2):

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

A continuación se muestran algunos ejemplos de la obtención de variables en CSCS:

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)

Flujo de control: If, Else If, Else

Las instrucciones de flujo de control If, Else If y Else también se implementan de forma interna como funciones del analizador. El analizador las registra como cualquier otra función:

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

Solo debe registrarse la palabra clave IF con el analizador. Las instrucciones ELSE_IF y ELSE se procesarán dentro de la implementación de 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;
}

La implementación real de la instrucción If está en la clase del intérprete, como se muestra en la Figura 4.

Figura 4. Implementación de la instrucción 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();
}

Se indica explícitamente que la condición If debe tener la forma: argumento 1, signo de comparación, 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);

Es aquí donde se pueden agregar instrucciones opcionales AND, OR o NOT.

La función EvalCondition sencillamente compara los tokens según el signo de comparación:

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

Aquí se muestra la implementación de una comparación 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);
  }
}

La comparación de cadenas es similar y está disponible en la descarga de código que se incluye, como también lo está la implementación sencilla de la función GetNextIfToken.

Cuando una condición if, else if o else es verdadera, se procesan todas las instrucciones de dentro del bloque. Esto se implementa en la Figura 5 en el método ProcessBlock. Si la condición no es verdadera, se omiten todas las instrucciones. Eso se implementa en el método SkipBlock (consulte el código fuente que se incluye).

Figura 5. Implementación del 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 se utilizan las instrucciones "Continue" y "Break" dentro del bucle while. Estas instrucciones también se implementan como funciones. Aquí se muestra Continue:

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

La implementación de la instrucción Break es análoga. Las dos se registran con el analizador como cualquier otra función:

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

Puede usar la función Break para salir de bloques If anidados o para salir de un bucle while.

Flujo de control: El bucle while

El bucle while también se implementa y se registra con el analizador como una función:

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

Cuando se analiza la palabra clave while, se llama al método Evaluate del objeto 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;
}

Por tanto, la implementación real del bucle while está en la clase del intérprete, como se muestra en la Figura 6.

Figura 6. Implementación del bucle 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 el bucle while comprueba de forma proactiva si hay un bucle infinito después de un número determinado de iteraciones, que se define en los ajustes de configuración con la constante CHECK_AFTER_LOOPS. La heurística es que si se comparan exactamente los mismos valores en la condición de while durante varias iteraciones, esto podría indicar un bucle infinito. En la Figura 7 se muestra un bucle while donde olvidé incrementar la variable de ciclo i dentro del bucle while.

Detección de un bucle while infinito en CSCS
Figura 7. Detección de un bucle while infinito en CSCS

Funciones, funciones, funciones

Para que CSCS haga más cosas útiles, debe echarse más leña; es decir, deben implementarse más funciones. La incorporación de una función nueva a CSCS es sencilla: Primero implemente una clase que derive de la clase ParserFunction (sobrescriba el método Evaluate) y después regístrela con el analizador. A continuación se muestra la implementación de la función 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;
}

La función imprime un número cualquiera de argumentos separados por comas que se le pase. La lectura real de los argumentos se realiza en la función auxiliar GetFunctionArgs, que devuelve todos los argumentos que se pasen como una lista de cadenas. Puede echar un vistazo a la función en el código fuente que se incluye.

El segundo y último paso es registrar la función Print con el analizador en la parte de inicialización del programa:

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

La constante Constants.PRINT se define como "print".

En la Figura 8 se muestra una implementación de una función que inicia un proceso nuevo.

Figura 8. Implementación de una función que ejecuta un proceso

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

Esta es la forma de encontrar archivos, iniciar y detener un proceso, e imprimir valores en 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);
}

En la Figura 9 se muestran las funciones que se implementan en el código fuente descargable, junto con una breve descripción. La mayoría de las funciones son contenedores de funciones de C# correspondientes.

Figura 9. Funciones de CSCS

abs obtiene el valor absoluto de una expresión.
append anexa una cadena o un número (que se convierte después en una cadena) a una cadena.
cd cambia un directorio.
cd.. cambia de directorio un nivel hacia arriba.
dir muestra el contenido del directorio actual.
enc obtiene el contenido de una variable de entorno.
exp función exponencial.
findfiles busca archivos con un patrón determinado.
findstr busca archivos que contengan una cadena que siga un patrón concreto.
indexof devuelve un índice de una subcadena, o -1 si no se encuentra.
kill detiene el proceso con un número de identificador de proceso determinado.
pi devuelve una aproximación a la constante pi.
pow devuelve el primer argumento elevado a la potencia del segundo argumento.
print imprime una lista de argumentos determinada (los números y las listas se convierten en cadenas).
psinfo devuelve la información del proceso con un nombre de proceso determinado.
pstime devuelve el tiempo de procesador total de este proceso; resulta útil para medir tiempos.
pwd muestra el nombre de la ruta de acceso del directorio actual.
run inicia un proceso con una lista de argumentos determinada y devuelve el identificador del proceso.
setenv obtiene el valor de una variable de entorno.
set obtiene el valor de una variable o de un elemento de matriz.
sin devuelve el valor del seno del argumento proporcionado.
size devuelve la longitud de la cadena o el tamaño de la lista.
sqrt devuelve la raíz cuadrada del número proporcionado.
substr devuelve la subcadena de la cadena que empiece a partir del índice proporcionado.
tolower convierte una cadena a minúsculas.
toupper convierte una cadena a mayúsculas.

Internacionalización

Tenga en cuenta que puede registrar con el analizador varias etiquetas (nombre de funciones) que correspondan a la misma función. De esta manera, es posible agregar un número cualquiera de idiomas distintos.

Para agregar una traducción, hay que registrar otra cadena con el mismo objeto de C#. El código de C# correspondiente queda como sigue:

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

El método AddTranslation agrega un sinónimo para una función ya existente:

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

Gracias a la compatibilidad de C# con Unicode, la mayoría de idiomas se pueden agregar de esta manera. Tenga en cuenta que los nombres también pueden estar en Unicode.

Todas las traducciones se especifican en el archivo de configuración. Este es el aspecto del archivo de configuración en español:

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

A continuación se muestra un ejemplo de código de CSCS en español:

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 el analizador ahora puede procesar funciones e instrucciones de control tanto en inglés como en español. No hay límite respecto al número de idiomas que puede agregar.

Resumen

Todos los elementos de CSCS (las instrucciones de flujo de control, variables, matrices y funciones) se implementan mediante la definición de una clase de C# que se derive de la clase base ParserFunction y reemplace su método Evaluate. Después registra un objeto de esta clase con el analizador. Este enfoque ofrece las ventajas siguientes:

  • Modularidad: cada una de las instrucciones de flujo de control y funciones de CSCS reside en su propia clase, por lo que es fácil definir una función o instrucción de flujo de control nuevas o modificar una existente.
  • Flexibilidad: es posible tener nombres de funciones y palabras clave de CSCS en cualquier idioma. Solo es necesario modificar el archivo de configuración. A diferencia de la mayoría del resto de lenguajes, en CSCS, los nombres de variables y las funciones de las instrucciones de flujo de control no tienen que estar en caracteres ASCII.

Por supuesto, en este momento el lenguaje CSCS es mucho más que completo. A continuación se indican algunas formas para que pueda resultar más útil:

  • La creación de matrices multidimensionales. Se puede usar la misma estructura de datos de C# que la de las matrices unidimensionales, List<Result>. Sin embargo, se debe agregar más funcionalidad de análisis cuando se obtiene y se establece un elemento de la matriz multidimensional.
  • La habilitación de la inicialización de tuplas en una línea.
  • La incorporación de operadores lógicos (AND, OR, NOT, etc.), que sería muy útil para las instrucciones if y while.
  • La incorporación de la funcionalidad para escribir funciones y métodos en CSCS. Actualmente, solo se pueden usar las funciones anteriormente escritas y compiladas en C#.
  • La incorporación de la funcionalidad para incluir código fuente de CSCS de otras unidades.
  • La incorporación de más funciones que realicen tareas habituales relacionadas con el sistema operativo. Como la mayoría de tareas de ese tipo se pueden implementar fácilmente en C#, la mayoría consistiría simplemente en un pequeño contenedor para sus equivalentes en C#.
  • La creación de un acceso directo para la función set(a, b) como "a = b".

Espero que haya disfrutado con este vistazo al lenguaje CSCS y haya aprendido cómo puede crear su propio lenguaje de scripting.


Vassili Kaplanes un antiguo desarrollador de Microsoft Lync. Es un apasionado de la programación en C# y C++. Actualmente vive en Zurich, Suiza, y trabaja como autónomo para varios bancos. Podréis encontrarlo en iLanguage.ch.

Gracias al siguiente experto técnico de Microsoft por revisar este artículo: James McCaffrey