# HG changeset patch # User Bram Moolenaar # Date 1600112704 -7200 # Node ID eb1f5f618c750edf6b0f35eb91e00ae74d4a0321 # Parent 0440d470ae70eb577fab61ae72f92e518695b071 patch 8.2.1685: Vim9: cannot declare a constant value Commit: https://github.com/vim/vim/commit/0b4c66c67a083f25816b9cdb8e76a41e02d9f560 Author: Bram Moolenaar Date: Mon Sep 14 21:39:44 2020 +0200 patch 8.2.1685: Vim9: cannot declare a constant value Problem: Vim9: cannot declare a constant value. Solution: Introduce ":const!". diff --git a/runtime/doc/vim9.txt b/runtime/doc/vim9.txt --- a/runtime/doc/vim9.txt +++ b/runtime/doc/vim9.txt @@ -1,4 +1,4 @@ -*vim9.txt* For Vim version 8.2. Last change: 2020 Sep 07 +*vim9.txt* For Vim version 8.2. Last change: 2020 Sep 13 VIM REFERENCE MANUAL by Bram Moolenaar @@ -192,6 +192,9 @@ To intentionally avoid a variable being } echo temp # Error! +Declaring a variable with a type but without an initializer will initialize to +zero, false or empty. + An existing variable cannot be assigned to with `:let`, since that implies a declaration. Global, window, tab, buffer and Vim variables can only be used without `:let`, because they are not really declared, they can also be deleted @@ -210,6 +213,40 @@ at the script level. > Since "&opt = value" is now assigning a value to option "opt", ":&" cannot be used to repeat a `:substitute` command. + *vim9-const* +In legacy Vim script "const list = []" would make the variable "list" +immutable and also the value. Thus you cannot add items to the list. This +differs from what many languages do. Vim9 script does it like TypeScript: only +"list" is immutable, the value can be changed. + +One can use `:const!` to make both the variable and the value immutable. Use +this for composite structures that you want to make sure will not be modified. + +How this works: > + vim9script + const list = [1, 2] + list = [3, 4] # Error! + list[0] = 2 # OK + + const! LIST = [1, 2] + LIST = [3, 4] # Error! + LIST[0] = 2 # Error! +It is common to write constants as ALL_CAPS, but you don't have to. + +The constant only applies to the value itself, not what it refers to. > + cont females = ["Mary"] + const! NAMES = [["John", "Peter"], females] + NAMES[0] = ["Jack"] # Error! + NAMES[0][0] = ["Jack"] # Error! + NAMES[1] = ["Emma"] # Error! + Names[1][0] = "Emma" # OK, now females[0] == "Emma" + +Rationale: TypeScript has no way to make the value immutable. One can use +immutable types, but that quickly gets complicated for nested values. And +with a type cast the value can be made mutable again, which means there is no +guarantee the value won't change. Vim supports immutable values, in legacy +script this was done with `:lockvar`. But that is an extra statement and also +applies to nested values. Therefore the solution to use `:const!`. *E1092* Declaring more than one variable at a time, using the unpack notation, is @@ -408,7 +445,7 @@ for using a list or job. This is very m few exceptions. type TRUE when ~ - bool v:true + bool v:true or 1 number non-zero float non-zero string non-empty @@ -946,26 +983,41 @@ declarations: > Expression evaluation was already close to what JavaScript and other languages are doing. Some details are unexpected and can be fixed. For example how the || and && operators work. Legacy Vim script: > - let result = 44 + let value = 44 ... - return result || 0 # returns 1 + let result = value || 0 # result == 1 Vim9 script works like JavaScript/TypeScript, keep the value: > - let result = 44 + let value = 44 ... - return result || 0 # returns 44 - -On the other hand, overloading "+" to use both for addition and string -concatenation goes against legacy Vim script and often leads to mistakes. -For that reason we will keep using ".." for string concatenation. Lua also -uses ".." this way. + let result = value || 0 # result == 44 There is no intention to completely match TypeScript syntax and semantics. We just want to take those parts that we can use for Vim and we expect Vim users -are happy with. TypeScript is a complex language with its own advantages and -disadvantages. People used to other languages (Java, Python, etc.) will also -find things in TypeScript that they do not like or do not understand. We'll -try to avoid those things. +will be happy with. TypeScript is a complex language with its own advantages +and disadvantages. To get an idea of the disadvantages read the book: +"JavaScript: The Good Parts". Or find the article "TypeScript: the good +parts" and read the "Things to avoid" section. + +People used to other languages (Java, Python, etc.) will also find things in +TypeScript that they do not like or do not understand. We'll try to avoid +those things. + +Specific items from TypeScript we avoid: +- Overloading "+", using it both for addition and string concatenation. This + goes against legacy Vim script and often leads to mistakes. For that reason + we will keep using ".." for string concatenation. Lua also uses ".." this + way. And it allows for conversion to string for more values. +- TypeScript can use an expression like "99 || 'yes'" in a condition, but + cannot assign the value to a boolean. That is inconsistent and can be + annoying. Vim recognizes an expression with && or || and allows using the + result as a bool. +- TypeScript considers an empty string as Falsy, but an empty list or dict as + Truthy. That is inconsistent. In Vim an empty list and dict are also + Falsy. +- TypeScript has various "Readonly" types, which have limited usefulness, + since a type cast can remove the immutable nature. Vim locks the value, + which is more flexible, but is only checked at runtime. Import and Export ~ diff --git a/src/errors.h b/src/errors.h --- a/src/errors.h +++ b/src/errors.h @@ -258,4 +258,12 @@ EXTERN char e_assert_fails_fifth_argumen INIT(= N_("E1116: assert_fails() fifth argument must be a string")); EXTERN char e_cannot_use_bang_with_nested_def[] INIT(= N_("E1117: Cannot use ! with nested :def")); +EXTERN char e_cannot_change_list[] + INIT(= N_("E1118: Cannot change list")); +EXTERN char e_cannot_change_list_item[] + INIT(= N_("E1119: Cannot change list item")); +EXTERN char e_cannot_change_dict[] + INIT(= N_("E1120: Cannot change dict")); +EXTERN char e_cannot_change_dict_item[] + INIT(= N_("E1121: Cannot change dict item")); #endif diff --git a/src/eval.c b/src/eval.c --- a/src/eval.c +++ b/src/eval.c @@ -1200,7 +1200,7 @@ set_var_lval( char_u *endp, typval_T *rettv, int copy, - int flags, // LET_IS_CONST and/or LET_NO_COMMAND + int flags, // LET_IS_CONST, LET_FORCEIT, LET_NO_COMMAND char_u *op) { int cc; diff --git a/src/evalvars.c b/src/evalvars.c --- a/src/evalvars.c +++ b/src/evalvars.c @@ -173,7 +173,6 @@ static char_u *list_arg_vars(exarg_T *ea static char_u *ex_let_one(char_u *arg, typval_T *tv, int copy, int flags, char_u *endchars, char_u *op); static int do_unlet_var(lval_T *lp, char_u *name_end, exarg_T *eap, int deep, void *cookie); static int do_lock_var(lval_T *lp, char_u *name_end, exarg_T *eap, int deep, void *cookie); -static void item_lock(typval_T *tv, int deep, int lock, int check_refcount); static void delete_var(hashtab_T *ht, hashitem_T *hi); static void list_one_var(dictitem_T *v, char *prefix, int *first); static void list_one_var_a(char *prefix, char_u *name, int type, char_u *string, int *first); @@ -709,6 +708,8 @@ ex_let(exarg_T *eap) // detect Vim9 assignment without ":let" or ":const" if (eap->arg == eap->cmd) flags |= LET_NO_COMMAND; + if (eap->forceit) + flags |= LET_FORCEIT; argend = skip_var_list(arg, TRUE, &var_count, &semicolon, FALSE); if (argend == NULL) @@ -859,7 +860,7 @@ ex_let_vars( int copy, // copy values from "tv", don't move int semicolon, // from skip_var_list() int var_count, // from skip_var_list() - int flags, // LET_IS_CONST and/or LET_NO_COMMAND + int flags, // LET_IS_CONST, LET_FORCEIT, LET_NO_COMMAND char_u *op) { char_u *arg = arg_start; @@ -1214,7 +1215,7 @@ ex_let_one( char_u *arg, // points to variable name typval_T *tv, // value to assign to variable int copy, // copy value from "tv" - int flags, // LET_IS_CONST and/or LET_NO_COMMAND + int flags, // LET_IS_CONST, LET_FORCEIT, LET_NO_COMMAND char_u *endchars, // valid chars after variable name or NULL char_u *op) // "+", "-", "." or NULL { @@ -1741,7 +1742,7 @@ do_lock_var( * When "check_refcount" is TRUE do not lock a list or dict with a reference * count larger than 1. */ - static void + void item_lock(typval_T *tv, int deep, int lock, int check_refcount) { static int recurse = 0; @@ -2937,7 +2938,7 @@ set_var_const( type_T *type, typval_T *tv_arg, int copy, // make copy of value in "tv" - int flags) // LET_IS_CONST and/or LET_NO_COMMAND + int flags) // LET_IS_CONST, LET_FORCEIT, LET_NO_COMMAND { typval_T *tv = tv_arg; typval_T bool_tv; @@ -3124,8 +3125,8 @@ set_var_const( init_tv(tv); } - // ":const var = val" locks the value, but not in Vim9 script - if ((flags & LET_IS_CONST) && !vim9script) + // ":const var = val" locks the value; in Vim9 script only with ":const!" + if ((flags & LET_IS_CONST) && (!vim9script || (flags & LET_FORCEIT))) // Like :lockvar! name: lock the value and what it contains, but only // if the reference count is up to one. That locks only literal // values. diff --git a/src/ex_cmds.h b/src/ex_cmds.h --- a/src/ex_cmds.h +++ b/src/ex_cmds.h @@ -398,7 +398,7 @@ EXCMD(CMD_confirm, "confirm", ex_wrongmo EX_NEEDARG|EX_EXTRA|EX_NOTRLCOM|EX_CMDWIN|EX_LOCK_OK, ADDR_NONE), EXCMD(CMD_const, "const", ex_let, - EX_EXTRA|EX_NOTRLCOM|EX_SBOXOK|EX_CMDWIN|EX_LOCK_OK, + EX_EXTRA|EX_BANG|EX_NOTRLCOM|EX_SBOXOK|EX_CMDWIN|EX_LOCK_OK, ADDR_NONE), EXCMD(CMD_copen, "copen", ex_copen, EX_RANGE|EX_COUNT|EX_TRLBAR, diff --git a/src/proto/evalvars.pro b/src/proto/evalvars.pro --- a/src/proto/evalvars.pro +++ b/src/proto/evalvars.pro @@ -23,6 +23,7 @@ void ex_unlet(exarg_T *eap); void ex_lockvar(exarg_T *eap); void ex_unletlock(exarg_T *eap, char_u *argstart, int deep, int glv_flags, int (*callback)(lval_T *, char_u *, exarg_T *, int, void *), void *cookie); int do_unlet(char_u *name, int forceit); +void item_lock(typval_T *tv, int deep, int lock, int check_refcount); void del_menutrans_vars(void); char_u *get_user_var_name(expand_T *xp, int idx); char *get_var_special_name(int nr); @@ -65,7 +66,7 @@ void unref_var_dict(dict_T *dict); void vars_clear(hashtab_T *ht); void vars_clear_ext(hashtab_T *ht, int free_val); void set_var(char_u *name, typval_T *tv, int copy); -void set_var_const(char_u *name, type_T *type, typval_T *tv, int copy, int flags); +void set_var_const(char_u *name, type_T *type, typval_T *tv_arg, int copy, int flags); int var_check_ro(int flags, char_u *name, int use_gettext); int var_check_fixed(int flags, char_u *name, int use_gettext); int var_wrong_func_name(char_u *name, int new_var); diff --git a/src/testdir/test_vim9_script.vim b/src/testdir/test_vim9_script.vim --- a/src/testdir/test_vim9_script.vim +++ b/src/testdir/test_vim9_script.vim @@ -828,10 +828,50 @@ def Test_const() let lines =<< trim END const list = [1, 2, 3] list[0] = 4 + list->assert_equal([4, 2, 3]) + const! other = [5, 6, 7] + other->assert_equal([5, 6, 7]) END CheckDefAndScriptSuccess(lines) enddef +def Test_const_bang() + let lines =<< trim END + const! var = 234 + var = 99 + END + CheckDefExecFailure(lines, 'E1018:', 2) + CheckScriptFailure(['vim9script'] + lines, 'E46:', 3) + + lines =<< trim END + const! ll = [2, 3, 4] + ll[0] = 99 + END + CheckDefExecFailure(lines, 'E1119:', 2) + CheckScriptFailure(['vim9script'] + lines, 'E741:', 3) + + lines =<< trim END + const! ll = [2, 3, 4] + ll[3] = 99 + END + CheckDefExecFailure(lines, 'E1118:', 2) + CheckScriptFailure(['vim9script'] + lines, 'E684:', 3) + + lines =<< trim END + const! dd = #{one: 1, two: 2} + dd["one"] = 99 + END + CheckDefExecFailure(lines, 'E1121:', 2) + CheckScriptFailure(['vim9script'] + lines, 'E741:', 3) + + lines =<< trim END + const! dd = #{one: 1, two: 2} + dd["three"] = 99 + END + CheckDefExecFailure(lines, 'E1120:') + CheckScriptFailure(['vim9script'] + lines, 'E741:', 3) +enddef + def Test_range_no_colon() CheckDefFailure(['%s/a/b/'], 'E1050:') CheckDefFailure(['+ s/a/b/'], 'E1050:') 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 */ /**/ + 1685, +/**/ 1684, /**/ 1683, diff --git a/src/vim.h b/src/vim.h --- a/src/vim.h +++ b/src/vim.h @@ -2136,7 +2136,8 @@ typedef enum { // Flags for assignment functions. #define LET_IS_CONST 1 // ":const" -#define LET_NO_COMMAND 2 // "var = expr" without ":let" or ":const" +#define LET_FORCEIT 2 // ":const!" (LET_IS_CONST is also set) +#define LET_NO_COMMAND 4 // "var = expr" without ":let" or ":const" #include "ex_cmds.h" // Ex command defines #include "spell.h" // spell checking stuff diff --git a/src/vim9.h b/src/vim9.h --- a/src/vim9.h +++ b/src/vim9.h @@ -58,6 +58,8 @@ typedef enum { ISN_UNLET, // unlet variable isn_arg.unlet.ul_name ISN_UNLETENV, // unlet environment variable isn_arg.unlet.ul_name + ISN_LOCKCONST, // lock constant value + // constants ISN_PUSHNR, // push number isn_arg.number ISN_PUSHBOOL, // push bool value isn_arg.number diff --git a/src/vim9compile.c b/src/vim9compile.c --- a/src/vim9compile.c +++ b/src/vim9compile.c @@ -1109,6 +1109,20 @@ generate_UNLET(cctx_T *cctx, isntype_T i } /* + * Generate an ISN_LOCKCONST instruction. + */ + static int +generate_LOCKCONST(cctx_T *cctx) +{ + isn_T *isn; + + RETURN_OK_IF_SKIP(cctx); + if ((isn = generate_instr(cctx, ISN_LOCKCONST)) == NULL) + return FAIL; + return OK; +} + +/* * Generate an ISN_LOADS instruction. */ static int @@ -4342,7 +4356,7 @@ compile_nested_function(exarg_T *eap, cc ufunc_T *ufunc; int r; - if (*name_start == '!') + if (eap->forceit) { emsg(_(e_cannot_use_bang_with_nested_def)); return NULL; @@ -5232,6 +5246,11 @@ compile_assignment(char_u *arg, exarg_T } else { + if (is_decl && eap->forceit && cmdidx == CMD_const + && (dest == dest_script || dest == dest_local)) + // ":const! var": lock the value, but not referenced variables + generate_LOCKCONST(cctx); + switch (dest) { case dest_option: @@ -6362,13 +6381,8 @@ compile_put(char_u *arg, exarg_T *eap, c char_u *line = arg; linenr_T lnum; char *errormsg; - int above = FALSE; - - if (*arg == '!') - { - above = TRUE; - line = skipwhite(arg + 1); - } + int above = eap->forceit; + eap->regname = *line; if (eap->regname == '=') @@ -6411,7 +6425,7 @@ compile_exec(char_u *line, exarg_T *eap, if (eap->cmdidx >= 0 && eap->cmdidx < CMD_SIZE) { - long argt = excmd_get_argt(eap->cmdidx); + long argt = eap->argt; int usefilter = FALSE; has_expr = argt & (EX_XFILE | EX_EXPAND); @@ -6870,8 +6884,6 @@ compile_def_function(ufunc_T *ufunc, int } } - p = skipwhite(p); - if (cctx.ctx_had_return && ea.cmdidx != CMD_elseif && ea.cmdidx != CMD_else @@ -6886,6 +6898,18 @@ compile_def_function(ufunc_T *ufunc, int goto erret; } + p = skipwhite(p); + if (ea.cmdidx != CMD_SIZE + && ea.cmdidx != CMD_write && ea.cmdidx != CMD_read) + { + ea.argt = excmd_get_argt(ea.cmdidx); + if ((ea.argt & EX_BANG) && *p == '!') + { + ea.forceit = TRUE; + p = skipwhite(p + 1); + } + } + switch (ea.cmdidx) { case CMD_def: @@ -7309,6 +7333,7 @@ delete_instr(isn_T *isn) case ISN_LOADTDICT: case ISN_LOADV: case ISN_LOADWDICT: + case ISN_LOCKCONST: case ISN_MEMBER: case ISN_NEGATENR: case ISN_NEWDICT: diff --git a/src/vim9execute.c b/src/vim9execute.c --- a/src/vim9execute.c +++ b/src/vim9execute.c @@ -678,6 +678,21 @@ call_partial(typval_T *tv, int argcount_ } /* + * Check if "lock" is VAR_LOCKED or VAR_FIXED. If so give an error and return + * TRUE. + */ + static int +error_if_locked(int lock, char *error) +{ + if (lock & (VAR_LOCKED | VAR_FIXED)) + { + emsg(_(error)); + return TRUE; + } + return FALSE; +} + +/* * Store "tv" in variable "name". * This is for s: and g: variables. */ @@ -1455,12 +1470,12 @@ call_def_function( typval_T *tv_list = STACK_TV_BOT(-1); list_T *list = tv_list->vval.v_list; + SOURCING_LNUM = iptr->isn_lnum; if (lidx < 0 && list->lv_len + lidx >= 0) // negative index is relative to the end lidx = list->lv_len + lidx; if (lidx < 0 || lidx > list->lv_len) { - SOURCING_LNUM = iptr->isn_lnum; semsg(_(e_listidx), lidx); goto on_error; } @@ -1469,12 +1484,18 @@ call_def_function( { listitem_T *li = list_find(list, lidx); + if (error_if_locked(li->li_tv.v_lock, + e_cannot_change_list_item)) + goto failed; // overwrite existing list item clear_tv(&li->li_tv); li->li_tv = *tv; } else { + if (error_if_locked(list->lv_lock, + e_cannot_change_list)) + goto failed; // append to list, only fails when out of memory if (list_append_tv(list, tv) == FAIL) goto failed; @@ -1495,9 +1516,9 @@ call_def_function( dict_T *dict = tv_dict->vval.v_dict; dictitem_T *di; + SOURCING_LNUM = iptr->isn_lnum; if (dict == NULL) { - SOURCING_LNUM = iptr->isn_lnum; emsg(_(e_dictionary_not_set)); goto on_error; } @@ -1507,12 +1528,18 @@ call_def_function( di = dict_find(dict, key, -1); if (di != NULL) { + if (error_if_locked(di->di_tv.v_lock, + e_cannot_change_dict_item)) + goto failed; // overwrite existing value clear_tv(&di->di_tv); di->di_tv = *tv; } else { + if (error_if_locked(dict->dv_lock, + e_cannot_change_dict)) + goto failed; // add to dict, only fails when out of memory if (dict_add_tv(dict, (char *)key, tv) == FAIL) goto failed; @@ -1603,6 +1630,10 @@ call_def_function( vim_unsetenv(iptr->isn_arg.unlet.ul_name); break; + case ISN_LOCKCONST: + item_lock(STACK_TV_BOT(-1), 100, TRUE, TRUE); + break; + // create a list from items on the stack; uses a single allocation // for the list header and the items case ISN_NEWLIST: @@ -3025,6 +3056,9 @@ ex_disassemble(exarg_T *eap) iptr->isn_arg.unlet.ul_forceit ? "!" : "", iptr->isn_arg.unlet.ul_name); break; + case ISN_LOCKCONST: + smsg("%4d LOCKCONST", current); + break; case ISN_NEWLIST: smsg("%4d NEWLIST size %lld", current, (long long)(iptr->isn_arg.number));