65.9K
CodeProject 正在变化。 阅读更多。
Home

Expression Script(灵感来自 MIT Scratch)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (14投票s)

Nov 9, 2014

CPOL

10分钟阅读

viewsIcon

21676

Expression Script 是一个表达式求值器,它在运行时编译代码并给出结果。

github.com/Mohamed-Ahmed-Abdullah/ProgramDesigner

引言

这基本上是一种新的可视化编程语言,可以让你在运行时编写和运行代码。我一直在思考为什么我们在商业世界中没有类似 Scratch 的东西,任何人都可以使用,简单直观,易于学习。当然,到目前为止我所做的还远不及 MIT Scratch,但我会弄清楚如何继续以及如何做到这一点。

问题

我一直在思考为什么 MIT Scratch 直观易用且易于学习,以及为什么我们在商业世界中没有类似的东西。你能想象用户更改他们自己的业务规则,或者他们自己解决系统错误,或者更改工作流程,或者从某个方程中获得不同的结果吗?从这里开始,我开始研究如何自己制作 Scratch,但将其目标定位于商业世界。

这篇 CodeProject 文章 讨论的主题与我在这里讨论的主题类似,但它没有 UI 部分,并且具有不同的语法、新概念、用例和愿景。

用例

我们可以用它在两个地方,我需要你把这两个地方作为案例研究。

第一:想象你有一个 HR 系统,你有基本工资和未知数量的津贴。每个客户都有自己的计算方式,但无论他的方程看起来如何,你都可以将参数保存在你的数据库中,用于获取结果。你可以用这个表达式脚本编写每个方程,以返回一个用于基本工资或津贴的单个值。

第二:想象你有一个公司的加班工作流程,这个工作流程只有 3 种状态,但从一个公司到另一个公司,过渡逻辑是不同的,逻辑是权限检查或数据验证。你可以用这个表达式脚本编写传输逻辑。脚本将决定是否移至下一步。如果不移,将抛出异常,应用程序应优雅地处理它。

何时可以使用它

我们现在拥有的东西无法使用,原因很多,但道路是清晰的,我们将如何调整它以达到我们想要的结果。

我们无法使用它的原因是

  • 我们使用该工具编写普通代码,而不是作为交互式和清晰含义的方式,换句话说,编写代码比拖放它更简单。
  • 语法尚未完成,它包含表达式、if 语句和变量以及一种数据类型,但你不能使用“while 语句”等。

术语

  • 代码块:简单来说,就是你在方法中可以写的任何内容,如 try catchforif、定义变量等……
  • 表达式:它是 C# 类的表达式树,表示树状数据结构中的代码,其中每个节点都是一个表达式,例如,方法调用或二元运算符,如 x < y
  • 语法:定义语言语法的规则集。

工作原理

  1. WPF UI:一个画布和一组矩形,你可以用它们编写代码,然后将其转换为简单的 string 代码。
  2. Irony:Irony 是一个在 .NET 平台上实现语言的开发工具包。
  3. 语法
  4. Irony Tree:Irony 提供你的语法的树。
  5. 表达式树:我们使用 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,然后根据鼠标移动更改 XY,是的,就是这么简单。

吸附功能

吸附是指当一个可拖动控件靠近另一个控件时,可拖动控件会吸附到该控件上,吸附区域由 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 中的新文本。

添加新的可拖动项

我们现在有可拖动的元素类型(ControlVarToken),但假设你想添加保留字,如 publicthis……你应该怎么做?

  1. toolBox Buttons 中添加按钮并命名为“Reserved Words”。
  2. 以及 toolbox 中的保留字,基本上你应该创建 DragGrip 控件并将它们放在分隔线后面,并将 IsDragable 指定为 false,将 IsToolBarItem 指定为 true
  3. 给它们一个独特的颜色。

从 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= 3x = 4,你可以根据需要将 x 设置为 34,或者更复杂的例子,如果你有 s = x +y,并且你有 x = 0 |1| 2 | 3 |...|9y = ;,那么你的语句可能是 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”部分拖放一些代码。
  • 更好的语法

参考文献

© . All rights reserved.