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

相关推荐
yue0086 小时前
winform控件篇-按钮
c#
陈珙7 小时前
.NET AI 实战:用 MCP 把 Claude Code 变成数据库 DBA
架构·.net·技术
DevOpenClub10 小时前
Markdown、HTML 和 PPT 如何稳定交付:文档转换任务的幂等发布流程
开发语言·前端·c#·html·powerpoint
软件黑马王子11 小时前
3.单例模式问题1:构造函数
开发语言·单例模式·c#
软件黑马王子12 小时前
4.单例模式问题2:重复挂载
开发语言·单例模式·c#
夏霞13 小时前
c# 不支持的目标框架 解决方案
开发语言·c#
软件黑马王子13 小时前
5.单例模式问题3:多线程问题
单例模式·前端框架·c#
geovindu13 小时前
CSharp: Template Method Pattern
开发语言·后端·c#·.net·模板方法模式·行为模式
CSDN_RTKLIB14 小时前
SetImpliedSelection
c#