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开源地址: