1

Think about a preprocessor which will read the raw text (no significant white space or tokens).

There are 3 rules.

  • resolve_para_entry should solve the Argument inside a call. The top-level text is returned as string.

  • resolve_para should resolve the whole Parameter list and put all the top-level Parameter in a string list.

  • resolve is the entry

On the way I track the iterator and get the text portion

Samples:

  • sometext(para) → expect para in the string list

  • sometext(para1,para2) → expect para1 and para2 in string list

  • sometext(call(a)) → expect call(a) in the string list

  • sometext(call(a,b)) ← here it fails; it seams that the "!lit(',')" wont take the Parser to step outside ..

Rules:

resolve_para_entry = +(  
     (iter_pos >> lit('(') >> (resolve_para_entry | eps) >> lit(')') >> iter_pos) [_val=  phoenix::bind(&appendString, _val, _1,_3)]
     | (!lit(',') >> !lit(')') >> !lit('(') >> (wide::char_ | wide::space))         [_val = phoenix::bind(&appendChar, _val, _1)]
    );

resolve_para = (lit('(') >> lit(')'))[_val = std::vector<std::wstring>()]  // empty para -> old style
    | (lit('(') >> resolve_para_entry >> *(lit(',') >> resolve_para_entry) > lit(')'))[_val = phoenix::bind(&appendStringList, _val, _1, _2)]
    | eps;
  ;

resolve = (iter_pos >> name_valid >> iter_pos >> resolve_para >> iter_pos);

In the end doesn't seem very elegant. Maybe there is a better way to parse such stuff without skipper

sehe
  • 374,641
  • 47
  • 450
  • 633
Markus
  • 373
  • 1
  • 11

1 Answers1

1

Indeed this should be a lot simpler.

First off, I fail to see why the absense of a skipper is at all relevant.

Second, exposing the raw input is best done using qi::raw[] instead of dancing with iter_pos and clumsy semantic actions¹.

Among the other observations I see:

  • negating a charset is done with ~, so e.g. ~char_(",()")
  • (p|eps) would be better spelled -p
  • (lit('(') >> lit(')')) could be just "()" (after all, there's no skipper, right)
  • p >> *(',' >> p) is equivalent to p % ','
  • With the above, resolve_para simplifies to this:

    resolve_para = '(' >> -(resolve_para_entry % ',') >> ')';
    
  • resolve_para_entry seems weird, to me. It appears that any nested parentheses are simply swallowed. Why not actually parse a recursive grammar so you detect syntax errors?


Here's my take on it:

Define An AST

I prefer to make this the first step because it helps me think about the parser productions:

namespace Ast {

    using ArgList = std::list<std::string>;

    struct Resolve {
        std::string name;
        ArgList arglist;
    };

    using Resolves = std::vector<Resolve>;
}

Creating The Grammar Rules

qi::rule<It, Ast::Resolves()> start;
qi::rule<It, Ast::Resolve()>  resolve;
qi::rule<It, Ast::ArgList()>  arglist;
qi::rule<It, std::string()>   arg, identifier;

And their definitions:

identifier = char_("a-zA-Z_") >> *char_("a-zA-Z0-9_");

arg        = raw [ +('(' >> -arg >> ')' | +~char_(",)(")) ];
arglist    = '(' >> -(arg % ',') >> ')';
resolve    = identifier >> arglist;

start      = *qr::seek[hold[resolve]];

Notes:

  • No more semantic actions
  • No more eps
  • No more iter_pos
  • I've opted to make arglist not-optional. If you really wanted that, change it back:

    resolve    = identifier >> -arglist;
    

    But in our sample it will generate a lot of noisy output.

  • Of course your entry point (start) will be different. I just did the simplest thing that could possibly work, using another handy parser directive from the Spirit Repository (like iter_pos that you were already using): seek[]

  • The hold is there for this reason: boost::spirit::qi duplicate parsing on the output - You might not need it in your actual parser.

Live On Coliru

#include <boost/fusion/include/adapt_struct.hpp>
#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/repository/include/qi_seek.hpp>

namespace Ast {

    using ArgList = std::list<std::string>;

    struct Resolve {
        std::string name;
        ArgList arglist;
    };

    using Resolves = std::vector<Resolve>;
}

BOOST_FUSION_ADAPT_STRUCT(Ast::Resolve, name, arglist)

namespace qi = boost::spirit::qi;
namespace qr = boost::spirit::repository::qi;

template <typename It>
struct Parser : qi::grammar<It, Ast::Resolves()>
{
    Parser() : Parser::base_type(start) {
        using namespace qi;

        identifier = char_("a-zA-Z_") >> *char_("a-zA-Z0-9_");

        arg        = raw [ +('(' >> -arg >> ')' | +~char_(",)(")) ];
        arglist    = '(' >> -(arg % ',') >> ')';
        resolve    = identifier >> arglist;

        start      = *qr::seek[hold[resolve]];
    }
  private:
    qi::rule<It, Ast::Resolves()> start;
    qi::rule<It, Ast::Resolve()>  resolve;
    qi::rule<It, Ast::ArgList()>  arglist;
    qi::rule<It, std::string()>   arg, identifier;
};

#include <iostream>

int main() {
    using It = std::string::const_iterator;
    std::string const samples = R"--(
Samples:

sometext(para)        → expect para in the string list
sometext(para1,para2) → expect para1 and para2 in string list
sometext(call(a))     → expect call(a) in the string list
sometext(call(a,b))   ← here it fails; it seams that the "!lit(',')" wont make the parser step outside
)--";
    It f = samples.begin(), l = samples.end();

    Ast::Resolves data;
    if (parse(f, l, Parser<It>{}, data)) {
        std::cout << "Parsed " << data.size() << " resolves\n";

    } else {
        std::cout << "Parsing failed\n";
    }

    for (auto& resolve: data) {
        std::cout << " - " << resolve.name << "\n   (\n";
        for (auto& arg : resolve.arglist) {
            std::cout << "       " << arg << "\n";
        }
        std::cout << "   )\n";
    }
}

Prints

Parsed 6 resolves
 - sometext
   (
       para
   )
 - sometext
   (
       para1
       para2
   )
 - sometext
   (
       call(a)
   )
 - call
   (
       a
   )
 - call
   (
       a
       b
   )
 - lit
   (
       '
       '
   )

More Ideas

That last output shows you a problem with your current grammar: lit(',') should obviously not be seen as a call with two parameters.

I recently did an answer on extracting (nested) function calls with parameters which does things more neatly:

BONUS

Bonus version that uses string_view and also shows exact line/column information of all extracted words.

Note that it still doesn't require any phoenix or semantic actions. Instead it simply defines the necesary trait to assign to boost::string_view from an iterator range.

Live On Coliru

#include <boost/fusion/include/adapt_struct.hpp>
#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/repository/include/qi_seek.hpp>
#include <boost/utility/string_view.hpp>

namespace Ast {

    using Source  = boost::string_view;
    using ArgList = std::list<Source>;

    struct Resolve {
        Source name;
        ArgList arglist;
    };

    using Resolves = std::vector<Resolve>;
}

BOOST_FUSION_ADAPT_STRUCT(Ast::Resolve, name, arglist)

namespace boost { namespace spirit { namespace traits {
    template <typename It>
    struct assign_to_attribute_from_iterators<boost::string_view, It, void> {
        static void call(It f, It l, boost::string_view& attr) { 
            attr = boost::string_view { f.base(), size_t(std::distance(f.base(),l.base())) };
        }
    };
} } }

namespace qi = boost::spirit::qi;
namespace qr = boost::spirit::repository::qi;

template <typename It>
struct Parser : qi::grammar<It, Ast::Resolves()>
{
    Parser() : Parser::base_type(start) {
        using namespace qi;

        identifier = raw [ char_("a-zA-Z_") >> *char_("a-zA-Z0-9_") ];

        arg        = raw [ +('(' >> -arg >> ')' | +~char_(",)(")) ];
        arglist    = '(' >> -(arg % ',') >> ')';
        resolve    = identifier >> arglist;

        start      = *qr::seek[hold[resolve]];
    }
  private:
    qi::rule<It, Ast::Resolves()> start;
    qi::rule<It, Ast::Resolve()>  resolve;
    qi::rule<It, Ast::ArgList()>  arglist;
    qi::rule<It, Ast::Source()>   arg, identifier;
};

#include <iostream>

struct Annotator {
    using Ref = boost::string_view;

    struct Manip {
        Ref fragment, context;

        friend std::ostream& operator<<(std::ostream& os, Manip const& m) {
            return os << "[" << m.fragment << " at line:" << m.line() << " col:" << m.column() << "]";
        }

        size_t line() const {
            return 1 + std::count(context.begin(), fragment.begin(), '\n');
        }
        size_t column() const {
            return 1 + (fragment.begin() - start_of_line().begin());
        }
        Ref start_of_line() const {
            return context.substr(context.substr(0, fragment.begin()-context.begin()).find_last_of('\n') + 1);
        }
    };

    Ref context;
    Manip operator()(Ref what) const { return {what, context}; }
};

int main() {
    using It = std::string::const_iterator;
    std::string const samples = R"--(Samples:

sometext(para)        → expect para in the string list
sometext(para1,para2) → expect para1 and para2 in string list
sometext(call(a))     → expect call(a) in the string list
sometext(call(a,b))   ← here it fails; it seams that the "!lit(',')" wont make the parser step outside
)--";
    It f = samples.begin(), l = samples.end();

    Ast::Resolves data;
    if (parse(f, l, Parser<It>{}, data)) {
        std::cout << "Parsed " << data.size() << " resolves\n";

    } else {
        std::cout << "Parsing failed\n";
    }

    Annotator annotate{samples};

    for (auto& resolve: data) {
        std::cout << " - " << annotate(resolve.name) << "\n   (\n";
        for (auto& arg : resolve.arglist) {
            std::cout << "       " << annotate(arg) << "\n";
        }
        std::cout << "   )\n";
    }
}

Prints

Parsed 6 resolves
 - [sometext at line:3 col:1]
   (
       [para at line:3 col:10]
   )
 - [sometext at line:4 col:1]
   (
       [para1 at line:4 col:10]
       [para2 at line:4 col:16]
   )
 - [sometext at line:5 col:1]
   (
       [call(a) at line:5 col:10]
   )
 - [call at line:5 col:34]
   (
       [a at line:5 col:39]
   )
 - [call at line:6 col:10]
   (
       [a at line:6 col:15]
       [b at line:6 col:17]
   )
 - [lit at line:6 col:62]
   (
       [' at line:6 col:66]
       [' at line:6 col:68]
   )

¹ Boost Spirit: "Semantic actions are evil"?

sehe
  • 374,641
  • 47
  • 450
  • 633
  • Bonus version that uses `string_view` and also shows exact line/column information of all extracted words: [Live On Coliru](http://coliru.stacked-crooked.com/a/a25f4a5ca44bb54f) – sehe Nov 16 '17 at 16:57
  • your comment is correct, this Parser will flat all Arguments. Think about a C/C++ preprocessor. It does not matter what you write inside a defined macro Parameter it is just used as it is and replaced inside the text. Your other Information Need some time for understanding .. i will report the result .. – Markus Nov 17 '17 at 07:17
  • just short Feedback .. the reason why the Code won´t run was the rule resolve_para .. there is a ">" instead ">>" before the ")" so the backtracking does not take place .. – Markus Nov 17 '17 at 11:46
  • Oh yeah. I wasn't really interested in that (but obviously I fixed that). I considered that treating a symptom instead of the disease :) – sehe Nov 17 '17 at 12:04