1 Semantic Analysis中的变量绑定解析
背景:当一个变量在一个循环里赋值100次。那么这个变量会被赋值100次。当=右侧是函数时,这非常没必要。这个可以通过静态的分析来避免这个问题。这个分析就是Sematic Analysis中的一个例子。
具体做的事情叫
2 把定义一个变量分成两步,声明 和 定义,的原因是解决如下问题。
此时我们选择第3个报错。
3 resolver这类要干什么?
我的理解:
静态扫描一下整个代码。记录在变量定义,或者赋值时,且此时右侧表达式是个变量时。这个变量在那个environment 那个变量里的链里是第几个节点。
解决这个问题:
4 这段代码不知道要干什么。
上面visitVariableExpr的expr 输入 是下图的initializer吗 可能是。当initializer是一个变量的时候就是。但如果是个函数,那么就不是。
确实这里的variable是个expr。它里面只存了变量的名字。
所以。图二这个initializer 可能是一个变量。也许是b 或者是c。但是反正不能是自己。也可能是别的表达式子。
5 这个helper 是干什么的?
原文解释:
这看起来很像 Environment 中评估变量的代码,这是有道理的。我们从最内层的作用域开始向外搜索,在每个映射中寻找匹配的名称。如果我们找到了变量,我们就解析它,同时传递当前最内层作用域与找到变量的作用域之间的作用域数目。因此,如果变量是在当前作用域中找到的,我们就输入 0;如果是在紧邻的外层作用域中,我们就输入 1。
如果我们走遍了所有的代码块作用域,却始终没有找到变量,我们就会让它悬而未决,并假定它是全局变量。我们稍后将讨论 resolve() 方法的实现。现在,让我们继续浏览其他语法节点。
我的理解:
大概明白了,其实还是在记录,下图的这个查找的次数。那就是在栈上,由外向内去寻找这个变量的定义。找到了最外面的,就直接返回了他所在栈的index。
这里还要看后面的resolve如何实现。
6 resolve asginment是为了什么?
和定义一个变量一样,也是要找到确定的那个右值变量。
6 resolve function的声明是为了什么?我猜也是为了里面的变量?不是为了变量。
这里function的声明也可以看作一个变量。
他也有variable一样的域的问题。和问题5 里面说的一样的问题。
resolve function可以让他明确指出用的是哪个function。
能看出来,他和variable不同。因为它可以递归调用自己。
7 还resolve了其他东西,就是为了遍历整个语法树。
感觉就是针对不同的stmt,遍历它里面的expr
以及各种Expr,如下
我感觉这段是为了凑字数 哈哈哈哈
这里都要把这些函数写上的原因是要想interpreter一样解析整颗语法树。
不能在某个节点停下来。
8 resolve 就是为了干这个的
让我们看看我们的解析器有什么用。每次访问变量时,它都会告诉解释器在当前作用域和变量定义的作用域之间有多少个作用域。在运行时,这正好对应于当前作用域和解释器可以找到变量值的外层作用域之间的环境数量。
但是为啥resolve的时候 和runtime的情况一致?
好问题 肯定是可以的。
下面可以看出来。虽然步会对for循环,但是block什么的谁在里面谁在外面这些是都会注意的
他只有在block 还有function的地方才会开新的scope。
还
。
什么时候解析?
他应该和interpret去遍历整个树的操作是一致的。所以才可以有同样的那个个数。
9 一般这个解析的结果会反到那个node里面。但是这个语言没有。
它放到一个map,方便ide解析。
10 为什么 Map<Expr, Integer>
足够?
你可能会认为我们需要某种嵌套树结构,以避免在有多个表达式引用同一变量时出现混乱,但每个表达式节点都是自己的 Java 对象,有自己的唯一标识。一个单一的映射不难将它们分开。
问题背景:
- 我们使用
Map<Expr, Integer>
来存储解析过程中的信息,其中Expr
是表示表达式的语法树节点,而Integer
则表示这个表达式在当前作用域中相对于定义该变量的作用域的深度。 - 变量名可以在不同的作用域中重复使用,表达式节点对应的是语法树中的具体位置,而不是变量的名字本身。
例子说明:
复制代码
var a = 10; { var a = 20; print a; }
这里有两个不同的 a
变量,分别处于不同的作用域:
- 第一层作用域(全局作用域):
var a = 10;
- 第二层作用域(内部块作用域):
var a = 20;
和print a;
对于解析器而言,它需要记录两个不同的 a
变量的作用域深度。在这种情况下,尽管变量名是相同的,解析器处理的是每个表达式节点,而不是变量名本身。
步骤:
- 在全局作用域定义变量
a
,这时全局作用域中的a
在深度为0
的作用域中。 - 当进入内部块时,再次定义了变量
a
,此时内部块中的a
在深度为1
的作用域中。 - 解析
print a;
时,解析器会知道它是在第二层作用域中,因此找到的是内部块中的a
,深度为0
。
在语法树中,每个 a
都有自己的独立节点(Expr
对象),即使它们的变量名相同。在解析时,解释器可以分别为两个 Expr
对象记录其作用域深度:
- 全局的
a
变量对应的表达式节点存储在locals
中,深度为0
。 - 内部块作用域的
a
对应的表达式节点存储在locals
中,深度为1
。 - 当遇到
print a
时,解析器根据当前的表达式节点(即print a
中的a
),在locals
中找到它的深度信息。
为什么 Map<Expr, Integer>
足够?
-
每个表达式(
Expr
)在语法树中是一个独立的 Java 对象。即使两个表达式节点都引用了变量a
,它们在内存中是不同的Expr
对象。所以我们可以将每个表达式和它的解析深度关联起来,而不需要担心同名变量的混淆。 -
在这个例子中,虽然有两个
a
,它们对应的Expr
是不同的,分别代表不同作用域中的a
。解释器根据Expr
对象的唯一性,将不同的作用域深度存储在Map<Expr, Integer>
中,可以准确区分同名变量。
总结来说,Map<Expr, Integer>
足够的原因在于:每个表达式节点在语法树中都是独立的对象,因此不需要担心变量名称的冲突,直接通过表达式节点的引用来存储和查找作用域深度。
11 改变了interpreter 对variable 的expr visit方式。
原来是这样的
现在是这样的
上面就能保证在Environment的那个 节点上找到对应的值吗?environment变量是个自定义的类型。
明白了,这个就是干这个的
12 现在可以看到,这个点assign就是和get反着。下面这个会穿越几个链条去赋值。
13 同一block下,多个相同变量会报错
15 同时增加了在global写return会报错的功能。
这个resolver其实可以解决不少静态时,检查的功能。
感悟
其实还是针对variable和function来搞的 resolveFunction,visitVariableExpr
其他的不重要。
然后只有发现了block才会有新的scope。