├── petitparser.png ├── .gitignore ├── AUTHORS ├── lib ├── indent.dart ├── src │ ├── parser │ │ ├── utils │ │ │ ├── sequential.dart │ │ │ ├── labeled.dart │ │ │ ├── resolvable.dart │ │ │ ├── failure_joiner.dart │ │ │ └── separated_list.dart │ │ ├── repeater │ │ │ ├── unbounded.dart │ │ │ ├── repeating.dart │ │ │ ├── limited.dart │ │ │ ├── possessive.dart │ │ │ ├── lazy.dart │ │ │ └── greedy.dart │ │ ├── character │ │ │ ├── predicate │ │ │ │ ├── digit.dart │ │ │ │ ├── lowercase.dart │ │ │ │ ├── uppercase.dart │ │ │ │ ├── letter.dart │ │ │ │ ├── word.dart │ │ │ │ ├── char.dart │ │ │ │ ├── not.dart │ │ │ │ ├── constant.dart │ │ │ │ ├── range.dart │ │ │ │ ├── whitespace.dart │ │ │ │ ├── ranges.dart │ │ │ │ └── lookup.dart │ │ │ ├── whitespace.dart │ │ │ ├── digit.dart │ │ │ ├── letter.dart │ │ │ ├── lowercase.dart │ │ │ ├── predicate.dart │ │ │ ├── uppercase.dart │ │ │ ├── word.dart │ │ │ ├── any.dart │ │ │ ├── range.dart │ │ │ ├── any_of.dart │ │ │ ├── none_of.dart │ │ │ ├── utils │ │ │ │ ├── code.dart │ │ │ │ └── optimize.dart │ │ │ ├── char.dart │ │ │ └── pattern.dart │ │ ├── combinator │ │ │ ├── delegate.dart │ │ │ ├── list.dart │ │ │ ├── and.dart │ │ │ ├── settable.dart │ │ │ ├── optional.dart │ │ │ ├── skip.dart │ │ │ ├── not.dart │ │ │ ├── sequence.dart │ │ │ ├── generated │ │ │ │ ├── sequence_2.dart │ │ │ │ ├── sequence_3.dart │ │ │ │ └── sequence_4.dart │ │ │ └── choice.dart │ │ ├── misc │ │ │ ├── position.dart │ │ │ ├── failure.dart │ │ │ ├── epsilon.dart │ │ │ ├── label.dart │ │ │ ├── end.dart │ │ │ └── newline.dart │ │ ├── action │ │ │ ├── cast.dart │ │ │ ├── cast_list.dart │ │ │ ├── token.dart │ │ │ ├── pick.dart │ │ │ ├── permute.dart │ │ │ ├── continuation.dart │ │ │ ├── where.dart │ │ │ ├── flatten.dart │ │ │ ├── map.dart │ │ │ └── trim.dart │ │ └── predicate │ │ │ ├── string.dart │ │ │ ├── pattern.dart │ │ │ ├── character.dart │ │ │ ├── converter.dart │ │ │ ├── predicate.dart │ │ │ ├── single_character.dart │ │ │ └── unicode_character.dart │ ├── definition │ │ ├── internal │ │ │ ├── undefined.dart │ │ │ └── reference.dart │ │ ├── resolve.dart │ │ └── grammar.dart │ ├── expression │ │ ├── utils.dart │ │ ├── result.dart │ │ ├── builder.dart │ │ └── group.dart │ ├── matcher │ │ ├── pattern.dart │ │ ├── accept.dart │ │ ├── matches │ │ │ ├── matches_iterable.dart │ │ │ └── matches_iterator.dart │ │ ├── pattern │ │ │ ├── pattern_iterable.dart │ │ │ ├── parser_match.dart │ │ │ ├── pattern_iterator.dart │ │ │ └── parser_pattern.dart │ │ └── matches.dart │ ├── shared │ │ ├── types.dart │ │ └── pragma.dart │ ├── reflection │ │ ├── internal │ │ │ ├── formatting.dart │ │ │ ├── first_set.dart │ │ │ ├── utilities.dart │ │ │ ├── cycle_set.dart │ │ │ ├── path.dart │ │ │ ├── follow_set.dart │ │ │ └── optimize_rules.dart │ │ ├── transform.dart │ │ ├── iterable.dart │ │ ├── optimize.dart │ │ ├── linter.dart │ │ └── analyzer.dart │ ├── core │ │ ├── exception.dart │ │ ├── context.dart │ │ ├── result.dart │ │ └── token.dart │ ├── indent │ │ └── indent.dart │ └── debug │ │ ├── progress.dart │ │ ├── profile.dart │ │ └── trace.dart ├── expression.dart ├── debug.dart ├── matcher.dart ├── definition.dart ├── petitparser.dart ├── reflection.dart ├── core.dart └── parser.dart ├── .github ├── dependabot.yml └── workflows │ └── dart.yml ├── example ├── README.md └── calc.dart ├── .vscode └── launch.json ├── pubspec.yaml ├── LICENSE ├── analysis_options.yaml └── test ├── utils └── assertions.dart ├── parser_misc_test.dart └── context_test.dart /petitparser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petitparser/dart-petitparser/HEAD/petitparser.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .dart_tool/ 3 | .DS_Store 4 | .idea/ 5 | .packages 6 | .pub/ 7 | petitparser_examples/build 8 | pubspec.lock 9 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Lukas Renggli (https://www.lukas-renggli.ch) and contributors (https://github.com/renggli/dart-petitparser/graphs/contributors). 2 | -------------------------------------------------------------------------------- /lib/indent.dart: -------------------------------------------------------------------------------- 1 | /// This package simplifies the creation of indention based parsers. 2 | library; 3 | 4 | export 'src/indent/indent.dart'; 5 | -------------------------------------------------------------------------------- /lib/src/parser/utils/sequential.dart: -------------------------------------------------------------------------------- 1 | /// Marker interface of a parser that consumes its children sequentially. 2 | abstract class SequentialParser {} 3 | -------------------------------------------------------------------------------- /lib/src/parser/repeater/unbounded.dart: -------------------------------------------------------------------------------- 1 | /// An [int] used to mark an unbounded maximum repetition. 2 | const unbounded = 9007199254740991; // Number.MAX_SAFE_INTEGER 3 | -------------------------------------------------------------------------------- /lib/expression.dart: -------------------------------------------------------------------------------- 1 | /// This package simplifies the creation of expression parsers. 2 | library; 3 | 4 | export 'src/expression/builder.dart'; 5 | export 'src/expression/group.dart'; 6 | -------------------------------------------------------------------------------- /lib/src/definition/internal/undefined.dart: -------------------------------------------------------------------------------- 1 | class _Undefined { 2 | const _Undefined(); 3 | } 4 | 5 | /// A unique sentinel object for undefined data. 6 | const undefined = _Undefined(); 7 | -------------------------------------------------------------------------------- /lib/debug.dart: -------------------------------------------------------------------------------- 1 | /// This package contains some simple debugging tools. 2 | library; 3 | 4 | export 'src/debug/profile.dart'; 5 | export 'src/debug/progress.dart'; 6 | export 'src/debug/trace.dart'; 7 | -------------------------------------------------------------------------------- /lib/matcher.dart: -------------------------------------------------------------------------------- 1 | /// This package contains helpers to simplify parsing and data extraction. 2 | library; 3 | 4 | export 'src/matcher/accept.dart'; 5 | export 'src/matcher/matches.dart'; 6 | export 'src/matcher/pattern.dart'; 7 | -------------------------------------------------------------------------------- /lib/definition.dart: -------------------------------------------------------------------------------- 1 | /// This package simplifies the creation of complicated recursive grammars. 2 | library; 3 | 4 | export 'src/definition/grammar.dart'; 5 | export 'src/definition/reference.dart'; 6 | export 'src/definition/resolve.dart'; 7 | -------------------------------------------------------------------------------- /lib/src/parser/utils/labeled.dart: -------------------------------------------------------------------------------- 1 | import '../../core/parser.dart'; 2 | 3 | /// Interface of a parser that has a debug label. 4 | abstract class LabeledParser implements Parser { 5 | /// Debug label of the parser object. 6 | String get label; 7 | } 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | 2 | version: 2 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | - package-ecosystem: "pub" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" -------------------------------------------------------------------------------- /lib/petitparser.dart: -------------------------------------------------------------------------------- 1 | /// This package exports the core library of PetitParser, a dynamic parser 2 | /// combinator framework. 3 | library; 4 | 5 | export 'core.dart'; 6 | export 'definition.dart'; 7 | export 'expression.dart'; 8 | export 'matcher.dart'; 9 | export 'parser.dart'; 10 | -------------------------------------------------------------------------------- /lib/src/expression/utils.dart: -------------------------------------------------------------------------------- 1 | import '../core/parser.dart'; 2 | import '../parser/combinator/choice.dart'; 3 | 4 | // Internal helper to build an optimal choice parser. 5 | Parser buildChoice(List> parsers) => 6 | parsers.length == 1 ? parsers.first : parsers.toChoiceParser(); 7 | -------------------------------------------------------------------------------- /lib/src/matcher/pattern.dart: -------------------------------------------------------------------------------- 1 | import '../core/parser.dart'; 2 | import 'pattern/parser_pattern.dart'; 3 | 4 | extension PatternParserExtension on Parser { 5 | /// Converts this [Parser] into a [Pattern] for basic searches within strings. 6 | Pattern toPattern() => ParserPattern(this); 7 | } 8 | -------------------------------------------------------------------------------- /lib/reflection.dart: -------------------------------------------------------------------------------- 1 | /// This package contains tools to reflect on and transform parsers. 2 | library; 3 | 4 | export 'src/reflection/analyzer.dart'; 5 | export 'src/reflection/iterable.dart'; 6 | export 'src/reflection/linter.dart'; 7 | export 'src/reflection/optimize.dart'; 8 | export 'src/reflection/transform.dart'; 9 | -------------------------------------------------------------------------------- /lib/core.dart: -------------------------------------------------------------------------------- 1 | /// This package contains the core classes of the framework. 2 | /// 3 | /// {@canonicalFor parser.Parser} 4 | library; 5 | 6 | export 'src/core/context.dart'; 7 | export 'src/core/exception.dart'; 8 | export 'src/core/parser.dart'; 9 | export 'src/core/result.dart'; 10 | export 'src/core/token.dart'; 11 | -------------------------------------------------------------------------------- /lib/src/parser/utils/resolvable.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/parser.dart'; 4 | 5 | /// Interface of a parser that can be resolved to another one. 6 | abstract class ResolvableParser implements Parser { 7 | /// Resolves this parser with another one of the same type. 8 | @useResult 9 | Parser resolve(); 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/parser/character/predicate/digit.dart: -------------------------------------------------------------------------------- 1 | import '../predicate.dart'; 2 | 3 | class DigitCharPredicate extends CharacterPredicate { 4 | const DigitCharPredicate(); 5 | 6 | @override 7 | bool test(int charCode) => 48 <= charCode && charCode <= 57; 8 | 9 | @override 10 | bool isEqualTo(CharacterPredicate other) => other is DigitCharPredicate; 11 | } 12 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # PetitParser Examples 2 | 3 | For more elaborate examples see the official [example repository](https://github.com/petitparser/dart-petitparser-examples) or the [demo page](https://petitparser.github.io/). 4 | 5 | This directory contains the command-line calculator that is described in the introductory tutorial: 6 | 7 | dart example/calc.dart "1 + 2 * 3" 8 | -------------------------------------------------------------------------------- /lib/src/parser/character/predicate/lowercase.dart: -------------------------------------------------------------------------------- 1 | import '../predicate.dart'; 2 | 3 | class LowercaseCharPredicate extends CharacterPredicate { 4 | const LowercaseCharPredicate(); 5 | 6 | @override 7 | bool test(int charCode) => 97 <= charCode && charCode <= 122; 8 | 9 | @override 10 | bool isEqualTo(CharacterPredicate other) => other is LowercaseCharPredicate; 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/parser/character/predicate/uppercase.dart: -------------------------------------------------------------------------------- 1 | import '../predicate.dart'; 2 | 3 | class UppercaseCharPredicate extends CharacterPredicate { 4 | const UppercaseCharPredicate(); 5 | 6 | @override 7 | bool test(int charCode) => 65 <= charCode && charCode <= 90; 8 | 9 | @override 10 | bool isEqualTo(CharacterPredicate other) => other is UppercaseCharPredicate; 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run all Tests", 6 | "type": "dart", 7 | "request": "launch", 8 | "program": "test" 9 | }, 10 | { 11 | "name": "Generate Sequence", 12 | "type": "dart", 13 | "request": "launch", 14 | "program": "bin/generate_sequence.dart" 15 | }, 16 | ] 17 | } -------------------------------------------------------------------------------- /lib/src/matcher/accept.dart: -------------------------------------------------------------------------------- 1 | import '../core/parser.dart'; 2 | 3 | extension AcceptParser on Parser { 4 | /// Tests if the [input] can be successfully parsed. 5 | /// 6 | /// For example, `letter().plus().accept('abc')` returns `true`, and 7 | /// `letter().plus().accept('123')` returns `false`. 8 | bool accept(String input, {int start = 0}) => fastParseOn(input, start) >= 0; 9 | } 10 | -------------------------------------------------------------------------------- /lib/src/parser/character/predicate/letter.dart: -------------------------------------------------------------------------------- 1 | import '../predicate.dart'; 2 | 3 | class LetterCharPredicate extends CharacterPredicate { 4 | const LetterCharPredicate(); 5 | 6 | @override 7 | bool test(int charCode) => 8 | (65 <= charCode && charCode <= 90) || (97 <= charCode && charCode <= 122); 9 | 10 | @override 11 | bool isEqualTo(CharacterPredicate other) => other is LetterCharPredicate; 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/parser/character/whitespace.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/parser.dart'; 4 | import '../predicate/character.dart'; 5 | import 'predicate/whitespace.dart'; 6 | 7 | /// Returns a parser that accepts any whitespace character. 8 | @useResult 9 | Parser whitespace({String message = 'whitespace expected'}) => 10 | CharacterParser(const WhitespaceCharPredicate(), message); 11 | -------------------------------------------------------------------------------- /lib/src/parser/character/digit.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/parser.dart'; 4 | import '../predicate/character.dart'; 5 | import 'predicate/digit.dart'; 6 | 7 | /// Returns a parser that accepts any digit character. The accepted input is 8 | /// equivalent to the character-set `0-9`. 9 | @useResult 10 | Parser digit({String message = 'digit expected'}) => 11 | CharacterParser(const DigitCharPredicate(), message); 12 | -------------------------------------------------------------------------------- /lib/src/shared/types.dart: -------------------------------------------------------------------------------- 1 | /// A generic callback function type returning a value of type [R] for a given 2 | /// input of type [T]. 3 | typedef Callback = R Function(T value); 4 | 5 | /// A generic predicate function type returning `true` or `false` for a given 6 | /// input of type [T]. 7 | typedef Predicate = Callback; 8 | 9 | /// A generic void callback with an argument of type [T], but not return value. 10 | typedef VoidCallback = Callback; 11 | -------------------------------------------------------------------------------- /lib/src/parser/character/letter.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/parser.dart'; 4 | import '../predicate/character.dart'; 5 | import 'predicate/letter.dart'; 6 | 7 | /// Returns a parser that accepts any letter character (lowercase or uppercase). 8 | /// The accepted input is equivalent to the character-set `a-zA-Z`. 9 | @useResult 10 | Parser letter({String message = 'letter expected'}) => 11 | CharacterParser(const LetterCharPredicate(), message); 12 | -------------------------------------------------------------------------------- /lib/src/parser/character/lowercase.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/parser.dart'; 4 | import '../predicate/character.dart'; 5 | import 'predicate/lowercase.dart'; 6 | 7 | /// Returns a parser that accepts any lowercase character. The accepted input is 8 | /// equivalent to the character-set `a-z`. 9 | @useResult 10 | Parser lowercase({String message = 'lowercase letter expected'}) => 11 | CharacterParser(const LowercaseCharPredicate(), message); 12 | -------------------------------------------------------------------------------- /lib/src/parser/character/predicate.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | /// Abstract class for character predicates. 4 | @immutable 5 | abstract class CharacterPredicate { 6 | const CharacterPredicate(); 7 | 8 | /// Tests if the [charCode] satisfies the predicate. 9 | bool test(int charCode); 10 | 11 | /// Compares the predicate and [other] for equality. 12 | bool isEqualTo(CharacterPredicate other); 13 | 14 | @override 15 | String toString() => '$runtimeType'; 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/parser/character/uppercase.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/parser.dart'; 4 | import '../predicate/character.dart'; 5 | import 'predicate/uppercase.dart'; 6 | 7 | /// Returns a parser that accepts any uppercase character. The accepted input is 8 | /// equivalent to the character-set `A-Z`. 9 | @useResult 10 | Parser uppercase({String message = 'uppercase letter expected'}) => 11 | CharacterParser(const UppercaseCharPredicate(), message); 12 | -------------------------------------------------------------------------------- /lib/src/parser/character/predicate/word.dart: -------------------------------------------------------------------------------- 1 | import '../predicate.dart'; 2 | 3 | class WordCharPredicate extends CharacterPredicate { 4 | const WordCharPredicate(); 5 | 6 | @override 7 | bool test(int charCode) => 8 | (65 <= charCode && charCode <= 90) || 9 | (97 <= charCode && charCode <= 122) || 10 | (48 <= charCode && charCode <= 57) || 11 | identical(charCode, 95); 12 | 13 | @override 14 | bool isEqualTo(CharacterPredicate other) => other is WordCharPredicate; 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/reflection/internal/formatting.dart: -------------------------------------------------------------------------------- 1 | /// Generates a human readable list of strings. 2 | String formatIterable(Iterable objects, {int? offset}) { 3 | final buffer = StringBuffer(); 4 | for (var i = 0, it = objects.iterator; it.moveNext(); i++) { 5 | if (0 < i) buffer.write('\n'); 6 | if (offset != null) { 7 | buffer.write(' ${offset + i}: '); 8 | } else { 9 | buffer.write(' - '); 10 | } 11 | buffer.write(it.current); 12 | } 13 | return buffer.toString(); 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/parser/character/word.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/parser.dart'; 4 | import '../predicate/character.dart'; 5 | import 'predicate/word.dart'; 6 | 7 | /// Returns a parser that accepts any word character (lowercase, uppercase, 8 | /// underscore, or digit). The accepted input is equivalent to the character-set 9 | /// `a-zA-Z_0-9`. 10 | @useResult 11 | Parser word({String message = 'letter or digit expected'}) => 12 | CharacterParser(const WordCharPredicate(), message); 13 | -------------------------------------------------------------------------------- /lib/src/parser/character/predicate/char.dart: -------------------------------------------------------------------------------- 1 | import '../predicate.dart'; 2 | 3 | class SingleCharPredicate extends CharacterPredicate { 4 | const SingleCharPredicate(this.charCode); 5 | 6 | final int charCode; 7 | 8 | @override 9 | bool test(int charCode) => identical(this.charCode, charCode); 10 | 11 | @override 12 | bool isEqualTo(CharacterPredicate other) => 13 | other is SingleCharPredicate && charCode == other.charCode; 14 | 15 | @override 16 | String toString() => '${super.toString()}($charCode)'; 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/parser/character/any.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/parser.dart'; 4 | import '../predicate/character.dart'; 5 | import 'predicate/constant.dart'; 6 | 7 | /// Returns a parser that accepts any character. 8 | /// 9 | /// For example, `any()` succeeds and consumes any given letter. It only 10 | /// fails for an empty input. 11 | @useResult 12 | Parser any({String message = 'input expected', bool unicode = false}) => 13 | CharacterParser(ConstantCharPredicate.any, message, unicode: unicode); 14 | -------------------------------------------------------------------------------- /lib/src/parser/character/predicate/not.dart: -------------------------------------------------------------------------------- 1 | import '../predicate.dart'; 2 | 3 | class NotCharPredicate extends CharacterPredicate { 4 | const NotCharPredicate(this.predicate); 5 | 6 | final CharacterPredicate predicate; 7 | 8 | @override 9 | bool test(int charCode) => !predicate.test(charCode); 10 | 11 | @override 12 | bool isEqualTo(CharacterPredicate other) => 13 | other is NotCharPredicate && predicate.isEqualTo(other.predicate); 14 | 15 | @override 16 | String toString() => '${super.toString()}($predicate)'; 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/matcher/matches/matches_iterable.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | import 'package:meta/meta.dart'; 4 | 5 | import '../../core/parser.dart'; 6 | import 'matches_iterator.dart'; 7 | 8 | @immutable 9 | class MatchesIterable extends IterableBase { 10 | const MatchesIterable(this.parser, this.input, this.start, this.overlapping); 11 | 12 | final Parser parser; 13 | final String input; 14 | final int start; 15 | final bool overlapping; 16 | 17 | @override 18 | Iterator get iterator => 19 | MatchesIterator(parser, input, start, overlapping); 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/matcher/pattern/pattern_iterable.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | import 'package:meta/meta.dart'; 4 | 5 | import 'parser_match.dart'; 6 | import 'parser_pattern.dart'; 7 | import 'pattern_iterator.dart'; 8 | 9 | @immutable 10 | class PatternIterable extends IterableBase { 11 | const PatternIterable(this.pattern, this.input, this.start); 12 | 13 | final ParserPattern pattern; 14 | final String input; 15 | final int start; 16 | 17 | @override 18 | Iterator get iterator => 19 | PatternIterator(pattern, pattern.parser, input, start); 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/core/exception.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import 'result.dart'; 4 | 5 | /// An exception raised in case of a parse error. 6 | @immutable 7 | class ParserException implements FormatException { 8 | const ParserException(this.failure); 9 | 10 | final Failure failure; 11 | 12 | @override 13 | String get message => failure.message; 14 | 15 | @override 16 | int get offset => failure.position; 17 | 18 | @override 19 | String get source => failure.buffer; 20 | 21 | @override 22 | String toString() => '$runtimeType[${failure.toPositionString()}]: $message'; 23 | } 24 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: petitparser 2 | version: 7.0.1 3 | 4 | homepage: https://petitparser.github.io 5 | repository: https://github.com/petitparser/dart-petitparser 6 | description: A dynamic parser framework to build efficient grammars and parsers quickly. 7 | screenshots: 8 | - description: 'PetitParser' 9 | path: petitparser.png 10 | topics: 11 | - grammar 12 | - parser 13 | - parser-combinator 14 | - parsing 15 | - peg 16 | 17 | environment: 18 | sdk: ^3.8.0 19 | dependencies: 20 | meta: ^1.16.0 21 | collection: ^1.19.0 22 | dev_dependencies: 23 | lints: ^6.0.0 24 | test: ^1.26.0 25 | -------------------------------------------------------------------------------- /lib/src/parser/character/predicate/constant.dart: -------------------------------------------------------------------------------- 1 | import '../predicate.dart'; 2 | 3 | class ConstantCharPredicate extends CharacterPredicate { 4 | static const any = ConstantCharPredicate(true); 5 | static const none = ConstantCharPredicate(false); 6 | 7 | const ConstantCharPredicate(this.constant); 8 | 9 | final bool constant; 10 | 11 | @override 12 | bool test(int charCode) => constant; 13 | 14 | @override 15 | bool isEqualTo(CharacterPredicate other) => 16 | other is ConstantCharPredicate && constant == other.constant; 17 | 18 | @override 19 | String toString() => '${super.toString()}($constant)'; 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/parser/combinator/delegate.dart: -------------------------------------------------------------------------------- 1 | import '../../core/parser.dart'; 2 | 3 | /// An abstract parser that delegates to a parser of type [T] and returns a 4 | /// result of type [R]. 5 | abstract class DelegateParser extends Parser { 6 | DelegateParser(this.delegate); 7 | 8 | /// The parser this parser delegates to. 9 | Parser delegate; 10 | 11 | @override 12 | List get children => [delegate]; 13 | 14 | @override 15 | void replace(Parser source, Parser target) { 16 | super.replace(source, target); 17 | if (delegate == source) { 18 | delegate = target as Parser; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/parser/character/predicate/range.dart: -------------------------------------------------------------------------------- 1 | import '../predicate.dart'; 2 | 3 | class RangeCharPredicate extends CharacterPredicate { 4 | const RangeCharPredicate(this.start, this.stop) 5 | : assert(start <= stop, 'Invalid range character range: $start-$stop'); 6 | 7 | final int start; 8 | final int stop; 9 | 10 | @override 11 | bool test(int charCode) => start <= charCode && charCode <= stop; 12 | 13 | @override 14 | bool isEqualTo(CharacterPredicate other) => 15 | other is RangeCharPredicate && start == other.start && stop == other.stop; 16 | 17 | @override 18 | String toString() => '${super.toString()}($start, $stop)'; 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/parser/combinator/list.dart: -------------------------------------------------------------------------------- 1 | import '../../core/parser.dart'; 2 | 3 | /// Abstract parser that parses a list of things in some way. 4 | abstract class ListParser extends Parser { 5 | ListParser(Iterable> children) 6 | : children = List>.of(children, growable: false); 7 | 8 | /// The children parsers being delegated to. 9 | @override 10 | final List> children; 11 | 12 | @override 13 | void replace(Parser source, Parser target) { 14 | super.replace(source, target); 15 | for (var i = 0; i < children.length; i++) { 16 | if (children[i] == source) { 17 | children[i] = target as Parser; 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/parser/misc/position.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/parser.dart'; 5 | import '../../core/result.dart'; 6 | 7 | /// Returns a parser that reports the current input position. 8 | @useResult 9 | Parser position() => PositionParser(); 10 | 11 | /// A parser that reports the current input position. 12 | class PositionParser extends Parser { 13 | PositionParser(); 14 | 15 | @override 16 | Result parseOn(Context context) => context.success(context.position); 17 | 18 | @override 19 | int fastParseOn(String buffer, int position) => position; 20 | 21 | @override 22 | PositionParser copy() => PositionParser(); 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/parser/character/range.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/parser.dart'; 4 | import '../predicate/character.dart'; 5 | import 'predicate/range.dart'; 6 | import 'utils/code.dart'; 7 | 8 | /// Returns a parser that accepts any character in the range 9 | /// between [start] and [stop]. 10 | @useResult 11 | Parser range( 12 | String start, 13 | String stop, { 14 | String? message, 15 | bool unicode = false, 16 | }) => CharacterParser( 17 | RangeCharPredicate( 18 | toCharCode(start, unicode: unicode), 19 | toCharCode(stop, unicode: unicode), 20 | ), 21 | message ?? 22 | '[${toReadableString(start, unicode: unicode)}-' 23 | '${toReadableString(stop, unicode: unicode)}] expected', 24 | unicode: unicode, 25 | ); 26 | -------------------------------------------------------------------------------- /lib/src/parser/character/any_of.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/parser.dart'; 4 | import '../predicate/character.dart'; 5 | import 'utils/code.dart'; 6 | import 'utils/optimize.dart'; 7 | 8 | /// Returns a parser that accepts any of the specified characters in [value]. 9 | @useResult 10 | Parser anyOf( 11 | String value, { 12 | String? message, 13 | bool ignoreCase = false, 14 | bool unicode = false, 15 | }) { 16 | final predicate = optimizedString( 17 | value, 18 | ignoreCase: ignoreCase, 19 | unicode: unicode, 20 | ); 21 | message ??= 22 | 'any of "${toReadableString(value, unicode: unicode)}"' 23 | '${ignoreCase ? ' (case-insensitive)' : ''} expected'; 24 | return CharacterParser(predicate, message, unicode: unicode); 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/matcher/pattern/parser_match.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import 'parser_pattern.dart'; 4 | 5 | @immutable 6 | class ParserMatch implements Match { 7 | const ParserMatch(this.pattern, this.input, this.start, this.end); 8 | 9 | @override 10 | final ParserPattern pattern; 11 | 12 | @override 13 | final String input; 14 | 15 | @override 16 | final int start; 17 | 18 | @override 19 | final int end; 20 | 21 | @override 22 | String? group(int group) => this[group]; 23 | 24 | @override 25 | String? operator [](int group) => 26 | group == 0 ? input.substring(start, end) : null; 27 | 28 | @override 29 | List groups(List groupIndices) => 30 | groupIndices.map(group).toList(growable: false); 31 | 32 | @override 33 | int get groupCount => 0; 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/parser/character/none_of.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/parser.dart'; 4 | import '../predicate/character.dart'; 5 | import 'predicate/not.dart'; 6 | import 'utils/code.dart'; 7 | import 'utils/optimize.dart'; 8 | 9 | /// Returns a parser that accepts none of the specified characters in [value]. 10 | @useResult 11 | Parser noneOf( 12 | String value, { 13 | String? message, 14 | bool ignoreCase = false, 15 | bool unicode = false, 16 | }) { 17 | final predicate = NotCharPredicate( 18 | optimizedString(value, ignoreCase: ignoreCase, unicode: unicode), 19 | ); 20 | message ??= 21 | 'none of "${toReadableString(value, unicode: unicode)}"' 22 | '${ignoreCase ? ' (case-insensitive)' : ''} expected'; 23 | return CharacterParser(predicate, message, unicode: unicode); 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/shared/pragma.dart: -------------------------------------------------------------------------------- 1 | /// True, if the code is running in JavaScript. 2 | const isJavaScript = identical(1, 1.0); 3 | 4 | /// True, if the code is running in WASM. 5 | const isWasm = bool.fromEnvironment('dart.tool.dart2wasm'); 6 | 7 | /// Inline a function or method when possible. 8 | const preferInline = isJavaScript 9 | ? preferInlineJs 10 | : isWasm 11 | ? preferInlineWasm 12 | : preferInlineVm; 13 | const preferInlineJs = pragma('dart2js:prefer-inline'); 14 | const preferInlineVm = pragma('vm:prefer-inline'); 15 | const preferInlineWasm = pragma('wasm:prefer-inline'); 16 | 17 | /// Removes all array bounds checks. 18 | const noBoundsChecks = isJavaScript ? noBoundsChecksJs : noBoundsChecksVm; 19 | const noBoundsChecksJs = pragma('dart2js:index-bounds:trust'); 20 | const noBoundsChecksVm = pragma('vm:unsafe:no-bounds-checks'); 21 | -------------------------------------------------------------------------------- /lib/src/matcher/matches/matches_iterator.dart: -------------------------------------------------------------------------------- 1 | import '../../core/context.dart'; 2 | import '../../core/parser.dart'; 3 | 4 | class MatchesIterator implements Iterator { 5 | MatchesIterator(this.parser, this.input, this.start, this.overlapping); 6 | 7 | final Parser parser; 8 | final String input; 9 | final bool overlapping; 10 | 11 | int start; 12 | 13 | @override 14 | late R current; 15 | 16 | @override 17 | bool moveNext() { 18 | while (start <= input.length) { 19 | final end = parser.fastParseOn(input, start); 20 | if (end < 0) { 21 | start++; 22 | } else { 23 | current = parser.parseOn(Context(input, start)).value; 24 | if (overlapping || start == end) { 25 | start++; 26 | } else { 27 | start = end; 28 | } 29 | return true; 30 | } 31 | } 32 | return false; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/expression/result.dart: -------------------------------------------------------------------------------- 1 | /// Encapsulates a prefix operation. 2 | class ExpressionResultPrefix { 3 | ExpressionResultPrefix(this.operator, this.callback); 4 | 5 | final O operator; 6 | final V Function(O operator, V value) callback; 7 | 8 | V call(V value) => callback(operator, value); 9 | } 10 | 11 | /// Encapsulates a postfix operation. 12 | class ExpressionResultPostfix { 13 | ExpressionResultPostfix(this.operator, this.callback); 14 | 15 | final O operator; 16 | final V Function(V value, O operator) callback; 17 | 18 | V call(V value) => callback(value, operator); 19 | } 20 | 21 | /// Encapsulates a infix operation. 22 | class ExpressionResultInfix { 23 | ExpressionResultInfix(this.operator, this.callback); 24 | 25 | final O operator; 26 | final V Function(V left, O operator, V right) callback; 27 | 28 | V call(V left, V right) => callback(left, operator, right); 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/matcher/pattern/pattern_iterator.dart: -------------------------------------------------------------------------------- 1 | import '../../core/parser.dart'; 2 | import 'parser_match.dart'; 3 | import 'parser_pattern.dart'; 4 | 5 | class PatternIterator implements Iterator { 6 | PatternIterator(this.pattern, this.parser, this.input, this.start); 7 | 8 | final ParserPattern pattern; 9 | final Parser parser; 10 | final String input; 11 | int start; 12 | 13 | @override 14 | late ParserMatch current; 15 | 16 | @override 17 | bool moveNext() { 18 | while (start <= input.length) { 19 | final end = parser.fastParseOn(input, start); 20 | if (end < 0) { 21 | start++; 22 | } else { 23 | current = ParserMatch(pattern, input, start, end); 24 | if (start == end) { 25 | start++; 26 | } else { 27 | start = end; 28 | } 29 | return true; 30 | } 31 | } 32 | return false; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/parser/repeater/repeating.dart: -------------------------------------------------------------------------------- 1 | import '../combinator/delegate.dart'; 2 | import 'unbounded.dart'; 3 | 4 | /// An abstract parser that repeatedly parses between 'min' and 'max' instances 5 | /// of its delegate. 6 | abstract class RepeatingParser extends DelegateParser { 7 | RepeatingParser(super.parser, this.min, this.max) 8 | : assert(0 <= min, 'min must be at least 0, but got $min'), 9 | assert(min <= max, 'max must be at least $min, but got $max'); 10 | 11 | /// The minimum amount of repetitions. 12 | final int min; 13 | 14 | /// The maximum amount of repetitions, or [unbounded]. 15 | final int max; 16 | 17 | @override 18 | String toString() => 19 | '${super.toString()}[$min..${max == unbounded ? '*' : max}]'; 20 | 21 | @override 22 | bool hasEqualProperties(RepeatingParser other) => 23 | super.hasEqualProperties(other) && min == other.min && max == other.max; 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/parser/repeater/limited.dart: -------------------------------------------------------------------------------- 1 | import '../../core/parser.dart'; 2 | import 'repeating.dart'; 3 | 4 | /// An abstract parser that repeatedly parses between 'min' and 'max' instances 5 | /// of its delegate and that requires the input to be completed with a specified 6 | /// parser 'limit'. Subclasses provide repeating behavior as typically seen in 7 | /// regular expression implementations (non-blind). 8 | abstract class LimitedRepeatingParser extends RepeatingParser> { 9 | LimitedRepeatingParser(Parser delegate, this.limit, int min, int max) 10 | : super(delegate, min, max); 11 | 12 | /// Parser restraining further consumption of the delegate parser. 13 | Parser limit; 14 | 15 | @override 16 | List get children => [delegate, limit]; 17 | 18 | @override 19 | void replace(Parser source, Parser target) { 20 | super.replace(source, target); 21 | if (limit == source) { 22 | limit = target; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/parser/action/cast.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/parser.dart'; 5 | import '../../core/result.dart'; 6 | import '../combinator/delegate.dart'; 7 | 8 | extension CastParserExtension on Parser { 9 | /// Returns a parser that casts itself to `Parser`. 10 | @useResult 11 | Parser cast() => CastParser(this); 12 | } 13 | 14 | /// A parser that casts a `Result` to a `Result`. 15 | class CastParser extends DelegateParser { 16 | CastParser(super.delegate); 17 | 18 | @override 19 | Result parseOn(Context context) { 20 | final result = delegate.parseOn(context); 21 | if (result is Failure) return result; 22 | return result.success(result.value as S); 23 | } 24 | 25 | @override 26 | int fastParseOn(String buffer, int position) => 27 | delegate.fastParseOn(buffer, position); 28 | 29 | @override 30 | CastParser copy() => CastParser(delegate); 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/parser/predicate/string.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart' show equalsIgnoreAsciiCase; 2 | import 'package:meta/meta.dart'; 3 | 4 | import '../../core/parser.dart'; 5 | import 'predicate.dart'; 6 | 7 | /// Returns a parser that accepts the [string]. 8 | /// 9 | /// - [message] defines a custom error message. 10 | /// - If [ignoreCase] is `true`, the string is matched in a case-insensitive 11 | /// manner. 12 | /// 13 | /// For example, `string('foo')` succeeds and consumes the input string 14 | /// `'foo'`. Fails for any other input. 15 | @useResult 16 | Parser string( 17 | String string, { 18 | String? message, 19 | bool ignoreCase = false, 20 | }) => ignoreCase 21 | ? predicate( 22 | string.length, 23 | (value) => equalsIgnoreAsciiCase(string, value), 24 | message ?? '"$string" (case-insensitive) expected', 25 | ) 26 | : predicate( 27 | string.length, 28 | (value) => string == value, 29 | message ?? '"$string" expected', 30 | ); 31 | -------------------------------------------------------------------------------- /lib/src/matcher/matches.dart: -------------------------------------------------------------------------------- 1 | import '../core/parser.dart'; 2 | import 'matches/matches_iterable.dart'; 3 | 4 | extension MatchesParserExtension on Parser { 5 | /// Returns a _lazy iterable_ over all non-overlapping successful parse 6 | /// results of type [T] over the provided [input]. 7 | /// 8 | /// If [start] is provided, parsing will start at that index. 9 | /// 10 | /// If [overlapping] is set to `true`, the parsing is attempted at each input 11 | /// position and does not skip over previous matches. 12 | /// 13 | /// For example, with the parser 14 | /// 15 | /// ```dart 16 | /// final parser = letter().plus().flatten(); 17 | /// ``` 18 | /// 19 | /// `parser.allMatches('abc de')` results in the iterable `['abc', 'de']`; and 20 | /// `parser.allMatches('abc de', overlapping: true)` results in the iterable 21 | /// `['abc', 'bc', 'c', 'de', 'e']`. 22 | Iterable allMatches( 23 | String input, { 24 | int start = 0, 25 | bool overlapping = false, 26 | }) => MatchesIterable(this, input, start, overlapping); 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/parser/utils/failure_joiner.dart: -------------------------------------------------------------------------------- 1 | import '../../core/result.dart'; 2 | 3 | /// Function definition that joins parse [Failure] instances. 4 | typedef FailureJoiner = Failure Function(Failure first, Failure second); 5 | 6 | /// Reports the first parse failure observed. 7 | Failure selectFirst(Failure first, Failure second) => first; 8 | 9 | /// Reports the last parse failure observed (default). 10 | Failure selectLast(Failure first, Failure second) => second; 11 | 12 | /// Reports the parser failure farthest down in the input string, preferring 13 | /// later failures over earlier ones. 14 | Failure selectFarthest(Failure first, Failure second) => 15 | first.position <= second.position ? second : first; 16 | 17 | /// Reports the parser failure farthest down in the input string, joining 18 | /// error messages at the same position. 19 | Failure selectFarthestJoined(Failure first, Failure second) => 20 | first.position > second.position 21 | ? first 22 | : first.position < second.position 23 | ? second 24 | : first.failure('${first.message} OR ${second.message}'); 25 | -------------------------------------------------------------------------------- /lib/src/parser/character/utils/code.dart: -------------------------------------------------------------------------------- 1 | /// Converts an string to a character code. 2 | int toCharCode(String value, {required bool unicode}) { 3 | final codes = unicode ? value.runes : value.codeUnits; 4 | assert(codes.length == 1, '"$value" is not a valid character'); 5 | return codes.single; 6 | } 7 | 8 | /// Converts a character to a readable string. 9 | String toReadableString(String value, {required bool unicode}) { 10 | final codePoints = unicode ? value.runes : value.codeUnits; 11 | return codePoints.map((int code) { 12 | if (_escapedChars[code] case final value?) return value; 13 | if (code < 0x20) return '\\x${code.toRadixString(16).padLeft(2, '0')}'; 14 | return String.fromCharCode(code); 15 | }).join(); 16 | } 17 | 18 | const _escapedChars = { 19 | 0x08: r'\b', // backspace 20 | 0x09: r'\t', // horizontal tab 21 | 0x0a: r'\n', // new line 22 | 0x0b: r'\v', // vertical tab 23 | 0x0c: r'\f', // form feed 24 | 0x0d: r'\r', // carriage return 25 | 0x22: r'\"', // double quote 26 | 0x27: r"\'", // single quote 27 | 0x5c: r'\\', // backslash 28 | }; 29 | -------------------------------------------------------------------------------- /lib/src/core/context.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../shared/pragma.dart'; 4 | import 'result.dart'; 5 | import 'token.dart'; 6 | 7 | /// An immutable parse context. 8 | @immutable 9 | class Context { 10 | @preferInline 11 | const Context(this.buffer, this.position); 12 | 13 | /// The buffer we are working on. 14 | final String buffer; 15 | 16 | /// The current position in the [buffer]. 17 | final int position; 18 | 19 | /// Returns a result indicating a parse success. 20 | @useResult 21 | @preferInline 22 | Success success(R result, [int? position]) => 23 | Success(buffer, position ?? this.position, result); 24 | 25 | /// Returns a result indicating a parse failure. 26 | @useResult 27 | @preferInline 28 | Failure failure(String message, [int? position]) => 29 | Failure(buffer, position ?? this.position, message); 30 | 31 | /// Returns the current line:column position in the [buffer]. 32 | String toPositionString() => Token.positionString(buffer, position); 33 | 34 | @override 35 | String toString() => '$runtimeType[${toPositionString()}]'; 36 | } 37 | -------------------------------------------------------------------------------- /lib/src/parser/action/cast_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/parser.dart'; 5 | import '../../core/result.dart'; 6 | import '../combinator/delegate.dart'; 7 | 8 | extension CastListParserExtension on Parser { 9 | /// Returns a parser that casts itself to `Parser>`. Assumes this 10 | /// parser to be of type `Parser`. 11 | @useResult 12 | Parser> castList() => CastListParser(this); 13 | } 14 | 15 | /// A parser that casts a `Result` to a `Result>`. 16 | class CastListParser extends DelegateParser> { 17 | CastListParser(super.delegate); 18 | 19 | @override 20 | Result> parseOn(Context context) { 21 | final result = delegate.parseOn(context); 22 | if (result is Failure) return result; 23 | return result.success(List.castFrom(result.value as List)); 24 | } 25 | 26 | @override 27 | int fastParseOn(String buffer, int position) => 28 | delegate.fastParseOn(buffer, position); 29 | 30 | @override 31 | CastListParser copy() => CastListParser(delegate); 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/parser/misc/failure.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/parser.dart'; 5 | import '../../core/result.dart'; 6 | 7 | /// Returns a parser that consumes nothing and fails. 8 | /// 9 | /// For example, `failure()` always fails, no matter what input it is given. 10 | @useResult 11 | Parser failure({String message = 'unable to parse'}) => 12 | FailureParser(message); 13 | 14 | /// A parser that consumes nothing and fails. 15 | class FailureParser extends Parser { 16 | FailureParser(this.message); 17 | 18 | /// Error message to annotate parse failures with. 19 | final String message; 20 | 21 | @override 22 | Result parseOn(Context context) => context.failure(message); 23 | 24 | @override 25 | int fastParseOn(String buffer, int position) => -1; 26 | 27 | @override 28 | String toString() => '${super.toString()}[$message]'; 29 | 30 | @override 31 | FailureParser copy() => FailureParser(message); 32 | 33 | @override 34 | bool hasEqualProperties(FailureParser other) => 35 | super.hasEqualProperties(other) && message == other.message; 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2006-2024 Lukas Renggli. 4 | All rights reserved. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/src/parser/character/char.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/parser.dart'; 4 | import '../predicate/character.dart'; 5 | import 'predicate/char.dart'; 6 | import 'utils/code.dart'; 7 | import 'utils/optimize.dart'; 8 | 9 | /// Returns a parser that accepts a specific character [value]. 10 | /// 11 | /// - [message] defines a custom error message. 12 | /// - If [ignoreCase] is `true`, the character is matched in a case-insensitive 13 | /// manner. 14 | /// - If [unicode] is `true`, the character is matched using full unicode 15 | /// character parsing (as opposed to UTF-16 code units). 16 | @useResult 17 | Parser char( 18 | String value, { 19 | String? message, 20 | bool ignoreCase = false, 21 | bool unicode = false, 22 | }) { 23 | final charCode = toCharCode(value, unicode: unicode); 24 | final predicate = ignoreCase 25 | ? optimizedString(value, ignoreCase: ignoreCase, unicode: unicode) 26 | : SingleCharPredicate(charCode); 27 | message ??= 28 | '"${toReadableString(value, unicode: unicode)}"' 29 | '${ignoreCase ? ' (case-insensitive)' : ''} expected'; 30 | return CharacterParser(predicate, message, unicode: unicode); 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/parser/character/predicate/whitespace.dart: -------------------------------------------------------------------------------- 1 | import '../predicate.dart'; 2 | 3 | class WhitespaceCharPredicate extends CharacterPredicate { 4 | const WhitespaceCharPredicate(); 5 | 6 | @override 7 | bool test(int charCode) { 8 | if (charCode < 256) { 9 | switch (charCode) { 10 | case 0x09: 11 | case 0x0A: 12 | case 0x0B: 13 | case 0x0C: 14 | case 0x0D: 15 | case 0x20: 16 | case 0x85: 17 | case 0xA0: 18 | return true; 19 | default: 20 | return false; 21 | } 22 | } 23 | switch (charCode) { 24 | case 0x1680: 25 | case 0x2000: 26 | case 0x2001: 27 | case 0x2002: 28 | case 0x2003: 29 | case 0x2004: 30 | case 0x2005: 31 | case 0x2006: 32 | case 0x2007: 33 | case 0x2008: 34 | case 0x2009: 35 | case 0x200A: 36 | case 0x2028: 37 | case 0x2029: 38 | case 0x202F: 39 | case 0x205F: 40 | case 0x3000: 41 | case 0xFEFF: 42 | return true; 43 | default: 44 | return false; 45 | } 46 | } 47 | 48 | @override 49 | bool isEqualTo(CharacterPredicate other) => other is WhitespaceCharPredicate; 50 | } 51 | -------------------------------------------------------------------------------- /lib/src/parser/misc/epsilon.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/parser.dart'; 5 | import '../../core/result.dart'; 6 | 7 | /// Returns a parser that consumes nothing and succeeds. 8 | /// 9 | /// For example, `char('a').or(epsilon())` is equivalent to 10 | /// `char('a').optional()`. 11 | @useResult 12 | Parser epsilon() => epsilonWith(null); 13 | 14 | /// Returns a parser that consumes nothing and succeeds with [result]. 15 | @useResult 16 | Parser epsilonWith(R result) => EpsilonParser(result); 17 | 18 | /// A parser that consumes nothing and succeeds. 19 | class EpsilonParser extends Parser { 20 | EpsilonParser(this.result); 21 | 22 | /// Value to be returned when the parser is activated. 23 | final R result; 24 | 25 | @override 26 | Result parseOn(Context context) => context.success(result); 27 | 28 | @override 29 | int fastParseOn(String buffer, int position) => position; 30 | 31 | @override 32 | String toString() => '${super.toString()}[$result]'; 33 | 34 | @override 35 | EpsilonParser copy() => EpsilonParser(result); 36 | 37 | @override 38 | bool hasEqualProperties(EpsilonParser other) => 39 | super.hasEqualProperties(other) && result == other.result; 40 | } 41 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | analyzer: 3 | language: 4 | strict-casts: true 5 | strict-inference: true 6 | strict-raw-types: true 7 | linter: 8 | rules: 9 | - annotate_redeclares 10 | - avoid_dynamic_calls 11 | - avoid_final_parameters 12 | - avoid_print 13 | - avoid_unused_constructor_parameters 14 | - combinators_ordering 15 | - comment_references 16 | - directives_ordering 17 | - invalid_case_patterns 18 | - missing_code_block_language_in_doc_comment 19 | - no_self_assignments 20 | - omit_local_variable_types 21 | - prefer_const_constructors 22 | - prefer_const_constructors_in_immutables 23 | - prefer_const_declarations 24 | - prefer_const_literals_to_create_immutables 25 | - prefer_expression_function_bodies 26 | - prefer_final_in_for_each 27 | - prefer_final_locals 28 | - prefer_if_elements_to_conditional_expressions 29 | - prefer_relative_imports 30 | - prefer_single_quotes 31 | - remove_deprecations_in_breaking_versions 32 | - unnecessary_await_in_return 33 | - unnecessary_breaks 34 | - unnecessary_lambdas 35 | - unnecessary_null_aware_operator_on_extension_on_nullable 36 | - unnecessary_null_checks 37 | - unnecessary_parenthesis 38 | - unnecessary_statements 39 | - unreachable_from_main -------------------------------------------------------------------------------- /lib/src/reflection/transform.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../core/parser.dart'; 4 | import 'iterable.dart'; 5 | 6 | /// A function transforming one parser to another one. 7 | typedef TransformationHandler = Parser Function(Parser parser); 8 | 9 | /// Transforms all parsers reachable from [parser] with the given [handler]. 10 | /// The identity function returns a copy of the the incoming parser. 11 | /// 12 | /// The implementation first creates a copy of each parser reachable in the 13 | /// input grammar; then the resulting grammar is traversed until all references 14 | /// to old parsers are replaced with the transformed ones. 15 | @useResult 16 | Parser transformParser(Parser parser, TransformationHandler handler) { 17 | final mapping = Map.identity(); 18 | for (final each in allParser(parser)) { 19 | mapping[each] = each.copy().captureResultGeneric(handler); 20 | } 21 | final todo = [...mapping.values]; 22 | final seen = {...mapping.values}; 23 | while (todo.isNotEmpty) { 24 | final parent = todo.removeLast(); 25 | for (final child in parent.children) { 26 | if (mapping.containsKey(child)) { 27 | parent.replace(child, mapping[child]!); 28 | } else if (seen.add(child)) { 29 | todo.add(child); 30 | } 31 | } 32 | } 33 | return mapping[parser] as Parser; 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/parser/misc/label.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/parser.dart'; 5 | import '../../core/result.dart'; 6 | import '../../parser/utils/labeled.dart'; 7 | import '../combinator/delegate.dart'; 8 | 9 | extension LabelParserExtension on Parser { 10 | /// Returns a parser that simply defers to its delegate, but that 11 | /// has a [label] for debugging purposes. 12 | @useResult 13 | LabeledParser labeled(String label) => LabelParser(this, label); 14 | } 15 | 16 | /// A parser that always defers to its delegate, but that also holds a label 17 | /// for debugging purposes. 18 | class LabelParser extends DelegateParser implements LabeledParser { 19 | LabelParser(super.delegate, this.label); 20 | 21 | /// Label of this parser. 22 | @override 23 | final String label; 24 | 25 | @override 26 | Result parseOn(Context context) => delegate.parseOn(context); 27 | 28 | @override 29 | int fastParseOn(String buffer, int position) => 30 | delegate.fastParseOn(buffer, position); 31 | 32 | @override 33 | String toString() => '${super.toString()}[$label]'; 34 | 35 | @override 36 | LabelParser copy() => LabelParser(delegate, label); 37 | 38 | @override 39 | bool hasEqualProperties(LabelParser other) => 40 | super.hasEqualProperties(other) && label == other.label; 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/reflection/iterable.dart: -------------------------------------------------------------------------------- 1 | import '../core/parser.dart'; 2 | 3 | /// Returns a lazy iterable over all parsers reachable from [root] using 4 | /// a [depth-first traversal](https://en.wikipedia.org/wiki/Depth-first_search) 5 | /// over the connected parser graph. 6 | /// 7 | /// For example, the following code prints the two parsers of the 8 | /// defined grammar: 9 | /// 10 | /// ```dart 11 | /// final parser = range('0', '9').star(); 12 | /// allParser(parser).forEach((each) { 13 | /// print(each); 14 | /// }); 15 | /// ``` 16 | Iterable allParser(Parser root) => _ParserIterable(root); 17 | 18 | class _ParserIterable extends Iterable { 19 | _ParserIterable(this.root); 20 | 21 | final Parser root; 22 | 23 | @override 24 | Iterator get iterator => _ParserIterator(root); 25 | } 26 | 27 | class _ParserIterator implements Iterator { 28 | _ParserIterator(Parser root) : todo = [root], seen = {root}; 29 | 30 | final List todo; 31 | final Set seen; 32 | 33 | @override 34 | late Parser current; 35 | 36 | @override 37 | bool moveNext() { 38 | if (todo.isEmpty) { 39 | seen.clear(); 40 | return false; 41 | } 42 | current = todo.removeLast(); 43 | for (final parser in current.children.reversed) { 44 | if (seen.add(parser)) { 45 | todo.add(parser); 46 | } 47 | } 48 | return true; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/src/core/result.dart: -------------------------------------------------------------------------------- 1 | import '../shared/pragma.dart'; 2 | import 'context.dart'; 3 | import 'exception.dart'; 4 | 5 | /// An immutable parse result that is either a [Success] or a [Failure]. 6 | sealed class Result extends Context { 7 | @preferInline 8 | const Result(super.buffer, super.position); 9 | 10 | /// Returns the parsed value of this result, or throws a [ParserException] 11 | /// if this is a parse failure. 12 | R get value; 13 | 14 | /// Returns the error message of this result, or throws an [UnsupportedError] 15 | /// if this is a parse success. 16 | String get message; 17 | } 18 | 19 | /// An immutable successful parse result. 20 | class Success extends Result { 21 | @preferInline 22 | const Success(super.buffer, super.position, this.value); 23 | 24 | @override 25 | final R value; 26 | 27 | @override 28 | String get message => 29 | throw UnsupportedError('Successful parse results do not have a message.'); 30 | 31 | @override 32 | String toString() => '${super.toString()}: $value'; 33 | } 34 | 35 | /// An immutable failed parse result. 36 | class Failure extends Result { 37 | @preferInline 38 | const Failure(super.buffer, super.position, this.message); 39 | 40 | @override 41 | Never get value => throw ParserException(this); 42 | 43 | @override 44 | final String message; 45 | 46 | @override 47 | String toString() => '${super.toString()}: $message'; 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/parser/combinator/and.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/parser.dart'; 5 | import '../../core/result.dart'; 6 | import 'delegate.dart'; 7 | 8 | extension AndParserExtension on Parser { 9 | /// Returns a parser (logical and-predicate) that succeeds whenever the 10 | /// receiver does, but never consumes input. 11 | /// 12 | /// For example, the parser `char('_').and().seq(identifier)` accepts 13 | /// identifiers that start with an underscore character. Since the predicate 14 | /// does not consume accepted input, the parser `identifier` is given the 15 | /// ability to process the complete identifier. 16 | @useResult 17 | Parser and() => AndParser(this); 18 | } 19 | 20 | /// The and-predicate, a parser that succeeds whenever its delegate does, but 21 | /// does not consume the input stream. 22 | class AndParser extends DelegateParser { 23 | AndParser(super.delegate); 24 | 25 | @override 26 | Result parseOn(Context context) { 27 | final result = delegate.parseOn(context); 28 | if (result is Failure) return result; 29 | return context.success(result.value); 30 | } 31 | 32 | @override 33 | int fastParseOn(String buffer, int position) { 34 | final result = delegate.fastParseOn(buffer, position); 35 | return result < 0 ? -1 : position; 36 | } 37 | 38 | @override 39 | AndParser copy() => AndParser(delegate); 40 | } 41 | -------------------------------------------------------------------------------- /lib/src/matcher/pattern/parser_pattern.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/parser.dart'; 4 | import 'parser_match.dart'; 5 | import 'pattern_iterable.dart'; 6 | 7 | @immutable 8 | class ParserPattern implements Pattern { 9 | const ParserPattern(this.parser); 10 | 11 | final Parser parser; 12 | 13 | /// Matches this parser against [string] repeatedly. 14 | /// 15 | /// If [start] is provided, matching will start at that index. The returned 16 | /// iterable lazily computes all the non-overlapping matches of the parser on 17 | /// the string, ordered by start index. 18 | /// 19 | /// If the pattern matches the empty string at some point, the next match is 20 | /// found by starting at the previous match's end plus one. 21 | @override 22 | Iterable allMatches(String string, [int start = 0]) => 23 | PatternIterable(this, string, start); 24 | 25 | /// Match this pattern against the start of [string]. 26 | /// 27 | /// If [start] is provided, this parser is tested against the string at the 28 | /// [start] position. That is, a [Match] is returned if the pattern can match 29 | /// a part of the string starting from position [start]. 30 | /// 31 | /// Returns `null` if the pattern doesn't match. 32 | @override 33 | ParserMatch? matchAsPrefix(String string, [int start = 0]) { 34 | final end = parser.fastParseOn(string, start); 35 | return end < 0 ? null : ParserMatch(this, string, start, end); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/parser/predicate/pattern.dart: -------------------------------------------------------------------------------- 1 | import '../../core/context.dart'; 2 | import '../../core/parser.dart'; 3 | import '../../core/result.dart'; 4 | 5 | /// A parser that uses a [Pattern] matcher for parsing. 6 | /// 7 | /// This parser wraps [Pattern.matchAsPrefix] in a [Parser]. This works for 8 | /// any implementation of [Pattern], but can lead to very inefficient parsers 9 | /// when not used carefully. 10 | class PatternParser extends Parser { 11 | PatternParser(this.pattern, this.message); 12 | 13 | /// The [Pattern] matcher this parser uses. 14 | final Pattern pattern; 15 | 16 | /// Error message to annotate parse failures with. 17 | final String message; 18 | 19 | @override 20 | Result parseOn(Context context) { 21 | final result = pattern.matchAsPrefix(context.buffer, context.position); 22 | if (result == null) return context.failure(message); 23 | return context.success(result, result.end); 24 | } 25 | 26 | @override 27 | int fastParseOn(String buffer, int position) { 28 | final result = pattern.matchAsPrefix(buffer, position); 29 | return result == null ? -1 : result.end; 30 | } 31 | 32 | @override 33 | String toString() => '${super.toString()}[$message]'; 34 | 35 | @override 36 | PatternParser copy() => PatternParser(pattern, message); 37 | 38 | @override 39 | bool hasEqualProperties(PatternParser other) => 40 | super.hasEqualProperties(other) && 41 | pattern == other.pattern && 42 | message == other.message; 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/parser/action/token.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/parser.dart'; 5 | import '../../core/result.dart'; 6 | import '../../core/token.dart'; 7 | import '../combinator/delegate.dart'; 8 | 9 | extension TokenParserExtension on Parser { 10 | /// Returns a parser that returns a [Token]. The token carries the parsed 11 | /// value of the receiver [Token.value], as well as the consumed input 12 | /// [Token.input] from [Token.start] to [Token.stop] of the input being 13 | /// parsed. 14 | /// 15 | /// For example, the parser `letter().plus().token()` returns the token 16 | /// `Token[start: 0, stop: 3, value: abc]` for the input `'abc'`. 17 | @useResult 18 | Parser> token() => TokenParser(this); 19 | } 20 | 21 | /// A parser that creates a token of the result its delegate parses. 22 | class TokenParser extends DelegateParser> { 23 | TokenParser(super.delegate); 24 | 25 | @override 26 | Result> parseOn(Context context) { 27 | final result = delegate.parseOn(context); 28 | if (result is Failure) return result; 29 | final token = Token( 30 | result.value, 31 | context.buffer, 32 | context.position, 33 | result.position, 34 | ); 35 | return result.success(token); 36 | } 37 | 38 | @override 39 | int fastParseOn(String buffer, int position) => 40 | delegate.fastParseOn(buffer, position); 41 | 42 | @override 43 | TokenParser copy() => TokenParser(delegate); 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/parser/character/predicate/ranges.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:collection/collection.dart' show ListEquality; 4 | 5 | import '../../../shared/pragma.dart'; 6 | import '../predicate.dart'; 7 | import 'range.dart'; 8 | 9 | class RangesCharPredicate extends CharacterPredicate { 10 | RangesCharPredicate.fromRanges(Iterable ranges) 11 | : ranges = Uint32List(size(ranges)) { 12 | var i = 0; 13 | for (final range in ranges) { 14 | this.ranges[i++] = range.start; 15 | this.ranges[i++] = range.stop; 16 | } 17 | } 18 | 19 | const RangesCharPredicate(this.ranges); 20 | 21 | final Uint32List ranges; 22 | 23 | @override 24 | @noBoundsChecks 25 | bool test(int charCode) { 26 | var min = 0; 27 | var max = ranges.length - 2; 28 | while (min <= max) { 29 | final mid = (min + ((max - min) >> 1)) & ~1; 30 | if (ranges[mid] <= charCode && charCode <= ranges[mid + 1]) { 31 | return true; 32 | } else if (charCode < ranges[mid]) { 33 | max = mid - 2; 34 | } else { 35 | min = mid + 2; 36 | } 37 | } 38 | return false; 39 | } 40 | 41 | @override 42 | bool isEqualTo(CharacterPredicate other) => 43 | other is RangesCharPredicate && 44 | _listEquality.equals(ranges, other.ranges); 45 | 46 | @override 47 | String toString() => '${super.toString()}($ranges)'; 48 | 49 | static int size(Iterable ranges) => 2 * ranges.length; 50 | } 51 | 52 | const _listEquality = ListEquality(); 53 | -------------------------------------------------------------------------------- /lib/src/reflection/internal/first_set.dart: -------------------------------------------------------------------------------- 1 | import '../../core/parser.dart'; 2 | import 'utilities.dart'; 3 | 4 | Map> computeFirstSets({ 5 | required Iterable parsers, 6 | required Parser sentinel, 7 | }) { 8 | final firstSets = { 9 | for (final parser in parsers) 10 | parser: { 11 | if (isTerminal(parser)) parser, 12 | if (isNullable(parser)) sentinel, 13 | }, 14 | }; 15 | var changed = false; 16 | do { 17 | changed = false; 18 | for (final parser in parsers) { 19 | changed |= expandFirstSet( 20 | parser: parser, 21 | firstSets: firstSets, 22 | sentinel: sentinel, 23 | ); 24 | } 25 | } while (changed); 26 | return firstSets; 27 | } 28 | 29 | bool expandFirstSet({ 30 | required Parser parser, 31 | required Map> firstSets, 32 | required Parser sentinel, 33 | }) { 34 | var changed = false; 35 | final firstSet = firstSets[parser]!; 36 | if (isSequence(parser)) { 37 | for (final child in parser.children) { 38 | var nullable = false; 39 | for (final first in firstSets[child]!) { 40 | if (isNullable(first)) { 41 | nullable = true; 42 | } else { 43 | changed |= firstSet.add(first); 44 | } 45 | } 46 | if (!nullable) { 47 | return changed; 48 | } 49 | } 50 | changed |= firstSet.add(sentinel); 51 | } else { 52 | for (final child in parser.children) { 53 | changed |= addAll(firstSet, firstSets[child]!); 54 | } 55 | } 56 | return changed; 57 | } 58 | -------------------------------------------------------------------------------- /.github/workflows/dart.yml: -------------------------------------------------------------------------------- 1 | name: Dart CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | schedule: 9 | - cron: "0 0 * * 0" 10 | workflow_dispatch: 11 | defaults: 12 | run: 13 | shell: bash 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | build: 19 | name: "Dart CI" 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/cache@v5 23 | with: 24 | path: "~/.pub-cache/hosted" 25 | key: "os:ubuntu-latest;pub-cache-hosted;dart:stable" 26 | - uses: actions/checkout@v6 27 | - uses: dart-lang/setup-dart@v1 28 | - name: "Dependencies" 29 | run: dart pub upgrade --no-precompile 30 | - name: "Dart Analyzer" 31 | run: dart analyze --fatal-infos . 32 | - name: "Dart Formatter" 33 | run: dart format --output=none --set-exit-if-changed . 34 | - name: "VM Tests" 35 | run: dart test --platform vm 36 | - name: "Chrome Tests" 37 | run: dart test --platform chrome 38 | - name: "Collect coverage" 39 | run: | 40 | dart pub global activate coverage 41 | dart pub global run coverage:test_with_coverage 42 | - name: "Upload coverage" 43 | uses: codecov/codecov-action@v5 44 | with: 45 | token: ${{ secrets.CODECOV_TOKEN }} 46 | check: 47 | name: "Flutter CI" 48 | runs-on: ubuntu-latest 49 | steps: 50 | - uses: actions/checkout@v6 51 | - uses: amake/dart-flutter-compat@v1 52 | with: 53 | channel: stable 54 | cache: true -------------------------------------------------------------------------------- /example/calc.dart: -------------------------------------------------------------------------------- 1 | /// Calculator from the tutorial. 2 | library; 3 | 4 | import 'dart:io'; 5 | import 'dart:math'; 6 | 7 | import 'package:petitparser/petitparser.dart'; 8 | 9 | Parser buildParser() { 10 | final builder = ExpressionBuilder(); 11 | builder.primitive( 12 | (pattern('+-').optional() & 13 | digit().plus() & 14 | (char('.') & digit().plus()).optional() & 15 | (pattern('eE') & pattern('+-').optional() & digit().plus()) 16 | .optional()) 17 | .flatten(message: 'number expected') 18 | .trim() 19 | .map(num.parse), 20 | ); 21 | builder.group().wrapper( 22 | char('(').trim(), 23 | char(')').trim(), 24 | (left, value, right) => value, 25 | ); 26 | builder.group().prefix(char('-').trim(), (op, a) => -a); 27 | builder.group().right(char('^').trim(), (a, op, b) => pow(a, b)); 28 | builder.group() 29 | ..left(char('*').trim(), (a, op, b) => a * b) 30 | ..left(char('/').trim(), (a, op, b) => a / b); 31 | builder.group() 32 | ..left(char('+').trim(), (a, op, b) => a + b) 33 | ..left(char('-').trim(), (a, op, b) => a - b); 34 | return builder.build().end(); 35 | } 36 | 37 | void main(List arguments) { 38 | final parser = buildParser(); 39 | final input = arguments.join(' '); 40 | switch (parser.parse(input)) { 41 | case Success(value: final value): 42 | stdout.writeln(' = $value'); 43 | case Failure(position: final position, message: final message): 44 | stderr.writeln(input); 45 | stderr.writeln('${' ' * (position - 1)}^-- $message'); 46 | exit(1); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/parser/predicate/character.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/parser.dart'; 4 | import '../character/predicate.dart'; 5 | import 'single_character.dart'; 6 | import 'unicode_character.dart'; 7 | 8 | /// Parser class for an individual character satisfying a [CharacterPredicate]. 9 | abstract class CharacterParser extends Parser { 10 | /// Constructs a new character parser. 11 | /// 12 | /// The [predicate] defines the character class to be detected. 13 | /// 14 | /// The [message] is the error text generated in case the predicate does not 15 | /// satisfy the input. 16 | /// 17 | /// By default, the parsers works on UTF-16 code units. If [unicode] is set 18 | /// to `true` unicode surrogate pairs are extracted from the input and matched 19 | /// against the predicate. 20 | factory CharacterParser( 21 | CharacterPredicate predicate, 22 | String message, { 23 | bool unicode = false, 24 | }) => switch (unicode) { 25 | false => SingleCharacterParser(predicate, message), 26 | true => UnicodeCharacterParser(predicate, message), 27 | }; 28 | 29 | /// Internal constructor 30 | @internal 31 | CharacterParser.internal(this.predicate, this.message); 32 | 33 | /// Predicate indicating whether a character can be consumed. 34 | final CharacterPredicate predicate; 35 | 36 | /// Error message to annotate parse failures with. 37 | final String message; 38 | 39 | @override 40 | String toString() => '${super.toString()}[$message]'; 41 | 42 | @override 43 | bool hasEqualProperties(CharacterParser other) => 44 | super.hasEqualProperties(other) && 45 | predicate.isEqualTo(other.predicate) && 46 | message == other.message; 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/parser/action/pick.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/parser.dart'; 5 | import '../../core/result.dart'; 6 | import '../combinator/delegate.dart'; 7 | 8 | extension PickParserExtension on Parser> { 9 | /// Returns a parser that transforms a successful parse result by returning 10 | /// the element at [index] of a list. A negative index can be used to access 11 | /// the elements from the back of the list. 12 | /// 13 | /// For example, the parser `letter().star().pick(-1)` returns the last 14 | /// letter parsed. For the input `'abc'` it returns `'c'`. 15 | @useResult 16 | Parser pick(int index) => PickParser(this, index); 17 | } 18 | 19 | /// A parser that performs a transformation with a given function on the 20 | /// successful parse result of the delegate. 21 | class PickParser extends DelegateParser, R> { 22 | PickParser(super.delegate, this.index); 23 | 24 | /// Indicates which element to return from the parsed list. 25 | final int index; 26 | 27 | @override 28 | Result parseOn(Context context) { 29 | final result = delegate.parseOn(context); 30 | if (result is Failure) return result; 31 | final value = result.value; 32 | return result.success(value[index < 0 ? value.length + index : index]); 33 | } 34 | 35 | @override 36 | int fastParseOn(String buffer, int position) => 37 | delegate.fastParseOn(buffer, position); 38 | 39 | @override 40 | String toString() => '${super.toString()}[$index]'; 41 | 42 | @override 43 | PickParser copy() => PickParser(delegate, index); 44 | 45 | @override 46 | bool hasEqualProperties(PickParser other) => 47 | super.hasEqualProperties(other) && index == other.index; 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/parser/predicate/converter.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/parser.dart'; 4 | import '../character/char.dart'; 5 | import '../character/pattern.dart'; 6 | import '../misc/epsilon.dart'; 7 | import 'string.dart'; 8 | 9 | extension ToParserStringExtension on String { 10 | /// Converts this string to a corresponding parser. 11 | /// 12 | /// - [message] defines a custom error message. 13 | /// - If [isPattern] is `true`, the string is considered a character-class 14 | /// like the ones accepted by [pattern]. 15 | /// - If [ignoreCase] is `true`, the string is matched in a case-insensitive 16 | /// manner. 17 | /// - If [unicode] is `true`, the string is matched using full unicode 18 | /// character decoding (as opposed to match UTF-16 code units). 19 | @useResult 20 | Parser toParser({ 21 | String? message, 22 | bool isPattern = false, 23 | bool ignoreCase = false, 24 | @Deprecated('Use `ignoreCase` instead') bool caseInsensitive = false, 25 | bool unicode = false, 26 | }) { 27 | // If this is a pattern, let the pattern handle everything. 28 | if (isPattern) { 29 | return pattern( 30 | this, 31 | message: message, 32 | ignoreCase: ignoreCase || caseInsensitive, 33 | unicode: unicode, 34 | ); 35 | } 36 | // Depending on length of the input create different parsers. 37 | return switch (unicode ? runes.length : codeUnits.length) { 38 | 0 => epsilonWith(this), 39 | 1 => char( 40 | this, 41 | message: message, 42 | ignoreCase: ignoreCase || caseInsensitive, 43 | unicode: unicode, 44 | ), 45 | _ => string( 46 | this, 47 | message: message, 48 | ignoreCase: ignoreCase || caseInsensitive, 49 | ), 50 | }; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/reflection/internal/utilities.dart: -------------------------------------------------------------------------------- 1 | import '../../core/parser.dart'; 2 | import '../../parser/combinator/optional.dart'; 3 | import '../../parser/misc/epsilon.dart'; 4 | import '../../parser/misc/position.dart'; 5 | import '../../parser/repeater/character.dart'; 6 | import '../../parser/repeater/repeating.dart'; 7 | import '../../parser/utils/sequential.dart'; 8 | 9 | /// Returns `true`, if [parser] is directly nullable. This means that the parser 10 | /// can succeed without involving any other parsers. 11 | bool isNullable(Parser parser) => 12 | parser is OptionalParser || 13 | parser is EpsilonParser || 14 | parser is PositionParser || 15 | (parser is RepeatingParser && parser.min == 0) || 16 | (parser is RepeatingCharacterParser && parser.min == 0); 17 | 18 | /// Returns `true`, if [parser] is a terminal or leaf parser. This means it 19 | /// does not delegate to any other parser. 20 | bool isTerminal(Parser parser) => parser.children.isEmpty; 21 | 22 | /// Returns `true`, if [parser] consumes its children in the declared 23 | /// sequence. 24 | bool isSequence(Parser parser) => 25 | parser is SequentialParser && parser.children.length > 1; 26 | 27 | /// Adds all [elements] to [result]. Returns `true` if [result] was changed. 28 | bool addAll(Set result, Iterable elements) { 29 | var changed = false; 30 | for (final element in elements) { 31 | changed |= result.add(element); 32 | } 33 | return changed; 34 | } 35 | 36 | /// Tests if two sets of parsers are equal. 37 | bool isParserIterableEqual(Iterable first, Iterable second) { 38 | for (final one in first) { 39 | if (!second.any(one.isEqualTo)) { 40 | return false; 41 | } 42 | } 43 | for (final two in second) { 44 | if (!first.any(two.isEqualTo)) { 45 | return false; 46 | } 47 | } 48 | return true; 49 | } 50 | -------------------------------------------------------------------------------- /lib/src/definition/internal/reference.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/parser.dart'; 5 | import '../../core/result.dart'; 6 | import '../../parser/utils/resolvable.dart'; 7 | 8 | /// Internal implementation of a reference parser. 9 | @immutable 10 | class ReferenceParser extends Parser implements ResolvableParser { 11 | ReferenceParser(this.function, this.arguments); 12 | 13 | final Function function; 14 | final List arguments; 15 | 16 | @override 17 | Parser resolve() => Function.apply(function, arguments) as Parser; 18 | 19 | @override 20 | Result parseOn(Context context) => _throwUnsupported(); 21 | 22 | @override 23 | ReferenceParser copy() => _throwUnsupported(); 24 | 25 | @override 26 | bool operator ==(Object other) { 27 | if (other is ReferenceParser) { 28 | if (function != other.function || 29 | arguments.length != other.arguments.length) { 30 | return false; 31 | } 32 | for (var i = 0; i < arguments.length; i++) { 33 | final a = arguments[i], b = other.arguments[i]; 34 | if (a is Parser && 35 | a is! ReferenceParser && 36 | b is Parser && 37 | b is! ReferenceParser) { 38 | // for parsers do a deep equality check 39 | if (!a.isEqualTo(b)) { 40 | return false; 41 | } 42 | } else { 43 | // for everything else just do standard equality 44 | if (a != b) { 45 | return false; 46 | } 47 | } 48 | } 49 | return true; 50 | } 51 | return false; 52 | } 53 | 54 | @override 55 | int get hashCode => function.hashCode; 56 | } 57 | 58 | Never _throwUnsupported() => 59 | throw UnsupportedError('Unsupported operation on parser reference'); 60 | -------------------------------------------------------------------------------- /lib/src/parser/misc/end.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/parser.dart'; 5 | import '../../core/result.dart'; 6 | import '../combinator/skip.dart'; 7 | 8 | extension EndOfInputParserExtension on Parser { 9 | /// Returns a parser that succeeds only if the receiver consumes the complete 10 | /// input, otherwise return a failure with the optional [message]. 11 | /// 12 | /// For example, the parser `letter().end()` succeeds on the input `'a'` 13 | /// and fails on `'ab'`. In contrast the parser `letter()` alone would 14 | /// succeed on both inputs, but not consume everything for the second input. 15 | @useResult 16 | Parser end({String message = 'end of input expected'}) => 17 | skip(after: endOfInput(message: message)); 18 | } 19 | 20 | /// Returns a parser that succeeds at the end of input. 21 | @useResult 22 | Parser endOfInput({String message = 'end of input expected'}) => 23 | EndOfInputParser(message); 24 | 25 | /// A parser that succeeds at the end of input. 26 | class EndOfInputParser extends Parser { 27 | EndOfInputParser(this.message); 28 | 29 | /// Error message to annotate parse failures with. 30 | final String message; 31 | 32 | @override 33 | Result parseOn(Context context) => 34 | context.position < context.buffer.length 35 | ? context.failure(message) 36 | : context.success(null); 37 | 38 | @override 39 | int fastParseOn(String buffer, int position) => 40 | position < buffer.length ? -1 : position; 41 | 42 | @override 43 | String toString() => '${super.toString()}[$message]'; 44 | 45 | @override 46 | EndOfInputParser copy() => EndOfInputParser(message); 47 | 48 | @override 49 | bool hasEqualProperties(EndOfInputParser other) => 50 | super.hasEqualProperties(other) && message == other.message; 51 | } 52 | -------------------------------------------------------------------------------- /lib/src/reflection/optimize.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../core/parser.dart'; 4 | import 'analyzer.dart'; 5 | import 'internal/optimize_rules.dart'; 6 | 7 | /// Function signature of a linter callback that is called whenever a linter 8 | /// rule identifies an issue. 9 | typedef ReplaceParser = void Function(Parser source, Parser target); 10 | 11 | /// Encapsulates a single optimization rule. 12 | @immutable 13 | abstract class OptimizeRule { 14 | /// Constructs a new optimization rule. 15 | const OptimizeRule(); 16 | 17 | /// Executes this rule using a provided [analyzer] on a [parser]. 18 | void run(Analyzer analyzer, Parser parser, ReplaceParser replace); 19 | } 20 | 21 | // All default optimizer rules to be run. 22 | const allOptimizerRules = [ 23 | CharacterRepeater(), 24 | FlattenChoice(), 25 | RemoveDelegate(), 26 | RemoveDuplicate(), 27 | ]; 28 | 29 | /// Returns an in-place optimized version of the parser. 30 | @useResult 31 | Parser optimize( 32 | Parser parser, { 33 | ReplaceParser? callback, 34 | List? rules, 35 | }) { 36 | final analyzer = Analyzer(parser); 37 | final selectedRules = rules ?? allOptimizerRules; 38 | final replacements = {}; 39 | for (final parser in analyzer.parsers) { 40 | parser.captureResultGeneric(

(parser) { 41 | for (final rule in selectedRules) { 42 | rule.run

(analyzer, parser, (a, b) { 43 | if (callback != null) callback(a, b); 44 | replacements[a] = b; 45 | }); 46 | } 47 | }); 48 | } 49 | if (replacements.isNotEmpty) { 50 | for (final parser in analyzer.parsers) { 51 | for (final replacement in replacements.entries) { 52 | parser.replace(replacement.key, replacement.value); 53 | } 54 | } 55 | return replacements[parser] as Parser? ?? parser; 56 | } 57 | return parser; 58 | } 59 | -------------------------------------------------------------------------------- /lib/src/indent/indent.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../core/parser.dart'; 4 | import '../parser/action/map.dart'; 5 | import '../parser/action/where.dart'; 6 | import '../parser/character/pattern.dart'; 7 | import '../parser/combinator/and.dart'; 8 | import '../parser/misc/epsilon.dart'; 9 | import '../parser/repeater/character.dart'; 10 | 11 | /// A stateful set of parsers to handled indentation based grammars. 12 | /// 13 | /// Based on https://stackoverflow.com/a/56926044/82303. 14 | @experimental 15 | class Indent { 16 | Indent({Parser? parser, String? message}) 17 | : parser = parser ?? pattern(' \t'), 18 | message = message ?? 'indented expected'; 19 | 20 | /// The parser used read a single indentation step. 21 | final Parser parser; 22 | 23 | /// The error message to use when an indention is expected. 24 | final String message; 25 | 26 | /// Internal field with the stack of indentations. 27 | @internal 28 | final List stack = []; 29 | 30 | /// Internal field of the currently active indentation. 31 | @internal 32 | String current = ''; 33 | 34 | /// A parser that increases the current indentation and returns it, but does 35 | /// not consume anything. 36 | late Parser increase = parser 37 | .plusString(message: message) 38 | .where((value) => value.length > current.length) 39 | .map((value) { 40 | stack.add(current); 41 | return current = value; 42 | }, hasSideEffects: true) 43 | .and(); 44 | 45 | /// A parser that consumes and returns the current indent. 46 | late Parser same = parser 47 | .starString(message: message) 48 | .where((value) => value == current); 49 | 50 | /// A parser that decreases the current indentation and returns it, but does 51 | /// not consume anything. 52 | late Parser decrease = epsilon() 53 | .where((_) => stack.isNotEmpty) 54 | .map((_) => current = stack.removeLast(), hasSideEffects: true); 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/parser/combinator/settable.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/parser.dart'; 5 | import '../../core/result.dart'; 6 | import '../combinator/delegate.dart'; 7 | import '../misc/failure.dart'; 8 | import '../utils/resolvable.dart'; 9 | 10 | extension SettableParserExtension on Parser { 11 | /// Returns a parser that points to the receiver, but can be changed to point 12 | /// to something else at a later point in time. 13 | /// 14 | /// For example, the parser `letter().settable()` behaves exactly the same 15 | /// as `letter()`, but it can be replaced with another parser using 16 | /// [SettableParser.set]. 17 | @useResult 18 | SettableParser settable() => SettableParser(this); 19 | } 20 | 21 | /// Returns a parser that is not defined, but that can be set at a later 22 | /// point in time. 23 | /// 24 | /// For example, the following code sets up a parser that points to itself 25 | /// and that accepts a sequence of a's ended with the letter b. 26 | /// 27 | /// ```dart 28 | /// final p = undefined(); 29 | /// p.set(char('a').seq(p).or(char('b'))); 30 | /// ``` 31 | @useResult 32 | SettableParser undefined({String message = 'undefined parser'}) => 33 | failure(message: message).settable(); 34 | 35 | /// A parser that is not defined, but that can be set at a later 36 | /// point in time. 37 | class SettableParser extends DelegateParser 38 | implements ResolvableParser { 39 | SettableParser(super.delegate); 40 | 41 | /// Sets the receiver to delegate to [parser]. 42 | void set(Parser parser) => replace(children[0], parser); 43 | 44 | @override 45 | Parser resolve() => delegate; 46 | 47 | @override 48 | Result parseOn(Context context) => delegate.parseOn(context); 49 | 50 | @override 51 | int fastParseOn(String buffer, int position) => 52 | delegate.fastParseOn(buffer, position); 53 | 54 | @override 55 | SettableParser copy() => SettableParser(delegate); 56 | } 57 | -------------------------------------------------------------------------------- /lib/src/definition/resolve.dart: -------------------------------------------------------------------------------- 1 | import '../core/parser.dart'; 2 | import '../parser/combinator/settable.dart'; 3 | import '../parser/utils/resolvable.dart'; 4 | import 'reference.dart'; 5 | 6 | /// Resolves all parser references reachable through [parser]. Returns an 7 | /// optimized parser graph that inlines all references directly. 8 | /// 9 | /// This code in-lines parsers that purely reference another one (subclasses 10 | /// of [ResolvableParser]). This includes, but is not limited to, parsers 11 | /// created with [ref0], [ref1], [ref2], ..., [undefined], or 12 | /// [SettableParserExtension], 13 | Parser resolve(Parser parser) { 14 | final mapping = , Parser>{}; 15 | parser = _dereference(parser, mapping); 16 | final todo = [parser]; 17 | final seen = {parser}; 18 | while (todo.isNotEmpty) { 19 | final parent = todo.removeLast(); 20 | for (var child in parent.children) { 21 | if (child is ResolvableParser) { 22 | final referenced = _dereference(child, mapping); 23 | parent.replace(child, referenced); 24 | child = referenced; 25 | } 26 | if (seen.add(child)) { 27 | todo.add(child); 28 | } 29 | } 30 | } 31 | return parser; 32 | } 33 | 34 | /// Internal helper to dereference and resolve a chain of [ResolvableParser] 35 | /// instances to their resolved counterpart. Throws a [StateError] if the there 36 | /// is a directly cyclic dependency on itself. 37 | Parser _dereference(Parser parser, Map mapping) { 38 | final references = >{}; 39 | while (parser is ResolvableParser) { 40 | if (mapping.containsKey(parser)) { 41 | return mapping[parser] as Parser; 42 | } else if (!references.add(parser)) { 43 | throw StateError('Recursive references detected: $references'); 44 | } 45 | parser = parser.resolve(); 46 | } 47 | for (final reference in references) { 48 | mapping[reference] = parser; 49 | } 50 | return parser; 51 | } 52 | -------------------------------------------------------------------------------- /lib/src/reflection/internal/cycle_set.dart: -------------------------------------------------------------------------------- 1 | import '../../core/parser.dart'; 2 | import 'utilities.dart'; 3 | 4 | Map> computeCycleSets({ 5 | required Iterable parsers, 6 | required Map> firstSets, 7 | }) { 8 | final cycleSets = >{}; 9 | for (final parser in parsers) { 10 | computeCycleSet(parser: parser, firstSets: firstSets, cycleSets: cycleSets); 11 | } 12 | return cycleSets; 13 | } 14 | 15 | void computeCycleSet({ 16 | required Parser parser, 17 | required Map> firstSets, 18 | required Map> cycleSets, 19 | List? stack, 20 | }) { 21 | if (cycleSets.containsKey(parser)) { 22 | return; 23 | } 24 | if (isTerminal(parser)) { 25 | cycleSets[parser] = const []; 26 | return; 27 | } 28 | stack ??= [parser]; 29 | final children = computeCycleChildren(parser: parser, firstSets: firstSets); 30 | for (final child in children) { 31 | final index = stack.indexOf(child); 32 | if (index >= 0) { 33 | final cycle = stack.sublist(index); 34 | for (final parser in cycle) { 35 | cycleSets[parser] = cycle; 36 | } 37 | return; 38 | } else { 39 | stack.add(child); 40 | computeCycleSet( 41 | parser: child, 42 | firstSets: firstSets, 43 | cycleSets: cycleSets, 44 | stack: stack, 45 | ); 46 | stack.removeLast(); 47 | } 48 | } 49 | if (!cycleSets.containsKey(parser)) { 50 | cycleSets[parser] = const []; 51 | return; 52 | } 53 | } 54 | 55 | List computeCycleChildren({ 56 | required Parser parser, 57 | required Map> firstSets, 58 | }) { 59 | if (isSequence(parser)) { 60 | final children = []; 61 | for (final child in parser.children) { 62 | children.add(child); 63 | if (!firstSets[child]!.any(isNullable)) { 64 | break; 65 | } 66 | } 67 | return children; 68 | } 69 | return parser.children; 70 | } 71 | -------------------------------------------------------------------------------- /lib/src/parser/action/permute.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/parser.dart'; 5 | import '../../core/result.dart'; 6 | import '../combinator/delegate.dart'; 7 | 8 | extension PermuteParserExtension on Parser> { 9 | /// Returns a parser that transforms a successful parse result by returning 10 | /// the permuted elements at [indexes] of a list. Negative indexes can be 11 | /// used to access the elements from the back of the list. 12 | /// 13 | /// For example, the parser `letter().star().permute([0, -1])` returns the 14 | /// first and last letter parsed. For the input `'abc'` it returns 15 | /// `['a', 'c']`. 16 | @useResult 17 | Parser> permute(List indexes) => PermuteParser(this, indexes); 18 | } 19 | 20 | /// A parser that performs a transformation with a given function on the 21 | /// successful parse result of the delegate. 22 | class PermuteParser extends DelegateParser, List> { 23 | PermuteParser(super.delegate, this.indexes); 24 | 25 | /// Indicates which elements to return from the parsed list. 26 | final List indexes; 27 | 28 | @override 29 | Result> parseOn(Context context) { 30 | final result = delegate.parseOn(context); 31 | if (result is Failure) return result; 32 | final value = result.value; 33 | final values = List.generate(indexes.length, (i) { 34 | final index = indexes[i]; 35 | return value[index < 0 ? value.length + index : index]; 36 | }, growable: false); 37 | return result.success(values); 38 | } 39 | 40 | @override 41 | int fastParseOn(String buffer, int position) => 42 | delegate.fastParseOn(buffer, position); 43 | 44 | @override 45 | String toString() => '${super.toString()}[${indexes.join(', ')}]'; 46 | 47 | @override 48 | PermuteParser copy() => PermuteParser(delegate, indexes); 49 | 50 | @override 51 | bool hasEqualProperties(PermuteParser other) => 52 | super.hasEqualProperties(other) && indexes == other.indexes; 53 | } 54 | -------------------------------------------------------------------------------- /lib/src/parser/predicate/predicate.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/parser.dart'; 5 | import '../../core/result.dart'; 6 | import '../../shared/pragma.dart'; 7 | import '../../shared/types.dart'; 8 | 9 | /// Returns a parser that reads input of the specified [length], accepts 10 | /// it if the [predicate] matches, or fails with the given [message]. 11 | @useResult 12 | Parser predicate( 13 | int length, 14 | Predicate predicate, 15 | String message, 16 | ) => PredicateParser(length, predicate, message); 17 | 18 | /// A parser for a literal satisfying a predicate. 19 | class PredicateParser extends Parser { 20 | PredicateParser(this.length, this.predicate, this.message) 21 | : assert(length > 0, 'length must be positive'); 22 | 23 | /// The length of the input to read. 24 | final int length; 25 | 26 | /// The predicate function testing the input. 27 | final Predicate predicate; 28 | 29 | /// Error message to annotate parse failures with. 30 | final String message; 31 | 32 | @override 33 | @noBoundsChecks 34 | Result parseOn(Context context) { 35 | final start = context.position; 36 | final stop = start + length; 37 | if (stop <= context.buffer.length) { 38 | final result = context.buffer.substring(start, stop); 39 | if (predicate(result)) return context.success(result, stop); 40 | } 41 | return context.failure(message); 42 | } 43 | 44 | @override 45 | @noBoundsChecks 46 | int fastParseOn(String buffer, int position) { 47 | final stop = position + length; 48 | return stop <= buffer.length && predicate(buffer.substring(position, stop)) 49 | ? stop 50 | : -1; 51 | } 52 | 53 | @override 54 | String toString() => '${super.toString()}[$message]'; 55 | 56 | @override 57 | PredicateParser copy() => PredicateParser(length, predicate, message); 58 | 59 | @override 60 | bool hasEqualProperties(PredicateParser other) => 61 | super.hasEqualProperties(other) && 62 | length == other.length && 63 | predicate == other.predicate && 64 | message == other.message; 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/reflection/internal/path.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/parser.dart'; 4 | import '../../shared/types.dart'; 5 | 6 | /// A continuous path through the parser graph. 7 | class ParserPath { 8 | /// Constructs a path from a list of parsers and indexes. 9 | ParserPath(this.parsers, this.indexes) 10 | : assert(parsers.isNotEmpty, 'parsers cannot be empty'), 11 | assert(indexes.length == parsers.length - 1, 'indexes wrong size'), 12 | assert( 13 | (() { 14 | for (var i = 0; i < indexes.length; i++) { 15 | if (parsers[i].children[indexes[i]] != parsers[i + 1]) { 16 | return false; 17 | } 18 | } 19 | return true; 20 | })(), 21 | 'indexes invalid', 22 | ); 23 | 24 | /// The non-empty list of parsers in this path. 25 | final List parsers; 26 | 27 | /// The parser where this path starts. 28 | Parser get source => parsers.first; 29 | 30 | /// The parser where this path ends. 31 | Parser get target => parsers.last; 32 | 33 | /// The number of parsers in this path. 34 | int get length => parsers.length; 35 | 36 | /// The child-indexes that navigate from one parser to the next one. This 37 | /// collection contains one element less than the number of parsers in the 38 | /// path. 39 | final List indexes; 40 | 41 | void _push(Parser parser, int index) { 42 | parsers.add(parser); 43 | indexes.add(index); 44 | } 45 | 46 | void _pop() { 47 | parsers.removeLast(); 48 | indexes.removeLast(); 49 | } 50 | } 51 | 52 | @internal 53 | Iterable depthFirstSearch( 54 | ParserPath path, 55 | Predicate predicate, 56 | ) sync* { 57 | if (predicate(path)) { 58 | yield ParserPath(List.from(path.parsers), List.from(path.indexes)); 59 | } else { 60 | final children = path.target.children; 61 | for (var i = 0; i < children.length; i++) { 62 | final child = children[i]; 63 | if (!path.parsers.contains(child)) { 64 | path._push(child, i); 65 | yield* depthFirstSearch(path, predicate); 66 | path._pop(); 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/src/parser/combinator/optional.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/parser.dart'; 5 | import '../../core/result.dart'; 6 | import 'delegate.dart'; 7 | 8 | extension OptionalParserExtension on Parser { 9 | /// Returns new parser that accepts the receiver, if possible. The resulting 10 | /// parser returns the result of the receiver, or `null` if not applicable. 11 | /// 12 | /// For example, the parser `letter().optional()` accepts a letter as input 13 | /// and returns that letter. When given something else the parser succeeds as 14 | /// well, does not consume anything and returns `null`. 15 | @useResult 16 | Parser optional() => OptionalParser(this, null); 17 | 18 | /// Returns new parser that accepts the receiver, if possible. The resulting 19 | /// parser returns the result of the receiver, or [value] if not applicable. 20 | /// 21 | /// For example, the parser `letter().optionalWith('!')` accepts a letter as 22 | /// input and returns that letter. When given something else the parser 23 | /// succeeds as well, does not consume anything and returns `'!'`. 24 | @useResult 25 | Parser optionalWith(R value) => OptionalParser(this, value); 26 | } 27 | 28 | /// A parser that optionally parsers its delegate, or answers `null`. 29 | class OptionalParser extends DelegateParser { 30 | OptionalParser(super.delegate, this.otherwise); 31 | 32 | /// The value returned if the [delegate] cannot be parsed. 33 | final R otherwise; 34 | 35 | @override 36 | Result parseOn(Context context) { 37 | final result = delegate.parseOn(context); 38 | if (result is! Failure) return result; 39 | return context.success(otherwise); 40 | } 41 | 42 | @override 43 | int fastParseOn(String buffer, int position) { 44 | final result = delegate.fastParseOn(buffer, position); 45 | return result < 0 ? position : result; 46 | } 47 | 48 | @override 49 | OptionalParser copy() => OptionalParser(delegate, otherwise); 50 | 51 | @override 52 | bool hasEqualProperties(OptionalParser other) => 53 | super.hasEqualProperties(other) && otherwise == other.otherwise; 54 | } 55 | -------------------------------------------------------------------------------- /lib/src/parser/character/predicate/lookup.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:collection/collection.dart' show ListEquality; 4 | 5 | import '../../../shared/pragma.dart'; 6 | import '../predicate.dart'; 7 | import 'range.dart'; 8 | 9 | class LookupCharPredicate extends CharacterPredicate { 10 | LookupCharPredicate.fromRanges(Iterable ranges) 11 | : start = ranges.first.start, 12 | stop = ranges.last.stop, 13 | bits = Uint32List(size(ranges)) { 14 | for (final range in ranges) { 15 | for ( 16 | var index = range.start - start; 17 | index <= range.stop - start; 18 | index++ 19 | ) { 20 | bits[index >> _shift] |= _mask[index & _offset]; 21 | } 22 | } 23 | } 24 | 25 | const LookupCharPredicate(this.start, this.stop, this.bits); 26 | 27 | final int start; 28 | final int stop; 29 | final Uint32List bits; 30 | 31 | @override 32 | bool test(int charCode) => 33 | start <= charCode && charCode <= stop && _testBit(charCode - start); 34 | 35 | @preferInline 36 | @noBoundsChecks 37 | bool _testBit(int value) => 38 | (bits[value >> _shift] & _mask[value & _offset]) != 0; 39 | 40 | @override 41 | bool isEqualTo(CharacterPredicate other) => 42 | other is LookupCharPredicate && 43 | start == other.start && 44 | stop == other.stop && 45 | _listEquality.equals(bits, other.bits); 46 | 47 | @override 48 | String toString() => '${super.toString()}($start, $stop, $bits)'; 49 | 50 | static int size(Iterable ranges) => 51 | (ranges.last.stop - ranges.first.start + _offset + 1) >> _shift; 52 | } 53 | 54 | const _listEquality = ListEquality(); 55 | 56 | const _shift = 5; 57 | const _offset = 31; 58 | const _mask = [ 59 | 1, 60 | 2, 61 | 4, 62 | 8, 63 | 16, 64 | 32, 65 | 64, 66 | 128, 67 | 256, 68 | 512, 69 | 1024, 70 | 2048, 71 | 4096, 72 | 8192, 73 | 16384, 74 | 32768, 75 | 65536, 76 | 131072, 77 | 262144, 78 | 524288, 79 | 1048576, 80 | 2097152, 81 | 4194304, 82 | 8388608, 83 | 16777216, 84 | 33554432, 85 | 67108864, 86 | 134217728, 87 | 268435456, 88 | 536870912, 89 | 1073741824, 90 | 2147483648, 91 | ]; 92 | -------------------------------------------------------------------------------- /lib/src/parser/utils/separated_list.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | /// A list of [elements] and its [separators]. 4 | class SeparatedList { 5 | SeparatedList(this.elements, this.separators) 6 | : assert( 7 | max(0, elements.length - 1) == separators.length, 8 | 'Inconsistent number of elements ($elements) and separators ($separators)', 9 | ); 10 | 11 | /// The parsed elements. 12 | final List elements; 13 | 14 | /// The parsed separators. 15 | final List separators; 16 | 17 | /// An (untyped) iterable over the [elements] and the interleaved [separators] 18 | /// in order of appearance. 19 | Iterable get sequential sync* { 20 | for (var i = 0; i < elements.length; i++) { 21 | yield elements[i]; 22 | if (i < separators.length) { 23 | yield separators[i]; 24 | } 25 | } 26 | } 27 | 28 | /// Combines the [elements] by grouping the elements from the left and 29 | /// calling [callback] on all consecutive elements with the corresponding 30 | /// `separator`. 31 | /// 32 | /// For example, if the elements are numbers and the separators are 33 | /// subtraction operations sequential values `1 - 2 - 3` are grouped like 34 | /// `(1 - 2) - 3`. 35 | R foldLeft(R Function(R left, S seperator, R right) callback) { 36 | var result = elements.first; 37 | for (var i = 1; i < elements.length; i++) { 38 | result = callback(result, separators[i - 1], elements[i]); 39 | } 40 | return result; 41 | } 42 | 43 | /// Combines the [elements] by grouping the elements from the right and 44 | /// calling [callback] on all consecutive elements with the corresponding 45 | /// `separator`. 46 | /// 47 | /// For example, if the elements are numbers and the separators are 48 | /// exponentiation operations sequential values `1 ^ 2 ^ 3` are grouped like 49 | /// `1 ^ (2 ^ 3)`. 50 | R foldRight(R Function(R left, S seperator, R right) callback) { 51 | var result = elements.last; 52 | for (var i = elements.length - 2; i >= 0; i--) { 53 | result = callback(elements[i], separators[i], result); 54 | } 55 | return result; 56 | } 57 | 58 | @override 59 | String toString() => '$runtimeType$sequential'; 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/debug/progress.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../core/context.dart'; 4 | import '../core/parser.dart'; 5 | import '../parser/action/continuation.dart'; 6 | import '../reflection/transform.dart'; 7 | import '../shared/types.dart'; 8 | 9 | /// Returns a transformed [Parser] that when being used to read input 10 | /// visually prints its progress while progressing. 11 | /// 12 | /// For example, the snippet 13 | /// 14 | /// ```dart 15 | /// final parser = letter() & word().star(); 16 | /// progress(parser).parse('f123'); 17 | /// ``` 18 | /// 19 | /// prints the following output: 20 | /// 21 | /// ```text 22 | /// * SequenceParser 23 | /// * SingleCharacterParser[letter expected] 24 | /// ** PossessiveRepeatingParser[0..*] 25 | /// ** SingleCharacterParser[letter or digit expected] 26 | /// *** SingleCharacterParser[letter or digit expected] 27 | /// **** SingleCharacterParser[letter or digit expected] 28 | /// ***** SingleCharacterParser[letter or digit expected] 29 | /// ``` 30 | /// 31 | /// Jumps backwards mean that the parser is back-tracking. Often choices can 32 | /// be reordered to avoid such expensive parses. 33 | /// 34 | /// The optional [output] callback can be used to continuously receive 35 | /// [ProgressFrame] updates with the current progress information. 36 | @useResult 37 | Parser progress( 38 | Parser root, { 39 | VoidCallback output = print, 40 | Predicate? predicate, 41 | }) => transformParser(root,

(parser) { 42 | if (predicate == null || predicate(parser)) { 43 | return parser.callCC((continuation, context) { 44 | output(_ProgressFrame(parser, context)); 45 | return continuation(context); 46 | }); 47 | } else { 48 | return parser; 49 | } 50 | }); 51 | 52 | /// Encapsulates the data around a parser progress. 53 | abstract class ProgressFrame { 54 | /// Return the parser of this frame. 55 | Parser get parser; 56 | 57 | /// Returns the activation context of this frame. 58 | Context get context; 59 | 60 | /// Returns the current position in the input. 61 | int get position => context.position; 62 | } 63 | 64 | class _ProgressFrame extends ProgressFrame { 65 | _ProgressFrame(this.parser, this.context); 66 | 67 | @override 68 | final Parser parser; 69 | 70 | @override 71 | final Context context; 72 | 73 | @override 74 | String toString() => '${'*' * (1 + position)} $parser'; 75 | } 76 | -------------------------------------------------------------------------------- /lib/src/parser/combinator/skip.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/parser.dart'; 5 | import '../../core/result.dart'; 6 | import '../../parser/misc/epsilon.dart'; 7 | import '../utils/sequential.dart'; 8 | import 'delegate.dart'; 9 | 10 | extension SkipParserExtension on Parser { 11 | /// Returns a parser that consumes input [before] and [after] the receiver, 12 | /// but discards the parse results of [before] and [after] and only returns 13 | /// the result of the receiver. 14 | /// 15 | /// For example, the parser `digit().skip(char('['), char(']'))` 16 | /// returns `'3'` for the input `'[3]'`. 17 | @useResult 18 | Parser skip({Parser? before, Parser? after}) => SkipParser( 19 | this, 20 | before: before ?? epsilon(), 21 | after: after ?? epsilon(), 22 | ); 23 | } 24 | 25 | /// A parser that silently consumes input of another parser before and after 26 | /// its delegate. 27 | class SkipParser extends DelegateParser implements SequentialParser { 28 | SkipParser(super.delegate, {required this.before, required this.after}); 29 | 30 | Parser before; 31 | Parser after; 32 | 33 | @override 34 | Result parseOn(Context context) { 35 | final beforeContext = before.parseOn(context); 36 | if (beforeContext is Failure) return beforeContext; 37 | final resultContext = delegate.parseOn(beforeContext); 38 | if (resultContext is Failure) return resultContext; 39 | final afterContext = after.parseOn(resultContext); 40 | if (afterContext is Failure) return afterContext; 41 | return afterContext.success(resultContext.value); 42 | } 43 | 44 | @override 45 | int fastParseOn(String buffer, int position) { 46 | position = before.fastParseOn(buffer, position); 47 | if (position < 0) return -1; 48 | position = delegate.fastParseOn(buffer, position); 49 | if (position < 0) return -1; 50 | return after.fastParseOn(buffer, position); 51 | } 52 | 53 | @override 54 | SkipParser copy() => SkipParser(delegate, before: before, after: after); 55 | 56 | @override 57 | List get children => [before, delegate, after]; 58 | 59 | @override 60 | void replace(Parser source, Parser target) { 61 | super.replace(source, target); 62 | if (before == source) before = target; 63 | if (after == source) after = target; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/utils/assertions.dart: -------------------------------------------------------------------------------- 1 | import 'package:petitparser/petitparser.dart'; 2 | import 'package:petitparser/reflection.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | import 'matchers.dart'; 6 | 7 | /// Shared invariants for all parsers. 8 | void expectParserInvariants(Parser parser) { 9 | test('copy', () { 10 | final copy = parser.copy(); 11 | expect(copy, isNot(same(parser))); 12 | expect(copy.toString(), parser.toString()); 13 | expect(copy.runtimeType, parser.runtimeType); 14 | expect( 15 | copy.children, 16 | pairwiseCompare(parser.children, identical, 'same children'), 17 | ); 18 | expect(copy, isParserDeepEqual(parser)); 19 | }); 20 | test('transform', () { 21 | final copy = transformParser(parser,

(parser) => parser); 22 | expect(copy, isNot(same(parser))); 23 | expect(copy.toString(), parser.toString()); 24 | expect(copy.runtimeType, parser.runtimeType); 25 | expect( 26 | copy.children, 27 | pairwiseCompare(parser.children, (parser, copy) { 28 | expect(copy, isNot(same(parser))); 29 | expect(copy.toString(), parser.toString()); 30 | expect(copy.runtimeType, parser.runtimeType); 31 | return true; 32 | }, 'same children'), 33 | ); 34 | expect(copy, isParserDeepEqual(parser)); 35 | }); 36 | test('isEqualTo', () { 37 | final copy = parser.copy(); 38 | expect(copy, isParserDeepEqual(parser)); 39 | expect(copy, isParserDeepEqual(copy)); 40 | expect(parser, isParserDeepEqual(copy)); 41 | }); 42 | test('replace', () { 43 | final copy = parser.copy(); 44 | final replaced = []; 45 | for (var i = 0; i < copy.children.length; i++) { 46 | final source = copy.children[i]; 47 | final target = source.copy(); 48 | expect(source, isNot(same(target))); 49 | copy.replace(source, target); 50 | expect(copy.children[i], same(target)); 51 | replaced.add(target); 52 | } 53 | expect( 54 | copy.children, 55 | pairwiseCompare(replaced, identical, 'replaced children'), 56 | ); 57 | }); 58 | test('toString', () { 59 | expect(parser.toString(), isToString(name: parser.runtimeType.toString())); 60 | if (parser case CharacterParser(predicate: final predicate)) { 61 | expect( 62 | predicate.toString(), 63 | isToString(name: predicate.runtimeType.toString()), 64 | ); 65 | } 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /lib/src/parser/action/continuation.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/parser.dart'; 5 | import '../../core/result.dart'; 6 | import '../../parser/combinator/delegate.dart'; 7 | 8 | /// Handler function for the [ContinuationParser]. 9 | typedef ContinuationHandler = 10 | Result Function(ContinuationFunction continuation, Context context); 11 | 12 | /// Continuation function of the [ContinuationHandler]. 13 | typedef ContinuationFunction = Result Function(Context context); 14 | 15 | extension ContinuationParserExtension on Parser { 16 | /// Returns a parser that when activated captures a continuation function 17 | /// and passes it together with the current context into the handler. 18 | /// 19 | /// Handlers are not required to call the continuation, but can completely 20 | /// ignore it, call it multiple times, and/or store it away for later use. 21 | /// Similarly handlers can modify the current context and/or modify the 22 | /// returned result. 23 | /// 24 | /// The following example shows a simple wrapper. Messages are printed before 25 | /// and after the `digit()` parser is activated: 26 | /// 27 | /// ```dart 28 | /// final parser = digit().callCC((continuation, context) { 29 | /// print('Parser will be activated, the context is $context.'); 30 | /// final result = continuation(context); 31 | /// print('Parser was activated, the result is $result.'); 32 | /// return result; 33 | /// }); 34 | /// ``` 35 | @useResult 36 | Parser callCC(ContinuationHandler handler) => 37 | ContinuationParser(this, handler); 38 | } 39 | 40 | /// Continuation parser that when activated captures a continuation function 41 | /// and passes it together with the current context into the handler. 42 | class ContinuationParser extends DelegateParser { 43 | ContinuationParser(super.delegate, this.handler); 44 | 45 | /// Activation handler of the continuation. 46 | final ContinuationHandler handler; 47 | 48 | @override 49 | Result parseOn(Context context) => handler(delegate.parseOn, context); 50 | 51 | @override 52 | ContinuationParser copy() => 53 | ContinuationParser(delegate, handler); 54 | 55 | @override 56 | bool hasEqualProperties(ContinuationParser other) => 57 | super.hasEqualProperties(other) && handler == other.handler; 58 | } 59 | -------------------------------------------------------------------------------- /lib/src/parser/action/where.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/parser.dart'; 5 | import '../../core/result.dart'; 6 | import '../../shared/types.dart'; 7 | import '../combinator/delegate.dart'; 8 | 9 | extension WhereParserExtension on Parser { 10 | /// Returns a parser that evaluates the [predicate] with the successful 11 | /// parse result. If the predicate returns `true` the parser proceeds with 12 | /// the parse result, otherwise a parse failure is created using the 13 | /// optionally specified [factory] callback, the provided [message], or 14 | /// otherwise an automatically created error message. 15 | /// 16 | /// The following example parses two characters, but only succeeds if they 17 | /// are equal: 18 | /// 19 | /// ```dart 20 | /// final inner = any() & any(); 21 | /// final parser = inner.where( 22 | /// (value) => value[0] == value[1], 23 | /// factory: (context, success) => 24 | /// context.failure('characters do not match')); 25 | /// parser.parse('aa'); // ==> Success: ['a', 'a'] 26 | /// parser.parse('ab'); // ==> Failure: characters do not match 27 | /// ``` 28 | @useResult 29 | Parser where( 30 | Predicate predicate, { 31 | String? message, 32 | FailureFactory? factory, 33 | }) => WhereParser(this, predicate, factory ?? defaultFactory_(message)); 34 | } 35 | 36 | typedef FailureFactory = 37 | Result Function(Context context, Success success); 38 | 39 | class WhereParser extends DelegateParser { 40 | WhereParser(super.parser, this.predicate, this.factory); 41 | 42 | final Predicate predicate; 43 | final FailureFactory factory; 44 | 45 | @override 46 | Result parseOn(Context context) { 47 | final result = delegate.parseOn(context); 48 | if (result is Success && !predicate(result.value)) { 49 | return factory(context, result); 50 | } 51 | return result; 52 | } 53 | 54 | @override 55 | Parser copy() => WhereParser(delegate, predicate, factory); 56 | 57 | @override 58 | bool hasEqualProperties(WhereParser other) => 59 | super.hasEqualProperties(other) && 60 | predicate == other.predicate && 61 | factory == other.factory; 62 | } 63 | 64 | FailureFactory defaultFactory_(String? message) => 65 | (context, success) => 66 | context.failure(message ?? 'unexpected "${success.value}"'); 67 | -------------------------------------------------------------------------------- /lib/src/parser/predicate/single_character.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/result.dart'; 5 | import '../../shared/pragma.dart'; 6 | import '../character/predicate.dart'; 7 | import '../character/predicate/constant.dart'; 8 | import 'character.dart'; 9 | 10 | /// Parser class for an individual 16-bit UTF-16 code units satisfying a 11 | /// specified [CharacterPredicate]. 12 | class SingleCharacterParser extends CharacterParser { 13 | factory SingleCharacterParser(CharacterPredicate predicate, String message) => 14 | ConstantCharPredicate.any.isEqualTo(predicate) 15 | ? AnySingleCharacterParser.internal(predicate, message) 16 | : SingleCharacterParser.internal(predicate, message); 17 | 18 | @internal 19 | SingleCharacterParser.internal(super.predicate, super.message) 20 | : super.internal(); 21 | 22 | @override 23 | @noBoundsChecks 24 | Result parseOn(Context context) { 25 | final buffer = context.buffer; 26 | final position = context.position; 27 | if (position < buffer.length && 28 | predicate.test(buffer.codeUnitAt(position))) { 29 | return context.success(buffer[position], position + 1); 30 | } 31 | return context.failure(message); 32 | } 33 | 34 | @override 35 | @noBoundsChecks 36 | int fastParseOn(String buffer, int position) => 37 | position < buffer.length && predicate.test(buffer.codeUnitAt(position)) 38 | ? position + 1 39 | : -1; 40 | 41 | @override 42 | SingleCharacterParser copy() => SingleCharacterParser(predicate, message); 43 | } 44 | 45 | /// Optimized version of [SingleCharacterParser] that parses any 16-bit UTF-16 46 | /// character (including possible surrogate pairs). 47 | class AnySingleCharacterParser extends SingleCharacterParser { 48 | AnySingleCharacterParser.internal(super.predicate, super.message) 49 | : assert(ConstantCharPredicate.any.isEqualTo(predicate)), 50 | super.internal(); 51 | 52 | @override 53 | @noBoundsChecks 54 | Result parseOn(Context context) { 55 | final buffer = context.buffer; 56 | final position = context.position; 57 | if (position < buffer.length) { 58 | return context.success(buffer[position], position + 1); 59 | } 60 | return context.failure(message); 61 | } 62 | 63 | @override 64 | int fastParseOn(String buffer, int position) => 65 | position < buffer.length ? position + 1 : -1; 66 | } 67 | -------------------------------------------------------------------------------- /lib/src/parser/character/utils/optimize.dart: -------------------------------------------------------------------------------- 1 | import '../predicate.dart'; 2 | import '../predicate/char.dart'; 3 | import '../predicate/constant.dart'; 4 | import '../predicate/lookup.dart'; 5 | import '../predicate/range.dart'; 6 | 7 | /// Creates an optimized character from a string. 8 | CharacterPredicate optimizedString( 9 | String string, { 10 | required bool unicode, 11 | bool ignoreCase = false, 12 | }) { 13 | if (ignoreCase) string = '${string.toLowerCase()}${string.toUpperCase()}'; 14 | return optimizedRanges( 15 | (unicode ? string.runes : string.codeUnits).map( 16 | (value) => RangeCharPredicate(value, value), 17 | ), 18 | unicode: unicode, 19 | ); 20 | } 21 | 22 | /// Creates an optimized predicate from a list of range predicates. 23 | CharacterPredicate optimizedRanges( 24 | Iterable ranges, { 25 | required bool unicode, 26 | }) { 27 | // 1. Sort the ranges: 28 | final sortedRanges = List.of(ranges, growable: false); 29 | sortedRanges.sort( 30 | (first, second) => first.start != second.start 31 | ? first.start - second.start 32 | : first.stop - second.stop, 33 | ); 34 | 35 | // 2. Merge adjacent or overlapping ranges: 36 | final mergedRanges = []; 37 | for (final thisRange in sortedRanges) { 38 | if (mergedRanges.isEmpty) { 39 | mergedRanges.add(thisRange); 40 | } else { 41 | final lastRange = mergedRanges.last; 42 | if (lastRange.stop + 1 >= thisRange.start) { 43 | final characterRange = RangeCharPredicate( 44 | lastRange.start, 45 | thisRange.stop, 46 | ); 47 | mergedRanges[mergedRanges.length - 1] = characterRange; 48 | } else { 49 | mergedRanges.add(thisRange); 50 | } 51 | } 52 | } 53 | 54 | // 3. Build the best resulting predicate: 55 | final matchingCount = mergedRanges.fold( 56 | 0, 57 | (current, range) => current + (range.stop - range.start + 1), 58 | ); 59 | if (matchingCount == 0) { 60 | return ConstantCharPredicate.none; 61 | } else if ((unicode && matchingCount - 1 == 0x10ffff) || 62 | (!unicode && matchingCount - 1 == 0xffff)) { 63 | return ConstantCharPredicate.any; 64 | } else if (mergedRanges.length == 1) { 65 | return mergedRanges[0].start == mergedRanges[0].stop 66 | ? SingleCharPredicate(mergedRanges[0].start) 67 | : mergedRanges[0]; 68 | } else { 69 | return LookupCharPredicate.fromRanges(mergedRanges); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/src/parser/action/flatten.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/parser.dart'; 5 | import '../../core/result.dart'; 6 | import '../combinator/delegate.dart'; 7 | 8 | extension FlattenParserExtension on Parser { 9 | /// Returns a parser that discards the result of the receiver and answers 10 | /// the sub-string its delegate consumes. 11 | /// 12 | /// If a [message] is provided, the flatten parser can switch to a fast mode 13 | /// where error tracking within the receiver is suppressed and in case of a 14 | /// problem [message] is reported instead. 15 | /// 16 | /// For example, the parser `letter().plus().flatten()` returns `'abc'` 17 | /// for the input `'abc'`. In contrast, the parser `letter().plus()` would 18 | /// return `['a', 'b', 'c']` for the same input instead. 19 | @useResult 20 | Parser flatten({String? message}) => FlattenParser(this, message); 21 | } 22 | 23 | /// A parser that discards the result of the delegate and answers the 24 | /// sub-string its delegate consumes. 25 | class FlattenParser extends DelegateParser { 26 | FlattenParser(super.delegate, [this.message]); 27 | 28 | /// Error message to indicate parse failures with. 29 | final String? message; 30 | 31 | @override 32 | Result parseOn(Context context) { 33 | if (message != null) { 34 | // If we have a message we can switch to fast mode. 35 | final position = delegate.fastParseOn(context.buffer, context.position); 36 | if (position < 0) return context.failure(message!); 37 | final output = context.buffer.substring(context.position, position); 38 | return context.success(output, position); 39 | } else { 40 | final result = delegate.parseOn(context); 41 | if (result is Failure) return result; 42 | final output = context.buffer.substring( 43 | context.position, 44 | result.position, 45 | ); 46 | return result.success(output); 47 | } 48 | } 49 | 50 | @override 51 | int fastParseOn(String buffer, int position) => 52 | delegate.fastParseOn(buffer, position); 53 | 54 | @override 55 | String toString() => 56 | message == null ? super.toString() : '${super.toString()}[$message]'; 57 | 58 | @override 59 | bool hasEqualProperties(FlattenParser other) => 60 | super.hasEqualProperties(other) && message == other.message; 61 | 62 | @override 63 | FlattenParser copy() => FlattenParser(delegate, message); 64 | } 65 | -------------------------------------------------------------------------------- /lib/src/parser/action/map.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/parser.dart'; 5 | import '../../core/result.dart'; 6 | import '../../shared/types.dart'; 7 | import '../combinator/delegate.dart'; 8 | 9 | extension MapParserExtension on Parser { 10 | /// Returns a parser that evaluates a [callback] as the production action 11 | /// on success of the receiver. 12 | /// 13 | /// [callback] should be side-effect free, meaning for the same input it 14 | /// always gives the same output. This allows the framework skip calling 15 | /// the callback if the result is not used, or to cache the results. If 16 | /// [callback] has side-effects, make sure to exactly understand the 17 | /// implications and set [hasSideEffects] to `true`. 18 | /// 19 | /// For example, the parser `digit().map((char) => int.parse(char))` returns 20 | /// the number `1` for the input string `'1'`. If the delegate fails, the 21 | /// production action is not executed and the failure is passed on. 22 | @useResult 23 | Parser map(Callback callback, {bool hasSideEffects = false}) => 24 | MapParser(this, callback, hasSideEffects: hasSideEffects); 25 | } 26 | 27 | /// A parser that performs a transformation with a given function on the 28 | /// successful parse result of the delegate. 29 | class MapParser extends DelegateParser { 30 | MapParser(super.delegate, this.callback, {this.hasSideEffects = false}); 31 | 32 | /// The production action to be called. 33 | final Callback callback; 34 | 35 | /// Whether the [callback] has side-effects. 36 | final bool hasSideEffects; 37 | 38 | @override 39 | Result parseOn(Context context) { 40 | final result = delegate.parseOn(context); 41 | if (result is Failure) return result; 42 | return result.success(callback(result.value)); 43 | } 44 | 45 | // If we know to have side-effects, we have to fall back to the slow mode. 46 | @override 47 | int fastParseOn(String buffer, int position) => hasSideEffects 48 | ? super.fastParseOn(buffer, position) 49 | : delegate.fastParseOn(buffer, position); 50 | 51 | @override 52 | bool hasEqualProperties(MapParser other) => 53 | super.hasEqualProperties(other) && 54 | callback == other.callback && 55 | hasSideEffects == other.hasSideEffects; 56 | 57 | @override 58 | MapParser copy() => 59 | MapParser(delegate, callback, hasSideEffects: hasSideEffects); 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/parser/combinator/not.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/parser.dart'; 5 | import '../../core/result.dart'; 6 | import '../character/any.dart'; 7 | import 'delegate.dart'; 8 | import 'skip.dart'; 9 | 10 | extension NotParserExtension on Parser { 11 | /// Returns a parser (logical not-predicate) that succeeds with the [Failure] 12 | /// whenever the receiver fails, but never consumes input. 13 | /// 14 | /// For example, the parser `char('_').not().seq(identifier)` accepts 15 | /// identifiers that do not start with an underscore character. If the parser 16 | /// `char('_')` accepts the input, the negation and subsequently the 17 | /// complete parser fails. Otherwise the parser `identifier` is given the 18 | /// ability to process the complete identifier. 19 | @useResult 20 | Parser not({String message = 'success not expected'}) => 21 | NotParser(this, message); 22 | 23 | /// Returns a parser that consumes any input token (character), but the 24 | /// receiver. 25 | /// 26 | /// For example, the parser `letter().neg()` accepts any input but a letter. 27 | /// The parser fails for inputs like `'a'` or `'Z'`, but succeeds for 28 | /// input like `'1'`, `'_'` or `'$'`. 29 | @useResult 30 | Parser neg({String message = 'input not expected'}) => 31 | any().skip(before: not(message: message)); 32 | } 33 | 34 | /// The not-predicate, a parser that succeeds whenever its delegate does not, 35 | /// but consumes no input. 36 | class NotParser extends DelegateParser { 37 | NotParser(super.delegate, this.message); 38 | 39 | /// Error message to annotate parse failures with. 40 | final String message; 41 | 42 | @override 43 | Result parseOn(Context context) { 44 | final result = delegate.parseOn(context); 45 | if (result is Failure) { 46 | return context.success(result); 47 | } else { 48 | return context.failure(message); 49 | } 50 | } 51 | 52 | @override 53 | int fastParseOn(String buffer, int position) { 54 | final result = delegate.fastParseOn(buffer, position); 55 | return result < 0 ? position : -1; 56 | } 57 | 58 | @override 59 | String toString() => '${super.toString()}[$message]'; 60 | 61 | @override 62 | NotParser copy() => NotParser(delegate, message); 63 | 64 | @override 65 | bool hasEqualProperties(NotParser other) => 66 | super.hasEqualProperties(other) && message == other.message; 67 | } 68 | -------------------------------------------------------------------------------- /lib/src/parser/misc/newline.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/parser.dart'; 5 | import '../../core/result.dart'; 6 | import '../../shared/pragma.dart'; 7 | 8 | /// Returns a parser that detects newlines platform independently. 9 | @useResult 10 | Parser newline({String message = 'newline expected'}) => 11 | NewlineParser(message); 12 | 13 | /// A parser that consumes newlines platform independently. 14 | class NewlineParser extends Parser { 15 | NewlineParser(this.message); 16 | 17 | final String message; 18 | 19 | @override 20 | @noBoundsChecks 21 | Result parseOn(Context context) { 22 | final buffer = context.buffer; 23 | final position = context.position; 24 | if (position < buffer.length) { 25 | switch (buffer.codeUnitAt(position)) { 26 | case 10: 27 | // Unix and Unix-like systems (Linux, macOS, FreeBSD, AIX, Xenix, etc.), 28 | // Multics, BeOS, Amiga, RISC OS. 29 | return context.success('\n', position + 1); 30 | case 13: 31 | if (position + 1 < buffer.length && 32 | buffer.codeUnitAt(position + 1) == 10) { 33 | // Microsoft Windows, DOS (MS-DOS, PC DOS, etc.), Atari TOS, DEC 34 | // TOPS-10, RT-11, CP/M, MP/M, OS/2, Symbian OS, Palm OS, Amstrad 35 | // CPC, and most other early non-Unix and non-IBM operating systems. 36 | return context.success('\r\n', position + 2); 37 | } else { 38 | // Commodore 8-bit machines (C64, C128), Acorn BBC, ZX Spectrum, 39 | // TRS-80, Apple II series, Oberon, the classic Mac OS, MIT Lisp 40 | // Machine and OS-9 41 | return context.success('\r', position + 1); 42 | } 43 | } 44 | } 45 | return context.failure(message); 46 | } 47 | 48 | @override 49 | @noBoundsChecks 50 | int fastParseOn(String buffer, int position) { 51 | if (position < buffer.length) { 52 | switch (buffer.codeUnitAt(position)) { 53 | case 10: 54 | return position + 1; 55 | case 13: 56 | return position + 1 < buffer.length && 57 | buffer.codeUnitAt(position + 1) == 10 58 | ? position + 2 59 | : position + 1; 60 | } 61 | } 62 | return -1; 63 | } 64 | 65 | @override 66 | String toString() => '${super.toString()}[$message]'; 67 | 68 | @override 69 | NewlineParser copy() => NewlineParser(message); 70 | } 71 | -------------------------------------------------------------------------------- /test/parser_misc_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:petitparser/petitparser.dart'; 2 | import 'package:test/test.dart' hide anyOf; 3 | 4 | import 'utils/assertions.dart'; 5 | import 'utils/matchers.dart'; 6 | 7 | void main() { 8 | group('end', () { 9 | expectParserInvariants(endOfInput()); 10 | test('default', () { 11 | final parser = char('a').end(); 12 | expect(parser, isParseFailure('', message: '"a" expected')); 13 | expect(parser, isParseSuccess('a', result: 'a')); 14 | expect( 15 | parser, 16 | isParseFailure('aa', position: 1, message: 'end of input expected'), 17 | ); 18 | }); 19 | }); 20 | group('epsilon', () { 21 | expectParserInvariants(epsilon()); 22 | test('default', () { 23 | final parser = epsilon(); 24 | expect(parser, isParseSuccess('', result: isNull)); 25 | expect(parser, isParseSuccess('a', result: isNull, position: 0)); 26 | }); 27 | }); 28 | group('failure', () { 29 | expectParserInvariants(failure()); 30 | test('default', () { 31 | final parser = failure(message: 'failure'); 32 | expect(parser, isParseFailure('', message: 'failure')); 33 | expect(parser, isParseFailure('a', message: 'failure')); 34 | }); 35 | }); 36 | group('label', () { 37 | expectParserInvariants(any().labeled('anything')); 38 | test('default', () { 39 | final parser = char('*').labeled('asterisk'); 40 | expect(parser.label, 'asterisk'); 41 | expect(parser, isParseSuccess('*', result: '*')); 42 | expect(parser, isParseFailure('a', message: '"*" expected')); 43 | }); 44 | }); 45 | group('newline', () { 46 | expectParserInvariants(newline()); 47 | test('default', () { 48 | final parser = newline(); 49 | expect(parser, isParseSuccess('\n', result: '\n')); 50 | expect(parser, isParseSuccess('\r\n', result: '\r\n')); 51 | expect(parser, isParseSuccess('\r', result: '\r')); 52 | expect(parser, isParseFailure('', message: 'newline expected')); 53 | expect(parser, isParseFailure('\f', message: 'newline expected')); 54 | }); 55 | }); 56 | group('position', () { 57 | expectParserInvariants(position()); 58 | test('default', () { 59 | final parser = (any().star() & position()).pick(-1); 60 | expect(parser, isParseSuccess('', result: 0)); 61 | expect(parser, isParseSuccess('a', result: 1)); 62 | expect(parser, isParseSuccess('aa', result: 2)); 63 | expect(parser, isParseSuccess('aaa', result: 3)); 64 | }); 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /lib/src/reflection/internal/follow_set.dart: -------------------------------------------------------------------------------- 1 | import '../../core/parser.dart'; 2 | import '../../parser/repeater/repeating.dart'; 3 | import 'utilities.dart'; 4 | 5 | Map> computeFollowSets({ 6 | required Parser root, 7 | required Iterable parsers, 8 | required Map> firstSets, 9 | required Parser sentinel, 10 | }) { 11 | final followSets = { 12 | for (final parser in parsers) parser: {if (parser == root) sentinel}, 13 | }; 14 | var changed = false; 15 | do { 16 | changed = false; 17 | for (final parser in parsers) { 18 | changed |= expandFollowSet( 19 | parser: parser, 20 | followSets: followSets, 21 | firstSets: firstSets, 22 | ); 23 | } 24 | } while (changed); 25 | return followSets; 26 | } 27 | 28 | bool expandFollowSet({ 29 | required Parser parser, 30 | required Map> followSets, 31 | required Map> firstSets, 32 | }) { 33 | if (isSequence(parser)) { 34 | return expandFollowSetOfSequence( 35 | parser: parser, 36 | children: parser.children, 37 | followSets: followSets, 38 | firstSets: firstSets, 39 | ); 40 | } else if (parser is RepeatingParser) { 41 | return expandFollowSetOfSequence( 42 | parser: parser, 43 | children: [parser.children[0], ...parser.children], 44 | followSets: followSets, 45 | firstSets: firstSets, 46 | ); 47 | } else { 48 | var changed = false; 49 | for (final child in parser.children) { 50 | changed |= addAll(followSets[child]!, followSets[parser]!); 51 | } 52 | return changed; 53 | } 54 | } 55 | 56 | bool expandFollowSetOfSequence({ 57 | required Parser parser, 58 | required List children, 59 | required Map> followSets, 60 | required Map> firstSets, 61 | }) { 62 | var changed = false; 63 | for (var i = 0; i < children.length; i++) { 64 | if (i == children.length - 1) { 65 | changed |= addAll(followSets[children[i]]!, followSets[parser]!); 66 | } else { 67 | final firstSet = {}; 68 | var j = i + 1; 69 | for (; j < children.length; j++) { 70 | firstSet.addAll(firstSets[children[j]]!); 71 | if (!firstSets[children[j]]!.any(isNullable)) { 72 | break; 73 | } 74 | } 75 | if (j == children.length) { 76 | changed |= addAll(followSets[children[i]]!, followSets[parser]!); 77 | } 78 | changed |= addAll( 79 | followSets[children[i]]!, 80 | firstSet.where((each) => !isNullable(each)), 81 | ); 82 | } 83 | } 84 | return changed; 85 | } 86 | -------------------------------------------------------------------------------- /lib/src/debug/profile.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../core/parser.dart'; 4 | import '../parser/action/continuation.dart'; 5 | import '../reflection/transform.dart'; 6 | import '../shared/types.dart'; 7 | 8 | /// Returns a transformed [Parser] that when being used measures 9 | /// the activation count and total time of each parser. 10 | /// 11 | /// For example, the snippet 12 | /// 13 | /// ```dart 14 | /// final parser = letter() & word().star(); 15 | /// profile(parser).parse('f1234567890'); 16 | /// ``` 17 | /// 18 | /// prints the following output: 19 | /// 20 | /// ```text 21 | /// 1 2006 SequenceParser 22 | /// 1 697 PossessiveRepeatingParser[0..*] 23 | /// 11 406 SingleCharacterParser[letter or digit expected] 24 | /// 1 947 SingleCharacterParser[letter expected] 25 | /// ``` 26 | /// 27 | /// The first number refers to the number of activations of each parser, and 28 | /// the second number is the microseconds spent in this parser and all its 29 | /// children. 30 | /// 31 | /// The optional [output] callback can be used to receive [ProfileFrame] 32 | /// objects with the full profiling information at the end of the parse. 33 | @useResult 34 | Parser profile( 35 | Parser root, { 36 | VoidCallback output = print, 37 | Predicate? predicate, 38 | }) { 39 | final frames = []; 40 | return transformParser(root,

(parser) { 41 | if (predicate == null || predicate(parser)) { 42 | final frame = _ProfileFrame(parser); 43 | frames.add(frame); 44 | return parser.callCC((continuation, context) { 45 | frame.count++; 46 | frame.stopwatch.start(); 47 | final result = continuation(context); 48 | frame.stopwatch.stop(); 49 | return result; 50 | }); 51 | } else { 52 | return parser; 53 | } 54 | }).callCC((continuation, context) { 55 | final result = continuation(context); 56 | frames.forEach(output); 57 | return result; 58 | }); 59 | } 60 | 61 | /// Encapsulates the data around a parser profile. 62 | abstract class ProfileFrame { 63 | /// Return the parser of this frame. 64 | Parser get parser; 65 | 66 | /// Return the number of times this parser was activated. 67 | int get count; 68 | 69 | /// Return the total elapsed time in this parser and its children. 70 | Duration get elapsed; 71 | } 72 | 73 | class _ProfileFrame extends ProfileFrame { 74 | _ProfileFrame(this.parser); 75 | 76 | final stopwatch = Stopwatch(); 77 | 78 | @override 79 | final Parser parser; 80 | 81 | @override 82 | int count = 0; 83 | 84 | @override 85 | Duration get elapsed => stopwatch.elapsed; 86 | 87 | @override 88 | String toString() => '$count\t${elapsed.inMicroseconds}\t$parser'; 89 | } 90 | -------------------------------------------------------------------------------- /lib/parser.dart: -------------------------------------------------------------------------------- 1 | /// This package contains the standard parser implementations. 2 | library; 3 | 4 | export 'src/core/parser.dart'; 5 | export 'src/parser/action/cast.dart'; 6 | export 'src/parser/action/cast_list.dart'; 7 | export 'src/parser/action/continuation.dart'; 8 | export 'src/parser/action/flatten.dart'; 9 | export 'src/parser/action/map.dart'; 10 | export 'src/parser/action/permute.dart'; 11 | export 'src/parser/action/pick.dart'; 12 | export 'src/parser/action/token.dart'; 13 | export 'src/parser/action/trim.dart'; 14 | export 'src/parser/action/where.dart'; 15 | export 'src/parser/character/any.dart'; 16 | export 'src/parser/character/any_of.dart'; 17 | export 'src/parser/character/char.dart'; 18 | export 'src/parser/character/digit.dart'; 19 | export 'src/parser/character/letter.dart'; 20 | export 'src/parser/character/lowercase.dart'; 21 | export 'src/parser/character/none_of.dart'; 22 | export 'src/parser/character/pattern.dart'; 23 | export 'src/parser/character/predicate.dart'; 24 | export 'src/parser/character/range.dart'; 25 | export 'src/parser/character/uppercase.dart'; 26 | export 'src/parser/character/whitespace.dart'; 27 | export 'src/parser/character/word.dart'; 28 | export 'src/parser/combinator/and.dart'; 29 | export 'src/parser/combinator/choice.dart'; 30 | export 'src/parser/combinator/delegate.dart'; 31 | export 'src/parser/combinator/list.dart'; 32 | export 'src/parser/combinator/not.dart'; 33 | export 'src/parser/combinator/optional.dart'; 34 | export 'src/parser/combinator/sequence.dart'; 35 | export 'src/parser/combinator/settable.dart'; 36 | export 'src/parser/combinator/skip.dart'; 37 | export 'src/parser/misc/end.dart'; 38 | export 'src/parser/misc/epsilon.dart'; 39 | export 'src/parser/misc/failure.dart'; 40 | export 'src/parser/misc/label.dart'; 41 | export 'src/parser/misc/newline.dart'; 42 | export 'src/parser/misc/position.dart'; 43 | export 'src/parser/predicate/character.dart'; 44 | export 'src/parser/predicate/converter.dart'; 45 | export 'src/parser/predicate/pattern.dart'; 46 | export 'src/parser/predicate/predicate.dart'; 47 | export 'src/parser/predicate/single_character.dart'; 48 | export 'src/parser/predicate/string.dart'; 49 | export 'src/parser/predicate/unicode_character.dart'; 50 | export 'src/parser/repeater/character.dart'; 51 | export 'src/parser/repeater/greedy.dart'; 52 | export 'src/parser/repeater/lazy.dart'; 53 | export 'src/parser/repeater/limited.dart'; 54 | export 'src/parser/repeater/possessive.dart'; 55 | export 'src/parser/repeater/repeating.dart'; 56 | export 'src/parser/repeater/separated.dart'; 57 | export 'src/parser/repeater/unbounded.dart'; 58 | export 'src/parser/utils/failure_joiner.dart'; 59 | export 'src/parser/utils/labeled.dart'; 60 | export 'src/parser/utils/resolvable.dart'; 61 | export 'src/parser/utils/separated_list.dart'; 62 | -------------------------------------------------------------------------------- /lib/src/reflection/internal/optimize_rules.dart: -------------------------------------------------------------------------------- 1 | import '../../core/parser.dart'; 2 | import '../../parser/action/flatten.dart'; 3 | import '../../parser/combinator/choice.dart'; 4 | import '../../parser/combinator/delegate.dart'; 5 | import '../../parser/combinator/settable.dart'; 6 | import '../../parser/misc/label.dart'; 7 | import '../../parser/predicate/single_character.dart'; 8 | import '../../parser/repeater/character.dart'; 9 | import '../../parser/repeater/possessive.dart'; 10 | import '../analyzer.dart'; 11 | import '../optimize.dart'; 12 | 13 | class CharacterRepeater extends OptimizeRule { 14 | const CharacterRepeater(); 15 | 16 | @override 17 | void run(Analyzer analyzer, Parser parser, ReplaceParser replace) { 18 | if (parser case FlattenParser(delegate: final repeating)) { 19 | if (repeating case PossessiveRepeatingParser( 20 | delegate: final character, 21 | )) { 22 | if (character case SingleCharacterParser()) { 23 | replace( 24 | parser, 25 | RepeatingCharacterParser( 26 | character.predicate, 27 | character.message, 28 | repeating.min, 29 | repeating.max, 30 | ) 31 | as Parser, 32 | ); 33 | } 34 | } 35 | } 36 | } 37 | } 38 | 39 | class FlattenChoice extends OptimizeRule { 40 | const FlattenChoice(); 41 | 42 | @override 43 | void run(Analyzer analyzer, Parser parser, ReplaceParser replace) { 44 | if (parser is ChoiceParser) { 45 | final children = parser.children.expand( 46 | (child) => 47 | child is ChoiceParser && 48 | parser.failureJoiner == child.failureJoiner 49 | ? child.children 50 | : [child], 51 | ); 52 | if (parser.children.length < children.length) { 53 | replace( 54 | parser, 55 | children.toChoiceParser(failureJoiner: parser.failureJoiner), 56 | ); 57 | } 58 | } 59 | } 60 | } 61 | 62 | class RemoveDelegate extends OptimizeRule { 63 | const RemoveDelegate(); 64 | 65 | @override 66 | void run(Analyzer analyzer, Parser parser, ReplaceParser replace) { 67 | final settables = >{}; 68 | while (parser is DelegateParser && 69 | (parser is SettableParser || parser is LabelParser)) { 70 | if (!settables.add(parser)) { 71 | break; // The grammar is looping. 72 | } 73 | parser = parser.delegate; 74 | } 75 | for (final settable in settables) { 76 | replace(settable, parser); 77 | } 78 | } 79 | } 80 | 81 | class RemoveDuplicate extends OptimizeRule { 82 | const RemoveDuplicate(); 83 | 84 | @override 85 | void run(Analyzer analyzer, Parser parser, ReplaceParser replace) { 86 | final other = analyzer.parsers.firstWhere( 87 | (each) => parser.isEqualTo(each), 88 | orElse: () => parser, 89 | ); 90 | if (parser != other) { 91 | replace(parser, other as Parser); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/src/parser/action/trim.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/parser.dart'; 5 | import '../../core/result.dart'; 6 | import '../../shared/pragma.dart'; 7 | import '../character/whitespace.dart'; 8 | import '../combinator/delegate.dart'; 9 | import '../utils/sequential.dart'; 10 | 11 | extension TrimmingParserExtension on Parser { 12 | /// Returns a parser that consumes input before and after the receiver, 13 | /// discards the excess input and only returns the result of the receiver. 14 | /// The optional arguments are parsers that consume the excess input. By 15 | /// default `whitespace()` is used. Up to two arguments can be provided to 16 | /// have different parsers on the [left] and [right] side. 17 | /// 18 | /// For example, the parser `letter().plus().trim()` returns `['a', 'b']` 19 | /// for the input `' ab\n'` and consumes the complete input string. 20 | @useResult 21 | Parser trim([Parser? left, Parser? right]) => 22 | TrimmingParser(this, left ??= whitespace(), right ??= left); 23 | } 24 | 25 | /// A parser that silently consumes input of another parser around 26 | /// its delegate. 27 | class TrimmingParser extends DelegateParser 28 | implements SequentialParser { 29 | TrimmingParser(super.delegate, this.left, this.right); 30 | 31 | /// Parser that consumes input before the delegate. 32 | Parser left; 33 | 34 | /// Parser that consumes input after the delegate. 35 | Parser right; 36 | 37 | @override 38 | Result parseOn(Context context) { 39 | final buffer = context.buffer; 40 | final before = _trim(left, buffer, context.position); 41 | if (before != context.position) { 42 | context = Context(buffer, before); 43 | } 44 | final result = delegate.parseOn(context); 45 | if (result is Failure) return result; 46 | final after = _trim(right, buffer, result.position); 47 | return after == result.position 48 | ? result 49 | : result.success(result.value, after); 50 | } 51 | 52 | @override 53 | int fastParseOn(String buffer, int position) { 54 | final result = delegate.fastParseOn(buffer, _trim(left, buffer, position)); 55 | return result < 0 ? -1 : _trim(right, buffer, result); 56 | } 57 | 58 | @preferInline 59 | int _trim(Parser parser, String buffer, int position) { 60 | for (;;) { 61 | final result = parser.fastParseOn(buffer, position); 62 | assert(result != position, '$parser must always consume'); 63 | if (result < 0) { 64 | break; 65 | } 66 | position = result; 67 | } 68 | return position; 69 | } 70 | 71 | @override 72 | TrimmingParser copy() => TrimmingParser(delegate, left, right); 73 | 74 | @override 75 | List get children => [delegate, left, right]; 76 | 77 | @override 78 | void replace(covariant Parser source, covariant Parser target) { 79 | super.replace(source, target); 80 | if (left == source) { 81 | left = target; 82 | } 83 | if (right == source) { 84 | right = target; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/src/debug/trace.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../core/context.dart'; 4 | import '../core/parser.dart'; 5 | import '../core/result.dart'; 6 | import '../parser/action/continuation.dart'; 7 | import '../reflection/transform.dart'; 8 | import '../shared/types.dart'; 9 | 10 | /// Returns a transformed [Parser] that when being used to read input prints a 11 | /// trace of all activated parsers and their respective parse results. 12 | /// 13 | /// For example, the snippet 14 | /// 15 | /// ```dart 16 | /// final parser = letter() & word().star(); 17 | /// trace(parser).parse('f1'); 18 | /// ``` 19 | /// 20 | /// produces the following output: 21 | /// 22 | /// ```text 23 | /// SequenceParser 24 | /// SingleCharacterParser[letter expected] 25 | /// Success[1:2]: f 26 | /// PossessiveRepeatingParser[0..*] 27 | /// SingleCharacterParser[letter or digit expected] 28 | /// Success[1:3]: 1 29 | /// SingleCharacterParser[letter or digit expected] 30 | /// Failure[1:3]: letter or digit expected 31 | /// Success[1:3]: [1] 32 | /// Success[1:3]: [f, [1]] 33 | /// ``` 34 | /// 35 | /// Indentation signifies the activation of a parser object. Reverse indentation 36 | /// signifies the returning of a parse result either with a success or failure 37 | /// context. 38 | /// 39 | /// The optional [output] callback can be used to continuously receive 40 | /// [TraceEvent] objects with current enter and exit data. 41 | @useResult 42 | Parser trace( 43 | Parser root, { 44 | VoidCallback output = print, 45 | Predicate? predicate, 46 | }) { 47 | TraceEvent? parent; 48 | return transformParser(root,

(parser) { 49 | if (predicate == null || predicate(parser)) { 50 | return parser.callCC((continuation, context) { 51 | final currentParent = parent; 52 | output(parent = _TraceEvent(currentParent, parser, context)); 53 | final result = continuation(context); 54 | output(_TraceEvent(currentParent, parser, context, result)); 55 | parent = currentParent; 56 | return result; 57 | }); 58 | } else { 59 | return parser; 60 | } 61 | }); 62 | } 63 | 64 | /// Encapsulates the entry and exit data around a parser trace. 65 | abstract class TraceEvent { 66 | /// Returns the parent trace event. 67 | TraceEvent? get parent; 68 | 69 | /// Returns the parser of this event. 70 | Parser get parser; 71 | 72 | /// Returns the activation context of this event. 73 | Context get context; 74 | 75 | /// Returns the result if this is a exit event, otherwise `null`. 76 | Result? get result; 77 | 78 | /// Returns the nesting level of this event. 79 | int get level => parent != null ? parent!.level + 1 : 0; 80 | } 81 | 82 | class _TraceEvent extends TraceEvent { 83 | _TraceEvent(this.parent, this.parser, this.context, [this.result]); 84 | 85 | @override 86 | final TraceEvent? parent; 87 | 88 | @override 89 | final Parser parser; 90 | 91 | @override 92 | final Context context; 93 | 94 | @override 95 | final Result? result; 96 | 97 | @override 98 | String toString() => '${' ' * level}${result ?? parser}'; 99 | } 100 | -------------------------------------------------------------------------------- /lib/src/parser/character/pattern.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/parser.dart'; 4 | import '../action/map.dart'; 5 | import '../combinator/choice.dart'; 6 | import '../combinator/sequence.dart'; 7 | import '../misc/end.dart'; 8 | import '../predicate/character.dart'; 9 | import '../repeater/possessive.dart'; 10 | import 'any.dart'; 11 | import 'char.dart'; 12 | import 'predicate/constant.dart'; 13 | import 'predicate/not.dart'; 14 | import 'predicate/range.dart'; 15 | import 'utils/code.dart'; 16 | import 'utils/optimize.dart'; 17 | 18 | /// Returns a parser that accepts a single character of a given character set 19 | /// [pattern] provided as a string. 20 | /// 21 | /// Characters match themselves. A dash `-` between two characters matches the 22 | /// range of those characters. A caret `^` at the beginning negates the pattern. 23 | /// 24 | /// For example, the parser `pattern('aou')` accepts the character 'a', 'o', or 25 | /// 'u', and fails for any other input. The parser `pattern('1-3')` accepts 26 | /// either '1', '2', or '3'; and fails for any other character. The parser 27 | /// `pattern('^aou') accepts any character, but fails for the characters 'a', 28 | /// 'o', or 'u'. 29 | /// 30 | /// If [ignoreCase] is set to `true` the pattern accepts lower and uppercase 31 | /// variations of its characters. If [unicode] is set to `true` unicode 32 | /// surrogate pairs are extracted and matched against the predicate. 33 | @useResult 34 | Parser pattern( 35 | String pattern, { 36 | String? message, 37 | bool ignoreCase = false, 38 | bool unicode = false, 39 | }) { 40 | var input = pattern; 41 | final isNegated = input.startsWith('^'); 42 | if (isNegated) input = input.substring(1); 43 | final inputs = ignoreCase 44 | ? [input.toLowerCase(), input.toUpperCase()] 45 | : [input]; 46 | final parser = unicode ? _patternUnicodeParser : _patternParser; 47 | var predicate = optimizedRanges( 48 | inputs.expand((each) => parser.parse(each).value), 49 | unicode: unicode, 50 | ); 51 | if (isNegated) { 52 | predicate = predicate is ConstantCharPredicate 53 | ? ConstantCharPredicate(!predicate.constant) 54 | : NotCharPredicate(predicate); 55 | } 56 | message ??= 57 | '[${toReadableString(pattern, unicode: unicode)}]' 58 | '${ignoreCase ? ' (case-insensitive)' : ''} expected'; 59 | return CharacterParser(predicate, message, unicode: unicode); 60 | } 61 | 62 | Parser> _createParser({required bool unicode}) { 63 | // Parser that consumes a single character. 64 | final character = any(unicode: unicode); 65 | // Parser that reads a single character. 66 | final single = character.map( 67 | (element) => RangeCharPredicate( 68 | toCharCode(element, unicode: unicode), 69 | toCharCode(element, unicode: unicode), 70 | ), 71 | ); 72 | // Parser that reads a character range. 73 | final range = (character, char('-'), character).toSequenceParser().map3( 74 | (start, _, stop) => RangeCharPredicate( 75 | toCharCode(start, unicode: unicode), 76 | toCharCode(stop, unicode: unicode), 77 | ), 78 | ); 79 | // Parser that reads a sequence of single characters or ranges. 80 | return [range, single].toChoiceParser().star().end(); 81 | } 82 | 83 | final _patternParser = _createParser(unicode: false); 84 | final _patternUnicodeParser = _createParser(unicode: true); 85 | -------------------------------------------------------------------------------- /test/context_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:petitparser/petitparser.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'utils/matchers.dart'; 5 | 6 | void main() { 7 | const buffer = 'a\nc'; 8 | const context = Context(buffer, 0); 9 | test('context', () { 10 | expect(context.buffer, buffer); 11 | expect(context.position, 0); 12 | expect(context.toString(), isToString(name: 'Context', rest: ['[1:1]'])); 13 | }); 14 | group('success', () { 15 | test('default', () { 16 | final success = context.success('result'); 17 | expect(success.buffer, buffer); 18 | expect(success.position, 0); 19 | expect(success.value, 'result'); 20 | expect(() => success.message, throwsA(isUnsupportedError)); 21 | expect( 22 | success.toString(), 23 | isToString( 24 | name: 'Success', 25 | generic: '', 26 | rest: ['[1:1]: result'], 27 | ), 28 | ); 29 | }); 30 | test('with position', () { 31 | final success = context.success('result', 2); 32 | expect(success.buffer, buffer); 33 | expect(success.position, 2); 34 | expect(success.value, 'result'); 35 | expect(() => success.message, throwsA(isUnsupportedError)); 36 | expect( 37 | success.toString(), 38 | isToString( 39 | name: 'Success', 40 | generic: '', 41 | rest: ['[2:1]: result'], 42 | ), 43 | ); 44 | }); 45 | }); 46 | group('failure', () { 47 | test('default', () { 48 | final failure = context.failure('error'); 49 | expect(failure.buffer, buffer); 50 | expect(failure.position, 0); 51 | expect( 52 | () => failure.value, 53 | throwsA( 54 | isParserException 55 | .having((error) => error.failure, 'failure', same(failure)) 56 | .having((error) => error.message, 'message', 'error') 57 | .having((error) => error.offset, 'offset', 0) 58 | .having((error) => error.source, 'source', same(buffer)) 59 | .having( 60 | (error) => error.toString(), 61 | 'toString', 62 | isToString(name: 'ParserException', rest: ['[1:1]: error']), 63 | ), 64 | ), 65 | ); 66 | expect(failure.message, 'error'); 67 | expect( 68 | failure.toString(), 69 | isToString(name: 'Failure', rest: ['[1:1]: error']), 70 | ); 71 | }); 72 | test('with position', () { 73 | final failure = context.failure('error', 2); 74 | expect(failure.buffer, buffer); 75 | expect(failure.position, 2); 76 | expect( 77 | () => failure.value, 78 | throwsA( 79 | isParserException 80 | .having((error) => error.failure, 'failure', same(failure)) 81 | .having((error) => error.message, 'message', 'error') 82 | .having((error) => error.offset, 'offset', 2) 83 | .having((error) => error.source, 'source', same(buffer)) 84 | .having( 85 | (error) => error.toString(), 86 | 'toString', 87 | isToString(name: 'ParserException', rest: ['[2:1]: error']), 88 | ), 89 | ), 90 | ); 91 | expect(failure.message, 'error'); 92 | expect( 93 | failure.toString(), 94 | isToString(name: 'Failure', rest: ['[2:1]: error']), 95 | ); 96 | }); 97 | }); 98 | } 99 | -------------------------------------------------------------------------------- /lib/src/parser/combinator/sequence.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/parser.dart'; 5 | import '../../core/result.dart'; 6 | import '../../shared/pragma.dart'; 7 | import '../utils/sequential.dart'; 8 | import 'list.dart'; 9 | 10 | export 'generated/sequence_2.dart'; 11 | export 'generated/sequence_3.dart'; 12 | export 'generated/sequence_4.dart'; 13 | export 'generated/sequence_5.dart'; 14 | export 'generated/sequence_6.dart'; 15 | export 'generated/sequence_7.dart'; 16 | export 'generated/sequence_8.dart'; 17 | export 'generated/sequence_9.dart'; 18 | 19 | extension SequenceParserExtension on Parser { 20 | /// Returns a parser that accepts the receiver followed by [other]. The 21 | /// resulting parser returns a list of the parse result of the receiver 22 | /// followed by the parse result of [other]. Calling this method on an 23 | /// existing sequence code does not nest this sequence into a new one, but 24 | /// instead augments the existing sequence with [other]. 25 | /// 26 | /// For example, the parser `letter().seq(digit()).seq(letter())` accepts a 27 | /// letter followed by a digit and another letter. The parse result of the 28 | /// input string `'a1b'` is the list `['a', '1', 'b']`. 29 | @useResult 30 | Parser> seq(Parser other) => switch (this) { 31 | SequenceParser(children: final children) => [ 32 | ...children, 33 | other, 34 | ].toSequenceParser(), 35 | _ => [this, other].toSequenceParser(), 36 | }; 37 | 38 | /// Convenience operator returning a parser that accepts the receiver followed 39 | /// by [other]. See [seq] for details. 40 | /// 41 | /// For example, the parser `letter() & digit() & letter()` accepts a 42 | /// letter followed by a digit and another letter. The parse result of the 43 | /// input string `'a1b'` is the list `['a', '1', 'b']`. 44 | @useResult 45 | Parser> operator &(Parser other) => seq(other); 46 | } 47 | 48 | extension SequenceIterableExtension on Iterable> { 49 | /// Converts the parser in this iterable to a sequence of parsers. 50 | /// 51 | /// For example, the parser `[letter(), digit(), letter()].toSequenceParser()` 52 | /// accepts a letter followed by a digit and another letter. The parse result 53 | /// of the input string `'a1b'` is the list `['a', '1', 'b']`. 54 | @useResult 55 | Parser> toSequenceParser() => SequenceParser(this); 56 | } 57 | 58 | /// A parser that parses a sequence of parsers. 59 | class SequenceParser extends ListParser> 60 | implements SequentialParser { 61 | SequenceParser(super.children); 62 | 63 | @override 64 | @noBoundsChecks 65 | Result> parseOn(Context context) { 66 | var current = context; 67 | final elements = []; 68 | for (var i = 0; i < children.length; i++) { 69 | final result = children[i].parseOn(current); 70 | if (result is Failure) return result; 71 | elements.add(result.value); 72 | current = result; 73 | } 74 | return current.success(elements); 75 | } 76 | 77 | @override 78 | @noBoundsChecks 79 | int fastParseOn(String buffer, int position) { 80 | for (var i = 0; i < children.length; i++) { 81 | position = children[i].fastParseOn(buffer, position); 82 | if (position < 0) return position; 83 | } 84 | return position; 85 | } 86 | 87 | @override 88 | SequenceParser copy() => SequenceParser(children); 89 | } 90 | -------------------------------------------------------------------------------- /lib/src/parser/combinator/generated/sequence_2.dart: -------------------------------------------------------------------------------- 1 | // AUTO-GENERATED CODE: DO NOT EDIT 2 | 3 | import 'package:meta/meta.dart'; 4 | 5 | import '../../../core/context.dart'; 6 | import '../../../core/parser.dart'; 7 | import '../../../core/result.dart'; 8 | import '../../../shared/pragma.dart'; 9 | import '../../action/map.dart'; 10 | import '../../utils/sequential.dart'; 11 | 12 | /// Creates a [Parser] that consumes the 2 parsers passed as argument in 13 | /// sequence and returns a [Record] with the 2 positional parse results. 14 | /// 15 | /// For example, 16 | /// the parser `seq2(char('a'), char('b'))` 17 | /// returns `('a', 'b')` 18 | /// for the input `'ab'`. 19 | @useResult 20 | Parser<(R1, R2)> seq2(Parser parser1, Parser parser2) => 21 | SequenceParser2(parser1, parser2); 22 | 23 | /// Extensions on a [Record] with 2 positional [Parser]s. 24 | extension RecordOfParsersExtension2 on (Parser, Parser) { 25 | /// Converts a [Record] of 2 positional parsers to a [Parser] that runs the 26 | /// parsers in sequence and returns a [Record] with 2 positional parse results. 27 | /// 28 | /// For example, 29 | /// the parser `(char('a'), char('b')).toSequenceParser()` 30 | /// returns `('a', 'b')` 31 | /// for the input `'ab'`. 32 | @useResult 33 | Parser<(R1, R2)> toSequenceParser() => SequenceParser2($1, $2); 34 | } 35 | 36 | /// A parser that consumes a sequence of 2 parsers and returns a [Record] with 37 | /// 2 positional parse results. 38 | class SequenceParser2 extends Parser<(R1, R2)> 39 | implements SequentialParser { 40 | SequenceParser2(this.parser1, this.parser2); 41 | 42 | Parser parser1; 43 | Parser parser2; 44 | 45 | @override 46 | Result<(R1, R2)> parseOn(Context context) { 47 | final result1 = parser1.parseOn(context); 48 | if (result1 is Failure) return result1; 49 | final result2 = parser2.parseOn(result1); 50 | if (result2 is Failure) return result2; 51 | return result2.success((result1.value, result2.value)); 52 | } 53 | 54 | @override 55 | int fastParseOn(String buffer, int position) { 56 | position = parser1.fastParseOn(buffer, position); 57 | if (position < 0) return -1; 58 | position = parser2.fastParseOn(buffer, position); 59 | if (position < 0) return -1; 60 | return position; 61 | } 62 | 63 | @override 64 | List get children => [parser1, parser2]; 65 | 66 | @override 67 | void replace(Parser source, Parser target) { 68 | super.replace(source, target); 69 | if (parser1 == source) parser1 = target as Parser; 70 | if (parser2 == source) parser2 = target as Parser; 71 | } 72 | 73 | @override 74 | SequenceParser2 copy() => SequenceParser2(parser1, parser2); 75 | } 76 | 77 | /// Extension on a [Record] with 2 positional values. 78 | extension RecordOfValuesExtension2 on (T1, T2) { 79 | /// Converts this [Record] with 2 positional values to a new type [R] using 80 | /// the provided [callback] with 2 positional arguments. 81 | @preferInline 82 | R map(R Function(T1, T2) callback) => callback($1, $2); 83 | } 84 | 85 | /// Extension on a [Parser] producing a [Record] of 2 positional values. 86 | extension RecordParserExtension2 on Parser<(T1, T2)> { 87 | /// Maps a parsed [Record] to [R] using the provided [callback], see 88 | /// [MapParserExtension.map] for details. 89 | @useResult 90 | Parser map2( 91 | R Function(T1, T2) callback, { 92 | bool hasSideEffects = false, 93 | }) => map((record) => record.map(callback), hasSideEffects: hasSideEffects); 94 | } 95 | -------------------------------------------------------------------------------- /lib/src/definition/grammar.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../core/parser.dart'; 4 | import 'reference.dart'; 5 | import 'resolve.dart'; 6 | 7 | /// Helper to conveniently define and build complex, recursive grammars using 8 | /// plain Dart code. 9 | /// 10 | /// To create a new grammar definition subclass [GrammarDefinition]. For every 11 | /// production create a new method returning the primitive parser defining it. 12 | /// The method called [start] is supposed to return the start production of the 13 | /// grammar (that can be customized when building the parsers). To refer to 14 | /// another production use [ref0] with the function reference as the argument. 15 | /// 16 | /// Consider the following example to parse a list of numbers: 17 | /// 18 | /// ```dart 19 | /// class ListGrammarDefinition extends GrammarDefinition { 20 | /// Parser start() => ref0(list).end(); 21 | /// Parser list() => ref0(element) & char(',') & ref0(list) 22 | /// | ref0(element); 23 | /// Parser element() => digit().plus().flatten(); 24 | /// } 25 | /// ``` 26 | /// 27 | /// Since this is plain Dart code, common refactorings such as renaming a 28 | /// production updates all references correctly. Also code navigation and code 29 | /// completion works as expected. 30 | /// 31 | /// To attach custom production actions you might want to further subclass your 32 | /// grammar definition and override overriding the necessary productions defined 33 | /// in the superclass: 34 | /// 35 | /// ```dart 36 | /// class ListParserDefinition extends ListGrammarDefinition { 37 | /// Parser element() => super.element().map((value) => int.parse(value)); 38 | /// } 39 | /// ``` 40 | /// 41 | /// Note that productions can be parametrized. Define such productions with 42 | /// positional arguments, and refer to them using [ref1], [ref2], ... where 43 | /// the number corresponds to the argument count. 44 | /// 45 | /// Consider extending the above grammar with a parametrized token production: 46 | /// 47 | /// ```dart 48 | /// class TokenizedListGrammarDefinition extends GrammarDefinition { 49 | /// Parser start() => ref0(list).end(); 50 | /// Parser list() => ref0(element) & ref1(token, char(',')) & ref0(list) 51 | /// | ref0(element); 52 | /// Parser element() => ref1(token, digit().plus()); 53 | /// Parser token(Parser parser) => parser.token().trim(); 54 | /// } 55 | /// ``` 56 | /// 57 | /// To get a runnable parser call the [build] method on the definition. It 58 | /// resolves recursive references and returns an efficient parser that can be 59 | /// further composed. The optional `start` reference specifies a different 60 | /// starting production within the grammar. The optional `arguments` 61 | /// parametrize the start production. 62 | /// 63 | /// ```dart 64 | /// final parser = new ListParserDefinition().build(); 65 | /// 66 | /// parser.parse('1'); // [1] 67 | /// parser.parse('1,2,3'); // [1, 2, 3] 68 | /// ``` 69 | @optionalTypeArgs 70 | abstract class GrammarDefinition { 71 | const GrammarDefinition(); 72 | 73 | /// The starting production of this definition. 74 | Parser start(); 75 | 76 | /// Builds the default composite parser starting at [start]. 77 | /// 78 | /// To start the building at a different production use [buildFrom]. 79 | @useResult 80 | Parser build() => buildFrom(ref0(start)); 81 | 82 | /// Builds a composite parser starting with the specified [parser]. 83 | /// 84 | /// As argument either pass a reference to a production in this definition, or 85 | /// any other parser using productions in this definition. 86 | @useResult 87 | Parser buildFrom(Parser parser) => resolve(parser); 88 | } 89 | -------------------------------------------------------------------------------- /lib/src/reflection/linter.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../core/parser.dart'; 4 | import 'analyzer.dart'; 5 | import 'internal/linter_rules.dart'; 6 | 7 | /// The type of a linter issue. 8 | enum LinterType { info, warning, error } 9 | 10 | /// Encapsulates a single linter rule. 11 | @immutable 12 | abstract class LinterRule { 13 | /// Constructs a new linter rule. 14 | const LinterRule(this.type, this.title); 15 | 16 | /// Severity of issues detected by this rule. 17 | final LinterType type; 18 | 19 | /// Human readable title of this rule. 20 | final String title; 21 | 22 | /// Executes this rule using a provided [analyzer] on a [parser]. Expected 23 | /// to call [callback] zero or more times as issues are detected. 24 | void run(Analyzer analyzer, Parser parser, LinterCallback callback); 25 | 26 | @override 27 | String toString() => '$runtimeType(type: $type, title: $title)'; 28 | } 29 | 30 | /// Encapsulates a single linter issue. 31 | @immutable 32 | class LinterIssue { 33 | /// Constructs a new linter rule. 34 | const LinterIssue(this.rule, this.parser, this.description); 35 | 36 | /// Rule that identified the issue. 37 | final LinterRule rule; 38 | 39 | /// Severity of the issue. 40 | LinterType get type => rule.type; 41 | 42 | /// Title of the issue. 43 | String get title => rule.title; 44 | 45 | /// Parser object with the issue. 46 | final Parser parser; 47 | 48 | /// Detailed explanation of the issue. 49 | final String description; 50 | 51 | @override 52 | String toString() => 53 | '$runtimeType(type: $type, title: $title, ' 54 | 'parser: $parser, description: $description)'; 55 | } 56 | 57 | /// Function signature of a linter callback that is called whenever a linter 58 | /// rule identifies an issue. 59 | typedef LinterCallback = void Function(LinterIssue issue); 60 | 61 | /// All default linter rules to be run. 62 | const allLinterRules = [ 63 | CharacterRepeater(), 64 | DuplicateParser(), 65 | LeftRecursion(), 66 | NestedChoice(), 67 | NullableRepeater(), 68 | OverlappingChoice(), 69 | RepeatedChoice(), 70 | UnnecessaryFlatten(), 71 | UnnecessaryResolvable(), 72 | UnoptimizedFlatten(), 73 | UnreachableChoice(), 74 | UnresolvedSettable(), 75 | UnusedResult(), 76 | ]; 77 | 78 | /// Returns a list of linter issues found when analyzing the parser graph 79 | /// reachable from [parser]. 80 | /// 81 | /// The optional [callback] is triggered during the search for each issue 82 | /// discovered. 83 | /// 84 | /// A custom list of [rules] can be provided, otherwise [allLinterRules] are 85 | /// used and filtered by the set of [excludedRules] and [excludedTypes] (rules 86 | /// of `LinterType.info` are ignored by default). 87 | List linter( 88 | Parser parser, { 89 | LinterCallback? callback, 90 | List? rules, 91 | Set excludedRules = const {}, 92 | Set excludedTypes = const {LinterType.info}, 93 | }) { 94 | final issues = []; 95 | final analyzer = Analyzer(parser); 96 | final selectedRules = 97 | rules ?? 98 | allLinterRules 99 | .where( 100 | (rule) => 101 | !excludedRules.contains(rule.title) && 102 | !excludedTypes.contains(rule.type), 103 | ) 104 | .toList(growable: false); 105 | for (final parser in analyzer.parsers) { 106 | for (final rule in selectedRules) { 107 | rule.run(analyzer, parser, (issue) { 108 | if (callback != null) { 109 | callback(issue); 110 | } 111 | issues.add(issue); 112 | }); 113 | } 114 | } 115 | return issues; 116 | } 117 | -------------------------------------------------------------------------------- /lib/src/core/token.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | 3 | import 'package:meta/meta.dart'; 4 | 5 | import '../matcher/matches.dart'; 6 | import '../parser/action/token.dart'; 7 | import '../parser/misc/newline.dart'; 8 | import 'parser.dart'; 9 | 10 | /// A token represents a parsed part of the input stream. 11 | /// 12 | /// The token holds the resulting value of the input, the input buffer, 13 | /// and the start and stop position in the input buffer. It provides many 14 | /// convenience methods to access the state of the token. 15 | @immutable 16 | class Token { 17 | /// Constructs a token from the parsed value, the input buffer, and the 18 | /// start and stop position in the input buffer. 19 | const Token(this.value, this.buffer, this.start, this.stop); 20 | 21 | /// The parsed value of the token. 22 | final R value; 23 | 24 | /// The parsed buffer of the token. 25 | final String buffer; 26 | 27 | /// The start position of the token in the buffer. 28 | final int start; 29 | 30 | /// The stop position of the token in the buffer. 31 | final int stop; 32 | 33 | /// The consumed input of the token. 34 | String get input => buffer.substring(start, stop); 35 | 36 | /// The length of the token. 37 | int get length => stop - start; 38 | 39 | /// The line number of the token. 40 | int get line => Token.lineAndColumnOf(buffer, start)[0]; 41 | 42 | /// The column number of this token. 43 | int get column => Token.lineAndColumnOf(buffer, start)[1]; 44 | 45 | @override 46 | String toString() => '$runtimeType[${positionString(buffer, start)}]: $value'; 47 | 48 | @override 49 | bool operator ==(Object other) => 50 | other is Token && 51 | value == other.value && 52 | start == other.start && 53 | stop == other.stop; 54 | 55 | @override 56 | int get hashCode => value.hashCode + start.hashCode + stop.hashCode; 57 | 58 | /// Combines multiple token into a single token with the list of its values. 59 | static Token> join(Iterable> token) { 60 | final iterator = token.iterator; 61 | if (!iterator.moveNext()) { 62 | throw ArgumentError.value(token, 'token', 'Require at least one token'); 63 | } 64 | final value = [iterator.current.value]; 65 | final buffer = iterator.current.buffer; 66 | var start = iterator.current.start; 67 | var stop = iterator.current.stop; 68 | while (iterator.moveNext()) { 69 | if (buffer != iterator.current.buffer) { 70 | throw ArgumentError.value( 71 | token, 72 | 'token', 73 | 'Token do not use same buffer', 74 | ); 75 | } 76 | value.add(iterator.current.value); 77 | start = math.min(start, iterator.current.start); 78 | stop = math.max(stop, iterator.current.stop); 79 | } 80 | return Token(value, buffer, start, stop); 81 | } 82 | 83 | /// Returns a parser that detects newlines platform independently. 84 | static Parser newlineParser() => _newlineParser; 85 | static final _newlineParser = newline(); 86 | 87 | /// Converts the [position] index in a [buffer] to a line and column tuple. 88 | static List lineAndColumnOf(String buffer, int position) { 89 | var line = 1, offset = 0; 90 | for (final token in newlineParser().token().allMatches(buffer)) { 91 | if (position < token.stop) { 92 | return [line, position - offset + 1]; 93 | } 94 | line++; 95 | offset = token.stop; 96 | } 97 | return [line, position - offset + 1]; 98 | } 99 | 100 | /// Returns a human readable string representing the [position] index in a 101 | /// [buffer]. 102 | static String positionString(String buffer, int position) { 103 | final lineAndColumn = lineAndColumnOf(buffer, position); 104 | return '${lineAndColumn[0]}:${lineAndColumn[1]}'; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /lib/src/parser/combinator/choice.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/parser.dart'; 5 | import '../../core/result.dart'; 6 | import '../../shared/pragma.dart'; 7 | import '../utils/failure_joiner.dart'; 8 | import 'list.dart'; 9 | 10 | extension ChoiceParserExtension on Parser { 11 | /// Returns a parser that accepts the receiver or [other]. The resulting 12 | /// parser returns the parse result of the receiver, if the receiver fails 13 | /// it returns the parse result of [other] (exclusive ordered choice). 14 | /// 15 | /// An optional [failureJoiner] can be specified that determines which 16 | /// [Failure] to return in case both parsers fail. By default the last 17 | /// failure is returned [selectLast], but [selectFarthest] is another 18 | /// common choice that usually gives better error messages. 19 | /// 20 | /// For example, the parser `letter().or(digit())` accepts a letter or a 21 | /// digit. An example where the order matters is the following choice between 22 | /// overlapping parsers: `letter().or(char('a'))`. In the example the parser 23 | /// `char('a')` will never be activated, because the input is always consumed 24 | /// by `letter()`. This can be problematic if the author intended to attach a 25 | /// production action to `char('a')`. 26 | /// 27 | /// Due to https://github.com/dart-lang/language/issues/1557 the resulting 28 | /// parser cannot be properly typed. Please use [ChoiceIterableExtension] 29 | /// as a workaround: `[first, second].toChoiceParser()`. 30 | @useResult 31 | ChoiceParser or(Parser other, {FailureJoiner? failureJoiner}) => 32 | switch (this) { 33 | ChoiceParser( 34 | children: final children, 35 | failureJoiner: final thisFailureJoiner, 36 | ) => 37 | [ 38 | ...children, 39 | other, 40 | ].toChoiceParser(failureJoiner: failureJoiner ?? thisFailureJoiner), 41 | _ => [this, other].toChoiceParser(failureJoiner: failureJoiner), 42 | }; 43 | 44 | /// Convenience operator returning a parser that accepts the receiver or 45 | /// [other]. See [or] for details. 46 | @useResult 47 | ChoiceParser operator |(Parser other) => or(other); 48 | } 49 | 50 | extension ChoiceIterableExtension on Iterable> { 51 | /// Converts the parser in this iterable to a choice of parsers. 52 | ChoiceParser toChoiceParser({FailureJoiner? failureJoiner}) => 53 | ChoiceParser(this, failureJoiner: failureJoiner); 54 | } 55 | 56 | /// A parser that uses the first parser that succeeds. 57 | class ChoiceParser extends ListParser { 58 | ChoiceParser(super.children, {FailureJoiner? failureJoiner}) 59 | : assert(children.isNotEmpty, 'Choice parser cannot be empty'), 60 | failureJoiner = failureJoiner ?? selectLast; 61 | 62 | /// Strategy to join multiple parse errors. 63 | final FailureJoiner failureJoiner; 64 | 65 | @override 66 | @noBoundsChecks 67 | Result parseOn(Context context) { 68 | // Check the first choice: 69 | final result = children[0].parseOn(context); 70 | if (result is! Failure) return result; 71 | var failure = result; 72 | // Check all other choices: 73 | for (var i = 1; i < children.length; i++) { 74 | final result = children[i].parseOn(context); 75 | if (result is! Failure) return result; 76 | failure = failureJoiner(failure, result); 77 | } 78 | return failure; 79 | } 80 | 81 | @override 82 | @noBoundsChecks 83 | int fastParseOn(String buffer, int position) { 84 | var result = -1; 85 | for (var i = 0; i < children.length; i++) { 86 | result = children[i].fastParseOn(buffer, position); 87 | if (result >= 0) return result; 88 | } 89 | return result; 90 | } 91 | 92 | @override 93 | bool hasEqualProperties(ChoiceParser other) => 94 | super.hasEqualProperties(other) && failureJoiner == other.failureJoiner; 95 | 96 | @override 97 | ChoiceParser copy() => 98 | ChoiceParser(children, failureJoiner: failureJoiner); 99 | } 100 | -------------------------------------------------------------------------------- /lib/src/parser/combinator/generated/sequence_3.dart: -------------------------------------------------------------------------------- 1 | // AUTO-GENERATED CODE: DO NOT EDIT 2 | 3 | import 'package:meta/meta.dart'; 4 | 5 | import '../../../core/context.dart'; 6 | import '../../../core/parser.dart'; 7 | import '../../../core/result.dart'; 8 | import '../../../shared/pragma.dart'; 9 | import '../../action/map.dart'; 10 | import '../../utils/sequential.dart'; 11 | 12 | /// Creates a [Parser] that consumes the 3 parsers passed as argument in 13 | /// sequence and returns a [Record] with the 3 positional parse results. 14 | /// 15 | /// For example, 16 | /// the parser `seq3(char('a'), char('b'), char('c'))` 17 | /// returns `('a', 'b', 'c')` 18 | /// for the input `'abc'`. 19 | @useResult 20 | Parser<(R1, R2, R3)> seq3( 21 | Parser parser1, 22 | Parser parser2, 23 | Parser parser3, 24 | ) => SequenceParser3(parser1, parser2, parser3); 25 | 26 | /// Extensions on a [Record] with 3 positional [Parser]s. 27 | extension RecordOfParsersExtension3 28 | on (Parser, Parser, Parser) { 29 | /// Converts a [Record] of 3 positional parsers to a [Parser] that runs the 30 | /// parsers in sequence and returns a [Record] with 3 positional parse results. 31 | /// 32 | /// For example, 33 | /// the parser `(char('a'), char('b'), char('c')).toSequenceParser()` 34 | /// returns `('a', 'b', 'c')` 35 | /// for the input `'abc'`. 36 | @useResult 37 | Parser<(R1, R2, R3)> toSequenceParser() => 38 | SequenceParser3($1, $2, $3); 39 | } 40 | 41 | /// A parser that consumes a sequence of 3 parsers and returns a [Record] with 42 | /// 3 positional parse results. 43 | class SequenceParser3 extends Parser<(R1, R2, R3)> 44 | implements SequentialParser { 45 | SequenceParser3(this.parser1, this.parser2, this.parser3); 46 | 47 | Parser parser1; 48 | Parser parser2; 49 | Parser parser3; 50 | 51 | @override 52 | Result<(R1, R2, R3)> parseOn(Context context) { 53 | final result1 = parser1.parseOn(context); 54 | if (result1 is Failure) return result1; 55 | final result2 = parser2.parseOn(result1); 56 | if (result2 is Failure) return result2; 57 | final result3 = parser3.parseOn(result2); 58 | if (result3 is Failure) return result3; 59 | return result3.success((result1.value, result2.value, result3.value)); 60 | } 61 | 62 | @override 63 | int fastParseOn(String buffer, int position) { 64 | position = parser1.fastParseOn(buffer, position); 65 | if (position < 0) return -1; 66 | position = parser2.fastParseOn(buffer, position); 67 | if (position < 0) return -1; 68 | position = parser3.fastParseOn(buffer, position); 69 | if (position < 0) return -1; 70 | return position; 71 | } 72 | 73 | @override 74 | List get children => [parser1, parser2, parser3]; 75 | 76 | @override 77 | void replace(Parser source, Parser target) { 78 | super.replace(source, target); 79 | if (parser1 == source) parser1 = target as Parser; 80 | if (parser2 == source) parser2 = target as Parser; 81 | if (parser3 == source) parser3 = target as Parser; 82 | } 83 | 84 | @override 85 | SequenceParser3 copy() => 86 | SequenceParser3(parser1, parser2, parser3); 87 | } 88 | 89 | /// Extension on a [Record] with 3 positional values. 90 | extension RecordOfValuesExtension3 on (T1, T2, T3) { 91 | /// Converts this [Record] with 3 positional values to a new type [R] using 92 | /// the provided [callback] with 3 positional arguments. 93 | @preferInline 94 | R map(R Function(T1, T2, T3) callback) => callback($1, $2, $3); 95 | } 96 | 97 | /// Extension on a [Parser] producing a [Record] of 3 positional values. 98 | extension RecordParserExtension3 on Parser<(T1, T2, T3)> { 99 | /// Maps a parsed [Record] to [R] using the provided [callback], see 100 | /// [MapParserExtension.map] for details. 101 | @useResult 102 | Parser map3( 103 | R Function(T1, T2, T3) callback, { 104 | bool hasSideEffects = false, 105 | }) => map((record) => record.map(callback), hasSideEffects: hasSideEffects); 106 | } 107 | -------------------------------------------------------------------------------- /lib/src/reflection/analyzer.dart: -------------------------------------------------------------------------------- 1 | import '../core/parser.dart'; 2 | import '../parser/misc/epsilon.dart'; 3 | import '../shared/types.dart'; 4 | import 'internal/cycle_set.dart'; 5 | import 'internal/first_set.dart'; 6 | import 'internal/follow_set.dart'; 7 | import 'internal/path.dart'; 8 | import 'iterable.dart'; 9 | 10 | /// Helper to reflect on properties of a grammar. 11 | class Analyzer { 12 | /// Constructs an analyzer on the parser graph starting at [root]. 13 | Analyzer(this.root); 14 | 15 | /// The start parser of analysis. 16 | final Parser root; 17 | 18 | /// Returns a set of all parsers reachable from [root]. 19 | Iterable get parsers => _parsers; 20 | 21 | late final Set _parsers = allParser(root).toSet(); 22 | 23 | /// Returns a set of all deep children reachable from [parser]. 24 | /// 25 | /// The returned set does only include the [parser] itself, if it is 26 | /// recursively calling itself. 27 | Set allChildren(Parser parser) { 28 | assert(parsers.contains(parser), 'parser is not part of the analyzer'); 29 | return _allChildren.putIfAbsent( 30 | parser, 31 | () => parser.children.fold( 32 | {}, 33 | (result, child) => result..addAll(allParser(child)), 34 | ), 35 | ); 36 | } 37 | 38 | late final Map> _allChildren = {}; 39 | 40 | /// Returns the shortest path from [source] that satisfies the given 41 | /// [predicate], if any. 42 | ParserPath? findPath(Parser source, Predicate predicate) { 43 | ParserPath? path; 44 | for (final current in findAllPaths(source, predicate)) { 45 | if (path == null || current.length < path.length) { 46 | path = current; 47 | } 48 | } 49 | return path; 50 | } 51 | 52 | /// Returns the shortest path from [source] to [target], if any. 53 | ParserPath? findPathTo(Parser source, Parser target) { 54 | assert(parsers.contains(target), 'target is not part of the analyzer'); 55 | return findPath(source, (path) => path.target == target); 56 | } 57 | 58 | /// Returns all paths starting at [source] that satisfy the given [predicate]. 59 | Iterable findAllPaths( 60 | Parser source, 61 | Predicate predicate, 62 | ) { 63 | assert(parsers.contains(source), 'source is not part of the analyzer'); 64 | return depthFirstSearch(ParserPath([source], []), predicate); 65 | } 66 | 67 | /// Returns all paths starting at [source] that end in [target]. 68 | Iterable findAllPathsTo(Parser source, Parser target) { 69 | assert(parsers.contains(target), 'target is not part of the analyzer'); 70 | return findAllPaths(source, (path) => path.target == target); 71 | } 72 | 73 | /// Returns `true` if [parser] is transitively nullable, that is it can 74 | /// successfully parse nothing. 75 | bool isNullable(Parser parser) => _firstSets[parser]!.contains(sentinel); 76 | 77 | /// Returns the first-set of [parser]. 78 | /// 79 | /// The first-set of a parser is the set of terminal parsers which can appear 80 | /// as the first element of any chain of parsers derivable from [parser]. 81 | /// Includes [sentinel], if the set is nullable. 82 | Iterable firstSet(Parser parser) => _firstSets[parser]!; 83 | 84 | late final Map> _firstSets = computeFirstSets( 85 | parsers: _parsers, 86 | sentinel: sentinel, 87 | ); 88 | 89 | /// Returns the follow-set of a [parser]. 90 | /// 91 | /// The follow-set of a parser is the list of terminal parsers that can 92 | /// appear immediately after [parser]. Includes [sentinel], if the parse can 93 | /// complete when starting at [root]. 94 | Iterable followSet(Parser parser) => _followSet[parser]!; 95 | 96 | late final Map> _followSet = computeFollowSets( 97 | root: root, 98 | parsers: _parsers, 99 | firstSets: _firstSets, 100 | sentinel: sentinel, 101 | ); 102 | 103 | /// Returns the cycle-set of a [parser]. 104 | Iterable cycleSet(Parser parser) => _cycleSet[parser]!; 105 | 106 | late final Map> _cycleSet = computeCycleSets( 107 | parsers: _parsers, 108 | firstSets: _firstSets, 109 | ); 110 | 111 | /// A unique parser used as a marker in [firstSet] and [followSet] 112 | /// computations. 113 | static final sentinel = EpsilonParser(null); 114 | } 115 | -------------------------------------------------------------------------------- /lib/src/parser/repeater/possessive.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/parser.dart'; 5 | import '../../core/result.dart'; 6 | import 'repeating.dart'; 7 | import 'unbounded.dart'; 8 | 9 | extension PossessiveRepeatingParserExtension on Parser { 10 | /// Returns a parser that accepts the receiver zero or more times. The 11 | /// resulting parser returns a list of the parse results of the receiver. 12 | /// 13 | /// This is a greedy and blind implementation that tries to consume as much 14 | /// input as possible and that does not consider what comes afterwards. 15 | /// 16 | /// For example, the parser `letter().star()` accepts the empty string or 17 | /// any sequence of letters and returns a possibly empty list of the parsed 18 | /// letters. 19 | @useResult 20 | Parser> star() => repeat(0, unbounded); 21 | 22 | /// Returns a parser that accepts the receiver one or more times. The 23 | /// resulting parser returns a list of the parse results of the receiver. 24 | /// 25 | /// This is a greedy and blind implementation that tries to consume as much 26 | /// input as possible and that does not consider what comes afterwards. 27 | /// 28 | /// For example, the parser `letter().plus()` accepts any sequence of 29 | /// letters and returns a list of the parsed letters. 30 | @useResult 31 | Parser> plus() => repeat(1, unbounded); 32 | 33 | /// Returns a parser that accepts the receiver exactly [count] times. The 34 | /// resulting parser returns a list of the parse results of the receiver. 35 | /// 36 | /// This is a greedy and blind implementation that tries to consume as much 37 | /// input as possible and that does not consider what comes afterwards. 38 | /// 39 | /// For example, the parser `letter().times(2)` accepts two letters and 40 | /// returns a list of the two parsed letters. 41 | @useResult 42 | Parser> times(int count) => repeat(count, count); 43 | 44 | /// Returns a parser that accepts the receiver between [min] and [max] times. 45 | /// The resulting parser returns a list of the parse results of the receiver. 46 | /// 47 | /// This is a greedy and blind implementation that tries to consume as much 48 | /// input as possible and that does not consider what comes afterwards. 49 | /// 50 | /// For example, the parser `letter().repeat(2, 4)` accepts a sequence of 51 | /// two, three, or four letters and returns the accepted letters as a list. 52 | @useResult 53 | Parser> repeat(int min, [int? max]) => 54 | PossessiveRepeatingParser(this, min, max ?? min); 55 | } 56 | 57 | /// A greedy parser that repeatedly parses between 'min' and 'max' instances of 58 | /// its delegate. 59 | class PossessiveRepeatingParser extends RepeatingParser> { 60 | PossessiveRepeatingParser(super.parser, super.min, super.max); 61 | 62 | @override 63 | Result> parseOn(Context context) { 64 | final elements = []; 65 | var current = context; 66 | while (elements.length < min) { 67 | final result = delegate.parseOn(current); 68 | if (result is Failure) return result; 69 | assert( 70 | current.position < result.position, 71 | '$delegate must always consume', 72 | ); 73 | elements.add(result.value); 74 | current = result; 75 | } 76 | while (elements.length < max) { 77 | final result = delegate.parseOn(current); 78 | if (result is Failure) break; 79 | assert( 80 | current.position < result.position, 81 | '$delegate must always consume', 82 | ); 83 | elements.add(result.value); 84 | current = result; 85 | } 86 | return current.success(elements); 87 | } 88 | 89 | @override 90 | int fastParseOn(String buffer, int position) { 91 | var count = 0; 92 | var current = position; 93 | while (count < min) { 94 | final result = delegate.fastParseOn(buffer, current); 95 | if (result < 0) return -1; 96 | assert(current < result, '$delegate must always consume'); 97 | current = result; 98 | count++; 99 | } 100 | while (count < max) { 101 | final result = delegate.fastParseOn(buffer, current); 102 | if (result < 0) break; 103 | assert(current < result, '$delegate must always consume'); 104 | current = result; 105 | count++; 106 | } 107 | return current; 108 | } 109 | 110 | @override 111 | PossessiveRepeatingParser copy() => 112 | PossessiveRepeatingParser(delegate, min, max); 113 | } 114 | -------------------------------------------------------------------------------- /lib/src/parser/repeater/lazy.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/parser.dart'; 5 | import '../../core/result.dart'; 6 | import 'greedy.dart'; 7 | import 'limited.dart'; 8 | import 'possessive.dart'; 9 | import 'unbounded.dart'; 10 | 11 | extension LazyRepeatingParserExtension on Parser { 12 | /// Returns a parser that parses the receiver zero or more times until it 13 | /// reaches a [limit]. This is a lazy non-blind implementation of the 14 | /// [PossessiveRepeatingParserExtension.star] operator. The [limit] is not 15 | /// consumed. 16 | /// 17 | /// For example, the parser `char('{') & any().starLazy(char('}')) & 18 | /// char('}')` only consumes the part `'{abc}'` of `'{abc}def}'`. 19 | /// 20 | /// See [GreedyRepeatingParserExtension.starGreedy] for the greedy and less 21 | /// efficient variation of this combinator. 22 | @useResult 23 | Parser> starLazy(Parser limit) => 24 | repeatLazy(limit, 0, unbounded); 25 | 26 | /// Returns a parser that parses the receiver one or more times until it 27 | /// reaches a [limit]. This is a lazy non-blind implementation of the 28 | /// [PossessiveRepeatingParserExtension.plus] operator. The [limit] is not 29 | /// consumed. 30 | /// 31 | /// For example, the parser `char('{') & any().plusLazy(char('}')) & 32 | /// char('}')` only consumes the part `'{abc}'` of `'{abc}def}'`. 33 | /// 34 | /// See [GreedyRepeatingParserExtension.plusGreedy] for the greedy and less 35 | /// efficient variation of this combinator. 36 | @useResult 37 | Parser> plusLazy(Parser limit) => 38 | repeatLazy(limit, 1, unbounded); 39 | 40 | /// Returns a parser that parses the receiver at least [min] and at most [max] 41 | /// times until it reaches a [limit]. This is a lazy non-blind implementation 42 | /// of the [PossessiveRepeatingParserExtension.repeat] operator. The [limit] 43 | /// is not consumed. 44 | /// 45 | /// This is the more generic variation of the [starLazy] and [plusLazy] 46 | /// combinators. 47 | @useResult 48 | Parser> repeatLazy(Parser limit, int min, int max) => 49 | LazyRepeatingParser(this, limit, min, max); 50 | } 51 | 52 | /// A lazy repeating parser, commonly seen in regular expression 53 | /// implementations. It limits its consumption to meet the 'limit' condition as 54 | /// early as possible. 55 | class LazyRepeatingParser extends LimitedRepeatingParser { 56 | LazyRepeatingParser(super.parser, super.limit, super.min, super.max); 57 | 58 | @override 59 | Result> parseOn(Context context) { 60 | var current = context; 61 | final elements = []; 62 | while (elements.length < min) { 63 | final result = delegate.parseOn(current); 64 | if (result is Failure) return result; 65 | assert( 66 | current.position < result.position, 67 | '$delegate must always consume', 68 | ); 69 | elements.add(result.value); 70 | current = result; 71 | } 72 | for (;;) { 73 | final limiter = limit.parseOn(current); 74 | if (limiter is Failure) { 75 | if (elements.length >= max) return limiter; 76 | final result = delegate.parseOn(current); 77 | if (result is Failure) return limiter; 78 | assert( 79 | current.position < result.position, 80 | '$delegate must always consume', 81 | ); 82 | elements.add(result.value); 83 | current = result; 84 | } else { 85 | return current.success(elements); 86 | } 87 | } 88 | } 89 | 90 | @override 91 | int fastParseOn(String buffer, int position) { 92 | var count = 0; 93 | var current = position; 94 | while (count < min) { 95 | final result = delegate.fastParseOn(buffer, current); 96 | if (result < 0) return -1; 97 | assert(current < result, '$delegate must always consume'); 98 | current = result; 99 | count++; 100 | } 101 | for (;;) { 102 | final limiter = limit.fastParseOn(buffer, current); 103 | if (limiter < 0) { 104 | if (count >= max) return -1; 105 | final result = delegate.fastParseOn(buffer, current); 106 | if (result < 0) return -1; 107 | assert(current < result, '$delegate must always consume'); 108 | current = result; 109 | count++; 110 | } else { 111 | return current; 112 | } 113 | } 114 | } 115 | 116 | @override 117 | LazyRepeatingParser copy() => 118 | LazyRepeatingParser(delegate, limit, min, max); 119 | } 120 | -------------------------------------------------------------------------------- /lib/src/expression/builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../core/parser.dart'; 4 | import '../parser/combinator/settable.dart'; 5 | import '../reflection/iterable.dart'; 6 | import 'group.dart'; 7 | import 'utils.dart'; 8 | 9 | /// A builder that allows the simple definition of expression grammars with 10 | /// prefix, postfix, and left- and right-associative infix operators. 11 | /// 12 | /// The following code creates the empty expression builder producing values of 13 | /// type [num]: 14 | /// 15 | /// ```dart 16 | /// final builder = ExpressionBuilder(); 17 | /// ``` 18 | /// 19 | /// Every [ExpressionBuilder] needs to define at least one primitive type to 20 | /// parse. In this example these are the literal numbers. The mapping function 21 | /// converts the string input into an actual number. 22 | /// 23 | /// ```dart 24 | /// builder.primitive(digit() 25 | /// .plus() 26 | /// .seq(char('.').seq(digit().plus()).optional()) 27 | /// .flatten() 28 | /// .trim() 29 | /// .map(num.parse)); 30 | /// ```dart 31 | /// 32 | /// Then we define the operator-groups in descending precedence. The highest 33 | /// precedence have parentheses. The mapping function receives both the opening 34 | /// parenthesis, the value, and the closing parenthesis as arguments: 35 | /// 36 | /// ```dart 37 | /// builder.group().wrapper( 38 | /// char('(').trim(), char(')').trim(), (left, value, right) => value); 39 | /// ``` 40 | /// 41 | /// Then come the normal arithmetic operators. We are using 42 | /// [cascade notation](https://dart.dev/guides/language/language-tour#cascade-notation) 43 | /// to define multiple operators on the same precedence-group. The mapping 44 | /// functions receive both, the terms and the parsed operator in the order they 45 | /// appear in the parsed input: 46 | /// 47 | /// ```dart 48 | /// // Negation is a prefix operator. 49 | /// builder.group().prefix(char('-').trim(), (operator, value) => -value); 50 | /// 51 | /// // Power is right-associative. 52 | /// builder.group().right(char('^').trim(), (left, operator, right) => math.pow(left, right)); 53 | /// 54 | /// // Multiplication and addition are left-associative, multiplication has 55 | /// // higher priority than addition. 56 | /// builder.group() 57 | /// ..left(char('*').trim(), (left, operator, right) => left * right) 58 | /// ..left(char('/').trim(), (left, operator, right) => left / right); 59 | /// builder.group() 60 | /// ..left(char('+').trim(), (left, operator, right) => left + right) 61 | /// ..left(char('-').trim(), (left, operator, right) => left - right); 62 | /// ``` 63 | /// 64 | /// Finally we can build the parser: 65 | /// 66 | /// ```dart 67 | /// final parser = builder.build(); 68 | /// ``` 69 | /// 70 | /// After executing the above code we get an efficient parser that correctly 71 | /// evaluates expressions like: 72 | /// 73 | /// ```dart 74 | /// parser.parse('-8'); // -8 75 | /// parser.parse('1+2*3'); // 7 76 | /// parser.parse('1*2+3'); // 5 77 | /// parser.parse('8/4/2'); // 2 78 | /// parser.parse('2^2^3'); // 256 79 | /// ``` 80 | class ExpressionBuilder { 81 | final List> _primitives = []; 82 | final List> _groups = []; 83 | final SettableParser _loopback = undefined(); 84 | 85 | /// The parser for this expression builder. Can be used to loop back to this 86 | /// parser. 87 | Parser get loopback => _loopback; 88 | 89 | /// Defines a new primitive, literal, or value [parser]. 90 | void primitive(Parser parser) => _primitives.add(parser); 91 | 92 | /// Creates a new group of operators that share the same priority. 93 | @useResult 94 | ExpressionGroup group() { 95 | final group = ExpressionGroup(_loopback); 96 | _groups.add(group); 97 | return group; 98 | } 99 | 100 | /// Builds the expression parser. 101 | @useResult 102 | Parser build() { 103 | assert(_primitives.isNotEmpty, 'At least one primitive parser expected'); 104 | final parser = _groups.fold>( 105 | buildChoice(_primitives), 106 | (parser, group) => group.build(parser), 107 | ); 108 | // Replace all uses of `_loopback` with `parser`. Do not use `resolve()` 109 | // because that might try to resolve unrelated parsers outside of the scope 110 | // of the `ExpressionBuilder` and cause infinite recursion. 111 | for (final parent in allParser(parser)) { 112 | parent.replace(_loopback, parser); 113 | } 114 | // Also update the `_loopback` parser, just in case somebody keeps a reference 115 | // to it (not that anybody should do that). 116 | _loopback.set(parser); 117 | return parser; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /lib/src/parser/combinator/generated/sequence_4.dart: -------------------------------------------------------------------------------- 1 | // AUTO-GENERATED CODE: DO NOT EDIT 2 | 3 | import 'package:meta/meta.dart'; 4 | 5 | import '../../../core/context.dart'; 6 | import '../../../core/parser.dart'; 7 | import '../../../core/result.dart'; 8 | import '../../../shared/pragma.dart'; 9 | import '../../action/map.dart'; 10 | import '../../utils/sequential.dart'; 11 | 12 | /// Creates a [Parser] that consumes the 4 parsers passed as argument in 13 | /// sequence and returns a [Record] with the 4 positional parse results. 14 | /// 15 | /// For example, 16 | /// the parser `seq4(char('a'), char('b'), char('c'), char('d'))` 17 | /// returns `('a', 'b', 'c', 'd')` 18 | /// for the input `'abcd'`. 19 | @useResult 20 | Parser<(R1, R2, R3, R4)> seq4( 21 | Parser parser1, 22 | Parser parser2, 23 | Parser parser3, 24 | Parser parser4, 25 | ) => SequenceParser4(parser1, parser2, parser3, parser4); 26 | 27 | /// Extensions on a [Record] with 4 positional [Parser]s. 28 | extension RecordOfParsersExtension4 29 | on (Parser, Parser, Parser, Parser) { 30 | /// Converts a [Record] of 4 positional parsers to a [Parser] that runs the 31 | /// parsers in sequence and returns a [Record] with 4 positional parse results. 32 | /// 33 | /// For example, 34 | /// the parser `(char('a'), char('b'), char('c'), char('d')).toSequenceParser()` 35 | /// returns `('a', 'b', 'c', 'd')` 36 | /// for the input `'abcd'`. 37 | @useResult 38 | Parser<(R1, R2, R3, R4)> toSequenceParser() => 39 | SequenceParser4($1, $2, $3, $4); 40 | } 41 | 42 | /// A parser that consumes a sequence of 4 parsers and returns a [Record] with 43 | /// 4 positional parse results. 44 | class SequenceParser4 extends Parser<(R1, R2, R3, R4)> 45 | implements SequentialParser { 46 | SequenceParser4(this.parser1, this.parser2, this.parser3, this.parser4); 47 | 48 | Parser parser1; 49 | Parser parser2; 50 | Parser parser3; 51 | Parser parser4; 52 | 53 | @override 54 | Result<(R1, R2, R3, R4)> parseOn(Context context) { 55 | final result1 = parser1.parseOn(context); 56 | if (result1 is Failure) return result1; 57 | final result2 = parser2.parseOn(result1); 58 | if (result2 is Failure) return result2; 59 | final result3 = parser3.parseOn(result2); 60 | if (result3 is Failure) return result3; 61 | final result4 = parser4.parseOn(result3); 62 | if (result4 is Failure) return result4; 63 | return result4.success(( 64 | result1.value, 65 | result2.value, 66 | result3.value, 67 | result4.value, 68 | )); 69 | } 70 | 71 | @override 72 | int fastParseOn(String buffer, int position) { 73 | position = parser1.fastParseOn(buffer, position); 74 | if (position < 0) return -1; 75 | position = parser2.fastParseOn(buffer, position); 76 | if (position < 0) return -1; 77 | position = parser3.fastParseOn(buffer, position); 78 | if (position < 0) return -1; 79 | position = parser4.fastParseOn(buffer, position); 80 | if (position < 0) return -1; 81 | return position; 82 | } 83 | 84 | @override 85 | List get children => [parser1, parser2, parser3, parser4]; 86 | 87 | @override 88 | void replace(Parser source, Parser target) { 89 | super.replace(source, target); 90 | if (parser1 == source) parser1 = target as Parser; 91 | if (parser2 == source) parser2 = target as Parser; 92 | if (parser3 == source) parser3 = target as Parser; 93 | if (parser4 == source) parser4 = target as Parser; 94 | } 95 | 96 | @override 97 | SequenceParser4 copy() => 98 | SequenceParser4(parser1, parser2, parser3, parser4); 99 | } 100 | 101 | /// Extension on a [Record] with 4 positional values. 102 | extension RecordOfValuesExtension4 on (T1, T2, T3, T4) { 103 | /// Converts this [Record] with 4 positional values to a new type [R] using 104 | /// the provided [callback] with 4 positional arguments. 105 | @preferInline 106 | R map(R Function(T1, T2, T3, T4) callback) => callback($1, $2, $3, $4); 107 | } 108 | 109 | /// Extension on a [Parser] producing a [Record] of 4 positional values. 110 | extension RecordParserExtension4 on Parser<(T1, T2, T3, T4)> { 111 | /// Maps a parsed [Record] to [R] using the provided [callback], see 112 | /// [MapParserExtension.map] for details. 113 | @useResult 114 | Parser map4( 115 | R Function(T1, T2, T3, T4) callback, { 116 | bool hasSideEffects = false, 117 | }) => map((record) => record.map(callback), hasSideEffects: hasSideEffects); 118 | } 119 | -------------------------------------------------------------------------------- /lib/src/parser/predicate/unicode_character.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/result.dart'; 5 | import '../../shared/pragma.dart'; 6 | import '../character/predicate.dart'; 7 | import '../character/predicate/constant.dart'; 8 | import 'character.dart'; 9 | 10 | /// Parser class for an individual Unicode code-point (including possible 11 | /// surrogate pairs) satisfying a specified [CharacterPredicate]. 12 | class UnicodeCharacterParser extends CharacterParser { 13 | factory UnicodeCharacterParser( 14 | CharacterPredicate predicate, 15 | String message, 16 | ) => ConstantCharPredicate.any.isEqualTo(predicate) 17 | ? AnyUnicodeCharacterParser.internal(predicate, message) 18 | : UnicodeCharacterParser.internal(predicate, message); 19 | 20 | @internal 21 | UnicodeCharacterParser.internal(super.predicate, super.message) 22 | : super.internal(); 23 | 24 | @override 25 | @noBoundsChecks 26 | Result parseOn(Context context) { 27 | final buffer = context.buffer; 28 | final position = context.position; 29 | if (position < buffer.length) { 30 | var codeUnit = buffer.codeUnitAt(position); 31 | var nextPosition = position + 1; 32 | if (_isLeadSurrogate(codeUnit) && nextPosition < buffer.length) { 33 | final nextCodeUnit = buffer.codeUnitAt(nextPosition); 34 | if (_isTrailSurrogate(nextCodeUnit)) { 35 | codeUnit = _combineSurrogatePair(codeUnit, nextCodeUnit); 36 | nextPosition++; 37 | } 38 | } 39 | if (predicate.test(codeUnit)) { 40 | return context.success( 41 | buffer.substring(position, nextPosition), 42 | nextPosition, 43 | ); 44 | } 45 | } 46 | return context.failure(message); 47 | } 48 | 49 | @override 50 | @noBoundsChecks 51 | int fastParseOn(String buffer, int position) { 52 | if (position < buffer.length) { 53 | var codeUnit = buffer.codeUnitAt(position++); 54 | if (_isLeadSurrogate(codeUnit) && position < buffer.length) { 55 | final nextCodeUnit = buffer.codeUnitAt(position); 56 | if (_isTrailSurrogate(nextCodeUnit)) { 57 | codeUnit = _combineSurrogatePair(codeUnit, nextCodeUnit); 58 | position++; 59 | } 60 | } 61 | if (predicate.test(codeUnit)) { 62 | return position; 63 | } 64 | } 65 | return -1; 66 | } 67 | 68 | @override 69 | UnicodeCharacterParser copy() => UnicodeCharacterParser(predicate, message); 70 | } 71 | 72 | /// Optimized version of [UnicodeCharacterParser] that parses any Unicode 73 | /// character (including possible surrogate pairs). 74 | class AnyUnicodeCharacterParser extends UnicodeCharacterParser { 75 | AnyUnicodeCharacterParser.internal(super.predicate, super.message) 76 | : assert(ConstantCharPredicate.any.isEqualTo(predicate)), 77 | super.internal(); 78 | 79 | @override 80 | @noBoundsChecks 81 | Result parseOn(Context context) { 82 | final buffer = context.buffer; 83 | final position = context.position; 84 | if (position < buffer.length) { 85 | var nextPosition = position + 1; 86 | if (_isLeadSurrogate(buffer.codeUnitAt(position)) && 87 | nextPosition < buffer.length && 88 | _isTrailSurrogate(buffer.codeUnitAt(nextPosition))) { 89 | nextPosition++; 90 | } 91 | return context.success( 92 | buffer.substring(position, nextPosition), 93 | nextPosition, 94 | ); 95 | } 96 | return context.failure(message); 97 | } 98 | 99 | @override 100 | @noBoundsChecks 101 | int fastParseOn(String buffer, int position) { 102 | if (position < buffer.length) { 103 | if (_isLeadSurrogate(buffer.codeUnitAt(position++)) && 104 | position < buffer.length && 105 | _isTrailSurrogate(buffer.codeUnitAt(position))) { 106 | position++; 107 | } 108 | return position; 109 | } 110 | return -1; 111 | } 112 | } 113 | 114 | // The following tests are adapted from the Dart SDK: 115 | // https://github.com/dart-lang/sdk/blob/1207250b0d5687f9016cf115068addf6593dba58/sdk/lib/core/string.dart#L932-L955 116 | 117 | // Tests if the code is a UTF-16 lead surrogate. 118 | @preferInline 119 | bool _isLeadSurrogate(int code) => (code & 0xFC00) == 0xD800; 120 | 121 | // Tests if the code is a UTF-16 trail surrogate. 122 | @preferInline 123 | bool _isTrailSurrogate(int code) => (code & 0xFC00) == 0xDC00; 124 | 125 | // Combines a lead and a trail surrogate value into a single code point. 126 | @preferInline 127 | int _combineSurrogatePair(int start, int end) => 128 | 0x10000 + ((start & 0x3FF) << 10) + (end & 0x3FF); 129 | -------------------------------------------------------------------------------- /lib/src/expression/group.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../core/parser.dart'; 4 | import '../parser/action/map.dart'; 5 | import '../parser/combinator/optional.dart'; 6 | import '../parser/combinator/sequence.dart'; 7 | import '../parser/repeater/possessive.dart'; 8 | import '../parser/repeater/separated.dart'; 9 | import 'result.dart'; 10 | import 'utils.dart'; 11 | 12 | /// Models a group of operators of the same precedence. 13 | class ExpressionGroup { 14 | @internal 15 | ExpressionGroup(this._loopback); 16 | 17 | /// Loopback parser used to establish the recursive expressions. 18 | final Parser _loopback; 19 | 20 | /// Defines a new wrapper using [left] and [right] parsers, that are typically 21 | /// used for parenthesis. Evaluates the [callback] with the parsed `left` 22 | /// delimiter, the `value` and `right` delimiter. 23 | void wrapper( 24 | Parser left, 25 | Parser right, 26 | T Function(L left, T value, R right) callback, 27 | ) => _wrapper.add((left, _loopback, right).toSequenceParser().map3(callback)); 28 | 29 | Parser _buildWrapper(Parser inner) => buildChoice([..._wrapper, inner]); 30 | 31 | final List> _wrapper = []; 32 | 33 | /// Adds a prefix operator [parser]. Evaluates the [callback] with the parsed 34 | /// `operator` and `value`. 35 | void prefix(Parser parser, T Function(O operator, T value) callback) => 36 | _prefix.add( 37 | parser.map( 38 | (operator) => ExpressionResultPrefix(operator, callback), 39 | ), 40 | ); 41 | 42 | Parser _buildPrefix(Parser inner) => _prefix.isEmpty 43 | ? inner 44 | : (buildChoice(_prefix).star(), inner).toSequenceParser().map2( 45 | (prefix, value) => 46 | prefix.reversed.fold(value, (each, result) => result.call(each)), 47 | ); 48 | 49 | final List>> _prefix = []; 50 | 51 | /// Adds a postfix operator [parser]. Evaluates the [callback] with the parsed 52 | /// `value` and `operator`. 53 | void postfix(Parser parser, T Function(T value, O operator) callback) => 54 | _postfix.add( 55 | parser.map( 56 | (operator) => ExpressionResultPostfix(operator, callback), 57 | ), 58 | ); 59 | 60 | Parser _buildPostfix(Parser inner) => _postfix.isEmpty 61 | ? inner 62 | : (inner, buildChoice(_postfix).star()).toSequenceParser().map2( 63 | (value, postfix) => 64 | postfix.fold(value, (each, result) => result.call(each)), 65 | ); 66 | 67 | final List>> _postfix = []; 68 | 69 | /// Adds a right-associative operator [parser]. Evaluates the [callback] with 70 | /// the parsed `left` term, `operator`, and `right` term. 71 | void right( 72 | Parser parser, 73 | T Function(T left, O operator, T right) callback, 74 | ) => _right.add( 75 | parser.map((operator) => ExpressionResultInfix(operator, callback)), 76 | ); 77 | 78 | Parser _buildRight(Parser inner) => _right.isEmpty 79 | ? inner 80 | : inner 81 | .plusSeparated(buildChoice(_right)) 82 | .map( 83 | (sequence) => sequence.foldRight( 84 | (left, result, right) => result.call(left, right), 85 | ), 86 | ); 87 | 88 | final List>> _right = []; 89 | 90 | /// Adds a left-associative operator [parser]. Evaluates the [callback] with 91 | /// the parsed `left` term, `operator`, and `right` term. 92 | void left( 93 | Parser parser, 94 | T Function(T left, O operator, T right) callback, 95 | ) => _left.add( 96 | parser.map((operator) => ExpressionResultInfix(operator, callback)), 97 | ); 98 | 99 | Parser _buildLeft(Parser inner) => _left.isEmpty 100 | ? inner 101 | : inner 102 | .plusSeparated(buildChoice(_left)) 103 | .map( 104 | (sequence) => sequence.foldLeft( 105 | (left, result, right) => result.call(left, right), 106 | ), 107 | ); 108 | 109 | final List>> _left = []; 110 | 111 | /// Makes the group optional and instead return the provided [value]. 112 | void optional(T value) { 113 | assert(!_optional, 'At most one optional value expected'); 114 | _optionalValue = value; 115 | _optional = true; 116 | } 117 | 118 | Parser _buildOptional(Parser inner) => 119 | _optional ? inner.optionalWith(_optionalValue) : inner; 120 | 121 | late T _optionalValue; 122 | bool _optional = false; 123 | 124 | // Internal helper to build the group of parsers. 125 | @internal 126 | Parser build(Parser inner) => _buildOptional( 127 | _buildLeft(_buildRight(_buildPostfix(_buildPrefix(_buildWrapper(inner))))), 128 | ); 129 | } 130 | -------------------------------------------------------------------------------- /lib/src/parser/repeater/greedy.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../core/context.dart'; 4 | import '../../core/parser.dart'; 5 | import '../../core/result.dart'; 6 | import 'lazy.dart'; 7 | import 'limited.dart'; 8 | import 'possessive.dart'; 9 | import 'unbounded.dart'; 10 | 11 | extension GreedyRepeatingParserExtension on Parser { 12 | /// Returns a parser that parses the receiver zero or more times until it 13 | /// reaches a [limit]. This is a greedy non-blind implementation of the 14 | /// [PossessiveRepeatingParserExtension.star] operator. The [limit] is not 15 | /// consumed. 16 | /// 17 | /// For example, the parser `char('{') & any().starGreedy(char('}')) & 18 | /// char('}')` consumes the complete input `'{abc}def}'` of `'{abc}def}'`. 19 | /// 20 | /// See [LazyRepeatingParserExtension.starLazy] for the lazy, more efficient, 21 | /// and generally preferred variation of this combinator. 22 | @useResult 23 | Parser> starGreedy(Parser limit) => 24 | repeatGreedy(limit, 0, unbounded); 25 | 26 | /// Returns a parser that parses the receiver one or more times until it 27 | /// reaches [limit]. This is a greedy non-blind implementation of the 28 | /// [PossessiveRepeatingParserExtension.plus] operator. The [limit] is not 29 | /// consumed. 30 | /// 31 | /// For example, the parser `char('{') & any().plusGreedy(char('}')) & 32 | /// char('}')` consumes the complete input `'{abc}def}'` of `'{abc}def}'`. 33 | /// 34 | /// See [LazyRepeatingParserExtension.plusLazy] for the lazy, more efficient, 35 | /// and generally preferred variation of this combinator. 36 | @useResult 37 | Parser> plusGreedy(Parser limit) => 38 | repeatGreedy(limit, 1, unbounded); 39 | 40 | /// Returns a parser that parses the receiver at least [min] and at most [max] 41 | /// times until it reaches a [limit]. This is a greedy non-blind 42 | /// implementation of the [PossessiveRepeatingParserExtension.repeat] 43 | /// operator. The [limit] is not consumed. 44 | /// 45 | /// This is the more generic variation of the [starGreedy] and [plusGreedy] 46 | /// combinators. 47 | @useResult 48 | Parser> repeatGreedy(Parser limit, int min, int max) => 49 | GreedyRepeatingParser(this, limit, min, max); 50 | } 51 | 52 | /// A greedy repeating parser, commonly seen in regular expression 53 | /// implementations. It aggressively consumes as much input as possible and then 54 | /// backtracks to meet the 'limit' condition. 55 | class GreedyRepeatingParser extends LimitedRepeatingParser { 56 | GreedyRepeatingParser(super.parser, super.limit, super.min, super.max); 57 | 58 | @override 59 | Result> parseOn(Context context) { 60 | var current = context; 61 | final elements = []; 62 | while (elements.length < min) { 63 | final result = delegate.parseOn(current); 64 | if (result is Failure) return result; 65 | assert( 66 | current.position < result.position, 67 | '$delegate must always consume', 68 | ); 69 | elements.add(result.value); 70 | current = result; 71 | } 72 | final contexts = [current]; 73 | while (elements.length < max) { 74 | final result = delegate.parseOn(current); 75 | if (result is Failure) break; 76 | assert( 77 | current.position < result.position, 78 | '$delegate must always consume', 79 | ); 80 | elements.add(result.value); 81 | contexts.add(current = result); 82 | } 83 | for (;;) { 84 | final limiter = limit.parseOn(contexts.last); 85 | if (limiter is Failure) { 86 | if (elements.isEmpty) return limiter; 87 | contexts.removeLast(); 88 | elements.removeLast(); 89 | if (contexts.isEmpty) return limiter; 90 | } else { 91 | return contexts.last.success(elements); 92 | } 93 | } 94 | } 95 | 96 | @override 97 | int fastParseOn(String buffer, int position) { 98 | var count = 0; 99 | var current = position; 100 | while (count < min) { 101 | final result = delegate.fastParseOn(buffer, current); 102 | if (result < 0) return -1; 103 | assert(current < result, '$delegate must always consume'); 104 | current = result; 105 | count++; 106 | } 107 | final positions = [current]; 108 | while (count < max) { 109 | final result = delegate.fastParseOn(buffer, current); 110 | if (result < 0) break; 111 | assert(current < result, '$delegate must always consume'); 112 | positions.add(current = result); 113 | count++; 114 | } 115 | for (;;) { 116 | final limiter = limit.fastParseOn(buffer, positions.last); 117 | if (limiter < 0) { 118 | if (count == 0) return -1; 119 | positions.removeLast(); 120 | count--; 121 | if (positions.isEmpty) return -1; 122 | } else { 123 | return positions.last; 124 | } 125 | } 126 | } 127 | 128 | @override 129 | GreedyRepeatingParser copy() => 130 | GreedyRepeatingParser(delegate, limit, min, max); 131 | } 132 | --------------------------------------------------------------------------------