2011-08-23 18 views
10

Esta pregunta se trata de construir árboles de expresiones personalizadas en .NET utilizando los operadores que se encuentran en C# (o en cualquier otro idioma). Proporciono la pregunta junto con la información de fondo.Construir árboles de expresiones personalizadas al usar operadores en C#


Para mi managed 2-phase 64-bit assembler Necesito ayuda para las expresiones. Por ejemplo, uno podría querer montar:

mystring: DB 'hello, world' 
      TIMES 64-$+mystring DB ' ' 

La expresión 64-$+mystring no debe ser una cadena, sino una expresión válida real con los beneficios de la sintaxis y el tipo de cheques y de IntelliSense en VS, algo a lo largo de las líneas de:

64 - Reference.CurrentOffset + new Reference("mystring"); 

Esta expresión no se evalúa cuando se construye. En cambio, se evalúa más adelante en el contexto de mi ensamblador (cuando determina los desplazamientos de símbolo y tal). El .NET Framework (desde .NET 3.5) proporciona soporte para árboles de expresiones, y me parece que es ideal para este tipo de expresiones que se evalúan más tarde o en otro lugar.

Pero no sé cómo garantizar que puedo usar la sintaxis # C (usando +, < <,%, etc ..) para construir el árbol de expresión. Quiero evitar cosas como:

var expression = AssemblerExpression.Subtract(64, 
    AssemblerExpression.Add(AssemblerExpression.CurrentOffset(), 
     AssemblerExpression.Reference("mystring"))) 

¿Qué le parece?


Nota: Necesito un árbol de expresión que ser capaz de convertir la expresión en una representación de cadena personalizada aceptable, y al mismo tiempo ser capaz de evaluarlo en un punto en el tiempo que no sea en su definición.


Una explicación de mi ejemplo: 64-$+mystring. El $ es el desplazamiento actual, por lo que es un número específico que se desconoce de antemano (pero se conoce en el momento de la evaluación). El mystring es un símbolo que puede o no conocerse en el momento de la evaluación (por ejemplo, cuando todavía no se ha definido). Restar una constante C de un símbolo S es lo mismo que S + -C. Restando dos símbolos S0 y S1 (S1 - S0) se obtiene la diferencia entera entre los valores de los dos símbolos.

Sin embargo, esta cuestión no es realmente acerca de cómo evaluar expresiones ensamblador, pero más sobre cómo evaluar cualquier expresión que tiene clases personalizadas en ellos (para cosas como los símbolos y $ en el ejemplo) y cómo todavía asegúrese de que se pueda imprimir bastante utilizando algún visitante (manteniendo así el árbol). Y dado que .NET Framework tiene sus árboles de expresión y visitantes, sería bueno usarlos, si es posible.

Respuesta

5

No sé qué es exactamente lo que se está buscando, pero el siguiente es cierto enfoque esquemático que creo que funcionaría.

Nota I

  1. demuestran expresiones de referencia sólo indexados (ignorando así el direccionamiento indirecto a través de los registros, por ahora, se podía añadir un RegisterInderectReference análoga a la clase SymbolicReference). Esto también se aplica a la función $ (compensación actual) sugerida. Probablemente sería seguro de un registro (?)
  2. no muestra explícitamente el unario/binaria operator- en el trabajo tampoco. Sin embargo, la mecánica es en gran medida la misma. Me detuve en seco de añadirla porque no podía trabajar a cabo la semántica de las expresiones de ejemplo en su pregunta
    (yo creo que restar la dirección de una cadena conocida no es útil, por ejemplo)
  3. el enfoque no coloca límites (semánticos): puede compensar cualquier IReferencia derivada de ReferenceBase. En la práctica, quizás solo desee permitir un nivel de indexación, y definir el operator+ directamente en SymbolicReference sería más apropiado.
  4. ha sacrificado estilo de codificación para los propósitos de demostración (en general, no querrá repetidamente Compile() sus árboles de expresión, y la evaluación directa con .Compile()() ve feo y confuso. Se deja a la OP para integrar de una manera más de manera legible

  5. la demostración del operador de conversión explícita es realmente fuera de tema. me dejé llevar slighlty (?)

  6. se puede observar el código running live on IdeOne.com

.

using System; 
using System.Collections.Generic; 
using System.Linq.Expressions; 
using System.Linq; 


namespace Assembler 
{ 
    internal class State 
    { 
     public readonly IDictionary<string, ulong> SymbolTable = new Dictionary<string, ulong>(); 

     public void Clear() 
     { 
      SymbolTable.Clear(); 
     } 
    } 

    internal interface IReference 
    { 
     ulong EvalAddress(State s); // evaluate reference to address 
    } 

    internal abstract class ReferenceBase : IReference 
    { 
     public static IndexedReference operator+(long directOffset, ReferenceBase baseRef) { return new IndexedReference(baseRef, directOffset); } 
     public static IndexedReference operator+(ReferenceBase baseRef, long directOffset) { return new IndexedReference(baseRef, directOffset); } 

     public abstract ulong EvalAddress(State s); 
    } 

    internal class SymbolicReference : ReferenceBase 
    { 
     public static explicit operator SymbolicReference(string symbol) { return new SymbolicReference(symbol); } 
     public SymbolicReference(string symbol) { _symbol = symbol; } 

     private readonly string _symbol; 

     public override ulong EvalAddress(State s) 
     { 
      return s.SymbolTable[_symbol]; 
     } 

     public override string ToString() { return string.Format("Sym({0})", _symbol); } 
    } 

    internal class IndexedReference : ReferenceBase 
    { 
     public IndexedReference(IReference baseRef, long directOffset) 
     { 
      _baseRef = baseRef; 
      _directOffset = directOffset; 
     } 

     private readonly IReference _baseRef; 
     private readonly long _directOffset; 

     public override ulong EvalAddress(State s) 
     { 
      return (_directOffset<0) 
       ? _baseRef.EvalAddress(s) - (ulong) Math.Abs(_directOffset) 
       : _baseRef.EvalAddress(s) + (ulong) Math.Abs(_directOffset); 
     } 

     public override string ToString() { return string.Format("{0} + {1}", _directOffset, _baseRef); } 
    } 
} 

namespace Program 
{ 
    using Assembler; 

    public static class Program 
    { 
     public static void Main(string[] args) 
     { 
      var myBaseRef1 = new SymbolicReference("mystring1"); 

      Expression<Func<IReference>> anyRefExpr =() => 64 + myBaseRef1; 
      Console.WriteLine(anyRefExpr); 

      var myBaseRef2 = (SymbolicReference) "mystring2"; // uses explicit conversion operator 

      Expression<Func<IndexedReference>> indexedRefExpr =() => 64 + myBaseRef2; 
      Console.WriteLine(indexedRefExpr); 

      Console.WriteLine(Console.Out.NewLine + "=== show compiletime types of returned values:"); 
      Console.WriteLine("myBaseRef1  -> {0}", myBaseRef1); 
      Console.WriteLine("myBaseRef2  -> {0}", myBaseRef2); 
      Console.WriteLine("anyRefExpr  -> {0}", anyRefExpr.Compile().Method.ReturnType); 
      Console.WriteLine("indexedRefExpr -> {0}", indexedRefExpr.Compile().Method.ReturnType); 

      Console.WriteLine(Console.Out.NewLine + "=== show runtime types of returned values:"); 
      Console.WriteLine("myBaseRef1  -> {0}", myBaseRef1); 
      Console.WriteLine("myBaseRef2  -> {0}", myBaseRef2); 
      Console.WriteLine("anyRefExpr  -> {0}", anyRefExpr.Compile()());  // compile() returns Func<...> 
      Console.WriteLine("indexedRefExpr -> {0}", indexedRefExpr.Compile()()); 

      Console.WriteLine(Console.Out.NewLine + "=== observe how you could add an evaluation model using some kind of symbol table:"); 
      var compilerState = new State(); 
      compilerState.SymbolTable.Add("mystring1", 0xdeadbeef); // raw addresses 
      compilerState.SymbolTable.Add("mystring2", 0xfeedface); 

      Console.WriteLine("myBaseRef1 evaluates to  0x{0:x8}", myBaseRef1.EvalAddress(compilerState)); 
      Console.WriteLine("myBaseRef2 evaluates to  0x{0:x8}", myBaseRef2.EvalAddress(compilerState)); 
      Console.WriteLine("anyRefExpr displays as  {0:x8}", anyRefExpr.Compile()()); 
      Console.WriteLine("indexedRefExpr displays as {0:x8}", indexedRefExpr.Compile()()); 
      Console.WriteLine("anyRefExpr evaluates to  0x{0:x8}", anyRefExpr.Compile()().EvalAddress(compilerState)); 
      Console.WriteLine("indexedRefExpr evaluates to 0x{0:x8}", indexedRefExpr.Compile()().EvalAddress(compilerState)); 
     } 
    } 
} 
+0

slighlty actualizado; Esperaré un poco de comentarios antes de expandir en direcciones innecesarias – sehe

+0

Gracias por tomarse el tiempo para escribir una publicación tan extensa. Creo que su solución va en la dirección correcta, pero algunos puntos clave no están claros o faltan: 1) Quiero evaluar símbolos (su 'EvalAddress()') más tarde, cuando evalúo toda la función. Esto no debería ser difícil, simplemente pasando el 'Estado' a la función. 2) Quiero conservar el árbol para poder recorrerlo con un visitante del árbol de expresiones e imprimir mi propia representación de cadenas de cada una de las clases personalizadas. Pruebe esto, por ejemplo: '10 + ref1 + 20 + ref2', ¿cómo funcionaría? – Virtlink

+0

@Virtlink: Respuestas rápidas 1) de hecho, siempre tendrá el estado del programa durante la etapa de emisión del código de operación; Opté por pasarlo (KISS) pero podría usar 'singleton' y/o IoC. 2) El código ya lo hace (solo compilando los árboles para evaluarlos). Simplemente manténgase con los árboles 'Expression <>' el mayor tiempo posible. En la impresión: mi código ya lo hace (ver ToString). Si desea realizar transformaciones más avanzadas/flexibles (optimización, anotación, etc.) debe ir a un patrón de visitante (use Visitor + Builder para realizar transformaciones de AST a AST, por ejemplo) – sehe

4

C# admite la asignación de una expresión lambda a Expression<TDelegate>, lo que hará que el compilador emita código para crear un árbol de expresiones que represente la expresión lambda, que luego puede manipular. Por ejemplo:

Expression<Func<int, int, int>> times = (a, b) => a * b; 

A continuación, podría potencialmente llevar el árbol de expresión generada y convertirlo en árbol de sintaxis del ensamblador, pero esto no parece ser exactamente lo que está buscando, y no lo hace pensar Podremos aprovechar el compilador de C# para hacer esto para una entrada arbitraria.

Probablemente va a tener que construir su propio analizador para su lenguaje ensamblador, ya que no creo que el compilador C# haga lo que quiera en este caso.

+0

He examinado las expresiones lambda, y el problema real que tengo se vuelve aparente cuando intento esto: 'Expresión > expr =() => 64 + nueva Referencia (" mystring ");'. El compilador no me permitirá agregar cosas arbitrarias (lo cual es lógico) y no sabría lo que las sobrecargas del operador tendrían que hacer o regresar para que esto funcione. – Virtlink

2

Una vez más, no muy seguro de si esto es exactamente lo que está buscando, pero desde el punto de partida de querer crear una especie de árbol de expresión usando sintaxis de C#, yo he llegado con ...

public abstract class BaseExpression 
{ 
    // Maybe a Compile() method here? 
} 

public class NumericExpression : BaseExpression 
{ 
    public static NumericExpression operator +(NumericExpression lhs, NumericExpression rhs) 
    { 
     return new NumericAddExpression(lhs, rhs); 
    } 

    public static NumericExpression operator -(NumericExpression lhs, NumericExpression rhs) 
    { 
     return new NumericSubtractExpression(lhs, rhs); 
    } 

    public static NumericExpression operator *(NumericExpression lhs, NumericExpression rhs) 
    { 
     return new NumericMultiplyExpression(lhs, rhs); 
    } 

    public static NumericExpression operator /(NumericExpression lhs, NumericExpression rhs) 
    { 
     return new NumericDivideExpression(lhs, rhs); 
    } 

    public static implicit operator NumericExpression(int value) 
    { 
     return new NumericConstantExpression(value); 
    } 

    public abstract int Evaluate(Dictionary<string,int> symbolTable); 
    public abstract override string ToString(); 
} 

public abstract class NumericBinaryExpression : NumericExpression 
{ 
    protected NumericExpression LHS { get; private set; } 
    protected NumericExpression RHS { get; private set; } 

    protected NumericBinaryExpression(NumericExpression lhs, NumericExpression rhs) 
    { 
     LHS = lhs; 
     RHS = rhs; 
    } 

    public override string ToString() 
    { 
     return string.Format("{0} {1} {2}", LHS, Operator, RHS); 
    } 
} 

public class NumericAddExpression : NumericBinaryExpression 
{ 
    protected override string Operator { get { return "+"; } } 

    public NumericAddExpression(NumericExpression lhs, NumericExpression rhs) 
     : base(lhs, rhs) 
    { 
    } 

    public override int Evaluate(Dictionary<string,int> symbolTable) 
    { 
     return LHS.Evaluate(symbolTable) + RHS.Evaluate(symbolTable); 
    } 
} 

public class NumericSubtractExpression : NumericBinaryExpression 
{ 
    protected override string Operator { get { return "-"; } } 

    public NumericSubtractExpression(NumericExpression lhs, NumericExpression rhs) 
     : base(lhs, rhs) 
    { 
    } 

    public override int Evaluate(Dictionary<string, int> symbolTable) 
    { 
     return LHS.Evaluate(symbolTable) - RHS.Evaluate(symbolTable); 
    } 
} 

public class NumericMultiplyExpression : NumericBinaryExpression 
{ 
    protected override string Operator { get { return "*"; } } 

    public NumericMultiplyExpression(NumericExpression lhs, NumericExpression rhs) 
     : base(lhs, rhs) 
    { 
    } 

    public override int Evaluate(Dictionary<string, int> symbolTable) 
    { 
     return LHS.Evaluate(symbolTable) * RHS.Evaluate(symbolTable); 
    } 
} 

public class NumericDivideExpression : NumericBinaryExpression 
{ 
    protected override string Operator { get { return "/"; } } 

    public NumericDivideExpression(NumericExpression lhs, NumericExpression rhs) 
     : base(lhs, rhs) 
    { 
    } 

    public override int Evaluate(Dictionary<string, int> symbolTable) 
    { 
     return LHS.Evaluate(symbolTable)/RHS.Evaluate(symbolTable); 
    } 
} 

public class NumericReferenceExpression : NumericExpression 
{ 
    public string Symbol { get; private set; } 

    public NumericReferenceExpression(string symbol) 
    { 
     Symbol = symbol; 
    } 

    public override int Evaluate(Dictionary<string, int> symbolTable) 
    { 
     return symbolTable[Symbol]; 
    } 

    public override string ToString() 
    { 
     return string.Format("Ref({0})", Symbol); 
    } 
} 

public class StringConstantExpression : BaseExpression 
{ 
    public string Value { get; private set; } 

    public StringConstantExpression(string value) 
    { 
     Value = value; 
    } 

    public static implicit operator StringConstantExpression(string value) 
    { 
     return new StringConstantExpression(value); 
    } 
} 

public class NumericConstantExpression : NumericExpression 
{ 
    public int Value { get; private set; } 

    public NumericConstantExpression(int value) 
    { 
     Value = value; 
    } 

    public override int Evaluate(Dictionary<string, int> symbolTable) 
    { 
     return Value; 
    } 

    public override string ToString() 
    { 
     return Value.ToString(); 
    } 
} 

Ahora, obviamente, ninguna de estas clases hace nada (lo que probablemente quiere un método Compile() allí, entre otros) y no todos los operadores se implementan, y es obvio que puede acortar los nombres de las clases para que sea más conciso, etc. ... pero te permite hacer cosas como:

var result = 100 * new NumericReferenceExpression("Test") + 50; 

Después de lo cual, result habrá:

 
NumericAddExpression 
- LHS = NumericMultiplyExpression 
     - LHS = NumericConstantExpression(100) 
     - RHS = NumericReferenceExpression(Test) 
- RHS = NumericConstantExpression(50) 

No es del todo perfecto - si utiliza las conversiones implícitas de valores numéricos a NumericConstantExpression (en vez de forma explícita a presión/construirlas), a continuación, en función de la ordenación de las sus términos, algunos de los cálculos pueden ser realizados por los operadores integrados, y solo obtendrá el número resultante (¡podría simplemente llamar a esto una "optimización en tiempo de compilación"!)

Para mostrar lo que quiero decir, si se va a ejecutar en su lugar esto:

var result = 25 * 4 * new NumericReferenceExpression("Test") + 50; 

en este caso, el 25 * 4 se evalúa mediante operadores enteros incorporadas, por lo que el resultado es realmente idéntica a la anterior , en lugar de construir un NumericMultiplyExpression adicional con dos NumericConstantExpression s (25 y 4) en el LHS y el RHS.

Estas expresiones se pueden imprimir utilizando ToString() y evaluados, si se proporciona una tabla de símbolos (en este caso un simple Dictionary<string, int>):

var result = 100 * new NumericReferenceExpression("Test") + 50; 
var symbolTable = new Dictionary<string, int> 
{ 
    { "Test", 30 } 
}; 
Console.WriteLine("Pretty printed: {0}", result); 
Console.WriteLine("Evaluated: {0}", result.Evaluate(symbolTable)); 

Resultados en:

 
Pretty printed: 100 * Ref(Test) + 50 
Evaluated: 3050 

Esperamos que a pesar de la desventaja (s) mencionado, esto es algo parecido a lo que buscabas (¡o acabo de malgastar la última media hora!)

+0

Gracias por tomarse el tiempo para responder a mi pregunta. Tu dirección es la misma que yo cuando encontré este problema por primera vez. Parece que no usas ninguno de los árboles de expresiones disponibles desde 3.5. Tal vez simplemente no es posible hacer lo que quiero con árboles de expresiones. Sin embargo ... ¿cómo evaluarías esto? – Virtlink

+0

Como indiqué en mi respuesta anterior, aunque los árboles de expresiones y la capacidad de compilarlos son ciertamente características muy útiles, debe conocer la firma de la función exacta para crear la expresión apropiada >. La evaluación es bastante sencilla, y editaré la respuesta para demostrar. – Iridium

2

Estás implementando g un ensamblador de dos fases (¿pasa?) El propósito de un ensamblador de dos pasadas es manejar referencias hacia delante (por ejemplo, de símbolos que no están definidos cuando se encuentran primero).

A continuación, casi no es necesario construir un árbol de expresión.

En fase (pase 1), analiza el texto fuente (por cualquier medio que desee: analizador ad hoc, descenso recursivo, generador de analizador sintáctico) y recopila valores de símbolos (en particular, los valores relativos de las etiquetas con respecto a el código o la sección de datos en la que están contenidos. Si encuentra una expresión, intenta evaluarla usando una evaluación de expresión sobre la marcha, que generalmente implica una pila de inserción para subexpresiones y produce un resultado final. símbolo cuyo valor es indefinido, propaga undefinedess como resultado de la expresión. Si el operador/comando de ensamblaje necesita el valor de expresión para definir un símbolo (p. ej., X EQU A + 2) o para determinar desplazamientos en un código/datos sección (p. ej., DS X + 23), se debe definir el valor o el ensamblador arroja un error. Esto permite que funcione ORG A + BC. Otros operadores de ensamblaje que no necesitan el valor durante el pase uno simplemente ignoran el resultado indefinido (por ejemplo, LOAD ABC no se preocupa de qué es ABC, pero puede determinar la duración de la instrucción LOAD).

En fase (pase II), vuelve a analizar el código de la misma manera. Esta vez todos los símbolos tienen valores, por lo que todas las expresiones deberían evaluar. Los que tuvieron que tener un valor en la Fase I se comparan con los valores producidos en la Fase II para garantizar que sean idénticos (de lo contrario, se obtiene un error de FASE). Otros operadores/instrucciones de ensamblado ahora tienen suficiente información para generar las instrucciones de máquina reales o las inicializaciones de datos.

El punto es, nunca se tiene que construir un árbol de expresión. Simplemente evalúas la expresión a medida que la encuentras.

Si usted construyó un pase ensamblador uno, puede ser necesario para modelar la expresión para permitir la re-evaluación posterior. Me resultó más fácil producir pulido inverso como secuencia de "PUSH value" y arithop, y almacenar la secuencia (equivalente al árbol de expresiones), porque es densa (los árboles no son) y trivial para evaluar al hacer una escaneo lineal usando (como arriba) una pequeña pila de pushdown.

De hecho, lo que hice fue producir pulido inverso que de hecho actuó como la pila de expresión en sí misma; durante un escaneo lineal, si los operandos podían evaluarse, se reemplazaban por un comando "PUSH value", y el pulido inverso restante se contrae para eliminar el bubble. Esto no es costoso porque la mayoría de las expresiones son muy pequeñas. Y significaba que cualquier expresión que tuviera que guardarse para una evaluación posterior era lo más pequeña posible. Si enhebró los comandos PUSH identificador a través de la tabla de símbolos, cuando se define como símbolo, puede completar todas las expresiones parcialmente evaluadas y reevaluarlas; los que producen un solo valor se procesan y su espacio se recicla. Esto me permitió ensamblar programas gigantes en una máquina de 4K y 16 bits, en 1974, porque la mayoría de las referencias avanzadas realmente no llegan muy lejos.

+0

Tiene razón, pero mi ensamblador no analiza nada. Todas las instrucciones, expresiones, secciones, símbolos y archivos de objetos son objetos, de modo que se pueden crear directamente desde otras aplicaciones de C#. Las expresiones también son creadas por dicha aplicación C#, y más tarde mi ensamblador las evalúa. Necesito el árbol de expresiones para poder imprimir bastante el modelo de objetos en, por ejemplo, el lenguaje NASM (para la depuración). – Virtlink

+0

Entonces, lo que realmente quieres es un árbol sintáctico abstracto para tu ensamblador (el resultado del análisis clásico). Puede insistir en que sus expresiones sean directamente evaluables por C# lo que hace que sea más difícil producirlas como lo ha señalado. Pero para cualquier cosa que no sea una "expresión" (por ejemplo, una especificación de instrucción LOAD), usted todavía tiene que escribir algo para escalar sobre ese árbol y producir el valor final. Las expresiones que son * realmente * fáciles de evaluar son solo una parte de esto. No estoy seguro de ver el valor real de las expresiones evaluables de C#. Y todavía tienes el problema de dos pases para resolver. –

+0

@IraBaxter: +1 para buena escritura; buen estilo de escritura también. Estoy dispuesto a apostar que eres un buen jugador de ajedrez. No me preguntes por qué :) ¡Salud! – sehe

Cuestiones relacionadas