[PATCH v13 0/1] MR10368: vbscript: Implement Eval, Execute, and ExecuteGlobal.
Eval compiles the string as an expression and returns the result. ExecuteGlobal compiles and executes the string at global scope. Execute compiles and executes the string in the calling scope. When called from within a Sub or Function, Eval and Execute use the calling scope for variable resolution, accessing local variables and arguments. When called from global scope, they fall back to exec_global_code. ExecuteGlobal always executes in global scope. The calling scope is passed through script_ctx_t.caller_exec, which exec_script picks up and stores on exec_ctx_t.caller. The interpreter's lookup_identifier checks the caller's locals, arguments, and dynamic vars when running FUNC_GLOBAL code with a caller context set. Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=49908 Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=53962 -- v13: vbscript: Implement Eval, Execute, and ExecuteGlobal. https://gitlab.winehq.org/wine/wine/-/merge_requests/10368
From: Francis De Brabandere <francisdb@gmail.com> Eval compiles the string as an expression and returns the result. ExecuteGlobal compiles and executes the string at global scope. Execute compiles and executes the string in the calling scope. When called from within a Sub or Function, Eval and Execute use the calling scope for variable resolution, accessing local variables and arguments. When called from global scope, they fall back to exec_global_code. ExecuteGlobal always executes in global scope. The calling scope is passed through script_ctx_t.caller_exec, which exec_script picks up and stores on exec_ctx_t.caller. The interpreter's lookup_identifier checks the caller's locals, arguments, and dynamic vars when running FUNC_GLOBAL code with a caller context set. Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=49908 Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=53962 --- dlls/vbscript/compile.c | 9 +++ dlls/vbscript/global.c | 87 +++++++++++++++++++++-- dlls/vbscript/interp.c | 39 ++++++++++ dlls/vbscript/tests/lang.vbs | 134 +++++++++++++++++++++++++++++++++++ dlls/vbscript/vbscript.c | 8 +-- dlls/vbscript/vbscript.h | 7 ++ 6 files changed, 274 insertions(+), 10 deletions(-) diff --git a/dlls/vbscript/compile.c b/dlls/vbscript/compile.c index 8c82d849a1b..f4e95b2c11a 100644 --- a/dlls/vbscript/compile.c +++ b/dlls/vbscript/compile.c @@ -63,6 +63,8 @@ typedef struct { function_t *func; function_decl_t *func_decls; + + DWORD flags; } compile_ctx_t; static HRESULT compile_expression(compile_ctx_t*,expression_t*); @@ -402,6 +404,12 @@ static HRESULT compile_error(script_ctx_t *ctx, compile_ctx_t *compiler, HRESULT ctx->ei.scode = error; ctx->ei.bstrSource = get_vbscript_string(VBS_COMPILE_ERROR); map_vbs_exception(&ctx->ei); + + /* For internal calls (Eval/Execute/ExecuteGlobal), don't report to OnScriptError. + * Just return SCRIPT_E_RECORDED to indicate ctx->ei is populated. */ + if(compiler->flags & SCRIPTTEXT_NOERRORREPORT) + return SCRIPT_E_RECORDED; + return report_script_error(ctx, compiler->code, compiler->loc, TRUE); } @@ -2103,6 +2111,7 @@ HRESULT compile_script(script_ctx_t *script, const WCHAR *src, const WCHAR *item } memset(&ctx, 0, sizeof(ctx)); + ctx.flags = flags; code = ctx.code = alloc_vbscode(&ctx, src, cookie, start_line); if(!ctx.code) return E_OUTOFMEMORY; diff --git a/dlls/vbscript/global.c b/dlls/vbscript/global.c index 1d244b8207f..f998e4c36d6 100644 --- a/dlls/vbscript/global.c +++ b/dlls/vbscript/global.c @@ -3333,20 +3333,95 @@ static HRESULT Global_Unescape(BuiltinDisp *This, VARIANT *arg, unsigned args_cn static HRESULT Global_Eval(BuiltinDisp *This, VARIANT *arg, unsigned args_cnt, VARIANT *res) { - FIXME("\n"); - return E_NOTIMPL; + vbscode_t *code; + HRESULT hres; + + TRACE("%s\n", debugstr_variant(arg)); + + if(V_VT(arg) != VT_BSTR) { + if(res) + return VariantCopy(res, arg); + return S_OK; + } + + hres = compile_script(This->ctx, V_BSTR(arg), NULL, NULL, 0, 0, + SCRIPTTEXT_ISEXPRESSION | SCRIPTTEXT_NOERRORREPORT, &code); + if(FAILED(hres)) { + /* Compilation failed. If compile_error didn't set up a proper VBS error, + * fall back to VBSE_SYNTAX_ERROR. */ + if(HRESULT_FACILITY(This->ctx->ei.scode) != FACILITY_VBS) { + clear_ei(&This->ctx->ei); + This->ctx->ei.scode = MAKE_VBSERROR(VBSE_SYNTAX_ERROR); + This->ctx->ei.bstrSource = get_vbscript_string(VBS_COMPILE_ERROR); + This->ctx->ei.bstrDescription = get_vbscript_string(VBSE_SYNTAX_ERROR); + } + return SCRIPT_E_RECORDED; + } + + if(is_exec_local_scope(This->ctx->current_exec)) { + This->ctx->caller_exec = This->ctx->current_exec; + return exec_script(This->ctx, FALSE, &code->main_code, NULL, NULL, res); + } + + return exec_global_code(This->ctx, code, res, FALSE); } static HRESULT Global_Execute(BuiltinDisp *This, VARIANT *arg, unsigned args_cnt, VARIANT *res) { - FIXME("\n"); - return E_NOTIMPL; + vbscode_t *code; + HRESULT hres; + + TRACE("%s\n", debugstr_variant(arg)); + + if(V_VT(arg) != VT_BSTR) + return MAKE_VBSERROR(VBSE_TYPE_MISMATCH); + + hres = compile_script(This->ctx, V_BSTR(arg), NULL, NULL, 0, 0, SCRIPTTEXT_NOERRORREPORT, &code); + if(FAILED(hres)) { + /* Compilation failed. If compile_error didn't set up a proper VBS error, + * fall back to VBSE_SYNTAX_ERROR. */ + if(HRESULT_FACILITY(This->ctx->ei.scode) != FACILITY_VBS) { + clear_ei(&This->ctx->ei); + This->ctx->ei.scode = MAKE_VBSERROR(VBSE_SYNTAX_ERROR); + This->ctx->ei.bstrSource = get_vbscript_string(VBS_COMPILE_ERROR); + This->ctx->ei.bstrDescription = get_vbscript_string(VBSE_SYNTAX_ERROR); + } + return SCRIPT_E_RECORDED; + } + + if(is_exec_local_scope(This->ctx->current_exec)) { + This->ctx->caller_exec = This->ctx->current_exec; + return exec_script(This->ctx, FALSE, &code->main_code, NULL, NULL, res); + } + + return exec_global_code(This->ctx, code, res, FALSE); } + static HRESULT Global_ExecuteGlobal(BuiltinDisp *This, VARIANT *arg, unsigned args_cnt, VARIANT *res) { - FIXME("\n"); - return E_NOTIMPL; + vbscode_t *code; + HRESULT hres; + + TRACE("%s\n", debugstr_variant(arg)); + + if(V_VT(arg) != VT_BSTR) + return MAKE_VBSERROR(VBSE_TYPE_MISMATCH); + + hres = compile_script(This->ctx, V_BSTR(arg), NULL, NULL, 0, 0, SCRIPTTEXT_NOERRORREPORT, &code); + if(FAILED(hres)) { + /* Compilation failed. If compile_error didn't set up a proper VBS error, + * fall back to VBSE_SYNTAX_ERROR. */ + if(HRESULT_FACILITY(This->ctx->ei.scode) != FACILITY_VBS) { + clear_ei(&This->ctx->ei); + This->ctx->ei.scode = MAKE_VBSERROR(VBSE_SYNTAX_ERROR); + This->ctx->ei.bstrSource = get_vbscript_string(VBS_COMPILE_ERROR); + This->ctx->ei.bstrDescription = get_vbscript_string(VBSE_SYNTAX_ERROR); + } + return SCRIPT_E_RECORDED; + } + + return exec_global_code(This->ctx, code, res, FALSE); } static HRESULT Global_GetRef(BuiltinDisp *This, VARIANT *arg, unsigned args_cnt, VARIANT *res) diff --git a/dlls/vbscript/interp.c b/dlls/vbscript/interp.c index d8f0f80dfaa..7ec6b36327f 100644 --- a/dlls/vbscript/interp.c +++ b/dlls/vbscript/interp.c @@ -47,6 +47,8 @@ typedef struct { VARIANT *stack; VARIANT ret_val; + + void *caller; } exec_ctx_t; typedef HRESULT (*instr_func_t)(exec_ctx_t*); @@ -182,6 +184,29 @@ static HRESULT lookup_identifier(exec_ctx_t *ctx, BSTR name, vbdisp_invoke_type_ } } + if(ctx->func->type == FUNC_GLOBAL && ctx->caller) { + exec_ctx_t *caller = ctx->caller; + + for(i=0; i < caller->func->var_cnt; i++) { + if(!wcsicmp(caller->func->vars[i].name, name)) { + ref->type = REF_VAR; + ref->u.v = caller->vars+i; + return S_OK; + } + } + + for(i=0; i < caller->func->arg_cnt; i++) { + if(!wcsicmp(caller->func->args[i].name, name)) { + ref->type = REF_VAR; + ref->u.v = caller->args+i; + return S_OK; + } + } + + if(lookup_dynamic_vars(caller->dynamic_vars, name, ref)) + return S_OK; + } + if(ctx->code->named_item) { if(lookup_global_vars(ctx->code->named_item->script_obj, name, ref)) return S_OK; @@ -2488,14 +2513,23 @@ static void release_exec(exec_ctx_t *ctx) free(ctx->stack); } +BOOL is_exec_local_scope(void *exec) +{ + exec_ctx_t *ctx = exec; + return ctx && ctx->func->type != FUNC_GLOBAL; +} + HRESULT exec_script(script_ctx_t *ctx, BOOL extern_caller, function_t *func, vbdisp_t *vbthis, DISPPARAMS *dp, VARIANT *res) { exec_ctx_t exec = {func->code_ctx}; named_item_t *prev_named_item; + void *prev_exec; vbsop_t op; HRESULT hres = S_OK; exec.code = func->code_ctx; + exec.caller = ctx->caller_exec; + ctx->caller_exec = NULL; if(dp ? func->arg_cnt != arg_cnt(dp) : func->arg_cnt) { FIXME("wrong arg_cnt %d, expected %d\n", dp ? arg_cnt(dp) : 0, func->arg_cnt); @@ -2568,6 +2602,9 @@ HRESULT exec_script(script_ctx_t *ctx, BOOL extern_caller, function_t *func, vbd prev_named_item = ctx->current_named_item; ctx->current_named_item = exec.code->named_item; + prev_exec = ctx->current_exec; + ctx->current_exec = &exec; + while(exec.instr) { op = exec.instr->op; hres = op_funcs[op](&exec); @@ -2628,6 +2665,8 @@ HRESULT exec_script(script_ctx_t *ctx, BOOL extern_caller, function_t *func, vbd exec.instr += op_move[op]; } + ctx->current_exec = prev_exec; + assert(!exec.top); if(extern_caller) { diff --git a/dlls/vbscript/tests/lang.vbs b/dlls/vbscript/tests/lang.vbs index c0d22606ba6..7b7d1402a16 100644 --- a/dlls/vbscript/tests/lang.vbs +++ b/dlls/vbscript/tests/lang.vbs @@ -2616,6 +2616,140 @@ Err.Clear Set getRefRef = GetRef(vbNullString) Call ok(Err.Number = 5, "GetRef vbNullString error is " & Err.Number) +' Eval tests +Call ok(Eval("1 + 2") = 3, "Eval(""1 + 2"") = " & Eval("1 + 2")) +Call ok(Eval("""test""") = "test", "Eval(""""""test"""""") = " & Eval("""test""")) +Call ok(Eval("true") = true, "Eval(""true"") = " & Eval("true")) + +x = 5 +Call ok(Eval("x + 1") = 6, "Eval(""x + 1"") = " & Eval("x + 1")) +Call ok(Eval("x * x") = 25, "Eval(""x * x"") = " & Eval("x * x")) + +Sub TestEvalLocalScope + Dim a + a = 10 + Call ok(Eval("a") = 10, "Eval(""a"") in local scope = " & Eval("a")) + Call ok(Eval("a + 5") = 15, "Eval(""a + 5"") in local scope = " & Eval("a + 5")) +End Sub +Call TestEvalLocalScope + +Function TestEvalLocalArgs(x) + TestEvalLocalArgs = Eval("x * 2") +End Function +Call ok(TestEvalLocalArgs(7) = 14, "TestEvalLocalArgs(7) = " & TestEvalLocalArgs(7)) + +Dim evalLocal +Sub TestEvalNoLeak + Dim evalLocal + evalLocal = 77 + Call ok(Eval("evalLocal") = 77, "Eval evalLocal = " & Eval("evalLocal")) +End Sub +Call TestEvalNoLeak +Call ok(getVT(evalLocal) = "VT_EMPTY*", "evalLocal leaked from Eval scope: " & getVT(evalLocal)) + +' ExecuteGlobal tests +x = 0 +ExecuteGlobal "x = 42" +Call ok(x = 42, "ExecuteGlobal x = " & x) + +ExecuteGlobal "Function evalTestFunc : evalTestFunc = 7 : End Function" +Call ok(evalTestFunc() = 7, "evalTestFunc() = " & evalTestFunc()) + +' Execute tests +x = 0 +Execute "x = 99" +Call ok(x = 99, "Execute x = " & x) + +Sub TestExecuteLocalScope + Dim a + a = 10 + Execute "a = 20" + Call ok(a = 20, "Execute local assign: a = " & a) +End Sub +Call TestExecuteLocalScope + +Dim executeLocal +Sub TestExecuteNoLeak + Dim executeLocal + executeLocal = 10 + Execute "executeLocal = 55" + Call ok(executeLocal = 55, "executeLocal = " & executeLocal) +End Sub +Call TestExecuteNoLeak +Call ok(getVT(executeLocal) = "VT_EMPTY*", "executeLocal leaked from Execute scope: " & getVT(executeLocal)) + +' Eval/Execute/ExecuteGlobal error handling tests +On Error Resume Next + +' Test Eval with syntax error +Err.Clear +Call Eval("1 + ") +Call ok(Err.Number = 1002, "Eval syntax error: Err.Number = " & Err.Number & " expected 1002") +Call ok(Err.Description = "Syntax error", "Eval syntax error: Err.Description = """ & Err.Description & """ expected ""Syntax error""") +Call ok(Err.Source = "Microsoft VBScript compilation error", "Eval syntax error: Err.Source = """ & Err.Source & """") + +' Test Execute with syntax error +Err.Clear +Execute "x = " +Call ok(Err.Number = 1002, "Execute syntax error: Err.Number = " & Err.Number & " expected 1002") +Call ok(Err.Description = "Syntax error", "Execute syntax error: Err.Description = """ & Err.Description & """ expected ""Syntax error""") +Call ok(Err.Source = "Microsoft VBScript compilation error", "Execute syntax error: Err.Source = """ & Err.Source & """") + +' Test ExecuteGlobal with syntax error +Err.Clear +ExecuteGlobal "y = " +Call ok(Err.Number = 1002, "ExecuteGlobal syntax error: Err.Number = " & Err.Number & " expected 1002") +Call ok(Err.Description = "Syntax error", "ExecuteGlobal syntax error: Err.Description = """ & Err.Description & """ expected ""Syntax error""") +Call ok(Err.Source = "Microsoft VBScript compilation error", "ExecuteGlobal syntax error: Err.Source = """ & Err.Source & """") + +' Eval with non-string arguments returns the value directly +Err.Clear +Call ok(Eval(123) = 123, "Eval(123) = " & Eval(123)) +Call ok(Err.Number = 0, "Eval(123) Err.Number = " & Err.Number) + +Err.Clear +Call ok(Eval(True) = True, "Eval(True) = " & Eval(True)) +Call ok(Err.Number = 0, "Eval(True) Err.Number = " & Err.Number) + +Err.Clear +Call ok(IsNull(Eval(Null)), "Eval(Null) should be Null") +Call ok(Err.Number = 0, "Eval(Null) Err.Number = " & Err.Number) + +Err.Clear +Call ok(IsEmpty(Eval(Empty)), "Eval(Empty) should be Empty") +Call ok(Err.Number = 0, "Eval(Empty) Err.Number = " & Err.Number) + +' Execute with non-string arguments returns type mismatch +Err.Clear +Execute 123 +Call ok(Err.Number = 13, "Execute 123 Err.Number = " & Err.Number) + +Err.Clear +Execute Null +Call ok(Err.Number = 13, "Execute Null Err.Number = " & Err.Number) + +Err.Clear +Execute Empty +Call ok(Err.Number = 13, "Execute Empty Err.Number = " & Err.Number) + +' ExecuteGlobal with non-string arguments returns type mismatch +Err.Clear +ExecuteGlobal 123 +Call ok(Err.Number = 13, "ExecuteGlobal 123 Err.Number = " & Err.Number) + +Err.Clear +ExecuteGlobal Null +Call ok(Err.Number = 13, "ExecuteGlobal Null Err.Number = " & Err.Number) + +Err.Clear +ExecuteGlobal Empty +Call ok(Err.Number = 13, "ExecuteGlobal Empty Err.Number = " & Err.Number) +' Test runtime error in Eval is caught by On Error Resume Next +Err.Clear +Call Eval("CBool(""notabool"")") +Call ok(Err.Number = 13, "Eval type mismatch: Err.Number = " & Err.Number & " expected 13") +Call ok(Err.Source = "Microsoft VBScript runtime error", "Eval type mismatch: Err.Source = """ & Err.Source & """") + On Error Goto 0 ' Test calling a dispatch variable as statement (invokes default property) diff --git a/dlls/vbscript/vbscript.c b/dlls/vbscript/vbscript.c index bc5a4ea416c..2c891401fff 100644 --- a/dlls/vbscript/vbscript.c +++ b/dlls/vbscript/vbscript.c @@ -93,7 +93,7 @@ static inline BOOL is_started(VBScript *This) || This->state == SCRIPTSTATE_DISCONNECTED; } -static HRESULT exec_global_code(script_ctx_t *ctx, vbscode_t *code, VARIANT *res) +HRESULT exec_global_code(script_ctx_t *ctx, vbscode_t *code, VARIANT *res, BOOL extern_caller) { ScriptDisp *obj = ctx->script_obj; function_t *func_iter, **new_funcs; @@ -191,7 +191,7 @@ static HRESULT exec_global_code(script_ctx_t *ctx, vbscode_t *code, VARIANT *res prev_caller = ctx->vbcaller->caller; ctx->vbcaller->caller = SP_CALLER_UNINITIALIZED; - hres = exec_script(ctx, TRUE, &code->main_code, NULL, NULL, res); + hres = exec_script(ctx, extern_caller, &code->main_code, NULL, NULL, res); ctx->vbcaller->caller = prev_caller; return hres; } @@ -202,7 +202,7 @@ static void exec_queued_code(script_ctx_t *ctx) LIST_FOR_EACH_ENTRY(iter, &ctx->code_list, vbscode_t, entry) { if(iter->pending_exec) - exec_global_code(ctx, iter, NULL); + exec_global_code(ctx, iter, NULL, TRUE); } } @@ -1099,7 +1099,7 @@ static HRESULT WINAPI VBScriptParse_ParseScriptText(IActiveScriptParse *iface, return S_OK; } - return exec_global_code(This->ctx, code, pvarResult); + return exec_global_code(This->ctx, code, pvarResult, TRUE); } static const IActiveScriptParseVtbl VBScriptParseVtbl = { diff --git a/dlls/vbscript/vbscript.h b/dlls/vbscript/vbscript.h index ced19f86bbc..2ee9e91b604 100644 --- a/dlls/vbscript/vbscript.h +++ b/dlls/vbscript/vbscript.h @@ -208,6 +208,9 @@ struct _script_ctx_t { BuiltinDisp *global_obj; BuiltinDisp *err_obj; + void *current_exec; + void *caller_exec; + EXCEPINFO ei; vbscode_t *error_loc_code; unsigned error_loc_offset; @@ -391,9 +394,13 @@ static inline void grab_vbscode(vbscode_t *code) } void release_vbscode(vbscode_t*); +/* Internal flag to suppress OnScriptError reporting - used by Eval/Execute/ExecuteGlobal */ +#define SCRIPTTEXT_NOERRORREPORT 0x80000000 HRESULT compile_script(script_ctx_t*,const WCHAR*,const WCHAR*,const WCHAR*,DWORD_PTR,unsigned,DWORD,vbscode_t**); HRESULT compile_procedure(script_ctx_t*,const WCHAR*,const WCHAR*,const WCHAR*,DWORD_PTR,unsigned,DWORD,class_desc_t**); HRESULT exec_script(script_ctx_t*,BOOL,function_t*,vbdisp_t*,DISPPARAMS*,VARIANT*); +HRESULT exec_global_code(script_ctx_t*,vbscode_t*,VARIANT*,BOOL); +BOOL is_exec_local_scope(void*); void release_dynamic_var(dynamic_var_t*); named_item_t *lookup_named_item(script_ctx_t*,const WCHAR*,unsigned); void release_named_item(named_item_t*); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10368
Jacek Caban (@jacek) commented about dlls/vbscript/compile.c:
ctx->ei.scode = error; ctx->ei.bstrSource = get_vbscript_string(VBS_COMPILE_ERROR); map_vbs_exception(&ctx->ei); + + /* For internal calls (Eval/Execute/ExecuteGlobal), don't report to OnScriptError. + * Just return SCRIPT_E_RECORDED to indicate ctx->ei is populated. */ + if(compiler->flags & SCRIPTTEXT_NOERRORREPORT) + return SCRIPT_E_RECORDED;
`SCRIPTTEXT_NOERRORREPORT` is a private flag mixed with public ones, which would be nice to avoid for clarity. In this particular case, I wonder if we could always record the location in `script_ctx_t` (with `error_loc_code` and `error_loc_offset`), always return `SCRIPT_E_RECORDED` and then have callers call `report_script_error` when appropriate. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10368#note_134363
Jacek Caban (@jacek) commented about dlls/vbscript/global.c:
+ if(res) + return VariantCopy(res, arg); + return S_OK; + } + + hres = compile_script(This->ctx, V_BSTR(arg), NULL, NULL, 0, 0, + SCRIPTTEXT_ISEXPRESSION | SCRIPTTEXT_NOERRORREPORT, &code); + if(FAILED(hres)) { + /* Compilation failed. If compile_error didn't set up a proper VBS error, + * fall back to VBSE_SYNTAX_ERROR. */ + if(HRESULT_FACILITY(This->ctx->ei.scode) != FACILITY_VBS) { + clear_ei(&This->ctx->ei); + This->ctx->ei.scode = MAKE_VBSERROR(VBSE_SYNTAX_ERROR); + This->ctx->ei.bstrSource = get_vbscript_string(VBS_COMPILE_ERROR); + This->ctx->ei.bstrDescription = get_vbscript_string(VBSE_SYNTAX_ERROR); + } Why do we need this? The compiler seems to record an error in all cases except for out-of-memory. There is a lot of room for improvements for more accurate error codes (as shown by your other MRs), but those belong to compiler/parser.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10368#note_134364
It would be nice to have tests that add new variable with `dim` inside eval/exec/execglobal calls. (If it's incomplete, that's fine, but it would be good to at least have a comment about that.) -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10368#note_134365
participants (3)
-
Francis De Brabandere -
Francis De Brabandere (@francisdb) -
Jacek Caban (@jacek)