981 lines
26 KiB
C#
981 lines
26 KiB
C#
#region Copyright & License Information
|
|
/*
|
|
* Copyright (c) The OpenRA Developers and Contributors
|
|
* This file is part of OpenRA, which is free software. It is made
|
|
* available to you under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation, either version 3 of
|
|
* the License, or (at your option) any later version. For more
|
|
* information, see COPYING.
|
|
*/
|
|
#endregion
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Linq.Expressions;
|
|
using Expressions = System.Linq.Expressions;
|
|
|
|
namespace OpenRA.Support
|
|
{
|
|
public abstract class VariableExpression
|
|
{
|
|
public static readonly IReadOnlyDictionary<string, int> NoVariables = new ReadOnlyDictionary<string, int>(new Dictionary<string, int>());
|
|
|
|
public readonly string Expression;
|
|
readonly HashSet<string> variables = new();
|
|
public IEnumerable<string> Variables => variables;
|
|
|
|
enum CharClass { Whitespace, Operator, Mixed, Id, Digit }
|
|
|
|
static CharClass CharClassOf(char c)
|
|
{
|
|
switch (c)
|
|
{
|
|
case '~':
|
|
case '!':
|
|
case '%':
|
|
case '^':
|
|
case '&':
|
|
case '*':
|
|
case '(':
|
|
case ')':
|
|
case '+':
|
|
case '=':
|
|
case '[':
|
|
case ']':
|
|
case '{':
|
|
case '}':
|
|
case '|':
|
|
case ':':
|
|
case ';':
|
|
case '\'':
|
|
case '"':
|
|
case '<':
|
|
case '>':
|
|
case '?':
|
|
case ',':
|
|
case '/':
|
|
return CharClass.Operator;
|
|
|
|
case '.':
|
|
case '$':
|
|
case '-':
|
|
case '@':
|
|
return CharClass.Mixed;
|
|
|
|
case '0':
|
|
case '1':
|
|
case '2':
|
|
case '3':
|
|
case '4':
|
|
case '5':
|
|
case '6':
|
|
case '7':
|
|
case '8':
|
|
case '9':
|
|
return CharClass.Digit;
|
|
|
|
// Fast-track normal whitespace
|
|
case ' ':
|
|
case '\t':
|
|
case '\n':
|
|
case '\r':
|
|
return CharClass.Whitespace;
|
|
|
|
// Should other whitespace be tested?
|
|
default:
|
|
return char.IsWhiteSpace(c) ? CharClass.Whitespace : CharClass.Id;
|
|
}
|
|
}
|
|
|
|
enum Associativity { Left, Right }
|
|
|
|
[Flags]
|
|
enum Sides
|
|
{
|
|
// Value type
|
|
None = 0,
|
|
|
|
// Postfix unary operator and/or group closer
|
|
Left = 1,
|
|
|
|
// Prefix unary operator and/or group opener
|
|
Right = 2,
|
|
|
|
// Binary+ operator
|
|
Both = Left | Right
|
|
}
|
|
|
|
enum Grouping { None, Parens }
|
|
|
|
enum TokenType
|
|
{
|
|
// fixed values
|
|
False,
|
|
True,
|
|
|
|
// varying values
|
|
Number,
|
|
Variable,
|
|
|
|
// operators
|
|
OpenParen,
|
|
CloseParen,
|
|
Not,
|
|
Negate,
|
|
OnesComplement,
|
|
And,
|
|
Or,
|
|
Equals,
|
|
NotEquals,
|
|
LessThan,
|
|
LessThanOrEqual,
|
|
GreaterThan,
|
|
GreaterThanOrEqual,
|
|
Add,
|
|
Subtract,
|
|
Multiply,
|
|
Divide,
|
|
Modulo,
|
|
|
|
Invalid
|
|
}
|
|
|
|
enum Precedence
|
|
{
|
|
Unary = 16,
|
|
Multiplication = 12,
|
|
Addition = 11,
|
|
Relation = 9,
|
|
Equality = 8,
|
|
And = 4,
|
|
Or = 3,
|
|
Binary = 0,
|
|
Value = 0,
|
|
Parens = -1,
|
|
Invalid = ~0
|
|
}
|
|
|
|
readonly struct TokenTypeInfo
|
|
{
|
|
public readonly string Symbol;
|
|
public readonly Precedence Precedence;
|
|
public readonly Sides OperandSides;
|
|
public readonly Sides WhitespaceSides;
|
|
public readonly Associativity Associativity;
|
|
public readonly Grouping Opens;
|
|
public readonly Grouping Closes;
|
|
|
|
public TokenTypeInfo(string symbol, Precedence precedence, Sides operandSides = Sides.None,
|
|
Associativity associativity = Associativity.Left,
|
|
Grouping opens = Grouping.None, Grouping closes = Grouping.None)
|
|
{
|
|
Symbol = symbol;
|
|
Precedence = precedence;
|
|
OperandSides = operandSides;
|
|
WhitespaceSides = Sides.None;
|
|
Associativity = associativity;
|
|
Opens = opens;
|
|
Closes = closes;
|
|
}
|
|
|
|
public TokenTypeInfo(string symbol, Precedence precedence, Sides operandSides,
|
|
Sides whitespaceSides,
|
|
Associativity associativity = Associativity.Left,
|
|
Grouping opens = Grouping.None, Grouping closes = Grouping.None)
|
|
{
|
|
Symbol = symbol;
|
|
Precedence = precedence;
|
|
OperandSides = operandSides;
|
|
WhitespaceSides = whitespaceSides;
|
|
Associativity = associativity;
|
|
Opens = opens;
|
|
Closes = closes;
|
|
}
|
|
|
|
public TokenTypeInfo(string symbol, Precedence precedence, Grouping opens, Grouping closes = Grouping.None,
|
|
Associativity associativity = Associativity.Left)
|
|
{
|
|
Symbol = symbol;
|
|
Precedence = precedence;
|
|
WhitespaceSides = Sides.None;
|
|
OperandSides = opens == Grouping.None ?
|
|
(closes == Grouping.None ? Sides.None : Sides.Left) :
|
|
(closes == Grouping.None ? Sides.Right : Sides.Both);
|
|
Associativity = associativity;
|
|
Opens = opens;
|
|
Closes = closes;
|
|
}
|
|
}
|
|
|
|
static IEnumerable<TokenTypeInfo> CreateTokenTypeInfoEnumeration()
|
|
{
|
|
for (var i = 0; i <= (int)TokenType.Invalid; i++)
|
|
{
|
|
switch ((TokenType)i)
|
|
{
|
|
case TokenType.Invalid:
|
|
yield return new TokenTypeInfo("(<INVALID>)", Precedence.Invalid);
|
|
continue;
|
|
case TokenType.False:
|
|
yield return new TokenTypeInfo("false", Precedence.Value);
|
|
continue;
|
|
case TokenType.True:
|
|
yield return new TokenTypeInfo("true", Precedence.Value);
|
|
continue;
|
|
case TokenType.Number:
|
|
yield return new TokenTypeInfo("(<number>)", Precedence.Value);
|
|
continue;
|
|
case TokenType.Variable:
|
|
yield return new TokenTypeInfo("(<variable>)", Precedence.Value);
|
|
continue;
|
|
case TokenType.OpenParen:
|
|
yield return new TokenTypeInfo("(", Precedence.Parens, Grouping.Parens);
|
|
continue;
|
|
case TokenType.CloseParen:
|
|
yield return new TokenTypeInfo(")", Precedence.Parens, Grouping.None, Grouping.Parens);
|
|
continue;
|
|
case TokenType.Not:
|
|
yield return new TokenTypeInfo("!", Precedence.Unary, Sides.Right, Associativity.Right);
|
|
continue;
|
|
case TokenType.OnesComplement:
|
|
yield return new TokenTypeInfo("~", Precedence.Unary, Sides.Right, Associativity.Right);
|
|
continue;
|
|
case TokenType.Negate:
|
|
yield return new TokenTypeInfo("-", Precedence.Unary, Sides.Right, Associativity.Right);
|
|
continue;
|
|
case TokenType.And:
|
|
yield return new TokenTypeInfo("&&", Precedence.And, Sides.Both, Sides.Both);
|
|
continue;
|
|
case TokenType.Or:
|
|
yield return new TokenTypeInfo("||", Precedence.Or, Sides.Both, Sides.Both);
|
|
continue;
|
|
case TokenType.Equals:
|
|
yield return new TokenTypeInfo("==", Precedence.Equality, Sides.Both, Sides.Both);
|
|
continue;
|
|
case TokenType.NotEquals:
|
|
yield return new TokenTypeInfo("!=", Precedence.Equality, Sides.Both, Sides.Both);
|
|
continue;
|
|
case TokenType.LessThan:
|
|
yield return new TokenTypeInfo("<", Precedence.Relation, Sides.Both, Sides.Both);
|
|
continue;
|
|
case TokenType.LessThanOrEqual:
|
|
yield return new TokenTypeInfo("<=", Precedence.Relation, Sides.Both, Sides.Both);
|
|
continue;
|
|
case TokenType.GreaterThan:
|
|
yield return new TokenTypeInfo(">", Precedence.Relation, Sides.Both, Sides.Both);
|
|
continue;
|
|
case TokenType.GreaterThanOrEqual:
|
|
yield return new TokenTypeInfo(">=", Precedence.Relation, Sides.Both, Sides.Both);
|
|
continue;
|
|
case TokenType.Add:
|
|
yield return new TokenTypeInfo("+", Precedence.Addition, Sides.Both, Sides.Both);
|
|
continue;
|
|
case TokenType.Subtract:
|
|
yield return new TokenTypeInfo("-", Precedence.Addition, Sides.Both, Sides.Both);
|
|
continue;
|
|
case TokenType.Multiply:
|
|
yield return new TokenTypeInfo("*", Precedence.Multiplication, Sides.Both, Sides.Both);
|
|
continue;
|
|
case TokenType.Divide:
|
|
yield return new TokenTypeInfo("/", Precedence.Multiplication, Sides.Both, Sides.Both);
|
|
continue;
|
|
case TokenType.Modulo:
|
|
yield return new TokenTypeInfo("%", Precedence.Multiplication, Sides.Both, Sides.Both);
|
|
continue;
|
|
}
|
|
|
|
throw new InvalidProgramException($"CreateTokenTypeInfoEnumeration is missing a TokenTypeInfo entry for TokenType.{Enum<TokenType>.GetValues()[i]}");
|
|
}
|
|
}
|
|
|
|
static readonly TokenTypeInfo[] TokenTypeInfos = CreateTokenTypeInfoEnumeration().ToArray();
|
|
|
|
static bool HasRightOperand(TokenType type)
|
|
{
|
|
return ((int)TokenTypeInfos[(int)type].OperandSides & (int)Sides.Right) != 0;
|
|
}
|
|
|
|
static bool IsLeftOperandOrNone(TokenType type)
|
|
{
|
|
return type == TokenType.Invalid || HasRightOperand(type);
|
|
}
|
|
|
|
static bool RequiresWhitespaceAfter(TokenType type)
|
|
{
|
|
return ((int)TokenTypeInfos[(int)type].WhitespaceSides & (int)Sides.Right) != 0;
|
|
}
|
|
|
|
static bool RequiresWhitespaceBefore(TokenType type)
|
|
{
|
|
return ((int)TokenTypeInfos[(int)type].WhitespaceSides & (int)Sides.Left) != 0;
|
|
}
|
|
|
|
static string GetTokenSymbol(TokenType type)
|
|
{
|
|
return TokenTypeInfos[(int)type].Symbol;
|
|
}
|
|
|
|
class Token
|
|
{
|
|
public readonly TokenType Type;
|
|
public readonly int Index;
|
|
|
|
public virtual string Symbol => TokenTypeInfos[(int)Type].Symbol;
|
|
|
|
public int Precedence => (int)TokenTypeInfos[(int)Type].Precedence;
|
|
public Sides OperandSides => TokenTypeInfos[(int)Type].OperandSides;
|
|
public Associativity Associativity => TokenTypeInfos[(int)Type].Associativity;
|
|
public bool LeftOperand => ((int)TokenTypeInfos[(int)Type].OperandSides & (int)Sides.Left) != 0;
|
|
public bool RightOperand => ((int)TokenTypeInfos[(int)Type].OperandSides & (int)Sides.Right) != 0;
|
|
|
|
public Grouping Opens => TokenTypeInfos[(int)Type].Opens;
|
|
public Grouping Closes => TokenTypeInfos[(int)Type].Closes;
|
|
|
|
public Token(TokenType type, int index)
|
|
{
|
|
Type = type;
|
|
Index = index;
|
|
}
|
|
|
|
static bool ScanIsNumber(string expression, int start, ref int i)
|
|
{
|
|
var cc = CharClassOf(expression[i]);
|
|
|
|
// Scan forwards until we find an non-digit character
|
|
if (cc == CharClass.Digit)
|
|
{
|
|
i++;
|
|
for (; i < expression.Length; i++)
|
|
{
|
|
cc = CharClassOf(expression[i]);
|
|
if (cc != CharClass.Digit)
|
|
{
|
|
if (cc != CharClass.Whitespace && cc != CharClass.Operator && cc != CharClass.Mixed)
|
|
throw new InvalidDataException($"Number {Exts.ParseInt32Invariant(expression[start..i])} and variable merged at index {start}");
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static TokenType VariableOrKeyword(string expression, int start, ref int i)
|
|
{
|
|
if (CharClassOf(expression[i - 1]) == CharClass.Mixed)
|
|
throw new InvalidDataException($"Invalid identifier end character at index {i - 1} for `{expression[start..i]}`");
|
|
|
|
return VariableOrKeyword(expression, start, i - start);
|
|
}
|
|
|
|
static TokenType VariableOrKeyword(string expression, int start, int length)
|
|
{
|
|
var i = start;
|
|
if (length == 4 && expression[i++] == 't' && expression[i++] == 'r' && expression[i++] == 'u'
|
|
&& expression[i] == 'e')
|
|
return TokenType.True;
|
|
|
|
if (length == 5 && expression[i++] == 'f' && expression[i++] == 'a' && expression[i++] == 'l'
|
|
&& expression[i++] == 's' && expression[i] == 'e')
|
|
return TokenType.False;
|
|
|
|
return TokenType.Variable;
|
|
}
|
|
|
|
public static TokenType GetNextType(string expression, ref int i, TokenType lastType = TokenType.Invalid)
|
|
{
|
|
var start = i;
|
|
|
|
switch (expression[i])
|
|
{
|
|
case '!':
|
|
i++;
|
|
if (i < expression.Length && expression[i] == '=')
|
|
{
|
|
i++;
|
|
return TokenType.NotEquals;
|
|
}
|
|
|
|
return TokenType.Not;
|
|
|
|
case '<':
|
|
i++;
|
|
if (i < expression.Length && expression[i] == '=')
|
|
{
|
|
i++;
|
|
return TokenType.LessThanOrEqual;
|
|
}
|
|
|
|
return TokenType.LessThan;
|
|
|
|
case '>':
|
|
i++;
|
|
if (i < expression.Length && expression[i] == '=')
|
|
{
|
|
i++;
|
|
return TokenType.GreaterThanOrEqual;
|
|
}
|
|
|
|
return TokenType.GreaterThan;
|
|
|
|
case '=':
|
|
i++;
|
|
if (i < expression.Length && expression[i] == '=')
|
|
{
|
|
i++;
|
|
return TokenType.Equals;
|
|
}
|
|
|
|
throw new InvalidDataException($"Unexpected character '=' at index {start} - should it be `==`?");
|
|
|
|
case '&':
|
|
i++;
|
|
if (i < expression.Length && expression[i] == '&')
|
|
{
|
|
i++;
|
|
return TokenType.And;
|
|
}
|
|
|
|
throw new InvalidDataException($"Unexpected character '&' at index {start} - should it be `&&`?");
|
|
|
|
case '|':
|
|
i++;
|
|
if (i < expression.Length && expression[i] == '|')
|
|
{
|
|
i++;
|
|
return TokenType.Or;
|
|
}
|
|
|
|
throw new InvalidDataException($"Unexpected character '|' at index {start} - should it be `||`?");
|
|
|
|
case '(':
|
|
i++;
|
|
return TokenType.OpenParen;
|
|
|
|
case ')':
|
|
i++;
|
|
return TokenType.CloseParen;
|
|
|
|
case '~':
|
|
i++;
|
|
return TokenType.OnesComplement;
|
|
case '+':
|
|
i++;
|
|
return TokenType.Add;
|
|
|
|
case '-':
|
|
if (++i < expression.Length && ScanIsNumber(expression, start, ref i))
|
|
return TokenType.Number;
|
|
|
|
i = start + 1;
|
|
if (IsLeftOperandOrNone(lastType))
|
|
return TokenType.Negate;
|
|
return TokenType.Subtract;
|
|
|
|
case '*':
|
|
i++;
|
|
return TokenType.Multiply;
|
|
|
|
case '/':
|
|
i++;
|
|
return TokenType.Divide;
|
|
|
|
case '%':
|
|
i++;
|
|
return TokenType.Modulo;
|
|
}
|
|
|
|
if (ScanIsNumber(expression, start, ref i))
|
|
return TokenType.Number;
|
|
|
|
var cc = CharClassOf(expression[start]);
|
|
|
|
if (cc != CharClass.Id)
|
|
throw new InvalidDataException($"Invalid character '{expression[i]}' at index {start}");
|
|
|
|
// Scan forwards until we find an invalid name character
|
|
for (i = start; i < expression.Length; i++)
|
|
{
|
|
cc = CharClassOf(expression[i]);
|
|
if (cc == CharClass.Whitespace || cc == CharClass.Operator)
|
|
return VariableOrKeyword(expression, start, ref i);
|
|
}
|
|
|
|
return VariableOrKeyword(expression, start, ref i);
|
|
}
|
|
|
|
public static Token GetNext(string expression, ref int i, TokenType lastType = TokenType.Invalid)
|
|
{
|
|
if (i == expression.Length)
|
|
return null;
|
|
|
|
// Check and eat whitespace
|
|
var whitespaceBefore = false;
|
|
if (CharClassOf(expression[i]) == CharClass.Whitespace)
|
|
{
|
|
whitespaceBefore = true;
|
|
while (CharClassOf(expression[i]) == CharClass.Whitespace)
|
|
{
|
|
if (++i == expression.Length)
|
|
return null;
|
|
}
|
|
}
|
|
else if (lastType == TokenType.Invalid)
|
|
whitespaceBefore = true;
|
|
else if (RequiresWhitespaceAfter(lastType))
|
|
throw new InvalidDataException($"Missing whitespace at index {i}, after `{GetTokenSymbol(lastType)}` operator.");
|
|
|
|
var start = i;
|
|
|
|
var type = GetNextType(expression, ref i, lastType);
|
|
if (!whitespaceBefore && RequiresWhitespaceBefore(type))
|
|
throw new InvalidDataException($"Missing whitespace at index {i}, before `{GetTokenSymbol(type)}` operator.");
|
|
|
|
switch (type)
|
|
{
|
|
case TokenType.Number:
|
|
return new NumberToken(start, expression[start..i]);
|
|
|
|
case TokenType.Variable:
|
|
return new VariableToken(start, expression[start..i]);
|
|
|
|
default:
|
|
return new Token(type, start);
|
|
}
|
|
}
|
|
}
|
|
|
|
sealed class VariableToken : Token
|
|
{
|
|
public readonly string Name;
|
|
|
|
public override string Symbol => Name;
|
|
|
|
public VariableToken(int index, string symbol)
|
|
: base(TokenType.Variable, index) { Name = symbol; }
|
|
}
|
|
|
|
sealed class NumberToken : Token
|
|
{
|
|
public readonly int Value;
|
|
readonly string symbol;
|
|
|
|
public override string Symbol => symbol;
|
|
|
|
public NumberToken(int index, string symbol)
|
|
: base(TokenType.Number, index)
|
|
{
|
|
Value = Exts.ParseInt32Invariant(symbol);
|
|
this.symbol = symbol;
|
|
}
|
|
}
|
|
|
|
protected VariableExpression(string expression)
|
|
{
|
|
Expression = expression;
|
|
}
|
|
|
|
Expression Build(ExpressionType resultType)
|
|
{
|
|
var tokens = new List<Token>();
|
|
var currentOpeners = new Stack<Token>();
|
|
Token lastToken = null;
|
|
for (var i = 0; ;)
|
|
{
|
|
var token = Token.GetNext(Expression, ref i, lastToken?.Type ?? TokenType.Invalid);
|
|
if (token == null)
|
|
{
|
|
// Sanity check parsed tree
|
|
if (lastToken == null)
|
|
throw new InvalidDataException("Empty expression");
|
|
|
|
// Expressions can't end with a binary or unary prefix operation
|
|
if (lastToken.RightOperand)
|
|
throw new InvalidDataException($"Missing value or sub-expression at end for `{lastToken.Symbol}` operator");
|
|
|
|
break;
|
|
}
|
|
|
|
if (token.Closes != Grouping.None)
|
|
{
|
|
if (currentOpeners.Count == 0)
|
|
throw new InvalidDataException($"Unmatched closing parenthesis at index {token.Index}");
|
|
|
|
currentOpeners.Pop();
|
|
}
|
|
|
|
if (token.Opens != Grouping.None)
|
|
currentOpeners.Push(token);
|
|
|
|
if (lastToken == null)
|
|
{
|
|
// Expressions can't start with a binary or unary postfix operation or closer
|
|
if (token.LeftOperand)
|
|
throw new InvalidDataException($"Missing value or sub-expression at beginning for `{token.Symbol}` operator");
|
|
}
|
|
else
|
|
{
|
|
// Disallow empty parentheses
|
|
if (lastToken.Opens != Grouping.None && token.Closes != Grouping.None)
|
|
throw new InvalidDataException($"Empty parenthesis at index {lastToken.Index}");
|
|
|
|
// Exactly one of two consecutive tokens must take the other's sub-expression evaluation as an operand
|
|
if (lastToken.RightOperand == token.LeftOperand)
|
|
{
|
|
if (lastToken.RightOperand)
|
|
throw new InvalidDataException(
|
|
$"Missing value or sub-expression or there is an extra operator `{lastToken.Symbol}` at index {lastToken.Index} or `{token.Symbol}` at index {token.Index}");
|
|
throw new InvalidDataException($"Missing binary operation before `{token.Symbol}` at index {token.Index}");
|
|
}
|
|
}
|
|
|
|
if (token.Type == TokenType.Variable)
|
|
variables.Add(token.Symbol);
|
|
|
|
tokens.Add(token);
|
|
lastToken = token;
|
|
}
|
|
|
|
if (currentOpeners.Count > 0)
|
|
throw new InvalidDataException($"Unclosed opening parenthesis at index {currentOpeners.Peek().Index}");
|
|
|
|
return new Compiler().Build(ToPostfix(tokens).ToArray(), resultType);
|
|
}
|
|
|
|
protected Func<IReadOnlyDictionary<string, int>, T> Compile<T>()
|
|
{
|
|
ExpressionType resultType;
|
|
if (typeof(T) == typeof(int))
|
|
resultType = ExpressionType.Int;
|
|
else if (typeof(T) == typeof(bool))
|
|
resultType = ExpressionType.Bool;
|
|
else
|
|
throw new InvalidCastException("Variable expressions can only be int or bool.");
|
|
|
|
return Expressions.Expression.Lambda<Func<IReadOnlyDictionary<string, int>, T>>(Build(resultType), SymbolsParam).Compile();
|
|
}
|
|
|
|
static int ParseSymbol(string symbol, IReadOnlyDictionary<string, int> symbols)
|
|
{
|
|
symbols.TryGetValue(symbol, out var value);
|
|
return value;
|
|
}
|
|
|
|
static IEnumerable<Token> ToPostfix(IEnumerable<Token> tokens)
|
|
{
|
|
var s = new Stack<Token>();
|
|
foreach (var t in tokens)
|
|
{
|
|
if (t.Opens != Grouping.None)
|
|
s.Push(t);
|
|
else if (t.Closes != Grouping.None)
|
|
{
|
|
Token temp;
|
|
while ((temp = s.Pop()).Opens == Grouping.None)
|
|
yield return temp;
|
|
}
|
|
else if (t.OperandSides == Sides.None)
|
|
yield return t;
|
|
else
|
|
{
|
|
while (s.Count > 0 && ((t.Associativity == Associativity.Right && t.Precedence < s.Peek().Precedence)
|
|
|| (t.Associativity == Associativity.Left && t.Precedence <= s.Peek().Precedence)))
|
|
yield return s.Pop();
|
|
|
|
s.Push(t);
|
|
}
|
|
}
|
|
|
|
while (s.Count > 0)
|
|
yield return s.Pop();
|
|
}
|
|
|
|
enum ExpressionType { Int, Bool }
|
|
|
|
static readonly ParameterExpression SymbolsParam =
|
|
Expressions.Expression.Parameter(typeof(IReadOnlyDictionary<string, int>), "symbols");
|
|
static readonly ConstantExpression Zero = Expressions.Expression.Constant(0);
|
|
static readonly ConstantExpression One = Expressions.Expression.Constant(1);
|
|
static readonly ConstantExpression False = Expressions.Expression.Constant(false);
|
|
static readonly ConstantExpression True = Expressions.Expression.Constant(true);
|
|
|
|
static Expression AsBool(Expression expression)
|
|
{
|
|
return Expressions.Expression.NotEqual(expression, Zero);
|
|
}
|
|
|
|
static Expression IfThenElse(Expression test, Expression ifTrue, Expression ifFalse)
|
|
{
|
|
return Expressions.Expression.Condition(test, ifTrue, ifFalse);
|
|
}
|
|
|
|
sealed class AstStack
|
|
{
|
|
readonly List<Expression> expressions = new();
|
|
readonly List<ExpressionType> types = new();
|
|
|
|
public ExpressionType PeekType() { return types[^1]; }
|
|
|
|
public Expression Peek(ExpressionType toType)
|
|
{
|
|
var fromType = types[^1];
|
|
var expression = expressions[^1];
|
|
if (toType == fromType)
|
|
return expression;
|
|
|
|
switch (toType)
|
|
{
|
|
case ExpressionType.Bool:
|
|
return IfThenElse(AsBool(expression), True, False);
|
|
case ExpressionType.Int:
|
|
return IfThenElse(expression, One, Zero);
|
|
}
|
|
|
|
throw new InvalidProgramException(
|
|
"Unable to convert " +
|
|
$"ExpressionType.{Enum<ExpressionType>.GetValues()[(int)fromType]} to " +
|
|
$"ExpressionType.{Enum<ExpressionType>.GetValues()[(int)toType]}");
|
|
}
|
|
|
|
public Expression Pop(ExpressionType type)
|
|
{
|
|
var expression = Peek(type);
|
|
expressions.RemoveAt(expressions.Count - 1);
|
|
types.RemoveAt(types.Count - 1);
|
|
return expression;
|
|
}
|
|
|
|
public void Push(Expression expression, ExpressionType type)
|
|
{
|
|
expressions.Add(expression);
|
|
if (type == ExpressionType.Int && expression.Type != typeof(int))
|
|
throw new InvalidOperationException($"Expected System.Int type instead of {expression.Type} for {expression}");
|
|
|
|
if (type == ExpressionType.Bool && expression.Type != typeof(bool))
|
|
throw new InvalidOperationException($"Expected System.Boolean type instead of {expression.Type} for {expression}");
|
|
types.Add(type);
|
|
}
|
|
|
|
public void Push(Expression expression)
|
|
{
|
|
expressions.Add(expression);
|
|
if (expression.Type == typeof(int))
|
|
types.Add(ExpressionType.Int);
|
|
else if (expression.Type == typeof(bool))
|
|
types.Add(ExpressionType.Bool);
|
|
else
|
|
throw new InvalidOperationException($"Unhandled result type {expression.Type} for {expression}");
|
|
}
|
|
}
|
|
|
|
sealed class Compiler
|
|
{
|
|
readonly AstStack ast = new();
|
|
|
|
public Expression Build(Token[] postfix, ExpressionType resultType)
|
|
{
|
|
foreach (var t in postfix)
|
|
{
|
|
switch (t.Type)
|
|
{
|
|
case TokenType.And:
|
|
{
|
|
var y = ast.Pop(ExpressionType.Bool);
|
|
var x = ast.Pop(ExpressionType.Bool);
|
|
ast.Push(Expressions.Expression.And(x, y));
|
|
continue;
|
|
}
|
|
|
|
case TokenType.Or:
|
|
{
|
|
var y = ast.Pop(ExpressionType.Bool);
|
|
var x = ast.Pop(ExpressionType.Bool);
|
|
ast.Push(Expressions.Expression.Or(x, y));
|
|
continue;
|
|
}
|
|
|
|
case TokenType.NotEquals:
|
|
{
|
|
var y = ast.Pop(ExpressionType.Int);
|
|
var x = ast.Pop(ExpressionType.Int);
|
|
ast.Push(Expressions.Expression.NotEqual(x, y));
|
|
continue;
|
|
}
|
|
|
|
case TokenType.Equals:
|
|
{
|
|
var y = ast.Pop(ExpressionType.Int);
|
|
var x = ast.Pop(ExpressionType.Int);
|
|
ast.Push(Expressions.Expression.Equal(x, y));
|
|
continue;
|
|
}
|
|
|
|
case TokenType.Not:
|
|
{
|
|
ast.Push(Expressions.Expression.Not(ast.Pop(ExpressionType.Bool)));
|
|
continue;
|
|
}
|
|
|
|
case TokenType.Negate:
|
|
{
|
|
ast.Push(Expressions.Expression.Negate(ast.Pop(ExpressionType.Int)));
|
|
continue;
|
|
}
|
|
|
|
case TokenType.OnesComplement:
|
|
{
|
|
ast.Push(Expressions.Expression.OnesComplement(ast.Pop(ExpressionType.Int)));
|
|
continue;
|
|
}
|
|
|
|
case TokenType.LessThan:
|
|
{
|
|
var y = ast.Pop(ExpressionType.Int);
|
|
var x = ast.Pop(ExpressionType.Int);
|
|
ast.Push(Expressions.Expression.LessThan(x, y));
|
|
continue;
|
|
}
|
|
|
|
case TokenType.LessThanOrEqual:
|
|
{
|
|
var y = ast.Pop(ExpressionType.Int);
|
|
var x = ast.Pop(ExpressionType.Int);
|
|
ast.Push(Expressions.Expression.LessThanOrEqual(x, y));
|
|
continue;
|
|
}
|
|
|
|
case TokenType.GreaterThan:
|
|
{
|
|
var y = ast.Pop(ExpressionType.Int);
|
|
var x = ast.Pop(ExpressionType.Int);
|
|
ast.Push(Expressions.Expression.GreaterThan(x, y));
|
|
continue;
|
|
}
|
|
|
|
case TokenType.GreaterThanOrEqual:
|
|
{
|
|
var y = ast.Pop(ExpressionType.Int);
|
|
var x = ast.Pop(ExpressionType.Int);
|
|
ast.Push(Expressions.Expression.GreaterThanOrEqual(x, y));
|
|
continue;
|
|
}
|
|
|
|
case TokenType.Add:
|
|
{
|
|
var y = ast.Pop(ExpressionType.Int);
|
|
var x = ast.Pop(ExpressionType.Int);
|
|
ast.Push(Expressions.Expression.Add(x, y));
|
|
continue;
|
|
}
|
|
|
|
case TokenType.Subtract:
|
|
{
|
|
var y = ast.Pop(ExpressionType.Int);
|
|
var x = ast.Pop(ExpressionType.Int);
|
|
ast.Push(Expressions.Expression.Subtract(x, y));
|
|
continue;
|
|
}
|
|
|
|
case TokenType.Multiply:
|
|
{
|
|
var y = ast.Pop(ExpressionType.Int);
|
|
var x = ast.Pop(ExpressionType.Int);
|
|
ast.Push(Expressions.Expression.Multiply(x, y));
|
|
continue;
|
|
}
|
|
|
|
case TokenType.Divide:
|
|
{
|
|
var y = ast.Pop(ExpressionType.Int);
|
|
var x = ast.Pop(ExpressionType.Int);
|
|
var isNotZero = Expressions.Expression.NotEqual(y, Zero);
|
|
var divide = Expressions.Expression.Divide(x, y);
|
|
ast.Push(IfThenElse(isNotZero, divide, Zero));
|
|
continue;
|
|
}
|
|
|
|
case TokenType.Modulo:
|
|
{
|
|
var y = ast.Pop(ExpressionType.Int);
|
|
var x = ast.Pop(ExpressionType.Int);
|
|
var isNotZero = Expressions.Expression.NotEqual(y, Zero);
|
|
var modulo = Expressions.Expression.Modulo(x, y);
|
|
ast.Push(IfThenElse(isNotZero, modulo, Zero));
|
|
continue;
|
|
}
|
|
|
|
case TokenType.False:
|
|
{
|
|
ast.Push(False);
|
|
continue;
|
|
}
|
|
|
|
case TokenType.True:
|
|
{
|
|
ast.Push(True);
|
|
continue;
|
|
}
|
|
|
|
case TokenType.Number:
|
|
{
|
|
ast.Push(Expressions.Expression.Constant(((NumberToken)t).Value));
|
|
continue;
|
|
}
|
|
|
|
case TokenType.Variable:
|
|
{
|
|
var symbol = Expressions.Expression.Constant(((VariableToken)t).Symbol);
|
|
Func<string, IReadOnlyDictionary<string, int>, int> parseSymbol = ParseSymbol;
|
|
ast.Push(Expressions.Expression.Call(parseSymbol.Method, symbol, SymbolsParam));
|
|
continue;
|
|
}
|
|
|
|
default:
|
|
throw new InvalidProgramException($"ConditionExpression.Compiler.Compile() is missing an expression builder for TokenType.{Enum<TokenType>.GetValues()[(int)t.Type]}");
|
|
}
|
|
}
|
|
|
|
return ast.Pop(resultType);
|
|
}
|
|
}
|
|
}
|
|
|
|
public class BooleanExpression : VariableExpression
|
|
{
|
|
readonly Func<IReadOnlyDictionary<string, int>, bool> asFunction;
|
|
|
|
public BooleanExpression(string expression)
|
|
: base(expression)
|
|
{
|
|
asFunction = Compile<bool>();
|
|
}
|
|
|
|
public bool Evaluate(IReadOnlyDictionary<string, int> symbols)
|
|
{
|
|
return asFunction(symbols);
|
|
}
|
|
}
|
|
|
|
public class IntegerExpression : VariableExpression
|
|
{
|
|
readonly Func<IReadOnlyDictionary<string, int>, int> asFunction;
|
|
|
|
public IntegerExpression(string expression)
|
|
: base(expression)
|
|
{
|
|
asFunction = Compile<int>();
|
|
}
|
|
|
|
public int Evaluate(IReadOnlyDictionary<string, int> symbols)
|
|
{
|
|
return asFunction(symbols);
|
|
}
|
|
}
|
|
}
|