# HG changeset patch # User Bram Moolenaar # Date 1609794004 -3600 # Node ID 36bf9a6fbd4ccdfa056b0b148bb695774c9e2672 # Parent 4a8d345c230b72790e4f080693e0312dedd0cd25 patch 8.2.2301: Vim9: cannot unlet a dict or list item Commit: https://github.com/vim/vim/commit/752fc692ace51459cb407ec117c147b3bbebc071 Author: Bram Moolenaar Date: Mon Jan 4 21:57:11 2021 +0100 patch 8.2.2301: Vim9: cannot unlet a dict or list item Problem: Vim9: cannot unlet a dict or list item. Solution: Add ISN_UNLETINDEX. Refactor assignment code to use for unlet. diff --git a/src/testdir/test_vim9_assign.vim b/src/testdir/test_vim9_assign.vim --- a/src/testdir/test_vim9_assign.vim +++ b/src/testdir/test_vim9_assign.vim @@ -1349,14 +1349,47 @@ def Test_unlet() assert_false(exists('s:somevar')) unlet! s:somevar + # dict unlet + var dd = {a: 1, b: 2, c: 3} + unlet dd['a'] + unlet dd.c + assert_equal({b: 2}, dd) + + # list unlet + var ll = [1, 2, 3, 4] + unlet ll[1] + unlet ll[-1] + assert_equal([1, 3], ll) + + # list of dict unlet + var dl = [{a: 1, b: 2}, {c: 3}] + unlet dl[0]['b'] + assert_equal([{a: 1}, {c: 3}], dl) + + CheckDefExecFailure([ + 'var ll = test_null_list()', + 'unlet ll[0]', + ], 'E684:') + CheckDefExecFailure([ + 'var ll = [1]', + 'unlet ll[2]', + ], 'E684:') + CheckDefExecFailure([ + 'var dd = test_null_dict()', + 'unlet dd["a"]', + ], 'E716:') + CheckDefExecFailure([ + 'var dd = {a: 1}', + 'unlet dd["b"]', + ], 'E716:') + # can compile unlet before variable exists - # This doesn't work yet - #g:someDict = {key: 'val'} - #var k = 'key' - #unlet g:someDict[k] - #assert_equal({}, g:someDict) - #unlet g:someDict - #assert_false(exists('g:someDict')) + g:someDict = {key: 'val'} + var k = 'key' + unlet g:someDict[k] + assert_equal({}, g:someDict) + unlet g:someDict + assert_false(exists('g:someDict')) CheckScriptFailure([ 'vim9script', diff --git a/src/version.c b/src/version.c --- a/src/version.c +++ b/src/version.c @@ -751,6 +751,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ /**/ + 2301, +/**/ 2300, /**/ 2299, diff --git a/src/vim9.h b/src/vim9.h --- a/src/vim9.h +++ b/src/vim9.h @@ -60,6 +60,7 @@ typedef enum { ISN_UNLET, // unlet variable isn_arg.unlet.ul_name ISN_UNLETENV, // unlet environment variable isn_arg.unlet.ul_name + ISN_UNLETINDEX, // unlet item of list or dict ISN_LOCKCONST, // lock constant value diff --git a/src/vim9compile.c b/src/vim9compile.c --- a/src/vim9compile.c +++ b/src/vim9compile.c @@ -5026,6 +5026,7 @@ static char *reserved[] = { NULL }; +// Destination for an assignment or ":unlet" with an index. typedef enum { dest_local, dest_option, @@ -5040,6 +5041,35 @@ typedef enum { dest_expr, } assign_dest_T; +// Used by compile_lhs() to store information about the LHS of an assignment +// and one argument of ":unlet" with an index. +typedef struct { + assign_dest_T lhs_dest; // type of destination + + char_u *lhs_name; // allocated name including + // "[expr]" or ".name". + size_t lhs_varlen; // length of the variable without + // "[expr]" or ".name" + char_u *lhs_dest_end; // end of the destination, including + // "[expr]" or ".name". + + int lhs_has_index; // has "[expr]" or ".name" + + int lhs_new_local; // create new local variable + int lhs_opt_flags; // for when destination is an option + int lhs_vimvaridx; // for when destination is a v:var + + lvar_T lhs_local_lvar; // used for existing local destination + lvar_T lhs_arg_lvar; // used for argument destination + lvar_T *lhs_lvar; // points to destination lvar + int lhs_scriptvar_sid; + int lhs_scriptvar_idx; + + int lhs_has_type; // type was specified + type_T *lhs_type; + type_T *lhs_member_type; +} lhs_T; + /* * Generate the load instruction for "name". */ @@ -5322,6 +5352,417 @@ generate_store_var( return FAIL; } + static int +is_decl_command(int cmdidx) +{ + return cmdidx == CMD_let || cmdidx == CMD_var + || cmdidx == CMD_final || cmdidx == CMD_const; +} + +/* + * Figure out the LHS type and other properties for an assignment or one item + * of ":unlet" with an index. + * Returns OK or FAIL. + */ + static int +compile_lhs( + char_u *var_start, + lhs_T *lhs, + int cmdidx, + int heredoc, + int oplen, + cctx_T *cctx) +{ + char_u *var_end; + int is_decl = is_decl_command(cmdidx); + + CLEAR_POINTER(lhs); + lhs->lhs_dest = dest_local; + lhs->lhs_vimvaridx = -1; + lhs->lhs_scriptvar_idx = -1; + + // "dest_end" is the end of the destination, including "[expr]" or + // ".name". + // "var_end" is the end of the variable/option/etc. name. + lhs->lhs_dest_end = skip_var_one(var_start, FALSE); + if (*var_start == '@') + var_end = var_start + 2; + else + { + // skip over the leading "&", "&l:", "&g:" and "$" + var_end = skip_option_env_lead(var_start); + var_end = to_name_end(var_end, TRUE); + } + + // "a: type" is declaring variable "a" with a type, not dict "a:". + if (is_decl && lhs->lhs_dest_end == var_start + 2 + && lhs->lhs_dest_end[-1] == ':') + --lhs->lhs_dest_end; + if (is_decl && var_end == var_start + 2 && var_end[-1] == ':') + --var_end; + + // compute the length of the destination without "[expr]" or ".name" + lhs->lhs_varlen = var_end - var_start; + lhs->lhs_name = vim_strnsave(var_start, lhs->lhs_varlen); + if (lhs->lhs_name == NULL) + return FAIL; + if (heredoc) + lhs->lhs_type = &t_list_string; + else + lhs->lhs_type = &t_any; + + if (cctx->ctx_skip != SKIP_YES) + { + int declare_error = FALSE; + + if (get_var_dest(lhs->lhs_name, &lhs->lhs_dest, cmdidx, + &lhs->lhs_opt_flags, &lhs->lhs_vimvaridx, + &lhs->lhs_type, cctx) == FAIL) + return FAIL; + if (lhs->lhs_dest != dest_local) + { + // Specific kind of variable recognized. + declare_error = is_decl; + } + else + { + int idx; + + // No specific kind of variable recognized, just a name. + for (idx = 0; reserved[idx] != NULL; ++idx) + if (STRCMP(reserved[idx], lhs->lhs_name) == 0) + { + semsg(_(e_cannot_use_reserved_name), lhs->lhs_name); + return FAIL; + } + + + if (lookup_local(var_start, lhs->lhs_varlen, + &lhs->lhs_local_lvar, cctx) == OK) + lhs->lhs_lvar = &lhs->lhs_local_lvar; + else + { + CLEAR_FIELD(lhs->lhs_arg_lvar); + if (arg_exists(var_start, lhs->lhs_varlen, + &lhs->lhs_arg_lvar.lv_idx, &lhs->lhs_arg_lvar.lv_type, + &lhs->lhs_arg_lvar.lv_from_outer, cctx) == OK) + { + if (is_decl) + { + semsg(_(e_str_is_used_as_argument), lhs->lhs_name); + return FAIL; + } + lhs->lhs_lvar = &lhs->lhs_arg_lvar; + } + } + if (lhs->lhs_lvar != NULL) + { + if (is_decl) + { + semsg(_(e_variable_already_declared), lhs->lhs_name); + return FAIL; + } + } + else + { + int script_namespace = lhs->lhs_varlen > 1 + && STRNCMP(var_start, "s:", 2) == 0; + int script_var = (script_namespace + ? script_var_exists(var_start + 2, lhs->lhs_varlen - 2, + FALSE, cctx) + : script_var_exists(var_start, lhs->lhs_varlen, + TRUE, cctx)) == OK; + imported_T *import = + find_imported(var_start, lhs->lhs_varlen, cctx); + + if (script_namespace || script_var || import != NULL) + { + char_u *rawname = lhs->lhs_name + + (lhs->lhs_name[1] == ':' ? 2 : 0); + + if (is_decl) + { + if (script_namespace) + semsg(_(e_cannot_declare_script_variable_in_function), + lhs->lhs_name); + else + semsg(_(e_variable_already_declared_in_script), + lhs->lhs_name); + return FAIL; + } + else if (cctx->ctx_ufunc->uf_script_ctx_version + == SCRIPT_VERSION_VIM9 + && script_namespace + && !script_var && import == NULL) + { + semsg(_(e_unknown_variable_str), lhs->lhs_name); + return FAIL; + } + + lhs->lhs_dest = dest_script; + + // existing script-local variables should have a type + lhs->lhs_scriptvar_sid = current_sctx.sc_sid; + if (import != NULL) + lhs->lhs_scriptvar_sid = import->imp_sid; + if (SCRIPT_ID_VALID(lhs->lhs_scriptvar_sid)) + { + lhs->lhs_scriptvar_idx = get_script_item_idx( + lhs->lhs_scriptvar_sid, + rawname, TRUE, cctx); + if (lhs->lhs_scriptvar_idx >= 0) + { + scriptitem_T *si = SCRIPT_ITEM( + lhs->lhs_scriptvar_sid); + svar_T *sv = + ((svar_T *)si->sn_var_vals.ga_data) + + lhs->lhs_scriptvar_idx; + lhs->lhs_type = sv->sv_type; + } + } + } + else if (check_defined(var_start, lhs->lhs_varlen, cctx) + == FAIL) + return FAIL; + } + } + + if (declare_error) + { + vim9_declare_error(lhs->lhs_name); + return FAIL; + } + } + + // handle "a:name" as a name, not index "name" on "a" + if (lhs->lhs_varlen > 1 || var_start[lhs->lhs_varlen] != ':') + var_end = lhs->lhs_dest_end; + + if (lhs->lhs_dest != dest_option) + { + if (is_decl && *var_end == ':') + { + char_u *p; + + // parse optional type: "let var: type = expr" + if (!VIM_ISWHITE(var_end[1])) + { + semsg(_(e_white_space_required_after_str), ":"); + return FAIL; + } + p = skipwhite(var_end + 1); + lhs->lhs_type = parse_type(&p, cctx->ctx_type_list, TRUE); + if (lhs->lhs_type == NULL) + return FAIL; + lhs->lhs_has_type = TRUE; + } + else if (lhs->lhs_lvar != NULL) + lhs->lhs_type = lhs->lhs_lvar->lv_type; + } + + if (oplen == 3 && !heredoc && lhs->lhs_dest != dest_global + && lhs->lhs_type->tt_type != VAR_STRING + && lhs->lhs_type->tt_type != VAR_ANY) + { + emsg(_(e_can_only_concatenate_to_string)); + return FAIL; + } + + if (lhs->lhs_lvar == NULL && lhs->lhs_dest == dest_local + && cctx->ctx_skip != SKIP_YES) + { + if (oplen > 1 && !heredoc) + { + // +=, /=, etc. require an existing variable + semsg(_(e_cannot_use_operator_on_new_variable), lhs->lhs_name); + return FAIL; + } + if (!is_decl) + { + semsg(_(e_unknown_variable_str), lhs->lhs_name); + return FAIL; + } + + // new local variable + if ((lhs->lhs_type->tt_type == VAR_FUNC + || lhs->lhs_type->tt_type == VAR_PARTIAL) + && var_wrong_func_name(lhs->lhs_name, TRUE)) + return FAIL; + lhs->lhs_lvar = reserve_local(cctx, var_start, lhs->lhs_varlen, + cmdidx == CMD_final || cmdidx == CMD_const, lhs->lhs_type); + if (lhs->lhs_lvar == NULL) + return FAIL; + lhs->lhs_new_local = TRUE; + } + + lhs->lhs_member_type = lhs->lhs_type; + if (lhs->lhs_dest_end > var_start + lhs->lhs_varlen) + { + // Something follows after the variable: "var[idx]" or "var.key". + // TODO: should we also handle "->func()" here? + if (is_decl) + { + emsg(_(e_cannot_use_index_when_declaring_variable)); + return FAIL; + } + + if (var_start[lhs->lhs_varlen] == '[' + || var_start[lhs->lhs_varlen] == '.') + { + char_u *after = var_start + lhs->lhs_varlen; + char_u *p; + + // Only the last index is used below, if there are others + // before it generate code for the expression. Thus for + // "ll[1][2]" the expression is "ll[1]" and "[2]" is the index. + for (;;) + { + p = skip_index(after); + if (*p != '[' && *p != '.') + break; + after = p; + } + if (after > var_start + lhs->lhs_varlen) + { + lhs->lhs_varlen = after - var_start; + lhs->lhs_dest = dest_expr; + // We don't know the type before evaluating the expression, + // use "any" until then. + lhs->lhs_type = &t_any; + } + + lhs->lhs_has_index = TRUE; + if (lhs->lhs_type->tt_member == NULL) + lhs->lhs_member_type = &t_any; + else + lhs->lhs_member_type = lhs->lhs_type->tt_member; + } + else + { + semsg("Not supported yet: %s", var_start); + return FAIL; + } + } + return OK; +} + +/* + * Assignment to a list or dict member, or ":unlet" for the item, using the + * information in "lhs". + * Returns OK or FAIL. + */ + static int +compile_assign_unlet( + char_u *var_start, + lhs_T *lhs, + int is_assign, + type_T *rhs_type, + cctx_T *cctx) +{ + char_u *p; + int r; + vartype_T dest_type; + size_t varlen = lhs->lhs_varlen; + garray_T *stack = &cctx->ctx_type_stack; + + // Compile the "idx" in "var[idx]" or "key" in "var.key". + p = var_start + varlen; + if (*p == '[') + { + p = skipwhite(p + 1); + r = compile_expr0(&p, cctx); + if (r == OK && *skipwhite(p) != ']') + { + // this should not happen + emsg(_(e_missbrac)); + r = FAIL; + } + } + else // if (*p == '.') + { + char_u *key_end = to_name_end(p + 1, TRUE); + char_u *key = vim_strnsave(p + 1, key_end - p - 1); + + r = generate_PUSHS(cctx, key); + } + if (r == FAIL) + return FAIL; + + if (lhs->lhs_type == &t_any) + { + // Index on variable of unknown type: check at runtime. + dest_type = VAR_ANY; + } + else + { + dest_type = lhs->lhs_type->tt_type; + if (dest_type == VAR_DICT && may_generate_2STRING(-1, cctx) == FAIL) + return FAIL; + if (dest_type == VAR_LIST + && ((type_T **)stack->ga_data)[stack->ga_len - 1]->tt_type + != VAR_NUMBER) + { + emsg(_(e_number_exp)); + return FAIL; + } + } + + // Load the dict or list. On the stack we then have: + // - value (for assignment, not for :unlet) + // - index + // - variable + if (lhs->lhs_dest == dest_expr) + { + int c = var_start[varlen]; + + // Evaluate "ll[expr]" of "ll[expr][idx]" + p = var_start; + var_start[varlen] = NUL; + if (compile_expr0(&p, cctx) == OK && p != var_start + varlen) + { + // this should not happen + emsg(_(e_missbrac)); + return FAIL; + } + var_start[varlen] = c; + + lhs->lhs_type = stack->ga_len == 0 ? &t_void + : ((type_T **)stack->ga_data)[stack->ga_len - 1]; + // now we can properly check the type + if (lhs->lhs_type->tt_member != NULL && rhs_type != &t_void + && need_type(rhs_type, lhs->lhs_type->tt_member, -2, cctx, + FALSE, FALSE) == FAIL) + return FAIL; + } + else + generate_loadvar(cctx, lhs->lhs_dest, lhs->lhs_name, + lhs->lhs_lvar, lhs->lhs_type); + + if (dest_type == VAR_LIST || dest_type == VAR_DICT || dest_type == VAR_ANY) + { + if (is_assign) + { + isn_T *isn = generate_instr_drop(cctx, ISN_STOREINDEX, 3); + + if (isn == NULL) + return FAIL; + isn->isn_arg.vartype = dest_type; + } + else + { + if (generate_instr_drop(cctx, ISN_UNLETINDEX, 2) == NULL) + return FAIL; + } + } + else + { + emsg(_(e_indexable_type_required)); + return FAIL; + } + + return OK; +} + /* * Compile declaration and assignment: * "let name" @@ -5342,21 +5783,16 @@ compile_assignment(char_u *arg, exarg_T char_u *ret = NULL; int var_count = 0; int var_idx; - int scriptvar_sid = 0; - int scriptvar_idx = -1; int semicolon = 0; garray_T *instr = &cctx->ctx_instr; garray_T *stack = &cctx->ctx_type_stack; char_u *op; int oplen = 0; int heredoc = FALSE; - type_T *type = &t_any; - type_T *member_type = &t_any; type_T *rhs_type = &t_any; - char_u *name = NULL; char_u *sp; - int is_decl = cmdidx == CMD_let || cmdidx == CMD_var - || cmdidx == CMD_final || cmdidx == CMD_const; + int is_decl = is_decl_command(cmdidx); + lhs_T lhs; // Skip over the "var" or "[var, var]" to get to any "=". p = skip_var_list(arg, TRUE, &var_count, &semicolon, TRUE); @@ -5370,6 +5806,7 @@ compile_assignment(char_u *arg, exarg_T emsg(_(e_cannot_use_list_for_declaration)); return NULL; } + lhs.lhs_name = NULL; sp = p; p = skipwhite(p); @@ -5407,8 +5844,6 @@ compile_assignment(char_u *arg, exarg_T li->li_tv.vval.v_string = NULL; } generate_NEWLIST(cctx, l->lv_len); - type = &t_list_string; - member_type = &t_list_string; } list_free(l); p += STRLEN(p); @@ -5461,276 +5896,25 @@ compile_assignment(char_u *arg, exarg_T var_start = arg; for (var_idx = 0; var_idx == 0 || var_idx < var_count; var_idx++) { - char_u *var_end; - char_u *dest_end; - size_t varlen; - int new_local = FALSE; - assign_dest_T dest = dest_local; - int opt_flags = 0; - int vimvaridx = -1; - lvar_T local_lvar; - lvar_T *lvar = NULL; - lvar_T arg_lvar; - int has_type = FALSE; - int has_index = FALSE; int instr_count = -1; - // "dest_end" is the end of the destination, including "[expr]" or - // ".name". - // "var_end" is the end of the variable/option/etc. name. - dest_end = skip_var_one(var_start, FALSE); - if (*var_start == '@') - var_end = var_start + 2; - else - { - // skip over the leading "&", "&l:", "&g:" and "$" - var_end = skip_option_env_lead(var_start); - var_end = to_name_end(var_end, TRUE); - } - - // "a: type" is declaring variable "a" with a type, not dict "a:". - if (is_decl && dest_end == var_start + 2 && dest_end[-1] == ':') - --dest_end; - if (is_decl && var_end == var_start + 2 && var_end[-1] == ':') - --var_end; - - // compute the length of the destination without "[expr]" or ".name" - varlen = var_end - var_start; - vim_free(name); - name = vim_strnsave(var_start, varlen); - if (name == NULL) - return NULL; - if (!heredoc) - type = &t_any; - - if (cctx->ctx_skip != SKIP_YES) - { - int declare_error = FALSE; - - if (get_var_dest(name, &dest, cmdidx, &opt_flags, - &vimvaridx, &type, cctx) == FAIL) - goto theend; - if (dest != dest_local) - { - // Specific kind of variable recognized. - declare_error = is_decl; - } - else - { - int idx; - - // No specific kind of variable recognized, just a name. - for (idx = 0; reserved[idx] != NULL; ++idx) - if (STRCMP(reserved[idx], name) == 0) - { - semsg(_(e_cannot_use_reserved_name), name); - goto theend; - } - - - if (lookup_local(var_start, varlen, &local_lvar, cctx) == OK) - lvar = &local_lvar; - else - { - CLEAR_FIELD(arg_lvar); - if (arg_exists(var_start, varlen, - &arg_lvar.lv_idx, &arg_lvar.lv_type, - &arg_lvar.lv_from_outer, cctx) == OK) - { - if (is_decl) - { - semsg(_(e_str_is_used_as_argument), name); - goto theend; - } - lvar = &arg_lvar; - } - } - if (lvar != NULL) - { - if (is_decl) - { - semsg(_(e_variable_already_declared), name); - goto theend; - } - } - else - { - int script_namespace = varlen > 1 - && STRNCMP(var_start, "s:", 2) == 0; - int script_var = (script_namespace - ? script_var_exists(var_start + 2, varlen - 2, - FALSE, cctx) - : script_var_exists(var_start, varlen, - TRUE, cctx)) == OK; - imported_T *import = - find_imported(var_start, varlen, cctx); - - if (script_namespace || script_var || import != NULL) - { - char_u *rawname = name + (name[1] == ':' ? 2 : 0); - - if (is_decl) - { - if (script_namespace) - semsg(_(e_cannot_declare_script_variable_in_function), - name); - else - semsg(_(e_variable_already_declared_in_script), - name); - goto theend; - } - else if (cctx->ctx_ufunc->uf_script_ctx_version - == SCRIPT_VERSION_VIM9 - && script_namespace - && !script_var && import == NULL) - { - semsg(_(e_unknown_variable_str), name); - goto theend; - } - - dest = dest_script; - - // existing script-local variables should have a type - scriptvar_sid = current_sctx.sc_sid; - if (import != NULL) - scriptvar_sid = import->imp_sid; - if (SCRIPT_ID_VALID(scriptvar_sid)) - { - scriptvar_idx = get_script_item_idx(scriptvar_sid, - rawname, TRUE, cctx); - if (scriptvar_idx >= 0) - { - scriptitem_T *si = SCRIPT_ITEM(scriptvar_sid); - svar_T *sv = - ((svar_T *)si->sn_var_vals.ga_data) - + scriptvar_idx; - type = sv->sv_type; - } - } - } - else if (check_defined(var_start, varlen, cctx) == FAIL) - goto theend; - } - } - - if (declare_error) - { - vim9_declare_error(name); - goto theend; - } - } - - // handle "a:name" as a name, not index "name" on "a" - if (varlen > 1 || var_start[varlen] != ':') - var_end = dest_end; - - if (dest != dest_option) - { - if (is_decl && *var_end == ':') - { - // parse optional type: "let var: type = expr" - if (!VIM_ISWHITE(var_end[1])) - { - semsg(_(e_white_space_required_after_str), ":"); - goto theend; - } - p = skipwhite(var_end + 1); - type = parse_type(&p, cctx->ctx_type_list, TRUE); - if (type == NULL) - goto theend; - has_type = TRUE; - } - else if (lvar != NULL) - type = lvar->lv_type; - } - - if (oplen == 3 && !heredoc && dest != dest_global - && type->tt_type != VAR_STRING - && type->tt_type != VAR_ANY) - { - emsg(_(e_can_only_concatenate_to_string)); + vim_free(lhs.lhs_name); + + /* + * Figure out the LHS type and other properties. + */ + if (compile_lhs(var_start, &lhs, cmdidx, heredoc, oplen, cctx) == FAIL) goto theend; - } - - if (lvar == NULL && dest == dest_local && cctx->ctx_skip != SKIP_YES) - { - if (oplen > 1 && !heredoc) - { - // +=, /=, etc. require an existing variable - semsg(_(e_cannot_use_operator_on_new_variable), name); - goto theend; - } - if (!is_decl) - { - semsg(_(e_unknown_variable_str), name); - goto theend; - } - - // new local variable - if ((type->tt_type == VAR_FUNC || type->tt_type == VAR_PARTIAL) - && var_wrong_func_name(name, TRUE)) - goto theend; - lvar = reserve_local(cctx, var_start, varlen, - cmdidx == CMD_final || cmdidx == CMD_const, type); - if (lvar == NULL) - goto theend; - new_local = TRUE; - } - - member_type = type; - if (dest_end > var_start + varlen) - { - // Something follows after the variable: "var[idx]" or "var.key". - // TODO: should we also handle "->func()" here? - if (is_decl) - { - emsg(_(e_cannot_use_index_when_declaring_variable)); - goto theend; - } - - if (var_start[varlen] == '[' || var_start[varlen] == '.') - { - char_u *after = var_start + varlen; - - // Only the last index is used below, if there are others - // before it generate code for the expression. Thus for - // "ll[1][2]" the expression is "ll[1]" and "[2]" is the index. - for (;;) - { - p = skip_index(after); - if (*p != '[' && *p != '.') - break; - after = p; - } - if (after > var_start + varlen) - { - varlen = after - var_start; - dest = dest_expr; - // We don't know the type before evaluating the expression, - // use "any" until then. - type = &t_any; - } - - has_index = TRUE; - if (type->tt_member == NULL) - member_type = &t_any; - else - member_type = type->tt_member; - } - else - { - semsg("Not supported yet: %s", var_start); - goto theend; - } - } - else if (lvar == &arg_lvar) - { - semsg(_(e_cannot_assign_to_argument), name); + + if (!lhs.lhs_has_index && lhs.lhs_lvar == &lhs.lhs_arg_lvar) + { + semsg(_(e_cannot_assign_to_argument), lhs.lhs_name); goto theend; } - if (!is_decl && lvar != NULL && lvar->lv_const && !has_index) - { - semsg(_(e_cannot_assign_to_constant), name); + if (!is_decl && lhs.lhs_lvar != NULL + && lhs.lhs_lvar->lv_const && !lhs.lhs_has_index) + { + semsg(_(e_cannot_assign_to_constant), lhs.lhs_name); goto theend; } @@ -5758,9 +5942,10 @@ compile_assignment(char_u *arg, exarg_T // for "+=", "*=", "..=" etc. first load the current value if (*op != '=') { - generate_loadvar(cctx, dest, name, lvar, type); - - if (has_index) + generate_loadvar(cctx, lhs.lhs_dest, lhs.lhs_name, + lhs.lhs_lvar, lhs.lhs_type); + + if (lhs.lhs_has_index) { // TODO: get member from list or dict emsg("Index with operation not supported yet"); @@ -5770,18 +5955,18 @@ compile_assignment(char_u *arg, exarg_T // Compile the expression. Temporarily hide the new local // variable here, it is not available to this expression. - if (new_local) + if (lhs.lhs_new_local) --cctx->ctx_locals.ga_len; instr_count = instr->ga_len; wp = op + oplen; if (may_get_next_line_error(wp, &p, cctx) == FAIL) { - if (new_local) + if (lhs.lhs_new_local) ++cctx->ctx_locals.ga_len; goto theend; } r = compile_expr0_ext(&p, cctx, &is_const); - if (new_local) + if (lhs.lhs_new_local) ++cctx->ctx_locals.ga_len; if (r == FAIL) goto theend; @@ -5802,14 +5987,14 @@ compile_assignment(char_u *arg, exarg_T rhs_type = stack->ga_len == 0 ? &t_void : ((type_T **)stack->ga_data)[stack->ga_len - 1]; - if (lvar != NULL && (is_decl || !has_type)) + if (lhs.lhs_lvar != NULL && (is_decl || !lhs.lhs_has_type)) { if ((rhs_type->tt_type == VAR_FUNC || rhs_type->tt_type == VAR_PARTIAL) - && var_wrong_func_name(name, TRUE)) + && var_wrong_func_name(lhs.lhs_name, TRUE)) goto theend; - if (new_local && !has_type) + if (lhs.lhs_new_local && !lhs.lhs_has_type) { if (rhs_type->tt_type == VAR_VOID) { @@ -5821,29 +6006,29 @@ compile_assignment(char_u *arg, exarg_T // An empty list or dict has a &t_unknown member, // for a variable that implies &t_any. if (rhs_type == &t_list_empty) - lvar->lv_type = &t_list_any; + lhs.lhs_lvar->lv_type = &t_list_any; else if (rhs_type == &t_dict_empty) - lvar->lv_type = &t_dict_any; + lhs.lhs_lvar->lv_type = &t_dict_any; else if (rhs_type == &t_unknown) - lvar->lv_type = &t_any; + lhs.lhs_lvar->lv_type = &t_any; else - lvar->lv_type = rhs_type; + lhs.lhs_lvar->lv_type = rhs_type; } } else if (*op == '=') { - type_T *use_type = lvar->lv_type; + type_T *use_type = lhs.lhs_lvar->lv_type; // without operator check type here, otherwise below - if (has_index) - use_type = member_type; + if (lhs.lhs_has_index) + use_type = lhs.lhs_member_type; if (need_type(rhs_type, use_type, -1, cctx, FALSE, is_const) == FAIL) goto theend; } } - else if (*p != '=' && need_type(rhs_type, member_type, -1, - cctx, FALSE, FALSE) == FAIL) + else if (*p != '=' && need_type(rhs_type, lhs.lhs_member_type, + -1, cctx, FALSE, FALSE) == FAIL) goto theend; } else if (cmdidx == CMD_final) @@ -5856,7 +6041,7 @@ compile_assignment(char_u *arg, exarg_T emsg(_(e_const_requires_a_value)); goto theend; } - else if (!has_type || dest == dest_option) + else if (!lhs.lhs_has_type || lhs.lhs_dest == dest_option) { emsg(_(e_type_or_initialization_required)); goto theend; @@ -5866,7 +6051,7 @@ compile_assignment(char_u *arg, exarg_T // variables are always initialized if (ga_grow(instr, 1) == FAIL) goto theend; - switch (member_type->tt_type) + switch (lhs.lhs_member_type->tt_type) { case VAR_BOOL: generate_PUSHBOOL(cctx, VVAL_FALSE); @@ -5923,7 +6108,7 @@ compile_assignment(char_u *arg, exarg_T if (*op == '.') expected = &t_string; else - expected = member_type; + expected = lhs.lhs_member_type; stacktype = ((type_T **)stack->ga_data)[stack->ga_len - 1]; if ( #ifdef FEAT_FLOAT @@ -5942,157 +6127,76 @@ compile_assignment(char_u *arg, exarg_T else if (*op == '+') { if (generate_add_instr(cctx, - operator_type(member_type, stacktype), - member_type, stacktype) == FAIL) + operator_type(lhs.lhs_member_type, stacktype), + lhs.lhs_member_type, stacktype) == FAIL) goto theend; } else if (generate_two_op(cctx, op) == FAIL) goto theend; } - if (has_index) - { - int r; - vartype_T dest_type; - - // Compile the "idx" in "var[idx]" or "key" in "var.key". - p = var_start + varlen; - if (*p == '[') - { - p = skipwhite(p + 1); - r = compile_expr0(&p, cctx); - if (r == OK && *skipwhite(p) != ']') - { - // this should not happen - emsg(_(e_missbrac)); - r = FAIL; - } - } - else // if (*p == '.') - { - char_u *key_end = to_name_end(p + 1, TRUE); - char_u *key = vim_strnsave(p + 1, key_end - p - 1); - - r = generate_PUSHS(cctx, key); - } - if (r == FAIL) + if (lhs.lhs_has_index) + { + // Use the info in "lhs" to store the value at the index in the + // list or dict. + if (compile_assign_unlet(var_start, &lhs, TRUE, rhs_type, cctx) + == FAIL) goto theend; - - if (type == &t_any) - { - // Index on variable of unknown type: check at runtime. - dest_type = VAR_ANY; - } - else - { - dest_type = type->tt_type; - if (dest_type == VAR_DICT - && may_generate_2STRING(-1, cctx) == FAIL) - goto theend; - if (dest_type == VAR_LIST - && ((type_T **)stack->ga_data)[stack->ga_len - 1]->tt_type - != VAR_NUMBER) - { - emsg(_(e_number_exp)); - goto theend; - } - } - - // Load the dict or list. On the stack we then have: - // - value - // - index - // - variable - if (dest == dest_expr) - { - int c = var_start[varlen]; - - // Evaluate "ll[expr]" of "ll[expr][idx]" - p = var_start; - var_start[varlen] = NUL; - if (compile_expr0(&p, cctx) == OK && p != var_start + varlen) - { - // this should not happen - emsg(_(e_missbrac)); - goto theend; - } - var_start[varlen] = c; - - type = stack->ga_len == 0 ? &t_void - : ((type_T **)stack->ga_data)[stack->ga_len - 1]; - // now we can properly check the type - if (type->tt_member != NULL - && need_type(rhs_type, type->tt_member, -2, cctx, - FALSE, FALSE) == FAIL) - goto theend; - } - else - generate_loadvar(cctx, dest, name, lvar, type); - - if (dest_type == VAR_LIST || dest_type == VAR_DICT - || dest_type == VAR_ANY) - { - isn_T *isn = generate_instr_drop(cctx, ISN_STOREINDEX, 3); - - if (isn == NULL) - goto theend; - isn->isn_arg.vartype = dest_type; - } - else - { - emsg(_(e_indexable_type_required)); - goto theend; - } } else { - if (is_decl && cmdidx == CMD_const - && (dest == dest_script || dest == dest_local)) + if (is_decl && cmdidx == CMD_const && (lhs.lhs_dest == dest_script + || lhs.lhs_dest == dest_local)) // ":const var": lock the value, but not referenced variables generate_LOCKCONST(cctx); if (is_decl - && (type->tt_type == VAR_DICT || type->tt_type == VAR_LIST) - && type->tt_member != NULL - && type->tt_member != &t_any - && type->tt_member != &t_unknown) + && (lhs.lhs_type->tt_type == VAR_DICT + || lhs.lhs_type->tt_type == VAR_LIST) + && lhs.lhs_type->tt_member != NULL + && lhs.lhs_type->tt_member != &t_any + && lhs.lhs_type->tt_member != &t_unknown) // Set the type in the list or dict, so that it can be checked, // also in legacy script. - generate_SETTYPE(cctx, type); - - if (dest != dest_local) + generate_SETTYPE(cctx, lhs.lhs_type); + + if (lhs.lhs_dest != dest_local) { - if (generate_store_var(cctx, dest, opt_flags, vimvaridx, - scriptvar_idx, scriptvar_sid, type, name) == FAIL) + if (generate_store_var(cctx, lhs.lhs_dest, + lhs.lhs_opt_flags, lhs.lhs_vimvaridx, + lhs.lhs_scriptvar_idx, lhs.lhs_scriptvar_sid, + lhs.lhs_type, lhs.lhs_name) == FAIL) goto theend; } - else if (lvar != NULL) + else if (lhs.lhs_lvar != NULL) { isn_T *isn = ((isn_T *)instr->ga_data) + instr->ga_len - 1; // optimization: turn "var = 123" from ISN_PUSHNR + // ISN_STORE into ISN_STORENR - if (!lvar->lv_from_outer + if (!lhs.lhs_lvar->lv_from_outer && instr->ga_len == instr_count + 1 && isn->isn_type == ISN_PUSHNR) { varnumber_T val = isn->isn_arg.number; isn->isn_type = ISN_STORENR; - isn->isn_arg.storenr.stnr_idx = lvar->lv_idx; + isn->isn_arg.storenr.stnr_idx = lhs.lhs_lvar->lv_idx; isn->isn_arg.storenr.stnr_val = val; if (stack->ga_len > 0) --stack->ga_len; } - else if (lvar->lv_from_outer) - generate_STORE(cctx, ISN_STOREOUTER, lvar->lv_idx, NULL); + else if (lhs.lhs_lvar->lv_from_outer) + generate_STORE(cctx, ISN_STOREOUTER, + lhs.lhs_lvar->lv_idx, NULL); else - generate_STORE(cctx, ISN_STORE, lvar->lv_idx, NULL); + generate_STORE(cctx, ISN_STORE, lhs.lhs_lvar->lv_idx, NULL); } } if (var_idx + 1 < var_count) - var_start = skipwhite(dest_end + 1); + var_start = skipwhite(lhs.lhs_dest_end + 1); } // for "[var, var] = expr" drop the "expr" value @@ -6105,7 +6209,7 @@ compile_assignment(char_u *arg, exarg_T ret = skipwhite(end); theend: - vim_free(name); + vim_free(lhs.lhs_name); return ret; } @@ -6137,40 +6241,60 @@ compile_unlet( int deep UNUSED, void *coookie) { - cctx_T *cctx = coookie; - - if (lvp->ll_tv == NULL) - { - char_u *p = lvp->ll_name; - int cc = *name_end; - int ret = OK; - + cctx_T *cctx = coookie; + char_u *p = lvp->ll_name; + int cc = *name_end; + int ret = OK; + + if (cctx->ctx_skip == SKIP_YES) + return OK; + + *name_end = NUL; + if (*p == '$') + { + // :unlet $ENV_VAR + ret = generate_UNLET(cctx, ISN_UNLETENV, p + 1, eap->forceit); + } + else if (vim_strchr(p, '.') != NULL || vim_strchr(p, '[') != NULL) + { + lhs_T lhs; + + // This is similar to assigning: lookup the list/dict, compile the + // idx/key. Then instead of storing the value unlet the item. + // unlet {list}[idx] + // unlet {dict}[key] dict.key + // + // Figure out the LHS type and other properties. + // + ret = compile_lhs(p, &lhs, CMD_unlet, FALSE, 0, cctx); + + // : unlet an indexed item + if (!lhs.lhs_has_index) + { + iemsg("called compile_lhs() without an index"); + ret = FAIL; + } + else + { + // Use the info in "lhs" to unlet the item at the index in the + // list or dict. + ret = compile_assign_unlet(p, &lhs, FALSE, &t_void, cctx); + } + + vim_free(lhs.lhs_name); + } + else if (check_vim9_unlet(p) == FAIL) + { + ret = FAIL; + } + else + { // Normal name. Only supports g:, w:, t: and b: namespaces. - *name_end = NUL; - if (vim_strchr(p, '.') != NULL || vim_strchr(p, '[') != NULL) - { - *name_end = cc; - goto failed; - } - - if (*p == '$') - ret = generate_UNLET(cctx, ISN_UNLETENV, p + 1, eap->forceit); - else if (check_vim9_unlet(p) == FAIL) - ret = FAIL; - else - ret = generate_UNLET(cctx, ISN_UNLET, p, eap->forceit); - - *name_end = cc; - return ret; - } - -failed: - // TODO: unlet {list}[idx] - // TODO: unlet {dict}[key] - // complication: {list} can be global while "idx" is local, thus we can't - // call ex_unlet(). - emsg("Sorry, :unlet not fully implemented yet"); - return FAIL; + ret = generate_UNLET(cctx, ISN_UNLET, p, eap->forceit); + } + + *name_end = cc; + return ret; } /* @@ -6188,7 +6312,6 @@ compile_unletlock(char_u *arg, exarg_T * return NULL; } - // TODO: this doesn't work for local variables ex_unletlock(eap, p, 0, GLV_NO_AUTOLOAD | GLV_COMPILING, compile_unlet, cctx); return eap->nextcmd == NULL ? (char_u *)"" : eap->nextcmd; @@ -8345,6 +8468,7 @@ delete_instr(isn_T *isn) case ISN_STRSLICE: case ISN_THROW: case ISN_TRY: + case ISN_UNLETINDEX: case ISN_UNPACK: // nothing allocated break; diff --git a/src/vim9execute.c b/src/vim9execute.c --- a/src/vim9execute.c +++ b/src/vim9execute.c @@ -1783,6 +1783,10 @@ call_def_function( typval_T *tv_dest = STACK_TV_BOT(-1); int status = OK; + // Stack contains: + // -3 value to be stored + // -2 index + // -1 dict or list tv = STACK_TV_BOT(-3); SOURCING_LNUM = iptr->isn_lnum; if (dest_type == VAR_ANY) @@ -1898,6 +1902,91 @@ call_def_function( } break; + // unlet item in list or dict variable + case ISN_UNLETINDEX: + { + typval_T *tv_idx = STACK_TV_BOT(-2); + typval_T *tv_dest = STACK_TV_BOT(-1); + int status = OK; + + // Stack contains: + // -2 index + // -1 dict or list + if (tv_dest->v_type == VAR_DICT) + { + // unlet a dict item, index must be a string + if (tv_idx->v_type != VAR_STRING) + { + semsg(_(e_expected_str_but_got_str), + vartype_name(VAR_STRING), + vartype_name(tv_idx->v_type)); + status = FAIL; + } + else + { + dict_T *d = tv_dest->vval.v_dict; + char_u *key = tv_idx->vval.v_string; + dictitem_T *di = NULL; + + if (key == NULL) + key = (char_u *)""; + if (d != NULL) + di = dict_find(d, key, (int)STRLEN(key)); + if (di == NULL) + { + // NULL dict is equivalent to empty dict + semsg(_(e_dictkey), key); + status = FAIL; + } + else + { + // TODO: check for dict or item locked + dictitem_remove(d, di); + } + } + } + else if (tv_dest->v_type == VAR_LIST) + { + // unlet a List item, index must be a number + if (tv_idx->v_type != VAR_NUMBER) + { + semsg(_(e_expected_str_but_got_str), + vartype_name(VAR_NUMBER), + vartype_name(tv_idx->v_type)); + status = FAIL; + } + else + { + list_T *l = tv_dest->vval.v_list; + varnumber_T n = tv_idx->vval.v_number; + listitem_T *li = NULL; + + li = list_find(l, n); + if (li == NULL) + { + semsg(_(e_listidx), n); + status = FAIL; + } + else + // TODO: check for list or item locked + listitem_remove(l, li); + } + } + else + { + status = FAIL; + semsg(_(e_cannot_index_str), + vartype_name(tv_dest->v_type)); + } + + clear_tv(tv_idx); + clear_tv(tv_dest); + ectx.ec_stack.ga_len -= 2; + if (status == FAIL) + goto on_error; + } + break; + // push constant case ISN_PUSHNR: case ISN_PUSHBOOL: @@ -3649,6 +3738,9 @@ ex_disassemble(exarg_T *eap) iptr->isn_arg.unlet.ul_forceit ? "!" : "", iptr->isn_arg.unlet.ul_name); break; + case ISN_UNLETINDEX: + smsg("%4d UNLETINDEX", current); + break; case ISN_LOCKCONST: smsg("%4d LOCKCONST", current); break;