From: Francis De Brabandere <francisdb@gmail.com> Alternative approach to fix/bug-54177 that moves the '(' vs tEXPRLBRACKET disambiguation into the parser grammar instead of using a lex lookahead. Not viable: LALR(1) cannot distinguish 'f(x) = y' as a property-put assignment from 'f(x) = y' as a Sub call whose argument is the equality expression '(x) = y' without 2-token lookahead. Produces 1 extra shift/reduce conflict and 4 test failures around DISPATCH_PROPERTYPUT semantics. --- dlls/vbscript/lex.c | 45 ++++++++- dlls/vbscript/parse.h | 2 + dlls/vbscript/parser.y | 9 +- dlls/vbscript/tests/lang.vbs | 178 +++++++++++++++++++++++++++++++++++ 4 files changed, 231 insertions(+), 3 deletions(-) diff --git a/dlls/vbscript/lex.c b/dlls/vbscript/lex.c index 81cc67e037b..42112ff08db 100644 --- a/dlls/vbscript/lex.c +++ b/dlls/vbscript/lex.c @@ -522,11 +522,17 @@ static int parse_next_token(void *lval, unsigned *loc, parser_ctx_t *ctx) } /* * Parser can't predict if bracket is part of argument expression or an argument - * in call expression. We predict it here instead. + * in call expression. At statement level (not inside another paren), an + * identifier followed by `(` always opens an expression group — VBScript + * disallows multi-arg paren calls of Subs without the Call keyword, and + * property-put assignment (`f(a, b) = c`) has its own grammar production. */ if(ctx->last_token == tIdentifier || ctx->last_token == ')' || ctx->last_token == tME - || ctx->last_token == tEMPTYBRACKETS) + || ctx->last_token == tEMPTYBRACKETS) { + if(ctx->is_statement_ctx && ctx->paren_depth == 0) + return tEXPRLBRACKET; return '('; + } return tEXPRLBRACKET; case '[': return parse_bracket_identifier(ctx, lval); @@ -609,5 +615,40 @@ int parser_lex(void *lval, unsigned *loc, parser_ctx_t *ctx) } ctx->after_continuation = FALSE; + + /* Track paren depth and update is_statement_ctx only at depth 0. + * Tokens inside parentheses (e.g. the 'idx' in 'obj(idx).method') + * must not affect statement context tracking. */ + if(ret == '(' || ret == tEXPRLBRACKET) + ctx->paren_depth++; + else if(ret == ')' || ret == tEMPTYBRACKETS) { + if(ctx->paren_depth > 0) + ctx->paren_depth--; + } + + if(ctx->paren_depth == 0) { + switch(ret) { + case tNL: + case ':': + case tTHEN: + case tELSE: + ctx->is_statement_ctx = TRUE; + break; + case tDOT: + case '.': + case ')': + case tEMPTYBRACKETS: + break; + case tIdentifier: + if(ctx->last_token != '.' && ctx->last_token != tDOT + && ctx->last_token != tNL && ctx->last_token != ':' + && ctx->last_token != tTHEN && ctx->last_token != tELSE) + ctx->is_statement_ctx = FALSE; + break; + default: + ctx->is_statement_ctx = FALSE; + break; + } + } return (ctx->last_token = ret); } diff --git a/dlls/vbscript/parse.h b/dlls/vbscript/parse.h index a4d5079d10e..a181f814c0f 100644 --- a/dlls/vbscript/parse.h +++ b/dlls/vbscript/parse.h @@ -309,6 +309,8 @@ typedef struct { LCID lcid; int last_token; + BOOL is_statement_ctx; + int paren_depth; unsigned last_nl; BOOL after_continuation; diff --git a/dlls/vbscript/parser.y b/dlls/vbscript/parser.y index e716bf98d3a..6c34aba504f 100644 --- a/dlls/vbscript/parser.y +++ b/dlls/vbscript/parser.y @@ -226,6 +226,9 @@ SimpleStatement | tCALL UnaryExpression { $$ = new_call_statement(ctx, @$, $2); CHECK_ERROR; } | CallExpression '=' Expression { $$ = new_assign_statement(ctx, @$, $1, $3); CHECK_ERROR; } + | CallExpression tEXPRLBRACKET ArgumentList ')' '=' Expression + { call_expression_t *ce = new_call_expression(ctx, $1, $3); CHECK_ERROR; + $$ = new_assign_statement(ctx, @$, &ce->expr, $6); CHECK_ERROR; } | tDIM DimDeclList { $$ = new_dim_statement(ctx, @$, $2); CHECK_ERROR; } | tREDIM Preserve_opt ReDimDeclList { $$ = new_redim_statement(ctx, @$, $2, $3); CHECK_ERROR; } | tERASE Identifier { $$ = new_erase_statement(ctx, @$, $2); CHECK_ERROR; } @@ -1335,14 +1338,18 @@ HRESULT parse_script(parser_ctx_t *ctx, const WCHAR *code, const WCHAR *delimite ctx->hres = S_OK; ctx->error_loc = -1; ctx->last_token = tNL; + ctx->is_statement_ctx = TRUE; + ctx->paren_depth = 0; ctx->last_nl = 0; ctx->stats = ctx->stats_tail = NULL; ctx->class_decls = NULL; ctx->option_explicit = FALSE; ctx->is_html = delimiter && !wcsicmp(delimiter, L"</script>"); - if(flags & SCRIPTTEXT_ISEXPRESSION) + if(flags & SCRIPTTEXT_ISEXPRESSION) { ctx->last_token = tEXPRESSION; + ctx->is_statement_ctx = FALSE; + } parser_parse(ctx); diff --git a/dlls/vbscript/tests/lang.vbs b/dlls/vbscript/tests/lang.vbs index d59f05c54a0..c598857553b 100644 --- a/dlls/vbscript/tests/lang.vbs +++ b/dlls/vbscript/tests/lang.vbs @@ -1217,6 +1217,184 @@ Call TestSubExit2 TestSubMultiArgs 1, 2, 3, 4, 5 Call TestSubMultiArgs(1, 2, 3, 4, 5) +Sub TestSubParenExpr(a, b) + Call ok(a=16, "a = " & a) + Call ok(b=7, "b = " & b) +End Sub + +TestSubParenExpr (2) * 8, 7 +TestSubParenExpr 8 * (2), 7 + +Sub TestSubParenExprAdd(a, b) + Call ok(a=6, "a = " & a) + Call ok(b=7, "b = " & b) +End Sub + +TestSubParenExprAdd (2) + 4, 7 +TestSubParenExprAdd 4 + (2), 7 + +Sub TestSubParenExprNoSpace(a) + Call ok(a=6, "a = " & a) +End Sub + +TestSubParenExprNoSpace(10 \ 2) + 1 + +' Regression test: function call with space before ( in expression context +' e.g. x = (CInt (2) + 1) * 3 must parse and evaluate correctly +x = CInt (2) + 1 +Call ok(x = 3, "CInt (2) + 1 = " & x) +x = (CInt (2) + 1) * 3 +Call ok(x = 9, "(CInt (2) + 1) * 3 = " & x) + +' Regression test: function call with space before ( and * in expression context +' e.g. x = CInt (2) * 3 must treat CInt (2) as a function call, not expression grouping +x = CInt (2) * 3 +Call ok(x = 6, "CInt (2) * 3 = " & x) + +' Test member expression in statement context: obj.Method (x) * y, z +Class TestObjParenExpr + Sub Check(a, b) + Call ok(a=16, "obj a = " & a) + Call ok(b=7, "obj b = " & b) + End Sub +End Class + +Dim objParenExpr +Set objParenExpr = New TestObjParenExpr +objParenExpr.Check (2) * 8, 7 + +Sub TestSubParenExprConcat(a, b) + Call ok(a="helloworld", "a = " & a) + Call ok(b=7, "b = " & b) +End Sub + +TestSubParenExprConcat ("hello") & "world", 7 + +' Test: function call as argument with & after paren must be a call, not grouping +' e.g. TestSub Mid ("hello", 2) & "x" should call TestSub with "ellox" +' Mid("hello", 2) returns "ello", & "x" concatenates to "ellox" +Sub TestSubArgCallConcat(a) + Call ok(a="ellox", "a = " & a) +End Sub + +TestSubArgCallConcat Mid ("hello", 2) & "x" + +' Test: obj(idx).method (expr) * val, y in statement context +' The (expr) after .method must be expression grouping, not call paren +Class TestIndexedObjParenExpr + Public arr_(1) + Public Sub Init() + Set arr_(0) = New TestObjParenExpr + End Sub + Public Default Property Get Item(idx) + Set Item = arr_(idx) + End Property +End Class + +Dim idxObj +Set idxObj = New TestIndexedObjParenExpr +Call idxObj.Init() +idxObj(0).Check (2) * 8, 7 + +' No-space variants of Sub-first-arg paren pattern. +' On native VBScript, `S(x) OP y` in statement context treats the whole +' `S(x) OP y` as a call to S with argument `(x) OP y` — for every binary +' operator except `=` (parsed as assignment). +' Each case is wrapped in Execute so parse failures of one don't abort the rest. +Dim npArg, npArgA, npArgB +Sub NpS(a) + npArg = a +End Sub +Sub NpT(a, b) + npArgA = a + npArgB = b +End Sub + +Sub CheckNpS(src, expected) + npArg = Empty + On Error Resume Next + Err.Clear + Execute src + Dim e : e = Err.Number + On Error GoTo 0 + Call ok(e = 0, "parse error for " & src & ": err=" & e) + If e = 0 Then Call ok(npArg = expected, src & ": npArg = " & npArg & " expected " & expected) +End Sub + +CheckNpS "NpS(10)+5", 15 +CheckNpS "NpS(10)-3", 7 +CheckNpS "NpS(10)*3", 30 +CheckNpS "NpS(10)/2", 5 +CheckNpS "NpS(10)\3", 3 +CheckNpS "NpS(10)^2", 100 +CheckNpS "NpS(""hi"")&""!""", "hi!" +CheckNpS "NpS(10) Mod 3", 1 +CheckNpS "NpS(10)<>10", False +CheckNpS "NpS(10)<5", False +CheckNpS "NpS(10)>5", True +CheckNpS "NpS(10)<=10", True +CheckNpS "NpS(10)>=5", True +CheckNpS "NpS(1) And 1", 1 +CheckNpS "NpS(0) Or 1", 1 +CheckNpS "NpS(1) Xor 1", 0 +CheckNpS "NpS(1) Eqv 1", -1 +CheckNpS "NpS(1) Imp 1", -1 +CheckNpS "NpS(Nothing) Is Nothing", True + +' Two-arg form: S(x) OP y, z — result of `(x) OP y` is first arg, z is second. +Sub CheckNpT(src, expectedA, expectedB) + npArgA = Empty + npArgB = Empty + On Error Resume Next + Err.Clear + Execute src + Dim e : e = Err.Number + On Error GoTo 0 + Call ok(e = 0, "parse error for " & src & ": err=" & e) + If e = 0 Then Call ok(npArgA = expectedA and npArgB = expectedB, _ + src & ": a=" & npArgA & " b=" & npArgB) +End Sub + +CheckNpT "NpT(10)+5, 7", 15, 7 +CheckNpT "NpT(10)*3, 7", 30, 7 +CheckNpT "NpT(""hi"")&""!"", 7", "hi!", 7 + +' Member expression: obj.Method(x) OP y — no space, same pattern. +Class NpCls + Sub Check(a) + npArg = a + End Sub +End Class +Dim npObj +Set npObj = New NpCls +CheckNpS "npObj.Check(10)+5", 15 +CheckNpS "npObj.Check(10)*3", 30 + +Function ParenId(a) + ParenId = a +End Function + +Dim parenRes +parenRes = 0 +If False Then +ElseIf ParenId(3) <= ParenId(4) + 0.1 Then + parenRes = 1 +End If +Call ok(parenRes = 1, "ElseIf f(x) <= f(y) + z: parenRes = " & parenRes) + +parenRes = 0 +If False Then +ElseIf ParenId(3) * 2 > 0 Then + parenRes = 1 +End If +Call ok(parenRes = 1, "ElseIf f(x) * y > z: parenRes = " & parenRes) + +Dim parenOuter, parenInner +ReDim parenOuter(3) +parenInner = Array(1, 3, 5, 7) +parenOuter(parenInner(1) And 1) = 99 +Call ok(parenOuter(1) = 99, "outer(inner(i) And k) = v: parenOuter(1) = " & parenOuter(1)) + Sub TestSubLocalVal x = false Call ok(not x, "local x is not false?") -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10691