Typemap matching rules enhancement for non-default typemaps. Previously all qualifiers were stripped in one step, now they are stripped one at a time starting with the left most qualifier.
git-svn-id: https://swig.svn.sourceforge.net/svnroot/swig/trunk@12007 626c5289-ae23-0410-ae9c-e8d60b6d4f22
This commit is contained in:
parent
58a7e2c3d6
commit
efd200ffe2
10 changed files with 398 additions and 38 deletions
|
|
@ -1,6 +1,14 @@
|
|||
Version 2.0.0 (in progress)
|
||||
============================
|
||||
|
||||
2010-05-01: wsfulton
|
||||
Typemap matching enhancement for non-default typemaps. Previously all
|
||||
qualifiers were stripped in one step, now they are stripped one at a time
|
||||
starting with the left most qualifier. For example, int const*const
|
||||
is first stripped to int *const then int *.
|
||||
|
||||
*** POTENTIAL INCOMPATIBILITY ***
|
||||
|
||||
2010-04-25: bhy
|
||||
[Python] Fix #2985655 - broken constructor renaming.
|
||||
|
||||
|
|
@ -37,7 +45,7 @@ Version 2.0.0 (in progress)
|
|||
Numerous subtle typemap matching rule fixes when using the default type. The typemap
|
||||
matching rules are to take a type and find the best default typemap (SWIGTYPE, SWIGTYPE* etc),
|
||||
then look for the next best match by reducing the chosen default type. The type deduction
|
||||
now follows C++ template partial specialization matching rules.
|
||||
now follows C++ class template partial specialization matching rules.
|
||||
|
||||
Below are the set of changes made showing the default type deduction
|
||||
along with the old reduced type and the new version of the reduced type:
|
||||
|
|
|
|||
|
|
@ -351,6 +351,7 @@
|
|||
<li><a href="Typemaps.html#Typemaps_nn17">Basic matching rules</a>
|
||||
<li><a href="Typemaps.html#Typemaps_typedef_reductions">Typedef reductions matching</a>
|
||||
<li><a href="Typemaps.html#Typemaps_nn19">Default typemap matching rules</a>
|
||||
<li><a href="Typemaps.html#Typemaps_matching_template_comparison">Matching comparison with C++ templates</a>
|
||||
<li><a href="Typemaps.html#Typemaps_multi_argument_typemaps_patterns">Multi-arguments typemaps</a>
|
||||
<li><a href="Typemaps.html#Typemaps_debugging_search">Debugging typemap pattern matching</a>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
<li><a href="#Typemaps_nn17">Basic matching rules</a>
|
||||
<li><a href="#Typemaps_typedef_reductions">Typedef reductions matching</a>
|
||||
<li><a href="#Typemaps_nn19">Default typemap matching rules</a>
|
||||
<li><a href="#Typemaps_matching_template_comparison">Matching comparison with C++ templates</a>
|
||||
<li><a href="#Typemaps_multi_argument_typemaps_patterns">Multi-arguments typemaps</a>
|
||||
<li><a href="#Typemaps_debugging_search">Debugging typemap pattern matching</a>
|
||||
</ul>
|
||||
|
|
@ -1026,8 +1027,10 @@ is used.
|
|||
</ul>
|
||||
|
||||
<p>
|
||||
If <tt>TYPE</tt> includes qualifiers (const, volatile, etc.), they are stripped to form a new stripped type
|
||||
If <tt>TYPE</tt> includes qualifiers (const, volatile, etc.), each qualifier is stripped one at a time to form a new stripped type
|
||||
and the matching rules above are repeated on the stripped type.
|
||||
The left-most qualifier is stripped first, resulting in the right-most (or top-level) qualifier being stripped last.
|
||||
For example <tt>int const*const</tt> is first stripped to <tt>int *const</tt> then <tt>int *</tt>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
|
|
@ -1056,8 +1059,8 @@ To find a typemap for the argument <tt>const char *s</tt>, SWIG will search for
|
|||
<pre>
|
||||
const char *s Exact type and name match
|
||||
const char * Exact type match
|
||||
char *s Type and name match (stripped qualifiers)
|
||||
char * Type match (stripped qualifiers)
|
||||
char *s Type and name match (qualifier stripped)
|
||||
char * Type match (qualifier stripped)
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
|
|
@ -1097,6 +1100,11 @@ void F(int x[1000]); // int [ANY] rule (typemap 5)
|
|||
</pre>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<b>Compatibility note: </b> SWIG-2.0.0 introduced stripping the qualifiers one step at a time. Prior versions
|
||||
stripped all qualifiers in one step.
|
||||
</p>
|
||||
|
||||
<H3><a name="Typemaps_typedef_reductions"></a>10.3.2 Typedef reductions matching</H3>
|
||||
|
||||
|
||||
|
|
@ -1280,7 +1288,7 @@ If the basic pattern matching rules result in no match being made, even after ty
|
|||
the default typemap matching rules are used to look for a suitable typemap match.
|
||||
These rules match a generic typemap based on the reserved <tt>SWIGTYPE</tt> base type.
|
||||
For example pointers will use <tt>SWIGTYPE *</tt> and references will use <tt>SWIGTYPE &</tt>.
|
||||
More precisely, the rules are based on the C++ template partial specialization matching rules used
|
||||
More precisely, the rules are based on the C++ class template partial specialization matching rules used
|
||||
by C++ compilers when looking for an appropriate partial template specialization.
|
||||
This means that a match is chosen from the most specialized set of generic typemap types available. For example,
|
||||
when looking for a match to <tt>int const *</tt>, the rules will prefer to match <tt>SWIGTYPE const *</tt>
|
||||
|
|
@ -1407,11 +1415,10 @@ Finally the best way to view the typemap matching rules in action is via the <a
|
|||
|
||||
<p>
|
||||
<b>Compatibility note: </b> The default typemap matching rules were modified in SWIG-2.0.0 from a slightly
|
||||
simpler scheme to match the current C++ template partial specialization matching rules.
|
||||
simpler scheme to match the current C++ class template partial specialization matching rules.
|
||||
</p>
|
||||
|
||||
|
||||
<H3><a name="Typemaps_multi_argument_typemaps_patterns"></a>10.3.4 Multi-arguments typemaps</H3>
|
||||
<H3><a name="Typemaps_multi_argument_typemaps_patterns"></a>10.3.5 Multi-arguments typemaps</H3>
|
||||
|
||||
|
||||
<p>
|
||||
|
|
@ -1441,7 +1448,165 @@ but all subsequent arguments must match exactly.
|
|||
</p>
|
||||
|
||||
|
||||
<H3><a name="Typemaps_debugging_search"></a>10.3.5 Debugging typemap pattern matching</H3>
|
||||
<H3><a name="Typemaps_matching_template_comparison"></a>10.3.4 Matching rules compared to C++ templates</H3>
|
||||
|
||||
<p>
|
||||
For those intimately familiar with C++ templates, a comparison of the typemap matching rules and template type deduction is interesting.
|
||||
The two areas considered are firstly the default typemaps and their similarities to partial template specialization and secondly, non-default typemaps and their similarities to full template specialization.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
For default (SWIGTYPE) typemaps the rules are inspired by C++ class template
|
||||
partial specialization. For example, given partial specialization for <tt>T const&</tt> :
|
||||
</p>
|
||||
|
||||
<div class="code">
|
||||
<pre>
|
||||
template <typename T> struct X { void a(); };
|
||||
template <typename T> struct X< T const& > { void b(); };
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
The full (unspecialized) template is matched with most types, such as:
|
||||
</p>
|
||||
|
||||
<div class="code">
|
||||
<pre>
|
||||
X< int & > x1; x1.a();
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
and the following all match the <tt>T const&</tt> partial specialization:
|
||||
</p>
|
||||
|
||||
<div class="code">
|
||||
<pre>
|
||||
X< int *const& > x2; x2.b();
|
||||
X< int const*const& > x3; x3.b();
|
||||
X< int const& > x4; x4.b();
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Now, given just these two default typemaps, where T is analogous to SWIGTYPE:
|
||||
</p>
|
||||
|
||||
<div class="code">
|
||||
<pre>
|
||||
%typemap(...) SWIGTYPE { ... }
|
||||
%typemap(...) SWIGTYPE const& { ... }
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
The generic default typemap <tt>SWIGTYPE</tt> is used with most types, such as
|
||||
</p>
|
||||
|
||||
<div class="code">
|
||||
<pre>
|
||||
int &
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
and the following all match the <tt>SWIGTYPE const&</tt> typemap, just like the partial template matching:
|
||||
</p>
|
||||
|
||||
<div class="code">
|
||||
<pre>
|
||||
int *const&
|
||||
int const*const&
|
||||
int const&
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Note that the template and typemap matching rules are not identical for all default typemaps though, for example, with arrays.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
For non-default typemaps, one might expect SWIG to follow the fully specialized template rules.
|
||||
This is nearly the case, but not quite.
|
||||
Consider a very similar example to the earlier partially specialized template but this time there is a fully specialized template:
|
||||
</p>
|
||||
|
||||
<div class="code">
|
||||
<pre>
|
||||
template <typename T> struct Y { void a(); };
|
||||
template <> struct Y< int const & > { void b(); };
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Only the one type matches the specialized template exactly:
|
||||
</p>
|
||||
|
||||
<div class="code">
|
||||
<pre>
|
||||
Y< int & > y1; y1.a();
|
||||
Y< int *const& > y2; y2.a();
|
||||
Y< int const *const& > y3; y3.a();
|
||||
Y< int const& > y4; y4.b(); // fully specialized match
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Given typemaps with the same types used for the template declared above, where T is again analogous to SWIGTYPE:
|
||||
</p>
|
||||
|
||||
<div class="code">
|
||||
<pre>
|
||||
%typemap(...) SWIGTYPE { ... }
|
||||
%typemap(...) int const& { ... }
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
The comparison between non-default typemaps and fully specialized single parameter templates turns out to be the same, as just the one type will match the non-default typemap:
|
||||
</p>
|
||||
|
||||
<div class="code">
|
||||
<pre>
|
||||
int &
|
||||
int *const&
|
||||
int const*const&
|
||||
int const& // matches non-default typemap int const&
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
However, if a non-const type is used instead:
|
||||
</p>
|
||||
|
||||
<div class="code">
|
||||
<pre>
|
||||
%typemap(...) SWIGTYPE { ... }
|
||||
%typemap(...) int & { ... }
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
then there is a clear difference to template matching as both the const and non-const types match the typemap:
|
||||
</p>
|
||||
|
||||
<div class="code">
|
||||
<pre>
|
||||
int & // matches non-default typemap int &
|
||||
int *const&
|
||||
int const*const&
|
||||
int const& // matches non-default typemap int &
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
There are other subtle differences such as typedef handling, but at least it should be clear that the typemap matching rules
|
||||
are similar to those for specialized template handling.
|
||||
</p>
|
||||
|
||||
|
||||
<H3><a name="Typemaps_debugging_search"></a>10.3.6 Debugging typemap pattern matching</H3>
|
||||
|
||||
|
||||
<p>
|
||||
|
|
|
|||
|
|
@ -393,6 +393,7 @@ CPP_TEST_CASES += \
|
|||
typemap_numinputs \
|
||||
typemap_template \
|
||||
typemap_out_optimal \
|
||||
typemap_qualifier_strip \
|
||||
typemap_variables \
|
||||
typemap_various \
|
||||
typename \
|
||||
|
|
|
|||
54
Examples/test-suite/python/typemap_qualifier_strip_runme.py
Normal file
54
Examples/test-suite/python/typemap_qualifier_strip_runme.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import typemap_qualifier_strip
|
||||
|
||||
val = typemap_qualifier_strip.create_int(111)
|
||||
if typemap_qualifier_strip.testA1(val) != 1234:
|
||||
raise RuntimeError
|
||||
|
||||
if typemap_qualifier_strip.testA2(val) != 1234:
|
||||
raise RuntimeError
|
||||
|
||||
if typemap_qualifier_strip.testA3(val) != 1234:
|
||||
raise RuntimeError
|
||||
|
||||
if typemap_qualifier_strip.testA4(val) != 1234:
|
||||
raise RuntimeError
|
||||
|
||||
|
||||
if typemap_qualifier_strip.testB1(val) != 111:
|
||||
raise RuntimeError
|
||||
|
||||
if typemap_qualifier_strip.testB2(val) != 111:
|
||||
raise RuntimeError
|
||||
|
||||
if typemap_qualifier_strip.testB3(val) != 111:
|
||||
raise RuntimeError
|
||||
|
||||
if typemap_qualifier_strip.testB4(val) != 111:
|
||||
raise RuntimeError
|
||||
|
||||
|
||||
if typemap_qualifier_strip.testC1(val) != 5678:
|
||||
raise RuntimeError
|
||||
|
||||
if typemap_qualifier_strip.testC2(val) != 111:
|
||||
raise RuntimeError
|
||||
|
||||
if typemap_qualifier_strip.testC3(val) != 5678:
|
||||
raise RuntimeError
|
||||
|
||||
if typemap_qualifier_strip.testC4(val) != 111:
|
||||
raise RuntimeError
|
||||
|
||||
|
||||
if typemap_qualifier_strip.testD1(val) != 111:
|
||||
raise RuntimeError
|
||||
|
||||
if typemap_qualifier_strip.testD2(val) != 3456:
|
||||
raise RuntimeError
|
||||
|
||||
if typemap_qualifier_strip.testD3(val) != 111:
|
||||
raise RuntimeError
|
||||
|
||||
if typemap_qualifier_strip.testD4(val) != 111:
|
||||
raise RuntimeError
|
||||
|
||||
76
Examples/test-suite/typemap_qualifier_strip.i
Normal file
76
Examples/test-suite/typemap_qualifier_strip.i
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
%module typemap_qualifier_strip
|
||||
|
||||
%typemap(in) int *ptr {
|
||||
int temp = 1234;
|
||||
$1 = &temp;
|
||||
}
|
||||
|
||||
%typemap(in) int *const ptrConst {
|
||||
int temp = 5678;
|
||||
$1 = &temp;
|
||||
}
|
||||
|
||||
%typemap(in) int const* constPtr {
|
||||
int temp = 3456;
|
||||
$1 = &temp;
|
||||
}
|
||||
|
||||
%inline %{
|
||||
int *create_int(int newval) {
|
||||
static int val = 0;
|
||||
val = newval;
|
||||
return &val;
|
||||
}
|
||||
int testA1(int const*const ptr) {
|
||||
return *ptr;
|
||||
}
|
||||
int testA2(int const* ptr) {
|
||||
return *ptr;
|
||||
}
|
||||
int testA3(int *const ptr) {
|
||||
return *ptr;
|
||||
}
|
||||
int testA4(int * ptr) {
|
||||
return *ptr;
|
||||
}
|
||||
|
||||
int testB1(int const*const p) {
|
||||
return *p;
|
||||
}
|
||||
int testB2(int const* p) {
|
||||
return *p;
|
||||
}
|
||||
int testB3(int *const p) {
|
||||
return *p;
|
||||
}
|
||||
int testB4(int * p) {
|
||||
return *p;
|
||||
}
|
||||
|
||||
int testC1(int const*const ptrConst) {
|
||||
return *ptrConst;
|
||||
}
|
||||
int testC2(int const* ptrConst) {
|
||||
return *ptrConst;
|
||||
}
|
||||
int testC3(int *const ptrConst) {
|
||||
return *ptrConst;
|
||||
}
|
||||
int testC4(int * ptrConst) {
|
||||
return *ptrConst;
|
||||
}
|
||||
|
||||
int testD1(int const*const constPtr) {
|
||||
return *constPtr;
|
||||
}
|
||||
int testD2(int const* constPtr) {
|
||||
return *constPtr;
|
||||
}
|
||||
int testD3(int *const constPtr) {
|
||||
return *constPtr;
|
||||
}
|
||||
int testD4(int * constPtr) {
|
||||
return *constPtr;
|
||||
}
|
||||
%}
|
||||
|
||||
|
|
@ -357,8 +357,8 @@ SwigType *SwigType_default_create(SwigType *ty) {
|
|||
* SwigType_default_deduce()
|
||||
*
|
||||
* This function implements type deduction used in the typemap matching rules
|
||||
* and is very close to the type deduction used in partial template specialization
|
||||
* matching in that the most specialized type is always chosen.
|
||||
* and is very close to the type deduction used in partial template class
|
||||
* specialization matching in that the most specialized type is always chosen.
|
||||
* SWIGTYPE is used as the generic type. The basic idea is to repeatedly call
|
||||
* this function to find a deduced type unless until nothing matches.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@ extern "C" {
|
|||
extern int SwigType_isenum(SwigType *t);
|
||||
extern int SwigType_check_decl(SwigType *t, const_String_or_char_ptr decl);
|
||||
extern SwigType *SwigType_strip_qualifiers(SwigType *t);
|
||||
extern SwigType *SwigType_strip_single_qualifier(SwigType *t);
|
||||
extern SwigType *SwigType_functionpointer_decompose(SwigType *t);
|
||||
extern String *SwigType_base(const SwigType *t);
|
||||
extern String *SwigType_namestr(const SwigType *t);
|
||||
|
|
|
|||
|
|
@ -698,11 +698,11 @@ static Hash *typemap_search(const_String_or_char_ptr tmap_method, SwigType *type
|
|||
Hash *backup = 0;
|
||||
SwigType *primitive = 0;
|
||||
SwigType *ctype = 0;
|
||||
SwigType *ctype_unstripped = 0;
|
||||
int ts;
|
||||
int isarray;
|
||||
const String *cname = 0;
|
||||
const String *cqualifiedname = 0;
|
||||
SwigType *unstripped = 0;
|
||||
String *tm_method = typemap_method_name(tmap_method);
|
||||
int debug_display = (in_typemap_search_multi == 0) && typemap_search_debug;
|
||||
|
||||
|
|
@ -718,7 +718,8 @@ static Hash *typemap_search(const_String_or_char_ptr tmap_method, SwigType *type
|
|||
Delete(typestr);
|
||||
}
|
||||
while (ts >= 0) {
|
||||
ctype = type;
|
||||
ctype = Copy(type);
|
||||
ctype_unstripped = Copy(ctype);
|
||||
while (ctype) {
|
||||
/* Try to get an exact type-match */
|
||||
tm = get_typemap(ts, ctype);
|
||||
|
|
@ -751,29 +752,25 @@ static Hash *typemap_search(const_String_or_char_ptr tmap_method, SwigType *type
|
|||
goto ret_result;
|
||||
}
|
||||
|
||||
/* No match so far. If the type is unstripped, we'll strip its
|
||||
qualifiers and check. Otherwise, we'll try to resolve a typedef */
|
||||
|
||||
if (!unstripped) {
|
||||
unstripped = ctype;
|
||||
ctype = SwigType_strip_qualifiers(ctype);
|
||||
if (!Equal(ctype, unstripped))
|
||||
continue; /* Types are different */
|
||||
Delete(ctype);
|
||||
ctype = unstripped;
|
||||
unstripped = 0;
|
||||
}
|
||||
/* No match so far - try with a qualifier stripped (strip one qualifier at a time until none remain)
|
||||
* The order of stripping in SwigType_strip_single_qualifier is used to provide some sort of consistency
|
||||
* with the default (SWIGTYPE) typemap matching rules for the first qualifier to be stripped. */
|
||||
{
|
||||
String *octype;
|
||||
if (unstripped) {
|
||||
Delete(ctype);
|
||||
ctype = unstripped;
|
||||
unstripped = 0;
|
||||
SwigType *oldctype = ctype;
|
||||
ctype = SwigType_strip_single_qualifier(oldctype);
|
||||
if (!Equal(ctype, oldctype)) {
|
||||
Delete(oldctype);
|
||||
continue;
|
||||
}
|
||||
octype = ctype;
|
||||
ctype = SwigType_typedef_resolve(ctype);
|
||||
if (octype != type)
|
||||
Delete(octype);
|
||||
Delete(oldctype);
|
||||
}
|
||||
|
||||
/* Once all qualifiers are stripped try resolve a typedef */
|
||||
{
|
||||
SwigType *oldctype = ctype;
|
||||
ctype = SwigType_typedef_resolve(ctype_unstripped);
|
||||
Delete(oldctype);
|
||||
ctype_unstripped = Copy(ctype);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -802,12 +799,10 @@ static Hash *typemap_search(const_String_or_char_ptr tmap_method, SwigType *type
|
|||
|
||||
ret_result:
|
||||
Delete(primitive);
|
||||
if ((unstripped) && (unstripped != type))
|
||||
Delete(unstripped);
|
||||
if (matchtype)
|
||||
*matchtype = Copy(ctype);
|
||||
if (type != ctype)
|
||||
Delete(ctype);
|
||||
Delete(ctype);
|
||||
Delete(ctype_unstripped);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1125,3 +1125,62 @@ SwigType *SwigType_strip_qualifiers(SwigType *t) {
|
|||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* SwigType_strip_single_qualifier()
|
||||
*
|
||||
* If the type contains a qualifier, strip one qualifier and return a new type.
|
||||
* The left most qualifier is stripped first (when viewed as C source code) but
|
||||
* this is the equivalent to the right most qualifier using SwigType notation.
|
||||
* Example:
|
||||
* r.q(const).p.q(const).int => r.q(const).p.int
|
||||
* r.q(const).p.int => r.p.int
|
||||
* r.p.int => r.p.int
|
||||
* ----------------------------------------------------------------------------- */
|
||||
|
||||
SwigType *SwigType_strip_single_qualifier(SwigType *t) {
|
||||
static Hash *memoize_stripped = 0;
|
||||
SwigType *r = 0;
|
||||
List *l;
|
||||
int numitems;
|
||||
|
||||
if (!memoize_stripped)
|
||||
memoize_stripped = NewHash();
|
||||
r = Getattr(memoize_stripped, t);
|
||||
if (r)
|
||||
return Copy(r);
|
||||
|
||||
l = SwigType_split(t);
|
||||
|
||||
numitems = Len(l);
|
||||
if (numitems >= 2) {
|
||||
int item;
|
||||
/* iterate backwards from last but one item */
|
||||
for (item = numitems - 2; item >= 0; --item) {
|
||||
String *subtype = Getitem(l, item);
|
||||
if (SwigType_isqualifier(subtype)) {
|
||||
Iterator it;
|
||||
Delitem(l, item);
|
||||
r = NewStringEmpty();
|
||||
for (it = First(l); it.item; it = Next(it)) {
|
||||
Append(r, it.item);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!r)
|
||||
r = Copy(t);
|
||||
|
||||
Delete(l);
|
||||
{
|
||||
String *key, *value;
|
||||
key = Copy(t);
|
||||
value = Copy(r);
|
||||
Setattr(memoize_stripped, key, value);
|
||||
Delete(key);
|
||||
Delete(value);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue