diff --git a/cmake/sg.pl b/cmake/sg.pl index 2f0bbc14..d32e1106 100755 --- a/cmake/sg.pl +++ b/cmake/sg.pl @@ -8,49 +8,129 @@ # granted to it by virtue of its status as an intergovernmental organisation nor # does it submit to any jurisdiction. +# ========================================================================== +# sg.pl -- Serialisation Generator +# ========================================================================== +# +# PURPOSE +# +# Generates .b ("bless") files that are #included into C++ class bodies +# to provide eckit's persistence / serialisation infrastructure: +# +# - Bless constructor ClassName(eckit::Bless& b) +# - Evolve constructor ClassName(eckit::Evolve b) +# - specName() static type identifier string +# - isa() RTTI-style type chain registration +# - schema() member layout introspection +# - describe() human-readable dump (unless hand-written) +# - _export() binary export via eckit::Exporter +# +# A header opts in by containing #include "path/ClassName.b" inside +# the class body. The build system (CMake) detects this and invokes +# sg.pl on the header at configure/build time. +# +# USAGE +# +# perl sg.pl [output_dir] [namespace] +# +# header.h -- C++ header to parse (required) +# output_dir -- directory for generated .b files (default: dirname of header) +# namespace -- C++ namespace qualifying helper functions (default: "eckit") +# +# HOW IT WORKS +# +# 1. Preprocessing (parse sub, lines 177-183 in original) +# - Strip all preprocessor lines (^#...) -- includes, guards, macros +# - Strip all C++ line comments (//...) +# - Tokenize: split on word boundaries (\b), then split each +# non-word token into individual characters so that operators, +# braces, and punctuation each become a single token. +# - Drop empty/whitespace-only tokens. +# +# 2. Parsing +# A hand-written recursive-descent parser walks the token stream +# looking for class/struct definitions (possibly preceded by +# template<...>). For each class found it records: +# - name, base classes (super), template parameters +# - member variables (name + type) +# - member functions (name, type, args, const/static/virtual/...) +# - nested classes +# +# 3. Code generation +# For each parsed class, a .b file is written to output_dir +# containing the C++ method bodies listed above. +# +# KNOWN LIMITATIONS +# +# The parser is intentionally minimal -- just enough to handle the +# class declarations found in eckit headers that use the .b mechanism. +# +# - Template parameters: only "class", "bool", and "int" are accepted +# as parameter keywords. "typename" and non-type template parameters +# other than bool/int will cause a parse error. +# - Multi-declaration members (int a, b, *c;) are not supported. +# - Block comments (/* ... */) are only handled inside class bodies, +# not at file scope. +# - C-style casts, complex SFINAE expressions, and many modern C++ +# constructs are not understood and will cause parse failures if +# they appear in headers processed by this script. +# - Pure virtual = 0 is supported; = default / = delete are not. +# - Explicit template specializations (template <>) are skipped +# entirely, since they are never persistence classes. +# +# ========================================================================== + use strict; -#use Data::Dumper; use File::Basename; -#$Data::Dumper::Indent = 1; -# $ARGV[0] = "test.cc"; -# $ARGV[0] = "/usr/include/g++-3/stl_pair.h"; -# $ARGV[0] = "/usr/include/g++-3/stl_vector.h"; -# $ARGV[0] = "/usr/include/g++-3/stl_list.h"; -# $ARGV[0] = "/usr/include/g++-3/stl_map.h"; -# $ARGV[0] = "x.cc"; -# $ARGV[0] = "/usr/include/g++-3/std/bastring.h"; - -# script takes 3 parameters: -# (1) file to process +# -------------------------------------------------------------------------- +# Command-line arguments +# -------------------------------------------------------------------------- + +# (1) C++ header file to process (required) my $file = $ARGV[0]; -# (2) [optional] directory to place the generated .b file + +# (2) Output directory for generated .b files (optional, defaults to +# the directory containing the input file) my $base = $ARGV[1]; -# (3) [optional] c++ namespace + +# (3) C++ namespace used to qualify helper functions in the generated +# code -- eckit::_describe, eckit::_export, etc. (optional, +# defaults to "eckit") my $namespace = $ARGV[2]; -# no argv[1] passed, take basedir from file if( $base eq "" ) { $base = dirname($file); } -# no argv[1] passed, take basedir from file if( $namespace eq "" ) { $namespace = "eckit" } -my @c = parser::parse($file); -#print Dumper(\@c); +# -------------------------------------------------------------------------- +# Parse the header and generate .b files +# -------------------------------------------------------------------------- +# Parse returns a list of "class" objects -- one per class definition +# found in the header (forward declarations are skipped). +my @c = parser::parse($file); foreach my $c ( @c ) { my $n = $c->name; + + # Each class gets its own .b file, written to stdout redirected + # to $base/$n.b. open(STDOUT,">$base/$n.b") || die "$base/$n.b: $!"; + # ------------------------------------------------------------------ + # Build the Bless constructor initialiser list. + # Bless is the "deserialise from raw memory" path. Base classes + # are initialised with (b) and members with (b(&member)). + # ------------------------------------------------------------------ my @init1; push @init1, map { "$_(b)" } $c->super; push @init1, map { "$_(b(\&$_))" } $c->members; @@ -59,6 +139,12 @@ $col1=":\n" if(@init1); my $init1 = join(",\n",map {"\t$_"} @init1); + # ------------------------------------------------------------------ + # Build the Evolve constructor initialiser list. + # Evolve is the "deserialise from named-field archive" path. + # Base classes get (b("ClassName")) and members get + # (b("ClassName","memberName")). + # ------------------------------------------------------------------ my @init2; push @init2, map { "$_(b(\"$n\"))" } $c->super; push @init2, map { "$_(b(\"$n\",\"$_\"))" } $c->members; @@ -67,15 +153,28 @@ $col2=":\n" if(@init2); my $init2 = join(",\n",map {"\t$_"} @init2); - + # ------------------------------------------------------------------ + # describe() -- human-readable recursive dump of the object tree. + # Calls _startClass / base::describe / _describe(member) / _endClass. + # ------------------------------------------------------------------ my @s = map { "${_}::describe(s,depth+1)" } $c->super; my @m = map { "${namespace}::_describe(s,depth+1,\"$_\",$_)" } $c->members; my $d = join(";\n\t","${namespace}::_startClass(s,depth,specName())",@s,@m,"${namespace}::_endClass(s,depth,specName())"); + # ------------------------------------------------------------------ + # _export() -- binary export via eckit::Exporter. + # Same pattern: _startClass / base::_export / _export(member) / _endClass. + # ------------------------------------------------------------------ my @s = map { "${_}::_export(h)" } $c->super; my @m = map { "${namespace}::_export(h,\"$_\",$_)" } $c->members; my $D = join(";\n\t","${namespace}::_startClass(h,\"$n\")",@s,@m,"${namespace}::_endClass(h,\"$n\")"); + # ------------------------------------------------------------------ + # specName() -- static method returning the type's string identifier. + # For plain classes this is a const char* literal "ClassName". + # For template instantiations it builds a std::string at runtime: + # "ClassName<" + Traits::name() + "," + Traits::name() + ">" + # ------------------------------------------------------------------ my $spec = "\"$n\""; my @tmpl = $c->template; @@ -91,17 +190,29 @@ $spec =~ s/\n/ /g; } + # ------------------------------------------------------------------ + # isa() -- registers this type (and all bases) in eckit's RTTI chain. + # Generates: Base1::isa(t); Base2::isa(t); eckit::Isa::add(t, specName()); + # ------------------------------------------------------------------ my $isa = "${namespace}::Isa::add(t,specName());"; foreach my $s ( $c->super ) { $isa = "${s}::isa(t);$isa"; } + # ------------------------------------------------------------------ + # schema() -- introspection of member layout (name, size, offset, type). + # ------------------------------------------------------------------ my $schema; @s = map { "${_}::schema(s)" } $c->super; @m = map { $a=$_->[0]; $b=$_->[1]; "s.member(\"$a\",member_size($n,$a),member_offset($n,$a),\"$b\")" } $c->members_types; $schema = join(";\n\t","s.start(specName(),sizeof($n))",@s,@m,"s.end(specName())"); + # ------------------------------------------------------------------ + # Emit the .b file + # ------------------------------------------------------------------ + + # Bless constructor and Evolve constructor print <<"EOF"; ${n}(${namespace}::Bless& b)$col1$init1 @@ -123,8 +234,10 @@ EOF -if(!$c->has_method("describe")) -{ + # describe() is only generated if the class doesn't already define + # its own -- allows hand-written overrides. + if(!$c->has_method("describe")) + { print <<"EOF"; void describe(std::ostream& s,int depth = 0) const { @@ -133,9 +246,9 @@ EOF -} + } -print <<"EOF"; + print <<"EOF"; void _export(${namespace}::Exporter& h) const { $D; @@ -145,6 +258,9 @@ EOF } + +# Dead code -- older schema-only generator kept for reference. +# Guarded by if(0) so it never executes. if(0) { foreach my $c ( @c ) @@ -156,7 +272,6 @@ foreach my $x ( $c->super ) { print "${x}::schema(s);\n"; - #print "s(\"$x\", 0,sizeof($x));\n"; } foreach my $x ( $c->members ) { @@ -165,23 +280,66 @@ print "}\n"; } } + +# ========================================================================== +# package parser -- recursive-descent C++ header parser +# ========================================================================== +# +# Parses a minimal subset of C++ sufficient to extract class definitions +# with their base classes, member variables, and method signatures. +# +# The token stream is stored in the package-level @TOKENS array. +# Each "sub" below either consumes tokens or peeks at the head. +# +# ========================================================================== + package parser; use Carp; my @TOKENS; + +# -------------------------------------------------------------------------- +# parse($file) -> @classes +# +# Entry point. Reads the file, preprocesses it into a token stream, +# and runs the top-level grammar loop. +# +# Preprocessing: +# 1. Strip lines starting with # (preprocessor directives, include +# guards, macros). This means #define'd code is invisible to us. +# 2. Strip // line comments. +# 3. Tokenize: split on word boundaries, then split non-word chunks +# into individual characters (so "{", "(", "*", etc. are each one +# token). Drop whitespace-only and empty tokens. +# +# Top-level grammar: +# Repeatedly scan for "typedef", "template", "class", or "struct". +# - typedef: skip to ";" (not interesting for .b generation) +# - template: parse template parameter list, then the class that follows +# - class/struct: parse class definition +# +# Returns a list of "class" objects (blessed hashrefs). Forward +# declarations and explicit specializations return undef and are +# filtered out. +# -------------------------------------------------------------------------- sub parse { my ($file) = @_; local $/ = undef; open(IN,"<$file") || croak "$file: $!"; my $x = ; close(IN); - $x =~ s/^#.*$//mg; - $x =~ s/\/\/.*$//mg; + + # Preprocessing + $x =~ s/^#.*$//mg; # strip preprocessor lines + $x =~ s/\/\/.*$//mg; # strip line comments + + # Tokenize @TOKENS = - grep { length($_); } - map { /\W/ ? split('',$_) : $_; } - map { s/\s//g; $_; } - split(/\b/, $x ); + grep { length($_); } # drop empty tokens + map { /\W/ ? split('',$_) : $_; } # split non-word tokens into chars + map { s/\s//g; $_; } # strip whitespace within tokens + split(/\b/, $x ); # split on word boundaries + # Top-level grammar loop my @c; my $x; while($x = consume_until("(typedef|template|class|struct)")) @@ -201,20 +359,71 @@ sub parse { push @c, parse_class(); } } + + # Filter out undefs (forward declarations, skipped specializations) return grep { defined $_; } @c; } +# -------------------------------------------------------------------------- +# parse_template() -> class | undef +# +# Called after consuming the "template" keyword. Parses the template +# parameter list <...>, then the class/struct definition that follows. +# +# Special case: explicit specializations (template <>) have an empty +# parameter list. These are never persistence classes so we skip the +# entire declaration and return undef. +# -------------------------------------------------------------------------- sub parse_template { my @tmp = template_args(); + if(!@tmp) + { + # Explicit specialization (template <>): not a new class, + # skip the entire declaration. + my $x = consume_until('(;|\{)'); + if($x eq '{') + { + unshift @TOKENS, $x; + consume_block('{','}'); + next_is(";"); + } + return; + } return parse_class(@tmp) if(next_is("(class|struct)")); } + +# -------------------------------------------------------------------------- +# template_args() -> @parameter_names +# +# Parses the angle-bracketed template parameter list. +# +# Handles: +# template <> -> empty list (explicit specialization) +# template -> ("T") +# template -> ("T", "U") +# template -> ("T") (default is discarded) +# template -> ("B") +# template -> ("N") +# +# Does NOT handle "typename" -- only "class", "bool", and "int" are +# accepted as parameter keywords. +# -------------------------------------------------------------------------- sub template_args { my @tmp; expect_next("<"); + + # Empty parameter list: template <> + if(next_is(">")) + { + return @tmp; + } + for(;;) { expect_next("(class|bool|int)"); push @tmp, next_ident(); + + # Skip default argument: = SomeType<...> if(next_is("=")) { my $x = consume_until('(,|\>|\<)'); @@ -231,14 +440,50 @@ sub template_args { expect_next(">"); return @tmp; } + +# -------------------------------------------------------------------------- +# parse_class(@template_params) -> class | undef +# +# Parses a class or struct definition. @template_params is empty for +# non-template classes. +# +# Grammar (simplified): +# ClassName ; -> forward declaration, return undef +# ClassName : [virtual] [public] Base -> inheritance +# ClassName { ... } ; -> full definition +# +# Inside the class body, recognises: +# - Access specifiers (public/private/protected:) +# - Friend declarations (skipped) +# - Nested typedefs, using, typename, enum (skipped to ;) +# - Nested class/struct (recursive parse_class) +# - Template member functions +# - Member variables (type + name, terminated by ; , or =) +# - Member functions (type + name + (...) + optional body) +# - C-style block comments (/* ... */) +# - Operator overloads (operator+, operator(), operator new[], etc.) +# - Pure virtual (= 0) +# - Constructor initialiser lists (: member(init), ...) +# +# Returns a blessed "class" hashref with fields: +# name -> string +# super -> [base class names] +# template -> [template parameter names] +# members -> [{name, type, static?}] +# methods -> [{name, type, args, const?, static?, virtual?, ...}] +# classes -> [nested class objects] +# -------------------------------------------------------------------------- sub parse_class { my (@tmp) = @_; my $self = {}; my $name = next_ident(); $self->{name} = $name; $self->{template} = \@tmp if(@tmp); - # Foreward declaration + + # Forward declaration: "class Foo;" return if(next_is(";")); + + # Inheritance list: "class Foo : public Base1, virtual Base2" if(next_is(":")) { for(;;) @@ -248,10 +493,12 @@ sub parse_class { last unless(next_is(",")); } } + + # Class body expect_next('{'); while(!peek_next('}')) { - # print "... : $TOKENS[0], $TOKENS[1], ... \n"; + # --- Block comments inside class body (/* ... */) --- if(next_is('\/')) { if(next_is('\*')) @@ -269,12 +516,14 @@ sub parse_class { } + # --- Access specifiers --- if(next_is("(public|private|protected)")) { expect_next(":"); next; } + # --- Friend declarations (skip entirely) --- if(next_is("friend")) { my $x = consume_until("(;|{)"); @@ -286,38 +535,47 @@ sub parse_class { next; } - # next_is("explicit"); + # --- typedef, using, typename, enum (skip to ;) --- if(next_is("(typedef|using|typename|enum)")) { consume_until(";"); next; } + + # --- Nested class/struct --- if(next_is("(class|struct)")) { push @{$self->{classes}}, parse_class(); next; } + + # --- Begin parsing a member variable or method declaration --- my %m; + + # Template member functions: template void foo(...) while(next_is("template")) { push @{ $m{template} } , template_args(); } my @x; -# push @x,"~" while(next_is('\~')); $m{explicit} = 1 if(next_is("explicit")); $m{static} = 1 if(next_is("static")); $m{virtual} = 1 if(next_is("virtual")); + + # Accumulate type + name tokens. The last identifier before + # a delimiter ( ; , = ( ) is the member/method name; everything + # before it is the type. my $x; while($x = next_is_ident()) { - # print "--- : $x\n"; push @x, $x; push @x,'*' while(next_is('\*')); push @x,'&' while(next_is('\&')); $m{name} = $x; - # int a,b,*c; does not work + + # --- Member variable: terminated by , ; or = --- my $s; if($s = next_is('(,|;|=)')) { @@ -334,13 +592,20 @@ sub parse_class { consume_until(";") if($s eq '='); last; } + + # --- Method: name followed by (...) --- if(peek_next('\(')) { pop @x; $m{type} = make_type(@x); + + # Parse argument list my @args = consume_block('(',')'); - shift @args; - pop @args; + shift @args; # remove opening '(' + pop @args; # remove closing ')' + + # Split arguments on top-level commas + # (commas inside <> or () are nested, not separators) my @a; my $n = 0; my @z; @@ -370,24 +635,25 @@ sub parse_class { { push @{$self->{methods}}, \%m; } - # print "f: $x\n"; + # Method body or declaration if(next_is(':')) { - # print "{: $TOKENS[0]\n"; + # Constructor initialiser list: skip to { and consume body consume_until('\{'); unshift @TOKENS, '{'; consume_block('{','}'); - # print "}: $TOKENS[0]\n"; } else { if(peek_next('\{')) { + # Inline method body consume_block('{','}'); } else { + # Pure virtual (= 0) or plain declaration (;) if(next_is("=")) { expect_next("0"); @@ -405,6 +671,17 @@ sub parse_class { expect_next(";"); return bless($self,"class"); } + +# ========================================================================== +# Token stream primitives +# ========================================================================== + +# -------------------------------------------------------------------------- +# consume_until($regex) -> $token | undef +# +# Discards tokens until one matches $regex, then returns that token. +# Returns undef if the token stream is exhausted. +# -------------------------------------------------------------------------- sub consume_until { my ($r) = @_; while(@TOKENS) @@ -414,6 +691,16 @@ sub consume_until { } return undef; } + +# -------------------------------------------------------------------------- +# consume_block($open, $close) -> @tokens +# +# Consumes a balanced block of tokens delimited by $open/$close +# (e.g. '{'/'}', '('/')', '<'/'>'). Returns all consumed tokens +# including the delimiters. Handles nesting. +# +# Precondition: $TOKENS[0] must equal $open. +# -------------------------------------------------------------------------- sub consume_block { my ($bra,$ket) = @_; my $n = 0; @@ -428,6 +715,13 @@ sub consume_block { return @x if($n == 0); } } + +# -------------------------------------------------------------------------- +# ignore_while($regex) +# +# Consumes and discards consecutive tokens matching $regex. +# Stops at the first non-matching token (which is left in the stream). +# -------------------------------------------------------------------------- sub ignore_while { my ($r) = @_; while(@TOKENS) @@ -436,17 +730,38 @@ sub ignore_while { shift @TOKENS; } } + +# -------------------------------------------------------------------------- +# expect_next($regex) -> $token +# +# Consumes the next token and asserts it matches $regex. +# Croaks (dies with stack trace) on mismatch. +# -------------------------------------------------------------------------- sub expect_next { my ($r) = @_; my $ident = shift @TOKENS; croak "$ident is not $r" unless($ident =~ /^$r$/); return $ident; } + +# -------------------------------------------------------------------------- +# next_ident() -> $identifier +# +# Consumes and returns the next identifier (via next_is_ident). +# Croaks if the next token is not an identifier. +# -------------------------------------------------------------------------- sub next_ident { my $x = next_is_ident(); croak "not an ident " unless($x); return $x; } + +# -------------------------------------------------------------------------- +# next_is($regex) -> $token | undef +# +# If the next token matches $regex, consumes and returns it. +# Otherwise returns undef and leaves the stream unchanged. +# -------------------------------------------------------------------------- sub next_is { my ($r) = @_; if($TOKENS[0] =~ /^$r$/) @@ -455,11 +770,30 @@ sub next_is { } return undef; } + +# -------------------------------------------------------------------------- +# next_is_ident() -> $identifier | undef +# +# Tries to consume a C++ identifier from the token stream. +# Handles several complex cases: +# +# - Operator overloads: "operator+", "operator()", "operator new", +# "operator delete[]", "operator<<", etc. +# - Destructors: "~ClassName" +# - Template identifiers: "Foo" (angle brackets consumed +# as a block and appended to the name) +# - Qualified names: "Namespace::Name" (one level of :: only) +# +# Returns the full identifier string, or undef if the next token is +# not the start of an identifier. +# -------------------------------------------------------------------------- sub next_is_ident { + # Check for "operator" keyword first my $op = next_is("operator"); if($op) { my $x; + # operator new / operator delete / operator new[] / operator delete[] if($x = next_is("(new|delete)")) { $op .= " $x"; @@ -471,11 +805,14 @@ sub next_is_ident { return $op; } + # operator() if(next_is('\(')) { expect_next('\)'); $op .= "()"; } + + # operator+, operator<<, operator==, operator!=, etc. my $z; while($z = next_is('[\-+/\*\[\]<>=!]')) { @@ -483,17 +820,24 @@ sub next_is_ident { } return $op; } + + # Optional ~ for destructors my $y = next_is('\~'); + + # The identifier itself (any \w+ token) my $x = next_is('\w+'); if($x) { $x = (defined $y ? "$y$x" : $x); + + # Template arguments: Foo if(peek_next("<")) { my @x = consume_block("<",">"); $x .= join("",@x); } + # Scope resolution: Namespace::Name (one level) if(next_is(":")) { if(next_is(":")) @@ -503,12 +847,19 @@ sub next_is_ident { } else { + # Single colon was not ::, put it back unshift @TOKENS, ":"; } } } return $x; } + +# -------------------------------------------------------------------------- +# peek_next($regex) -> bool +# +# Returns true if the next token matches $regex, without consuming it. +# -------------------------------------------------------------------------- sub peek_next { my ($r) = @_; if($TOKENS[0] =~ /^$r$/) @@ -517,6 +868,15 @@ sub peek_next { } return 0; } + +# -------------------------------------------------------------------------- +# make_type(@tokens) -> $type_string +# +# Joins a list of type tokens into a human-readable type string. +# Inserts spaces between consecutive word tokens and between +# pointer/reference sigils and following words. Normalises ">>" +# to "> >" for C++03 compatibility. +# -------------------------------------------------------------------------- sub make_type { my (@a) = @_; my $p; @@ -531,39 +891,68 @@ sub make_type { $s =~ s/>>/> >/g; return $s; } + +# ========================================================================== +# package class -- data object representing a parsed C++ class +# ========================================================================== +# +# Fields (hashref keys): +# name -> string: class name +# super -> [string]: base class names +# template -> [string]: template parameter names +# members -> [{name, type}]: non-static member variables +# class_members -> [{name, type}]: static member variables +# methods -> [{name, type, args, const?, override?}]: non-static methods +# class_methods -> [{name, type, args}]: static methods +# classes -> [class]: nested class definitions +# +# ========================================================================== + package class; + +# name() -> $string sub name { my ($self) = @_; return $self->{name}; } + +# super() -> @strings (base class names) sub super { my ($self) = @_; return $self->{super} ? @{$self->{super}} : (); } +# members() -> @strings (member variable names) sub members { my ($self) = @_; my @x = $self->{members} ? @{$self->{members}} : (); return map { $_->{name} } @x; } +# methods() -> @strings (method names) sub methods { my ($self) = @_; my @x = $self->{methods} ? @{$self->{methods}} : (); return map { $_->{name} } @x; } +# has_method($name) -> bool +# Returns true if the class has a method with the given name. +# Used to check whether describe() is hand-written. sub has_method { my ($self,$name) = @_; return grep { $_ eq $name } $self->methods; } +# members_types() -> @[ [$name, $type] ] +# Returns member variables with both name and type. sub members_types { my ($self) = @_; my @x = $self->{members} ? @{$self->{members}} : (); return map { [ $_->{name}, $_->{type} ] } @x; } +# template() -> @strings (template parameter names) sub template { my ($self) = @_; return $self->{template} ? @{$self->{template}} : (); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2da2ac05..6e16f5af 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -23,3 +23,4 @@ add_subdirectory( test_requires_simple ) add_subdirectory( test_requires_complex ) add_subdirectory( test_custom_build_type ) add_subdirectory( ecbuild_install_dependency_with_soversion ) +add_subdirectory( sg_pl ) diff --git a/tests/sg_pl/CMakeLists.txt b/tests/sg_pl/CMakeLists.txt new file mode 100644 index 00000000..3a3be502 --- /dev/null +++ b/tests/sg_pl/CMakeLists.txt @@ -0,0 +1,10 @@ +ecbuild_find_perl( REQUIRED ) + +set(_dir ${CMAKE_CURRENT_BINARY_DIR}) +configure_file(run-test.sh.in ${_dir}/run-test.sh @ONLY) + +ecbuild_add_test( + TARGET test_sg_pl + TYPE SCRIPT + COMMAND run-test.sh +) diff --git a/tests/sg_pl/run-test.sh.in b/tests/sg_pl/run-test.sh.in new file mode 100644 index 00000000..62b5e064 --- /dev/null +++ b/tests/sg_pl/run-test.sh.in @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +set -ex + +# ------------------------------------------------------------------ +# Test: sg.pl handles explicit template specializations (template <>) +# +# Verifies: +# 1. sg.pl exits 0 (no crash on template <>) +# 2. RegularClass.b exists and contains correct specName +# 3. TemplateClass.b exists and contains correct template specName +# 4. No .b file generated for the explicit specialization +# 5. AfterSpecClass.b exists (parser recovery after specialization) +# 6. Exactly 3 .b files total +# ------------------------------------------------------------------ + +PERL="@PERL_EXECUTABLE@" +SG_PL="@ECBUILD_MACROS_DIR@/sg.pl" +HEADER="@CMAKE_CURRENT_SOURCE_DIR@/test_sg_pl.h" +OUTDIR="@CMAKE_CURRENT_BINARY_DIR@/sg_output" + +# Clean and create output directory +rm -rf "${OUTDIR}" +mkdir -p "${OUTDIR}" + +# Run sg.pl -- primary assertion: exit code 0 +"${PERL}" "${SG_PL}" "${HEADER}" "${OUTDIR}" eckit + +# ------------------------------------------------------------------ +# Assert expected .b files exist +# ------------------------------------------------------------------ + +echo "Checking RegularClass.b exists..." +test -f "${OUTDIR}/RegularClass.b" + +echo "Checking TemplateClass.b exists..." +test -f "${OUTDIR}/TemplateClass.b" + +echo "Checking AfterSpecClass.b exists..." +test -f "${OUTDIR}/AfterSpecClass.b" + +# ------------------------------------------------------------------ +# Assert NO .b file for the explicit specialization +# ------------------------------------------------------------------ + +echo "Checking no .b file for explicit specialization..." +count=$(ls -1 "${OUTDIR}" | grep -c 'TemplateClass<' || true) +if [ "${count}" -ne 0 ]; then + echo "FAIL: found .b file for template specialization" + ls -la "${OUTDIR}" + exit 1 +fi + +# Exactly 3 .b files expected +file_count=$(ls -1 "${OUTDIR}"/*.b | wc -l | tr -d ' ') +if [ "${file_count}" -ne 3 ]; then + echo "FAIL: expected 3 .b files, found ${file_count}" + ls -la "${OUTDIR}" + exit 1 +fi + +# ------------------------------------------------------------------ +# Assert .b file content +# ------------------------------------------------------------------ + +echo "Checking RegularClass.b content..." +grep -q 'static const char\* specName' "${OUTDIR}/RegularClass.b" +grep -q 'return "RegularClass"' "${OUTDIR}/RegularClass.b" + +echo "Checking TemplateClass.b content..." +grep -q 'static std::string specName' "${OUTDIR}/TemplateClass.b" +grep -q 'Traits::name()' "${OUTDIR}/TemplateClass.b" + +echo "Checking AfterSpecClass.b content..." +grep -q 'static const char\* specName' "${OUTDIR}/AfterSpecClass.b" +grep -q 'return "AfterSpecClass"' "${OUTDIR}/AfterSpecClass.b" + +echo "All checks passed." diff --git a/tests/sg_pl/test_sg_pl.h b/tests/sg_pl/test_sg_pl.h new file mode 100644 index 00000000..19708d3f --- /dev/null +++ b/tests/sg_pl/test_sg_pl.h @@ -0,0 +1,34 @@ +#ifndef TEST_SG_PL_H +#define TEST_SG_PL_H + +// Case 1: Regular (non-template) class with base and member +class RegularClass : public BaseClass { +public: + int value; +}; + +// Case 2: Template class with base and member +template +class TemplateClass : public BaseClass { +public: + T data; +}; + +// Case 3: Explicit template specialization -- must not crash sg.pl +template <> +class TemplateClass : public BaseClass { +public: + int data; +}; + +// Case 4: Explicit specialization with no body (semicolon-only form) +template <> +class TemplateClass; + +// Case 5: Class after the specializations -- tests parser recovery +class AfterSpecClass : public BaseClass { +public: + double result; +}; + +#endif