26

出于兴趣,我想学习如何为一种简单的语言编写解析器,最终为我自己的小代码高尔夫语言编写一个解释器,只要我了解了这些东西的一般工作原理。

所以我开始阅读 Douglas Crockfords 的文章Top Down Operator Precedence

注意:如果您想更深入地了解以下代码片段的上下文,您可能应该阅读这篇文章

我很难理解var语句和赋值运算符=应该如何协同工作。

DC 定义了一个赋值运算符,例如

var assignment = function (id) {
    return infixr(id, 10, function (left) {
        if (left.id !== "." && left.id !== "[" &&
                left.arity !== "name") {
            left.error("Bad lvalue.");
        }
        this.first = left;
        this.second = expression(9);
        this.assignment = true;
        this.arity = "binary";
        return this;
    });
};
assignment("=");  

注意:[[value]] 指代币,简化为它的值

现在如果表达式函数达到 eg [[t]],[[=]],[[2]],结果[[=]].led是这样的。

{
    "arity": "binary",
    "value": "=",
    "assignment": true, //<-
    "first": {
        "arity": "name",
        "value": "t"
    },
    "second": {
        "arity": "literal",
        "value": "2"
    }
}

DC使assignment功能,因为

我们希望它做两件额外的事情:检查左操作数以确保它是正确的左值,并设置一个赋值成员,以便我们稍后可以快速识别赋值语句。

这对我来说是有意义的,直到他介绍了 var声明,定义如下。

var 语句定义当前块中的一个或多个变量。每个名称可以选择后跟 = 和一个初始化表达式。

stmt("var", function () {
    var a = [], n, t;
    while (true) {
        n = token;
        if (n.arity !== "name") {
            n.error("Expected a new variable name.");
        }
        scope.define(n);
        advance();
        if (token.id === "=") {
            t = token;
            advance("=");
            t.first = n;
            t.second = expression(0);
            t.arity = "binary";
            a.push(t);
        }
        if (token.id !== ",") {
            break;
        }
        advance(",");
    }
    advance(";");
    return a.length === 0 ? null : a.length === 1 ? a[0] : a;
});

现在,如果解析器到达一组标记,比如[[var]],[[t]],[[=]],[[1]]生成的树,它会看起来像这样。

{
    "arity": "binary",
    "value": "=",
    "first": {
        "arity": "name",
        "value": "t"
    },
    "second": {
        "arity": "literal",
        "value": "1"
    }
}

我的问题的关键部分是if (token.id === "=") {...}部分。

我不明白为什么我们打电话

    t = token;
    advance("=");
    t.first = n;
    t.second = expression(0);
    t.arity = "binary";
    a.push(t);

而不是

    t = token;
    advance("=");
    t.led (n);
    a.push(t);  

...部分。

这将调用我们的[[=]]操作符led函数(赋值函数),它确实

确保它是一个正确的左值,并设置一个赋值成员,以便我们以后可以快速识别赋值语句。 例如

{
    "arity": "binary",
    "value": "=",
    "assignment": true,
    "first": {
        "arity": "name",
        "value": "t"
    },
    "second": {
        "arity": "literal",
        "value": "1"
    }
}

由于没有lbp介于 0 和 10 之间的运算符,因此调用expression(0) vs. expression (9)没有区别。( !(0<0) && !(9<0) && 0<10 && 9<10))

并且该token.id === "="条件阻止了对对象成员的分配,无论token.id是被调用'['还是不会被调用。'.'t.led

简而言之,我的问题是:

为什么我们不可选地在变量声明后调用赋值运算符的可用led函数。而是手动设置语句的firstandsecond成员而不是assignment成员?

这是两个解析简单字符串的小提琴。使用原始代码和一个使用赋值运算符led

4

3 回答 3

8

解析语言时,有两件事很重要——语义和语法。

语义上,如果不相同var x=5;var x;x=5看起来非常接近(因为在这两种情况下,首先声明一个变量,然后为该声明的变量分配一个值。这是您所观察到的,并且在大多数情况下是正确的。

然而,在语法上,两者不同(这是清晰可见的)。

在自然语言中,类似物是:

  • 男孩有一个苹果。
  • 有一个苹果,男孩有它。

现在要简洁!让我们看两个例子。

虽然两者(几乎)意思相同,但它们显然不是同一个句子。回到 JavaScript!

第一个:阅读方式var x=5如下:

var                      x              =                  5
-----------------------VariableStatement--------------------
var -------------------        VariableDeclarationList 
var -------------------        VariableDeclaration
var            Identifier -------   Initialiser(opt)
var ------------------- x              = AssignmentExpression
var ------------------- x ------------ = LogicalORExpression
var ------------------- x ------------ = LogicalANDExpression
var ------------------- x ------------ = BitwiseORExpression
var ------------------- x ------------ = BitwiseXORExpression
var ------------------- x ------------ = BitwiseANDExpression 
var ------------------- x ------------ = EqualityExpression
var ------------------- x ------------ = ShiftExpression
var ------------------- x ------------ = AdditiveExpression
var ------------------- x ------------ = MultiplicativeExpression
var ------------------- x ------------ = UnaryExpression
var ------------------- x ------------ = PostfixExpression 
var ------------------- x ------------ = NewExpression
var ------------------- x ------------ = MemberExpression
var ------------------- x ------------ = PrimaryExpression
var ------------------- x ------------ = Literal
var ------------------- x ------------ = NumericLiteral
var ------------------- x ------------ = DecimalLiteral
var ------------------- x ------------ = DecimalDigit 
var ------------------- x ------------ = 5

呸!所有这一切都必须在语法上进行 parse var x = 5,当然,其中很多都在处理表达式 - 但它就是这样,让我们​​检查另一个版本。

这分为两种说法。var x; x = 5第一个是:

var                      x 
--------VariableStatement---
var ---- VariableDeclarationList 
var ---- VariableDeclaration
var                 Idenfifier (optional initializer not present)
var                      x

第二部分是x=5赋值语句。我可以继续用同样的表情疯狂——但这几乎是一样的。

所以总而言之,虽然两者在语义上产生相同的结果,在语法上正如官方语言语法所指定的那样——它们是不同的。结果,在这种情况下 - 确实是一样的。

于 2013-09-23T10:56:16.133 回答
1

我没有时间阅读整篇文章,所以我不是百分百确定。在我看来,原因是var语句中的赋值运算符有点特殊。它不接受所有可能的左值 - 不允许对象的任何成员(否.[运算符)。只允许使用普通变量名。

所以我们不能使用普通assignment函数,因为它允许所有左值。

我对此非常确定,但以下只是猜测:

我们必须assignment选择性地调用函数,并且只有在我们检查了我们使用了赋值运算符之后。

  advance();
  if (token.id === "=") {
      // OK, Now we know that there is an assignment.

但是该函数assignment假定当前标记是左值,而不是 operator =


我不知道为什么该assignment成员未设置为true. 这取决于您想对生成的树做什么。同样,var语句中的赋值有点特殊,设置它可能不可行。

于 2013-09-19T00:17:44.430 回答
1

赋值(例如var t; t = 1;)在概念上不同于初始化(例如var t = 1;),尽管两者都会导致内存状态改变。使用同一段代码来实现两者是不可取的,因为在该语言的未来版本中,一个可能会独立于另一个进行更改。

在谈论赋值运算符重载和复制构造函数时,可以在 C++ 上显示概念上的差异。初始化可以调用复制构造函数,赋值可以调用赋值运算符重载。赋值永远不会触发复制构造函数,初始化永远不会使用赋值运算符重载。请参阅有关复制构造函数和赋值运算符重载的教程

另一个例子是 Strix 的例子:到目前为止,并不是所有的左值都可以var在 JavaScript 中使用。我认为这是它们在 JavaScript 中最大的区别,如果不是唯一的区别的话。当然,忽略 var 中明显的范围变化。

人们可以认为两者使用等号是一种巧合。Pascal:=用于赋值和=初始化。JavaScript 也可以使用类似var t : 1;.

于 2013-09-19T18:50:36.697 回答