This page documents Hapy Actions API. Actions augment the grammar with semantic checks and on-the-fly interpretation code.
Action is a piece of code that can be attached to a grammar rule. The Hapy parser executes the attached Action right after the rule matches. Note that a single rule may match many times during the parsing process and a local match may be rejected by further rule matching and backtracking. The action is executed every time the corresponding rule matches.
An action is passed the parsing subtree formed by the match, the result status code, and other housekeeping information. An action may:
Use the parsing tree in read-only mode. The action may update some external (to parser and parsing tree) objects, depending on what was matched.
Reject the match by changing the result code to Result::scMiss. This will force the parser to look for alternative matches. If no alternative matches can be found, then the rule will fail to match, triggering the usual backtracking mechanisms. For "committed" rules, the action is executed before the result is committed so rule can backtrack if the attached action rejects the match.
Generate a fatal parsing error by changing the result code to Result::scError. The error propagates upwards the parsing stack, causing the parser to fail. Generating errors is useful when the action detects a non-recoverable semantics error that makes further parsing useless.
Modify the parsing tree and other internal parsing state. While technically possible, doing so requires good knowledge of internal Hapy mechanisms and is usually not the right thing to do.
Under the Hapy hood, an Action is a wrapper around ActionBase, a simple C++ class with a function operator that takes a pointer to Action::Params as a parameter (see Action.h). The operator calls a pure virtual act() method. Defining your own actions by overriding the act() method is efficient but awkward. The sections below describe two alternative ways to define actions. Examples source code is also available in Hapy distribution as docs/actions.cc.
This section explains how to attach an existing action to a grammar rule. For information on how to create an action, see sections below.
To enable an action, you must attach it to one of the parsing rules. There are two ways to do that. First, an action can be attached using a subscript operator[] of the Rule class:
Rule instruction, name, expression, ...; instruction = name[&grokName] >> expression[&grokExpression] >> ';'; name = ... expression = ...
This method keeps semantic actions and syntax rules together, for a compact grammar declaration. The subscript operator[] method does not modify its rule. Thus, the first method can be applied to predefined constant rules such as empty_r and to temporary rules:
call = empty_r[&prepCall] >> (name >> parameters)[&grokCall];
The alternative is to use an action method of the Rule class:
Rule instruction, name, expression, ...; instruction = name >> expression >> ';'; name = ... expression = ... name.action(&grokName); expression.action(&grokExpression);
This second method allows to isolate semantic actions and syntax rules, for a clean, readable grammar. Naturally, the action method modifies its rule. Thus, the second method makes it easy to attach the same action to all occurrences of the rule in the grammar:
instruction = name >> expression >> ';'; call = name >> parameters; ... name.action(&grokName); // will be called for each name above
The best method to use depends on your grammar, actions, and taste. Both methods can be used inside the same grammar.
Simple actions can be defined as stand-alone C++ functions. An action function takes a pointer to Action::Params as a parameter and returns nothing. The following is an example of an action that turns on debugging every time a particular rule matches the input.
#include <Hapy/Actions.h> /* required header if you do actions */ ... using namespace Hapy; static void TurnDebuggingOn(Action::Params *) { Rule::Debug(true); } ... Rule particular; particular = ...; particular.action(&TurnDebuggingOn);
To pass additional parameters to your action or change its profile, use standard STL (e.g., bind2nd) or similar custom wrappers. For example, lets add a simple "reason" parameter to the above TurnDebuggingOn() function and call the resulting function ExplainDebuggingOn()
#include <Hapy/Actions.h> #include <functional> /* to get access to bind() */ ... static void ExplainDebuggingOn(Action::Params *, const char *reason) { clog << "turning debugging on because " << reason << endl; Rule::Debug(true); } ... Rule r1, r2; ... r1.action(bind2nd(ptr_fun(&ExplainDebuggingOn), "rule r1 matched!")); r2.action(bind2nd(ptr_fun(&ExplainDebuggingOn), "coredump is near"));
Here is a more realistic example. The following piece of code adds a Pree tree (corresponding to the matched portion of the input) to an STL list. The pointer to the list is supplied as an additional action parameter:
#include <Hapy/Actions.h> #include <list> ... typedef std::list<Pree> Matches; static void AddMatch(Action::Params *p, Matches *matches) { matches->push_back(p->pree); } ... Matches matches; Rule particular; particular = ...; particular.action(bind2nd(ptr_fun(&AddMatch), &matches));
Non-trivial actions are usually implemented as class methods. A class method can be converted to an Action using the mem_action() function. The function is declared in Hapy/MemAction.h header file (which is included by Hapy/Actions.h).
The following is an example of a class implementing two actions common to configuration file interpretation. The Interpreter::handleAssignment() method records a value of a user-defined variable. The Interpreter::handleOption() method processes the configuration option. Each method is associated with the corresponding grammar rule via the Rule::action() call.
#include <Hapy/FunAction.h> using namespace Hapy; class Interpreter { public: void handleAssignment(Action::Params *params); void handleOption(Action::Params *params); ... }; ... Interpreter interpreter; Rule grammar, statement, assignment, option, ...; ... grammar = *(statement >> ";"); statement = assignment | option; assignment = name >> "=" >> value; option = name >> *value; ... assignment.action(mem_action(&interpreter, &Interpreter::handleAssignment)); option.action(mem_action(&interpreter, &Interpreter::handleOption));
The mem_action() function creates an Action object that keeps the pointer to the Interpreter object and the pointer to one of the Interpreter's methods. The action method is called every time the corresponding rule matches.
Hapy actions are always executed after the rule matches. Sometimes, it may be useful to execute an action before the rule is tried. This is possible by attaching the action to a zero-width matching rule:
Rule realCore; // the actual rule we are interested in realCore = ...; Rule core; // use "core" instead of the "realCore" rule core = empty_r[&PreemptiveAction] >> realCore;
When the core rule is used instead of the realCore rule, the PreemptiveAction() action is executed before the realCore rule is matched.
If the above tricks end up being used a lot, we can add a Rule::pre_action() method.