Построение пользовательских деревьев выражений при использовании операторов в С#
Этот вопрос касается построения пользовательских деревьев выражений в .NET с использованием операторов, найденных на С# (или любом другом языке). Я задаю этот вопрос вместе с некоторой справочной информацией.
Для моего управляемого двухфазного 64-битного ассемблера Мне нужна поддержка выражений. Например, можно захотеть собрать:
mystring: DB 'hello, world'
TIMES 64-$+mystring DB ' '
Выражение 64-$+mystring
не должно быть строкой, а действительным действительным выражением с преимуществами синтаксиса и проверки типов и IntelliSense в VS, что-то вроде строк:
64 - Reference.CurrentOffset + new Reference("mystring");
Это выражение не оценивается при его построении. Вместо этого он оценивается позже в моем контексте ассемблера (когда он определяет смещения символа и т.д.)..NET framework (начиная с .NET 3.5) обеспечивает поддержку деревьев выражений, и мне кажется, что он идеально подходит для таких выражений, которые оцениваются позже или где-то еще.
Но я не знаю, как обеспечить, чтобы я мог использовать синтаксис С# (используя +, < <,% и т.д.) для построения дерева выражений. Я хочу предотвратить такие вещи, как:
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 есть деревья выражений и посетителей, было бы неплохо использовать их, если это возможно.
Ответы
Ответ 1
Я не знаю, к чему именно вы стремитесь, но следующий подход, который, я думаю, будет работать.
Примечание I
- демонстрируют только индексированные ссылочные выражения (таким образом, игнорируя косвенную адресацию через регистры, вы можете добавить RegisterInderectReference, аналогичную классу SymbolicReference). Это также говорит о том, что вы предложили функцию
$
(текущее смещение). Вероятно, вероятно, что регистр (?)
- явно не показывает унарный/двоичный
operator-
. Однако механики в основном одинаковы. Я не стал добавлять его, потому что не смог выработать семантику выборочных выражений в вашем вопросе
(я бы подумал, что вычитание адреса известной строки не полезно, например)суб >
- подход не устанавливает (семантические) пределы: вы можете компенсировать любые IRB-запросы, связанные с базой данных. На практике вам может потребоваться только один уровень индексации, и определение
operator+
непосредственно на SymbolicReference было бы более уместным.
-
Пожертвовал стиль кодирования для демонстрационных целей (в общем, вы не захотите повторно использовать Compile()
ваши деревья выражений, а прямая оценка с помощью .Compile()()
выглядит уродливой и запутанной. интегрировать его более понятным образом
-
Демонстрация явного оператора преобразования действительно вне темы. Я увлекся слайдом (?)
- Вы можете увидеть код работающий на 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));
}
}
}
Ответ 2
С# поддерживает назначение лямбда-выражения для Expression<TDelegate>
, что заставит компилятор испускать код для создания дерева выражений, представляющего выражение лямбда, которое вы затем можете манипулировать. Например:.
Expression<Func<int, int, int>> times = (a, b) => a * b;
Затем вы могли бы взять сгенерированное дерево выражений и преобразовать его в свое дерево синтаксиса ассемблера, но это, похоже, не совсем то, что вы ищете, и я не думаю, что вы сможете для использования компилятора С# для этого для произвольного ввода.
Вероятно, вам придется создать собственный парсер для вашего языка ассемблера, поскольку я не думаю, что компилятор С# будет делать то, что вы хотите в этом случае.
Ответ 3
Опять же, не совсем уверен, что это именно то, что вы ищете, но из начальной точки захотелось создать какое-то дерево выражений с помощью синтаксиса С#, я придумал...
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
Надеюсь, несмотря на упомянутый недостаток, это что-то приближается к тому, что вы искали (или я просто потратил впустую последние полчаса!)
Ответ 4
Вы реализуете двухэтапный (pass?) ассемблер? Назначение двухпроходного ассемблера
- обрабатывать прямые ссылки (например, символ undefined при первом обнаружении).
Тогда вам в значительной степени не нужно создавать дерево выражений.
В фазе (проход 1) вы анализируете исходный текст (любыми способами: ad hoc parser, рекурсивный спуск, генератор парсера) и собираете значения символов (в частности, относительные значения меток относительно кода или данных, в которых они содержатся.Если вы сталкиваетесь с выражением, вы пытаетесь оценить его с помощью экспресс-проверки на лету, как правило, с использованием стека push down для подвыражений и получения окончательного результата. Если вы столкнулись с символом значение которого undefined, вы распространяете неопределенность как результат выражения. Если оператору/команде сборки требуется значение выражения для определения символа (например, X EQU A + 2) или для определения смещений в секцию кода/данных (например, DS X + 23), тогда значение должно быть определено или сборщик выдает ошибку. Это позволяет работать с ORG A + BC. Другие операторы сборки, которые не нуждаются в значении во время прохождения, просто игнорируют undefined результат (например, LOAD ABC не волнует, что такое ABC, но может определить удлинение h инструкции LOAD).
В фазе (проход II) вы повторно разбираете код таким же образом. На этот раз все символы имеют значения, поэтому все выражения должны оцениваться. Те, которые должны были иметь значение в Фазе I, проверяются на значения, полученные в Фазе II, чтобы убедиться, что они идентичны (иначе вы получите ошибку PHASE). Другие операторы/инструкции для сборки теперь имеют достаточную информацию для генерации фактических машинных инструкций или инициализаций данных.
Дело в том, что вам никогда не нужно строить дерево выражений. Вы просто оцениваете выражение, когда сталкиваетесь с ним.
Если вы построили однопроходный ассемблер, вам может потребоваться смоделировать выражение, чтобы позднее было переоценено. Мне было легче создать обратный полиш в виде последовательности "значение PUSH" и арифпопа и сохранить последовательность (эквивалентную дереву выражения), поскольку она плотная (деревья нет) и тривиальна для оценки путем выполнения линейного сканирования с использованием ( как указано выше) небольшой пакет выталкивания.
Фактически то, что я сделал, это создать обратный польский, который фактически действовал как сам стек выражения; во время линейного сканирования, если бы операнды могли быть оценены, они были заменены командой "PUSH value", а оставшаяся обратная полировка была сжата для удаления пузыря. Это не дорого, потому что большинство выражений на самом деле крошечные. И это означало, что любое выражение, которое нужно было сохранить для последующей оценки, было как можно меньше. Если вы пронумеровали команды идентификатора PUSH через таблицу символов, тогда, когда в качестве символа станет определено, вы можете заполнить все частично оцененные выражения и переоценить их; те, которые производят одно значение, затем обрабатываются и их пространство перерабатывается. Это позволило мне собрать гигантские программы в 4-килобайтном слове, 16-битной машине, еще в 1974 году, потому что большинство прямых ссылок на самом деле не очень далеко.