如何为指定语言实现语义检查器?附函数调用规则示例
实现符合规则的函数调用语义检查器
首先,先明确我们要解决的问题:给定一段类JS代码,需要检查所有函数调用是否满足调用时该函数已在当前作用域或父作用域中声明(静态代码顺序上在调用点之前)。针对你提供的示例代码:
for(var x in z) { a = x + 1 + 2; foo(); } function bar() {} for(t in []){ function hello(a) { a = t + 'hello' + 'world'; } bar(); hello(); } hello(); function hello() {} 15 * 30; 1 + 2 + 3; a = 3 - 2 - 1; a + 10;
按照规则,第3行的foo()无任何声明、第13行的hello()调用在全局声明之前,这两处需要报错。下面我会一步步讲解如何实现这样的语义检查器:
第一步:生成抽象语法树(AST)
要做语义检查,首先得把源代码转换成结构化的AST——这是后续所有分析的基础。如果你是从零实现,可以自己写一个递归下降解析器,处理目标语言的语法规则(比如函数声明、调用语句、循环结构等);如果想快速落地,可以用成熟的解析库(比如针对JS的Acorn、Esprima)直接生成AST,这样能把精力集中在语义检查逻辑上。
第二步:构建作用域链与符号表
语义检查的核心是追踪每个作用域里的函数声明,所以我们需要:
- 定义作用域结构:每个作用域包含三个关键信息:父作用域的引用(形成作用域链)、当前作用域的符号表(存储已声明的函数名和对应的声明行号)、作用域的类型(全局/函数)。
- 遍历AST维护作用域:
- 从全局作用域开始初始化,遍历AST节点。
- 遇到
function声明节点时,创建新的函数作用域,将其挂载到当前作用域下,然后进入新作用域继续遍历函数内部的代码。 - 离开函数节点时,切回父作用域。
- 每遇到一个函数声明(不管是全局还是函数内的),就把函数名和它的行号添加到当前作用域的符号表中——注意这里按代码的静态顺序添加,不做JS式的函数提升处理,因为你的规则要求调用必须在声明之后。
第三步:检查函数调用的合法性
遍历AST时,每碰到一个函数调用节点(比如CallExpression),按以下逻辑检查:
- 提取被调用的函数名称(比如
foo、hello)。 - 从当前作用域开始,向上遍历整个作用域链:
- 对于当前作用域:检查符号表中是否存在该函数名,并且函数的声明行号小于当前调用的行号(确保声明在调用之前)。
- 对于父作用域:只要父作用域的符号表中存在该函数名即可——因为父作用域的代码在子作用域之前执行,其声明相当于在子作用域创建前就已存在。
- 如果遍历完所有作用域都找不到符合要求的声明,就输出错误信息,比如:
Error: 第3行的函数调用 'foo()' 未在当前或父作用域中提前声明。
针对示例代码的检查过程
我们用示例代码走一遍逻辑:
- 第3行
foo();:当前作用域是全局,全局符号表中没有foo的条目,直接报错。 - 第8行
hello();:当前作用域是全局(ES5中循环没有块级作用域),全局符号表中已经在第6行添加了hello的声明,且声明行号小于调用行号,合法。 - 第13行
hello();:当前作用域是全局,此时遍历到这里还没处理第14行的function hello() {},全局符号表中没有hello的条目,所以报错。
额外细节优化
- 处理函数表达式:比如
var foo = function() {}这种形式,也要当作函数声明添加到符号表中,因为它也是合法的函数定义方式。 - 嵌套作用域检查:比如函数内部调用父函数中提前声明的函数,要能正确沿着作用域链找到父作用域的符号表。
- 可选的类型检查:如果需要更严格的规则,可以额外检查符号表中的条目是否确实是函数类型(避免把普通变量当作函数调用)。
内容的提问来源于stack exchange,提问作者dongerpep
相关产品推荐
相关产品推荐

