AScript之编译递归函数
AScript是一个开源的C#动态脚本解析执行引擎,支持解释执行和编译执行两种模式,本篇介绍AScript中如何编译脚本中的递归函数。
一 解析函数
我们先来看个脚本:
1 int fib(int n) { 2 if (n <= 1) return n; 3 return fib(n - 1) + fib(n - 2); 4 } 5 fib(10)
该脚本定义了一个fib递归函数,然后调用该递归函数,返回函数值。
解析脚本时,将函数定义生成语法树DefineFuncNode:
- 函数名:fib
- 返回类型:int
- 参数列表:int n
- 函数体
二 编译函数
对DefineFuncNode进行编译时,我们并不需要提前知道它的函数体是否存在递归调用。
2.1 编译上下文
首先,有个编译上下文BuildContext,保存编译期间的变量定义、函数定义。
编译函数时先定义一个临时上下文,用于保存函数内部的变量定义和函数定义:
1 var tempBuildContext = new BuildContext(buildContext) 2 { 3 RewriteLocalVariables = false, 4 ReturnType = funcReturnType, 5 IsMain = true 6 };
2.2 函数参数
向临时上下文添加参数定义:
1 Type[] argTypes = null; 2 if (this.Args != null && this.Args.Length > 0) 3 { 4 argTypes = new Type[this.Args.Length]; 5 for (int i = 0; i < this.Args.Length; i++) 6 { 7 var arg = this.Args[i]; 8 var type = arg.SystemType ?? scriptContext.EvalType(arg.Type); 9 if (type == null) 10 { 11 throw new ScriptAnalyzingException($"unknown parameter type {arg.Type} in function {this.Name}"); 12 } 13 argTypes[i] = type; 14 string argName = arg.Name; 15 // 匿名参数名:_1、_2、_3 ... 16 if (IsAnonymous(argName)) argName = "_" + i; 17 tempBuildContext.Parameters[argName] = Expression.Parameter(type, argName); 18 } 19 }
2.3 函数头定义
这一步非常关键,先保存函数头定义,后续编译函数体时如果有函数调用,首先会搜索函数头定义列表是否有匹配的函数,实现递归调用。
1 // 匿名函数不生成函数头定义 2 var delegateDefine = IsAnonymous(this.Name) ? null : buildContext.AddDelegateDefine(this.Name, argTypes, funcReturnType);
注意,函数定义要添加到全局buildContext上下文,而不是函数临时上下文tempBuildContext中。
我们来看一下AddDelegateDefine方法:
1 public DelegateDefine AddDelegateDefine(string name, Type[] argTypes, Type returnType) 2 { 3 List<DelegateDefine> list; 4 if (_DelegateDefines == null) 5 { 6 _DelegateDefines = new Dictionary<string, List<DelegateDefine>>(); 7 _DelegateDefines[name] = list = new List<DelegateDefine>(); 8 } 9 else if (!_DelegateDefines.TryGetValue(name, out list)) 10 { 11 _DelegateDefines[name] = list = new List<DelegateDefine>(); 12 } 13 var delegateDefine = new DelegateDefine(name, argTypes, returnType); 14 list.Add(delegateDefine); 15 return delegateDefine; 16 }
函数头DelegateDefine定义如下:
1 /// <summary> 2 /// int fib(int n) 3 /// </summary> 4 public class DelegateDefine 5 { 6 public string Name { get; private set; } 7 public Type[] ArgTypes { get; private set; } 8 public Type ReturnType { get; private set; } 9 public ParameterExpression Variable { get; set; } 10 11 public DelegateDefine(string name, Type[] argTypes, Type returnType) 12 { 13 this.Name = name; 14 this.ArgTypes = argTypes; 15 this.ReturnType = returnType; 16 } 17 }
其中Variable字段有什么用途呢?
它的类型是ParameterExpression,很明显是一个变量,用于引用编译的函数LambdaExpression,那什么时候创建这个变量呢?
2.4 编译函数体
前面函数参数定义、函数头定义都已就绪,接下来开始编译函数体:
1 var body = this.Body.Build(tempBuildContext, scriptContext, buildOptions);
我们具体看看函数体中调用函数是如何编译的,调用函数fib(n - 1)会解析生成CallFuncNode语法树:
1 public class CallFuncNode : TreeNode 2 { 3 public string Name { get; set; } 4 public ITreeNode[] Args { get; set; } 5 6 public Expression Build(BuildContext buildContext, ScriptContext scriptContext, BuildOptions options); 7 }
编译时会搜索上下文中的函数定义:
1 Expression[] argExprs = null; 2 Type[] argTypes = null; 3 4 // 从编译上下文环境中构建 5 var tempBuildContext = buildContext; 6 while (tempBuildContext != null) 7 { 8 var result = BuildFunc(buildContext, options, tempBuildContext.TempFunctions, name, args, ref argExprs, ref argTypes); 9 if (result != null) return result; 10 if (tempBuildContext.HasDelegateDefine(name)) 11 { 12 if (args != null && args.Count > 0 && argExprs == null) 13 { 14 argExprs = new Expression[args.Count]; 15 argTypes = new Type[args.Count]; 16 for (int i = 0; i < args.Count; i++) 17 { 18 var arg = args[i].Build(buildContext, this, options); 19 argExprs[i] = arg; 20 argTypes[i] = arg.Type; 21 } 22 } 23 var del = tempBuildContext.GetDelegateDefine(name, argTypes); 24 if (del != null) 25 { 26 return Expression.Invoke(del, argExprs); 27 } 28 } 29 tempBuildContext = tempBuildContext.Parent; 30 }
前面调用AddDelegateDefine添加函数定义,这里GetDelegateDefine获取匹配的函数定义:
1 public ParameterExpression GetDelegateDefine(string name, IList<Type> inArgTypes) 2 { 3 if (_DelegateDefines == null || _DelegateDefines.Count == 0) return null; 4 if (!_DelegateDefines.TryGetValue(name, out var list)) return null; 5 if (list.Count == 0) return null; 6 var delegateDefine = list.FirstOrDefault(a => ScriptUtils.IsMatchArgTypes(inArgTypes, a.ArgTypes)); 7 if (delegateDefine == null) return null; 8 if (delegateDefine.Variable == null) 9 { 10 delegateDefine.Variable = Expression.Variable(ScriptUtils.GetDelegateType(delegateDefine.ArgTypes, delegateDefine.ReturnType ?? typeof(object)), delegateDefine.Name); 11 } 12 return delegateDefine.Variable; 13 }
匹配到对应的函数定义时,创建DelegateDefine中的Variable变量,就表示有递归调用了。
2.5 生成Lambda
前面我们在编译函数体中的调用函数语句时,会检索当前函数定义,来实现递归调用。
最后,就是编译生成完整的函数:
1 // 如果函数未定义返回类型,但是有递归调用,此时无法自动根据函数体推导返回类型,强制定义为object类型 2 if (funcReturnType == null && delegateDefine?.Variable != null) 3 { 4 tempBuildContext.ReturnType = typeof(object); 5 } 6 // 生成LambdaExpression 7 var lambda = tempBuildContext.Build(scriptContext, buildOptions, body); 8 // 将函数赋值给临时函数变量 9 var tmpVar = delegateDefine?.Variable ?? Expression.Variable(lambda.Type); 10 var assign = Expression.Assign(tmpVar, lambda); 11 int hashCode = tmpVar.GetHashCode(); 12 string tmpVarName = hashCode > 0 ? $"<>$tmpVar_{hashCode}" : $"<>$tmpVar__{-hashCode}"; 13 buildContext.Variables[tmpVarName] = tmpVar; 14 buildContext.LocalVariables.Add(tmpVarName); 15 buildContext.PrevExpressions.Add(assign);
2.6 回写函数
最后的最后,将函数添加到上下文,并回写到ScriptContext中,用于后续脚本调用:
1 if (!IsAnonymous(this.Name)) 2 { 3 // 添加到编译上下文 4 buildContext.AddTempFunc(this.Name, tmpVar); 5 // 回写到脚本上下文 6 if (buildContext.RewriteLocalVariables && (options?.RewriteFunctions ?? true) && !(options?.Standalone ?? false)) 7 { 8 var addTempFuncExpression = Expression.Call( 9 buildContext.GetScriptContextParameter(), 10 ScriptUtils.Method_ScriptContext_AddTempFunc, 11 Expression.Constant(this.Name), 12 tmpVar); 13 return Expression.Block(addTempFuncExpression, tmpVar); 14 } 15 } 16 // 返回函数引用 17 return tmpVar;
三 编译结果
我们来看看编译开头的脚本示例是什么样的。
1 var script = new Script(); 2 // 仅生成LambdaExpression,断点调试可以查看DebugView 3 var lambda = script.Lambda(code);
查看lambda的DebugView信息:
1 .Lambda #Lambda1<System.Func`2[AScript.ScriptContext,System.Int32]>(AScript.ScriptContext $var1) { 2 .Block(System.Func`2[System.Int32,System.Int32] $fib) { 3 $fib = .Lambda #Lambda2<System.Func`2[System.Int32,System.Int32]>; 4 .Call $var1.AddTempFunc( 5 "fib", 6 $fib); 7 .Invoke $fib(10) 8 } 9 } 10 11 .Lambda #Lambda2<System.Func`2[System.Int32,System.Int32]>(System.Int32 $n) { 12 .Block(System.Int32 $var2) { 13 .If ($n <= 1) { 14 .Block() { 15 $var2 = $n; 16 .Return #Label1 { } 17 } 18 } .Else { 19 .Default(System.Void) 20 }; 21 $var2 = .Invoke $fib($n - 1) + .Invoke $fib($n - 2); 22 .Return #Label1 { }; 23 .Label 24 .LabelTarget #Label1:; 25 $var2 26 } 27 }
可以看到,首先定义了$fib变量引用fib函数,然后fib函数体调用$fib,实现递归调用。
四 结束语
通过预先定义函数头的方式,不仅能实现递归调用,也可以实现C语言中的函数头定义功能。如果函数在脚本末尾定义,前面如何发现和调用这个函数呢?我们可以把函数头定义放在最前面,函数实现就可以放后面了。
AScript开源地址:

浙公网安备 33010602011771号