From 195727e9771e011ffc4333facacf2705349eae06 Mon Sep 17 00:00:00 2001
From: Alan Wu <XrXr@users.noreply.github.com>
Date: Thu, 11 Apr 2019 23:46:28 -0400
Subject: [PATCH] Eagerly name modules and classes

* variable.c: make the hidden ivars `classpath` and `tmp_classpath` the source
  of truth for module and constant names. Assign to them when modules are bind
  to constants.

* variable.c: remove references to module name cache, as what used to be the cache
  is now the source of truth. Remove rb_class_path_no_cache().

* variable.c: remove the hidden ivar `classid`. This existed for the purposes of
  module name search, which is now replaced. Also, remove the associated
  rb_name_class().

* class.c: use rb_set_class_path_string to set the name of Object during boot.
  Must use a fstring as this runs before rb_cString is initialized and
  creating a normal string leads to a VALUE without a class.

* spec/ruby/core/module/name_spec.rb: add a few specs to specify what happens
  to Module#name across multiple operations. These specs pass without other
  code changes in this commit.
---
 class.c                            |   5 +-
 gc.c                               |   6 +-
 include/ruby/intern.h              |   1 -
 spec/ruby/core/module/name_spec.rb |  36 ++++
 variable.c                         | 322 ++++++++++-------------------
 vm.c                               |   4 +-
 6 files changed, 145 insertions(+), 229 deletions(-)

diff --git a/class.c b/class.c
index b76af269bb..f90f58a766 100644
--- a/class.c
+++ b/class.c
@@ -537,7 +537,6 @@ boot_defclass(const char *name, VALUE super)
     VALUE obj = rb_class_boot(super);
     ID id = rb_intern(name);
 
-    rb_name_class(obj, id);
     rb_const_set((rb_cObject ? rb_cObject : obj), id, obj);
     return obj;
 }
@@ -550,7 +549,7 @@ Init_class_hierarchy(void)
     rb_gc_register_mark_object(rb_cObject);
 
     /* resolve class name ASAP for order-independence */
-    rb_class_name(rb_cObject);
+    rb_set_class_path_string(rb_cObject, rb_cObject, rb_fstring_lit("Object"));
 
     rb_cModule = boot_defclass("Module", rb_cObject);
     rb_cClass =  boot_defclass("Class",  rb_cModule);
@@ -665,7 +664,6 @@ rb_define_class(const char *name, VALUE super)
     }
     klass = rb_define_class_id(id, super);
     rb_vm_add_root_module(id, klass);
-    rb_name_class(klass, id);
     rb_const_set(rb_cObject, id, klass);
     rb_class_inherited(super, klass);
 
@@ -759,7 +757,6 @@ rb_define_module_id(ID id)
     VALUE mdl;
 
     mdl = rb_module_new();
-    rb_name_class(mdl, id);
 
     return mdl;
 }
diff --git a/gc.c b/gc.c
index a92c82695d..17b8b033c7 100644
--- a/gc.c
+++ b/gc.c
@@ -9738,7 +9738,7 @@ rb_raw_obj_info(char *buff, const int buff_size, VALUE obj)
 	    snprintf(buff, buff_size, "%s (temporary internal)", buff);
 	}
 	else {
-	    VALUE class_path = rb_class_path_cached(RBASIC(obj)->klass);
+	    VALUE class_path = rb_class_path(RBASIC(obj)->klass);
 	    if (!NIL_P(class_path)) {
 		snprintf(buff, buff_size, "%s (%s)", buff, RSTRING_PTR(class_path));
 	    }
@@ -9787,7 +9787,7 @@ rb_raw_obj_info(char *buff, const int buff_size, VALUE obj)
           case T_CLASS:
           case T_MODULE:
             {
-                VALUE class_path = rb_class_path_cached(obj);
+                VALUE class_path = rb_class_path(obj);
                 if (!NIL_P(class_path)) {
                     snprintf(buff, buff_size, "%s %s", buff, RSTRING_PTR(class_path));
                 }
@@ -9795,7 +9795,7 @@ rb_raw_obj_info(char *buff, const int buff_size, VALUE obj)
             }
           case T_ICLASS:
             {
-                VALUE class_path = rb_class_path_cached(RBASIC_CLASS(obj));
+                VALUE class_path = rb_class_path(RBASIC_CLASS(obj));
                 if (!NIL_P(class_path)) {
                     snprintf(buff, buff_size, "%s src:%s", buff, RSTRING_PTR(class_path));
                 }
diff --git a/include/ruby/intern.h b/include/ruby/intern.h
index 17aafd7f8e..533bef4f9f 100644
--- a/include/ruby/intern.h
+++ b/include/ruby/intern.h
@@ -942,7 +942,6 @@ void rb_set_class_path(VALUE, VALUE, const char*);
 void rb_set_class_path_string(VALUE, VALUE, VALUE);
 VALUE rb_path_to_class(VALUE);
 VALUE rb_path2class(const char*);
-void rb_name_class(VALUE, ID);
 VALUE rb_class_name(VALUE);
 VALUE rb_autoload_load(VALUE, ID);
 VALUE rb_autoload_p(VALUE, ID);
diff --git a/spec/ruby/core/module/name_spec.rb b/spec/ruby/core/module/name_spec.rb
index f56e35f204..5b0fd88729 100644
--- a/spec/ruby/core/module/name_spec.rb
+++ b/spec/ruby/core/module/name_spec.rb
@@ -18,6 +18,33 @@ module m::N; end
     m::N.name.should =~ /#<Module:0x[0-9a-f]+>::N/
   end
 
+  it "changes when the module is reachable through a constant path" do
+    m = Module.new
+    module m::N; end
+    m::N.name.should =~ /#<Module:0x[0-9a-f]+>::N/
+    ModuleSpecs::Anonymous::WasAnnon = m::N
+    m::N.name.should == "ModuleSpecs::Anonymous::WasAnnon"
+  end
+
+  it "is set after it is removed from a constant" do
+    module ModuleSpecs
+      module ModuleToRemove
+      end
+
+      mod = ModuleToRemove
+      remove_const(:ModuleToRemove)
+      mod.name.should == "ModuleSpecs::ModuleToRemove"
+    end
+  end
+
+  it "is set after it is removed from a constant under an anonymous module" do
+    m = Module.new
+    module m::Child; end
+    child = m::Child
+    m.send(:remove_const, :Child)
+    child.name.should =~ /#<Module:0x[0-9a-f]+>::Child/
+  end
+
   it "is set when opened with the module keyword" do
     ModuleSpecs.name.should == "ModuleSpecs"
   end
@@ -40,6 +67,15 @@ module m::N; end
     m.name.should == "ModuleSpecs::Anonymous::B"
   end
 
+  it "is not modified when assigned to a different anonymous module" do
+    m = Module.new
+    module m::M; end
+    first_name = m::M.name.dup
+    module m::N; end
+    m::N::F = m::M
+    m::M.name.should == first_name
+  end
+
   # http://bugs.ruby-lang.org/issues/6067
   it "is set with a conditional assignment to a nested constant" do
     eval("ModuleSpecs::Anonymous::F ||= Module.new")
diff --git a/variable.c b/variable.c
index f4988b699d..86754b25a7 100644
--- a/variable.c
+++ b/variable.c
@@ -25,7 +25,7 @@
 #include "transient_heap.h"
 
 static struct rb_id_table *rb_global_tbl;
-static ID autoload, classpath, tmp_classpath, classid;
+static ID autoload, classpath, tmp_classpath;
 static VALUE autoload_featuremap; /* feature => autoload_i */
 
 static void check_before_mod_set(VALUE, ID, VALUE, const char *);
@@ -59,8 +59,6 @@ Init_var_tables(void)
     classpath = rb_intern_const("__classpath__");
     /* __tmp_classpath__: temporary class path which contains anonymous names */
     tmp_classpath = rb_intern_const("__tmp_classpath__");
-    /* __classid__: name given to class/module under an anonymous namespace */
-    classid = rb_intern_const("__classid__");
 }
 
 static inline bool
@@ -73,163 +71,29 @@ rb_namespace_p(VALUE obj)
     return false;
 }
 
-struct fc_result {
-    ID name, preferred;
-    VALUE klass;
-    VALUE path;
-    VALUE track;
-    struct fc_result *prev;
-};
-
-static VALUE
-fc_path(struct fc_result *fc, ID name)
-{
-    VALUE path, tmp;
-
-    path = rb_id2str(name);
-    while (fc) {
-	st_data_t n;
-	if (fc->track == rb_cObject) break;
-	if (RCLASS_IV_TBL(fc->track) &&
-	    st_lookup(RCLASS_IV_TBL(fc->track), (st_data_t)classpath, &n)) {
-	    tmp = rb_str_dup((VALUE)n);
-	    rb_str_cat2(tmp, "::");
-	    rb_str_append(tmp, path);
-	    path = tmp;
-	    break;
-	}
-	tmp = rb_str_dup(rb_id2str(fc->name));
-	rb_str_cat2(tmp, "::");
-	rb_str_append(tmp, path);
-	path = tmp;
-	fc = fc->prev;
-    }
-    OBJ_FREEZE(path);
-    return path;
-}
-
-static enum rb_id_table_iterator_result
-fc_i(ID key, VALUE v, void *a)
-{
-    rb_const_entry_t *ce = (rb_const_entry_t *)v;
-    struct fc_result *res = a;
-    VALUE value = ce->value;
-    if (!rb_is_const_id(key)) return ID_TABLE_CONTINUE;
-
-    if (value == res->klass && (!res->preferred || key == res->preferred)) {
-	res->path = fc_path(res, key);
-	return ID_TABLE_STOP;
-    }
-    if (rb_namespace_p(value)) {
-	if (!RCLASS_CONST_TBL(value)) return ID_TABLE_CONTINUE;
-	else {
-	    struct fc_result arg;
-	    struct fc_result *list;
-
-	    list = res;
-	    while (list) {
-		if (list->track == value) return ID_TABLE_CONTINUE;
-		list = list->prev;
-	    }
-
-	    arg.name = key;
-	    arg.preferred = res->preferred;
-	    arg.path = 0;
-	    arg.klass = res->klass;
-	    arg.track = value;
-	    arg.prev = res;
-	    rb_id_table_foreach(RCLASS_CONST_TBL(value), fc_i, &arg);
-	    if (arg.path) {
-		res->path = arg.path;
-		return ID_TABLE_STOP;
-	    }
-	}
-    }
-    return ID_TABLE_CONTINUE;
-}
-
-/**
- * Traverse constant namespace and find +classpath+ for _klass_.  If
- * _preferred_ is not 0, choice the path whose base name is set to it.
- * If +classpath+ is found, the hidden instance variable __classpath__
- * is set to the found path, and __tmp_classpath__ is removed.
- * The path is frozen.
- */
-static VALUE
-find_class_path(VALUE klass, ID preferred)
-{
-    struct fc_result arg;
-
-    arg.preferred = preferred;
-    arg.name = 0;
-    arg.path = 0;
-    arg.klass = klass;
-    arg.track = rb_cObject;
-    arg.prev = 0;
-    if (RCLASS_CONST_TBL(rb_cObject)) {
-	rb_id_table_foreach(RCLASS_CONST_TBL(rb_cObject), fc_i, &arg);
-    }
-    if (arg.path) {
-	st_data_t tmp = tmp_classpath;
-	if (!RCLASS_IV_TBL(klass)) {
-	    RCLASS_IV_TBL(klass) = st_init_numtable();
-	}
-	rb_class_ivar_set(klass, classpath, arg.path);
-
-	st_delete(RCLASS_IV_TBL(klass), &tmp, 0);
-	return arg.path;
-    }
-    return Qnil;
-}
-
 /**
  * Returns +classpath+ of _klass_, if it is named, or +nil+ for
- * anonymous +class+/+module+.  The last part of named +classpath+ is
- * never anonymous, but anonymous +class+/+module+ names may be
- * contained.  If the path is "permanent", that means it has no
- * anonymous names, <code>*permanent</code> is set to 1.
+ * anonymous +class+/+module+. A named +classpath+ may contain
+ * an anonymous component, but the last component is guaranteed
+ * to not be anonymous. <code>*permanent</code> is set to 1
+ * if +classpath+ has no anonymous components. There is no builtin
+ * Ruby level APIs that can change a permanent +classpath+.
  */
 static VALUE
 classname(VALUE klass, int *permanent)
 {
-    VALUE path = Qnil;
+    st_table *ivtbl;
     st_data_t n;
 
-    if (!klass) klass = rb_cObject;
-    *permanent = 1;
-    if (RCLASS_IV_TBL(klass)) {
-	if (!st_lookup(RCLASS_IV_TBL(klass), (st_data_t)classpath, &n)) {
-	    ID cid = 0;
-	    if (st_lookup(RCLASS_IV_TBL(klass), (st_data_t)classid, &n)) {
-		VALUE cname = (VALUE)n;
-		cid = rb_check_id(&cname);
-		if (cid) path = find_class_path(klass, cid);
-	    }
-	    if (NIL_P(path)) {
-		path = find_class_path(klass, (ID)0);
-	    }
-	    if (NIL_P(path)) {
-		if (!cid) {
-		    return Qnil;
-		}
-		if (!st_lookup(RCLASS_IV_TBL(klass), (st_data_t)tmp_classpath, &n)) {
-		    path = rb_id2str(cid);
-		    return path;
-		}
-		*permanent = 0;
-		path = (VALUE)n;
-		return path;
-	    }
-	}
-	else {
-	    path = (VALUE)n;
-	}
-	if (!RB_TYPE_P(path, T_STRING)) {
-	    rb_bug("class path is not set properly");
-	}
-	return path;
+    *permanent = 0;
+    if (!RCLASS_EXT(klass)) return Qnil;
+    if (!(ivtbl = RCLASS_IV_TBL(klass))) return Qnil;
+    if (st_lookup(ivtbl, (st_data_t)classpath, &n)) {
+        *permanent = 1;
+        return (VALUE)n;
     }
-    return find_class_path(klass, (ID)0);
+    if (st_lookup(ivtbl, (st_data_t)tmp_classpath, &n)) return (VALUE)n;
+    return Qnil;
 }
 
 /*
@@ -268,22 +132,16 @@ make_temporary_path(VALUE obj, VALUE klass)
     return path;
 }
 
-typedef VALUE (*path_cache_func)(VALUE obj, VALUE name);
+typedef VALUE (*fallback_func)(VALUE obj, VALUE name);
 
 static VALUE
-rb_tmp_class_path(VALUE klass, int *permanent, path_cache_func cache_path)
+rb_tmp_class_path(VALUE klass, int *permanent, fallback_func fallback)
 {
     VALUE path = classname(klass, permanent);
-    st_data_t n = (st_data_t)path;
 
     if (!NIL_P(path)) {
 	return path;
     }
-    if (RCLASS_IV_TBL(klass) && st_lookup(RCLASS_IV_TBL(klass),
-					  (st_data_t)tmp_classpath, &n)) {
-	*permanent = 0;
-	return (VALUE)n;
-    }
     else {
 	if (RB_TYPE_P(klass, T_MODULE)) {
 	    if (rb_obj_class(klass) == rb_cModule) {
@@ -291,40 +149,19 @@ rb_tmp_class_path(VALUE klass, int *permanent, path_cache_func cache_path)
 	    }
 	    else {
 		int perm;
-		path = rb_tmp_class_path(RBASIC(klass)->klass, &perm, cache_path);
+		path = rb_tmp_class_path(RBASIC(klass)->klass, &perm, fallback);
 	    }
 	}
 	*permanent = 0;
-	return cache_path(klass, path);
+	return fallback(klass, path);
     }
 }
 
-static VALUE
-ivar_cache(VALUE obj, VALUE name)
-{
-    return rb_ivar_set(obj, tmp_classpath, make_temporary_path(obj, name));
-}
-
 VALUE
 rb_class_path(VALUE klass)
 {
     int permanent;
-    VALUE path = rb_tmp_class_path(klass, &permanent, ivar_cache);
-    if (!NIL_P(path)) path = rb_str_dup(path);
-    return path;
-}
-
-static VALUE
-null_cache(VALUE obj, VALUE name)
-{
-    return make_temporary_path(obj, name);
-}
-
-VALUE
-rb_class_path_no_cache(VALUE klass)
-{
-    int permanent;
-    VALUE path = rb_tmp_class_path(klass, &permanent, null_cache);
+    VALUE path = rb_tmp_class_path(klass, &permanent, make_temporary_path);
     if (!NIL_P(path)) path = rb_str_dup(path);
     return path;
 }
@@ -332,18 +169,12 @@ rb_class_path_no_cache(VALUE klass)
 VALUE
 rb_class_path_cached(VALUE klass)
 {
-    st_table *ivtbl;
-    st_data_t n;
-
-    if (!RCLASS_EXT(klass)) return Qnil;
-    if (!(ivtbl = RCLASS_IV_TBL(klass))) return Qnil;
-    if (st_lookup(ivtbl, (st_data_t)classpath, &n)) return (VALUE)n;
-    if (st_lookup(ivtbl, (st_data_t)tmp_classpath, &n)) return (VALUE)n;
-    return Qnil;
+    int permanent;
+    return classname(klass, &permanent);
 }
 
 static VALUE
-never_cache(VALUE obj, VALUE name)
+no_fallback(VALUE obj, VALUE name)
 {
     return name;
 }
@@ -352,7 +183,13 @@ VALUE
 rb_search_class_path(VALUE klass)
 {
     int permanent;
-    return rb_tmp_class_path(klass, &permanent, never_cache);
+    return rb_tmp_class_path(klass, &permanent, no_fallback);
+}
+
+static VALUE
+save_temporary_path(VALUE obj, VALUE name)
+{
+    return rb_ivar_set(obj, tmp_classpath, make_temporary_path(obj, name));
 }
 
 void
@@ -366,13 +203,12 @@ rb_set_class_path_string(VALUE klass, VALUE under, VALUE name)
     }
     else {
 	int permanent;
-	str = rb_str_dup(rb_tmp_class_path(under, &permanent, ivar_cache));
+	str = rb_str_dup(rb_tmp_class_path(under, &permanent, save_temporary_path));
 	rb_str_cat2(str, "::");
 	rb_str_append(str, name);
 	OBJ_FREEZE(str);
 	if (!permanent) {
 	    pathid = tmp_classpath;
-	    rb_ivar_set(klass, classid, rb_str_intern(name));
 	}
     }
     rb_ivar_set(klass, pathid, str);
@@ -389,12 +225,11 @@ rb_set_class_path(VALUE klass, VALUE under, const char *name)
     }
     else {
 	int permanent;
-	str = rb_str_dup(rb_tmp_class_path(under, &permanent, ivar_cache));
+	str = rb_str_dup(rb_tmp_class_path(under, &permanent, save_temporary_path));
 	rb_str_cat2(str, "::");
 	rb_str_cat2(str, name);
 	if (!permanent) {
 	    pathid = tmp_classpath;
-	    rb_ivar_set(klass, classid, rb_str_intern(rb_str_new_cstr(name)));
 	}
     }
     OBJ_FREEZE(str);
@@ -449,12 +284,6 @@ rb_path2class(const char *path)
     return rb_path_to_class(rb_str_new_cstr(path));
 }
 
-void
-rb_name_class(VALUE klass, ID id)
-{
-    rb_ivar_set(klass, classid, ID2SYM(id));
-}
-
 VALUE
 rb_class_name(VALUE klass)
 {
@@ -465,7 +294,7 @@ const char *
 rb_class2name(VALUE klass)
 {
     int permanent;
-    VALUE path = rb_tmp_class_path(rb_class_real(klass), &permanent, ivar_cache);
+    VALUE path = rb_tmp_class_path(rb_class_real(klass), &permanent, make_temporary_path);
     if (NIL_P(path)) return NULL;
     return RSTRING_PTR(path);
 }
@@ -2833,6 +2662,64 @@ check_before_mod_set(VALUE klass, ID id, VALUE val, const char *dest)
     rb_check_frozen(klass);
 }
 
+static VALUE
+build_const_path(VALUE head, ID tail)
+{
+    VALUE path = rb_str_dup(head);
+    rb_str_cat2(path, "::");
+    rb_str_append(path, rb_id2str(tail));
+    OBJ_FREEZE(path);
+    return path;
+}
+
+static void finalize_classpath_for_children(VALUE named_namespace);
+
+static enum rb_id_table_iterator_result
+finalize_classpath_i(ID id, VALUE v, void *payload)
+{
+    rb_const_entry_t *ce = (rb_const_entry_t *)v;
+    VALUE value = ce->value;
+    int has_permanent_classpath;
+    VALUE parental_path = *((VALUE *) payload);
+    if (!rb_is_const_id(id)) {
+        return ID_TABLE_CONTINUE;
+    }
+    if (!rb_namespace_p(value)) {
+        return ID_TABLE_CONTINUE;
+    }
+    classname(value, &has_permanent_classpath);
+    if (has_permanent_classpath) {
+        return ID_TABLE_CONTINUE;
+    }
+    rb_ivar_set(value, classpath, build_const_path(parental_path, id));
+    if (RCLASS_IV_TBL(value)) {
+        st_data_t tmp = tmp_classpath;
+        st_delete(RCLASS_IV_TBL(value), &tmp, 0);
+    }
+    finalize_classpath_for_children(value);
+
+    return ID_TABLE_CONTINUE;
+}
+
+/*
+ * Assign permanent classpaths to all namespaces that are directly or indirectly
+ * nested under +named_namespace+. +named_namespace+ must have a permanent
+ * classpath.
+ */
+static void
+finalize_classpath_for_children(VALUE named_namespace)
+{
+    struct rb_id_table *const_table = RCLASS_CONST_TBL(named_namespace);
+
+    if (const_table) {
+        int permanent;
+        VALUE parental_path = classname(named_namespace, &permanent);
+        VM_ASSERT(RB_TYPE_P(parental_path, T_STRING));
+        VM_ASSERT(permanent);
+        rb_id_table_foreach(const_table, finalize_classpath_i, &parental_path);
+    }
+}
+
 void
 rb_const_set(VALUE klass, ID id, VALUE val)
 {
@@ -2865,24 +2752,23 @@ rb_const_set(VALUE klass, ID id, VALUE val)
      * and avoid order-dependency on const_tbl
      */
     if (rb_cObject && rb_namespace_p(val)) {
-	if (NIL_P(rb_class_path_cached(val))) {
+        int val_path_permanent;
+        VALUE val_path = classname(val, &val_path_permanent);
+        if (NIL_P(val_path) || !val_path_permanent) {
 	    if (klass == rb_cObject) {
 		rb_ivar_set(val, classpath, rb_id2str(id));
-		rb_name_class(val, id);
+                finalize_classpath_for_children(val);
 	    }
 	    else {
-		VALUE path;
-		ID pathid;
-		st_data_t n;
-		st_table *ivtbl = RCLASS_IV_TBL(klass);
-		if (ivtbl &&
-		    (st_lookup(ivtbl, (st_data_t)(pathid = classpath), &n) ||
-		     st_lookup(ivtbl, (st_data_t)(pathid = tmp_classpath), &n))) {
-		    path = rb_str_dup((VALUE)n);
-		    rb_str_append(rb_str_cat2(path, "::"), rb_id2str(id));
-		    OBJ_FREEZE(path);
-		    rb_ivar_set(val, pathid, path);
-		    rb_name_class(val, id);
+                int parental_path_permanent;
+                VALUE parental_path = classname(klass, &parental_path_permanent);
+                if (!NIL_P(parental_path)) {
+                    if (parental_path_permanent && !val_path_permanent) {
+                        rb_ivar_set(val, classpath, build_const_path(parental_path, id));
+                        finalize_classpath_for_children(val);
+                    } else if (!parental_path_permanent && NIL_P(val_path)) {
+                        rb_ivar_set(val, tmp_classpath, build_const_path(parental_path, id));
+                    }
 		}
 	    }
 	}
diff --git a/vm.c b/vm.c
index 142573929a..f49cfa2c2b 100644
--- a/vm.c
+++ b/vm.c
@@ -374,8 +374,6 @@ rb_vm_inc_const_missing_count(void)
     ruby_vm_const_missing_count +=1;
 }
 
-VALUE rb_class_path_no_cache(VALUE _klass);
-
 MJIT_FUNC_EXPORTED int
 rb_dtrace_setup(rb_execution_context_t *ec, VALUE klass, ID id,
 		struct ruby_dtrace_method_hook_args *args)
@@ -395,7 +393,7 @@ rb_dtrace_setup(rb_execution_context_t *ec, VALUE klass, ID id,
     }
     type = BUILTIN_TYPE(klass);
     if (type == T_CLASS || type == T_ICLASS || type == T_MODULE) {
-	VALUE name = rb_class_path_no_cache(klass);
+	VALUE name = rb_class_path(klass);
 	const char *classname, *filename;
 	const char *methodname = rb_id2name(id);
 	if (methodname && (filename = rb_source_location_cstr(&args->line_no)) != 0) {
-- 
2.21.0

