| #!/usr/bin/python | |
| # Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file | |
| # for details. All rights reserved. Use of this source code is governed by a | |
| # BSD-style license that can be found in the LICENSE file. | |
| import logging | |
| import re | |
| import weakref | |
| _logger = logging.getLogger('pegparser') | |
| # functions can refer to each other, hence creating infinite loops. The | |
| # following hashmap is used to memoize functions that were already compiled. | |
| _compiled_functions_memory = weakref.WeakKeyDictionary() | |
| _regex_type = type(re.compile(r'')) | |
| _list_type = type([]) | |
| _function_type = type(lambda func: 0) | |
| class _PegParserState(object): | |
| """Object for storing parsing state variables and options""" | |
| def __init__(self, text, whitespace_rule, strings_are_tokens): | |
| # Parsing state: | |
| self.text = text | |
| self.is_whitespace_mode = False | |
| # Error message helpers: | |
| self.max_pos = None | |
| self.max_rule = None | |
| # Parsing options: | |
| self.whitespace_rule = whitespace_rule | |
| self.strings_are_tokens = strings_are_tokens | |
| class _PegParserRule(object): | |
| """Base class for all rules""" | |
| def __init__(self): | |
| return | |
| def __str__(self): | |
| return self.__class__.__name__ | |
| def _match_impl(self, state, pos): | |
| """Default implementation of the matching algorithm. | |
| Should be overwritten by sub-classes. | |
| """ | |
| raise RuntimeError('_match_impl not implemented') | |
| def match(self, state, pos): | |
| """Matches the rule against the text in the given position. | |
| The actual rule evaluation is delegated to _match_impl, | |
| while this function deals mostly with support tasks such as | |
| skipping whitespace, debug information and data for exception. | |
| Args: | |
| state -- the current parsing state and options. | |
| pos -- the current offset in the text. | |
| Returns: | |
| (next position, value) if the rule matches, or | |
| (None, None) if it doesn't. | |
| """ | |
| if not state.is_whitespace_mode: | |
| # Skip whitespace | |
| pos = _skip_whitespace(state, pos) | |
| # Track position for possible error messaging | |
| if pos > state.max_pos: | |
| # Store position and the rule. | |
| state.max_pos = pos | |
| if isinstance(self, _StringRule): | |
| state.max_rule = [self] | |
| else: | |
| state.max_rule = [] | |
| elif pos == state.max_pos: | |
| if isinstance(self, _StringRule): | |
| state.max_rule.append(self) | |
| if _logger.isEnabledFor(logging.DEBUG): | |
| # Used for debugging | |
| _logger.debug('Try: pos=%s char=%s rule=%s' % \ | |
| (pos, state.text[pos:pos + 1], self)) | |
| # Delegate the matching logic to the the specialized function. | |
| res = self._match_impl(state, pos) | |
| if not state.is_whitespace_mode \ | |
| and _logger.isEnabledFor(logging.DEBUG): | |
| # More debugging information | |
| (nextPos, ast) = res | |
| if nextPos is not None: | |
| _logger.debug('Match! pos=%s char=%s rule=%s' % \ | |
| (pos, state.text[pos:pos + 1], self)) | |
| else: | |
| _logger.debug('Fail. pos=%s char=%s rule=%s' % \ | |
| (pos, state.text[pos:pos + 1], self)) | |
| return res | |
| def _compile(rule): | |
| """Recursively compiles user-defined rules into parser rules. | |
| Compilation is performed by converting strings, regular expressions, lists | |
| and functions into _StringRule, _RegExpRule, SEQUENCE and _FunctionRule | |
| (respectively). Memoization is used to avoid infinite recursion as rules | |
| may refer to each other.""" | |
| if rule is None: | |
| raise RuntimeError('None is not a valid rule') | |
| elif isinstance(rule, str): | |
| return _StringRule(rule) | |
| elif isinstance(rule, _regex_type): | |
| return _RegExpRule(rule) | |
| elif isinstance(rule, _list_type): | |
| return SEQUENCE(*rule) | |
| elif isinstance(rule, _function_type): | |
| # Memoize compiled functions to avoid infinite compliation loops. | |
| if rule in _compiled_functions_memory: | |
| return _compiled_functions_memory[rule] | |
| else: | |
| compiled_function = _FunctionRule(rule) | |
| _compiled_functions_memory[rule] = compiled_function | |
| compiled_function._sub_rule = _compile(rule()) | |
| return compiled_function | |
| elif isinstance(rule, _PegParserRule): | |
| return rule | |
| else: | |
| raise RuntimeError('Invalid rule type %s: %s', (type(rule), rule)) | |
| def _skip_whitespace(state, pos): | |
| """Returns the next non-whitespace position. | |
| This is done by matching the optional whitespace_rule with the current | |
| text.""" | |
| if not state.whitespace_rule: | |
| return pos | |
| state.is_whitespace_mode = True | |
| nextPos = pos | |
| while nextPos is not None: | |
| pos = nextPos | |
| (nextPos, ast) = state.whitespace_rule.match(state, pos) | |
| state.is_whitespace_mode = False | |
| return pos | |
| class _StringRule(_PegParserRule): | |
| """This rule tries to match a whole string.""" | |
| def __init__(self, string): | |
| """Constructor. | |
| Args: | |
| string -- string to match. | |
| """ | |
| _PegParserRule.__init__(self) | |
| self._string = string | |
| def __str__(self): | |
| return '"%s"' % self._string | |
| def _match_impl(self, state, pos): | |
| """Tries to match the string at the current position""" | |
| if state.text.startswith(self._string, pos): | |
| nextPos = pos + len(self._string) | |
| if state.strings_are_tokens: | |
| return (nextPos, None) | |
| else: | |
| return (nextPos, self._string) | |
| return (None, None) | |
| class _RegExpRule(_PegParserRule): | |
| """This rule tries to matches a regular expression.""" | |
| def __init__(self, reg_exp): | |
| """Constructor. | |
| Args: | |
| reg_exp -- a regular expression used in matching. | |
| """ | |
| _PegParserRule.__init__(self) | |
| self.reg_exp = reg_exp | |
| def __str__(self): | |
| return 'regexp' | |
| def _match_impl(self, state, pos): | |
| """Tries to match the regular expression with current text""" | |
| matchObj = self.reg_exp.match(state.text, pos) | |
| if matchObj: | |
| matchStr = matchObj.group() | |
| return (pos + len(matchStr), matchStr) | |
| return (None, None) | |
| class _FunctionRule(_PegParserRule): | |
| """Function rule wraps a rule defined via a Python function. | |
| Defining rules via functions helps break the grammar into parts, labeling | |
| the ast, and supporting recursive definitions in the grammar | |
| Usage Example: | |
| def Func(): return ['function', TOKEN('('), TOKEN(')')] | |
| def Var(): return OR('x', 'y') | |
| def Program(): return OR(Func, Var) | |
| When matched with 'function()', will return the tuple: | |
| ('Program', ('Func', 'function')) | |
| When matched with 'x', will return the tuple: | |
| ('Program', ('Var', 'x')) | |
| Functions who's name begins with '_' will not be labelled. This is useful | |
| for creating utility rules. Extending the example above: | |
| def _Program(): return OR(Func, Var) | |
| When matched with 'function()', will return the tuple: | |
| ('Func', 'function') | |
| """ | |
| def __init__(self, func): | |
| """Constructor. | |
| Args: | |
| func -- the original function will be used for labeling output. | |
| """ | |
| _PegParserRule.__init__(self) | |
| self._func = func | |
| # Sub-rule is compiled by _compile to avoid infinite recursion. | |
| self._sub_rule = None | |
| def __str__(self): | |
| return self._func.__name__ | |
| def _match_impl(self, state, pos): | |
| """Simply invokes the sub rule""" | |
| (nextPos, ast) = self._sub_rule.match(state, pos) | |
| if nextPos is not None: | |
| if not self._func.__name__.startswith('_'): | |
| ast = (self._func.__name__, ast) | |
| return (nextPos, ast) | |
| return (None, None) | |
| class SEQUENCE(_PegParserRule): | |
| """This rule expects all given rules to match in sequence. | |
| Note that SEQUENCE is equivalent to a rule composed of a Python list of | |
| rules. | |
| Usage example: SEQUENCE('A', 'B', 'C') | |
| or: ['A', 'B', 'C'] | |
| Will match 'ABC' but not 'A', 'B' or ''. | |
| """ | |
| def __init__(self, *rules): | |
| """Constructor. | |
| Args: | |
| rules -- one or more rules to match. | |
| """ | |
| _PegParserRule.__init__(self) | |
| self._sub_rules = [] | |
| for rule in rules: | |
| self._sub_rules.append(_compile(rule)) | |
| def _match_impl(self, state, pos): | |
| """Tries to match all the sub rules""" | |
| sequence = [] | |
| for rule in self._sub_rules: | |
| (nextPos, ast) = rule.match(state, pos) | |
| if nextPos is not None: | |
| if ast: | |
| if isinstance(ast, _list_type): | |
| sequence.extend(ast) | |
| else: | |
| sequence.append(ast) | |
| pos = nextPos | |
| else: | |
| return (None, None) | |
| return (pos, sequence) | |
| class OR(_PegParserRule): | |
| """This rule matches one and only one of multiple sub-rules. | |
| Usage example: OR('A', 'B', 'C') | |
| Will match 'A', 'B' or 'C'. | |
| """ | |
| def __init__(self, *rules): | |
| """Constructor. | |
| Args: | |
| rules -- rules to choose from. | |
| """ | |
| _PegParserRule.__init__(self) | |
| self._sub_rules = [] | |
| for rule in rules: | |
| self._sub_rules.append(_compile(rule)) | |
| def _match_impl(self, state, pos): | |
| """Tries to match at leat one of the sub rules""" | |
| for rule in self._sub_rules: | |
| (nextPos, ast) = rule.match(state, pos) | |
| if nextPos is not None: | |
| return (nextPos, ast) | |
| return (None, None) | |
| class MAYBE(_PegParserRule): | |
| """Will try to match the given rule, tolerating absence. | |
| Usage example: MAYBE('A') | |
| Will match 'A' but also ''. | |
| """ | |
| def __init__(self, rule): | |
| """Constructor. | |
| Args: | |
| rule -- the rule that may be absent. | |
| """ | |
| _PegParserRule.__init__(self) | |
| self._sub_rule = _compile(rule) | |
| def _match_impl(self, state, pos): | |
| """Tries to match at leat one of the sub rules""" | |
| (nextPos, ast) = self._sub_rule.match(state, pos) | |
| if nextPos is not None: | |
| return (nextPos, ast) | |
| return (pos, None) | |
| class MANY(_PegParserRule): | |
| """Will try to match the given rule one or more times. | |
| Usage example 1: MANY('A') | |
| Will match 'A', 'AAAAA' but not ''. | |
| Usage example 2: MANY('A', separator=',') | |
| Will match 'A', 'A,A' but not 'AA'. | |
| """ | |
| def __init__(self, rule, separator=None): | |
| """Constructor. | |
| Args: | |
| rule -- the rule to match multiple times. | |
| separator -- this optional rule is used to match separators. | |
| """ | |
| _PegParserRule.__init__(self) | |
| self._sub_rule = _compile(rule) | |
| self._separator = _compile(separator) if separator else None | |
| def _match_impl(self, state, pos): | |
| res = [] | |
| count = 0 | |
| while True: | |
| if count > 0 and self._separator: | |
| (nextPos, ast) = self._separator.match(state, pos) | |
| if nextPos is not None: | |
| pos = nextPos | |
| if ast: | |
| res.append(ast) | |
| else: | |
| break | |
| (nextPos, ast) = self._sub_rule.match(state, pos) | |
| if nextPos is None: | |
| break | |
| count += 1 | |
| pos = nextPos | |
| res.append(ast) | |
| if count > 0: | |
| return (pos, res) | |
| return (None, None) | |
| class TOKEN(_PegParserRule): | |
| """The matched rule will not appear in the the output. | |
| Usage example: ['A', TOKEN('.'), 'B'] | |
| When matching 'A.B', will return the sequence ['A', 'B']. | |
| """ | |
| def __init__(self, rule): | |
| """Constructor. | |
| Args: | |
| rule -- the rule to match. | |
| """ | |
| _PegParserRule.__init__(self) | |
| self._sub_rule = _compile(rule) | |
| def _match_impl(self, state, pos): | |
| (nextPos, ast) = self._sub_rule.match(state, pos) | |
| if nextPos is not None: | |
| return (nextPos, None) | |
| return (None, None) | |
| class LABEL(_PegParserRule): | |
| """The matched rule will appear in the output with the given label. | |
| Usage example: LABEL('number', re.compile(r'[0-9]+')) | |
| When matched with '1234', will return ('number', '1234'). | |
| Keyword arguments: | |
| label -- a string. | |
| rule -- the rule to match. | |
| """ | |
| def __init__(self, label, rule): | |
| """Constructor. | |
| Args: | |
| rule -- the rule to match. | |
| """ | |
| _PegParserRule.__init__(self) | |
| self._label = label | |
| self._sub_rule = _compile(rule) | |
| def _match_impl(self, state, pos): | |
| (nextPos, ast) = self._sub_rule.match(state, pos) | |
| if nextPos is not None: | |
| return (nextPos, (self._label, ast)) | |
| return (None, None) | |
| class RAISE(_PegParserRule): | |
| """Raises a SyntaxError with a user-provided message. | |
| Usage example: ['A','B', RAISE('should have not gotten here')] | |
| Will not match 'A' but will raise an exception for 'AB'. | |
| This rule is useful mostly for debugging grammars. | |
| """ | |
| def __init__(self, message): | |
| """Constructor. | |
| Args: | |
| message -- the message for the raised exception. | |
| """ | |
| _PegParserRule.__init__(self) | |
| self._message = message | |
| def _match_impl(self, state, pos): | |
| raise RuntimeError(self._message) | |
| class PegParser(object): | |
| """PegParser class. | |
| This generic parser can be configured with rules to parse a wide | |
| range of inputs. | |
| """ | |
| def __init__(self, root_rule, whitespace_rule=None, | |
| strings_are_tokens=False): | |
| """Initializes a PegParser with rules and parsing options. | |
| Args: | |
| root_rule -- the top level rule to start matching at. Rule can be | |
| a regular expression, a string, or one of the special rules | |
| such as SEQUENCE, MANY, OR, etc. | |
| whitespace_rule -- used to identify and strip whitespace. Default | |
| isNone, configuring the parser to not tolerate whitespace. | |
| strings_are_tokens -- by default string rules are not treated as | |
| tokens. In many programming languages, strings are tokens, | |
| so this should be set to True. | |
| """ | |
| self._strings_are_tokens = strings_are_tokens | |
| self._root_rule = _compile(root_rule) | |
| if whitespace_rule is None: | |
| self._whitespace_rule = None | |
| else: | |
| self._whitespace_rule = _compile(whitespace_rule) | |
| def parse(self, text, start_pos=0): | |
| """Parses the given text input | |
| Args: | |
| text -- data to parse. | |
| start_pos -- the offset to start parsing at. | |
| Returns: | |
| An abstract syntax tree, with nodes being pairs of the format | |
| (label, value), where label is a string or a function, and value | |
| is a string, a pair or a list of pairs. | |
| """ | |
| def calculate_line_number_and_offset(globalOffset): | |
| """Calculates the line number and in-line offset""" | |
| i = 0 | |
| lineNumber = 1 | |
| lineOffset = 0 | |
| lineData = [] | |
| while i < globalOffset and i < len(text): | |
| if text[i] == '\n': | |
| lineNumber += 1 | |
| lineOffset = 0 | |
| lineData = [] | |
| else: | |
| lineData.append(text[i]) | |
| lineOffset += 1 | |
| i += 1 | |
| while i < len(text) and text[i] != '\n': | |
| lineData.append(text[i]) | |
| i += 1 | |
| return (lineNumber, lineOffset, ''.join(lineData)) | |
| def analyze_result(state, pos, ast): | |
| """Analyze match output""" | |
| if pos is not None: | |
| # Its possible that matching is successful but trailing | |
| # whitespace remains, so skip it. | |
| pos = _skip_whitespace(state, pos) | |
| if pos == len(state.text): | |
| # End of intput reached. Success! | |
| return ast | |
| # Failure - analyze and raise an error. | |
| (lineNumber, lineOffset, lineData) = \ | |
| calculate_line_number_and_offset(state.max_pos) | |
| message = 'unexpected error' | |
| if state.max_rule: | |
| set = {} | |
| map(set.__setitem__, state.max_rule, []) | |
| def to_str(item): | |
| return item.__str__() | |
| expected = ' or '.join(map(to_str, set.keys())) | |
| found = state.text[state.max_pos:state.max_pos + 1] | |
| message = 'Expected %s but "%s" found: "%s"' % \ | |
| (expected, found, lineData) | |
| raise SyntaxError( | |
| 'At line %s offset %s: %s' % \ | |
| (lineNumber, lineOffset, message)) | |
| # Initialize state | |
| state = _PegParserState(text, | |
| whitespace_rule=self._whitespace_rule, | |
| strings_are_tokens=self._strings_are_tokens) | |
| # Match and analyze result | |
| (pos, ast) = self._root_rule.match(state, start_pos) | |
| return analyze_result(state, pos, ast) |