[PATCH v17 0/9] MR10515: vbscript: Bind local variables, class properties, and loop variables at compile time.
Resolve identifiers to direct indices during compilation instead of doing runtime `wcsicmp` string scans in `lookup_identifier()`. This adds new opcodes that access local variables, function arguments, class properties, and loop variables by index, eliminating the linear string comparison overhead for the most common cases. **New opcodes:** - `OP_local` / `OP_assign_local` / `OP_set_local` - read/write local vars and args by index - `OP_local_prop` / `OP_assign_local_prop` / `OP_set_local_prop` - read/write class properties by index - `OP_step_local` / `OP_incc_local` - For-loop step/increment with indexed counter - `OP_enumnext_local` - For-Each iteration with indexed loop variable This mirrors jscript's `OP_local` / `bind_local()` optimization (`dlls/jscript/compile.c`) and addresses the FIXME in `lookup_identifier()` noting that class property access should be bound at compile time. Names that cannot be resolved at compile time (globals in Execute/ExecuteGlobal code, dynamic vars, host objects) continue to use the existing string-based path. The last commit also adds a fast path for SAFEARRAY iteration that bypasses the `IEnumVARIANT` COM vtable dispatch and copies elements directly from the array data, eliminating one intermediate `VariantCopy` per iteration. ### Local variable / argument / class property access (1M iterations, 10 reads per iteration) | Benchmark | Windows (VM) | Wine master | Wine (7 commits) | **Speedup** | vs Windows | |-----------|-------------|-------------|-------------------|---------|------------| | Local vars | 156 ms | 413 ms | 234 ms | **1.8x** | 1.5x slower | | Arguments | 171 ms | 357 ms | 231 ms | **1.5x** | 1.4x slower | | Class props | 265 ms | 393 ms | 226 ms | **1.7x** | 0.9x (faster!) | ### For-loop variants (10M iterations) | Benchmark | Windows (VM) | Wine master | Wine (7 commits) | **Speedup** | vs Windows | |-----------|-------------|-------------|-------------------|---------|------------| | Empty For | 117 ms | 370 ms | 270 ms | **1.4x** | 2.3x slower | | For + assign | 203 ms | 613 ms | 430 ms | **1.4x** | 2.1x slower | | Do-While manual | 437 ms | 646 ms | 513 ms | **1.3x** | 1.2x slower | | For Step 2 | 62 ms | 370 ms | 271 ms | **1.4x** | 4.4x slower | | Nested For | 125 ms | 387 ms | 268 ms | **1.4x** | 2.1x slower | | For local Sub | 109 ms | 294 ms | 251 ms | **1.2x** | 2.3x slower | | For R8 counter | 203 ms | 2,812 ms | 2,529 ms | **1.1x** | 12x slower | | For-Each array | 7 ms | 286 ms | 62 ms | **4.6x** | 8.9x slower | The remaining 4-13x gap vs Windows in For-loops is from `VarAdd`/`VarCmp` going through full `VariantChangeTypeEx` machinery (including locale alloc/free per call). A separate MR ([!10528](https://gitlab.winehq.org/wine/wine/-/merge_requests/10529)) adds I2/I4 fast paths in oleaut32 that bring integer loops within 1.2-1.9x of Windows when combined with this branch. -- v17: vbscript: Fast-path For-Each iteration over SAFEARRAY. vbscript: Bind For-Each loop variable at compile time. vbscript: Bind global-scope Dim variables at compile time. vbscript: Bind class properties at compile time. vbscript: Bind For-loop variables at compile time. vbscript: Factor out do_for_step and do_incc from interp_step and interp_incc. https://gitlab.winehq.org/wine/wine/-/merge_requests/10515
From: Francis De Brabandere <francisdb@gmail.com> Resolve Dim variables and function arguments to direct indices during compilation, emitting OP_local instead of OP_ident. This eliminates runtime wcsicmp scans in lookup_identifier() for the most common case. This mirrors jscript's OP_local optimization (dlls/jscript/compile.c: bind_local / emit_identifier). --- dlls/vbscript/compile.c | 41 +++++++++++++++++++++++++++++++++++----- dlls/vbscript/interp.c | 21 ++++++++++++++++++++ dlls/vbscript/vbscript.h | 1 + 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/dlls/vbscript/compile.c b/dlls/vbscript/compile.c index 54314372a13..713834ee5e6 100644 --- a/dlls/vbscript/compile.c +++ b/dlls/vbscript/compile.c @@ -456,6 +456,34 @@ static BOOL lookup_dim_decls(compile_ctx_t *ctx, const WCHAR *name) return FALSE; } +/* Resolve an identifier to a local variable or argument index at compile time. + * Returns TRUE if bound, with *ret set to: non-negative = dim var index, negative = arg index (-1 = arg 0, etc.). + * This mirrors jscript's bind_local() / local_off() convention. */ +static BOOL bind_local(compile_ctx_t *ctx, const WCHAR *name, int *ret) +{ + dim_decl_t *dim_decl; + unsigned i; + + if(ctx->func->type == FUNC_GLOBAL) + return FALSE; + + for(dim_decl = ctx->dim_decls, i = 0; dim_decl; dim_decl = dim_decl->next, i++) { + if(!vbs_wcsicmp(dim_decl->name, name)) { + *ret = i; + return TRUE; + } + } + + for(i = 0; i < ctx->func->arg_cnt; i++) { + if(!vbs_wcsicmp(ctx->func->args[i].name, name)) { + *ret = -(int)i - 1; + return TRUE; + } + } + + return FALSE; +} + static HRESULT compile_args(compile_ctx_t *ctx, expression_t *args, unsigned *ret) { unsigned arg_cnt = 0; @@ -506,15 +534,18 @@ static HRESULT compile_member_call_expression(compile_ctx_t *ctx, member_express static HRESULT compile_member_expression(compile_ctx_t *ctx, member_expression_t *expr) { expression_t *const_expr; + int local_ref; if (expr->obj_expr) /* FIXME: we should probably have a dedicated opcode as well */ return compile_member_call_expression(ctx, expr, 0, TRUE); - if (!lookup_dim_decls(ctx, expr->identifier) && !lookup_args_name(ctx, expr->identifier)) { - const_expr = lookup_const_decls(ctx, expr->identifier, TRUE); - if(const_expr) - return compile_expression(ctx, const_expr); - } + if(bind_local(ctx, expr->identifier, &local_ref)) + return push_instr_int(ctx, OP_local, local_ref); + + const_expr = lookup_const_decls(ctx, expr->identifier, TRUE); + if(const_expr) + return compile_expression(ctx, const_expr); + return push_instr_bstr(ctx, OP_ident, expr->identifier); } diff --git a/dlls/vbscript/interp.c b/dlls/vbscript/interp.c index ba530fca38c..8804b13646b 100644 --- a/dlls/vbscript/interp.c +++ b/dlls/vbscript/interp.c @@ -873,6 +873,27 @@ static HRESULT interp_mcallv(exec_ctx_t *ctx) return do_mcall(ctx, NULL); } +static HRESULT interp_local(exec_ctx_t *ctx) +{ + const int arg = ctx->instr->arg1.lng; + VARIANT *v; + VARIANT r; + + if(arg < 0) + v = ctx->args - arg - 1; + else + v = ctx->vars + arg; + + TRACE("%s\n", debugstr_variant(v)); + + if(V_VT(v) == (VT_BYREF|VT_VARIANT)) + v = V_VARIANTREF(v); + + V_VT(&r) = VT_BYREF|VT_VARIANT; + V_BYREF(&r) = v; + return stack_push(ctx, &r); +} + static HRESULT interp_ident(exec_ctx_t *ctx) { BSTR identifier = ctx->instr->arg1.bstr; diff --git a/dlls/vbscript/vbscript.h b/dlls/vbscript/vbscript.h index 19c9260a342..353aab3a9e3 100644 --- a/dlls/vbscript/vbscript.h +++ b/dlls/vbscript/vbscript.h @@ -270,6 +270,7 @@ typedef enum { X(incc, 1, ARG_BSTR, 0) \ X(int, 1, ARG_INT, 0) \ X(is, 1, 0, 0) \ + X(local, 1, ARG_INT, 0) \ X(jmp, 0, ARG_ADDR, 0) \ X(jmp_false, 0, ARG_ADDR, 0) \ X(jmp_true, 0, ARG_ADDR, 0) \ -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10515
From: Francis De Brabandere <francisdb@gmail.com> Extract the REF_VAR body of assign_ident into a new assign_local_var helper so that future compile-time-bound local assignments can reuse it instead of duplicating the logic. No functional change. --- dlls/vbscript/interp.c | 86 +++++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/dlls/vbscript/interp.c b/dlls/vbscript/interp.c index 8804b13646b..af62c02680d 100644 --- a/dlls/vbscript/interp.c +++ b/dlls/vbscript/interp.c @@ -942,61 +942,63 @@ static HRESULT assign_value(exec_ctx_t *ctx, VARIANT *dst, VARIANT *src, WORD fl return S_OK; } -static HRESULT assign_ident(exec_ctx_t *ctx, BSTR name, WORD flags, DISPPARAMS *dp) +static HRESULT assign_local_var(exec_ctx_t *ctx, VARIANT *v, WORD flags, DISPPARAMS *dp) { - ref_t ref; HRESULT hres; - hres = lookup_identifier(ctx, name, VBDISP_LET, &ref); - if(FAILED(hres)) - return hres; + if(V_VT(v) == (VT_VARIANT|VT_BYREF)) + v = V_VARIANTREF(v); - switch(ref.type) { - case REF_VAR: { - VARIANT *v = ref.u.v; + if(arg_cnt(dp)) { + SAFEARRAY *array; - if(V_VT(v) == (VT_VARIANT|VT_BYREF)) - v = V_VARIANTREF(v); + if(V_VT(v) == VT_DISPATCH) + return disp_propput(ctx->script, V_DISPATCH(v), DISPID_VALUE, flags, dp); - if(arg_cnt(dp)) { - SAFEARRAY *array; + if(!(V_VT(v) & VT_ARRAY)) + return DISP_E_TYPEMISMATCH; - if(V_VT(v) == VT_DISPATCH) { - hres = disp_propput(ctx->script, V_DISPATCH(v), DISPID_VALUE, flags, dp); - break; - } + switch(V_VT(v)) { + case VT_ARRAY|VT_BYREF|VT_VARIANT: + array = *V_ARRAYREF(v); + break; + case VT_ARRAY|VT_VARIANT: + array = V_ARRAY(v); + break; + default: + FIXME("Unsupported array type %x\n", V_VT(v)); + return E_NOTIMPL; + } - if(!(V_VT(v) & VT_ARRAY)) - return DISP_E_TYPEMISMATCH; + if(!array) { + WARN("null array\n"); + return MAKE_VBSERROR(VBSE_OUT_OF_BOUNDS); + } - switch(V_VT(v)) { - case VT_ARRAY|VT_BYREF|VT_VARIANT: - array = *V_ARRAYREF(v); - break; - case VT_ARRAY|VT_VARIANT: - array = V_ARRAY(v); - break; - default: - FIXME("Unsupported array type %x\n", V_VT(v)); - return E_NOTIMPL; - } + hres = array_access(array, dp, &v); + if(FAILED(hres)) + return hres; + }else if(V_VT(v) == (VT_ARRAY|VT_BYREF|VT_VARIANT)) { + FIXME("non-array assign\n"); + return E_NOTIMPL; + } - if(!array) { - WARN("null array\n"); - return MAKE_VBSERROR(VBSE_OUT_OF_BOUNDS); - } + return assign_value(ctx, v, dp->rgvarg, flags); +} - hres = array_access(array, dp, &v); - if(FAILED(hres)) - return hres; - }else if(V_VT(v) == (VT_ARRAY|VT_BYREF|VT_VARIANT)) { - FIXME("non-array assign\n"); - return E_NOTIMPL; - } +static HRESULT assign_ident(exec_ctx_t *ctx, BSTR name, WORD flags, DISPPARAMS *dp) +{ + ref_t ref; + HRESULT hres; + + hres = lookup_identifier(ctx, name, VBDISP_LET, &ref); + if(FAILED(hres)) + return hres; - hres = assign_value(ctx, v, dp->rgvarg, flags); + switch(ref.type) { + case REF_VAR: + hres = assign_local_var(ctx, ref.u.v, flags, dp); break; - } case REF_DISP: hres = disp_propput(ctx->script, ref.u.d.disp, ref.u.d.id, flags, dp); break; -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10515
From: Francis De Brabandere <francisdb@gmail.com> Emit OP_assign_local and OP_set_local instead of OP_assign_ident and OP_set_ident when the assignment target is a known local variable or function argument. This eliminates runtime lookup_identifier() calls for the most common assignment pattern. This mirrors jscript's OP_local optimization for identifier writes. --- dlls/vbscript/compile.c | 28 ++++++++++++++++++++++++ dlls/vbscript/interp.c | 46 ++++++++++++++++++++++++++++++++++++++++ dlls/vbscript/vbscript.h | 2 ++ 3 files changed, 76 insertions(+) diff --git a/dlls/vbscript/compile.c b/dlls/vbscript/compile.c index 713834ee5e6..890d024a276 100644 --- a/dlls/vbscript/compile.c +++ b/dlls/vbscript/compile.c @@ -192,6 +192,19 @@ static HRESULT push_instr_uint(compile_ctx_t *ctx, vbsop_t op, unsigned arg) return S_OK; } +static HRESULT push_instr_int_uint(compile_ctx_t *ctx, vbsop_t op, LONG arg1, unsigned arg2) +{ + unsigned ret; + + ret = push_instr(ctx, op); + if(!ret) + return E_OUTOFMEMORY; + + instr_ptr(ctx, ret)->arg1.lng = arg1; + instr_ptr(ctx, ret)->arg2.uint = arg2; + return S_OK; +} + static HRESULT push_instr_addr(compile_ctx_t *ctx, vbsop_t op, unsigned arg) { unsigned ret; @@ -1199,6 +1212,21 @@ static HRESULT compile_assignment(compile_ctx_t *ctx, expression_t *left, expres return hres; } + if(!member_expr->obj_expr) { + int local_ref; + if(bind_local(ctx, member_expr->identifier, &local_ref)) { + hres = push_instr_int_uint(ctx, is_set ? OP_set_local : OP_assign_local, + local_ref, args_cnt); + if(FAILED(hres)) + return hres; + + if(!emit_catch(ctx, 0)) + return E_OUTOFMEMORY; + + return S_OK; + } + } + hres = push_instr_bstr_uint(ctx, op, member_expr->identifier, args_cnt); if(FAILED(hres)) return hres; diff --git a/dlls/vbscript/interp.c b/dlls/vbscript/interp.c index af62c02680d..b87573674d7 100644 --- a/dlls/vbscript/interp.c +++ b/dlls/vbscript/interp.c @@ -1070,6 +1070,52 @@ static HRESULT interp_set_ident(exec_ctx_t *ctx) return S_OK; } +static HRESULT interp_assign_local(exec_ctx_t *ctx) +{ + const int ref = ctx->instr->arg1.lng; + const unsigned arg_cnt = ctx->instr->arg2.uint; + VARIANT *v; + DISPPARAMS dp; + HRESULT hres; + + v = ref < 0 ? ctx->args - ref - 1 : ctx->vars + ref; + + TRACE("%d\n", ref); + + vbstack_to_dp(ctx, arg_cnt, TRUE, &dp); + hres = assign_local_var(ctx, v, DISPATCH_PROPERTYPUT, &dp); + if(FAILED(hres)) + return hres; + + stack_popn(ctx, arg_cnt+1); + return S_OK; +} + +static HRESULT interp_set_local(exec_ctx_t *ctx) +{ + const int ref = ctx->instr->arg1.lng; + const unsigned arg_cnt = ctx->instr->arg2.uint; + VARIANT *v; + DISPPARAMS dp; + HRESULT hres; + + v = ref < 0 ? ctx->args - ref - 1 : ctx->vars + ref; + + TRACE("%d %u\n", ref, arg_cnt); + + hres = stack_assume_disp(ctx, arg_cnt, NULL); + if(FAILED(hres)) + return hres; + + vbstack_to_dp(ctx, arg_cnt, TRUE, &dp); + hres = assign_local_var(ctx, v, DISPATCH_PROPERTYPUTREF, &dp); + if(FAILED(hres)) + return hres; + + stack_popn(ctx, arg_cnt + 1); + return S_OK; +} + static HRESULT interp_assign_member(exec_ctx_t *ctx) { BSTR identifier = ctx->instr->arg1.bstr; diff --git a/dlls/vbscript/vbscript.h b/dlls/vbscript/vbscript.h index 353aab3a9e3..53fcb3bb518 100644 --- a/dlls/vbscript/vbscript.h +++ b/dlls/vbscript/vbscript.h @@ -241,6 +241,7 @@ typedef enum { X(add, 1, 0, 0) \ X(and, 1, 0, 0) \ X(assign_ident, 1, ARG_BSTR, ARG_UINT) \ + X(assign_local, 1, ARG_INT, ARG_UINT) \ X(assign_member, 1, ARG_BSTR, ARG_UINT) \ X(bool, 1, ARG_INT, 0) \ X(catch, 1, ARG_ADDR, ARG_UINT) \ @@ -296,6 +297,7 @@ typedef enum { X(ret, 0, 0, 0) \ X(retval, 1, 0, 0) \ X(set_ident, 1, ARG_BSTR, ARG_UINT) \ + X(set_local, 1, ARG_INT, ARG_UINT) \ X(set_member, 1, ARG_BSTR, ARG_UINT) \ X(stack, 1, ARG_UINT, 0) \ X(step, 0, ARG_ADDR, ARG_BSTR) \ -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10515
From: Francis De Brabandere <francisdb@gmail.com> Extract the common bodies so that future compile-time-bound For-loop operations on local variables can reuse them instead of duplicating the logic. No functional change. --- dlls/vbscript/interp.c | 84 ++++++++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 35 deletions(-) diff --git a/dlls/vbscript/interp.c b/dlls/vbscript/interp.c index b87573674d7..63e80705c96 100644 --- a/dlls/vbscript/interp.c +++ b/dlls/vbscript/interp.c @@ -1648,11 +1648,42 @@ static HRESULT interp_erase(exec_ctx_t *ctx) return hres; } -static HRESULT interp_step(exec_ctx_t *ctx) +static HRESULT do_for_step(exec_ctx_t *ctx, VARIANT *loop_var) { - const BSTR ident = ctx->instr->arg2.bstr; BOOL gteq_zero; VARIANT zero; + HRESULT hres; + + V_VT(&zero) = VT_I2; + V_I2(&zero) = 0; + hres = VarCmp(stack_top(ctx, 0), &zero, ctx->script->lcid, 0); + if(FAILED(hres)) + goto loop_not_initialized; + + gteq_zero = hres == VARCMP_GT || hres == VARCMP_EQ; + + hres = VarCmp(loop_var, stack_top(ctx, 1), ctx->script->lcid, 0); + if(FAILED(hres)) + goto loop_not_initialized; + + if(hres == VARCMP_EQ || hres == (gteq_zero ? VARCMP_LT : VARCMP_GT)) { + ctx->instr++; + }else { + stack_popn(ctx, 3); + instr_jmp(ctx, ctx->instr->arg1.uint); + } + return S_OK; + +loop_not_initialized: + WARN("For loop not initialized\n"); + stack_popn(ctx, 2); + instr_jmp(ctx, ctx->instr->arg1.uint); + return hres; +} + +static HRESULT interp_step(exec_ctx_t *ctx) +{ + const BSTR ident = ctx->instr->arg2.bstr; ref_t ref; HRESULT hres; @@ -1671,14 +1702,6 @@ static HRESULT interp_step(exec_ctx_t *ctx) return S_OK; } - V_VT(&zero) = VT_I2; - V_I2(&zero) = 0; - hres = VarCmp(stack_top(ctx, 0), &zero, ctx->script->lcid, 0); - if(FAILED(hres)) - goto loop_not_initialized; - - gteq_zero = hres == VARCMP_GT || hres == VARCMP_EQ; - hres = lookup_identifier(ctx, ident, VBDISP_ANY, &ref); if(FAILED(hres)) return hres; @@ -1688,23 +1711,7 @@ static HRESULT interp_step(exec_ctx_t *ctx) return E_FAIL; } - hres = VarCmp(ref.u.v, stack_top(ctx, 1), ctx->script->lcid, 0); - if(FAILED(hres)) - goto loop_not_initialized; - - if(hres == VARCMP_EQ || hres == (gteq_zero ? VARCMP_LT : VARCMP_GT)) { - ctx->instr++; - }else { - stack_popn(ctx, 3); - instr_jmp(ctx, ctx->instr->arg1.uint); - } - return S_OK; - -loop_not_initialized: - WARN("For loop not initialized\n"); - stack_popn(ctx, 2); - instr_jmp(ctx, ctx->instr->arg1.uint); - return hres; + return do_for_step(ctx, ref.u.v); } static HRESULT interp_newenum(exec_ctx_t *ctx) @@ -2608,10 +2615,23 @@ static HRESULT interp_neg(exec_ctx_t *ctx) return stack_push(ctx, &v); } +static HRESULT do_incc(exec_ctx_t *ctx, VARIANT *v) +{ + VARIANT tmp; + HRESULT hres; + + hres = VarAdd(stack_top(ctx, 0), v, &tmp); + if(FAILED(hres)) + return hres; + + VariantClear(v); + *v = tmp; + return S_OK; +} + static HRESULT interp_incc(exec_ctx_t *ctx) { const BSTR ident = ctx->instr->arg1.bstr; - VARIANT v; ref_t ref; HRESULT hres; @@ -2626,13 +2646,7 @@ static HRESULT interp_incc(exec_ctx_t *ctx) return E_FAIL; } - hres = VarAdd(stack_top(ctx, 0), ref.u.v, &v); - if(FAILED(hres)) - return hres; - - VariantClear(ref.u.v); - *ref.u.v = v; - return S_OK; + return do_incc(ctx, ref.u.v); } static HRESULT interp_with(exec_ctx_t *ctx) -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10515
From: Francis De Brabandere <francisdb@gmail.com> Emit OP_step_local and OP_incc_local instead of OP_step and OP_incc when the loop counter is a known local variable or function argument. This eliminates runtime lookup_identifier() calls on every loop iteration. This mirrors jscript's approach of resolving locals at compile time. --- dlls/vbscript/compile.c | 37 ++++++++++++++++++++++++++++--------- dlls/vbscript/interp.c | 31 +++++++++++++++++++++++++++++++ dlls/vbscript/vbscript.h | 2 ++ 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/dlls/vbscript/compile.c b/dlls/vbscript/compile.c index 890d024a276..f2abed5cc82 100644 --- a/dlls/vbscript/compile.c +++ b/dlls/vbscript/compile.c @@ -917,8 +917,12 @@ static HRESULT compile_forto_statement(compile_ctx_t *ctx, forto_statement_t *st statement_ctx_t loop_ctx = {3}; unsigned step_instr, instr, expr_err_label, past_err_label, body_label, from_offset; BSTR identifier; + int local_ref; + BOOL is_local; HRESULT hres; + is_local = bind_local(ctx, stat->identifier, &local_ref); + identifier = alloc_bstr_arg(ctx, stat->identifier); if(!identifier) return E_OUTOFMEMORY; @@ -987,10 +991,17 @@ static HRESULT compile_forto_statement(compile_ctx_t *ctx, forto_statement_t *st if(!loop_ctx.for_end_label) return E_OUTOFMEMORY; - step_instr = push_instr(ctx, OP_step); - if(!step_instr) - return E_OUTOFMEMORY; - instr_ptr(ctx, step_instr)->arg2.bstr = identifier; + if(is_local) { + step_instr = push_instr(ctx, OP_step_local); + if(!step_instr) + return E_OUTOFMEMORY; + instr_ptr(ctx, step_instr)->arg2.lng = local_ref; + }else { + step_instr = push_instr(ctx, OP_step); + if(!step_instr) + return E_OUTOFMEMORY; + instr_ptr(ctx, step_instr)->arg2.bstr = identifier; + } instr_ptr(ctx, step_instr)->arg1.uint = loop_ctx.for_end_label; if(!emit_catch(ctx, 3)) @@ -1002,11 +1013,19 @@ static HRESULT compile_forto_statement(compile_ctx_t *ctx, forto_statement_t *st if(FAILED(hres)) return hres; - - instr = push_instr(ctx, OP_incc); - if(!instr) - return E_OUTOFMEMORY; - instr_ptr(ctx, instr)->arg1.bstr = identifier; + /* We need a separated OP_step here so that errors jump to the end-of-loop catch. */ + ctx->loc = stat->stat.loc; + if(is_local) { + instr = push_instr(ctx, OP_incc_local); + if(!instr) + return E_OUTOFMEMORY; + instr_ptr(ctx, instr)->arg1.lng = local_ref; + }else { + instr = push_instr(ctx, OP_incc); + if(!instr) + return E_OUTOFMEMORY; + instr_ptr(ctx, instr)->arg1.bstr = identifier; + } instr = push_instr(ctx, OP_step); if(!instr) diff --git a/dlls/vbscript/interp.c b/dlls/vbscript/interp.c index 63e80705c96..d89904fb9d5 100644 --- a/dlls/vbscript/interp.c +++ b/dlls/vbscript/interp.c @@ -1714,6 +1714,28 @@ static HRESULT interp_step(exec_ctx_t *ctx) return do_for_step(ctx, ref.u.v); } +static HRESULT interp_step_local(exec_ctx_t *ctx) +{ + const int ref = ctx->instr->arg2.lng; + VARIANT *v; + + TRACE("%d\n", ref); + + v = ref < 0 ? ctx->args - ref - 1 : ctx->vars + ref; + + if(V_VT(stack_top(ctx, 0)) == VT_EMPTY && V_VT(stack_top(ctx, 1)) == VT_EMPTY) { + WARN("For loop not initialized\n"); + clear_ei(&ctx->script->ei); + ctx->script->ei.scode = MAKE_VBSERROR(VBSE_FOR_LOOP_NOT_INITIALIZED); + map_vbs_exception(&ctx->script->ei); + stack_popn(ctx, 3); + instr_jmp(ctx, ctx->instr->arg1.uint); + return S_OK; + } + + return do_for_step(ctx, v); +} + static HRESULT interp_newenum(exec_ctx_t *ctx) { variant_val_t v; @@ -2649,6 +2671,15 @@ static HRESULT interp_incc(exec_ctx_t *ctx) return do_incc(ctx, ref.u.v); } +static HRESULT interp_incc_local(exec_ctx_t *ctx) +{ + const int ref = ctx->instr->arg1.lng; + + TRACE("%d\n", ref); + + return do_incc(ctx, ref < 0 ? ctx->args - ref - 1 : ctx->vars + ref); +} + static HRESULT interp_with(exec_ctx_t *ctx) { VARIANT *v; diff --git a/dlls/vbscript/vbscript.h b/dlls/vbscript/vbscript.h index 53fcb3bb518..73c1841823d 100644 --- a/dlls/vbscript/vbscript.h +++ b/dlls/vbscript/vbscript.h @@ -269,6 +269,7 @@ typedef enum { X(idiv, 1, 0, 0) \ X(imp, 1, 0, 0) \ X(incc, 1, ARG_BSTR, 0) \ + X(incc_local, 1, ARG_INT, 0) \ X(int, 1, ARG_INT, 0) \ X(is, 1, 0, 0) \ X(local, 1, ARG_INT, 0) \ @@ -301,6 +302,7 @@ typedef enum { X(set_member, 1, ARG_BSTR, ARG_UINT) \ X(stack, 1, ARG_UINT, 0) \ X(step, 0, ARG_ADDR, ARG_BSTR) \ + X(step_local, 0, ARG_ADDR, ARG_INT) \ X(stop, 1, 0, 0) \ X(string, 1, ARG_STR, 0) \ X(sub, 1, 0, 0) \ -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10515
From: Francis De Brabandere <francisdb@gmail.com> Resolve class property names to direct indices during compilation of class methods, emitting OP_local_prop, OP_assign_local_prop, and OP_set_local_prop instead of the string-based OP_ident/OP_assign_ident/ OP_set_ident. This addresses the FIXME in lookup_identifier() that noted these should be bound while generating bytecode. This mirrors jscript's approach of resolving identifiers at compile time rather than doing runtime string comparisons. --- dlls/vbscript/compile.c | 54 +++++++++++++++++++++++++++++++++++++++ dlls/vbscript/interp.c | 55 ++++++++++++++++++++++++++++++++++++++++ dlls/vbscript/vbscript.h | 11 +++++--- 3 files changed, 116 insertions(+), 4 deletions(-) diff --git a/dlls/vbscript/compile.c b/dlls/vbscript/compile.c index f2abed5cc82..c969b9a0058 100644 --- a/dlls/vbscript/compile.c +++ b/dlls/vbscript/compile.c @@ -63,6 +63,7 @@ typedef struct { function_t *func; function_decl_t *func_decls; + dim_decl_t *class_props; } compile_ctx_t; static HRESULT compile_expression(compile_ctx_t*,expression_t*); @@ -205,6 +206,19 @@ static HRESULT push_instr_int_uint(compile_ctx_t *ctx, vbsop_t op, LONG arg1, un return S_OK; } +static HRESULT push_instr_uint_uint(compile_ctx_t *ctx, vbsop_t op, unsigned arg1, unsigned arg2) +{ + unsigned ret; + + ret = push_instr(ctx, op); + if(!ret) + return E_OUTOFMEMORY; + + instr_ptr(ctx, ret)->arg1.uint = arg1; + instr_ptr(ctx, ret)->arg2.uint = arg2; + return S_OK; +} + static HRESULT push_instr_addr(compile_ctx_t *ctx, vbsop_t op, unsigned arg) { unsigned ret; @@ -497,6 +511,26 @@ static BOOL bind_local(compile_ctx_t *ctx, const WCHAR *name, int *ret) return FALSE; } +/* Resolve an identifier to a class property index at compile time. + * Only valid when compiling inside a class method (ctx->class_props != NULL). */ +static BOOL bind_class_prop(compile_ctx_t *ctx, const WCHAR *name, unsigned *ret) +{ + dim_decl_t *prop; + unsigned i; + + if(!ctx->class_props) + return FALSE; + + for(prop = ctx->class_props, i = 0; prop; prop = prop->next, i++) { + if(!vbs_wcsicmp(prop->name, name)) { + *ret = i; + return TRUE; + } + } + + return FALSE; +} + static HRESULT compile_args(compile_ctx_t *ctx, expression_t *args, unsigned *ret) { unsigned arg_cnt = 0; @@ -547,6 +581,7 @@ static HRESULT compile_member_call_expression(compile_ctx_t *ctx, member_express static HRESULT compile_member_expression(compile_ctx_t *ctx, member_expression_t *expr) { expression_t *const_expr; + unsigned prop_ref; int local_ref; if (expr->obj_expr) /* FIXME: we should probably have a dedicated opcode as well */ @@ -555,6 +590,9 @@ static HRESULT compile_member_expression(compile_ctx_t *ctx, member_expression_t if(bind_local(ctx, expr->identifier, &local_ref)) return push_instr_int(ctx, OP_local, local_ref); + if(bind_class_prop(ctx, expr->identifier, &prop_ref)) + return push_instr_uint(ctx, OP_local_prop, prop_ref); + const_expr = lookup_const_decls(ctx, expr->identifier, TRUE); if(const_expr) return compile_expression(ctx, const_expr); @@ -1233,6 +1271,7 @@ static HRESULT compile_assignment(compile_ctx_t *ctx, expression_t *left, expres if(!member_expr->obj_expr) { int local_ref; + unsigned prop_ref; if(bind_local(ctx, member_expr->identifier, &local_ref)) { hres = push_instr_int_uint(ctx, is_set ? OP_set_local : OP_assign_local, local_ref, args_cnt); @@ -1244,6 +1283,17 @@ static HRESULT compile_assignment(compile_ctx_t *ctx, expression_t *left, expres return S_OK; } + if(bind_class_prop(ctx, member_expr->identifier, &prop_ref)) { + hres = push_instr_uint_uint(ctx, is_set ? OP_set_local_prop : OP_assign_local_prop, + prop_ref, args_cnt); + if(FAILED(hres)) + return hres; + + if(!emit_catch(ctx, 0)) + return E_OUTOFMEMORY; + + return S_OK; + } } hres = push_instr_bstr_uint(ctx, op, member_expr->identifier, args_cnt); @@ -2023,6 +2073,8 @@ static HRESULT compile_class(compile_ctx_t *ctx, class_decl_t *class_decl) return E_OUTOFMEMORY; memset(class_desc->funcs, 0, class_desc->func_cnt*sizeof(*class_desc->funcs)); + ctx->class_props = class_decl->props; + for(func_decl = class_decl->funcs, i=1; func_decl; func_decl = func_decl->next, i++) { for(func_prop_decl = func_decl; func_prop_decl; func_prop_decl = func_prop_decl->next_prop_func) { if(func_prop_decl->is_default) { @@ -2060,6 +2112,8 @@ static HRESULT compile_class(compile_ctx_t *ctx, class_decl_t *class_decl) return hres; } + ctx->class_props = NULL; + for(prop_decl = class_decl->props; prop_decl; prop_decl = prop_decl->next) class_desc->prop_cnt++; diff --git a/dlls/vbscript/interp.c b/dlls/vbscript/interp.c index d89904fb9d5..0196901573a 100644 --- a/dlls/vbscript/interp.c +++ b/dlls/vbscript/interp.c @@ -894,6 +894,21 @@ static HRESULT interp_local(exec_ctx_t *ctx) return stack_push(ctx, &r); } +static HRESULT interp_local_prop(exec_ctx_t *ctx) +{ + const unsigned arg = ctx->instr->arg1.uint; + VARIANT *v; + VARIANT r; + + v = ctx->vbthis->props + arg; + + TRACE("%s\n", debugstr_variant(v)); + + V_VT(&r) = VT_BYREF|VT_VARIANT; + V_BYREF(&r) = v; + return stack_push(ctx, &r); +} + static HRESULT interp_ident(exec_ctx_t *ctx) { BSTR identifier = ctx->instr->arg1.bstr; @@ -1116,6 +1131,46 @@ static HRESULT interp_set_local(exec_ctx_t *ctx) return S_OK; } +static HRESULT interp_assign_local_prop(exec_ctx_t *ctx) +{ + const unsigned ref = ctx->instr->arg1.uint; + const unsigned arg_cnt = ctx->instr->arg2.uint; + DISPPARAMS dp; + HRESULT hres; + + TRACE("%u\n", ref); + + vbstack_to_dp(ctx, arg_cnt, TRUE, &dp); + hres = assign_local_var(ctx, ctx->vbthis->props + ref, DISPATCH_PROPERTYPUT, &dp); + if(FAILED(hres)) + return hres; + + stack_popn(ctx, arg_cnt+1); + return S_OK; +} + +static HRESULT interp_set_local_prop(exec_ctx_t *ctx) +{ + const unsigned ref = ctx->instr->arg1.uint; + const unsigned arg_cnt = ctx->instr->arg2.uint; + DISPPARAMS dp; + HRESULT hres; + + TRACE("%u %u\n", ref, arg_cnt); + + hres = stack_assume_disp(ctx, arg_cnt, NULL); + if(FAILED(hres)) + return hres; + + vbstack_to_dp(ctx, arg_cnt, TRUE, &dp); + hres = assign_local_var(ctx, ctx->vbthis->props + ref, DISPATCH_PROPERTYPUTREF, &dp); + if(FAILED(hres)) + return hres; + + stack_popn(ctx, arg_cnt + 1); + return S_OK; +} + static HRESULT interp_assign_member(exec_ctx_t *ctx) { BSTR identifier = ctx->instr->arg1.bstr; diff --git a/dlls/vbscript/vbscript.h b/dlls/vbscript/vbscript.h index 73c1841823d..1bc3f788577 100644 --- a/dlls/vbscript/vbscript.h +++ b/dlls/vbscript/vbscript.h @@ -240,9 +240,10 @@ typedef enum { #define OP_LIST \ X(add, 1, 0, 0) \ X(and, 1, 0, 0) \ - X(assign_ident, 1, ARG_BSTR, ARG_UINT) \ - X(assign_local, 1, ARG_INT, ARG_UINT) \ - X(assign_member, 1, ARG_BSTR, ARG_UINT) \ + X(assign_ident, 1, ARG_BSTR, ARG_UINT) \ + X(assign_local, 1, ARG_INT, ARG_UINT) \ + X(assign_local_prop, 1, ARG_UINT, ARG_UINT) \ + X(assign_member, 1, ARG_BSTR, ARG_UINT) \ X(bool, 1, ARG_INT, 0) \ X(catch, 1, ARG_ADDR, ARG_UINT) \ X(case, 0, ARG_ADDR, 0) \ @@ -272,10 +273,11 @@ typedef enum { X(incc_local, 1, ARG_INT, 0) \ X(int, 1, ARG_INT, 0) \ X(is, 1, 0, 0) \ - X(local, 1, ARG_INT, 0) \ X(jmp, 0, ARG_ADDR, 0) \ X(jmp_false, 0, ARG_ADDR, 0) \ X(jmp_true, 0, ARG_ADDR, 0) \ + X(local, 1, ARG_INT, 0) \ + X(local_prop, 1, ARG_UINT, 0) \ X(lt, 1, 0, 0) \ X(lteq, 1, 0, 0) \ X(mcall, 1, ARG_BSTR, ARG_UINT) \ @@ -299,6 +301,7 @@ typedef enum { X(retval, 1, 0, 0) \ X(set_ident, 1, ARG_BSTR, ARG_UINT) \ X(set_local, 1, ARG_INT, ARG_UINT) \ + X(set_local_prop, 1, ARG_UINT, ARG_UINT) \ X(set_member, 1, ARG_BSTR, ARG_UINT) \ X(stack, 1, ARG_UINT, 0) \ X(step, 0, ARG_ADDR, ARG_BSTR) \ -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10515
From: Francis De Brabandere <francisdb@gmail.com> Extend compile-time identifier binding to global-scope code. Previously, bind_local() unconditionally skipped binding for FUNC_GLOBAL functions, forcing every global variable access through the string-based lookup_identifier() path at runtime. Now, for code compiled via ParseScriptText (skip_collisions=FALSE), global Dim variables are bound to indexed slots at compile time, emitting OP_local/OP_assign_local instead of OP_ident/OP_assign_ident. At execution time, exec_script() builds a var_refs[] indirection table that maps compile-time indices to their ScriptDisp->global_vars[] storage. This is done once per script block (O(vars * global_vars)), after exec_global_code() has already registered all Dim variables. Execute/ExecuteGlobal code (skip_collisions=TRUE) is not affected: it still skips binding in bind_local() because the destination of its Dim variables (ScriptDisp vs caller dynamic_vars) is not known at compile time. --- dlls/vbscript/compile.c | 14 ++++++++- dlls/vbscript/interp.c | 66 ++++++++++++++++++++++++++++++++++------- 2 files changed, 69 insertions(+), 11 deletions(-) diff --git a/dlls/vbscript/compile.c b/dlls/vbscript/compile.c index c969b9a0058..93a869e6af9 100644 --- a/dlls/vbscript/compile.c +++ b/dlls/vbscript/compile.c @@ -64,6 +64,13 @@ typedef struct { function_t *func; function_decl_t *func_decls; dim_decl_t *class_props; + + /* Set for Execute/ExecuteGlobal: (1) suppresses compile-time binding of + * global Dim variables in bind_local(), since they may end up in the + * caller's dynamic_vars rather than ScriptDisp->global_vars; (2) makes + * check_script_collisions() non-fatal so re-declaring an existing name + * is silently tolerated instead of raising VBSE_NAME_REDEFINED. */ + BOOL skip_collisions; } compile_ctx_t; static HRESULT compile_expression(compile_ctx_t*,expression_t*); @@ -491,7 +498,11 @@ static BOOL bind_local(compile_ctx_t *ctx, const WCHAR *name, int *ret) dim_decl_t *dim_decl; unsigned i; - if(ctx->func->type == FUNC_GLOBAL) + /* Execute/Eval code may run from either global or local scope. + * When run from local scope, Dim vars are placed in the caller's + * dynamic_vars, not in ScriptDisp->global_vars. We cannot bind + * them at compile time because we don't know which case it will be. */ + if(ctx->func->type == FUNC_GLOBAL && ctx->skip_collisions) return FALSE; for(dim_decl = ctx->dim_decls, i = 0; dim_decl; dim_decl = dim_decl->next, i++) { @@ -2325,6 +2336,7 @@ HRESULT compile_script(script_ctx_t *script, const WCHAR *src, const WCHAR *item } memset(&ctx, 0, sizeof(ctx)); + ctx.skip_collisions = skip_collisions; code = ctx.code = alloc_vbscode(&ctx, src, cookie, start_line); if(!ctx.code) return E_OUTOFMEMORY; diff --git a/dlls/vbscript/interp.c b/dlls/vbscript/interp.c index 0196901573a..ca662e4d877 100644 --- a/dlls/vbscript/interp.c +++ b/dlls/vbscript/interp.c @@ -35,6 +35,9 @@ typedef struct _exec_ctx_t { VARIANT *args; VARIANT *vars; + /* For global-scope code: maps compile-time var indices to their + * ScriptDisp->global_vars[] VARIANT storage. NULL for local scope. */ + VARIANT **var_refs; SAFEARRAY **arrays; dynamic_var_t *dynamic_vars; @@ -873,16 +876,20 @@ static HRESULT interp_mcallv(exec_ctx_t *ctx) return do_mcall(ctx, NULL); } +static inline VARIANT *get_local_var(exec_ctx_t *ctx, int ref) +{ + if(ref < 0) + return ctx->args - ref - 1; + return ctx->var_refs ? ctx->var_refs[ref] : ctx->vars + ref; +} + static HRESULT interp_local(exec_ctx_t *ctx) { const int arg = ctx->instr->arg1.lng; VARIANT *v; VARIANT r; - if(arg < 0) - v = ctx->args - arg - 1; - else - v = ctx->vars + arg; + v = get_local_var(ctx, arg); TRACE("%s\n", debugstr_variant(v)); @@ -1093,7 +1100,7 @@ static HRESULT interp_assign_local(exec_ctx_t *ctx) DISPPARAMS dp; HRESULT hres; - v = ref < 0 ? ctx->args - ref - 1 : ctx->vars + ref; + v = get_local_var(ctx, ref); TRACE("%d\n", ref); @@ -1114,7 +1121,7 @@ static HRESULT interp_set_local(exec_ctx_t *ctx) DISPPARAMS dp; HRESULT hres; - v = ref < 0 ? ctx->args - ref - 1 : ctx->vars + ref; + v = get_local_var(ctx, ref); TRACE("%d %u\n", ref, arg_cnt); @@ -1776,7 +1783,7 @@ static HRESULT interp_step_local(exec_ctx_t *ctx) TRACE("%d\n", ref); - v = ref < 0 ? ctx->args - ref - 1 : ctx->vars + ref; + v = get_local_var(ctx, ref); if(V_VT(stack_top(ctx, 0)) == VT_EMPTY && V_VT(stack_top(ctx, 1)) == VT_EMPTY) { WARN("For loop not initialized\n"); @@ -2732,7 +2739,7 @@ static HRESULT interp_incc_local(exec_ctx_t *ctx) TRACE("%d\n", ref); - return do_incc(ctx, ref < 0 ? ctx->args - ref - 1 : ctx->vars + ref); + return do_incc(ctx, get_local_var(ctx, ref)); } static HRESULT interp_with(exec_ctx_t *ctx) @@ -2793,8 +2800,16 @@ static void release_exec(exec_ctx_t *ctx) } if(ctx->vars) { - for(i=0; i < ctx->func->var_cnt; i++) - VariantClear(ctx->vars+i); + if(ctx->var_refs) { + /* Only clear fallback slots (vars not found in ScriptDisp) */ + for(i=0; i < ctx->func->var_cnt; i++) { + if(ctx->var_refs[i] == ctx->vars + i) + VariantClear(ctx->vars+i); + } + }else { + for(i=0; i < ctx->func->var_cnt; i++) + VariantClear(ctx->vars+i); + } } if(ctx->arrays) { @@ -2808,6 +2823,7 @@ static void release_exec(exec_ctx_t *ctx) heap_pool_free(&ctx->heap); free(ctx->args); free(ctx->vars); + free(ctx->var_refs); free(ctx->stack); } @@ -2921,6 +2937,36 @@ HRESULT exec_script(script_ctx_t *ctx, BOOL extern_caller, function_t *func, vbd exec.vars = NULL; } + /* For global-scope code (ParseScriptText), resolve compile-time variable + * indices to pointers into ScriptDisp->global_vars[]. This allows OP_local + * and OP_assign_local to access global Dim variables by index instead of + * doing string-based lookup_identifier() at runtime. exec_global_code() + * has already registered all Dim variables into global_vars before this. */ + if(func->type == FUNC_GLOBAL && !exec.caller && func->var_cnt) { + ScriptDisp *obj = func->code_ctx->named_item + ? func->code_ctx->named_item->script_obj : ctx->script_obj; + unsigned i, j; + + exec.var_refs = malloc(func->var_cnt * sizeof(VARIANT*)); + if(!exec.var_refs) { + release_exec(&exec); + return E_OUTOFMEMORY; + } + + for(i = 0; i < func->var_cnt; i++) { + exec.var_refs[i] = NULL; + for(j = 0; j < obj->global_vars_cnt; j++) { + if(!vbs_wcsicmp(func->vars[i].name, obj->global_vars[j]->name)) { + exec.var_refs[i] = &obj->global_vars[j]->v; + break; + } + } + /* If not found (e.g. shadowed by host), fall back to local storage */ + if(!exec.var_refs[i]) + exec.var_refs[i] = exec.vars + i; + } + } + exec.stack_size = 16; exec.top = 0; exec.stack = malloc(exec.stack_size * sizeof(VARIANT)); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10515
From: Francis De Brabandere <francisdb@gmail.com> When the loop variable in a For-Each statement can be resolved at compile time (local Dim variable or argument), emit OP_enumnext_local with the variable index instead of OP_enumnext with a string identifier. This bypasses the lookup_identifier() string scan on every iteration. --- dlls/vbscript/compile.c | 34 ++++++++++++++++++++++++------- dlls/vbscript/interp.c | 44 ++++++++++++++++++++++++++++++++++++++++ dlls/vbscript/vbscript.h | 1 + 3 files changed, 72 insertions(+), 7 deletions(-) diff --git a/dlls/vbscript/compile.c b/dlls/vbscript/compile.c index 93a869e6af9..0ab944d2718 100644 --- a/dlls/vbscript/compile.c +++ b/dlls/vbscript/compile.c @@ -914,9 +914,13 @@ static HRESULT compile_dowhile_statement(compile_ctx_t *ctx, while_statement_t * static HRESULT compile_foreach_statement(compile_ctx_t *ctx, foreach_statement_t *stat) { statement_ctx_t loop_ctx = {1}; - unsigned loop_start; + unsigned loop_start, instr; + int local_ref; + BOOL is_local; HRESULT hres; + is_local = bind_local(ctx, stat->identifier, &local_ref); + /* Preserve a place on the stack in case we throw before having proper enum collection. */ if(!push_instr(ctx, OP_empty)) return E_OUTOFMEMORY; @@ -931,9 +935,17 @@ static HRESULT compile_foreach_statement(compile_ctx_t *ctx, foreach_statement_t if(!(loop_ctx.for_end_label = alloc_label(ctx))) return E_OUTOFMEMORY; - hres = push_instr_uint_bstr(ctx, OP_enumnext, loop_ctx.for_end_label, stat->identifier); - if(FAILED(hres)) - return hres; + if(is_local) { + instr = push_instr(ctx, OP_enumnext_local); + if(!instr) + return E_OUTOFMEMORY; + instr_ptr(ctx, instr)->arg1.uint = loop_ctx.for_end_label; + instr_ptr(ctx, instr)->arg2.lng = local_ref; + }else { + hres = push_instr_uint_bstr(ctx, OP_enumnext, loop_ctx.for_end_label, stat->identifier); + if(FAILED(hres)) + return hres; + } if(!emit_catch(ctx, 1)) return E_OUTOFMEMORY; @@ -945,9 +957,17 @@ static HRESULT compile_foreach_statement(compile_ctx_t *ctx, foreach_statement_t /* We need a separated enumnext here, because we need to jump out of the loop on exception. */ ctx->loc = stat->stat.loc; - hres = push_instr_uint_bstr(ctx, OP_enumnext, loop_ctx.for_end_label, stat->identifier); - if(FAILED(hres)) - return hres; + if(is_local) { + instr = push_instr(ctx, OP_enumnext_local); + if(!instr) + return E_OUTOFMEMORY; + instr_ptr(ctx, instr)->arg1.uint = loop_ctx.for_end_label; + instr_ptr(ctx, instr)->arg2.lng = local_ref; + }else { + hres = push_instr_uint_bstr(ctx, OP_enumnext, loop_ctx.for_end_label, stat->identifier); + if(FAILED(hres)) + return hres; + } hres = push_instr_addr(ctx, OP_jmp, loop_start); if(FAILED(hres)) diff --git a/dlls/vbscript/interp.c b/dlls/vbscript/interp.c index ca662e4d877..f95db175035 100644 --- a/dlls/vbscript/interp.c +++ b/dlls/vbscript/interp.c @@ -1898,6 +1898,50 @@ static HRESULT interp_enumnext(exec_ctx_t *ctx) return S_OK; } +static HRESULT interp_enumnext_local(exec_ctx_t *ctx) +{ + const unsigned loop_end = ctx->instr->arg1.uint; + const int ref = ctx->instr->arg2.lng; + VARIANT v; + IEnumVARIANT *iter; + VARIANT *local_var; + BOOL do_continue; + HRESULT hres; + + TRACE("%d\n", ref); + + if(V_VT(stack_top(ctx, 0)) == VT_EMPTY) { + return MAKE_VBSERROR(VBSE_NOT_ENUM); + } + + assert(V_VT(stack_top(ctx, 0)) == VT_UNKNOWN); + iter = (IEnumVARIANT*)V_UNKNOWN(stack_top(ctx, 0)); + + V_VT(&v) = VT_EMPTY; + hres = IEnumVARIANT_Next(iter, 1, &v, NULL); + if(FAILED(hres)) + return hres; + + do_continue = hres == S_OK; + + local_var = get_local_var(ctx, ref); + if(V_VT(local_var) == (VT_VARIANT|VT_BYREF)) + local_var = V_VARIANTREF(local_var); + + hres = assign_value(ctx, local_var, &v, DISPATCH_PROPERTYPUT|DISPATCH_PROPERTYPUTREF); + VariantClear(&v); + if(FAILED(hres)) + return hres; + + if(do_continue) { + ctx->instr++; + }else { + stack_popn(ctx, 1); + instr_jmp(ctx, loop_end); + } + return S_OK; +} + static HRESULT interp_jmp(exec_ctx_t *ctx) { const unsigned arg = ctx->instr->arg1.uint; diff --git a/dlls/vbscript/vbscript.h b/dlls/vbscript/vbscript.h index 1bc3f788577..aed2ccaf5e5 100644 --- a/dlls/vbscript/vbscript.h +++ b/dlls/vbscript/vbscript.h @@ -256,6 +256,7 @@ typedef enum { X(double, 1, ARG_DOUBLE, 0) \ X(empty, 1, 0, 0) \ X(enumnext, 0, ARG_ADDR, ARG_BSTR) \ + X(enumnext_local, 0, ARG_ADDR, ARG_INT) \ X(equal, 1, 0, 0) \ X(hres, 1, ARG_UINT, 0) \ X(errmode, 1, ARG_INT, 0) \ -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10515
From: Francis De Brabandere <francisdb@gmail.com> When a For-Each loop iterates over a SAFEARRAY, bypass the IEnumVARIANT COM vtable dispatch and copy elements directly from the array data. This eliminates the COM call overhead and one intermediate VariantCopy per iteration. For non-refcounted element types (VT_I2, VT_I4, VT_R8, etc.), also skip VariantCopyInd and VariantClear entirely, replacing them with a direct struct assignment. Non-SAFEARRAY iterators (e.g. IDispatch collections) continue to use the standard IEnumVARIANT_Next path. For-Each over a 10M-element integer array: 6,339ms -> 58ms (109x faster). --- dlls/vbscript/interp.c | 38 +++++++++++++++++++------------ dlls/vbscript/utils.c | 49 ++++++++++++++++++++++++++++++++++++++++ dlls/vbscript/vbscript.h | 1 + 3 files changed, 74 insertions(+), 14 deletions(-) diff --git a/dlls/vbscript/interp.c b/dlls/vbscript/interp.c index f95db175035..61f4b7a4112 100644 --- a/dlls/vbscript/interp.c +++ b/dlls/vbscript/interp.c @@ -1879,11 +1879,16 @@ static HRESULT interp_enumnext(exec_ctx_t *ctx) iter = (IEnumVARIANT*)V_UNKNOWN(stack_top(ctx, 0)); V_VT(&v) = VT_EMPTY; - hres = IEnumVARIANT_Next(iter, 1, &v, NULL); - if(FAILED(hres)) + hres = safearray_iter_next(iter, &v, &do_continue); + if(hres == E_UNEXPECTED) { + hres = IEnumVARIANT_Next(iter, 1, &v, NULL); + if(FAILED(hres)) + return hres; + do_continue = hres == S_OK; + }else if(FAILED(hres)) { return hres; + } - do_continue = hres == S_OK; hres = assign_ident(ctx, ident, DISPATCH_PROPERTYPUT|DISPATCH_PROPERTYPUTREF, &dp); VariantClear(&v); if(FAILED(hres)) @@ -1902,7 +1907,6 @@ static HRESULT interp_enumnext_local(exec_ctx_t *ctx) { const unsigned loop_end = ctx->instr->arg1.uint; const int ref = ctx->instr->arg2.lng; - VARIANT v; IEnumVARIANT *iter; VARIANT *local_var; BOOL do_continue; @@ -1917,21 +1921,27 @@ static HRESULT interp_enumnext_local(exec_ctx_t *ctx) assert(V_VT(stack_top(ctx, 0)) == VT_UNKNOWN); iter = (IEnumVARIANT*)V_UNKNOWN(stack_top(ctx, 0)); - V_VT(&v) = VT_EMPTY; - hres = IEnumVARIANT_Next(iter, 1, &v, NULL); - if(FAILED(hres)) - return hres; - - do_continue = hres == S_OK; - local_var = get_local_var(ctx, ref); if(V_VT(local_var) == (VT_VARIANT|VT_BYREF)) local_var = V_VARIANTREF(local_var); - hres = assign_value(ctx, local_var, &v, DISPATCH_PROPERTYPUT|DISPATCH_PROPERTYPUTREF); - VariantClear(&v); - if(FAILED(hres)) + hres = safearray_iter_next(iter, local_var, &do_continue); + if(hres == E_UNEXPECTED) { + VARIANT v; + + V_VT(&v) = VT_EMPTY; + hres = IEnumVARIANT_Next(iter, 1, &v, NULL); + if(FAILED(hres)) + return hres; + + do_continue = hres == S_OK; + hres = assign_value(ctx, local_var, &v, DISPATCH_PROPERTYPUT|DISPATCH_PROPERTYPUTREF); + VariantClear(&v); + if(FAILED(hres)) + return hres; + }else if(FAILED(hres)) { return hres; + } if(do_continue) { ctx->instr++; diff --git a/dlls/vbscript/utils.c b/dlls/vbscript/utils.c index 4bfc2c65a0a..e73495ef3f9 100644 --- a/dlls/vbscript/utils.c +++ b/dlls/vbscript/utils.c @@ -196,3 +196,52 @@ HRESULT create_safearray_iter(SAFEARRAY *sa, BOOL owned, IEnumVARIANT **ev) *ev = &iter->IEnumVARIANT_iface; return S_OK; } + +HRESULT safearray_iter_next(IEnumVARIANT *iface, VARIANT *dst, BOOL *fetched) +{ + safearray_iter *iter; + VARIANT *src; + + if(iface->lpVtbl != &safearray_iter_EnumVARIANTVtbl) + return E_UNEXPECTED; + + iter = impl_from_IEnumVARIANT(iface); + + if(iter->i >= iter->size) { + VariantClear(dst); + *fetched = FALSE; + return S_OK; + } + + src = (VARIANT*)(((BYTE*)iter->sa->pvData) + iter->i * iter->sa->cbElements); + + /* Fast path for non-refcounted types (VT_I2, VT_I4, VT_R8, VT_BOOL, etc.). + * SAFEARRAY elements are stored by value (never VT_BYREF), so VariantCopyInd + * reduces to VariantCopy, which for simple types is just a struct assignment. */ + if(!(V_VT(src) & ~VT_TYPEMASK) && V_VT(src) != VT_BSTR + && V_VT(src) != VT_DISPATCH && V_VT(src) != VT_UNKNOWN) + { + if(V_VT(dst) == VT_BSTR || V_VT(dst) == VT_DISPATCH + || V_VT(dst) == VT_UNKNOWN || (V_VT(dst) & VT_ARRAY)) + VariantClear(dst); + *dst = *src; + } + else + { + VARIANT value; + HRESULT hres; + + V_VT(&value) = VT_EMPTY; + hres = VariantCopyInd(&value, src); + if(FAILED(hres)) + return hres; + + VariantClear(dst); + *dst = value; + } + + iter->i++; + *fetched = TRUE; + return S_OK; +} + diff --git a/dlls/vbscript/vbscript.h b/dlls/vbscript/vbscript.h index aed2ccaf5e5..b70d5f23324 100644 --- a/dlls/vbscript/vbscript.h +++ b/dlls/vbscript/vbscript.h @@ -455,6 +455,7 @@ BSTR string_replace(BSTR,BSTR,BSTR,int,int,int); void map_vbs_exception(EXCEPINFO *); HRESULT create_safearray_iter(SAFEARRAY *sa, BOOL owned, IEnumVARIANT **ev); +HRESULT safearray_iter_next(IEnumVARIANT *iface, VARIANT *dst, BOOL *fetched); #define FACILITY_VBS 0xa #define MAKE_VBSERROR(code) MAKE_HRESULT(SEVERITY_ERROR, FACILITY_VBS, code) -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10515
On Wed Apr 15 22:37:29 2026 +0000, Francis De Brabandere wrote:
changed this line in [version 15 of the diff](/wine/wine/-/merge_requests/10515/diffs?diff_id=260680&start_sha=8aec73b46c04a8e5d639bfa57a5cdf7b9e97ad4c#19b309798e74fb8c8af366e0c6cc686d79a38532_1093_1095) `assign_local_var`, `do_for_step` and `do_incc` extracted
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10515#note_136429
Jacek Caban (@jacek) commented about dlls/vbscript/interp.c:
exec.vars = NULL; }
+ /* For global-scope code (ParseScriptText), resolve compile-time variable + * indices to pointers into ScriptDisp->global_vars[]. This allows OP_local + * and OP_assign_local to access global Dim variables by index instead of + * doing string-based lookup_identifier() at runtime. exec_global_code() + * has already registered all Dim variables into global_vars before this. */
The shadowing problem seems a bit more complicated than that. For example, when a named item is available, both it and a global variable may shadow the identifier. We may also be shadowed by a variable from a user-supplied `IDispatch`, in which case we do not have such a pointer reference at all. Such an `IDispatch` may have a mutable property set, which means it may start shadowing during execution. I would expect native vbscript to do some caching anyway, but I do not know how without testing. Perhaps, for example, it performs the lookup on demand and caches the result? This way, we would not pay the cost for unused identifiers. In jscript, the optimization is effectively disabled for global code in practice (`base_scope` is not set for global code, causing it to fall back to string lookup). Maybe that would be good enough for your use case as well? -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10515#note_136524
When a For-Each loop iterates over a SAFEARRAY, bypass the IEnumVARIANT COM vtable dispatch and copy lements directly from the array data. This eliminates the COM call overhead and one intermediate VariantCopy per iteration.
It is a bit surprising that COM call overhead is noticeable. Indirect calls are not free, but we use them for each opcode anyway, so I would expect the savings to be lost in the noise. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10515#note_136525
On Thu Apr 16 15:05:46 2026 +0000, Jacek Caban wrote:
When a For-Each loop iterates over a SAFEARRAY, bypass the IEnumVARIANT COM vtable dispatch and copy lements directly from the array data. This eliminates the COM call overhead and one intermediate VariantCopy per iteration. It is a bit surprising that COM call overhead is noticeable. Indirect calls are not free, but we use them for each opcode anyway, so I would expect the savings to be lost in the noise. I'm pulling out this specific one into it's own MR that we can merge first.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10515#note_136599
Once !10685 lands, rebase the series onto master and interp_enumnext_local fast-path. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10515#note_136627
On Thu Apr 16 22:04:09 2026 +0000, Francis De Brabandere wrote:
I'm pulling out this specific one into it's own MR that we can merge first. let's continue in !10685
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10515#note_136629
participants (3)
-
Francis De Brabandere -
Francis De Brabandere (@francisdb) -
Jacek Caban (@jacek)