About
RSS

Bit Focus


句读探析 - Stekinscript 实现折行及多行 lambda 语法

Posted at 2012-11-23 13:39:17 | Updated at 2024-03-28 16:45:22

上篇

自动机与表达式折行语法分析

概述

    在分析折行实现前, 先说说对基本的算术结构的分析.
    自动机基类 grammar::AutomationBase (grammar/automation-base.h: 30) 能够处理所有没有在 Yacc 语法规则中给出的细节, 比如将因子与算符组成算式, 分析括号配对, 还有对逗号, 冒号等分隔符的处理.
    自动机对可以处理的项目作了基本的分类, 对应于其中各名为 pushXxx 的函数和 matchClosing 函数. 而 matchClosing 函数相当于将 pushCloseParen, pushCloseBracket 等三种不同的结束括号合并成一个函数了, 因为大部分自动机自身不处理三种结束括号, 而将它们派发给其它的自动机处理.
    什么, 自动机还有很多种? 没错, 在 Stekinscript 语法模块设计中, 用来分析表达式的自动机有多种, 不同的自动机互相利用使得代码复杂度能够大大降低. 举个例子, 算术运算自动机 grammar::ArithAutomation (grammar/expr-automations.h: 9) 本身不处理大括号配对, 但表达式里面终归是可以出现大括号裹起来的字典的; 当这种情况发生时, 算术自动机会创建一个字典识别自动机 grammar::DictAutomation (grammar/expr-automations.h: 132), 让它顶替自己处理接下来的部分; 而这个字典自动机自己又是很懒的, 它才不会自己动手去识别用表达式描述的字典的键跟值, 怎么办呢? 与是它再新建一些算术自动机来识别表达式, 而自己只坐等逗号冒号等分隔符, 或者花括号结束.
    无论哪一种自动机, 都只会对特定的一些种类的 Token 感兴趣, 而基类 grammar::AutomationBase 对任何项目的实现都是一句话 (grammar/automation-base.cpp: 58-97)
error::unexpectedToken(posimage);
    那就是报错. 也就是说如果实现的自动机子类不重写它的函数, 就相当于子类丢弃了这个 Token 并报错. 除了这种粗暴的对待方式, 大致上来说每个函数还有如下方式对待 Token

自动机栈

    刚才提到, 各种不同的自动机会相互委托, 一起完成表达式语法分析过程, 这个过程无可避免地要用到栈结构, 此栈名字毫无亮点地称之为自动机栈 grammar::AutomationStack (grammar/automation-base.h: 67).
    引入自动机栈之后, 重新描述刚才 ArithAutomationDictAutomation 的基情应该这么说, 当 ArithAutomation 对象遇到大括号时, 新建一个 DictAutomation 对象压进栈中; 而当 DictAutomation 分析完字典后, 将自己从栈中弹出, 然后将归约得到的 Dictionary 表达式对象交给栈顶的自动机.
    自动机栈自然拥有栈基本的功能如 push, pop 等等, 此外它还支持其它一些函数, 如 reduced 的两个重载 (grammar/automation-base.cpp: 31-43), 当位于栈顶的自动机完成使命之后, 通过这两个函数传入自己归约得到的表达式或表达式列表, 自动机栈这时会弹出栈顶的自动机, 然后将传入的结果传给当时的次栈顶.

折行判定

    在自动机栈运作的过程中, 如果遇到行尾, 这时需要判定折行是否为语句结束.
    每个自动机类型的对象有其固有的判定法, 比如, ArithAutomation 刚刚才吃掉一个 * 符号, 之后遇到换行, 当然不会认为这是语句结束, 它还等着二元运算后面的项呢, 如读入代码进行到 a * 这里时遇到折行
x: a *
    b
    但是, 如果此时 ArithAutomation 自身已经完结, 它并不能直接决定语句是否结束, 因为它有可能在某个没有关闭的实参列表中, 如
f(0, a + b
  * c)
代码进行到 a * b 时, 栈顶的 ArithAutomation 可以认为语句结束了, 可是如果此时贸然归约当前内容 (a + b) 为一个表达式却是错误的, 因此它要去询问一下次栈顶, 也就是它下面的那个自动机是否应该结束语句. 如此一来, 只有整个自动机栈都表示折行是语句结束, 那么才会结束语句.

    几个典型的 eolAsBreak 重写实现如下

lambda 冒号后的折行

    有典型的当然有非典型的, 它便是 grammar::NestedOrParamsAutomation (grammar/expr-automations.h: 96).
    是想一下, 如果一个 ArithAutomation 在等待因子时, 来了一个括号该怎么处理? 更一般地, 如下面这种情况
x: (
看似一个变量定义后, 初值部分开头就是个开括号.
    这时以下两种情况都有可能
x: (a + b) * c
x: (a, b): c
    前者的括号是用来改变优先级的而后者的括号则用来括起 lambda 的参数列表. 所以为什么这自动机类要叫如此扭曲的名字大家明白了吧.
    而对于下面这两种情况则区分更加麻烦
x: (a) * c
x: (a): c
直到结束括号之后的冒号出现之前, (a) 表示什么都是未知的, 可以是一个无聊程序员忘记擦掉的括号, 或者某个代码生成程序额外生成的括号, 括起一个常量什么的, 又有可能是正常的 lambda 形参. 因此与其它自动机对待括号的方式不同, NestedOrParamsAutomation::matchClosing (grammar/expr-automations.cpp: 491) 在结束括号头一次出现时 (即成员变量 _waiting_for_closing 还为 true 时), 对其的处理是 "接受并丢弃" 而不是 "将自身归约并丢弃", 并且在丢弃这个圆括号后, 开始痴心等冒号 (置 _wait_for_colon = true).
    如果这时果真来了冒号 (NestedOrParamsAutomation::pushColon L 511), 那么自动机会向栈里面再压入一个 ArithAutomation 对象, 这个对象开始接收行间 lambda 的返回值表达式.
    问题来了, 如果这时马上来了个换行呢? 也就是说, 这个新上的 ArithAutomation 还没接到任何 Token 就迎来了折行, 该怎么做?
    回顾 ArithAutomation::eolAsBreak, 对于 ArithAutomation 来说, 如果它自己是空的, 那么它会允许此时折行, 并询问次栈顶该如何做; 此时它会向次栈顶传入一个 bool 参数, 告诉次栈顶自己是不是空的
bool ArithAutomation::eolAsBreak(bool) const
{
    if (_need_factor && !_empty()) { // _need_factor: 当前是否正在等因子, 比如在二元运算符后该值为 true
                                     // _empty(): 当前已经接受的内容是否为空
        return false;
    }
    return _previous->eolAsBreak(_empty());
}
与之配合地, NestedOrParamsAutomation::eolAsBreak (L 578) 则会在冒号之后并且 lambda 返回值为空表达式时表示一定能折行
bool NestedOrParamsAutomation::eolAsBreak(bool sub_empty) const
{
    if (_afterColon() && sub_empty) {
        return true;
    }
    if (_wait_for_closing) {
        return false;
    }
    return _previous->eolAsBreak(false);
}
    这样就能正确判定折行了.

自动机栈与 ClauseBase 对象

自动机栈归约语句

    讲了这么一通, 下面该讲讲自动机栈是如何与 ClauseBase 对象合体了.
    在每个 ClauseBase 对象中都有个自动机栈对象 _stack (grammar/automation-base.h: 105), 如果正在解析的这一句与表达式有关系 (比如表达式语句, 或 if 分支), 那么这个栈就是非空的, 并且此栈正在解析表达式. 当折行要发生时, ClauseBase::tryEol 被调用 (上篇中最后就说到这里停下), 而这个函数实现先看看一下栈是不是空的, 如果不是, 则看看当前能不能结束语句, 如果能, 就调用自动机的 eol 函数结束语句, 否则就续行.
    而在送入表达式 Token 序列之前, ClauseBuilder 对象会调用 ClauseBaseprepareXxx 函数 (grammar/automation-base.cpp: 132-145), 这些函数中会构造特殊的自动机, 并且构造时将 ClauseBase 对象自身传入. 以 grammar::ReturnAutomation (grammar/expr-automation.h: 174) 为例, 其 eol 函数实现为
/* grammar/expr-automation.cpp: 723 */
void ReturnAutomation::eol(ClauseStackWrapper&, AutomationStack& stack, misc::position const&)
{
    if (_expr->empty()) {
        _clause->acceptStmt(util::mkptr(new ReturnNothing(_expr->pos)));
    } else {
        _clause->acceptStmt(util::mkptr(new Return(_expr->pos, std::move(_expr))));
    }
    stack.pop();
}
将其分析所得的表达式打包为一只 Return 语句 (grammar/stmt-nodes.h: 73) 对象或 ReturnNothing 语句 (grammar/stmt-nodes.h: 86) 对象.

自动机栈处理多行 lambda

    之前说到, lambda 在其形参列表及冒号后遇到折行时, 会将该折行视为语句结束, 但问题在于此时语句并没有真正结束, 如
setTimeout(():
    console.log('Aki Misawa')
, 2000)
    在冒号后, 第二行确实内容不再接着第一行继续, 第一行的继续应该在第三行 , 2000) 处, 那么这个问题如何处理呢?
    来看看 eol 函数中的第一个参数类型 grammar::ClauseStackWrapper (grammar/automation-base.h: 109) 在 eol 函数中是如何被使用到的. 在自动机实现文件 grammar/expr-automations.cpp 中检索「ClauseStackWrapper&,」会发现大部分自动机在实现时根本没有理睬这个参数; 少部分将此参数直接传递给其它自动机, 自身并没有使用, 唯一只用了该参数的只有 NestedOrParamsAutomation::eol (L 589).
    它对此参数的使用并不复杂, 如果当前自动机正处理到冒号之后, 并且返回值为空 (也就是正好在 (): 这样的情况下), 那么就调用 ClauseStackWrapper::pushBlockReceiver 函数 (grammar/automation-base.cpp: 196). 这个函数会向该 wrapper 所持有的 ClauseBase 栈 (就是 ClauseBuilder 对象里面那个) 压入一个 ClauseBase (其子类的 L 160) 对象, 这个对象会使得新进的语句成为语句块, 也就是多行 lambda 函数体中的一员, 而不是后续的平级语句, 直到出现一个缩进较低的语句, 终止多行 lambda 的函数体. 多行 lambda 相关的折行也就藉由这个方式解决了.
    (全文完)

Post tags:   Syntax Analysis  Stekin  Compiler Construction

Leave a comment:




Creative Commons License Your comment will be licensed under
CC-NC-ND 3.0


. Back to Bit Focus
NijiPress - Copyright (C) Neuron Teckid @ Bit Focus
About this site