Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for side-effect actions (pull request) #56

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
12 changes: 0 additions & 12 deletions docs/parser.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,6 @@ used instead and this parameter is ignored.
A boolean whose default value is `False`. If set to `True` parser will call
actions that will build the [parse tree](./parse_trees.md).

## call_actions_during_tree_build

By default, this parameter is set to `False`. If set to `True`, parser will call
actions during the parse tree [parse tree](./parse_trees.md) building process.
The return value of each action will be discarded, since they directly affect
the parse tree building process.

!!! note

Use this parameter with a special care when GLR is used, since actions will
be called even on trees that can't be completed (unsuccessful parses).

## prefer_shifts

By default set to `True` for LR parser and to `False` for GLR parser. In case
Expand Down
17 changes: 8 additions & 9 deletions parglare/glr.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ class GLRParser(Parser):
def __init__(self, grammar, start_production=1, actions=None,
layout_actions=None, debug=False, debug_trace=False,
debug_colors=False, debug_layout=False, ws='\n\r\t ',
build_tree=False, call_actions_during_tree_build=False,
tables=LALR, layout=False, position=False, prefer_shifts=None,
prefer_shifts_over_empty=None, error_recovery=False,
dynamic_filter=None, custom_lexical_disambiguation=None):
build_tree=False, tables=LALR, layout=False, position=False,
prefer_shifts=None, prefer_shifts_over_empty=None,
error_recovery=False, dynamic_filter=None,
custom_lexical_disambiguation=None, side_actions=None):

# The default for GLR is not to use any strategy preferring shifts
# over reduce thus investigating all possibilitites.
Expand All @@ -51,13 +51,12 @@ def __init__(self, grammar, start_production=1, actions=None,
actions=actions, layout_actions=layout_actions,
debug=debug, debug_trace=debug_trace,
debug_colors=debug_colors, debug_layout=debug_layout, ws=ws,
build_tree=build_tree,
call_actions_during_tree_build=call_actions_during_tree_build,
tables=tables, layout=layout, position=position,
prefer_shifts=prefer_shifts,
build_tree=build_tree, tables=tables, layout=layout,
position=position, prefer_shifts=prefer_shifts,
prefer_shifts_over_empty=prefer_shifts_over_empty,
error_recovery=error_recovery, dynamic_filter=dynamic_filter,
custom_lexical_disambiguation=custom_lexical_disambiguation)
custom_lexical_disambiguation=custom_lexical_disambiguation,
side_actions=side_actions)

def _check_parser(self):
"""
Expand Down
100 changes: 100 additions & 0 deletions parglare/grammar.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ def __init__(self, productions, terminals=None, imports=None,
self.imported_with = imported_with
self.recognizers = recognizers
self.actions = {}
self.side_actions = {}

self.collect_and_unify_symbols()

Expand All @@ -476,6 +477,7 @@ def __init__(self, productions, terminals=None, imports=None,

self.resolve_references()
self.load_actions()
self.load_side_actions()
self.load_recognizers()

def collect_and_unify_symbols(self):
Expand Down Expand Up @@ -580,6 +582,31 @@ def load_actions(self):
'decorator defined.'.format(actions_file))
self.actions = actions_module.action.all

def load_side_actions(self):
"""
Loads side-effect actions from <grammar_name>_side_actions.py if the
file exists. Actions must be collected with action decorator and the
decorator must be called `side_action`.
"""
side_actions_file = None
if self.file_path:
side_actions_file = path.join(
path.dirname(self.file_path),
"{}_side_actions.py".format(path.splitext(
path.basename(self.file_path))[0]))
if path.exists(side_actions_file):
mod_name = "{}side_actions".format(
self.imported_with.fqn
if self.imported_with is not None else "")
side_actions_module = load_python_module(
mod_name, side_actions_file)
if not hasattr(side_actions_module, 'side_action'):
raise GrammarError(
Location(file_name=side_actions_file),
message='Side actions file "{}" must have "side_action"'
' decorator defined.'.format(side_actions_file))
self.side_actions = side_actions_module.side_action.all

def load_recognizers(self):
"""Load recognizers from <grammar_name>_recognizers.py. Override
with provided recognizers.
Expand Down Expand Up @@ -688,6 +715,15 @@ def resolve_action_by_name(self, action_name):
imported_pg_file = self.imports[import_module_name]
return imported_pg_file.resolve_action_by_name(name)

def resolve_side_action_by_name(self, side_action_name):
if side_action_name in self.side_actions:
return self.side_actions[side_action_name]
elif '.' in side_action_name:
import_module_name, name = side_action_name.split('.', 1)
if import_module_name in self.imports:
imported_pg_file = self.imports[import_module_name]
return imported_pg_file.resolve_side_action_by_name(name)

def make_multiplicity_symbol(self, symbol_ref, base_symbol, separator,
imported_with):
"""
Expand Down Expand Up @@ -848,6 +884,7 @@ def _init_grammar(self):
self._enumerate_productions()
self._fix_keyword_terminals()
self._resolve_actions()
self._resolve_side_actions()

# Connect recognizers, override grammar provided
if not self._no_check_recognizers:
Expand Down Expand Up @@ -1000,6 +1037,64 @@ def _resolve_actions(self, action_overrides=None,
else:
symbol.action = symbol.grammar_action

def _resolve_side_actions(self, side_action_overrides=None):
for symbol in self:

# Resolve trying from most specific to least specific
action = None

# 1. Resolve by fully qualified symbol name
if '.' in symbol.fqn:
if side_action_overrides:
action = side_action_overrides.get(symbol.fqn, None)

if action is None:
action = self.resolve_side_action_by_name(symbol.fqn)

# 2. Fully qualified action name
if action is None and symbol.action_fqn is not None \
and '.' in symbol.action_fqn:
if side_action_overrides:
action = side_action_overrides.get(symbol.action_fqn, None)

if action is None:
action = self.resolve_side_action_by_name(symbol.action_fqn)

# 3. Symbol name
if action is None:
if side_action_overrides:
action = side_action_overrides.get(symbol.name, None)

if action is None:
action = self.resolve_side_action_by_name(symbol.name)

# 4. Action name
if action is None and symbol.action_name is not None:
if side_action_overrides:
action = side_action_overrides.get(symbol.action_name, None)

if action is None:
action = self.resolve_side_action_by_name(
symbol.action_name)

if action is not None:
symbol.side_action = action

# Some sanity checks for actions
if type(symbol.side_action) is list:
if type(symbol) is Terminal:
raise ParserInitError(
'Cannot use a list of actions for '
'terminal "{}".'.format(symbol.name))
else:
if len(symbol.side_action) != len(symbol.productions):
raise ParserInitError(
'Lenght of list of actions must match the '
'number of productions for non-terminal '
'"{}".'.format(symbol.name))
else:
symbol.side_action = None

def _connect_override_recognizers(self):
for term in self.terminals:
if self.recognizers and term.fqn in self.recognizers:
Expand Down Expand Up @@ -1173,6 +1268,11 @@ def resolve_action_by_name(self, action_name):

return self.pgfile.resolve_action_by_name(action_name)

def resolve_side_action_by_name(self, side_action_name):
"Resolves side action from the imported file."

return self.pgfile.resolve_side_action_by_name(side_action_name)


def create_productions_terminals(productions):
"""Creates Production instances from the list of productions given in
Expand Down
86 changes: 45 additions & 41 deletions parglare/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,20 @@ class Parser(object):
def __init__(self, grammar, start_production=1, actions=None,
layout_actions=None, debug=False, debug_trace=False,
debug_colors=False, debug_layout=False, ws='\n\r\t ',
build_tree=False, call_actions_during_tree_build=False,
tables=LALR, layout=False, position=False, prefer_shifts=True,
prefer_shifts_over_empty=True, error_recovery=False,
dynamic_filter=None, custom_lexical_disambiguation=None):
build_tree=False, tables=LALR, layout=False, position=False,
prefer_shifts=True, prefer_shifts_over_empty=True,
error_recovery=False, dynamic_filter=None,
custom_lexical_disambiguation=None, side_actions=None):
self.grammar = grammar
self.start_production = start_production
EMPTY.action = pass_none
EOF.action = pass_none
if actions:
self.grammar._resolve_actions(action_overrides=actions,
fail_on_no_resolve=True)
if side_actions:
self.grammar._resolve_side_actions(
side_action_overrides=side_actions)

self.layout_parser = None
if not layout:
Expand All @@ -62,7 +65,6 @@ def __init__(self, grammar, start_production=1, actions=None,
self.debug_layout = debug_layout

self.build_tree = build_tree
self.call_actions_during_tree_build = call_actions_during_tree_build

self.error_recovery = error_recovery
self.dynamic_filter = dynamic_filter
Expand Down Expand Up @@ -562,21 +564,15 @@ def _call_shift_action(self, symbol, matched_str, context):
Calls registered shift action for the given grammar symbol.
"""
debug = self.debug
sem_action = symbol.action

if hasattr(symbol, "side_action") and symbol.side_action:
symbol.side_action(context, matched_str)

if self.build_tree:
# call action for building tree node if tree building is enabled
if debug:
h_print("Building terminal node",
"'{}'.".format(symbol.name), level=2)

# If both build_tree and call_actions_during_build are set to
# True, semantic actions will be call but their result will be
# discarded. For more info check following issue:
# https://github.com/igordejanovic/parglare/issues/44
if self.call_actions_during_tree_build and sem_action:
sem_action(context, matched_str)

return treebuild_shift_action(context, matched_str)

sem_action = symbol.action
Expand All @@ -598,26 +594,35 @@ def _call_shift_action(self, symbol, matched_str, context):

return result

def _do_sem_action(self, sem_action, assgn_results, production, subresults,
context):
if type(sem_action) is list:
if assgn_results:
result = sem_action[production.prod_symbol_id](
context, subresults, **assgn_results)
else:
result = sem_action[production.prod_symbol_id](context,
subresults)
else:
if assgn_results:
result = sem_action(context, subresults, **assgn_results)
else:
result = sem_action(context, subresults)
return result

def _call_reduce_action(self, production, subresults, context):
"""
Calls registered reduce action for the given grammar symbol.
"""
debug = self.debug
result = None
bt_result = None

if self.build_tree:
# call action for building tree node if enabled.
if debug:
h_print("Building non-terminal node",
"'{}'.".format(production.symbol.name), level=2)

bt_result = treebuild_reduce_action(context, nodes=subresults)
if not self.call_actions_during_tree_build:
return bt_result
assgn_results = None

sem_action = production.symbol.action
if sem_action:
side_action = production.symbol.side_action if hasattr(
production.symbol, "side_action") else None

if (sem_action and not self.build_tree) or side_action:
assignments = production.assignments
if assignments:
assgn_results = {}
Expand All @@ -627,19 +632,20 @@ def _call_reduce_action(self, production, subresults, context):
else:
assgn_results[a.name] = bool(subresults[a.index])

if type(sem_action) is list:
if assignments:
result = sem_action[production.prod_symbol_id](
context, subresults, **assgn_results)
else:
result = sem_action[production.prod_symbol_id](context,
subresults)
else:
if assignments:
result = sem_action(context, subresults, **assgn_results)
else:
result = sem_action(context, subresults)
if side_action:
self._do_sem_action(side_action, assgn_results, production,
subresults, context)

if self.build_tree:
# call action for building tree node if enabled.
if debug:
h_print("Building non-terminal node",
"'{}'.".format(production.symbol.name), level=2)
return treebuild_reduce_action(context, nodes=subresults)

if sem_action:
result = self._do_sem_action(sem_action, assgn_results, production,
subresults, context)
else:
if debug:
h_print("No action defined",
Expand All @@ -658,9 +664,7 @@ def _call_reduce_action(self, production, subresults, context):
"type:{} value:{}"
.format(type(result), repr(result)), level=1)

# If build_tree is set to True, discard the result of the semantic
# action, and return the result of treebuild_reduce_action.
return bt_result if bt_result is not None else result
return result

def _lexical_disambiguation(self, tokens):
"""
Expand Down
42 changes: 0 additions & 42 deletions tests/func/test_build_tree.py

This file was deleted.