仓库源文

上文说到每个语法树节点的行号和列号现在都是 0。首先确认它们是用在何处,就费了番功夫。ast 库文档并未明确说明。

(写到这刚想起之前扩展 Python 控制台sys.exc_info取了traceback,其中的确有行号,但无列号)

那时半夜里没头绪的时候,先试了这个单行程序1/0(因为当时只支持单行程序解析)。结果返回错误中,行号显示是 1:

Traceback (most recent call last):
  File "./数.py", line 25, in <module>
    exec(可执行码, 环境变量)
  File "测试/除零.ul", line 1, in <module>   <<<<<========此处
    1/0
  File "/Users/xuanwu/work/木兰/prototype/环境.py", line 6, in __内置_除
    return math.floor(a / b)
ZeroDivisionError: division by zero

于是懵逼,不是 0 ??那这个行号用在哪里?兜兜转转不死心,直接把代码里生成数节点时的行号写成随便一个数,比如 4:

        return ast.Num(值, lineno = 4, col_offset = 列号)

果不然,报错时就报 line 4 ,代码也定位不了了:

Traceback (most recent call last):
  File "./数.py", line 25, in <module>
    exec(可执行码, 环境变量)
  File "测试/除零.ul", line 4, in <module>
  File "/Users/xuanwu/work/木兰/prototype/环境.py", line 6, in __内置_除
    return math.floor(a / b)
ZeroDivisionError: division by zero

这样,至少确定设置行号能体现在错误信息中。接下来,想找个最简单的方法实现多行程序。因为本来只想添加行号列号,想尽量少为此添加其他语法。灵机一动,想木兰能不能忽略一个表达式中的换行,于是试了这样:

print(
4/2)

果然可以!但是,接下去就是个大坑,一路找到错误处理部分,还没找到出路,回头再说。

退回来,决定添加多行支持,以便测试行号设置。比想的简单些(当然,参考逆向工程),下面是语法分析器相关部分(详见此 commit):

    @分析器母机.production('模块 : 声明列表')
    def 模块(片段):
        return 语法树.模块(主体=片段[0], 忽略类型=[])

    @分析器母机.production('声明列表 : 表达式声明')
    @分析器母机.production('声明列表 : 声明列表 换行 表达式声明')
    def 声明列表(片段):
        if len(片段) == 1:
            return [片段[0]]
        片段[0].append(片段[(-1)])
        return 片段[0]

    @分析器母机.production('表达式声明 : 表达式')
    def 表达式声明(片段):
        return 语法树.表达式(值 = 片段[0], 行号=0, 列号=0)

现在就支持了这样的代码:

print(2)
print(3)

输出23。下面这样的代码出错信息会是如何呢?

print(2)
print(1/0)

因为第二行出的错,但因为节点的行号为 0,错误仍定位于行 1,估计是 python 默认值:

2Traceback (most recent call last):
  File "./数.py", line 25, in <module>
    exec(可执行码, 环境变量)
  File "测试/多行除零.ul", line 1, in <module>   <<<<<========此处
    print(2)
  File "/Users/xuanwu/work/木兰/prototype/环境.py", line 6, in __内置_除
    return math.floor(a / b)
ZeroDivisionError: division by zero

继续看逆向,似乎取行号还算简单,摘取了能够满足这个例子的部分(列号仍不知道有啥用,先不管),在语法分析器中实现如下

    def 取源码位置(片段):
        if isinstance(片段, Token):
            return 片段.getsourcepos()

    def 取行号(片段):
        try:
            return 语法分析器.取源码位置(片段).lineno
        except:
            return 0
...
    @分析器母机.production('数 : 整数')
    def 数(片段):
        数值 = int(片段[0].getstr(), 0)
        return 语法树.数(数值, 行号=语法分析器.取行号(片段[0]), 列号=0)

运行多行除零代码,终于在错误信息中显示位置正确:

2Traceback (most recent call last):
  File "./数.py", line 25, in <module>
    exec(可执行码, 环境变量)
  File "测试/多行除零.ul", line 2, in <module>
    print(1/0)
  File "/Users/xuanwu/work/木兰/prototype/环境.py", line 6, in __内置_除
    return math.floor(a / b)
ZeroDivisionError: division by zero

代码中已经有很多处创建节点时需要取行号,而且以后会更多,下面打算重构下减少将来的重复代码。与逆向中不同,至今没在语法分析器类中用 self。重构时顺便研究下 self,以及是否需要。