From aacc966c5d0ed91e33ed32b08f17cf4df3ca1394 Mon Sep 17 00:00:00 2001
From: Bram Moolenaar <Bram@vim.org>
Date: Fri, 13 Aug 2021 19:40:51 +0200
Subject: [PATCH] patch 8.2.3339: Vim9: cannot lock a member in a local dict

Problem:    Vim9: cannot lock a member in a local dict.
Solution:   Get the local dict from the stack and pass it to get_lval().
---
 src/eval.c                            | 30 +++++++++-----
 src/globals.h                         |  2 +
 src/testdir/test_vim9_cmd.vim         | 17 ++++++++
 src/testdir/test_vim9_disassemble.vim | 19 +++++++++
 src/version.c                         |  2 +
 src/vim9.h                            |  1 +
 src/vim9compile.c                     | 25 +++++++++---
 src/vim9execute.c                     | 58 ++++++++++++++++++++-------
 8 files changed, 122 insertions(+), 32 deletions(-)

diff --git a/src/eval.c b/src/eval.c
index edef8785d5..0dd2058872 100644
--- a/src/eval.c
+++ b/src/eval.c
@@ -902,17 +902,26 @@ get_lval(
     if ((*p != '[' && *p != '.') || lp->ll_name == NULL)
 	return p;
 
-    cc = *p;
-    *p = NUL;
-    // When we would write to the variable pass &ht and prevent autoload.
-    writing = !(flags & GLV_READ_ONLY);
-    v = find_var(lp->ll_name, writing ? &ht : NULL,
+    if (in_vim9script() && lval_root != NULL)
+    {
+	// using local variable
+	lp->ll_tv = lval_root;
+    }
+    else
+    {
+	cc = *p;
+	*p = NUL;
+	// When we would write to the variable pass &ht and prevent autoload.
+	writing = !(flags & GLV_READ_ONLY);
+	v = find_var(lp->ll_name, writing ? &ht : NULL,
 					 (flags & GLV_NO_AUTOLOAD) || writing);
-    if (v == NULL && !quiet)
-	semsg(_(e_undefined_variable_str), lp->ll_name);
-    *p = cc;
-    if (v == NULL)
-	return NULL;
+	if (v == NULL && !quiet)
+	    semsg(_(e_undefined_variable_str), lp->ll_name);
+	*p = cc;
+	if (v == NULL)
+	    return NULL;
+	lp->ll_tv = &v->di_tv;
+    }
 
     if (in_vim9script() && (flags & GLV_NO_DECL) == 0)
     {
@@ -924,7 +933,6 @@ get_lval(
     /*
      * Loop until no more [idx] or .key is following.
      */
-    lp->ll_tv = &v->di_tv;
     var1.v_type = VAR_UNKNOWN;
     var2.v_type = VAR_UNKNOWN;
     while (*p == '[' || (*p == '.' && p[1] != '=' && p[1] != '.'))
diff --git a/src/globals.h b/src/globals.h
index ebf236b000..f09ad482fb 100644
--- a/src/globals.h
+++ b/src/globals.h
@@ -1835,6 +1835,8 @@ EXTERN int  timer_busy INIT(= 0);   // when timer is inside vgetc() then > 0
 #endif
 #ifdef FEAT_EVAL
 EXTERN int  input_busy INIT(= 0);   // when inside get_user_input() then > 0
+
+EXTERN typval_T	*lval_root INIT(= NULL);
 #endif
 
 #ifdef FEAT_BEVAL_TERM
diff --git a/src/testdir/test_vim9_cmd.vim b/src/testdir/test_vim9_cmd.vim
index 84cb6d6650..7ec43e9d58 100644
--- a/src/testdir/test_vim9_cmd.vim
+++ b/src/testdir/test_vim9_cmd.vim
@@ -1195,6 +1195,23 @@ def Test_lockvar()
   s:theList[1] = 44
   assert_equal([1, 44, 3], s:theList)
 
+  var d = {a: 1, b: 2}
+  d.a = 3
+  d.b = 4
+  assert_equal({a: 3, b: 4}, d)
+  lockvar d.a
+  d.b = 5
+  var ex = ''
+  try
+    d.a = 6
+  catch
+    ex = v:exception
+  endtry
+  assert_match('E1121:', ex)
+  unlockvar d.a
+  d.a = 7
+  assert_equal({a: 7, b: 5}, d)
+
   var lines =<< trim END
       vim9script
       var theList = [1, 2, 3]
diff --git a/src/testdir/test_vim9_disassemble.vim b/src/testdir/test_vim9_disassemble.vim
index 1d1730e004..15a68dd502 100644
--- a/src/testdir/test_vim9_disassemble.vim
+++ b/src/testdir/test_vim9_disassemble.vim
@@ -588,6 +588,25 @@ def Test_disassemble_unlet()
         res)
 enddef
 
+def s:LockLocal()
+  var d = {a: 1}
+  lockvar d.a
+enddef
+
+def Test_disassemble_locl_local()
+  var res = execute('disass s:LockLocal')
+  assert_match('<SNR>\d*_LockLocal\_s*' ..
+        'var d = {a: 1}\_s*' ..
+        '\d PUSHS "a"\_s*' ..
+        '\d PUSHNR 1\_s*' ..
+        '\d NEWDICT size 1\_s*' ..
+        '\d STORE $0\_s*' ..
+        'lockvar d.a\_s*' ..
+        '\d LOAD $0\_s*' ..
+        '\d LOCKUNLOCK lockvar d.a\_s*',
+        res)
+enddef
+
 def s:ScriptFuncTry()
   try
     echo "yes"
diff --git a/src/version.c b/src/version.c
index 1acc17adf4..4c1be4ecf1 100644
--- a/src/version.c
+++ b/src/version.c
@@ -755,6 +755,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    3339,
 /**/
     3338,
 /**/
diff --git a/src/vim9.h b/src/vim9.h
index da39949fa3..67c9a710a5 100644
--- a/src/vim9.h
+++ b/src/vim9.h
@@ -70,6 +70,7 @@ typedef enum {
     ISN_UNLETINDEX,	// unlet item of list or dict
     ISN_UNLETRANGE,	// unlet items of list
 
+    ISN_LOCKUNLOCK,	// :lock and :unlock for local variable member
     ISN_LOCKCONST,	// lock constant value
 
     // constants
diff --git a/src/vim9compile.c b/src/vim9compile.c
index d6a7e0aa37..e29d963778 100644
--- a/src/vim9compile.c
+++ b/src/vim9compile.c
@@ -2262,12 +2262,12 @@ generate_PUT(cctx_T *cctx, int regname, linenr_T lnum)
 }
 
     static int
-generate_EXEC(cctx_T *cctx, char_u *line)
+generate_EXEC(cctx_T *cctx, isntype_T isntype, char_u *line)
 {
     isn_T	*isn;
 
     RETURN_OK_IF_SKIP(cctx);
-    if ((isn = generate_instr(cctx, ISN_EXEC)) == NULL)
+    if ((isn = generate_instr(cctx, isntype)) == NULL)
 	return FAIL;
     isn->isn_arg.string = vim_strsave(line);
     return OK;
@@ -7426,6 +7426,7 @@ compile_lock_unlock(
     int		ret = OK;
     size_t	len;
     char_u	*buf;
+    isntype_T	isn = ISN_EXEC;
 
     if (cctx->ctx_skip == SKIP_YES)
 	return OK;
@@ -7437,8 +7438,19 @@ compile_lock_unlock(
 
 	if (lookup_local(p, end - p, NULL, cctx) == OK)
 	{
-	    emsg(_(e_cannot_lock_unlock_local_variable));
-	    return FAIL;
+	    char_u *s = p;
+
+	    if (*end != '.' && *end != '[')
+	    {
+		emsg(_(e_cannot_lock_unlock_local_variable));
+		return FAIL;
+	    }
+
+	    // For "d.member" put the local variable on the stack, it will be
+	    // passed to ex_lockvar() indirectly.
+	    if (compile_load(&s, end, cctx, FALSE, FALSE) == FAIL)
+		return FAIL;
+	    isn = ISN_LOCKUNLOCK;
 	}
     }
 
@@ -7453,7 +7465,7 @@ compile_lock_unlock(
 	vim_snprintf((char *)buf, len, "%s %s",
 		eap->cmdidx == CMD_lockvar ? "lockvar" : "unlockvar",
 		p);
-	ret = generate_EXEC(cctx, buf);
+	ret = generate_EXEC(cctx, isn, buf);
 
 	vim_free(buf);
 	*name_end = cc;
@@ -9110,7 +9122,7 @@ compile_exec(char_u *line_arg, exarg_T *eap, cctx_T *cctx)
 	generate_EXECCONCAT(cctx, count);
     }
     else
-	generate_EXEC(cctx, line);
+	generate_EXEC(cctx, ISN_EXEC, line);
 
 theend:
     if (*nextcmd != NUL)
@@ -10198,6 +10210,7 @@ delete_instr(isn_T *isn)
 	case ISN_LOADOPT:
 	case ISN_LOADT:
 	case ISN_LOADW:
+	case ISN_LOCKUNLOCK:
 	case ISN_PUSHEXC:
 	case ISN_PUSHFUNC:
 	case ISN_PUSHS:
diff --git a/src/vim9execute.c b/src/vim9execute.c
index 84de4fee5a..04d5e5f27c 100644
--- a/src/vim9execute.c
+++ b/src/vim9execute.c
@@ -1396,6 +1396,27 @@ fill_partial_and_closure(partial_T *pt, ufunc_T *ufunc, ectx_T *ectx)
     return OK;
 }
 
+/*
+ * Execute iptr->isn_arg.string as an Ex command.
+ */
+    static int
+exec_command(isn_T *iptr)
+{
+    source_cookie_T cookie;
+
+    SOURCING_LNUM = iptr->isn_lnum;
+    // Pass getsourceline to get an error for a missing ":end"
+    // command.
+    CLEAR_FIELD(cookie);
+    cookie.sourcing_lnum = iptr->isn_lnum - 1;
+    if (do_cmdline(iptr->isn_arg.string,
+		getsourceline, &cookie,
+			     DOCMD_VERBOSE|DOCMD_NOWAIT|DOCMD_KEYTYPED) == FAIL
+		|| did_emsg)
+	return FAIL;
+    return OK;
+}
+
 // used for v_instr of typval of VAR_INSTR
 struct instr_S {
     ectx_T	*instr_ectx;
@@ -1637,21 +1658,8 @@ exec_instructions(ectx_T *ectx)
 	{
 	    // execute Ex command line
 	    case ISN_EXEC:
-		{
-		    source_cookie_T cookie;
-
-		    SOURCING_LNUM = iptr->isn_lnum;
-		    // Pass getsourceline to get an error for a missing ":end"
-		    // command.
-		    CLEAR_FIELD(cookie);
-		    cookie.sourcing_lnum = iptr->isn_lnum - 1;
-		    if (do_cmdline(iptr->isn_arg.string,
-				getsourceline, &cookie,
-				   DOCMD_VERBOSE|DOCMD_NOWAIT|DOCMD_KEYTYPED)
-									== FAIL
-				|| did_emsg)
-			goto on_error;
-		}
+		if (exec_command(iptr) == FAIL)
+		    goto on_error;
 		break;
 
 	    // execute Ex command line split at NL characters.
@@ -2880,6 +2888,23 @@ exec_instructions(ectx_T *ectx)
 		vim_unsetenv(iptr->isn_arg.unlet.ul_name);
 		break;
 
+	    case ISN_LOCKUNLOCK:
+		{
+		    typval_T	*lval_root_save = lval_root;
+		    int		res;
+
+		    // Stack has the local variable, argument the whole :lock
+		    // or :unlock command, like ISN_EXEC.
+		    --ectx->ec_stack.ga_len;
+		    lval_root = STACK_TV_BOT(0);
+		    res = exec_command(iptr);
+		    clear_tv(lval_root);
+		    lval_root = lval_root_save;
+		    if (res == FAIL)
+			goto on_error;
+		}
+		break;
+
 	    case ISN_LOCKCONST:
 		item_lock(STACK_TV_BOT(-1), 100, TRUE, TRUE);
 		break;
@@ -5244,6 +5269,9 @@ list_instructions(char *pfx, isn_T *instr, int instr_count, ufunc_T *ufunc)
 	    case ISN_UNLETRANGE:
 		smsg("%s%4d UNLETRANGE", pfx, current);
 		break;
+	    case ISN_LOCKUNLOCK:
+		smsg("%s%4d LOCKUNLOCK %s", pfx, current, iptr->isn_arg.string);
+		break;
 	    case ISN_LOCKCONST:
 		smsg("%s%4d LOCKCONST", pfx, current);
 		break;
-- 
GitLab