Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
68 changes: 43 additions & 25 deletions lib/src/parser_petit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<Expression>();

Expand All @@ -116,31 +110,55 @@ 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')
.trim()
.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<Expression> 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<Expression>([
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));
Expand Down
5 changes: 4 additions & 1 deletion test/parser_petit_test_set.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class PetitParserTests extends TestSet {
'Power': parsePower,
'Modulo': parseModulo,
'Multiplication': parseMultiplication,
//'ImplicitMultiplication': parseImplicitMultiplication,
'ImplicitMultiplication': parseImplicitMultiplication,
'Division': parseDivision,
'Addition': parsePlus,
'Subtraction': parseMinus,
Expand Down Expand Up @@ -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));
Expand Down