如何从过程调用中区分数组?
How do I tell an array from a procedure call?
上下文
我正在解析 vba 代码,其中...
此代码输出索引 i
处数组 a
第一维的内容:
Debug.Print a(i, 1)
此代码输出函数 a
给定参数 i
和 1
:
的结果
Debug.Print a(i, 1)
此代码调用过程 DoSomething
,同时将 foo
计算为值并将其按值传递给过程(无论签名是否将其作为 "by reference" 参数):
DoSomething (foo)
此代码调用过程 DoSomething
而不将 foo
计算为一个值,如果签名采用参数 "by reference":[=50,则通过引用传递它=]
Call DoSomething(foo)
所以我的 lExpression
解析器规则有问题,因为第一个选择 (#indexExpr
) 匹配数组和过程调用:
lExpression :
lExpression whiteSpace? LPAREN whiteSpace? argumentList? whiteSpace? RPAREN # indexExpr
| lExpression mandatoryLineContinuation? DOT mandatoryLineContinuation? unrestrictedIdentifier # memberAccessExpr
| lExpression mandatoryLineContinuation? EXCLAMATIONPOINT mandatoryLineContinuation? unrestrictedIdentifier # dictionaryAccessExpr
| ME # instanceExpr
| identifier # simpleNameExpr
| DOT mandatoryLineContinuation? unrestrictedIdentifier # withMemberAccessExpr
| EXCLAMATIONPOINT mandatoryLineContinuation? unrestrictedIdentifier # withDictionaryAccessExpr
;
问题
我试图在此处解决的具体问题,最好由堆栈跟踪描述我正在摆脱使用此代码抛出的解析异常:
Sub Test()
DoSomething (foo), bar
End Sub
我可以看到 callStmt()
规则正常启动,但是本来要匹配 DoSomething
的 expression
正在匹配 #lExpr
来捕获应该捕获的内容是 "argument list",但被拾取为数组索引。
我尝试过的一切,从将 #parenthesizedExpr
提高到比 #lExpr
更高的优先级,到制定 memberExpression
规则并使用它代替 expression
在 callStmt
规则中,已失败(项目构建,但我最终得到 1500 个失败的测试,因为不再解析)。
#lExpr
匹配 DoSomething (foo)
的具体原因是,那里有一个 indexExpr
是完全合法的——就好像我需要某种方法来忽略解析,但只有当我知道血统中有一个 callStmt
时。
甚至可以消除 a(i, 1)
(数组调用)与 a(i, 1)
(函数调用)的歧义吗?
如果是...怎么办?
其他上下文
这是从中调用 lExpression
规则的 expression
规则:
expression :
// Literal Expression has to come before lExpression, otherwise it'll be classified as simple name expression instead.
literalExpression # literalExpr
| lExpression # lExpr
| builtInType # builtInTypeExpr
| LPAREN whiteSpace? expression whiteSpace? RPAREN # parenthesizedExpr
| TYPEOF whiteSpace expression # typeofexpr // To make the grammar SLL, the type-of-is-expression is actually the child of an IS relational op.
| NEW whiteSpace expression # newExpr
| expression whiteSpace? POW whiteSpace? expression # powOp
| MINUS whiteSpace? expression # unaryMinusOp
| expression whiteSpace? (MULT | DIV) whiteSpace? expression # multOp
| expression whiteSpace? INTDIV whiteSpace? expression # intDivOp
| expression whiteSpace? MOD whiteSpace? expression # modOp
| expression whiteSpace? (PLUS | MINUS) whiteSpace? expression # addOp
| expression whiteSpace? AMPERSAND whiteSpace? expression # concatOp
| expression whiteSpace? (EQ | NEQ | LT | GT | LEQ | GEQ | LIKE | IS) whiteSpace? expression # relationalOp
| NOT whiteSpace? expression # logicalNotOp
| expression whiteSpace? AND whiteSpace? expression # logicalAndOp
| expression whiteSpace? OR whiteSpace? expression # logicalOrOp
| expression whiteSpace? XOR whiteSpace? expression # logicalXorOp
| expression whiteSpace? EQV whiteSpace? expression # logicalEqvOp
| expression whiteSpace? IMP whiteSpace? expression # logicalImpOp
| HASH expression # markedFileNumberExpr // Added to support special forms such as Input(file1, #file1)
;
和 callStmt
规则,这意味着只接收过程调用(可以或不可以以 Call
关键字开头):
callStmt :
CALL whiteSpace expression
| expression (whiteSpace argumentList)?
;
(我构建了 VB6/VBA 个解析器)。
不,你无法在 parse 时区分,正是因为函数调用和数组访问的语法是相同的,使用的是纯上下文无关的解析引擎。
简单的做法是简单地将构造解析为 array_access_or_function_call,并在通过对树进行后处理进行解析后消除歧义,发现实体的声明(例如构建符号 table) 其范围包含引用(查询符号 table),并使用它来决定。
这个问题不是 VB 独有的; C and C++ famously have a similar problem。大多数 C/C++ 解析器使用的解决方案是让解析器在解析时收集声明信息作为副作用,然后在遇到实例语法时参考该信息来决定。
这种方法将解析器更改为上下文相关的解析器。不利之处在于它会(至少是部分)符号 table 构建与解析纠缠在一起,并且您的解析引擎可能会或可能不会合作,这或多或少难以实现。
(我认为 ANTLR 会让你在解析过程中的不同点调用任意代码,可以用来保存声明信息,ANTLR 会让你调用解析时谓词来帮助指导解析器;这些应该是够了。
我更喜欢先解析后解析的方法,因为它更简洁且更易于维护。
您不能从过程调用中区分数组。即使在解决时间,您仍然不一定知道,因为变量的子类型可能会在 运行 时间后更改。
此示例显示了 默认 成员接受可选参数
的影响
Dim var As Variant
Set var = Range("A1:B2")
Debug.Print var(1, 1) 'Access the _Default/Item property with indice arguments
var = var 'Accesses the _Default/Item property without arguments
Debug.Print var(1, 1) 'Array indices
您甚至不能可靠地判断过程的结果是过程调用还是数组索引:
Dim var1 As Variant
Set var1 = New Dictionary
Dim var2 As Variant
Set var2 = New Dictionary
var2.Add 0, "Foo"
var1.Add 0, var2
Debug.Print var1(0)(0) 'Accesses the default/Item of the default/Item
var1 = Array(Array(1))
Debug.Print var1(0)(0) 'Accesses the first index of the first index
您需要将变量名后面带括号的块视为可能属于过程或数组。事实上,将访问数组成员视为 具有 默认 Item 成员甚至可能是有用的。这样,数组与具有默认成员的对象没有什么不同,后者需要恰好是索引的参数(并且恰好具有专用的构造函数语法)。
上下文
我正在解析 vba 代码,其中...
此代码输出索引
i
处数组a
第一维的内容:Debug.Print a(i, 1)
此代码输出函数
的结果a
给定参数i
和1
:Debug.Print a(i, 1)
此代码调用过程
DoSomething
,同时将foo
计算为值并将其按值传递给过程(无论签名是否将其作为 "by reference" 参数):DoSomething (foo)
此代码调用过程
DoSomething
而不将foo
计算为一个值,如果签名采用参数 "by reference":[=50,则通过引用传递它=]Call DoSomething(foo)
所以我的 lExpression
解析器规则有问题,因为第一个选择 (#indexExpr
) 匹配数组和过程调用:
lExpression :
lExpression whiteSpace? LPAREN whiteSpace? argumentList? whiteSpace? RPAREN # indexExpr
| lExpression mandatoryLineContinuation? DOT mandatoryLineContinuation? unrestrictedIdentifier # memberAccessExpr
| lExpression mandatoryLineContinuation? EXCLAMATIONPOINT mandatoryLineContinuation? unrestrictedIdentifier # dictionaryAccessExpr
| ME # instanceExpr
| identifier # simpleNameExpr
| DOT mandatoryLineContinuation? unrestrictedIdentifier # withMemberAccessExpr
| EXCLAMATIONPOINT mandatoryLineContinuation? unrestrictedIdentifier # withDictionaryAccessExpr
;
问题
我试图在此处解决的具体问题,最好由堆栈跟踪描述我正在摆脱使用此代码抛出的解析异常:
Sub Test()
DoSomething (foo), bar
End Sub
我可以看到 callStmt()
规则正常启动,但是本来要匹配 DoSomething
的 expression
正在匹配 #lExpr
来捕获应该捕获的内容是 "argument list",但被拾取为数组索引。
我尝试过的一切,从将 #parenthesizedExpr
提高到比 #lExpr
更高的优先级,到制定 memberExpression
规则并使用它代替 expression
在 callStmt
规则中,已失败(项目构建,但我最终得到 1500 个失败的测试,因为不再解析)。
#lExpr
匹配 DoSomething (foo)
的具体原因是,那里有一个 indexExpr
是完全合法的——就好像我需要某种方法来忽略解析,但只有当我知道血统中有一个 callStmt
时。
甚至可以消除 a(i, 1)
(数组调用)与 a(i, 1)
(函数调用)的歧义吗?
如果是...怎么办?
其他上下文
这是从中调用 lExpression
规则的 expression
规则:
expression :
// Literal Expression has to come before lExpression, otherwise it'll be classified as simple name expression instead.
literalExpression # literalExpr
| lExpression # lExpr
| builtInType # builtInTypeExpr
| LPAREN whiteSpace? expression whiteSpace? RPAREN # parenthesizedExpr
| TYPEOF whiteSpace expression # typeofexpr // To make the grammar SLL, the type-of-is-expression is actually the child of an IS relational op.
| NEW whiteSpace expression # newExpr
| expression whiteSpace? POW whiteSpace? expression # powOp
| MINUS whiteSpace? expression # unaryMinusOp
| expression whiteSpace? (MULT | DIV) whiteSpace? expression # multOp
| expression whiteSpace? INTDIV whiteSpace? expression # intDivOp
| expression whiteSpace? MOD whiteSpace? expression # modOp
| expression whiteSpace? (PLUS | MINUS) whiteSpace? expression # addOp
| expression whiteSpace? AMPERSAND whiteSpace? expression # concatOp
| expression whiteSpace? (EQ | NEQ | LT | GT | LEQ | GEQ | LIKE | IS) whiteSpace? expression # relationalOp
| NOT whiteSpace? expression # logicalNotOp
| expression whiteSpace? AND whiteSpace? expression # logicalAndOp
| expression whiteSpace? OR whiteSpace? expression # logicalOrOp
| expression whiteSpace? XOR whiteSpace? expression # logicalXorOp
| expression whiteSpace? EQV whiteSpace? expression # logicalEqvOp
| expression whiteSpace? IMP whiteSpace? expression # logicalImpOp
| HASH expression # markedFileNumberExpr // Added to support special forms such as Input(file1, #file1)
;
和 callStmt
规则,这意味着只接收过程调用(可以或不可以以 Call
关键字开头):
callStmt :
CALL whiteSpace expression
| expression (whiteSpace argumentList)?
;
(我构建了 VB6/VBA 个解析器)。
不,你无法在 parse 时区分,正是因为函数调用和数组访问的语法是相同的,使用的是纯上下文无关的解析引擎。
简单的做法是简单地将构造解析为 array_access_or_function_call,并在通过对树进行后处理进行解析后消除歧义,发现实体的声明(例如构建符号 table) 其范围包含引用(查询符号 table),并使用它来决定。
这个问题不是 VB 独有的; C and C++ famously have a similar problem。大多数 C/C++ 解析器使用的解决方案是让解析器在解析时收集声明信息作为副作用,然后在遇到实例语法时参考该信息来决定。
这种方法将解析器更改为上下文相关的解析器。不利之处在于它会(至少是部分)符号 table 构建与解析纠缠在一起,并且您的解析引擎可能会或可能不会合作,这或多或少难以实现。
(我认为 ANTLR 会让你在解析过程中的不同点调用任意代码,可以用来保存声明信息,ANTLR 会让你调用解析时谓词来帮助指导解析器;这些应该是够了。
我更喜欢先解析后解析的方法,因为它更简洁且更易于维护。
您不能从过程调用中区分数组。即使在解决时间,您仍然不一定知道,因为变量的子类型可能会在 运行 时间后更改。
此示例显示了 默认 成员接受可选参数
的影响 Dim var As Variant
Set var = Range("A1:B2")
Debug.Print var(1, 1) 'Access the _Default/Item property with indice arguments
var = var 'Accesses the _Default/Item property without arguments
Debug.Print var(1, 1) 'Array indices
您甚至不能可靠地判断过程的结果是过程调用还是数组索引:
Dim var1 As Variant
Set var1 = New Dictionary
Dim var2 As Variant
Set var2 = New Dictionary
var2.Add 0, "Foo"
var1.Add 0, var2
Debug.Print var1(0)(0) 'Accesses the default/Item of the default/Item
var1 = Array(Array(1))
Debug.Print var1(0)(0) 'Accesses the first index of the first index
您需要将变量名后面带括号的块视为可能属于过程或数组。事实上,将访问数组成员视为 具有 默认 Item 成员甚至可能是有用的。这样,数组与具有默认成员的对象没有什么不同,后者需要恰好是索引的参数(并且恰好具有专用的构造函数语法)。