Построение пользовательских деревьев выражений при использовании операторов в C#

этот вопрос касается построения пользовательских деревьев выражений в .NET с использованием операторов, найденных в C# (или любом другом языке). Я предоставляю вопрос вместе с некоторой справочной информацией.


для меня управляемый 2-фазный 64-разрядный ассемблер мне нужна поддержка выражений. Например, может потребоваться собрать:

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

выражение 64-$+mystring должно быть не строкой, а действительным допустимым выражением с преимуществами синтаксиса и введите проверку и IntelliSense в VS, что-то вроде:

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

это выражение не вычисляется при его построении. Вместо этого он оценивается позже в контексте моего ассемблера (когда он определяет смещения символов и тому подобное). .NET framework (начиная с .NET 3.5) обеспечивает поддержку деревьев выражений, и мне кажется, что он идеально подходит для такого рода выражений, которые оцениваются позже или где-то еще.

но я не знаю как убедитесь, что я могу использовать синтаксис C# (используя +,

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

как бы вы это сделали?


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


объяснение моего пример: 64-$+mystring. The $ - текущее смещение, поэтому это определенное число, которое неизвестно заранее (но известно во время оценки). The mystring - символ, который может быть известен или не известен во время оценки (например, когда он еще не определен). Вычитание константы C из символа S равно S + -C. Вычитание двух символов S0 и S1 (S1 - S0) дает целочисленную разницу между значениями двух символов.

однако этот вопрос на самом деле не о как оценить выражения ассемблера, но подробнее о как оценить любое выражение, которое содержит пользовательские классы в них (для таких вещей, как символы и $ в Примере) и как еще убедиться, что он может быть довольно напечатан с помощью какого-то посетителя (таким образом, сохраняя дерево). И поскольку .NET framework имеет свои деревья выражений и посетителей, было бы неплохо использовать их, если это возможно.

4 ответов


Я не знаю, к чему именно вы стремитесь, но ниже приведен некоторый отрывочный подход, который, я думаю, сработает.

Примечание I

  1. продемонстрировать только индексированные ссылочные выражения (таким образом, игнорируя косвенную адресацию через регистры на данный момент; Вы можете добавить RegisterInderectReference аналогично классу SymbolicReference). Это также относится к вам предложил $ (текущее смещение). Это, вероятно, будет уверен регистр (?)
  2. не явно показывает унарный / двоичный operator- на работе. Однако механика в основном та же. Я не стал добавлять его, потому что не мог понять семантику примеров выражений в вашем вопросе
    думаю что вычитание адреса известной строки не полезно, например)
  3. подход не место (смысловое) ограничения: вы можете компенсировать любые ReferenceBase производный объект ireference. На практике вы можете разрешить только один уровень индексирования и определение operator+ непосредственно на SymbolicReference было бы более уместно.
  4. пожертвовал стилем кодирования для демонстрационных целей (в общем, вы не захотите повторно Compile() ваши деревья выражений и прямая оценка с .Compile()() выглядит некрасиво и запутанно. это осталось до OP, чтобы интегрировать его более разборчиво

  5. демонстрация явный оператор преобразования действительно не по теме. Я увлекся Слай (?)

  6. вы можете наблюдать кодом запуск в прямом эфире 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));
        }
    }
}

C# поддерживает назначение лямбда-выражения Expression<TDelegate>, что заставит компилятор выдавать код для создания дерева выражений, представляющего лямбда-выражение, которым затем можно управлять. Например:

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

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

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


опять же, не совсем уверен, что это именно то, что вы ищете, но с начальной точки желания создать какое-то дерево выражений с использованием синтаксиса c#, я придумал...

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

теперь, очевидно, ни один из этих классов на самом деле ничего не делает (вы, вероятно, захотите Compile() метод там среди других), и не все операторы реализованы, и вы можете, очевидно, сократить имена классов, чтобы сделать его более кратким и т. д... но это позволяет вам делать вещи например:

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

после чего, result будет:

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

это не совсем идеально - если вы используете неявные преобразования числовых значений NumericConstantExpression (вместо явного приведения/построения их), то в зависимости от порядка ваших терминов некоторые вычисления могут выполняться встроенными операторами, и вы получите только результирующее число (вы можете просто назвать это "оптимизацией времени компиляции"!)

чтобы показать, что я имею в виду, если вы было выполнить это:

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

в этом случае 25 * 4 оценивается с помощью встроенных целочисленных операторов, поэтому результат фактически идентичен приведенному выше, а не создает дополнительный NumericMultiplyExpression С NumericConstantExpressions (25 и 4) на LHS и RHS.

эти выражения могут быть напечатаны с помощью ToString() и оценивается, если вы предоставляете таблицу символов (здесь просто простой 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));

результаты:

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

надеюсь несмотря на упомянутый недостаток(ы), это что-то приближается к тому, что вы искали (или я просто потратил последние полчаса!)


вы реализуете двухфазный (pass? ассемблер? Назначение двухпроходного сборщика это обрабатывать прямых ссылок (например, символ, который не определен при первой встрече).

тогда вам в значительной степени не нужно создавать дерево выражений.

в фазе (Проход 1) вы анализируете исходный текст (любыми способами: специальным парсером, рекурсивным спуском, генератором парсера) и собираете значения символов (в частности, относительные значения меток в отношении кода или раздела данных, в котором они содержатся. Если вы сталкиваетесь с выражением, вы пытаетесь оценить его, используя оценку выражения "на лету", обычно включающую стек push down для подвыражений и получение конечного результата. Если вы сталкиваетесь с символом, значение которого не определено, вы распространяете неопределенность как результат выражения. Если оператор сборки / команда должен значение выражения для определения символа (например., X EQU A+2) или определить смещения в раздел код/данные (e.g, DS X+23), то значение должно быть определено или ассемблер выдает ошибку. Это позволяет организации A+B-C работать. Другие операторы сборки, которым не нужно значение во время передачи, просто игнорируют неопределенный результат (например, LOAD ABC не заботится о том, что такое ABC, но может определить длину инструкции LOAD).

в фазе (pass II) вы повторно анализируете код таким же образом. На этот раз все символы имеют значения, поэтому все выражения должны оценивать. Те, которые должны были иметь значение в фазе I, проверяются по значениям, полученным в фазе II, чтобы убедиться, что они идентичны (иначе вы получите ошибку фазы). Другие операторы сборки / инструкции теперь имеют достаточно информации для создания фактических инструкций машины или инициализации данных.

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

Если вы построили один пройти ассемблер, вам может потребоваться смоделировать выражение, чтобы разрешить повторную оценку позже. Я обнаружил, что легче производить обратный польский как последовательность " PUSH стоимостью" и arithop и сохраните последовательность (эквивалентную дереву выражений), потому что она плотная (деревья не являются) и тривиальная для оценки путем выполнения линейного сканирования с использованием (как указано выше) небольшого стека pushdown.

на самом деле я сделал это, чтобы произвести обратный польский, который на самом деле действовал как стек выражений сам; во время линейного сканирования, если операнды могут быть оценены они были заменены на "толчок стоимостью " команда, и оставшаяся обратная полировка сжимается, чтобы удалить пузырь. Это не дорого, потому что большинство выражений на самом деле маленькие. И это означало, что любое выражение, которое нужно было сохранить для последующей оценки, было как можно меньше. Если вы резьбовой PUSH идентификатор команды через таблицу символов, затем, когда символ будет определен, вы можете заполнить все частично оцененные выражения и переоценить их; те, которые производят одно значение затем обрабатываются и их пространство рециркулируется. Это позволило мне собрать гигантские программы в 4K word, 16-битной машине, еще в 1974 году, потому что большинство прямых ссылок не очень далеко.