From 11ebac4c0cb5728bb269681e5ed6841f88da146e Mon Sep 17 00:00:00 2001 From: Frederik Leonhardt Date: Tue, 6 Jan 2026 21:46:24 +1300 Subject: [PATCH] feat: support implicit multiplication in grammar parser --- CHANGELOG.md | 8 +++- lib/src/parser_petit.dart | 68 +++++++++++++++++++++------------ test/parser_petit_test_set.dart | 5 ++- 3 files changed, 54 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d1f146..ec9bd56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [unreleased] + +### Added + +- Add support for implicit multiplication to `GrammarParser`. + ## [3.1.0] - 2025-06-29 ### Changed @@ -107,7 +113,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Renamed the existing expression parser to `ShuntingYardParser`. This parser is now in maintenance mode and will not receive any new features. - `Parser` remains as a deprecated type alias to this legacy implemenation to + `Parser` remains as a deprecated type alias to this legacy implementation to maintain backwards compatibility. ### Fixed diff --git a/lib/src/parser_petit.dart b/lib/src/parser_petit.dart index 35bf226..24ea06b 100644 --- a/lib/src/parser_petit.dart +++ b/lib/src/parser_petit.dart @@ -87,13 +87,7 @@ class GrammarParser implements ExpressionParser { /// Creates a new parser. /// The given [options] can be used to configure the behaviour. GrammarParser([ParserOptions options = const ParserOptions()]) { - if (options.implicitMultiplication) { - throw UnimplementedError( - 'Implicit multiplication is not supported by this parser', - ); - } - - this.constants.addAll(options.constants); + constants.addAll(options.constants); final builder = ExpressionBuilder(); @@ -116,7 +110,7 @@ class GrammarParser implements ExpressionParser { final functionOrVariable = seq2( identifier, arguments, - ).map2((name, args) => _createBinding(name, args)); + ).map2((name, args) => _createBinding(name, args)).trim(); final number = (digit().plus() & (char('.') & digit().plus()).optional()) .flatten(message: 'Number expected') @@ -124,23 +118,47 @@ class GrammarParser implements ExpressionParser { .map(num.parse) .map(Number.new); - // Numbers and variables - builder - // Numbers - ..primitive(number) - // Special case for exponential function notation of form `e^x` - ..primitive( - seq2( - char('e').trim() & char('^').trim(), - builder.loopback, - ).map2((_, exp) => Exponential(exp)), - ) - // Generic constants, functions or variables - ..primitive(functionOrVariable) - // Parenthesis to group terms - ..group().wrapper(char('(').trim(), char(')').trim(), (l, e, r) => e) - // Backwards compat: Curly braces to group function arguments - ..group().wrapper(char('{').trim(), char('}').trim(), (l, e, r) => e); + final exponential = seq2( + char('e').trim() & char('^').trim(), + builder.loopback, + ).map2((_, exp) => Exponential(exp)); + + final parenthesized = seq3( + char('(').trim(), + builder.loopback, + char(')').trim(), + ).map3((_, expr, _) => expr); + + final curly = seq3( + char('{').trim(), + builder.loopback, + char('}').trim(), + ).map3((_, expr, _) => expr); + + Expression foldImplicitProduct(List terms) { + if (terms.length == 1) { + return terms.first; + } + Expression product = terms.first; + for (var i = 1; i < terms.length; i++) { + product = Times(product, terms[i]); + } + return product; + } + + final baseTerm = ChoiceParser([ + number, + exponential, + functionOrVariable, + parenthesized, + curly, + ]); + + final implicitTerm = options.implicitMultiplication + ? baseTerm.plus().map(foldImplicitProduct) + : baseTerm; + + builder.primitive(implicitTerm); // Binary operators (right associative) builder.group().right(char('^').trim(), (l, op, r) => Power(l, r)); diff --git a/test/parser_petit_test_set.dart b/test/parser_petit_test_set.dart index fad529b..2daf7b9 100644 --- a/test/parser_petit_test_set.dart +++ b/test/parser_petit_test_set.dart @@ -32,7 +32,7 @@ class PetitParserTests extends TestSet { 'Power': parsePower, 'Modulo': parseModulo, 'Multiplication': parseMultiplication, - //'ImplicitMultiplication': parseImplicitMultiplication, + 'ImplicitMultiplication': parseImplicitMultiplication, 'Division': parseDivision, 'Addition': parsePlus, 'Subtraction': parseMinus, @@ -184,8 +184,11 @@ class PetitParserTests extends TestSet { void parseImplicitMultiplication() { var cases = { + '2x': Number(2) * Variable('x'), '(5)(5)': Number(5) * Number(5), '(-2.0)5': -Number(2.0) * Number(5), + '2(x+1)': Number(2) * (Variable('x') + Number(1)), + '(x+1)(x-1)': (Variable('x') + Number(1)) * (Variable('x') - Number(1)), }; var parser = GrammarParser(ParserOptions(implicitMultiplication: true));