Problem: Vim9: cannot declare a constant value.
Solution: Introduce ":const!".
-*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
}
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
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
few exceptions.
type TRUE when ~
- bool v:true
+ bool v:true or 1
number non-zero
float non-zero
string non-empty
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 ~
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
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;
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);
// 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)
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;
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
{
* 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;
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;
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.
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,
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);
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);
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:')
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 1685,
/**/
1684,
/**/
// 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
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
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.
*/
ufunc_T *ufunc;
int r;
- if (*name_start == '!')
+ if (eap->forceit)
{
emsg(_(e_cannot_use_bang_with_nested_def));
return NULL;
}
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:
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 == '=')
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);
}
}
- p = skipwhite(p);
-
if (cctx.ctx_had_return
&& ea.cmdidx != CMD_elseif
&& ea.cmdidx != CMD_else
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:
case ISN_LOADTDICT:
case ISN_LOADV:
case ISN_LOADWDICT:
+ case ISN_LOCKCONST:
case ISN_MEMBER:
case ISN_NEGATENR:
case ISN_NEWDICT:
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.
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;
}
{
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;
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;
}
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;
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:
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));