changeset 28002:1012048eed26 v8.2.4526

patch 8.2.4526: Vim9: cannot set variables to a null value Commit: https://github.com/vim/vim/commit/8acb9cc6209768ca7ec75c9f7af8c389312ea8d6 Author: Bram Moolenaar <Bram@vim.org> Date: Tue Mar 8 13:18:55 2022 +0000 patch 8.2.4526: Vim9: cannot set variables to a null value Problem: Vim9: cannot set variables to a null value. Solution: Add null_list, null_job, etc.
author Bram Moolenaar <Bram@vim.org>
date Tue, 08 Mar 2022 14:30:05 +0100
parents 5357aeb3985f
children 2e2a2a7058b4
files runtime/doc/vim9.txt src/eval.c src/evalvars.c src/proto/eval.pro src/testdir/test_expr.vim src/testdir/test_vim9_assign.vim src/testdir/test_vim9_disassemble.vim src/testdir/test_vim9_func.vim src/version.c src/vim9.h src/vim9compile.c src/vim9execute.c src/vim9expr.c src/vim9instr.c src/vim9script.c src/vim9type.c
diffstat 16 files changed, 346 insertions(+), 51 deletions(-) [+]
line wrap: on
line diff
--- a/runtime/doc/vim9.txt
+++ b/runtime/doc/vim9.txt
@@ -94,8 +94,20 @@ script and `:def` functions; details are
 	def CallMe(count: number, message: string): bool
 - Call functions without `:call`: >
 	writefile(['done'], 'file.txt')
-- You cannot use old Ex commands `:xit`, `:t`, `:k`, `:append`, `:change`,
-  `:insert`, `:open`, and `:s` or `:d` with only flags.
+- You cannot use old Ex commands:
+	`:Print`
+	`:append`
+	`:change`
+	`:d`  directly followed by 'd' or 'p'.
+	`:insert`
+	`:k`
+	`:mode`
+	`:open`
+	`:s`  with only flags
+	`:t`
+  	`:xit`
+- Some commands, especially those used for flow control, cannot be shortened.
+  E.g., `:throw` cannot be written as `:th`. *E839*
 - You cannot use curly-braces names.
 - A range before a command must be prefixed with a colon: >
 	:%s/this/that
@@ -305,7 +317,7 @@ function, the function does not need to 
 
 
 Variable declarations with :var, :final and :const ~
-				*vim9-declaration* *:var*
+				*vim9-declaration* *:var* *E1079*
 				*E1017* *E1020* *E1054* *E1087* *E1108* *E1124*
 Local variables need to be declared with `:var`.  Local constants need to be
 declared with `:final` or `:const`.  We refer to both as "variables" in this
@@ -375,6 +387,9 @@ In Vim9 script `:let` cannot be used.  A
 without any command.  The same for global, window, tab, buffer and Vim
 variables, because they are not really declared.  Those can also be deleted
 with `:unlet`.
+							*E1065*
+You cannot use `:va` to declare a variable, it must be written with the full
+name `:var`.  Just to make sure it is easy to read.
 							*E1178*
 `:lockvar` does not work on local variables.  Use `:const` and `:final`
 instead.
@@ -952,10 +967,37 @@ always converted to string: >
 Simple types are Number, Float, Special and Bool.  For other types |string()|
 should be used.
 						*false* *true* *null* *E1034*
-In Vim9 script one can use "true" for v:true, "false" for v:false and "null"
-for v:null.  When converting a boolean to a string "false" and "true" are
-used, not "v:false" and "v:true" like in legacy script.  "v:none" is not
-changed, it is only used in JSON and has no equivalent in other languages.
+In Vim9 script one can use the following predefined values: >
+	true
+	false
+	null
+	null_blob
+	null_channel
+	null_dict
+	null_function
+	null_job
+	null_list
+	null_partial
+	null_string
+`true` is the same as `v:true`, `false` the same as `v:false`, `null` the same
+as `v:null`.
+
+While `null` has the type "special", the other "null_" types have the type
+indicated by their name.  Quite often a null value is handled the same as an
+empty value, but not always.  The values can be useful to clear a script-local
+variable, since they cannot be deleted with `:unlet`.  E.g.: >
+	var theJob = job_start(...)
+	# let the job do its work
+	theJob = null_job
+
+The values can also be useful as the default value for an argument: >
+	def MyFunc(b: blob = null_blob)
+	   if b == null_blob
+	      # b argument was not given
+
+When converting a boolean to a string `false` and `true` are used, not
+`v:false` and `v:true` like in legacy script.  `v:none` has no `none`
+replacement, it has no equivalent in other languages.
 
 Indexing a string with [idx] or taking a slice with [idx : idx] uses character
 indexes instead of byte indexes.  Composing characters are included.
--- a/src/eval.c
+++ b/src/eval.c
@@ -943,6 +943,7 @@ get_lval(
 		    type_list = &SCRIPT_ITEM(current_sctx.sc_sid)->sn_type_list;
 		else
 		{
+		    // TODO: should we give an error here?
 		    type_list = &tmp_type_list;
 		    ga_init2(type_list, sizeof(type_T), 10);
 		}
@@ -3483,6 +3484,100 @@ eval_leader(char_u **arg, int vim9)
 }
 
 /*
+ * Check for a predefined value "true", "false" and "null.*".
+ * Return OK when recognized.
+ */
+    int
+handle_predefined(char_u *s, int len, typval_T *rettv)
+{
+    switch (len)
+    {
+	case 4: if (STRNCMP(s, "true", 4) == 0)
+		{
+		    rettv->v_type = VAR_BOOL;
+		    rettv->vval.v_number = VVAL_TRUE;
+		    return OK;
+		}
+		if (STRNCMP(s, "null", 4) == 0)
+		{
+		    rettv->v_type = VAR_SPECIAL;
+		    rettv->vval.v_number = VVAL_NULL;
+		    return OK;
+		}
+		break;
+	case 5: if (STRNCMP(s, "false", 5) == 0)
+		{
+		    rettv->v_type = VAR_BOOL;
+		    rettv->vval.v_number = VVAL_FALSE;
+		    return OK;
+		}
+		break;
+#ifdef FEAT_JOB_CHANNEL
+	case 8: if (STRNCMP(s, "null_job", 8) == 0)
+		{
+		    rettv->v_type = VAR_JOB;
+		    rettv->vval.v_job = NULL;
+		    return OK;
+		}
+		break;
+#endif
+	case 9:
+		if (STRNCMP(s, "null_", 5) != 0)
+		    break;
+		if (STRNCMP(s + 5, "list", 4) == 0)
+		{
+		    rettv->v_type = VAR_LIST;
+		    rettv->vval.v_list = NULL;
+		    return OK;
+		}
+		if (STRNCMP(s + 5, "dict", 4) == 0)
+		{
+		    rettv->v_type = VAR_DICT;
+		    rettv->vval.v_dict = NULL;
+		    return OK;
+		}
+		if (STRNCMP(s + 5, "blob", 4) == 0)
+		{
+		    rettv->v_type = VAR_BLOB;
+		    rettv->vval.v_blob = NULL;
+		    return OK;
+		}
+		break;
+	case 11: if (STRNCMP(s, "null_string", 11) == 0)
+		{
+		    rettv->v_type = VAR_STRING;
+		    rettv->vval.v_string = NULL;
+		    return OK;
+		}
+		break;
+	case 12:
+#ifdef FEAT_JOB_CHANNEL
+		if (STRNCMP(s, "null_channel", 12) == 0)
+		{
+		    rettv->v_type = VAR_CHANNEL;
+		    rettv->vval.v_channel = NULL;
+		    return OK;
+		}
+#endif
+		if (STRNCMP(s, "null_partial", 12) == 0)
+		{
+		    rettv->v_type = VAR_PARTIAL;
+		    rettv->vval.v_partial = NULL;
+		    return OK;
+		}
+		break;
+	case 13: if (STRNCMP(s, "null_function", 13) == 0)
+		{
+		    rettv->v_type = VAR_FUNC;
+		    rettv->vval.v_string = NULL;
+		    return OK;
+		}
+		break;
+    }
+    return FAIL;
+}
+
+/*
  * Handle sixth level expression:
  *  number		number constant
  *  0zFFFFFFFF		Blob constant
@@ -3757,26 +3852,11 @@ eval7(
 		ret = FAIL;
 	    else if (evaluate)
 	    {
-		// get the value of "true", "false" or a variable
-		if (len == 4 && vim9script && STRNCMP(s, "true", 4) == 0)
-		{
-		    rettv->v_type = VAR_BOOL;
-		    rettv->vval.v_number = VVAL_TRUE;
-		    ret = OK;
-		}
-		else if (len == 5 && vim9script && STRNCMP(s, "false", 5) == 0)
-		{
-		    rettv->v_type = VAR_BOOL;
-		    rettv->vval.v_number = VVAL_FALSE;
-		    ret = OK;
-		}
-		else if (len == 4 && vim9script && STRNCMP(s, "null", 4) == 0)
-		{
-		    rettv->v_type = VAR_SPECIAL;
-		    rettv->vval.v_number = VVAL_NULL;
-		    ret = OK;
-		}
-		else
+		// get the value of "true", "false", etc. or a variable
+		ret = FAIL;
+		if (vim9script)
+		    ret = handle_predefined(s, len, rettv);
+		if (ret == FAIL)
 		{
 		    name_start = s;
 		    ret = eval_variable(s, len, 0, rettv, NULL,
--- a/src/evalvars.c
+++ b/src/evalvars.c
@@ -999,6 +999,11 @@ ex_let_vars(
     listitem_T	*item;
     typval_T	ltv;
 
+    if (tv->v_type == VAR_VOID)
+    {
+	emsg(_(e_cannot_use_void_value));
+	return FAIL;
+    }
     if (*arg != '[')
     {
 	// ":let var = expr" or ":for var in list"
--- a/src/proto/eval.pro
+++ b/src/proto/eval.pro
@@ -42,6 +42,7 @@ int eval1(char_u **arg, typval_T *rettv,
 void eval_addblob(typval_T *tv1, typval_T *tv2);
 int eval_addlist(typval_T *tv1, typval_T *tv2);
 int eval_leader(char_u **arg, int vim9);
+int handle_predefined(char_u *s, int len, typval_T *rettv);
 int check_can_index(typval_T *rettv, int evaluate, int verbose);
 void f_slice(typval_T *argvars, typval_T *rettv);
 int eval_index_inner(typval_T *rettv, int is_range, typval_T *var1, typval_T *var2, int exclusive, char_u *key, int keylen, int verbose);
--- a/src/testdir/test_expr.vim
+++ b/src/testdir/test_expr.vim
@@ -157,12 +157,28 @@ endfunc
 
 func Test_loop_over_null_list()
   let lines =<< trim END
-      VAR null_list = test_null_list()
-      for i in null_list
+      VAR nulllist = test_null_list()
+      for i in nulllist
         call assert_report('should not get here')
       endfor
   END
   call v9.CheckLegacyAndVim9Success(lines)
+
+  let lines =<< trim END
+      var nulllist = null_list
+      for i in nulllist
+        call assert_report('should not get here')
+      endfor
+  END
+  call v9.CheckDefAndScriptSuccess(lines)
+
+  let lines =<< trim END
+      let nulllist = null_list
+      for i in nulllist
+        call assert_report('should not get here')
+      endfor
+  END
+  call v9.CheckScriptFailure(lines, 'E121: Undefined variable: null_list')
 endfunc
 
 func Test_setreg_null_list()
--- a/src/testdir/test_vim9_assign.vim
+++ b/src/testdir/test_vim9_assign.vim
@@ -306,12 +306,44 @@ def Test_assign_register()
 enddef
 
 def Test_reserved_name()
-  for name in ['true', 'false', 'null']
+  var more_names = ['null_job', 'null_channel']
+  if !has('job')
+    more_names = []
+  endif
+
+  for name in ['true',
+               'false',
+               'null',
+               'null_blob',
+               'null_dict',
+               'null_function',
+               'null_list',
+               'null_partial',
+               'null_string',
+               ] + more_names
     v9.CheckDefExecAndScriptFailure(['var ' .. name .. ' =  0'], 'E1034:')
     v9.CheckDefExecAndScriptFailure(['var ' .. name .. ': bool'], 'E1034:')
   endfor
 enddef
 
+def Test_null_values()
+  var lines =<< trim END
+      var b: blob = null_blob
+      var dn: dict<number> = null_dict
+      var ds: dict<string> = null_dict
+      var ln: list<number> = null_list
+      var ls: list<string> = null_list
+      var Ff: func(string): string = null_function
+      var Fp: func(number): number = null_partial
+      var s: string = null_string
+      if has('job')
+        var j: job = null_job
+        var c: channel = null_channel
+      endif
+  END
+  v9.CheckDefAndScriptSuccess(lines)
+enddef
+
 def Test_skipped_assignment()
   var lines =<< trim END
       for x in []
--- a/src/testdir/test_vim9_disassemble.vim
+++ b/src/testdir/test_vim9_disassemble.vim
@@ -415,6 +415,58 @@ def Test_disassemble_store_member()
         res)
 enddef
 
+if has('job')
+  def s:StoreNull()
+    var ss = null_string
+    var bb = null_blob
+    var dd = null_dict
+    var ll = null_list
+    var Ff = null_function
+    var Pp = null_partial
+    var jj = null_job
+    var cc = null_channel
+  enddef
+
+  def Test_disassemble_assign_null()
+    var res = execute('disass s:StoreNull')
+    assert_match('<SNR>\d*_StoreNull\_s*' ..
+          'var ss = null_string\_s*' ..
+          '\d\+ PUSHS "\[NULL\]"\_s*' ..
+          '\d\+ STORE $\d\_s*' ..
+
+          'var bb = null_blob\_s*' ..
+          '\d\+ PUSHBLOB 0z\_s*' ..
+          '\d\+ STORE $\d\_s*' ..
+
+          'var dd = null_dict\_s*' ..
+          '\d\+ NEWDICT size 0\_s*' ..
+          '\d\+ STORE $\d\_s*' ..
+
+          'var ll = null_list\_s*' ..
+          '\d\+ NEWLIST size 0\_s*' ..
+          '\d\+ STORE $\d\_s*' ..
+
+          'var Ff = null_function\_s*' ..
+          '\d\+ PUSHFUNC "\[none\]"\_s*' ..
+          '\d\+ STORE $\d\_s*' ..
+
+          'var Pp = null_partial\_s*' ..
+          '\d\+ NEWPARTIAL\_s*' ..
+          '\d\+ STORE $\d\_s*' ..
+
+          'var jj = null_job\_s*' ..
+          '\d\+ PUSHJOB "no process"\_s*' ..
+          '\d\+ STORE $\d\_s*' ..
+
+          'var cc = null_channel\_s*' ..
+          '\d\+ PUSHCHANNEL 0\_s*' ..
+          '\d\+ STORE $\d\_s*' ..
+
+          '\d\+ RETURN void',
+          res)
+  enddef
+endif
+
 def s:ScriptFuncStoreIndex()
   var d = {dd: {}}
   d.dd[0] = 0
--- a/src/testdir/test_vim9_func.vim
+++ b/src/testdir/test_vim9_func.vim
@@ -3326,7 +3326,7 @@ def Test_partial_call()
       var Expr: func(dict<any>): dict<any>
       const Call = Foo(Expr)
   END
-  v9.CheckScriptFailure(lines, 'E1235:')
+  v9.CheckScriptFailure(lines, 'E1031:')
 enddef
 
 def Test_partial_double_nested()
--- 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 */
 /**/
+    4526,
+/**/
     4525,
 /**/
     4524,
--- a/src/vim9.h
+++ b/src/vim9.h
@@ -91,6 +91,7 @@ typedef enum {
     ISN_PUSHJOB,	// push channel isn_arg.job
     ISN_NEWLIST,	// push list from stack items, size is isn_arg.number
     ISN_NEWDICT,	// push dict from stack items, size is isn_arg.number
+    ISN_NEWPARTIAL,	// push NULL partial
 
     ISN_AUTOLOAD,	// get item from autoload import, function or variable
 
--- a/src/vim9compile.c
+++ b/src/vim9compile.c
@@ -403,8 +403,12 @@ need_type_where(
     if (ret == OK)
 	return OK;
 
+    // If actual a constant a runtime check makes no sense.  If it's
+    // null_function it is OK.
+    if (actual_is_const && ret == MAYBE && actual == &t_func_unknown)
+	return OK;
+
     // If the actual type can be the expected type add a runtime check.
-    // If it's a constant a runtime check makes no sense.
     if (!actual_is_const && ret == MAYBE && use_typecheck(actual, expected))
     {
 	generate_TYPECHECK(cctx, expected, offset, where.wt_index);
--- a/src/vim9execute.c
+++ b/src/vim9execute.c
@@ -3440,6 +3440,17 @@ exec_instructions(ectx_T *ectx)
 		}
 		break;
 
+	    // create a partial with NULL value
+	    case ISN_NEWPARTIAL:
+		if (GA_GROW_FAILS(&ectx->ec_stack, 1))
+		    goto theend;
+		++ectx->ec_stack.ga_len;
+		tv = STACK_TV_BOT(-1);
+		tv->v_type = VAR_PARTIAL;
+		tv->v_lock = 0;
+		tv->vval.v_partial = NULL;
+		break;
+
 	    // call a :def function
 	    case ISN_DCALL:
 		SOURCING_LNUM = iptr->isn_lnum;
@@ -5720,6 +5731,9 @@ list_instructions(char *pfx, isn_T *inst
 		smsg("%s%4d NEWDICT size %lld", pfx, current,
 					    (varnumber_T)(iptr->isn_arg.number));
 		break;
+	    case ISN_NEWPARTIAL:
+		smsg("%s%4d NEWPARTIAL", pfx, current);
+		break;
 
 	    // function call
 	    case ISN_BCALL:
--- a/src/vim9expr.c
+++ b/src/vim9expr.c
@@ -2107,14 +2107,20 @@ compile_expr8(
 		    break;
 
 	/*
-	 * "null" constant
+	 * "null" or "null_*" constant
 	 */
-	case 'n':   if (STRNCMP(*arg, "null", 4) == 0
-						   && !eval_isnamec((*arg)[4]))
+	case 'n':   if (STRNCMP(*arg, "null", 4) == 0)
 		    {
-			*arg += 4;
-			rettv->v_type = VAR_SPECIAL;
-			rettv->vval.v_number = VVAL_NULL;
+			char_u  *p = *arg + 4;
+			int	len;
+
+			for (len = 0; eval_isnamec(p[len]); ++len)
+			    ;
+			ret = handle_predefined(*arg, len + 4, rettv);
+			if (ret == FAIL)
+			    ret = NOTDONE;
+			else
+			    *arg += len + 4;
 		    }
 		    else
 			ret = NOTDONE;
--- a/src/vim9instr.c
+++ b/src/vim9instr.c
@@ -570,6 +570,40 @@ generate_tv_PUSH(cctx_T *cctx, typval_T 
 		generate_PUSHBLOB(cctx, tv->vval.v_blob);
 		tv->vval.v_blob = NULL;
 		break;
+	    case VAR_LIST:
+		if (tv->vval.v_list != NULL)
+		    iemsg("non-empty list constant not supported");
+		generate_NEWLIST(cctx, 0);
+		break;
+	    case VAR_DICT:
+		if (tv->vval.v_dict != NULL)
+		    iemsg("non-empty dict constant not supported");
+		generate_NEWDICT(cctx, 0);
+		break;
+#ifdef FEAT_JOB_CHANNEL
+	    case VAR_JOB:
+		if (tv->vval.v_job != NULL)
+		    iemsg("non-null job constant not supported");
+		generate_PUSHJOB(cctx, NULL);
+		break;
+	    case VAR_CHANNEL:
+		if (tv->vval.v_channel != NULL)
+		    iemsg("non-null channel constant not supported");
+		generate_PUSHCHANNEL(cctx, NULL);
+		break;
+#endif
+	    case VAR_FUNC:
+		if (tv->vval.v_string != NULL)
+		    iemsg("non-null function constant not supported");
+		generate_PUSHFUNC(cctx, NULL, &t_func_unknown);
+		break;
+	    case VAR_PARTIAL:
+		if (tv->vval.v_partial != NULL)
+		    iemsg("non-null partial constant not supported");
+		if (generate_instr_type(cctx, ISN_NEWPARTIAL, &t_func_unknown)
+								       == NULL)
+		    return FAIL;
+		break;
 	    case VAR_STRING:
 		generate_PUSHS(cctx, &tv->vval.v_string);
 		tv->vval.v_string = NULL;
@@ -706,7 +740,7 @@ generate_PUSHJOB(cctx_T *cctx, job_T *jo
     isn_T	*isn;
 
     RETURN_OK_IF_SKIP(cctx);
-    if ((isn = generate_instr_type(cctx, ISN_PUSHJOB, &t_channel)) == NULL)
+    if ((isn = generate_instr_type(cctx, ISN_PUSHJOB, &t_job)) == NULL)
 	return FAIL;
     isn->isn_arg.job = job;
 
@@ -2185,6 +2219,7 @@ delete_instr(isn_T *isn)
 	case ISN_NEGATENR:
 	case ISN_NEWDICT:
 	case ISN_NEWLIST:
+	case ISN_NEWPARTIAL:
 	case ISN_OPANY:
 	case ISN_OPFLOAT:
 	case ISN_OPNR:
--- a/src/vim9script.c
+++ b/src/vim9script.c
@@ -1062,6 +1062,14 @@ static char *reserved[] = {
     "true",
     "false",
     "null",
+    "null_blob",
+    "null_dict",
+    "null_function",
+    "null_list",
+    "null_partial",
+    "null_string",
+    "null_channel",
+    "null_job",
     "this",
     NULL
 };
--- a/src/vim9type.c
+++ b/src/vim9type.c
@@ -567,22 +567,19 @@ check_typval_type(type_T *expected, typv
 
     if (expected == NULL)
 	return OK;  // didn't expect anything.
+		    //
+    ga_init2(&type_list, sizeof(type_T *), 10);
 
-    // For some values there is no type, assume an error will be given later
-    // for an invalid value.
+    // A null_function and null_partial are special cases, they can be used to
+    // clear a variable.
     if ((actual_tv->v_type == VAR_FUNC && actual_tv->vval.v_string == NULL)
 	    || (actual_tv->v_type == VAR_PARTIAL
 					 && actual_tv->vval.v_partial == NULL))
-    {
-	emsg(_(e_function_reference_is_not_set));
-	return FAIL;
-    }
-
-    ga_init2(&type_list, sizeof(type_T *), 10);
-
-    // When the actual type is list<any> or dict<any> go through the values to
-    // possibly get a more specific type.
-    actual_type = typval2type(actual_tv, get_copyID(), &type_list,
+	actual_type = &t_func_unknown;
+    else
+	// When the actual type is list<any> or dict<any> go through the values
+	// to possibly get a more specific type.
+	actual_type = typval2type(actual_tv, get_copyID(), &type_list,
 					  TVTT_DO_MEMBER | TVTT_MORE_SPECIFIC);
     if (actual_type != NULL)
     {