Expression Script(灵感来自 MIT Scratch)






4.97/5 (14投票s)
Expression Script 是一个表达式求值器,它在运行时编译代码并给出结果。
github.com/Mohamed-Ahmed-Abdullah/ProgramDesigner
引言
这基本上是一种新的可视化编程语言,可以让你在运行时编写和运行代码。我一直在思考为什么我们在商业世界中没有类似 Scratch 的东西,任何人都可以使用,简单直观,易于学习。当然,到目前为止我所做的还远不及 MIT Scratch,但我会弄清楚如何继续以及如何做到这一点。
问题
我一直在思考为什么 MIT Scratch 直观易用且易于学习,以及为什么我们在商业世界中没有类似的东西。你能想象用户更改他们自己的业务规则,或者他们自己解决系统错误,或者更改工作流程,或者从某个方程中获得不同的结果吗?从这里开始,我开始研究如何自己制作 Scratch,但将其目标定位于商业世界。
这篇 CodeProject 文章 讨论的主题与我在这里讨论的主题类似,但它没有 UI 部分,并且具有不同的语法、新概念、用例和愿景。
用例
我们可以用它在两个地方,我需要你把这两个地方作为案例研究。
第一:想象你有一个 HR 系统,你有基本工资和未知数量的津贴。每个客户都有自己的计算方式,但无论他的方程看起来如何,你都可以将参数保存在你的数据库中,用于获取结果。你可以用这个表达式脚本编写每个方程,以返回一个用于基本工资或津贴的单个值。
第二:想象你有一个公司的加班工作流程,这个工作流程只有 3 种状态,但从一个公司到另一个公司,过渡逻辑是不同的,逻辑是权限检查或数据验证。你可以用这个表达式脚本编写传输逻辑。脚本将决定是否移至下一步。如果不移,将抛出异常,应用程序应优雅地处理它。
何时可以使用它
我们现在拥有的东西无法使用,原因很多,但道路是清晰的,我们将如何调整它以达到我们想要的结果。
我们无法使用它的原因是
- 我们使用该工具编写普通代码,而不是作为交互式和清晰含义的方式,换句话说,编写代码比拖放它更简单。
- 语法尚未完成,它包含表达式、
if
语句和变量以及一种数据类型,但你不能使用“while
语句”等。
术语
- 代码块:简单来说,就是你在方法中可以写的任何内容,如
try catch
、for
、if
、定义变量等…… - 表达式:它是 C# 类的表达式树,表示树状数据结构中的代码,其中每个节点都是一个表达式,例如,方法调用或二元运算符,如
x < y
。 - 语法:定义语言语法的规则集。
工作原理
- WPF UI:一个画布和一组矩形,你可以用它们编写代码,然后将其转换为简单的
string
代码。 - Irony:Irony 是一个在 .NET 平台上实现语言的开发工具包。
- 语法
- Irony Tree:Irony 提供你的语法的树。
- 表达式树:我们使用 Irony 树并将其转换为表达式树。
这里的想法很简单。首先,我们需要为 Irony 创建一个语法,用于生成代码树,然后使用 UI 获取代码,将其传递给 Irony,结果将是代码树,然后将此树转换为 C# 表达式树并从中获取最终结果。
Using the Code
程序设计器提供了拖放功能,输出的代码字符串将传递给表达式编译器,如果有任何语法错误,Irony 将指出并返回,如果没有,我们将继续生成代码树。
你会发现 ExpressionCompiler>Nodes
一组类。每个类代表语法中的一个规则,它负责从该规则生成 Expression Tree 的一部分。例如,VariableDeclarationNode.cs 类,它的工作是创建一个 Expression.Parameter
如果找到变量声明,它将被转换为 Expression Parameter。
转换完成后,将返回一个单一值。
UI
这是一个画布,左边是工具包,右边是工作区,你可以将框从工具包拖到工作区(它在同一个画布上)。完成应用程序后,读取所有这些框并将它们转换为代码。
拖放
有一个名为 DragGrip: Border
的控件,它继承自 Border
并实现新功能。
IsDragable
IsSelected
IsToolBarItem
ContextMenue
Clone()
DragGrip
代表你可以在画布中拖动的任何东西,换句话说,如果它不是 DragGrip
,你就不能拖动它。
通过将此控件的 RenderTransform
更改为 TranslateTransform
,然后根据鼠标移动更改 X
和 Y
,是的,就是这么简单。
吸附功能
吸附是指当一个可拖动控件靠近另一个控件时,可拖动控件会吸附到该控件上,吸附区域由 Point _snapOffset = new Point {X = 5, Y = 15};
定义,这意味着如果你从左边或右边靠近,两个控件(静止的那个和可拖动的那个)将不会吸附在一起,直到你达到 5 个点的距离,但如果你从顶部或底部靠近,吸附点是 15。
选择和取消选择
有两种方法可以选择或取消选择:
- 点击框
- 在
Canvas
的空白区域拖动,并确保控件在选择区域内。
如果你点击了框 (DragGrip
),控件将处理它(将 IsSelected
更改为 true
),这将改变 UI 并设置选择边框,但如果你拖动以选择多个项目……
……你必须计算哪个控件在选择区域内,哪个不在。
//if(dragging in empty space in the canvas) then {draw rectangle}
if (isCanvasDragging && e.OriginalSource is Canvas)
{
//get drag dimentions
var currentPosition = e.GetPosition(MainCanvas);
var x1 = currentPosition.X;
var y1 = currentPosition.Y;
var x2 = clickPosition.X.ZeroBased();
var y2 = clickPosition.Y.ZeroBased();
Extentions.Order(ref x1, ref x2);
Extentions.Order(ref y1, ref y2);
//if(drag inside empty space in the canvas exceeded the limit that defined by _canvasDragOffset)
if (x2 - x1 > _canvasDragOffset.X && y2 - y1 > _canvasDragOffset.Y)
{
_rectangleDrawn = true;
//draw rectangle (change his shape depending on the mouse movment)
SelectionRectangle.Visibility = Visibility.Visible;
Canvas.SetLeft(SelectionRectangle, (x1 < x2) ? x1 : x2);
Canvas.SetTop(SelectionRectangle, (y1 < y2) ? y1 : y2);
SelectionRectangle.Width = Math.Abs(x1 - x2);
SelectionRectangle.Height = Math.Abs(y1 - y2);
//foreach(draggable child in the canvas)
foreach (var child in MainCanvas.Children.OfType<DragGrip>())
{
//get his dimensions
var translate = ((TranslateTransform) child.RenderTransform);
var cx1 = translate.X;
var cy1 = translate.Y;
var cx2 = cx1 + child.ActualWidth;
var cy2 = cy1 + child.ActualHeight;
//if(control inside the selection area)
if (x1 < cx1 && x2 > cx2 && y1 < cy1 && y2 > cy2)
{
//select it
child.IsSelected = true;
}
}
}
}
工具箱
如果你点击了工具箱按钮,与该按钮相关的项目将从下方的工具箱中消失,这是直接通过以下方式完成的:
Control.MouseDown += (a, b) =>
{
if (b.LeftButton != MouseButtonState.Pressed)
return;
_isControlsVisible = !_isControlsVisible;
//get the X dimension of the line that separate the toolbox from working area
var x = Canvas.GetLeft(VerticalBarrier);
MainCanvas.Children.OfType<DragGrip>()
.Where( w =>
((TranslateTransform) w.RenderTransform).X <= x &&
((Border) w.Child).Background == Brushes.Orange)
.ToList()
.ForEach(f =>
{
f.Visibility = _isControlsVisible ? Visibility.Visible : Visibility.Collapsed;
});
NotifyPropertyChanged("");
};
画布内有一条垂直线,将工具箱与工作区分隔开。
重命名和写入值
这很简单,只是将 DragGrip
控件中保存的 textblock
更改为 TextBox
中的新文本。
添加新的可拖动项
我们现在有可拖动的元素类型(Control
、Var
、Token
),但假设你想添加保留字,如 public
、this
……你应该怎么做?
- 在
toolBox Button
s 中添加按钮并命名为“Reserved Words
”。 - 以及
toolbox
中的保留字,基本上你应该创建DragGrip
控件并将它们放在分隔线后面,并将IsDragable
指定为false
,将IsToolBarItem
指定为true
。 - 给它们一个独特的颜色。
从 UI 元素中提取代码
每个框(可拖动元素)内部都有文本,如果你只取该文本,你就会得到代码。
string code = "";
dragGrips.Select(s => ((TextBlock)((Border)s.Child).Child).Text).ToList().ForEach(f =>
{
code += f;
});
try
{
//pass it to Irony and getting the result
Result.Text = Compiler.Compile(code).ToString();
}
catch (Exception ex)
{
Result.Text = ex.Message;
}
Irony
Irony
是一个库,它极大地简化了编写编译器,因为它将检测语法错误和编译过程的许多方面。你只需要交给它你的语法、你的转换类,以及最后你的代码。转换类将在本文稍后讨论。
使用 Irony
var grammar = new MyGrammar();
var compiler = new LanguageCompiler(grammar);
var program = (IExpressionGenerator)compiler.Parse(sourceCode);
var expression = program.GenerateExpression(null);
世界上任何编译器都做一件事,就是将你的语法(代码)从一种形状转换为另一种形状,这种情况也不例外,我想将 C# 语法转换为表达式树,这将在本文稍后介绍。
错误处理
Irony 会开箱即用地为你提供此功能。你只需要处理这些错误并在 UI 中显示它们。
var grammar = new MyGrammar();
var compiler = new LanguageCompiler(grammar);
var program = (IExpressionGenerator)compiler.Parse(sourceCode);
if (program == null || compiler.Context.Errors.Count > 0)
{
// Didn't compile. Generate an error message.
var errors = "";
foreach (var error in compiler.Context.Errors)
{
var location = string.Empty;
if (error.Location.Line > 0 && error.Location.Column > 0)
{
location = "Line " + (error.Location.Line + 1) + ", column " + (error.Location.Column + 1);
}
errors = location + ": " + error.Message + ":" + Environment.NewLine;
errors += sourceCode.Split('\n')[error.Location.Line];
}
throw new CompilationException(errors);
}
var expression = program.GenerateExpression(null);
获取结果
我们预期一种结果——至少目前是一个十进制数,但要实现工作流程案例(参见用例部分),我们预期不同类型的结果——一个异常。
return ((Expression<Func<decimal?>>)expression).Compile()();
代码树转换类
通过编写你的语法,你就是在告诉 Irony 这是我的语言的格式(语法),任何不同的东西,它都不会接受。
首先,你需要定义节点。
终结符:这些是叶子,树中的最终项,如数字、保留字、符号 + -……
非终结符:这是变量,每个非终结符都有一个规则——你用它来替换它,就像如果你有 x
是非终结符,并且你有规则 x= 3
和 x = 4
,你可以根据需要将 x
设置为 3
或 4
,或者更复杂的例子,如果你有 s = x +y
,并且你有 x = 0 |1| 2 | 3 |...|9
和 y = ;
,那么你的语句可能是 1;
或者可能是 4;
,你明白了。
var program = new NonTerminal("program", typeof(ProgramNode));
var statementList = new NonTerminal("statementList", typeof(StatementNode));
var statement = new NonTerminal("statement", typeof(SkipNode));
var expression = new NonTerminal("expression", typeof(ExpressionNode));
var binaryOperator = new NonTerminal("binaryOperator", typeof(SkipNode));
var variableDeclaration = new NonTerminal("variableDeclaration", typeof(VariableDeclarationNode));
var variableAssignment = new NonTerminal("variableAssignment", typeof(VariableAssignmentNode));
var ifStatement = new NonTerminal("ifStatement", typeof(IfStatementNode));
var elseStatement = new NonTerminal("elseStatement", typeof(ElseStatementNode));
// define all the terminals
var variable = new IdentifierTerminal("variable");
variable.AddKeywords("set", "var" , "to",
"if", "freight", "cost", "is", "loop", "through", "order");
var number = new NumberLiteral("number");
var stringLiteral = new StringLiteral("string", "\"", ScanFlags.None);
RegisterPunctuation(";", "[", "]", "(", ")");
规则
Root = program;
binaryOperator.Rule = Symbol("+") | "-" | "*" |
"/" | "<" | "==" | "!=" | ">" | "<=" | ">=" | "is";
program.Rule = statementList;
statementList.Rule = MakeStarRule(statementList, null, statement);
statement.Rule = variableDeclaration + ";" |
variableAssignment + ";" | expression + ";" | ifStatement;
variableAssignment.Rule = variable + "=" + expression;
variableDeclaration.Rule = Symbol("var") + variable;
ifStatement.Rule = "if" + Symbol("(") + expression + Symbol(")")
+ Symbol("{") + statementList + Symbol("}")
+ elseStatement;
elseStatement.Rule = Empty | "else" + Symbol("{") + statementList + Symbol("}");
expression.Rule = number | variable | stringLiteral
| expression + binaryOperator + expression
| "(" + expression + ")";
表达式树
如我们之前提到的,我们正在尝试将代码转换为表达式,但什么是表达式。
你写的任何 Linq 表达式,例如 numbersList.Where(w => w > 10);
- 这个语句将被转换为表达式,如果你尝试反编译一个包含 Linq 语句的代码,你将找不到真正的 where(w=>w>10)
,但你会找到映射到该 Linq 语句的表达式。
Microsoft 对表达式的定义:表达式树以树状数据结构表示代码,其中每个节点都是一个表达式,例如,方法调用或二元运算符,如 x < y。你可以编译和运行由表达式树表示的代码。这使得可执行代码的动态修改、在各种数据库中执行 LINQ 查询以及创建动态查询成为可能。
因此,为了生成表达式树,我们必须告诉 Irony 如何做到这一点,因为此功能不是开箱即用的,我们必须为此编写一些代码。
让我们看一个例子。
public class VariableAssignmentNode : AstNode, IExpressionGenerator
{
public VariableAssignmentNode(AstNodeArgs args): base(args)
{}
public Expression GenerateExpression(object tree)
{
var twoExpressionsDto = (TwoExpressionsDto)tree;
return Expression.Assign(twoExpressionsDto.Expression2, twoExpressionsDto.Expression1);
}
}
当你有一个像 x =3
这样的节点时,这是一个赋值节点,你想将其转换为赋值表达式——最好是创建一个类并在其中创建功能,因此,基本上,你将有一个类映射到你的语法中的每个节点,但这还不够。
我们有语法的根节点。
var program = new NonTerminal("program", typeof(ProgramNode));
这被映射到 ProgramNode
类,所以这是在将代码转换为表达式的过程中调用的第一个类,Irony 会为你调用它。从这里开始,你就可以控制了,你最了解语法,所以你需要让 ProgramNode
类递归地调用任何你创建的其他类,这取决于节点。
ProgramNode
>CompileStatementList()
首先,我们遍历 Irony 创建的顶级节点。
foreach (var statement in childNode)
然后我们确定节点的类型。
if (statement.ChildNodes[0] is VariableDeclarationNode) {}
else if (statement.ChildNodes[0] is VariableAssignmentNode) {}
else if (statement.ChildNodes[0] is ExpressionNode) {}
else if (statement.ChildNodes[0] is IfStatementNode) {}
Irony 会为你生成你的 Node
类的实例,但你必须调用 GenerateExpression()
来从该节点获取 Expression 并使用它。
最后,别忘了使用 lambda,正如我们之前所说,我们期望这里只有一种类型的结果,所以:
var lambda = Expression.Lambda<Func<decimal?>>(Expression.Block(
VariableList.Select(q => (ParameterExpression) q.FirstExpression).ToArray(),
lastExpressionNode));
获取结果
var grammar = new MyGrammar();
var compiler = new LanguageCompiler(grammar);
var program = (IExpressionGenerator)compiler.Parse(sourceCode);
var expression = program.GenerateExpression(null);
var result = ((Expression<Func<decimal?>>)expression).Compile()();
通过调用 program.GenerateExpression
,Irony 将调用根节点所属的 GenerateExpression()
,在这种情况下,它是 ProgramNode
方法,它返回我们宝贵的值。
未来的增强
- 以人为本而非以代码为中心:现在你只是在编写代码——你没有使用一种视觉化的、有趣易用的方式来生成你的业务规则或方程。
- 可拖放:你应该能够看到完整的骨架(例如
is
语句),并在条件部分、“if
”部分和“else
”部分拖放一些代码。 - 更好的语法
参考文献
- irony.codeplex.com
- blogs.msdn.com/b/kirillosenkov/archive/2009/10/31/irony.aspx
- msdn.microsoft.com/en-us/library/bb397951.aspx
- msdn.microsoft.com/en-us/library/bb397951.aspx
- www.codeproject.com/Articles/29058/Writing-your-first-Domain-Specific-Language-Part
- www.codeproject.com/Articles/272494/Implementing-Programming-Languages-using-Csharp