patch 9.0.1977: Vim9: object members can change type

Commit: 
https://github.com/vim/vim/commit/fe7b20a1a39dc645a6ea7ae925512f9227fd1695
Author: Yegappan Lakshmanan <yegap...@yahoo.com>
Date:   Wed Oct 4 19:47:52 2023 +0200

    patch 9.0.1977: Vim9: object members can change type
    
    Problem:  Vim9: object members can change type
    Solution: Check type during assignment to object/class var
    
    closes: #13127
    closes: #13262
    
    Signed-off-by: Christian Brabandt <c...@256bit.org>
    Co-authored-by: Yegappan Lakshmanan <yegap...@yahoo.com>

diff --git a/src/proto/vim9class.pro b/src/proto/vim9class.pro
index 9edf35408..f2e642ec7 100644
--- a/src/proto/vim9class.pro
+++ b/src/proto/vim9class.pro
@@ -2,6 +2,7 @@
 int object_index_from_itf_index(class_T *itf, int is_method, int idx, class_T 
*cl);
 void ex_class(exarg_T *eap);
 type_T *class_member_type(class_T *cl, int is_object, char_u *name, char_u 
*name_end, int *member_idx);
+type_T *class_member_type_by_idx(class_T *cl, int is_object, int member_idx);
 void ex_enum(exarg_T *eap);
 void ex_type(exarg_T *eap);
 int class_object_index(char_u **arg, typval_T *rettv, evalarg_T *evalarg, int 
verbose);
diff --git a/src/structs.h b/src/structs.h
index 8443a4211..c7360a30c 100644
--- a/src/structs.h
+++ b/src/structs.h
@@ -1509,10 +1509,11 @@ typedef enum {
  * Entry for an object or class member variable.
  */
 typedef struct {
-    char_u     *ocm_name;   // allocated
+    char_u     *ocm_name;      // allocated
     omacc_T    ocm_access;
+    int                ocm_has_type;   // type specified explicitly
     type_T     *ocm_type;
-    char_u     *ocm_init;   // allocated
+    char_u     *ocm_init;      // allocated
 } ocmember_T;
 
 // used for the lookup table of a class member index and object method index
diff --git a/src/testdir/test_vim9_class.vim b/src/testdir/test_vim9_class.vim
index 4a92a3d3a..2aa5c0833 100644
--- a/src/testdir/test_vim9_class.vim
+++ b/src/testdir/test_vim9_class.vim
@@ -6418,4 +6418,390 @@ def Test_extended_obj_method_type_check()
   v9.CheckSourceFailure(lines, 'E1383: Method "Doit": type mismatch, expected 
func(object<B>): object<B> but got func(object<B>): object<A>', 20)
 enddef
 
+" Test type checking for class variable in assignments
+func Test_class_variable_complex_type_check()
+  " class variable with a specific type.  Try assigning a different type at
+  " script level.
+  let lines =<< trim END
+    vim9script
+    def Foo(l: list<dict<number>>): dict<list<number>>
+      return {}
+    enddef
+    class A
+      public static Fn: func(list<dict<number>>): dict<list<number>> = Foo
+    endclass
+    test_garbagecollect_now()
+    A.Fn = "abc"
+  END
+  call v9.CheckSourceFailure(lines, 'E1012: Type mismatch; expected 
func(list<dict<number>>): dict<list<number>> but got string', 9)
+
+  " class variable with a specific type.  Try assigning a different type at
+  " class def method level.
+  let lines =<< trim END
+    vim9script
+    def Foo(l: list<dict<number>>): dict<list<number>>
+      return {}
+    enddef
+    class A
+      public static Fn: func(list<dict<number>>): dict<list<number>> = Foo
+      def Bar()
+        Fn = "abc"
+      enddef
+    endclass
+    var a = A.new()
+    test_garbagecollect_now()
+    a.Bar()
+  END
+  call v9.CheckSourceFailure(lines, 'E1012: Type mismatch; expected 
func(list<dict<number>>): dict<list<number>> but got string', 1)
+
+  " class variable with a specific type.  Try assigning a different type at
+  " script def method level.
+  let lines =<< trim END
+    vim9script
+    def Foo(l: list<dict<number>>): dict<list<number>>
+      return {}
+    enddef
+    class A
+      public static Fn: func(list<dict<number>>): dict<list<number>> = Foo
+    endclass
+    def Bar()
+      A.Fn = "abc"
+    enddef
+    test_garbagecollect_now()
+    Bar()
+  END
+  call v9.CheckSourceFailure(lines, 'E1012: Type mismatch; expected 
func(list<dict<number>>): dict<list<number>> but got string', 1)
+
+  " class variable without any type.  Should be set to the initialization
+  " expression type.  Try assigning a different type from script level.
+  let lines =<< trim END
+    vim9script
+    def Foo(l: list<dict<number>>): dict<list<number>>
+      return {}
+    enddef
+    class A
+      public static Fn = Foo
+    endclass
+    test_garbagecollect_now()
+    A.Fn = "abc"
+  END
+  call v9.CheckSourceFailure(lines, 'E1012: Type mismatch; expected 
func(list<dict<number>>): dict<list<number>> but got string', 9)
+
+  " class variable without any type.  Should be set to the initialization
+  " expression type.  Try assigning a different type at class def level.
+  let lines =<< trim END
+    vim9script
+    def Foo(l: list<dict<number>>): dict<list<number>>
+      return {}
+    enddef
+    class A
+      public static Fn = Foo
+      def Bar()
+        Fn = "abc"
+      enddef
+    endclass
+    var a = A.new()
+    test_garbagecollect_now()
+    a.Bar()
+  END
+  call v9.CheckSourceFailure(lines, 'E1012: Type mismatch; expected 
func(list<dict<number>>): dict<list<number>> but got string', 1)
+
+  " class variable without any type.  Should be set to the initialization
+  " expression type.  Try assigning a different type at script def level.
+  let lines =<< trim END
+    vim9script
+    def Foo(l: list<dict<number>>): dict<list<number>>
+      return {}
+    enddef
+    class A
+      public static Fn = Foo
+    endclass
+    def Bar()
+      A.Fn = "abc"
+    enddef
+    test_garbagecollect_now()
+    Bar()
+  END
+  call v9.CheckSourceFailure(lines, 'E1012: Type mismatch; expected 
func(list<dict<number>>): dict<list<number>> but got string', 1)
+
+  " class variable with 'any" type.  Can be assigned different types.
+  let lines =<< trim END
+    vim9script
+    def Foo(l: list<dict<number>>): dict<list<number>>
+      return {}
+    enddef
+    class A
+      public static Fn: any = Foo
+      public static Fn2: any
+    endclass
+    test_garbagecollect_now()
+    assert_equal('func(list<dict<number>>): dict<list<number>>', 
typename(A.Fn))
+    A.Fn = "abc"
+    test_garbagecollect_now()
+    assert_equal('string', typename(A.Fn))
+    A.Fn2 = Foo
+    test_garbagecollect_now()
+    assert_equal('func(list<dict<number>>): dict<list<number>>', 
typename(A.Fn2))
+    A.Fn2 = "xyz"
+    test_garbagecollect_now()
+    assert_equal('string', typename(A.Fn2))
+  END
+  call v9.CheckSourceSuccess(lines)
+
+  " class variable with 'any" type.  Can be assigned different types.
+  let lines =<< trim END
+    vim9script
+    def Foo(l: list<dict<number>>): dict<list<number>>
+      return {}
+    enddef
+    class A
+      public static Fn: any = Foo
+      public static Fn2: any
+
+      def Bar()
+        assert_equal('func(list<dict<number>>): dict<list<number>>', 
typename(Fn))
+        Fn = "abc"
+        assert_equal('string', typename(Fn))
+        Fn2 = Foo
+        assert_equal('func(list<dict<number>>): dict<list<number>>', 
typename(Fn2))
+        Fn2 = "xyz"
+        assert_equal('string', typename(Fn2))
+      enddef
+    endclass
+    var a = A.new()
+    test_garbagecollect_now()
+    a.Bar()
+    test_garbagecollect_now()
+    A.Fn = Foo
+    a.Bar()
+  END
+  call v9.CheckSourceSuccess(lines)
+
+  " class variable with 'any" type.  Can be assigned different types.
+  let lines =<< trim END
+    vim9script
+    def Foo(l: list<dict<number>>): dict<list<number>>
+      return {}
+    enddef
+    class A
+      public static Fn: any = Foo
+      public static Fn2: any
+    endclass
+
+    def Bar()
+      assert_equal('func(list<dict<number>>): dict<list<number>>', 
typename(A.Fn))
+      A.Fn = "abc"
+      assert_equal('string', typename(A.Fn))
+      A.Fn2 = Foo
+      assert_equal('func(list<dict<number>>): dict<list<number>>', 
typename(A.Fn2))
+      A.Fn2 = "xyz"
+      assert_equal('string', typename(A.Fn2))
+    enddef
+    Bar()
+    test_garbagecollect_now()
+    A.Fn = Foo
+    Bar()
+  END
+  call v9.CheckSourceSuccess(lines)
+endfunc
+
+" Test type checking for object variable in assignments
+func Test_object_variable_complex_type_check()
+  " object variable with a specific type.  Try assigning a different type at
+  " script level.
+  let lines =<< trim END
+    vim9script
+    def Foo(l: list<dict<number>>): dict<list<number>>
+      return {}
+    enddef
+    class A
+      public this.Fn: func(list<dict<number>>): dict<list<number>> = Foo
+    endclass
+    var a = A.new()
+    test_garbagecollect_now()
+    a.Fn = "abc"
+  END
+  call v9.CheckSourceFailure(lines, 'E1012: Type mismatch; expected 
func(list<dict<number>>): dict<list<number>> but got string', 10)
+
+  " object variable with a specific type.  Try assigning a different type at
+  " object def method level.
+  let lines =<< trim END
+    vim9script
+    def Foo(l: list<dict<number>>): dict<list<number>>
+      return {}
+    enddef
+    class A
+      public this.Fn: func(list<dict<number>>): dict<list<number>> = Foo
+      def Bar()
+        this.Fn = "abc"
+        this.Fn = Foo
+      enddef
+    endclass
+    var a = A.new()
+    test_garbagecollect_now()
+    a.Bar()
+  END
+  call v9.CheckSourceFailure(lines, 'E1012: Type mismatch; expected 
func(list<dict<number>>): dict<list<number>> but got string', 1)
+
+  " object variable with a specific type.  Try assigning a different type at
+  " script def method level.
+  let lines =<< trim END
+    vim9script
+    def Foo(l: list<dict<number>>): dict<list<number>>
+      return {}
+    enddef
+    class A
+      public this.Fn: func(list<dict<number>>): dict<list<number>> = Foo
+    endclass
+    def Bar()
+      var a = A.new()
+      a.Fn = "abc"
+      a.Fn = Foo
+    enddef
+    test_garbagecollect_now()
+    Bar()
+  END
+  call v9.CheckSourceFailure(lines, 'E1012: Type mismatch; expected 
func(list<dict<number>>): dict<list<number>> but got string', 2)
+
+  " object variable without any type.  Should be set to the initialization
+  " expression type.  Try assigning a different type from script level.
+  let lines =<< trim END
+    vim9script
+    def Foo(l: list<dict<number>>): dict<list<number>>
+      return {}
+    enddef
+    class A
+      public this.Fn = Foo
+    endclass
+    var a = A.new()
+    test_garbagecollect_now()
+    a.Fn = "abc"
+  END
+  call v9.CheckSourceFailure(lines, 'E1012: Type mismatch; expected 
func(list<dict<number>>): dict<list<number>> but got string', 10)
+
+  " object variable without any type.  Should be set to the initialization
+  " expression type.  Try assigning a different type at object def level.
+  let lines =<< trim END
+    vim9script
+    def Foo(l: list<dict<number>>): dict<list<number>>
+      return {}
+    enddef
+    class A
+      public this.Fn = Foo
+      def Bar()
+        this.Fn = "abc"
+        this.Fn = Foo
+      enddef
+    endclass
+    var a = A.new()
+    test_garbagecollect_now()
+    a.Bar()
+  END
+  call v9.CheckSourceFailure(lines, 'E1012: Type mismatch; expected 
func(list<dict<number>>): dict<list<number>> but got string', 1)
+
+  " object variable without any type.  Should be set to the initialization
+  " expression type.  Try assigning a different type at script def level.
+  let lines =<< trim END
+    vim9script
+    def Foo(l: list<dict<number>>): dict<list<number>>
+      return {}
+    enddef
+    class A
+      public this.Fn = Foo
+    endclass
+    def Bar()
+      var a = A.new()
+      a.Fn = "abc"
+      a.Fn = Foo
+    enddef
+    test_garbagecollect_now()
+    Bar()
+  END
+  call v9.CheckSourceFailure(lines, 'E1012: Type mismatch; expected 
func(list<dict<number>>): dict<list<number>> but got string', 2)
+
+  " object variable with 'any" type.  Can be assigned different types.
+  let lines =<< trim END
+    vim9script
+    def Foo(l: list<dict<number>>): dict<list<number>>
+      return {}
+    enddef
+    class A
+      public this.Fn: any = Foo
+      public this.Fn2: any
+    endclass
+
+    var a = A.new()
+    test_garbagecollect_now()
+    assert_equal('func(list<dict<number>>): dict<list<number>>', 
typename(a.Fn))
+    a.Fn = "abc"
+    test_garbagecollect_now()
+    assert_equal('string', typename(a.Fn))
+    a.Fn2 = Foo
+    test_garbagecollect_now()
+    assert_equal('func(list<dict<number>>): dict<list<number>>', 
typename(a.Fn2))
+    a.Fn2 = "xyz"
+    test_garbagecollect_now()
+    assert_equal('string', typename(a.Fn2))
+  END
+  call v9.CheckSourceSuccess(lines)
+
+  " object variable with 'any" type.  Can be assigned different types.
+  let lines =<< trim END
+    vim9script
+    def Foo(l: list<dict<number>>): dict<list<number>>
+      return {}
+    enddef
+    class A
+      public this.Fn: any = Foo
+      public this.Fn2: any
+
+      def Bar()
+        assert_equal('func(list<dict<number>>): dict<list<number>>', 
typename(this.Fn))
+        this.Fn = "abc"
+        assert_equal('string', typename(this.Fn))
+        this.Fn2 = Foo
+        assert_equal('func(list<dict<number>>): dict<list<number>>', 
typename(this.Fn2))
+        this.Fn2 = "xyz"
+        assert_equal('string', typename(this.Fn2))
+      enddef
+    endclass
+
+    var a = A.new()
+    test_garbagecollect_now()
+    a.Bar()
+    test_garbagecollect_now()
+    a.Fn = Foo
+    a.Bar()
+  END
+  call v9.CheckSourceSuccess(lines)
+
+  " object variable with 'any" type.  Can be assigned different types.
+  let lines =<< trim END
+    vim9script
+    def Foo(l: list<dict<number>>): dict<list<number>>
+      return {}
+    enddef
+    class A
+      public this.Fn: any = Foo
+      public this.Fn2: any
+    endclass
+
+    def Bar()
+      var a = A.new()
+      assert_equal('func(list<dict<number>>): dict<list<number>>', 
typename(a.Fn))
+      a.Fn = "abc"
+      assert_equal('string', typename(a.Fn))
+      a.Fn2 = Foo
+      assert_equal('func(list<dict<number>>): dict<list<number>>', 
typename(a.Fn2))
+      a.Fn2 = "xyz"
+      assert_equal('string', typename(a.Fn2))
+    enddef
+    test_garbagecollect_now()
+    Bar()
+    test_garbagecollect_now()
+    Bar()
+  END
+  call v9.CheckSourceSuccess(lines)
+endfunc
+
 " vim: ts=8 sw=2 sts=2 expandtab tw=80 fdm=marker
diff --git a/src/version.c b/src/version.c
index 83cf515ec..4d037b472 100644
--- a/src/version.c
+++ b/src/version.c
@@ -704,6 +704,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    1977,
 /**/
     1976,
 /**/
diff --git a/src/vim9class.c b/src/vim9class.c
index 885ac0385..05d9afcf2 100644
--- a/src/vim9class.c
+++ b/src/vim9class.c
@@ -72,6 +72,7 @@ parse_member(
     char_u     *varname,
     int                has_public,         // TRUE if "public" seen before 
"varname"
     char_u     **varname_end,
+    int                *has_type,
     garray_T   *type_list,
     type_T     **type_ret,
     char_u     **init_expr)
@@ -86,6 +87,7 @@ parse_member(
     char_u *colon = skipwhite(*varname_end);
     char_u *type_arg = colon;
     type_T *type = NULL;
+    *has_type = FALSE;
     if (*colon == ':')
     {
        if (VIM_ISWHITE(**varname_end))
@@ -102,6 +104,7 @@ parse_member(
        type = parse_type(&type_arg, type_list, TRUE);
        if (type == NULL)
            return FAIL;
+       *has_type = TRUE;
     }
 
     char_u *init_arg = skipwhite(type_arg);
@@ -160,6 +163,7 @@ add_member(
     char_u     *varname,
     char_u     *varname_end,
     int                has_public,
+    int                has_type,
     type_T     *type,
     char_u     *init_expr)
 {
@@ -169,6 +173,7 @@ add_member(
     m->ocm_name = vim_strnsave(varname, varname_end - varname);
     m->ocm_access = has_public ? VIM_ACCESS_ALL
                      : *varname == '_' ? VIM_ACCESS_PRIVATE : VIM_ACCESS_READ;
+    m->ocm_has_type = has_type;
     m->ocm_type = type;
     if (init_expr != NULL)
        m->ocm_init = init_expr;
@@ -1149,6 +1154,10 @@ add_lookup_tables(class_T *cl, class_T *extends_cl, 
garray_T *objmethods_gap)
     static void
 add_class_members(class_T *cl, exarg_T *eap)
 {
+    garray_T   type_list;
+
+    ga_init2(&type_list, sizeof(type_T *), 10);
+
     // Allocate a typval for each class member and initialize it.
     cl->class_members_tv = ALLOC_CLEAR_MULT(typval_T,
                                            cl->class_class_member_count);
@@ -1164,6 +1173,13 @@ add_class_members(class_T *cl, exarg_T *eap)
            typval_T *etv = eval_expr(m->ocm_init, eap);
            if (etv != NULL)
            {
+               if (m->ocm_type->tt_type == VAR_ANY
+                       && !m->ocm_has_type
+                       && etv->v_type != VAR_SPECIAL)
+                   // If the member variable type is not yet set, then use
+                   // the initialization expression type.
+                   m->ocm_type = typval2type(etv, get_copyID(), &type_list,
+                                   TVTT_DO_MEMBER|TVTT_MORE_SPECIFIC);
                *tv = *etv;
                vim_free(etv);
            }
@@ -1175,6 +1191,8 @@ add_class_members(class_T *cl, exarg_T *eap)
            tv->vval.v_string = NULL;
        }
     }
+
+    clear_type_list(&type_list);
 }
 
 /*
@@ -1643,6 +1661,7 @@ early_ret:
            char_u *varname_end = NULL;
            type_T *type = NULL;
            char_u *init_expr = NULL;
+           int     has_type = FALSE;
 
            if (!is_class && *varname == '_')
            {
@@ -1653,7 +1672,7 @@ early_ret:
            }
 
            if (parse_member(eap, line, varname, has_public,
-                         &varname_end, &type_list, &type,
+                         &varname_end, &has_type, &type_list, &type,
                          is_class ? &init_expr: NULL) == FAIL)
                break;
            if (is_reserved_varname(varname, varname_end))
@@ -1668,7 +1687,7 @@ early_ret:
                break;
            }
            if (add_member(&objmembers, varname, varname_end,
-                                         has_public, type, init_expr) == FAIL)
+                               has_public, has_type, type, init_expr) == FAIL)
            {
                vim_free(init_expr);
                break;
@@ -1776,12 +1795,14 @@ early_ret:
            //  "static _varname"
            //  "static varname"
            //  "public static varname"
-           char_u *varname = p;
-           char_u *varname_end = NULL;
-           type_T *type = NULL;
-           char_u *init_expr = NULL;
+           char_u  *varname = p;
+           char_u  *varname_end = NULL;
+           int     has_type = FALSE;
+           type_T  *type = NULL;
+           char_u  *init_expr = NULL;
+
            if (parse_member(eap, line, varname, has_public,
-                     &varname_end, &type_list, &type,
+                     &varname_end, &has_type, &type_list, &type,
                      is_class ? &init_expr : NULL) == FAIL)
                break;
            if (is_reserved_varname(varname, varname_end))
@@ -1796,7 +1817,7 @@ early_ret:
                break;
            }
            if (add_member(&classmembers, varname, varname_end,
-                                     has_public, type, init_expr) == FAIL)
+                             has_public, has_type, type, init_expr) == FAIL)
            {
                vim_free(init_expr);
                break;
@@ -2080,6 +2101,35 @@ class_member_type(
     return m->ocm_type;
 }
 
+/*
+ * Given a class or object variable index, return the variable type
+ */
+    type_T *
+class_member_type_by_idx(
+    class_T    *cl,
+    int                is_object,
+    int                member_idx)
+{
+    ocmember_T *m;
+    int                member_count;
+
+    if (is_object)
+    {
+       m = cl->class_obj_members;
+       member_count = cl->class_obj_member_count;
+    }
+    else
+    {
+       m = cl->class_class_members;
+       member_count = cl->class_class_member_count;
+    }
+
+    if (member_idx >= member_count)
+       return NULL;
+
+    return m[member_idx].ocm_type;
+}
+
 /*
  * Handle ":enum" up to ":endenum".
  */
diff --git a/src/vim9compile.c b/src/vim9compile.c
index 086a322ca..abe1e2fb5 100644
--- a/src/vim9compile.c
+++ b/src/vim9compile.c
@@ -1766,6 +1766,9 @@ compile_lhs(
                }
                lhs->lhs_dest = dest_class_member;
                lhs->lhs_class = cctx->ctx_ufunc->uf_class;
+               lhs->lhs_type =
+                   class_member_type_by_idx(cctx->ctx_ufunc->uf_class,
+                                       FALSE, lhs->lhs_classmember_idx);
            }
            else
            {
@@ -3308,7 +3311,15 @@ compile_def_function(
                    }
 
                    type_T      *type = get_type_on_stack(&cctx, 0);
-                   if (m->ocm_type->tt_type != type->tt_type)
+                   if (m->ocm_type->tt_type == VAR_ANY
+                           && !m->ocm_has_type
+                           && type->tt_type != VAR_SPECIAL)
+                   {
+                       // If the member variable type is not yet set, then use
+                       // the initialization expression type.
+                       m->ocm_type = type;
+                   }
+                   else if (m->ocm_type->tt_type != type->tt_type)
                    {
                        // The type of the member initialization expression is
                        // determined at run time.  Add a runtime type check.

-- 
-- 
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/E1qo6AI-007hdH-LZ%40256bit.org.

Raspunde prin e-mail lui