Improve SWIG %template directive scoping - take 2 (#1753)

* Correct convert_swig regex for qualified templates

Closes #1751

* Improve SWIG %template directive scoping - take 2

Refs #768

* Add nested namespace tests

Refs #768

* Replace ">>" with "> >"

Probably time to drop support for CentOS 7, ya know.

* Add SWIG template scoping documentation

Refs #1753
This commit is contained in:
dbankieris 2024-11-12 11:22:55 -06:00 committed by GitHub
parent e8508ea575
commit aecf6a06fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 279 additions and 125 deletions

View File

@ -408,24 +408,51 @@ Trick may use model code with any type of inheritance. Some limitations are pres
### Namespaces
Currently one level of namespace is supported. Additional levels of namespaces are ignored. Similarly classes and enumerations embedded in other classes are ignored.
ICG supports namespaces and nested scopes. Data recording and variable access via Trick View should work regardless of how many levels there are.
```C++
namespace my_ns {
// BB is processed
class BB {
public:
std::string str;
// Class CC is ignored.
class CC {
...
Namespaces and nested scopes are similarly supported in Python contexts, such as the input file and variable server, with some caveats regarding templates.
1. A template instantiation may be unqualified (have no use of the scope resolution operator `::`) only if its corresponding template is declared in the immediately-enclosing namespace.
2. Otherwise, a template instantiation must be fully qualified, starting from the global namespace.
3. Finally, instantiations of templates declared within the same class must be excluded from SWIG.
In the following examples, all template instantiations occur in `example::prime::Soup`. The immediately-enclosing namespace is `prime`, so only instantiations of templates declared directly in `prime` (only `Celery`) may be unqualified. All other template instantiations must be fully qualified, starting from the global namespace, even if the C++ name lookup process would find them with partial qualification.
```c++
template <class T> class Potato {};
namespace example {
template <class T> class Onion {};
namespace peer {
template <class T> class Raddish {};
}
namespace prime {
namespace inner {
template <class T> class Carrot {};
}
template <class T> class Celery {};
class Soup {
public:
template <class T> class Broth {};
::Potato<int> potato; // Rule 2
example::Onion<int> onion; // Rule 2
example::peer::Raddish<int> raddish; // Rule 2
example::prime::inner::Carrot<int> carrot; // Rule 2
Celery<int> celery; // Rule 1
#ifndef SWIG
Broth<int> broth; // Rule 3
#endif
};
// Everything enclosed in inner_ns is ignored.
namespace inner_ns {
...
};
};
}
}
```
### Function Overloading

View File

@ -57,7 +57,6 @@ my %sim ;
my %out_of_date ;
my ($version, $thread, $year) ;
my %processed_templates ;
my $global_template_typedefs ;
my $typedef_def = qr/typedef\s+ # the word typedef
(?:[_A-Za-z][\s\w]*\s*) # resolved type
@ -336,7 +335,6 @@ sub process_file() {
}
print OUT "\n$new_contents" ;
print OUT "$contents\n" ;
print OUT $global_template_typedefs ;
# Add a trick_cast_as macro line for each class parsed in the file. These lines must appear at the bottom of the
# file to ensure they are not in a namespace directive and they are after the #define statements they depend on.
@ -592,7 +590,8 @@ sub process_class($$$$$) {
my $extracted ;
my ($class_name) ;
my $template_typedefs ;
my @qualified_template_typedefs ;
my @unqualified_template_typedefs ;
## Extract the class_name from the class_string
$class_string =~ /^(?:class|struct)\s+ # keyword class or struct
@ -675,10 +674,6 @@ sub process_class($$$$$) {
my ($template_type_no_sp) = $template_full_type ;
$template_type_no_sp =~ s/\s//g ;
# If the type is qualified, assume it's fully qualified and put the
# %template directive in the global namespace.
# See https://github.com/nasa/trick/issues/768
my $qualified = $template_type_no_sp =~ /^\w+(::)\w+</ ;
#print "*** template_type_no_sp = $template_type_no_sp ***\n" ;
if ( ! exists $processed_templates{$template_type_no_sp} ) {
@ -686,29 +681,48 @@ sub process_class($$$$$) {
my $identifier = "${sanitized_namespace}${class_name}_${var_name}" ;
my $trick_swig_template = "TRICK_SWIG_TEMPLATE_$identifier" ;
# Insert template directive immediately before intsance
# Insert template directive immediately before instance
# This is required as of SWIG 4
my $typedef = "\n#ifndef $trick_swig_template\n" ;
$typedef .= "#define $trick_swig_template\n" ;
$typedef .= "\%template($identifier) $template_full_type;\n" ;
$typedef .= "#endif\n" ;
# SWIG namespace resolution for template directives starts at the local space
# Therefore, if the type is qualified, assume it's fully qualified and put the
# %template directive in the global namespace by escaping the current namespace
if ($curr_namespace ne "") {
my $in_same_namespace = 1 ;
if ($template_full_type =~ /^\w*(::)\w+</) {
$in_same_namespace = 0 ;
}
if ($in_same_namespace eq 0) {
$curr_namespace =~ /(.*)::/ ;
$typedef = "\n}" . $typedef . "namespace " . $1 . " {" ;
}
}
# A SWIG %template directive must:
# 1. Appear before each template instantiation
# 2. Be in scope where the instantiation is declared
# 3. Not be enclosed within a different namespace
#
# See https://www.swig.org/Doc4.2/SWIGPlus.html#SWIGPlus_template_scoping
#
# Generally, convert_swig is not smart enough to put all %template
# directives in the right scope because:
# 1. We do not keep track of what scope each type we encounter is
# declared in.
# 2. We cannot easily add %template directives to already-processed
# scopes.
#
# As a compromise:
# 1. For an unqualified template instantiation, the template declaration
# is necessarily in the current or a containing scope. Hope it's in the
# current namespace and put the %template directive there. This will
# create invalid SWIG input code for templates declared in a containing
# namespace and for member templates declared in this class.
# 2. For a qualified template instantiation, the template declaration is
# in a (possibly nested) scope contained by the current or a containing
# scope. Assume the instantiation is fully qualified and put it in the
# global namespace. This will create invalid SWIG code for partially-
# qualified instantiations.
#
# See https://github.com/nasa/trick/issues/768
if ($isSwigExcludeBlock == 0) {
$template_typedefs .= $typedef ;
if ($template_full_type =~ /^\w*(::\w+)+</) {
push @qualified_template_typedefs, $typedef ;
}
else {
push @unqualified_template_typedefs, $typedef ;
}
}
$processed_templates{$template_type_no_sp} = 1 ;
@ -721,12 +735,32 @@ sub process_class($$$$$) {
push @$class_names_ref , "$curr_namespace$class_name" ;
# write out the templated variable declaration lines found in this class.
$$new_contents_ref .= $template_typedefs."\n" ;
# Write out the %template directives for template instantiations found in
# this class. Put the directives for unqualified instantiations in the
# namespace containing this class, just before the class definition.
foreach (@unqualified_template_typedefs) {
$$new_contents_ref .= $_ ;
}
# Assume qualified template instantiations are fully qualified and put
# their %template directives in the global namespace.
# 1. close all namespaces, returning to the global namespace
# 2. add the %template directives
# 3. open all namespaces, restoring the scope
if (@qualified_template_typedefs) {
my @namespaces = split(/::/, $curr_namespace) ;
$$new_contents_ref .= "}" x @namespaces . "\n";
foreach (@qualified_template_typedefs) {
$$new_contents_ref .= $_ ;
}
$$new_contents_ref .= "\n";
foreach (@namespaces) {
$$new_contents_ref .= "namespace " . $_ . " { " ;
}
}
$$new_contents_ref .= $my_class_contents ;
# write the class contents and semicolon to ensure any template declarations below are after the semicolon.
$$new_contents_ref .= $extracted . ";\n" ;
$$new_contents_ref .= "\n" . $my_class_contents . $extracted . ";\n" ;
my $c_ = "$curr_namespace$class_name" ;
$c_ =~ s/\:/_/g ;

View File

@ -1,3 +1,2 @@
TRICK_CONVERT_SWIG_FLAGS := -s
TRICK_CFLAGS += -Imodels
TRICK_CXXFLAGS += -Imodels

View File

@ -1,93 +1,185 @@
#include <vector>
// @trick_parse{everything}
namespace a {
template<class> class A {
private:
void operator=(const A&);
#ifndef Classes_HH
#define Classes_HH
template <class T>
class Foo {};
namespace peer {
template <class T>
class Bar {};
struct Leek {
template <class T>
class Eggplant {};
};
namespace nested {
template <class T>
class Broccoli {};
struct Carrot {
template <class T>
class Celery {};
};
}
}
namespace b {
namespace prime {
template<class> class A {};
template<class> class B {};
template<class T>
class Potato {};
class C {
public:
std::vector<int> v;
a::A<int> a;
a::A<int> a2;
A<int> a3;
A<int> a4;
B<int> b;
B<int> b2;
b::B<int> b3;
b::B<int> b4;
B<a::A<int> > ba;
B<a::A<int> > ba2;
b::B<a::A<int> > ba3;
b::B<a::A<int> > ba4;
B<A<int> > ba5;
B<A<int> > ba6;
// These don't work. Because the type is qualified, convert_swig assumes
// it's fully qualified and puts the %template directive in the global
// namespace. However, there is no A in the global namespace, so the wrapper
// code fails to compile. Bonus points if you can fix this without breaking
// something else.
//b::B<A<int> > ba7;
//b::B<A<int> > ba8;
struct Turnip {
template <class T>
class Radish {};
};
// This class is the same as the previous. Seems weird, but it reveals scoping
// errors under SWIG 2. The replication is not necessary in SWIG 4, which has
// better error detection.
class D {
public:
std::vector<int> v;
a::A<int> a;
a::A<int> a2;
A<int> a3;
A<int> a4;
B<int> b;
B<int> b2;
b::B<int> b3;
b::B<int> b4;
B<a::A<int> > ba;
B<a::A<int> > ba2;
b::B<a::A<int> > ba3;
b::B<a::A<int> > ba4;
B<A<int> > ba5;
B<A<int> > ba6;
//b::B<A<int> > ba7;
//b::B<A<int> > ba8;
namespace nested {
template <class T>
class Baz {};
struct Artichoke {
template <class T>
class Asparagus {};
};
}
struct Onion {
// ======================= WORKING =============================================
/**
* We assume a qualified template instantiation is fully qualified
* starting from the global namespace, and we put the corresponding
* %template directive in the global namespace, which works if our
* assumption is correct.
*/
::Foo<int> fooInt;
peer::Bar<int> barInt;
::peer::Bar<double> barDouble;
peer::Leek::Eggplant<int> eggplantInt;
::peer::Leek::Eggplant<double> eggplantDouble;
peer::nested::Broccoli<int> broccoliInt;
::peer::nested::Broccoli<double> broccoliDouble;
peer::nested::Carrot::Celery<int> celeryInt;
::peer::nested::Carrot::Celery<double> celeryDouble;
prime::Potato<int> potatoInt;
::prime::Potato<double> potatoDouble;
prime::Turnip::Radish<int> radishInt;
::prime::Turnip::Radish<double> radishDouble;
prime::nested::Baz<int> bazInt;
::prime::nested::Baz<double> bazDouble;
prime::nested::Artichoke::Asparagus<int> asparagusint;
::prime::nested::Artichoke::Asparagus<double> asparagusDouble;
/**
* We put the %template directive for unqualified template
* instantiations in the namespace containing the current class,
* which only works if the template is defined in that namespace as
* well.
*/
Potato<bool> potatoBool;
// ======================= BROKEN ==============================================
/**
* Because Foo is declared in the global namespace, its %template
* directive must be in that scope as well. However, because the
* template instation here is unqualified, we put the %template
* directive in the containing namespace, which is an error.
*
* Error: 'Foo' resolves to '::Foo' and was incorrectly
* instantiated in scope 'prime' instead of within scope ''.
*/
// Foo<double> fooDouble;
/**
* Because these template instantiations are qualified, their %template
* directives are placed in the global namespace. However, because they
* are not _fully_ qualified, the generated SWIG input code is invalid.
* For partially-qualified instantiations, the directive should either be:
* 1. Declared in the global namespace and qualified with prime::
* 2. Declared in the prime namespace
*
* We don't track enough information to do either.
*
* Error: Template 'Turnip::Radish' undefined.
* Error: Template 'nested::Baz' undefined.
* Error: Template 'nested::Artichoke::Asparagus' undefined.
*/
// Turnip::Radish<bool> radishBool;
// nested::Baz<bool> bazBool;
// nested::Artichoke::Asparagus<bool> asparagusBool;
template<class T>
class InnerOnion {}; // onions have layers!
/**
* All %template directives are placed before the class containing their
* corresponding template instantiations. For member templates
* (templates declared within the class being processed), this results
* in the %template directive referencing the member template before it
* has been declared, which is an error.
*
* Error: Template 'InnerOnion' undefined.
* Error: Template 'Onion::InnerOnion' undefined.
* Error: Template 'prime::Onion::InnerOnion' undefined.
* Error: Template '::prime::Onion::InnerOnion' undefined.
*/
// InnerOnion<int> layer1;
// Onion::InnerOnion<double> layer2;
// prime::Onion::InnerOnion<bool> layer2;
// ::prime::Onion::InnerOnion<short> layer4;
};
// Same tests as above, but within a nested namespace.
namespace nested {
class Corn {
::Foo<peer::Leek> fooLeek;
peer::Bar<Foo<long> > barFoo;
::peer::Bar<peer::Leek> barLeek;
peer::Leek::Eggplant<long> eggplantLong;
::peer::Leek::Eggplant<peer::Leek> eggplantLeek;
peer::nested::Broccoli<long> broccoliLong;
::peer::nested::Broccoli<peer::Leek> broccoliLeek;
peer::nested::Carrot::Celery<long> celeryLong;
::peer::nested::Carrot::Celery<peer::Leek> celeryLeek;
prime::Potato<long> potatoLong;
::prime::Potato<peer::Leek> potatoLeek;
prime::Turnip::Radish<long> radishLong;
::prime::Turnip::Radish<peer::Leek> radishLeek;
prime::nested::Baz<long> bazLong;
::prime::nested::Baz<peer::Leek> bazLeek;
prime::nested::Artichoke::Asparagus<long> asparagusLong;
::prime::nested::Artichoke::Asparagus<peer::Leek> asparagusLeek;
// These fail in Onion, but work here because Onion has already been processed.
prime::Onion::InnerOnion<long> onionLong;
::prime::Onion::InnerOnion<peer::Leek> onionLeek;
// These fail for the same reason as in Onion.
// Foo<float> fooFloat;
// Turnip::Radish<float> radishFloat;
// nested::Baz<float> bazFloat;
// nested::Artichoke::Asparagus<float> asparagusFloat;
// Onion::InnerOnion<float> innerOnionFloat;
};
}
}
class E {
public:
std::vector<int> v;
a::A<int> a;
a::A<int> a2;
b::B<int> b3;
b::B<int> b4;
b::B<a::A<int> > ba3;
b::B<a::A<int> > ba4;
};
namespace a {
class B {
public:
std::vector<int> v;
a::A<int> a;
a::A<int> a2;
A<int> a3;
A<int> a4;
b::B<int> b3;
b::B<int> b4;
b::B<a::A<int> > ba3;
b::B<a::A<int> > ba4;
//b::B<A<int> > ba7;
//b::B<A<int> > ba8;
};
}
#endif

View File

@ -21,6 +21,8 @@ SIM_parse_s_define:
path: test/SIM_parse_s_define
SIM_target_specific_variables:
path: test/SIM_target_specific_variables
SIM_swig_template_scoping:
path: test/SIM_swig_template_scoping
SIM_test_abstract:
path: test/SIM_test_abstract
SIM_test_inherit: