#!/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)