开发者

Is it possible to interpret a C# expression tree to emit JavaScript?

开发者 https://www.devze.com 2023-03-06 01:24 出处:网络
For example, if you have an expression like this: Expression<Func<int, int>> fn = x => x * x;

For example, if you have an expression like this:

Expression<Func<int, int>> fn = x => x * x;

Is there anything that will traverse the e开发者_高级运维xpression tree and generate this?

"function(x) { return x * x; }"


It's probably not easy, but yes, it's absolutely feasible. ORMs like Entity Framework or Linq to SQL do it to translate Linq queries into SQL, but you can actually generate anything you want from the expression tree...

You should implement an ExpressionVisitor to analyse and transform the expression.


EDIT: here's a very basic implementation that works for your example:

Expression<Func<int, int>> fn = x => x * x;
var visitor = new JsExpressionVisitor();
visitor.Visit(fn);
Console.WriteLine(visitor.JavaScriptCode);

...

class JsExpressionVisitor : ExpressionVisitor
{
    private readonly StringBuilder _builder;

    public JsExpressionVisitor()
    {
        _builder = new StringBuilder();
    }

    public string JavaScriptCode
    {
        get { return _builder.ToString(); }
    }

    public override Expression Visit(Expression node)
    {
        _builder.Clear();
        return base.Visit(node);
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        _builder.Append(node.Name);
        base.VisitParameter(node);
        return node;
    }

    protected override Expression VisitBinary(BinaryExpression node)
    {
        base.Visit(node.Left);
        _builder.Append(GetOperator(node.NodeType));
        base.Visit(node.Right);
        return node;
    }

    protected override Expression VisitLambda<T>(Expression<T> node)
    {
        _builder.Append("function(");
        for (int i = 0; i < node.Parameters.Count; i++)
        {
            if (i > 0)
                _builder.Append(", ");
            _builder.Append(node.Parameters[i].Name);
        }
        _builder.Append(") {");
        if (node.Body.Type != typeof(void))
        {
            _builder.Append("return ");
        }
        base.Visit(node.Body);
        _builder.Append("; }");
        return node;
    }

    private static string GetOperator(ExpressionType nodeType)
    {
        switch (nodeType)
        {
            case ExpressionType.Add:
                return " + ";
            case ExpressionType.Multiply:
                return " * ";
            case ExpressionType.Subtract:
                return " - ";
            case ExpressionType.Divide:
                return " / ";
            case ExpressionType.Assign:
                return " = ";
            case ExpressionType.Equal:
                return " == ";
            case ExpressionType.NotEqual:
                return " != ";

            // TODO: Add other operators...
        }
        throw new NotImplementedException("Operator not implemented");
    }
}

It only handles lambdas with a single instruction, but anyway the C# compiler can't generate an expression tree for a block lambda.

There's still a lot of work to do of course, this is a very minimal implementation... you probably need to add method calls (VisitMethodCall), property and field access (VisitMember), etc.


Script# is used by Microsoft internal developers to do exactly this.


Take a look at Lambda2Js, a library created by Miguel Angelo for this exact purpose.

It adds a CompileToJavascript extension method to any Expression.

Example 1:

Expression<Func<MyClass, object>> expr = x => x.PhonesByName["Miguel"].DDD == 32 | x.Phones.Length != 1;
var js = expr.CompileToJavascript();
Assert.AreEqual("PhonesByName[\"Miguel\"].DDD==32|Phones.length!=1", js);

Example 2:

Expression<Func<MyClass, object>> expr = x => x.Phones.FirstOrDefault(p => p.DDD > 10);
var js = expr.CompileToJavascript();
Assert.AreEqual("System.Linq.Enumerable.FirstOrDefault(Phones,function(p){return p.DDD>10;})", js);

More examples here.


The expression has already been parsed for you by the C# compiler; all that remains is for you to traverse the expression tree and generate the code. Traversing the tree can be done recursively, and each node could be handled by checking what type it is (there are several subclasses of Expression, representing e.g. functions, operators, and member lookup). The handler for each type can generate the appropriate code and traverse the node's children (which will be available in different properties depending on which expression type it is). For instance, a function node could be processed by first outputting "function(" followed by the parameter name followed by ") {". Then, the body could be processed recursively, and finally, you output "}".


A few people have developed open source libraries seeking to solve this problem. The one I have been looking at is Linq2CodeDom, which converts expressions into a CodeDom graph, which can then be compiled to JavaScript as long as the code is compatible.

Script# leverages the original C# source code and the compiled assembly, not an expression tree.

I made some minor edits to Linq2CodeDom to add JScript as a supported language--essentially just adding a reference to Microsoft.JScript, updating an enum, and adding one more case in GenerateCode. Here is the code to convert an expression:

var c = new CodeDomGenerator();
c.AddNamespace("Example")
    .AddClass("Container")
    .AddMethod(
        MemberAttributes.Public | MemberAttributes.Static,
        (int x) => "Square",
        Emit.@return<int, int>(x => x * x)
    );
Console.WriteLine(c.GenerateCode(CodeDomGenerator.Language.JScript));

And here is the result:

package Example
{
    public class Container
    {
        public static function Square(x : int)
        {
            return (x * x);
        }
    }
}

The method signature reflects the more strongly-typed nature of JScript. It may be better to use Linq2CodeDom to generate C# and then pass this to Script# to convert this to JavaScript. I believe the first answer is the most correct, but as you can see by reviewing the Linq2CodeDom source, there is a lot of effort involved on handling every case to generate the code correctly.

0

精彩评论

暂无评论...
验证码 换一张
取 消