patch 9.0.1955: Vim9: lockvar issues with objects/classes

Commit: 
https://github.com/vim/vim/commit/ee865f37acab6cac2cee6a171d60e1b365f852b0
Author: Ernie Rael <err...@raelity.com>
Date:   Fri Sep 29 19:53:55 2023 +0200

    patch 9.0.1955: Vim9: lockvar issues with objects/classes
    
    Problem:  Vim9: lockvar issues with objects/classes
    Solution: fix `get_lhs()` object/class access and avoid `SEGV`,
              make error messages more accurate.
    
    - `get_lval()` detects/returns object/class access
    - `compile_lock_unlock()` generate code for bare static and obj_arg access
    - `do_lock_var()` check lval for `ll_object`/`ll_class` and fail if so.
    
    Details:
    - Add `ll_object`/`ll_class`/`ll_oi` to `lval_T`.
    - Add `lockunlock_T` to `isn_T` for `is_arg` to specify handling of 
`lval_root` in `get_lval()`.
    - In `get_lval()`, fill in `ll_object`/`ll_class`/`ll_oi` as needed; when 
no `[idx] or .key`, check lval_root on the way out.
    - In `do_lock_var()` check for `ll_object`/`ll_class`; also bullet proof 
ll_dict case
      and give `Dictionay required` if problem. (not needed to avoid lockvar 
crash anymore)
    - In `compile_lock_unlock()` compile for the class variable and func arg 
cases.
    
    closes: #13174
    
    Signed-off-by: Christian Brabandt <c...@256bit.org>
    Co-authored-by: Ernie Rael <err...@raelity.com>

diff --git a/src/errors.h b/src/errors.h
index 19779ebd0..6b4416963 100644
--- a/src/errors.h
+++ b/src/errors.h
@@ -3534,8 +3534,12 @@ EXTERN char e_missing_name_after_implements[]
        INIT(= N_("E1389: Missing name after implements"));
 EXTERN char e_cannot_use_an_object_variable_except_with_the_new_method_str[]
        INIT(= N_("E1390: Cannot use an object variable \"this.%s\" except with 
the \"new\" method"));
+EXTERN char e_cannot_lock_object_variable_str[]
+       INIT(= N_("E1391: Cannot (un)lock variable \"%s\" in class \"%s\""));
+EXTERN char e_cannot_lock_class_variable_str[]
+       INIT(= N_("E1392: Cannot (un)lock class variable \"%s\" in class 
\"%s\""));
 #endif
-// E1391 - E1499 unused (reserved for Vim9 class support)
+// E1393 - E1499 unused (reserved for Vim9 class support)
 EXTERN char e_cannot_mix_positional_and_non_positional_str[]
        INIT(= N_("E1500: Cannot mix positional and non-positional arguments: 
%s"));
 EXTERN char e_fmt_arg_nr_unused_str[]
diff --git a/src/eval.c b/src/eval.c
index 5edda6820..ae48a6085 100644
--- a/src/eval.c
+++ b/src/eval.c
@@ -985,6 +985,62 @@ eval_foldexpr(win_T *wp, int *cp)
 }
 #endif
 
+/*
+ * Fill in "lp" using "root". This is used in a special case when
+ * "get_lval()" parses a bare word when "lval_root" is not NULL.
+ *
+ * This is typically called with "lval_root" as "root". For a class, find
+ * the name from lp in the class from root, fill in lval_T if found. For a
+ * complex type, list/dict use it as the result; just put the root into
+ * ll_tv.
+ *
+ * "lval_root" is a hack used during run-time/instr-execution to provide the
+ * starting point for "get_lval()" to traverse a chain of indexes. In some
+ * cases get_lval sees a bare name and uses this function to populate the
+ * lval_T.
+ *
+ * For setting up "lval_root" (currently only used with lockvar)
+ *     compile_lock_unlock - pushes object on stack (which becomes lval_root)
+ *     execute_instructions: ISN_LOCKUNLOCK - sets lval_root from stack.
+ */
+    static void
+get_lval_root(lval_T *lp, typval_T *root, int is_arg)
+{
+#ifdef LOG_LOCKVAR
+    ch_log(NULL, "LKVAR: get_lvalroot(): name %s", lp->ll_name);
+#endif
+    if (!is_arg && root->v_type == VAR_CLASS)
+    {
+       if (root->vval.v_class != NULL)
+       {
+           // Special special case. Look for a bare class variable reference.
+           class_T     *cl = root->vval.v_class;
+           int         m_idx;
+           ocmember_T  *m = class_member_lookup(cl, lp->ll_name,
+                                       lp->ll_name_end - lp->ll_name, &m_idx);
+           if (m != NULL)
+           {
+               // Assuming "inside class" since bare reference.
+               lp->ll_class = root->vval.v_class;
+               lp->ll_oi = m_idx;
+               lp->ll_valtype = m->ocm_type;
+               lp->ll_tv = &lp->ll_class->class_members_tv[m_idx];
+#ifdef LOG_LOCKVAR
+               ch_log(NULL, "LKVAR: get_lvalroot() class member: name %s",
+                                                               lp->ll_name);
+#endif
+               return;
+           }
+       }
+    }
+
+#ifdef LOG_LOCKVAR
+    ch_log(NULL, "LKVAR: get_lvalroot() any type");
+#endif
+    lp->ll_tv = root;
+    lp->ll_is_root = TRUE;
+}
+
 /*
  * Get an lval: variable, Dict item or List item that can be assigned a value
  * to: "name", "na{me}", "name[expr]", "name[expr:expr]", "name[expr][expr]",
@@ -1028,6 +1084,11 @@ get_lval(
     int                writing = 0;
     int                vim9script = in_vim9script();
 
+#ifdef LOG_LOCKVAR
+    ch_log(NULL, "LKVAR: get_lval(): name %s, lval_root %p",
+                                                   name, (void*)lval_root);
+#endif
+
     // Clear everything in "lp".
     CLEAR_POINTER(lp);
 
@@ -1122,6 +1183,7 @@ get_lval(
                    return NULL;
                lp->ll_name_end = tp;
            }
+           // TODO: check inside class?
        }
     }
     if (lp->ll_name == NULL)
@@ -1157,7 +1219,11 @@ get_lval(
 
     // Without [idx] or .key we are done.
     if ((*p != '[' && *p != '.'))
+    {
+       if (lval_root != NULL)
+           get_lval_root(lp, lval_root, lval_root_is_arg);
        return p;
+    }
 
     if (vim9script && lval_root != NULL)
     {
@@ -1350,6 +1416,8 @@ get_lval(
                }
            }
            lp->ll_list = NULL;
+           lp->ll_object = NULL;
+           lp->ll_class = NULL;
 
            // a NULL dict is equivalent with an empty dict
            if (lp->ll_tv->vval.v_dict == NULL)
@@ -1482,6 +1550,8 @@ get_lval(
            clear_tv(&var1);
 
            lp->ll_dict = NULL;
+           lp->ll_object = NULL;
+           lp->ll_class = NULL;
            lp->ll_list = lp->ll_tv->vval.v_list;
            lp->ll_li = check_range_index_one(lp->ll_list, &lp->ll_n1,
                                     (flags & GLV_ASSIGN_WITH_OP) == 0, quiet);
@@ -1516,10 +1586,22 @@ get_lval(
        }
        else  // v_type == VAR_CLASS || v_type == VAR_OBJECT
        {
-           class_T *cl = (v_type == VAR_OBJECT
-                                          && lp->ll_tv->vval.v_object != NULL)
-                           ? lp->ll_tv->vval.v_object->obj_class
-                           : lp->ll_tv->vval.v_class;
+           lp->ll_dict = NULL;
+           lp->ll_list = NULL;
+
+           class_T *cl;
+           if (v_type == VAR_OBJECT && lp->ll_tv->vval.v_object != NULL)
+           {
+               cl = lp->ll_tv->vval.v_object->obj_class;
+               lp->ll_object = lp->ll_tv->vval.v_object;
+           }
+           else
+           {
+               cl = lp->ll_tv->vval.v_class;
+               lp->ll_object = NULL;
+           }
+           lp->ll_class = cl;
+
            // TODO: what if class is NULL?
            if (cl != NULL)
            {
@@ -1539,6 +1621,7 @@ get_lval(
                        fp = method_lookup(cl,
                                round == 1 ? VAR_CLASS : VAR_OBJECT,
                                key, p - key, &m_idx);
+                       lp->ll_oi = m_idx;
                        if (fp != NULL)
                        {
                            lp->ll_ufunc = fp;
@@ -1548,12 +1631,16 @@ get_lval(
                    }
                }
 
+               // TODO: dont' check access if inside class
+               // TODO: is GLV_READ_ONLY the right thing to use
+               //           for class/object member access?
+               //           Probably in some cases. Need inside class check
                if (lp->ll_valtype == NULL)
                {
                    int         m_idx;
-                   ocmember_T  *om;
-
-                   om = member_lookup(cl, v_type, key, p - key, &m_idx);
+                   ocmember_T  *om
+                           = member_lookup(cl, v_type, key, p - key, &m_idx);
+                   lp->ll_oi = m_idx;
                    if (om != NULL)
                    {
                        switch (om->ocm_access)
diff --git a/src/evalvars.c b/src/evalvars.c
index 243899394..fc977c66a 100644
--- a/src/evalvars.c
+++ b/src/evalvars.c
@@ -2123,6 +2123,30 @@ do_unlet(char_u *name, int forceit)
     return FAIL;
 }
 
+    static void
+report_lockvar_member(char *msg, lval_T *lp)
+{
+    int did_alloc = FALSE;
+    char_u *vname = (char_u *)"";
+    char_u *class_name = lp->ll_class != NULL
+                                   ? lp->ll_class->class_name : (char_u *)"";
+    if (lp->ll_name != NULL)
+    {
+       if (lp->ll_name_end == NULL)
+           vname = lp->ll_name;
+       else
+       {
+           vname = vim_strnsave(lp->ll_name, lp->ll_name_end - lp->ll_name);
+           if (vname == NULL)
+               return;
+           did_alloc = TRUE;
+       }
+    }
+    semsg(_(msg), vname, class_name);
+    if (did_alloc)
+       vim_free(vname);
+}
+
 /*
  * Lock or unlock variable indicated by "lp".
  * "deep" is the levels to go (-1 for unlimited);
@@ -2141,6 +2165,10 @@ do_lock_var(
     int                cc;
     dictitem_T *di;
 
+#ifdef LOG_LOCKVAR
+    ch_log(NULL, "LKVAR: do_lock_var(): name %s, is_root %d", lp->ll_name, 
lp->ll_is_root);
+#endif
+
     if (lp->ll_tv == NULL)
     {
        cc = *name_end;
@@ -2201,10 +2229,13 @@ do_lock_var(
        }
        *name_end = cc;
     }
-    else if (deep == 0)
+    else if (deep == 0 && lp->ll_object == NULL && lp->ll_class == NULL)
     {
        // nothing to do
     }
+    else if (lp->ll_is_root)
+       // (un)lock the item.
+       item_lock(lp->ll_tv, deep, lock, FALSE);
     else if (lp->ll_range)
     {
        listitem_T    *li = lp->ll_li;
@@ -2220,13 +2251,57 @@ do_lock_var(
     else if (lp->ll_list != NULL)
        // (un)lock a List item.
        item_lock(&lp->ll_li->li_tv, deep, lock, FALSE);
+    else if (lp->ll_object != NULL)  // This check must be before ll_class.
+    {
+       // (un)lock an object variable.
+       report_lockvar_member(e_cannot_lock_object_variable_str, lp);
+       ret = FAIL;
+    }
+    else if (lp->ll_class != NULL)
+    {
+       // (un)lock a class variable.
+       report_lockvar_member(e_cannot_lock_class_variable_str, lp);
+       ret = FAIL;
+    }
     else
+    {
        // (un)lock a Dictionary item.
-       item_lock(&lp->ll_di->di_tv, deep, lock, FALSE);
+       if (lp->ll_di == NULL)
+       {
+           emsg(_(e_dictionary_required));
+           ret = FAIL;
+       }
+       else
+           item_lock(&lp->ll_di->di_tv, deep, lock, FALSE);
+    }
 
     return ret;
 }
 
+#ifdef LOG_LOCKVAR
+    static char *
+vartype_tostring(vartype_T vartype)
+{
+    return
+               vartype == VAR_BOOL ? "v_number"
+             : vartype == VAR_SPECIAL ? "v_number"
+             : vartype == VAR_NUMBER ? "v_number"
+             : vartype == VAR_FLOAT ? "v_float"
+             : vartype == VAR_STRING ? "v_string"
+             : vartype == VAR_BLOB ? "v_blob"
+             : vartype == VAR_FUNC ? "v_string"
+             : vartype == VAR_PARTIAL ? "v_partial"
+             : vartype == VAR_LIST ? "v_list"
+             : vartype == VAR_DICT ? "v_dict"
+             : vartype == VAR_JOB ? "v_job"
+             : vartype == VAR_CHANNEL ? "v_channel"
+             : vartype == VAR_INSTR ? "v_instr"
+             : vartype == VAR_CLASS ? "v_class"
+             : vartype == VAR_OBJECT ? "v_object"
+             : "";
+}
+#endif
+
 /*
  * Lock or unlock an item.  "deep" is nr of levels to go.
  * When "check_refcount" is TRUE do not lock a list or dict with a reference
@@ -2243,6 +2318,10 @@ item_lock(typval_T *tv, int deep, int lock, int 
check_refcount)
     hashitem_T *hi;
     int                todo;
 
+#ifdef LOG_LOCKVAR
+    ch_log(NULL, "LKVAR: item_lock(): type %s", vartype_tostring(tv->v_type));
+#endif
+
     if (recurse >= DICT_MAXNEST)
     {
        emsg(_(e_variable_nested_too_deep_for_unlock));
diff --git a/src/globals.h b/src/globals.h
index 68e7ef2a2..993783957 100644
--- a/src/globals.h
+++ b/src/globals.h
@@ -1954,6 +1954,7 @@ EXTERN int  timer_busy INIT(= 0);   // when timer is 
inside vgetc() then > 0
 EXTERN int  input_busy INIT(= 0);   // when inside get_user_input() then > 0
 
 EXTERN typval_T        *lval_root INIT(= NULL);
+EXTERN int     lval_root_is_arg INIT(= 0);
 #endif
 
 #ifdef FEAT_BEVAL_TERM
diff --git a/src/proto/vim9instr.pro b/src/proto/vim9instr.pro
index 7c13f9333..898eb9751 100644
--- a/src/proto/vim9instr.pro
+++ b/src/proto/vim9instr.pro
@@ -69,6 +69,7 @@ int generate_MULT_EXPR(cctx_T *cctx, isntype_T isn_type, int 
count);
 int generate_ECHOWINDOW(cctx_T *cctx, int count, long time);
 int generate_SOURCE(cctx_T *cctx, int sid);
 int generate_PUT(cctx_T *cctx, int regname, linenr_T lnum);
+int generate_LOCKUNLOCK(cctx_T *cctx, char_u *line, int is_arg);
 int generate_EXEC_copy(cctx_T *cctx, isntype_T isntype, char_u *line);
 int generate_EXEC(cctx_T *cctx, isntype_T isntype, char_u *str);
 int generate_LEGACY_EVAL(cctx_T *cctx, char_u *line);
diff --git a/src/structs.h b/src/structs.h
index f5ad17414..9813c4e94 100644
--- a/src/structs.h
+++ b/src/structs.h
@@ -4535,6 +4535,11 @@ typedef struct
  *     "tv"        points to the (first) list item value
  *     "li"        points to the (first) list item
  *     "range", "n1", "n2" and "empty2" indicate what items are used.
+ * For a member in a class/object: TODO: verify fields
+ *     "name"      points to the (expanded) variable name.
+ *     "exp_name"  NULL or non-NULL, to be freed later.
+ *     "tv"        points to the (first) list item value
+ *     "oi"        index into member array, see _type to determine which array
  * For an existing Dict item:
  *     "name"      points to the (expanded) variable name.
  *     "exp_name"  NULL or non-NULL, to be freed later.
@@ -4571,6 +4576,11 @@ typedef struct lval_S
     type_T     *ll_valtype;    // type expected for the value or NULL
     blob_T     *ll_blob;       // The Blob or NULL
     ufunc_T    *ll_ufunc;      // The function or NULL
+    object_T   *ll_object;     // The object or NULL, class is not NULL
+    class_T    *ll_class;      // The class or NULL, object may be NULL
+    int                ll_oi;          // The object/class member index
+    int                ll_is_root;     // Special case. ll_tv is lval_root,
+                               // ignore the rest.
 } lval_T;
 
 // Structure used to save the current state.  Used when executing Normal mode
diff --git a/src/testdir/test_vim9_class.vim b/src/testdir/test_vim9_class.vim
index c643457cc..9799a2f51 100644
--- a/src/testdir/test_vim9_class.vim
+++ b/src/testdir/test_vim9_class.vim
@@ -3480,7 +3480,7 @@ enddef
 
 " Test for locking a variable referring to an object and reassigning to another
 " object.
-def Test_object_lockvar()
+def Test_lockvar_object()
   var lines =<< trim END
     vim9script
 
@@ -3515,6 +3515,480 @@ def Test_object_lockvar()
   v9.CheckSourceSuccess(lines)
 enddef
 
+" Test trying to lock an object variable from various places
+def Test_lockvar_object_variable()
+  # An object variable lockvar has several cases:
+  # object method, scriptlevel, scriplevel from :def, :def arg
+  # method arg, static method arg.
+  # Also different depths
+
+  # TODO: handle inside_class in vim9class
+  # lockvar of a read-only currently fails even if inside
+
+  #
+  # lockvar of read-only object variable
+  #
+
+  # read-only lockvar from object method
+  var lines =<< trim END
+    vim9script
+
+    class C
+      this.val1: number
+      def Lock()
+        lockvar this.val1
+      enddef
+    endclass
+    var o = C.new(3)
+    o.Lock()
+  END
+  # TODO: wrong error
+  v9.CheckSourceFailure(lines, 'E1335: Variable "val1" in class "C" is not 
writable')
+
+  # read-only lockvar from scriptlevel
+  lines =<< trim END
+    vim9script
+
+    class C
+      this.val2: number
+    endclass
+    var o = C.new(3)
+    lockvar o.val2
+  END
+  v9.CheckSourceFailure(lines, 'E1335: Variable "val2" in class "C" is not 
writable')
+
+  # read-only lockvar of scriptlevel variable from def
+  lines =<< trim END
+    vim9script
+
+    class C
+      this.val3: number
+    endclass
+    var o = C.new(3)
+    def Lock()
+      lockvar o.val3
+    enddef
+    Lock()
+  END
+  v9.CheckSourceFailure(lines, 'E1335: Variable "val3" in class "C" is not 
writable')
+
+  # read-only lockvar of def argument variable
+  lines =<< trim END
+    vim9script
+
+    class C
+      this.val4: number
+    endclass
+    def Lock(o: C)
+      lockvar o.val4
+    enddef
+    Lock(C.new(3))
+  END
+  v9.CheckSourceFailure(lines, 'E1335: Variable "val4" in class "C" is not 
writable')
+
+  # TODO: the following tests use type "any" for argument. Need a run time
+  #       check for access. Probably OK as is for now.
+
+  # read-only lockvar from object method arg
+  lines =<< trim END
+    vim9script
+
+    class C
+      this.val5: number
+      def Lock(o_any: any)
+        lockvar o_any.val5
+      enddef
+    endclass
+    var o = C.new(3)
+    o.Lock(C.new(5))
+  END
+  # TODO: wrong error, tricky since type "any"
+  v9.CheckSourceFailure(lines, 'E1335: Variable "val5" in class "C" is not 
writable')
+
+  # read-only lockvar from class method arg
+  lines =<< trim END
+    vim9script
+
+    class C
+      this.val6: number
+      static def Lock(o_any: any)
+        lockvar o_any.val6
+      enddef
+    endclass
+    var o = C.new(3)
+    C.Lock(o)
+  END
+  # TODO: wrong error, tricky since type "any"
+  v9.CheckSourceFailure(lines, 'E1335: Variable "val6" in class "C" is not 
writable')
+
+  #
+  # lockvar of public object variable
+  #
+
+  # lockvar from object method
+  lines =<< trim END
+    vim9script
+
+    class C
+      public this.val1: number
+      def Lock()
+        lockvar this.val1
+      enddef
+    endclass
+    var o = C.new(3)
+    o.Lock()
+  END
+  v9.CheckSourceFailure(lines, 'E1391: Cannot (un)lock variable "this.val1" in 
class "C"', 1)
+
+  # lockvar from scriptlevel
+  lines =<< trim END
+    vim9script
+
+    class C
+      public this.val2: number
+    endclass
+    var o = C.new(3)
+    lockvar o.val2
+  END
+  v9.CheckSourceFailure(lines, 'E1391: Cannot (un)lock variable "o.val2" in 
class "C"', 7)
+
+  # lockvar of scriptlevel variable from def
+  lines =<< trim END
+    vim9script
+
+    class C
+      public this.val3: number
+    endclass
+    var o = C.new(3)
+    def Lock()
+      lockvar o.val3
+    enddef
+    Lock()
+  END
+  v9.CheckSourceFailure(lines, 'E1391: Cannot (un)lock variable "o.val3" in 
class "C"', 1)
+
+  # lockvar of def argument variable
+  lines =<< trim END
+    vim9script
+
+    class C
+      public this.val4: number
+    endclass
+    def Lock(o: C)
+      lockvar o.val4
+    enddef
+    Lock(C.new(3))
+  END
+  v9.CheckSourceFailure(lines, 'E1391: Cannot (un)lock variable "o.val4" in 
class "C"', 1)
+
+  # lockvar from object method arg
+  lines =<< trim END
+    vim9script
+
+    class C
+      public this.val5: number
+      def Lock(o_any: any)
+        lockvar o_any.val5
+      enddef
+    endclass
+    var o = C.new(3)
+    o.Lock(C.new(5))
+  END
+  v9.CheckSourceFailure(lines, 'E1391: Cannot (un)lock variable "o_any.val5" 
in class "C"', 1)
+
+  # lockvar from class method arg
+  lines =<< trim END
+    vim9script
+
+    class C
+      public this.val6: number
+      static def Lock(o_any: any)
+        lockvar o_any.val6
+      enddef
+    endclass
+    var o = C.new(3)
+    C.Lock(o)
+  END
+  v9.CheckSourceFailure(lines, 'E1391: Cannot (un)lock variable "o_any.val6" 
in class "C"', 1)
+enddef
+
+" Test trying to lock a class variable from various places
+def Test_lockvar_class_variable()
+
+  # lockvar bare static from object method
+  var lines =<< trim END
+    vim9script
+
+    class C
+      public static sval1: number
+      def Lock()
+        lockvar sval1
+      enddef
+    endclass
+    var o = C.new()
+    o.Lock()
+  END
+  v9.CheckSourceFailure(lines, 'E1392: Cannot (un)lock class variable "sval1" 
in class "C"', 1)
+
+  # lockvar C.static from object method
+  lines =<< trim END
+    vim9script
+
+    class C
+      public static sval2: number
+      def Lock()
+        lockvar C.sval2
+      enddef
+    endclass
+    var o = C.new()
+    o.Lock()
+  END
+  v9.CheckSourceFailure(lines, 'E1392: Cannot (un)lock class variable 
"C.sval2" in class "C"', 1)
+
+  # lockvar bare static from class method
+  lines =<< trim END
+    vim9script
+
+    class C
+      public static sval3: number
+      static def Lock()
+        lockvar sval3
+      enddef
+    endclass
+    C.Lock()
+  END
+  v9.CheckSourceFailure(lines, 'E1392: Cannot (un)lock class variable "sval3" 
in class "C"', 1)
+
+  # lockvar C.static from class method
+  lines =<< trim END
+    vim9script
+
+    class C
+      public static sval4: number
+      static def Lock()
+        lockvar C.sval4
+      enddef
+    endclass
+    C.Lock()
+  END
+  v9.CheckSourceFailure(lines, 'E1392: Cannot (un)lock class variable 
"C.sval4" in class "C"', 1)
+
+  # lockvar C.static from script level
+  lines =<< trim END
+    vim9script
+
+    class C
+      public static sval5: number
+    endclass
+    lockvar C.sval5
+  END
+  v9.CheckSourceFailure(lines, 'E1392: Cannot (un)lock class variable 
"C.sval5" in class "C"', 6)
+
+  # lockvar o.static from script level
+  lines =<< trim END
+    vim9script
+
+    class C
+      public static sval6: number
+    endclass
+    var o = C.new()
+    lockvar o.sval6
+  END
+  v9.CheckSourceFailure(lines, 'E1375: Class variable "sval6" accessible only 
using class "C"', 7)
+enddef
+
+" Test locking an argument to :def
+def Test_lockvar_argument()
+  # Lockvar a function arg
+  var lines =<< trim END
+    vim9script
+
+    def Lock(val: any)
+        lockvar val
+    enddef
+
+    var d = {a: 1, b: 2}
+    Lock(d)
+
+    d->extend({c: 3})
+  END
+  v9.CheckSourceFailure(lines, 'E741: Value is locked: extend() argument')
+
+  # Lockvar a function arg. Verify "sval" is interpreted as argument and not a
+  # class member in "C". This tests lval_root_is_arg.
+  lines =<< trim END
+    vim9script
+
+    class C
+      public static sval: list<number>
+    endclass
+
+    def Lock2(sval: any)
+      lockvar sval
+    enddef
+
+    var o = C.new()
+    Lock2(o)
+  END
+  v9.CheckSourceSuccess(lines)
+
+  # Lock a class.
+  lines =<< trim END
+    vim9script
+
+    class C
+      public static sval: list<number>
+    endclass
+
+    def Lock2(sval: any)
+      lockvar sval
+    enddef
+
+    Lock2(C)
+  END
+  v9.CheckSourceSuccess(lines)
+
+  # Lock an object.
+  lines =<< trim END
+    vim9script
+
+    class C
+      public static sval: list<number>
+    endclass
+
+    def Lock2(sval: any)
+      lockvar sval
+    enddef
+
+    Lock2(C.new())
+  END
+  v9.CheckSourceSuccess(lines)
+
+  # In this case (unlike previous) "lockvar sval" is a class member.
+  lines =<< trim END
+    vim9script
+
+    class C
+      public static sval: list<number>
+      def Lock2()
+        lockvar sval
+      enddef
+    endclass
+
+
+    var o = C.new()
+    o.Lock2()
+  END
+  v9.CheckSourceFailure(lines, 'E1392: Cannot (un)lock class variable "sval" 
in class "C"', 1)
+enddef
+
+" Test that this can be locked without error
+def Test_lockvar_this()
+  # lockvar this
+  var lines =<< trim END
+    vim9script
+    class C
+      def TLock()
+        lockvar this
+      enddef
+    endclass
+    var o = C.new()
+    o.TLock()
+  END
+  v9.CheckSourceSuccess(lines)
+
+  # lockvar four   (four letter word, but not this)
+  lines =<< trim END
+    vim9script
+    class C
+      def TLock4()
+        var four: number
+        lockvar four
+      enddef
+    endclass
+    var o = C.new()
+    o.TLock4()
+  END
+  v9.CheckSourceFailure(lines, 'E1178: Cannot lock or unlock a local variable')
+
+  # lockvar this5; "this" + one char, 5 letter word, starting with "this"
+  lines =<< trim END
+    vim9script
+    class C
+      def TLock5()
+        var this5: number
+        lockvar this5
+      enddef
+    endclass
+    var o = C.new()
+    o.TLock5()
+  END
+  v9.CheckSourceFailure(lines, 'E1178: Cannot lock or unlock a local variable')
+enddef
+
+" Test some general lockvar cases
+def Test_lockvar_general()
+  # lockvar an object and a class. It does nothing
+  var lines =<< trim END
+    vim9script
+    class C
+    endclass
+    var o = C.new()
+    lockvar o
+    lockvar C
+  END
+  v9.CheckSourceSuccess(lines)
+
+  # Lock a list element that's nested in an object variable from a :def
+  lines =<< trim END
+    vim9script
+
+    class C
+      public this.val: list<list<number>> = [ [1], [2], [3] ]
+    endclass
+    def Lock2(obj: any)
+      lockvar obj.val[1]
+    enddef
+
+    var o = C.new()
+    Lock2(o)
+    o.val[0] = [9]
+    assert_equal([ [9], [2], [3] ], o.val)
+    try
+      o.val[1] = [999]
+      call assert_false(true, 'assign should have failed')
+    catch
+      assert_exception('E741:')
+    endtry
+    o.val[2] = [8]
+    assert_equal([ [9], [2], [8] ], o.val)
+  END
+  v9.CheckSourceSuccess(lines)
+
+  # Lock a list element that's nested in an object variable from scriptlevel
+  lines =<< trim END
+    vim9script
+
+    class C
+      public this.val: list<list<number>> = [ [1], [2], [3] ]
+    endclass
+
+    var o = C.new()
+    lockvar o.val[1]
+    o.val[0] = [9]
+    assert_equal([ [9], [2], [3] ], o.val)
+    try
+      o.val[1] = [999]
+      call assert_false(true, 'assign should have failed')
+    catch
+      assert_exception('E741:')
+    endtry
+    o.val[2] = [8]
+    assert_equal([ [9], [2], [8] ], o.val)
+  END
+  v9.CheckSourceSuccess(lines)
+enddef
+
 " Test for a private object method
 def Test_private_object_method()
   # Try calling a private method using an object (at the script level)
diff --git a/src/version.c b/src/version.c
index 954c993b9..da13c18e1 100644
--- a/src/version.c
+++ b/src/version.c
@@ -699,6 +699,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    1955,
 /**/
     1954,
 /**/
diff --git a/src/vim9.h b/src/vim9.h
index 03f6dade6..320e35bde 100644
--- a/src/vim9.h
+++ b/src/vim9.h
@@ -499,12 +499,19 @@ typedef struct {
     class_T    *cm_class;
     int                cm_idx;
 } classmember_T;
+
 // arguments to ISN_STOREINDEX
 typedef struct {
     vartype_T  si_vartype;
     class_T    *si_class;
 } storeindex_T;
 
+// arguments to ISN_LOCKUNLOCK
+typedef struct {
+    char_u     *string;        // for exec_command
+    int                is_arg;         // is lval_root a function arg
+} lockunlock_T;
+
 /*
  * Instruction
  */
@@ -561,6 +568,7 @@ struct isn_S {
        construct_T         construct;
        classmember_T       classmember;
        storeindex_T        storeindex;
+       lockunlock_T        lockunlock;
     } isn_arg;
 };
 
diff --git a/src/vim9cmds.c b/src/vim9cmds.c
index 0ca3be11a..0d6dc01f9 100644
--- a/src/vim9cmds.c
+++ b/src/vim9cmds.c
@@ -189,10 +189,15 @@ compile_lock_unlock(
     int                cc = *name_end;
     char_u     *p = lvp->ll_name;
     int                ret = OK;
-    size_t     len;
     char_u     *buf;
     isntype_T  isn = ISN_EXEC;
     char       *cmd = eap->cmdidx == CMD_lockvar ? "lockvar" : "unlockvar";
+    int                is_arg = FALSE;
+
+#ifdef LOG_LOCKVAR
+    ch_log(NULL, "LKVAR: compile_lock_unlock(): cookie %p, name %s",
+                                                               coookie, p);
+#endif
 
     if (cctx->ctx_skip == SKIP_YES)
        return OK;
@@ -207,20 +212,68 @@ compile_lock_unlock(
     if (p[1] != ':')
     {
        char_u *end = find_name_end(p, NULL, NULL, FNE_CHECK_START);
+       // If name is is locally accessible, except for local var,
+       // then put it on the stack to use with ISN_LOCKUNLOCK.
+       // This could be v.memb, v[idx_key]; bare class variable,
+       // function arg. The local variable on the stack, will be passed
+       // to ex_lockvar() indirectly.
 
-       if (lookup_local(p, end - p, NULL, cctx) == OK)
-       {
-           char_u *s = p;
+       char_u  *name = NULL;
+       int     len = end - p;
 
-           if (*end != '.' && *end != '[')
+       if (lookup_local(p, len, NULL, cctx) == OK)
+       {
+           // Handle "this", "this.val", "anyvar[idx]"
+           if (*end != '.' && *end != '['
+                               && (len != 4 || STRNCMP("this", p, len) != 0))
            {
                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)
+           // Push the local on the stack, could be "this".
+           name = p;
+#ifdef LOG_LOCKVAR
+           ch_log(NULL, "LKVAR: compile... lookup_local: name %s", name);
+#endif
+       }
+       if (name == NULL)
+       {
+           class_T *cl;
+           if (cctx_class_member_idx(cctx, p, len, &cl) >= 0)
+           {
+               if (*end != '.' && *end != '[')
+               {
+                   // Push the class of the bare class variable name
+                   name = cl->class_name;
+                   len = STRLEN(name);
+#ifdef LOG_LOCKVAR
+                   ch_log(NULL, "LKVAR: compile... cctx_class_member: name %s",
+                          name);
+#endif
+               }
+           }
+       }
+       if (name == NULL)
+       {
+           int     idx;
+           type_T  *type;
+           // Can lockvar any function arg.
+           // TODO: test arg[idx]/arg.member
+           if (arg_exists(p, len, &idx, &type, NULL, cctx) == OK)
+           {
+               name = p;
+               is_arg = TRUE;
+#ifdef LOG_LOCKVAR
+               ch_log(NULL, "LKVAR: compile... arg_exists: name %s", name);
+#endif
+           }
+       }
+       if (name != NULL)
+       {
+#ifdef LOG_LOCKVAR
+           ch_log(NULL, "LKVAR: compile... INS_LOCKUNLOCK %s", name);
+#endif
+           if (compile_load(&name, name + len, cctx, FALSE, FALSE) == FAIL)
                return FAIL;
            isn = ISN_LOCKUNLOCK;
        }
@@ -228,7 +281,7 @@ compile_lock_unlock(
 
     // Checking is done at runtime.
     *name_end = NUL;
-    len = name_end - p + 20;
+    size_t len = name_end - p + 20;
     buf = alloc(len);
     if (buf == NULL)
        ret = FAIL;
@@ -238,7 +291,13 @@ compile_lock_unlock(
            vim_snprintf((char *)buf, len, "%s! %s", cmd, p);
        else
            vim_snprintf((char *)buf, len, "%s %d %s", cmd, deep, p);
-       ret = generate_EXEC_copy(cctx, isn, buf);
+#ifdef LOG_LOCKVAR
+       ch_log(NULL, "LKVAR: compile... buf %s", buf);
+#endif
+       if (isn == ISN_LOCKUNLOCK)
+           ret = generate_LOCKUNLOCK(cctx, buf, is_arg);
+       else
+           ret = generate_EXEC_copy(cctx, isn, buf);
 
        vim_free(buf);
        *name_end = cc;
diff --git a/src/vim9execute.c b/src/vim9execute.c
index d005deb4d..209f86fb5 100644
--- a/src/vim9execute.c
+++ b/src/vim9execute.c
@@ -1957,7 +1957,7 @@ fill_partial_and_closure(
  * Execute iptr->isn_arg.string as an Ex command.
  */
     static int
-exec_command(isn_T *iptr)
+exec_command(isn_T *iptr, char_u *cmd_string)
 {
     source_cookie_T cookie;
 
@@ -1965,8 +1965,7 @@ exec_command(isn_T *iptr)
     // 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,
+    if (do_cmdline(cmd_string, getsourceline, &cookie,
                             DOCMD_VERBOSE|DOCMD_NOWAIT|DOCMD_KEYTYPED) == FAIL
                || did_emsg)
        return FAIL;
@@ -3182,7 +3181,7 @@ exec_instructions(ectx_T *ectx)
 
            // execute Ex command line
            case ISN_EXEC:
-               if (exec_command(iptr) == FAIL)
+               if (exec_command(iptr, iptr->isn_arg.string) == FAIL)
                    goto on_error;
                break;
 
@@ -4179,16 +4178,24 @@ exec_instructions(ectx_T *ectx)
 
            case ISN_LOCKUNLOCK:
                {
+                   // TODO: could put lval_root info in struct
                    typval_T    *lval_root_save = lval_root;
+                   int         lval_root_is_arg_save = lval_root_is_arg;
                    int         res;
+#ifdef LOG_LOCKVAR
+                   ch_log(NULL, "LKVAR: execute INS_LOCKUNLOCK isn_arg %s",
+                                                       iptr->isn_arg.string);
+#endif
 
                    // 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);
+                   lval_root_is_arg = iptr->isn_arg.lockunlock.is_arg;
+                   res = exec_command(iptr, iptr->isn_arg.lockunlock.string);
                    clear_tv(lval_root);
                    lval_root = lval_root_save;
+                   lval_root_is_arg = lval_root_is_arg_save;
                    if (res == FAIL)
                        goto on_error;
                }
diff --git a/src/vim9instr.c b/src/vim9instr.c
index b229d4a11..697c83d8a 100644
--- a/src/vim9instr.c
+++ b/src/vim9instr.c
@@ -2169,6 +2169,23 @@ generate_PUT(cctx_T *cctx, int regname, linenr_T lnum)
     return OK;
 }
 
+/*
+ * Generate an EXEC instruction that takes a string argument.
+ * A copy is made of "line".
+ */
+    int
+generate_LOCKUNLOCK(cctx_T *cctx, char_u *line, int is_arg)
+{
+    isn_T      *isn;
+
+    RETURN_OK_IF_SKIP(cctx);
+    if ((isn = generate_instr(cctx, ISN_LOCKUNLOCK)) == NULL)
+       return FAIL;
+    isn->isn_arg.lockunlock.string = vim_strsave(line);
+    isn->isn_arg.lockunlock.is_arg = is_arg;
+    return OK;
+}
+
 /*
  * Generate an EXEC instruction that takes a string argument.
  * A copy is made of "line".

-- 
-- 
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php

--- 
You received this message because you are subscribed to the Google Groups 
"vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to vim_dev+unsubscr...@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/vim_dev/E1qmHmZ-00HVMD-SA%40256bit.org.

Raspunde prin e-mail lui