Optimize array pattern matching by caching #deconstruct value
This commit is contained in:
parent
6770d8f1b0
commit
5320375732
Notes:
git
2020-06-27 13:51:28 +09:00
80
compile.c
80
compile.c
@ -5609,10 +5609,10 @@ compile_case2(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const orig_no
|
|||||||
return COMPILE_OK;
|
return COMPILE_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int iseq_compile_pattern_match(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, LABEL *unmatched, int in_alt_pattern);
|
static int iseq_compile_pattern_match(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, LABEL *unmatched, int in_alt_pattern, int deconstructed_pos);
|
||||||
|
|
||||||
static int
|
static int
|
||||||
iseq_compile_pattern_each(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, LABEL *matched, LABEL *unmatched, int in_alt_pattern)
|
iseq_compile_pattern_each(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, LABEL *matched, LABEL *unmatched, int in_alt_pattern, int deconstructed_pos)
|
||||||
{
|
{
|
||||||
const int line = nd_line(node);
|
const int line = nd_line(node);
|
||||||
|
|
||||||
@ -5679,14 +5679,19 @@ iseq_compile_pattern_each(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *c
|
|||||||
const int use_rest_num = apinfo->rest_arg && (NODE_NAMED_REST_P(apinfo->rest_arg) ||
|
const int use_rest_num = apinfo->rest_arg && (NODE_NAMED_REST_P(apinfo->rest_arg) ||
|
||||||
(!NODE_NAMED_REST_P(apinfo->rest_arg) && post_args_num > 0));
|
(!NODE_NAMED_REST_P(apinfo->rest_arg) && post_args_num > 0));
|
||||||
|
|
||||||
LABEL *match_failed, *type_error;
|
LABEL *match_failed, *type_error, *deconstruct, *deconstructed;
|
||||||
int i;
|
int i;
|
||||||
match_failed = NEW_LABEL(line);
|
match_failed = NEW_LABEL(line);
|
||||||
type_error = NEW_LABEL(line);
|
type_error = NEW_LABEL(line);
|
||||||
|
deconstruct = NEW_LABEL(line);
|
||||||
|
deconstructed = NEW_LABEL(line);
|
||||||
|
|
||||||
if (use_rest_num) {
|
if (use_rest_num) {
|
||||||
ADD_INSN1(ret, line, putobject, INT2FIX(0)); /* allocate stack for rest_num */
|
ADD_INSN1(ret, line, putobject, INT2FIX(0)); /* allocate stack for rest_num */
|
||||||
ADD_INSN(ret, line, swap);
|
ADD_INSN(ret, line, swap);
|
||||||
|
if (deconstructed_pos) {
|
||||||
|
deconstructed_pos++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node->nd_pconst) {
|
if (node->nd_pconst) {
|
||||||
@ -5696,17 +5701,52 @@ iseq_compile_pattern_each(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *c
|
|||||||
ADD_INSNL(ret, line, branchunless, match_failed);
|
ADD_INSNL(ret, line, branchunless, match_failed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: this optimization allows us to re-use the #deconstruct value
|
||||||
|
// (or its absence).
|
||||||
|
// `deconstructed_pos` contains the distance to the stack relative location
|
||||||
|
// where the value is stored.
|
||||||
|
if (deconstructed_pos) {
|
||||||
|
// If value is nil then we haven't tried to deconstruct
|
||||||
|
ADD_INSN1(ret, line, topn, INT2FIX(deconstructed_pos));
|
||||||
|
ADD_INSNL(ret, line, branchnil, deconstruct);
|
||||||
|
|
||||||
|
// If false then the value is not deconstructable
|
||||||
|
ADD_INSN1(ret, line, topn, INT2FIX(deconstructed_pos));
|
||||||
|
ADD_INSNL(ret, line, branchunless, match_failed);
|
||||||
|
|
||||||
|
// Drop value, add deconstructed to the stack and jump
|
||||||
|
ADD_INSN(ret, line, pop);
|
||||||
|
ADD_INSN1(ret, line, topn, INT2FIX(deconstructed_pos - 1));
|
||||||
|
ADD_INSNL(ret, line, jump, deconstructed);
|
||||||
|
} else {
|
||||||
|
ADD_INSNL(ret, line, jump, deconstruct);
|
||||||
|
}
|
||||||
|
|
||||||
|
ADD_LABEL(ret, deconstruct);
|
||||||
ADD_INSN(ret, line, dup);
|
ADD_INSN(ret, line, dup);
|
||||||
ADD_INSN1(ret, line, putobject, ID2SYM(rb_intern("deconstruct")));
|
ADD_INSN1(ret, line, putobject, ID2SYM(rb_intern("deconstruct")));
|
||||||
ADD_SEND(ret, line, idRespond_to, INT2FIX(1));
|
ADD_SEND(ret, line, idRespond_to, INT2FIX(1));
|
||||||
|
|
||||||
|
// Cache the result of respond_to? (in case it's false is stays there, if true — it's overwritten after #deconstruct)
|
||||||
|
if (deconstructed_pos) {
|
||||||
|
ADD_INSN1(ret, line, setn, INT2FIX(deconstructed_pos + 1));
|
||||||
|
}
|
||||||
|
|
||||||
ADD_INSNL(ret, line, branchunless, match_failed);
|
ADD_INSNL(ret, line, branchunless, match_failed);
|
||||||
|
|
||||||
ADD_SEND(ret, line, rb_intern("deconstruct"), INT2FIX(0));
|
ADD_SEND(ret, line, rb_intern("deconstruct"), INT2FIX(0));
|
||||||
|
|
||||||
|
// Cache the result (if it's cacheable — currently, only top-level array patterns)
|
||||||
|
if (deconstructed_pos) {
|
||||||
|
ADD_INSN1(ret, line, setn, INT2FIX(deconstructed_pos));
|
||||||
|
}
|
||||||
|
|
||||||
ADD_INSN(ret, line, dup);
|
ADD_INSN(ret, line, dup);
|
||||||
ADD_INSN1(ret, line, checktype, INT2FIX(T_ARRAY));
|
ADD_INSN1(ret, line, checktype, INT2FIX(T_ARRAY));
|
||||||
ADD_INSNL(ret, line, branchunless, type_error);
|
ADD_INSNL(ret, line, branchunless, type_error);
|
||||||
|
ADD_INSNL(ret, line, jump, deconstructed);
|
||||||
|
|
||||||
|
ADD_LABEL(ret, deconstructed);
|
||||||
ADD_INSN(ret, line, dup);
|
ADD_INSN(ret, line, dup);
|
||||||
ADD_SEND(ret, line, idLength, INT2FIX(0));
|
ADD_SEND(ret, line, idLength, INT2FIX(0));
|
||||||
ADD_INSN1(ret, line, putobject, INT2FIX(min_argc));
|
ADD_INSN1(ret, line, putobject, INT2FIX(min_argc));
|
||||||
@ -5717,7 +5757,7 @@ iseq_compile_pattern_each(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *c
|
|||||||
ADD_INSN(ret, line, dup);
|
ADD_INSN(ret, line, dup);
|
||||||
ADD_INSN1(ret, line, putobject, INT2FIX(i));
|
ADD_INSN1(ret, line, putobject, INT2FIX(i));
|
||||||
ADD_SEND(ret, line, idAREF, INT2FIX(1));
|
ADD_SEND(ret, line, idAREF, INT2FIX(1));
|
||||||
CHECK(iseq_compile_pattern_match(iseq, ret, args->nd_head, match_failed, in_alt_pattern));
|
CHECK(iseq_compile_pattern_match(iseq, ret, args->nd_head, match_failed, in_alt_pattern, FALSE));
|
||||||
args = args->nd_next;
|
args = args->nd_next;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -5732,7 +5772,7 @@ iseq_compile_pattern_each(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *c
|
|||||||
ADD_INSN1(ret, line, setn, INT2FIX(4));
|
ADD_INSN1(ret, line, setn, INT2FIX(4));
|
||||||
ADD_SEND(ret, line, idAREF, INT2FIX(2));
|
ADD_SEND(ret, line, idAREF, INT2FIX(2));
|
||||||
|
|
||||||
CHECK(iseq_compile_pattern_match(iseq, ret, apinfo->rest_arg, match_failed, in_alt_pattern));
|
CHECK(iseq_compile_pattern_match(iseq, ret, apinfo->rest_arg, match_failed, in_alt_pattern, FALSE));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (post_args_num > 0) {
|
if (post_args_num > 0) {
|
||||||
@ -5755,7 +5795,7 @@ iseq_compile_pattern_each(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *c
|
|||||||
ADD_SEND(ret, line, idPLUS, INT2FIX(1));
|
ADD_SEND(ret, line, idPLUS, INT2FIX(1));
|
||||||
|
|
||||||
ADD_SEND(ret, line, idAREF, INT2FIX(1));
|
ADD_SEND(ret, line, idAREF, INT2FIX(1));
|
||||||
CHECK(iseq_compile_pattern_match(iseq, ret, args->nd_head, match_failed, in_alt_pattern));
|
CHECK(iseq_compile_pattern_match(iseq, ret, args->nd_head, match_failed, in_alt_pattern, FALSE));
|
||||||
args = args->nd_next;
|
args = args->nd_next;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6078,7 +6118,7 @@ iseq_compile_pattern_each(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *c
|
|||||||
ADD_INSN(match_values, line, dup);
|
ADD_INSN(match_values, line, dup);
|
||||||
ADD_INSN1(match_values, line, putobject, key);
|
ADD_INSN1(match_values, line, putobject, key);
|
||||||
ADD_SEND(match_values, line, node->nd_pkwrestarg ? rb_intern("delete") : idAREF, INT2FIX(1));
|
ADD_SEND(match_values, line, node->nd_pkwrestarg ? rb_intern("delete") : idAREF, INT2FIX(1));
|
||||||
CHECK(iseq_compile_pattern_match(iseq, match_values, value_node, match_failed, in_alt_pattern));
|
CHECK(iseq_compile_pattern_match(iseq, match_values, value_node, match_failed, in_alt_pattern, FALSE));
|
||||||
args = args->nd_next->nd_next;
|
args = args->nd_next->nd_next;
|
||||||
}
|
}
|
||||||
ADD_SEQ(ret, match_values);
|
ADD_SEQ(ret, match_values);
|
||||||
@ -6098,7 +6138,7 @@ iseq_compile_pattern_each(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *c
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
ADD_INSN(ret, line, dup);
|
ADD_INSN(ret, line, dup);
|
||||||
CHECK(iseq_compile_pattern_match(iseq, ret, node->nd_pkwrestarg, match_failed, in_alt_pattern));
|
CHECK(iseq_compile_pattern_match(iseq, ret, node->nd_pkwrestarg, match_failed, in_alt_pattern, FALSE));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6188,7 +6228,7 @@ iseq_compile_pattern_each(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *c
|
|||||||
case NODE_UNLESS: {
|
case NODE_UNLESS: {
|
||||||
LABEL *match_failed;
|
LABEL *match_failed;
|
||||||
match_failed = unmatched;
|
match_failed = unmatched;
|
||||||
CHECK(iseq_compile_pattern_match(iseq, ret, node->nd_body, unmatched, in_alt_pattern));
|
CHECK(iseq_compile_pattern_match(iseq, ret, node->nd_body, unmatched, in_alt_pattern, deconstructed_pos));
|
||||||
CHECK(COMPILE(ret, "case in if", node->nd_cond));
|
CHECK(COMPILE(ret, "case in if", node->nd_cond));
|
||||||
if (nd_type(node) == NODE_IF) {
|
if (nd_type(node) == NODE_IF) {
|
||||||
ADD_INSNL(ret, line, branchunless, match_failed);
|
ADD_INSNL(ret, line, branchunless, match_failed);
|
||||||
@ -6211,8 +6251,8 @@ iseq_compile_pattern_each(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *c
|
|||||||
}
|
}
|
||||||
|
|
||||||
ADD_INSN(ret, line, dup);
|
ADD_INSN(ret, line, dup);
|
||||||
CHECK(iseq_compile_pattern_match(iseq, ret, n->nd_head, match_failed, in_alt_pattern));
|
CHECK(iseq_compile_pattern_match(iseq, ret, n->nd_head, match_failed, in_alt_pattern, deconstructed_pos ? deconstructed_pos + 1 : FALSE));
|
||||||
CHECK(iseq_compile_pattern_each(iseq, ret, n->nd_next->nd_head, matched, match_failed, in_alt_pattern));
|
CHECK(iseq_compile_pattern_each(iseq, ret, n->nd_next->nd_head, matched, match_failed, in_alt_pattern, FALSE));
|
||||||
|
|
||||||
ADD_LABEL(ret, match_failed);
|
ADD_LABEL(ret, match_failed);
|
||||||
ADD_INSN(ret, line, pop);
|
ADD_INSN(ret, line, pop);
|
||||||
@ -6225,12 +6265,12 @@ iseq_compile_pattern_each(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *c
|
|||||||
fin = NEW_LABEL(line);
|
fin = NEW_LABEL(line);
|
||||||
|
|
||||||
ADD_INSN(ret, line, dup);
|
ADD_INSN(ret, line, dup);
|
||||||
CHECK(iseq_compile_pattern_each(iseq, ret, node->nd_1st, match_succeeded, fin, TRUE));
|
CHECK(iseq_compile_pattern_each(iseq, ret, node->nd_1st, match_succeeded, fin, TRUE, deconstructed_pos ? deconstructed_pos + 1 : FALSE));
|
||||||
ADD_LABEL(ret, match_succeeded);
|
ADD_LABEL(ret, match_succeeded);
|
||||||
ADD_INSN(ret, line, pop);
|
ADD_INSN(ret, line, pop);
|
||||||
ADD_INSNL(ret, line, jump, matched);
|
ADD_INSNL(ret, line, jump, matched);
|
||||||
ADD_LABEL(ret, fin);
|
ADD_LABEL(ret, fin);
|
||||||
CHECK(iseq_compile_pattern_each(iseq, ret, node->nd_2nd, matched, unmatched, TRUE));
|
CHECK(iseq_compile_pattern_each(iseq, ret, node->nd_2nd, matched, unmatched, TRUE, deconstructed_pos));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@ -6240,10 +6280,10 @@ iseq_compile_pattern_each(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *c
|
|||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
iseq_compile_pattern_match(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, LABEL *unmatched, int in_alt_pattern)
|
iseq_compile_pattern_match(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, LABEL *unmatched, int in_alt_pattern, int deconstructed_pos)
|
||||||
{
|
{
|
||||||
LABEL *fin = NEW_LABEL(nd_line(node));
|
LABEL *fin = NEW_LABEL(nd_line(node));
|
||||||
CHECK(iseq_compile_pattern_each(iseq, ret, node, fin, unmatched, in_alt_pattern));
|
CHECK(iseq_compile_pattern_each(iseq, ret, node, fin, unmatched, in_alt_pattern, deconstructed_pos));
|
||||||
ADD_LABEL(ret, fin);
|
ADD_LABEL(ret, fin);
|
||||||
return COMPILE_OK;
|
return COMPILE_OK;
|
||||||
}
|
}
|
||||||
@ -6278,6 +6318,8 @@ compile_case3(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const orig_no
|
|||||||
endlabel = NEW_LABEL(line);
|
endlabel = NEW_LABEL(line);
|
||||||
elselabel = NEW_LABEL(line);
|
elselabel = NEW_LABEL(line);
|
||||||
|
|
||||||
|
ADD_INSN(head, line, putnil); /* allocate stack for cached #deconstruct value */
|
||||||
|
ADD_INSN(head, line, swap);
|
||||||
ADD_SEQ(ret, head); /* case VAL */
|
ADD_SEQ(ret, head); /* case VAL */
|
||||||
|
|
||||||
while (type == NODE_IN) {
|
while (type == NODE_IN) {
|
||||||
@ -6286,6 +6328,7 @@ compile_case3(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const orig_no
|
|||||||
l1 = NEW_LABEL(line);
|
l1 = NEW_LABEL(line);
|
||||||
ADD_LABEL(body_seq, l1);
|
ADD_LABEL(body_seq, l1);
|
||||||
ADD_INSN(body_seq, line, pop);
|
ADD_INSN(body_seq, line, pop);
|
||||||
|
ADD_INSN(body_seq, line, pop); /* discard cached #deconstruct value */
|
||||||
add_trace_branch_coverage(
|
add_trace_branch_coverage(
|
||||||
iseq,
|
iseq,
|
||||||
body_seq,
|
body_seq,
|
||||||
@ -6301,7 +6344,10 @@ compile_case3(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const orig_no
|
|||||||
int pat_line = nd_line(pattern);
|
int pat_line = nd_line(pattern);
|
||||||
LABEL *next_pat = NEW_LABEL(pat_line);
|
LABEL *next_pat = NEW_LABEL(pat_line);
|
||||||
ADD_INSN (cond_seq, pat_line, dup);
|
ADD_INSN (cond_seq, pat_line, dup);
|
||||||
CHECK(iseq_compile_pattern_each(iseq, cond_seq, pattern, l1, next_pat, FALSE));
|
|
||||||
|
// NOTE: set deconstructed_pos to the current cached value location
|
||||||
|
// (it's "under" the matchee value, so it's position is 2)
|
||||||
|
CHECK(iseq_compile_pattern_each(iseq, cond_seq, pattern, l1, next_pat, FALSE, 2));
|
||||||
ADD_LABEL(cond_seq, next_pat);
|
ADD_LABEL(cond_seq, next_pat);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -6320,6 +6366,7 @@ compile_case3(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const orig_no
|
|||||||
if (node) {
|
if (node) {
|
||||||
ADD_LABEL(cond_seq, elselabel);
|
ADD_LABEL(cond_seq, elselabel);
|
||||||
ADD_INSN(cond_seq, line, pop);
|
ADD_INSN(cond_seq, line, pop);
|
||||||
|
ADD_INSN(cond_seq, line, pop); /* discard cached #deconstruct value */
|
||||||
add_trace_branch_coverage(iseq, cond_seq, node, branch_id, "else", branches);
|
add_trace_branch_coverage(iseq, cond_seq, node, branch_id, "else", branches);
|
||||||
CHECK(COMPILE_(cond_seq, "else", node, popped));
|
CHECK(COMPILE_(cond_seq, "else", node, popped));
|
||||||
ADD_INSNL(cond_seq, line, jump, endlabel);
|
ADD_INSNL(cond_seq, line, jump, endlabel);
|
||||||
@ -6334,6 +6381,7 @@ compile_case3(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const orig_no
|
|||||||
ADD_SEND(cond_seq, nd_line(orig_node), id_core_raise, INT2FIX(2));
|
ADD_SEND(cond_seq, nd_line(orig_node), id_core_raise, INT2FIX(2));
|
||||||
ADD_INSN(cond_seq, nd_line(orig_node), pop);
|
ADD_INSN(cond_seq, nd_line(orig_node), pop);
|
||||||
ADD_INSN(cond_seq, nd_line(orig_node), pop);
|
ADD_INSN(cond_seq, nd_line(orig_node), pop);
|
||||||
|
ADD_INSN(cond_seq, nd_line(orig_node), pop); /* discard cached #deconstruct value */
|
||||||
if (!popped) {
|
if (!popped) {
|
||||||
ADD_INSN(cond_seq, nd_line(orig_node), putnil);
|
ADD_INSN(cond_seq, nd_line(orig_node), putnil);
|
||||||
}
|
}
|
||||||
|
@ -1265,6 +1265,85 @@ END
|
|||||||
|
|
||||||
################################################################
|
################################################################
|
||||||
|
|
||||||
|
class CDeconstructCache
|
||||||
|
def initialize(v)
|
||||||
|
@v = v
|
||||||
|
end
|
||||||
|
|
||||||
|
def deconstruct
|
||||||
|
@v.shift
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_deconstruct_cache
|
||||||
|
assert_block do
|
||||||
|
case CDeconstructCache.new([[0]])
|
||||||
|
in [1]
|
||||||
|
in [0]
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_block do
|
||||||
|
case CDeconstructCache.new([[0, 1]])
|
||||||
|
in [1,]
|
||||||
|
in [0,]
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_block do
|
||||||
|
case CDeconstructCache.new([[[0]]])
|
||||||
|
in [[1]]
|
||||||
|
in [[*a]]
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_block do
|
||||||
|
case CDeconstructCache.new([[0]])
|
||||||
|
in [x] if x > 0
|
||||||
|
in [0]
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_block do
|
||||||
|
case CDeconstructCache.new([[0]])
|
||||||
|
in []
|
||||||
|
in [1] | [0]
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_block do
|
||||||
|
case CDeconstructCache.new([[0]])
|
||||||
|
in [1] => one
|
||||||
|
in [0] => zero
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_block do
|
||||||
|
case CDeconstructCache.new([[0]])
|
||||||
|
in C[0]
|
||||||
|
in CDeconstructCache[0]
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_block do
|
||||||
|
case [CDeconstructCache.new([[0], [1]])]
|
||||||
|
in [[1]]
|
||||||
|
false
|
||||||
|
in [[1]]
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
################################################################
|
||||||
|
|
||||||
class TestPatternMatchingRefinements < Test::Unit::TestCase
|
class TestPatternMatchingRefinements < Test::Unit::TestCase
|
||||||
class C1
|
class C1
|
||||||
def deconstruct
|
def deconstruct
|
||||||
|
Loading…
x
Reference in New Issue
Block a user