首先声明一下,我对解析器的工作原理一窍不通。话虽如此,gram.y文件中的第296行定义了以下标记来表示R语言(YACC?)解析器中的赋值:
%token LEFT_ASSIGN EQ_ASSIGN RIGHT_ASSIGN LBB
然后,
在gram.c的5140到5150行, 这看起来像对应的C代码:
case '-':
if (nextchar('>')) {
if (nextchar('>')) {
yylval = install_and_save2("<<-", "->>");
return RIGHT_ASSIGN;
}
else {
yylval = install_and_save2("<-", "->");
return RIGHT_ASSIGN;
}
}
最后,在
gram.c的第5044行开始,
install_and_save2
的定义:
static SEXP install_and_save2(char * text, char * savetext)
{
strcpy(yytext, savetext);
return install(text);
}
再说一遍,对于没有使用解析器的经验来说,似乎
->
和
->>
在解释过程中被直接翻译成
<-
和
<<-
,分别处于一个非常低的层次。
你提出了一个非常好的观点,询问解析器如何“知道”将参数逆转为
->
——考虑到
->
似乎已安装到R符号表中作为
<-
,因此能够正确解释
x -> y
为
y <- x
而不是
x <- y
。我所能做的就是在继续遇到支持我的主张的“证据”的同时提供进一步的猜测。希望有些慈悲的YACC专家会碰巧看到这个问题并提供一些见解;尽管如此,我不会抱太大期望。
回到
gram.y的383和384行,这看起来像是与前面提到的
LEFT_ASSIGN
和
RIGHT_ASSIGN
符号相关的一些解析逻辑:
| expr LEFT_ASSIGN expr { $$ = xxbinary($2,$1,$3); setId( $$, @$); }
| expr RIGHT_ASSIGN expr { $$ = xxbinary($2,$3,$1); setId( $$, @$); }
虽然我无法真正理解这个疯狂的语法,但我注意到xxbinary
的第二个和第三个参数在WRT LEFT_ASSIGN
(xxbinary($2,$1,$3)
)和RIGHT_ASSIGN
(xxbinary($2,$3,$1)
)时会被交换。
这是我脑海中的想象:
LEFT_ASSIGN
场景:y <- x
$2
是上述表达式中解析器的第二“参数”,即<-
$1
是第一个;即y
$3
是第三个;即x
因此,结果(C?)调用将为xxbinary(<-, y, x)
。
将这个逻辑应用于RIGHT_ASSIGN
,即x -> y
,再加上我之前关于<-
和->
被交换的猜想,
$2
从 ->
被翻译成 <-
$1
是 x
$3
是 y
但由于结果是xxbinary($2,$3,$1)
而不是xxbinary($2,$1,$3)
,所以结果仍然是xxbinary(<-, y, x)
。
进一步扩展这个话题,我们在
gram.c的3310行中定义了
xxbinary
。
static SEXP xxbinary(SEXP n1, SEXP n2, SEXP n3)
{
SEXP ans;
if (GenerateCode)
PROTECT(ans = lang3(n1, n2, n3));
else
PROTECT(ans = R_NilValue);
UNPROTECT_PTR(n2);
UNPROTECT_PTR(n3);
return ans;
}
很不幸,我在 R 源代码中找不到lang3
(或其变体lang1
、lang2
等)的确切定义,但我认为它用于以与解释器同步的方式评估特殊函数(即符号)。
更新 我会尽我所能回答评论中的一些额外问题,但考虑到我对解析过程的了解非常有限,可能无法给出最佳答案。
1)这真的是R中唯一表现出这种行为的对象吗?(我想起了John Chambers通过Hadley的书所引用的话:“存在的每一件事都是一个对象。发生的每一件事都是一个函数调用。”这显然超出了那个范畴——还有其他像这样的东西吗?
首先,我同意这超出了那个范畴。我认为 Chambers 的这句话涉及 R 环境,即在此低级解析阶段之后发生的所有过程。不过稍后我会更详细地讨论这个问题。无论如何,我能找到的唯一另一个展现这种行为的例子是 **
运算符,它是更常见的指数运算符 ^
的同义词。和右赋值一样,解释器似乎没有将 **
视为函数调用等,
R> `->`
R> `**`
我发现这个是因为它是唯一一个被C解析器使用的install_and_save2的情况:
case '*':
if (nextchar('*')) {
yylval = install_and_save2("^", "**");
return '^';
} else
yylval = install_and_save("*");
return c;
2) 这是在什么时候发生的?我想到substitute(3 -> y)已经翻转了表达式;我无法从源代码中找出substitute是如何触发YACC的...
当然,我这里还在推测,但是是的,我认为我们可以安全地假设,当你调用substitute(3 -> y)
时,从替换函数的角度来看,表达式一直都是y <- 3
;例如,该函数完全不知道你输入了3 -> y
。像R使用的99%的C函数一样,do_substitute
只处理SEXP
参数——在3 -> y
(== y <- 3
)的情况下是EXPRSXP
。这就是我上面提到的R环境和解析过程之间的区别。我认为没有任何特定的触发器会引起解析器开始工作,而是所有你输入到解释器中的内容都会被解析。昨晚我对YACC/Bison解析器生成器进行了一些阅读,据我所知(也就是说,不要把所有赌注都压在这上面),Bison使用你定义的语法(在.y
文件中)来生成一个C解析器——即一个实际执行输入解析的C函数。反过来,你在R会话中输入的所有内容都首先由这个C解析函数处理,然后将适当的操作委托给R环境(顺便说一下,我非常宽泛地使用了这个术语)。在此阶段,lhs -> rhs
将被翻译为rhs <- lhs
,**
将被翻译为^
等等...例如,这是names.c中原始函数表的摘录:
{"if", do_if, 0, 200, -1, {PP_IF, PREC_FN, 1}},
{"while", do_while, 0, 100, 2, {PP_WHILE, PREC_FN, 0}},
{"for", do_for, 0, 100, 3, {PP_FOR, PREC_FN, 0}},
{"repeat", do_repeat, 0, 100, 1, {PP_REPEAT, PREC_FN, 0}},
{"break", do_break, CTXT_BREAK, 0, 0, {PP_BREAK, PREC_FN, 0}},
{"next", do_break, CTXT_NEXT, 0, 0, {PP_NEXT, PREC_FN, 0}},
{"return", do_return, 0, 0, -1, {PP_RETURN, PREC_FN, 0}},
{"function", do_function, 0, 0, -1, {PP_FUNCTION,PREC_FN, 0}},
{"<-", do_set, 1, 100, -1, {PP_ASSIGN, PREC_LEFT, 1}},
{"=", do_set, 3, 100, -1, {PP_ASSIGN, PREC_EQ, 1}},
{"<<-", do_set, 2, 100, -1, {PP_ASSIGN2, PREC_LEFT, 1}},
{"{", do_begin, 0, 200, -1, {PP_CURLY, PREC_FN, 0}},
{"(", do_paren, 0, 1, 1, {PP_PAREN, PREC_FN, 0}},
你会注意到,
->
、
->>
和
**
在这里没有定义。据我所知,R原始表达式如
<-
和
[
等是R环境与任何底层C代码之间最接近的交互。我的建议是,在这个过程的这个阶段(从你在解释器中输入一组字符并按下“Enter”键,直到实际评估有效的R表达式),解析器已经完成了它的工作,这就是为什么你不能通过用反引号将它们括起来来获得
->
或
**
的函数定义,就像你通常可以做的那样。