Создание пользовательских деревьев выражений при использовании операторов в C # - PullRequest
10 голосов
/ 23 августа 2011

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


Для моего управляемого двухфазного 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 # (используя +, <<,% и т. д.) для построения дерева выражений.Я хочу предотвратить такие вещи, как: </p>

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

Как бы вы поступили об этом?


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


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

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

Ответы [ 4 ]

5 голосов
/ 06 октября 2011

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

Примечание I

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

  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));
        }
    }
}
4 голосов
/ 23 августа 2011

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

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

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

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

2 голосов
/ 11 октября 2011

Вы реализуете двухфазный (проходной) ассемблер? Цель двухпроходного ассемблера должен обрабатывать прямые ссылки (например, символ, который не определен при первом обнаружении).

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

В фазе (проход 1) вы анализируете исходный текст (любыми средствами, которые вам нравятся: специальный анализатор, рекурсивный спуск, генератор анализатора) и собираете значения символов (в частности, относительные значения меток по отношению к раздел кода или данных, в котором они содержатся. Если вы встретите выражение, вы попытаетесь оценить его, используя оценку выражения «на лету», как правило, с использованием стека для подвыражений и получения окончательного результата. Если вы встретите символ значение которого не определено, вы передаете undefinedess как результат выражения.Если оператору / команде сборки необходимо значение выражения для определения символа (например, X EQU A + 2) или для определения смещений в раздел кода / данных (например, DS X + 23), то значение должно быть определено или ассемблер выдает ошибку. Это позволяет ORG A + BC работать. Другие операторы сборки, которым не нужно значение во время первого прохода, просто игнорируют неопределенный результат (например, LOAD ABC не волнует, что такое ABC, но может определить длину инструкции LOAD).

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

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

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

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

2 голосов
/ 06 октября 2011

Опять же, я не совсем уверен, что это именно то, что вам нужно, но с момента начала создания какого-либо дерева выражений с использованием синтаксиса 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 с двумя NumericConstantExpression с (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

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

...