Skip to content

Commit

Permalink
patch 8.2.1685: Vim9: cannot declare a constant value
Browse files Browse the repository at this point in the history
Problem:    Vim9: cannot declare a constant value.
Solution:   Introduce ":const!".
  • Loading branch information
brammool committed Sep 14, 2020
1 parent efd5d8a commit 0b4c66c
Show file tree
Hide file tree
Showing 12 changed files with 204 additions and 38 deletions.
82 changes: 67 additions & 15 deletions runtime/doc/vim9.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 ~
Expand Down
8 changes: 8 additions & 0 deletions src/errors.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion src/eval.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
15 changes: 8 additions & 7 deletions src/evalvars.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/ex_cmds.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion src/proto/evalvars.pro
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
40 changes: 40 additions & 0 deletions src/testdir/test_vim9_script.vim
Original file line number Diff line number Diff line change
Expand Up @@ -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:')
Expand Down
2 changes: 2 additions & 0 deletions src/version.c
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,8 @@ static char *(features[]) =

static int included_patches[] =
{ /* Add new patch number below this line */
/**/
1685,
/**/
1684,
/**/
Expand Down
3 changes: 2 additions & 1 deletion src/vim.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/vim9.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 0b4c66c

Please sign in to comment.