import 'dart:math' as math; import 'package:petitparser/petitparser.dart'; import 'package:petitparser/reflection.dart'; import 'package:test/test.dart'; import 'utils/matchers.dart'; Parser buildParser() { final builder = ExpressionBuilder(); builder.primitive( digit() .plus() .seq(char('.').seq(digit().plus()).optional()) .flatten(message: 'number expected') .trim(), ); builder.group() ..wrapper( char('(').trim(), char(')').trim(), (left, value, right) => [left, value, right], ) ..wrapper( string('sqrt(').trim(), char(')').trim(), (left, value, right) => [left, value, right], ); builder.group().prefix(char('-').trim(), (op, a) => [op, a]); builder.group() ..postfix(string('++').trim(), (a, op) => [a, op]) ..postfix(string('--').trim(), (a, op) => [a, op]); builder.group().right(char('^').trim(), (a, op, b) => [a, op, b]); builder.group() ..left(char('*').trim(), (a, op, b) => [a, op, b]) ..left(char('/').trim(), (a, op, b) => [a, op, b]); builder.group() ..left(char('+').trim(), (a, op, b) => [a, op, b]) ..left(char('-').trim(), (a, op, b) => [a, op, b]); return builder.build().end(); } Parser buildEvaluator() { final builder = ExpressionBuilder(); builder.primitive( digit() .plus() .seq(char('.').seq(digit().plus()).optional()) .flatten(message: 'number expected') .trim() .map(num.parse), ); builder.group() ..wrapper(char('(').trim(), char(')').trim(), (left, value, right) => value) ..wrapper( string('sqrt(').trim(), char(')').trim(), (left, value, right) => math.sqrt(value), ); builder.group().prefix(char('-').trim(), (op, a) => -a); builder.group() ..postfix(string('++').trim(), (a, op) => ++a) ..postfix(string('--').trim(), (a, op) => --a); builder.group().right(char('^').trim(), (a, op, b) => math.pow(a, b)); builder.group() ..left(char('*').trim(), (a, op, b) => a * b) ..left(char('/').trim(), (a, op, b) => a / b); builder.group() ..left(char('+').trim(), (a, op, b) => a + b) ..left(char('-').trim(), (a, op, b) => a - b); return builder.build().end(); } void main() { const epsilon = 1e-5; final parser = buildParser(); final evaluator = buildEvaluator(); group('add', () { test('parser', () { expect(parser, isParseSuccess('1 + 2', result: ['1', '+', '2'])); expect( parser, isParseSuccess( '1 + 2 + 3', result: [ ['1', '+', '2'], '+', '3', ], ), ); }); test('evaluator', () { expect(evaluator, isParseSuccess('1 + 2', result: closeTo(3, epsilon))); expect(evaluator, isParseSuccess('2 + 1', result: closeTo(3, epsilon))); expect( evaluator, isParseSuccess('1 + 2.3', result: closeTo(3.3, epsilon)), ); expect( evaluator, isParseSuccess('2.3 + 1', result: closeTo(3.3, epsilon)), ); expect(evaluator, isParseSuccess('1 + -2', result: closeTo(-1, epsilon))); expect(evaluator, isParseSuccess('-2 + 1', result: closeTo(-1, epsilon))); }); test('evaluator many', () { expect(evaluator, isParseSuccess('1', result: closeTo(1, epsilon))); expect(evaluator, isParseSuccess('1 + 2', result: closeTo(3, epsilon))); expect( evaluator, isParseSuccess('1 + 2 + 3', result: closeTo(6, epsilon)), ); expect( evaluator, isParseSuccess('1 + 2 + 3 + 4', result: closeTo(10, epsilon)), ); expect( evaluator, isParseSuccess('1 + 2 + 3 + 4 + 5', result: closeTo(15, epsilon)), ); }); test('error', () { expect( evaluator, isParseFailure('1 +', message: 'end of input expected', position: 2), ); expect( evaluator, isParseFailure( '1 + 2 +', message: 'end of input expected', position: 6, ), ); }); }); group('sub', () { test('parser', () { expect(parser, isParseSuccess('1 - 2', result: ['1', '-', '2'])); expect( parser, isParseSuccess( '1 - 2 - 3', result: [ ['1', '-', '2'], '-', '3', ], ), ); }); test('evaluator', () { expect(evaluator, isParseSuccess('1 - 2', result: closeTo(-1, epsilon))); expect( evaluator, isParseSuccess('1.2 - 1.2', result: closeTo(0, epsilon)), ); expect(evaluator, isParseSuccess('1 - -2', result: closeTo(3, epsilon))); expect(evaluator, isParseSuccess('-1 - -2', result: closeTo(1, epsilon))); }); test('evaluator many', () { expect(evaluator, isParseSuccess('1', result: closeTo(1, epsilon))); expect(evaluator, isParseSuccess('1 - 2', result: closeTo(-1, epsilon))); expect( evaluator, isParseSuccess('1 - 2 - 3', result: closeTo(-4, epsilon)), ); expect( evaluator, isParseSuccess('1 - 2 - 3 - 4', result: closeTo(-8, epsilon)), ); expect( evaluator, isParseSuccess('1 - 2 - 3 - 4 - 5', result: closeTo(-13, epsilon)), ); }); test('error', () { expect( evaluator, isParseFailure('1 -', message: 'end of input expected', position: 2), ); expect( evaluator, isParseFailure( '1 - 2 -', message: 'end of input expected', position: 6, ), ); }); }); group('mul', () { test('parser', () { expect(parser, isParseSuccess('1 * 2', result: ['1', '*', '2'])); expect( parser, isParseSuccess( '1 * 2 * 3', result: [ ['1', '*', '2'], '*', '3', ], ), ); }); test('evaluator', () { expect(evaluator, isParseSuccess('2 * 3', result: closeTo(6, epsilon))); expect(evaluator, isParseSuccess('2 * -4', result: closeTo(-8, epsilon))); }); test('evaluator many', () { expect(evaluator, isParseSuccess('1 * 2', result: closeTo(2, epsilon))); expect( evaluator, isParseSuccess('1 * 2 * 3', result: closeTo(6, epsilon)), ); expect( evaluator, isParseSuccess('1 * 2 * 3 * 4', result: closeTo(24, epsilon)), ); expect( evaluator, isParseSuccess('1 * 2 * 3 * 4 * 5', result: closeTo(120, epsilon)), ); }); test('error', () { expect( evaluator, isParseFailure('1 *', message: 'end of input expected', position: 2), ); expect( evaluator, isParseFailure( '1 * 2 *', message: 'end of input expected', position: 6, ), ); }); }); group('div', () { test('parser', () { expect(parser, isParseSuccess('1 / 2', result: ['1', '/', '2'])); expect( parser, isParseSuccess( '1 / 2 / 3', result: [ ['1', '/', '2'], '/', '3', ], ), ); }); test('evaluator', () { expect(evaluator, isParseSuccess('12 / 3', result: closeTo(4, epsilon))); expect( evaluator, isParseSuccess('-16 / -4', result: closeTo(4, epsilon)), ); }); test('evaluator many', () { expect( evaluator, isParseSuccess('100 / 2', result: closeTo(50, epsilon)), ); expect( evaluator, isParseSuccess('100 / 2 / 2', result: closeTo(25, epsilon)), ); expect( evaluator, isParseSuccess('100 / 2 / 2 / 5', result: closeTo(5, epsilon)), ); expect( evaluator, isParseSuccess('100 / 2 / 2 / 5 / 5', result: closeTo(1, epsilon)), ); }); test('error', () { expect( evaluator, isParseFailure('1 /', message: 'end of input expected', position: 2), ); expect( evaluator, isParseFailure( '1 / 2 /', message: 'end of input expected', position: 6, ), ); }); }); group('pow', () { test('parser', () { expect(parser, isParseSuccess('1 ^ 2', result: ['1', '^', '2'])); expect( parser, isParseSuccess( '1 ^ 2 ^ 3', result: [ '1', '^', ['2', '^', '3'], ], ), ); }); test('evaluator', () { expect(evaluator, isParseSuccess('2 ^ 3', result: closeTo(8, epsilon))); expect(evaluator, isParseSuccess('-2 ^ 3', result: closeTo(-8, epsilon))); expect( evaluator, isParseSuccess('-2 ^ -3', result: closeTo(-0.125, epsilon)), ); }); test('evaluator many', () { expect(evaluator, isParseSuccess('4 ^ 3', result: closeTo(64, epsilon))); expect( evaluator, isParseSuccess('4 ^ 3 ^ 2', result: closeTo(262144, epsilon)), ); expect( evaluator, isParseSuccess('4 ^ 3 ^ 2 ^ 1', result: closeTo(262144, epsilon)), ); expect( evaluator, isParseSuccess('4 ^ 3 ^ 2 ^ 1 ^ 0', result: closeTo(262144, epsilon)), ); }); test('error', () { expect( evaluator, isParseFailure('1 ^', message: 'end of input expected', position: 2), ); expect( evaluator, isParseFailure( '1 ^ 2 ^', message: 'end of input expected', position: 6, ), ); }); }); group('parens', () { test('parser', () { expect(parser, isParseSuccess('(1)', result: ['(', '1', ')'])); expect( parser, isParseSuccess( '(1 + 2)', result: [ '(', ['1', '+', '2'], ')', ], ), ); expect( parser, isParseSuccess( '((1))', result: [ '(', ['(', '1', ')'], ')', ], ), ); expect( parser, isParseSuccess( '((1 + 2))', result: [ '(', [ '(', ['1', '+', '2'], ')', ], ')', ], ), ); expect( parser, isParseSuccess( '2 * (3 + 4)', result: [ '2', '*', [ '(', ['3', '+', '4'], ')', ], ], ), ); expect( parser, isParseSuccess( '(2 + 3) * 4', result: [ [ '(', ['2', '+', '3'], ')', ], '*', '4', ], ), ); expect( parser, isParseSuccess( '6 / (2 + 4)', result: [ '6', '/', [ '(', ['2', '+', '4'], ')', ], ], ), ); expect( parser, isParseSuccess( '(2 + 6) / 2', result: [ [ '(', ['2', '+', '6'], ')', ], '/', '2', ], ), ); }); test('evaluator', () { expect(evaluator, isParseSuccess('(1)', result: closeTo(1, epsilon))); expect(evaluator, isParseSuccess('(1 + 2)', result: closeTo(3, epsilon))); expect(evaluator, isParseSuccess('((1))', result: closeTo(1, epsilon))); expect( evaluator, isParseSuccess('((1 + 2))', result: closeTo(3, epsilon)), ); expect( evaluator, isParseSuccess('2 * (3 + 4)', result: closeTo(14, epsilon)), ); expect( evaluator, isParseSuccess('(2 + 3) * 4', result: closeTo(20, epsilon)), ); expect( evaluator, isParseSuccess('6 / (2 + 4)', result: closeTo(1, epsilon)), ); expect( evaluator, isParseSuccess('(2 + 6) / 2', result: closeTo(4, epsilon)), ); }); test('error', () { expect(evaluator, isParseFailure('(', message: 'number expected')); expect(evaluator, isParseFailure('()', message: 'number expected')); expect(evaluator, isParseFailure('(1', message: 'number expected')); expect(evaluator, isParseFailure('((', message: 'number expected')); expect(evaluator, isParseFailure('((2', message: 'number expected')); expect(evaluator, isParseFailure('((2)', message: 'number expected')); }); }); group('sqrt', () { test('parser', () { expect(parser, isParseSuccess('sqrt(4)', result: ['sqrt(', '4', ')'])); expect( parser, isParseSuccess( 'sqrt(1 + 3)', result: [ 'sqrt(', ['1', '+', '3'], ')', ], ), ); expect( parser, isParseSuccess( '1 + sqrt(16)', result: [ '1', '+', ['sqrt(', '16', ')'], ], ), ); expect( parser, isParseSuccess( 'sqrt(sqrt(16))', result: [ 'sqrt(', ['sqrt(', '16', ')'], ')', ], ), ); }); test('evaluator', () { expect(evaluator, isParseSuccess('sqrt(4)', result: closeTo(2, epsilon))); expect( evaluator, isParseSuccess('sqrt(1 + 3)', result: closeTo(2, epsilon)), ); expect( evaluator, isParseSuccess('1 + sqrt(16)', result: closeTo(5, epsilon)), ); expect( evaluator, isParseSuccess('sqrt(sqrt(16))', result: closeTo(2, epsilon)), ); }); test('error', () { expect(evaluator, isParseFailure('sqrt(', message: 'number expected')); expect(evaluator, isParseFailure('sqrt()', message: 'number expected')); expect(evaluator, isParseFailure('sqrt(1', message: 'number expected')); expect( evaluator, isParseFailure('sqrt(sqrt(', message: 'number expected'), ); expect( evaluator, isParseFailure('sqrt(sqrt(1', message: 'number expected'), ); expect( evaluator, isParseFailure('sqrt(sqrt(1)', message: 'number expected'), ); }); }); group('postfix add', () { test('parser', () { expect(parser, isParseSuccess('0++', result: ['0', '++'])); expect( parser, isParseSuccess( '0++++', result: [ ['0', '++'], '++', ], ), ); expect( parser, isParseSuccess( '0++++++', result: [ [ ['0', '++'], '++', ], '++', ], ), ); expect( parser, isParseSuccess( '0+++1', result: [ ['0', '++'], '+', '1', ], ), ); expect( parser, isParseSuccess( '0+++++1', result: [ [ ['0', '++'], '++', ], '+', '1', ], ), ); expect( parser, isParseSuccess( '0+++++++1', result: [ [ [ ['0', '++'], '++', ], '++', ], '+', '1', ], ), ); }); test('evaluator', () { expect(evaluator, isParseSuccess('0++', result: closeTo(1, epsilon))); expect(evaluator, isParseSuccess('0++++', result: closeTo(2, epsilon))); expect(evaluator, isParseSuccess('0++++++', result: closeTo(3, epsilon))); expect(evaluator, isParseSuccess('0+++1', result: closeTo(2, epsilon))); expect(evaluator, isParseSuccess('0+++++1', result: closeTo(3, epsilon))); expect( evaluator, isParseSuccess('0+++++++1', result: closeTo(4, epsilon)), ); }); test('error', () { expect(evaluator, isParseFailure('++', message: 'number expected')); expect( evaluator, isParseFailure('0+++', message: 'end of input expected', position: 3), ); }); }); group('postfix sub', () { test('parser', () { expect(parser, isParseSuccess('0--', result: ['0', '--'])); expect( parser, isParseSuccess( '0----', result: [ ['0', '--'], '--', ], ), ); expect( parser, isParseSuccess( '0------', result: [ [ ['0', '--'], '--', ], '--', ], ), ); expect( parser, isParseSuccess( '0---1', result: [ ['0', '--'], '-', '1', ], ), ); expect( parser, isParseSuccess( '0-----1', result: [ [ ['0', '--'], '--', ], '-', '1', ], ), ); expect( parser, isParseSuccess( '0-------1', result: [ [ [ ['0', '--'], '--', ], '--', ], '-', '1', ], ), ); }); test('evaluator', () { expect(evaluator, isParseSuccess('1--', result: closeTo(0, epsilon))); expect(evaluator, isParseSuccess('2----', result: closeTo(0, epsilon))); expect(evaluator, isParseSuccess('3------', result: closeTo(0, epsilon))); expect(evaluator, isParseSuccess('2---1', result: closeTo(0, epsilon))); expect(evaluator, isParseSuccess('3-----1', result: closeTo(0, epsilon))); expect( evaluator, isParseSuccess('4-------1', result: closeTo(0, epsilon)), ); }); test('error', () { expect( evaluator, isParseFailure('--', message: 'number expected', position: 2), ); expect( evaluator, isParseFailure('0---', message: 'end of input expected', position: 3), ); }); }); group('negate', () { test('parser', () { expect(parser, isParseSuccess('1', result: '1')); expect(parser, isParseSuccess('-1', result: ['-', '1'])); expect( parser, isParseSuccess( '--1', result: [ '-', ['-', '1'], ], ), ); expect( parser, isParseSuccess( '---1', result: [ '-', [ '-', ['-', '1'], ], ], ), ); }); test('evaluator', () { expect(evaluator, isParseSuccess('1', result: closeTo(1, epsilon))); expect(evaluator, isParseSuccess('-1', result: closeTo(-1, epsilon))); expect(evaluator, isParseSuccess('--1', result: closeTo(1, epsilon))); expect(evaluator, isParseSuccess('---1', result: closeTo(-1, epsilon))); }); test('error', () { expect( evaluator, isParseFailure('-', message: 'number expected', position: 1), ); expect( evaluator, isParseFailure('--', message: 'number expected', position: 2), ); expect( evaluator, isParseFailure('+2', message: 'number expected', position: 0), ); }); }); group('number', () { test('parser', () { expect(parser, isParseSuccess('0', result: '0')); expect(parser, isParseSuccess('0.1', result: '0.1')); expect(parser, isParseSuccess('-1', result: ['-', '1'])); }); test('evaluator', () { expect(evaluator, isParseSuccess('0', result: closeTo(0, epsilon))); expect(evaluator, isParseSuccess('0.0', result: closeTo(0, epsilon))); expect(evaluator, isParseSuccess('1', result: closeTo(1, epsilon))); expect(evaluator, isParseSuccess('1.2', result: closeTo(1.2, epsilon))); expect(evaluator, isParseSuccess('34', result: closeTo(34, epsilon))); expect(evaluator, isParseSuccess('34.7', result: closeTo(34.7, epsilon))); expect( evaluator, isParseSuccess('56.78', result: closeTo(56.78, epsilon)), ); }); test('error', () { expect(evaluator, isParseFailure('', message: 'number expected')); expect( evaluator, isParseFailure('-', message: 'number expected', position: 1), ); expect(evaluator, isParseFailure('(', message: 'number expected')); expect( evaluator, isParseFailure('0.', message: 'end of input expected', position: 1), ); }); }); group('priority', () { test('parser', () { expect( parser, isParseSuccess( '2 * 3 + 4', result: [ ['2', '*', '3'], '+', '4', ], ), ); expect( parser, isParseSuccess( '2 + 3 * 4', result: [ '2', '+', ['3', '*', '4'], ], ), ); }); test('evaluator', () { expect( evaluator, isParseSuccess('2 * 3 + 4', result: closeTo(10, epsilon)), ); expect( evaluator, isParseSuccess('2 + 3 * 4', result: closeTo(14, epsilon)), ); expect( evaluator, isParseSuccess('6 / 3 + 4', result: closeTo(6, epsilon)), ); expect( evaluator, isParseSuccess('2 + 6 / 2', result: closeTo(5, epsilon)), ); }); }); group('builder', () { test('empty', () { final builder = ExpressionBuilder(); expect( builder.build, throwsA( isAssertionError.having( (exception) => exception.message, 'message', 'At least one primitive parser expected', ), ), ); }, skip: !hasAssertionsEnabled()); test('no primitive', () { final builder = ExpressionBuilder(); builder.group().wrapper(char('('), char(')'), (l, v, r) => '[$v]'); expect( builder.build, throwsA( isAssertionError.having( (exception) => exception.message, 'message', 'At least one primitive parser expected', ), ), ); }, skip: !hasAssertionsEnabled()); test('loopback', () { final builder = ExpressionBuilder(); builder.primitive(seq2(char('a'), builder.loopback).flatten()); builder.primitive(char('b')); final parser = builder.build(); expect(parser, isParseSuccess('b', result: 'b')); expect(parser, isParseSuccess('ab', result: 'ab')); expect(parser, isParseSuccess('aab', result: 'aab')); }); group('epsilon', () { test('primitive', () { final builder = ExpressionBuilder(); builder ..primitive(noneOf('()')) ..primitive(epsilonWith('*')); builder.group().wrapper(char('('), char(')'), (_, v, _) => '[$v]'); final parser = builder.build().end(); expect(parser, isParseSuccess('', result: '*')); expect(parser, isParseSuccess('a', result: 'a')); expect(parser, isParseSuccess('(a)', result: '[a]')); expect(parser, isParseSuccess('((a))', result: '[[a]]')); expect(parser, isParseSuccess('()', result: '[*]')); expect(parser, isParseSuccess('(())', result: '[[*]]')); }); test('left', () { final builder = ExpressionBuilder(); builder.primitive(any()); builder.group().left(epsilonWith(null), (a, _, b) => '[$a$b]'); final parser = builder.build().end(); expect(parser, isParseFailure('')); expect(parser, isParseSuccess('a', result: 'a')); expect(parser, isParseSuccess('ab', result: '[ab]')); expect(parser, isParseSuccess('abc', result: '[[ab]c]')); expect(parser, isParseSuccess('abcd', result: '[[[ab]c]d]')); }); test('right', () { final builder = ExpressionBuilder(); builder.primitive(any()); builder.group().right(epsilonWith(null), (a, _, b) => '[$a$b]'); final parser = builder.build().end(); expect(parser, isParseFailure('')); expect(parser, isParseSuccess('a', result: 'a')); expect(parser, isParseSuccess('ab', result: '[ab]')); expect(parser, isParseSuccess('abc', result: '[a[bc]]')); expect(parser, isParseSuccess('abcd', result: '[a[b[cd]]]')); }); }); group('optional', () { test('basic', () { final builder = ExpressionBuilder(); builder.primitive(digit()); builder.group() ..wrapper(char('('), char(')'), (_, v, _) => '($v)') ..optional('∅'); final parser = builder.build().end(); expect(parser, isParseSuccess('', result: '∅')); expect(parser, isParseSuccess('()', result: '(∅)')); expect(parser, isParseSuccess('1', result: '1')); expect(parser, isParseSuccess('(1)', result: '(1)')); }); test('repeated', () { final builder = ExpressionBuilder(); final group = builder.group(); group.optional('foo'); expect( () => group.optional('bar'), throwsA( isAssertionError.having( (exception) => exception.message, 'message', 'At most one optional value expected', ), ), ); }, skip: !hasAssertionsEnabled()); }); }); group('examples', () { test('regex', () { final builder = ExpressionBuilder(); builder.primitive(noneOf(')')); builder.group() ..wrapper(char('('), char(')'), (_, value, _) => '($value)') ..prefix(char('!'), (_, value) => '!($value)') ..postfix(char('?'), (value, _) => '($value)?') ..left(char('|'), (left, _, right) => '($left|$right)') ..right(char('&'), (left, _, right) => '($left&$right)'); builder.group() ..left(epsilonWith(null), (a, _, b) => '[$a$b]') ..optional('∅'); final parser = builder.build().end(); expect(parser, isParseSuccess('', result: '∅')); expect(parser, isParseSuccess('a', result: 'a')); expect(parser, isParseSuccess('ab', result: '[ab]')); expect(parser, isParseSuccess('abc', result: '[[ab]c]')); expect(parser, isParseSuccess('a&b', result: '(a&b)')); expect(parser, isParseSuccess('a&b&c', result: '(a&(b&c))')); expect(parser, isParseSuccess('a|b', result: '(a|b)')); expect(parser, isParseSuccess('a|b|c', result: '((a|b)|c)')); expect(parser, isParseSuccess('a?', result: '(a)?')); expect(parser, isParseSuccess('a??', result: '((a)?)?')); expect(parser, isParseSuccess('!a', result: '!(a)')); expect(parser, isParseSuccess('!!a', result: '!(!(a))')); expect(parser, isParseSuccess('()', result: '(∅)')); expect(parser, isParseSuccess('(a)', result: '(a)')); expect(parser, isParseSuccess('(ab)', result: '([ab])')); expect(parser, isParseSuccess('(abc)', result: '([[ab]c])')); }); }); test('linter', () { expect(linter(parser), isEmpty); expect(linter(evaluator), isEmpty); }); }