]> granicus.if.org Git - vim/commitdiff
patch 8.2.1685: Vim9: cannot declare a constant value v8.2.1685
authorBram Moolenaar <Bram@vim.org>
Mon, 14 Sep 2020 19:39:44 +0000 (21:39 +0200)
committerBram Moolenaar <Bram@vim.org>
Mon, 14 Sep 2020 19:39:44 +0000 (21:39 +0200)
Problem:    Vim9: cannot declare a constant value.
Solution:   Introduce ":const!".

12 files changed:
runtime/doc/vim9.txt
src/errors.h
src/eval.c
src/evalvars.c
src/ex_cmds.h
src/proto/evalvars.pro
src/testdir/test_vim9_script.vim
src/version.c
src/vim.h
src/vim9.h
src/vim9compile.c
src/vim9execute.c

index a32ce183c371081f5724ce5ea355466830e69ee3..71c454ae9496cad0c553c90d451da34e3e7d1404 100644 (file)
@@ -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 available later, a block can be used:
        }
        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 much like JavaScript, but there are a
 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 ~
index c76afbc839d20052047ed583a7a9fa40352468b3..badbabd3aac9db5b9534c2f5b323386ee6dbc490 100644 (file)
@@ -258,4 +258,12 @@ EXTERN char e_assert_fails_fifth_argument[]
        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
index 18f3a5bc05ae7b66406ffeb8f2cecbec2659fbee..79c548809688dca601fc5c557bad242659729629 100644 (file)
@@ -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;
index bcc425da5ab19ce8c1538e2c3dd675056b78b3a6..1c40f4111f78f7f8e331121038fcb24c0b544114 100644 (file)
@@ -173,7 +173,6 @@ static char_u *list_arg_vars(exarg_T *eap, char_u *arg, int *first);
 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.
index 3977c4d6ee4f7cbdf23932abe1314237f56b55f1..8ef9eb41de5f65f4a1317f158df3f48b6d859359 100644 (file)
@@ -398,7 +398,7 @@ EXCMD(CMD_confirm,  "confirm",      ex_wrongmodifier,
        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,
index f304872230ea1a36f5bbf2b2984759a4d087f840..010e630d7e9d7ae39bb7899bfb61e71535b20b64 100644 (file)
@@ -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);
index 63581818e951b726b953818664aa86d2ad828e13..904ee634de1a3ba268e3ffd431ca2ba865052039 100644 (file)
@@ -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:')
index 67ccfb6215464400d0c1e6446bf85a4cca70531d..f958df91d01ccdd478dabd051e8e0f910e95e904 100644 (file)
@@ -750,6 +750,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    1685,
 /**/
     1684,
 /**/
index 60a565b0a56a3514d051d5177efd36aba30ec04b..208128e697fa24497c78736b4e2b7937b8dc59a2 100644 (file)
--- 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
index 367c05ce0b56fdc02dce45c4911e4cf6a6344f22..310b59e719889618b63232e7e12b10076f4f2ffe 100644 (file)
@@ -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
index dd3eff49cd303c8bd56a0875f608e9994e87e016..103e696bf03ce0609b4710f5fe3f72794a284d4b 100644 (file)
@@ -1108,6 +1108,20 @@ generate_UNLET(cctx_T *cctx, isntype_T isn_type, char_u *name, int forceit)
     return OK;
 }
 
+/*
+ * 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.
  */
@@ -4342,7 +4356,7 @@ compile_nested_function(exarg_T *eap, cctx_T *cctx)
     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 *eap, cmdidx_T cmdidx, cctx_T *cctx)
        }
        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, cctx_T *cctx)
     char_u     *line = arg;
     linenr_T   lnum;
     char       *errormsg;
-    int                above = FALSE;
+    int                above = eap->forceit;
 
-    if (*arg == '!')
-    {
-       above = TRUE;
-       line = skipwhite(arg + 1);
-    }
     eap->regname = *line;
 
     if (eap->regname == '=')
@@ -6411,7 +6425,7 @@ compile_exec(char_u *line, exarg_T *eap, cctx_T *cctx)
 
     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 set_return_type, cctx_T *outer_cctx)
            }
        }
 
-       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 set_return_type, cctx_T *outer_cctx)
            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:
index c2bd857ab02fc545765c9ac1eac7ee67b4d96481..cd6990f90db12d396c1af02840273352a4f7b91f 100644 (file)
@@ -677,6 +677,21 @@ call_partial(typval_T *tv, int argcount_arg, ectx_T *ectx)
     return OK;
 }
 
+/*
+ * 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));