// 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. library PegParser; /* * The following functions are combinators for building Rules. * * A rule is one of the following * - A String which matches the string literally. * - A Symbol which matches the symbol's definition. * - A list of rules with an optional reducing function, which matches a sequence. * - The result of calling one of the combinators. * * Some rules are 'value-generating' rules, they return an 'abstract syntax * tree' with the match. If a rule is not value-generating [:null:] is the * value. * * A Symbol is always a value-generating rule. If the value is not required, use * [:SKIP(aSymbol):] in place of [:aSymbol:]. * * A String is not a value-generating rule but can be converted into one by * using [:TEXT('string'):] in place of [:'string':]. * * A list or sequence is value-generating depending on the subrules. The * sequence is value-generating if any of the subrules are value-generating or * if there is a reducing function. If no reducing function is given, the value * returned depends on the number of value-generating subrules. If there is * only one value generating subrule, that provideds the value for the sequence. * If there are more, then the value is a list of the values of the * value-generating subrules. */ /** * Matches one character by a predicate on the character code. * If [spec] is an int, that character is matched. * If [spec] is a function it is used * * Example [: CHARCODE((code) => 48 <= code && code <= 57) :] recognizes an * ASCII digit. * * CHARCODE does not generate a value. */ _Rule CHARCODE(spec, [name]) { if (spec is int) return new _CharCodeRule((code) => code == spec, name); else return new _CharCodeRule(spec, name); } /** * Matches one of the [characters]. * * CHAR does not generate a value. */ _Rule CHAR([characters]) { if (characters == null) return const _AnyCharRule(); if (characters is int) return CHARCODE(characters); // Find the range of character codes and construct an array of flags for codes // within the range. List codes = characters.codeUnits.toList(); codes.sort((a, b) => a < b ? -1 : a > b ? 1 : 0); int lo = codes[0]; int hi = codes[codes.length - 1]; if (lo == hi) return CHARCODE(lo); int len = hi - lo + 1; var flags = new List(len); for (int i = 0; i < len; ++i) flags[i] = false; for (int code in codes) flags[code - lo] = true; return CHARCODE((code) => code >= lo && code <= hi && flags[code - lo]); } /** * Matches the end of the input. * * END does not generate a value. */ _Rule get END => new _EndOfInputRule(); /** * Throws an exception. */ _Rule ERROR(String message) => new _ErrorRule(message); /** * Matches [rule] but does not consume the input. Useful for matching a right * context. * * AT does not generate a value. */ _Rule AT(rule) => new _ContextRule(_compile(rule)); /** * Matches when [rule] does not match. No input is consumed. * * NOT does not generate a value. */ _Rule NOT(rule) => new _NegativeContextRule(_compile(rule)); /** * Matches [rule] but generates no value even if [rule] generates a value. * * SKIP never generates a value. */ _Rule SKIP(rule) => new _SkipRule(_compile(rule)); /** * Matches [rule] in a lexical context where whitespace is not automatically * skipped. Useful for matching what would normally be considered to be tokens. * [name] is a user-friendly description of what is being matched and is used in * error messages. * * LEX(rule) * LEX(name, rule) * * LEX does not generate a value. If a value is required, wrap LEX with TEXT. */ _Rule LEX(arg1, [arg2]) { if (arg2 == null) return new _LexicalRule(arg1 is String ? arg1 : null, _compile(arg1)); else return new _LexicalRule(arg1, _compile(arg2)); } /** * Matches [rule] and generates a value from the matched text. If the [rule] * matches, then TEXT(rule) matches and has a value derived from the string * fragment that was matched. The default derived value is the string fragment. * * TEXT always generates a value. */ _Rule TEXT(rule, [extractor]) => new _TextValueRule( _compile(rule), extractor == null ? (string, start, end) => string.substring(start, end) : extractor); /** * Matches an optional rule. * * MAYBE is a value generating matcher. * * If [rule] is value generating then the value is the value generated by [rule] * if it matches, and [:null:] if it does not. * * If [rule] is not value generating then the value is [:true:] if [rule] * matches and [:false:] if it does not. */ _Rule MAYBE(rule) => new _OptionalRule(_compile(rule)); /** * MANY(rule) matches [rule] [min] or more times. * [min] must be 0 or 1. * If [separator] is provided it is used to match a separator between matches of * [rule]. * * MANY is a value generating matcher. The value is a list of the matches of * [rule]. The list may be empty if [:min == 0:]. */ _Rule MANY(rule, {separator: null, int min: 1}) { assert(0 <= min && min <= 1); return new _RepeatRule(_compile(rule), _compileOptional(separator), min); } /** * Matches [rule] zero or more times. Shorthand for [:MANY(rule, min:0):] * TODO: retire min: parameter? * * MANY0 is a value generating matcher. */ _Rule MANY0(rule, [separator = null]) { return new _RepeatRule(_compile(rule), _compileOptional(separator), 0); } /** * Matches [rules] in order until one succeeds. * * OR is value-generating. */ _Rule OR( [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z]) => _compileMultiRule( (a is List && b == null) // Backward compat. OR([a, b]) => OR(a, b). ? a : _unspread(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z), false, (compiledRules, valueCount, reducer) => new _ChoiceRule(compiledRules)); _Rule SEQ( [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z]) => _compile(_unspread(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z)); /** * Matches [rule] */ _Rule MEMO(rule) => new _MemoRule(_compile(rule)); _Rule TAG(tag, rule) => _compile([ rule, (ast) => [tag, ast] ]); class ParseError implements Exception { const ParseError(String this._message); String toString() => _message; final String _message; } /** * A grammar is a collection of symbols and rules that may be used to parse an * input. */ class Grammar { Map _symbols; /** This rule may be set by the user to define whitespace. */ _Rule _whitespace; _Rule get whitespace => _whitespace; void set whitespace(rule) { _whitespace = _compile(rule); } Grammar() { _symbols = new Map(); whitespace = CHAR(' \t\r\n'); } /** * operator [] is used to find or create symbols. Symbols may appear in rules * to define recursive rules. */ Symbol operator [](String name) { if (_symbols.containsKey(name)) return _symbols[name]; Symbol s = new Symbol(name, this); _symbols[name] = s; return s; } /** * Parses the input string and returns the parsed AST, or throws an exception * if the input can't be parsed. */ parse(root, String text) { for (var symbol in _symbols.values) if (symbol._rule == null) print('${symbol.name} is undefined'); var state = new _ParserState(text, whitespace: whitespace); var match = _compile(root).match(state, 0); if (match == null) return diagnose(state); var pos = match[0]; pos = _skip_whitespace(state, pos); if (pos == state._end) return match[1]; // TODO: Make this complain about expecting end of file. return diagnose(state); } diagnose(state) { var message = 'unexpected error'; if (!state.max_rule.isEmpty) { var s = new Set(); for (var rule in state.max_rule) s.add(rule.description()); var tokens = new List.from(s); tokens.sort((a, b) => a.startsWith("'") == b.startsWith("'") ? a.compareTo(b) : a.startsWith("'") ? 1 : -1); var expected = tokens.join(' or '); var found = state.max_pos == state._end ? 'end of file' : "'${state._text[state.max_pos]}'"; message = 'Expected $expected but found $found'; } int start = state.max_pos; int end = start; while (start >= 1 && state._text[start - 1] != '\n') --start; while (end < state._text.length && state._text[end] != '\n') ++end; var line = state._text.substring(start, end); var indicator = ''; for (var i = 0; i < line.length && start + i < state.max_pos; i++) indicator = ' $indicator'; indicator = '$indicator^'; // TODO: Convert to an exception. print(message); print(line); print(indicator); return null; } } class Symbol { final String name; final Grammar grammar; _Rule _rule; Symbol(this.name, this.grammar); void set def(rule) { assert(_rule == null); // Assign once. _rule = _compile(rule); } toString() => _rule == null ? '<$name>' : '<$name = $_rule>'; } class _ParserState { _ParserState(this._text, {_Rule whitespace}) { _end = this._text.length; whitespaceRule = whitespace; max_rule = []; } String _text; int _end; // bool inWhitespaceMode = false; _Rule whitespaceRule = null; // Used for constructing an error message. int inhibitExpectedTrackingDepth = 0; int max_pos = 0; var max_rule; } /** * An interface tag for rules. If this tag is on a rule, then the description() * of the rule is something sensible to put in a message. */ abstract class _Expectable { String description(); } class _Rule { const _Rule(); // Returns null for a match failure or [pos, ast] for success. match(_ParserState state, int pos) { if (!state.inWhitespaceMode) { pos = _skip_whitespace(state, pos); } return matchAfterWS(state, pos); } // Faster entry point for matching a sub-rule that is matched to the start // position of the super-rule. Whitespace has already been skipped so no need // to try to skip it again. matchAfterWS(_ParserState state, int pos) { if (state.inhibitExpectedTrackingDepth == 0) { // Track position for possible error messaging if (pos > state.max_pos) { // Store position and the rule. state.max_pos = pos; if (this is _Expectable) { state.max_rule = [this]; } else { state.max_rule = []; } } else if (pos == state.max_pos) { if (this is _Expectable) { state.max_rule.add(this); } } } // Delegate the matching logic to the specialized function. return _match(state, pos); } // Overridden in subclasses to match the rule. _match(_ParserState state, int pos) => null; // Does the rule generate a value (AST) with the match? bool get generatesValue => false; get defaultValue => null; } int _skip_whitespace(state, pos) { // Returns the next non-whitespace position. // This is done by matching the optional whitespaceRule with the current text. if (state.whitespaceRule == null) return pos; state.inWhitespaceMode = true; state.inhibitExpectedTrackingDepth++; while (true) { var match = state.whitespaceRule.match(state, pos); if (match == null) break; pos = match[0]; } state.inWhitespaceMode = false; state.inhibitExpectedTrackingDepth--; return pos; } _Rule _compileOptional(rule) { return rule == null ? null : _compile(rule); } _Rule _compile(rule) { if (rule is _Rule) return rule; if (rule is String) return new _StringRule(rule); if (rule is Symbol) return new _SymbolRule(rule); if (rule is RegExp) return new _RegExpRule(rule); if (rule is List) { return _compileMultiRule( rule, true, (compiledRules, valueCount, reducer) => new _SequenceRule(compiledRules, valueCount, reducer)); } throw new Exception('Cannot compile rule: $rule'); } class _EndOfInputRule extends _Rule { _match(_ParserState state, int pos) { if (pos == state._end) return [pos, null]; return null; } toString() => 'END'; } class _ErrorRule extends _Rule { String message; _ErrorRule(String this.message); _match(_ParserState state, int pos) { throw new ParseError(message); } toString() => 'ERROR($message)'; } class _CharCodeRule extends _Rule { Function _predicate; var _name; _CharCodeRule(this._predicate, this._name); _match(_ParserState state, int pos) { if (pos == state._end) return null; int code = state._text.codeUnitAt(pos); if (_predicate(code)) return [pos + 1, null]; return null; } toString() => _name == null ? 'CHARCODE($_predicate)' : 'CHARCODE($_name)'; } class _AnyCharRule extends _Rule { const _AnyCharRule(); _match(_ParserState state, int pos) { if (pos == state._end) return null; return [pos + 1, null]; } toString() => 'CHAR()'; } class _SymbolRule extends _Rule { final Symbol _symbol; _SymbolRule(Symbol this._symbol); _match(_ParserState state, int pos) { if (_symbol._rule == null) throw new Exception("Symbol '${_symbol.name}' is undefined"); return _symbol._rule.match(state, pos); } bool get generatesValue => true; toString() => '<${_symbol.name}>'; } class _SkipRule extends _Rule { // A rule that has no value. _Rule _rule; _SkipRule(_Rule this._rule); _match(_ParserState state, int pos) { var match = _rule.matchAfterWS(state, pos); if (match == null) return null; return [match[0], null]; } toString() => 'TOKEN($_rule)'; } class _StringRule extends _Rule implements _Expectable { final String _string; int _len; _StringRule(this._string) { _len = _string.length; } _match(_ParserState state, int pos) { if (pos + _len > state._end) return null; for (int i = 0; i < _len; i++) { if (state._text.codeUnitAt(pos + i) != _string.codeUnitAt(i)) return null; } return [pos + _len, null]; } //get defaultValue => _string; toString() => '"$_string"'; description() => "'$_string'"; } class _RegExpRule extends _Rule { RegExp _re; _RegExpRule(this._re) { // There is no convenient way to match an anchored substring. throw new Exception('RegExp matching not supported'); } toString() => '"$_re"'; } class _LexicalRule extends _Rule implements _Expectable { final String _name; final _Rule _rule; _LexicalRule(String this._name, _Rule this._rule); _match(_ParserState state, int pos) { state.inWhitespaceMode = true; state.inhibitExpectedTrackingDepth++; var match = _rule.matchAfterWS(state, pos); state.inhibitExpectedTrackingDepth--; state.inWhitespaceMode = false; return match; } toString() => _name; description() => _name == null ? '?' : _name; } class _TextValueRule extends _Rule { final _Rule _rule; final _extract; // Function _TextValueRule(_Rule this._rule, Function this._extract); _match(_ParserState state, int pos) { var match = _rule.matchAfterWS(state, pos); if (match == null) { return null; } var endPos = match[0]; return [endPos, _extract(state._text, pos, endPos)]; } bool get generatesValue => true; toString() => 'TEXT($_rule)'; } _Rule _compileMultiRule( List rules, bool allowReducer, finish(compiledRules, valueCount, reducer)) { int valueCount = 0; List compiledRules = new List<_Rule>(); Function reducer; for (var rule in rules) { if (reducer != null) throw new Exception('Reducer must be last in sequence: $rule'); if (rule is Function) { if (allowReducer) reducer = rule; else throw new Exception('Bad rule: "$rule"'); } else { _Rule compiledRule = _compile(rule); if (compiledRule.generatesValue) ++valueCount; compiledRules.add(compiledRule); } } return finish(compiledRules, valueCount, reducer); } String _formatMultiRule(String functor, List rules) { var sb = new StringBuffer(functor); sb.write('('); var separator = ''; for (var rule in rules) { sb.write(separator); sb.write(rule); separator = ','; } sb.write(')'); return sb.toString(); } class _SequenceRule extends _Rule { // This rule matches the component rules in order. final List<_Rule> _rules; final int _generatingSubRules; final Function _reducer; bool _generatesValue; _SequenceRule(List<_Rule> this._rules, int this._generatingSubRules, Function this._reducer) { _generatesValue = _generatingSubRules > 0 || _reducer != null; } _match(state, pos) { var sequence = []; for (var rule in _rules) { var match = rule.match(state, pos); if (match == null) return null; if (rule.generatesValue) { var ast = match[1]; sequence.add(ast); } pos = match[0]; } if (_reducer == null) { if (_generatingSubRules == 0) return [pos, null]; if (_generatingSubRules == 1) return [pos, sequence[0]]; return [pos, sequence]; } else { return [pos, _apply(_reducer, sequence)]; } } bool get generatesValue => _generatesValue; toString() => _formatMultiRule('SEQ', _rules); } class _ChoiceRule extends _Rule { // This rule matches the first component rule that matches. List<_Rule> _rules; _ChoiceRule(List<_Rule> this._rules); _match(state, pos) { for (var rule in _rules) { var match = rule.match(state, pos); if (match != null) { /* if (!rule.generatesValue) { var value = rule.defaultValue; if (value != null) return [match[0], value]; } */ return match; } } return null; } bool get generatesValue => true; toString() => _formatMultiRule('OR', _rules); } class _OptionalRule extends _Rule { _Rule _rule; _OptionalRule(_Rule this._rule); _match(_ParserState state, int pos) { var match = _rule.match(state, pos); if (_rule.generatesValue) return match == null ? [pos, null] : match; return match == null ? [pos, false] : [match[0], true]; } bool get generatesValue => true; toString() => 'MAYBE($_rule)'; } class _ContextRule extends _Rule { _Rule _rule; _ContextRule(_Rule this._rule); _match(_ParserState state, int pos) { // TODO: protect error state. var match = _rule._match(state, pos); if (match == null) return null; return [pos, null]; } toString() => 'AT($_rule)'; } class _NegativeContextRule extends _Rule { _Rule _rule; _NegativeContextRule(_Rule this._rule); _match(_ParserState state, int pos) { // TODO: protect error state. var match = _rule._match(state, pos); if (match == null) return [pos, null]; return null; } toString() => 'NOT($_rule)'; } class _RepeatRule extends _Rule { // Matches zero, one or more items. _Rule _rule; _Rule _separator; int _min; _RepeatRule(this._rule, this._separator, this._min); _match(state, pos) { // First match. var match = _rule.match(state, pos); if (match == null) if (_min == 0) return [pos, []]; else return null; pos = match[0]; var result = [match[1]]; // Subsequent matches: while (true) { var newPos = pos; if (_separator != null) { match = _separator.match(state, pos); if (match == null) return [pos, result]; newPos = match[0]; } match = _rule.match(state, newPos); if (match == null) return [pos, result]; pos = match[0]; result.add(match[1]); } } bool get generatesValue => true; toString() => 'MANY(min:$_min, $_rule${_separator==null?'':", sep: $_separator"})'; } class _MemoRule extends _Rule { final _Rule _rule; var parseInstance; // A map from position to result. Can this be replaced with something // smaller? // TODO: figure out how to discard the map and parseInstance after parsing. Map map; _MemoRule(this._rule); _match(state, pos) { // See if we are still parsing the same input. Relies on the fact that the // input is a string and strings are immutable. if (!identical(parseInstance, state._text)) { map = new Map(); parseInstance = state._text; } // TODO: does this have to check or preserve parse state (like // inWhitespaceMode, error position info etc?) // Stored result can be null (memoized failure). if (map.containsKey(pos)) { return map[pos]; } var match = _rule.match(state, pos); map[pos] = match; return match; } bool get generatesValue => _rule.generatesValue; toString() => 'MEMO($_rule)'; } _apply(fn, List args) { switch (args.length) { case 0: return fn(); case 1: return fn(args[0]); case 2: return fn(args[0], args[1]); case 3: return fn(args[0], args[1], args[2]); case 4: return fn(args[0], args[1], args[2], args[3]); case 5: return fn(args[0], args[1], args[2], args[3], args[4]); case 6: return fn(args[0], args[1], args[2], args[3], args[4], args[5]); case 7: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6]); case 8: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]); case 9: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]); case 10: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]); default: throw new Exception('Too many arguments in _apply: $args'); } } List _unspread(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z) { List list = new List(); add(element) { if (element != null) list.add(element); } add(a); add(b); add(c); add(d); add(e); add(f); add(g); add(h); add(i); add(j); add(k); add(l); add(m); add(n); add(o); add(p); add(q); add(r); add(s); add(t); add(u); add(v); add(w); add(x); add(y); add(z); return list; }