refactor: Cleanup git state - commit all staged changes
Major refactoring cleanup: - Add new controller architecture (class-controller-*.php) - Add new settings-v2 UI (views/settings-v2/) - Add new CSS architecture (agentic-sidebar.css, tokens) - Add esbuild build pipeline (scripts/build.js, package.json) - Add composer dependencies (vendor/) - Add frontend src directory (assets/js/src/index.jsx) - Add documentation files - Remove old/obsolete files (class-settings.php, old CSS) This commits all pending changes from previous refactoring efforts.
This commit is contained in:
65
vendor/parsica-php/parsica/docs/before.php
vendored
Normal file
65
vendor/parsica-php/parsica/docs/before.php
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of the Parsica library.
|
||||
*
|
||||
* Copyright (c) 2020 Mathias Verraes <mathias@verraes.net>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/** @noinspection ALL */
|
||||
|
||||
namespace Docs;
|
||||
|
||||
// This code is executed by UpToDocs before each code block
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use Parsica\Parsica\Parser;
|
||||
use Parsica\Parsica\ParserHasFailed;
|
||||
use Parsica\Parsica\StringStream;
|
||||
use function Parsica\Parsica\{alphaChar,
|
||||
alphaNumChar,
|
||||
atLeastOne,
|
||||
between,
|
||||
char,
|
||||
charI,
|
||||
choice,
|
||||
collect,
|
||||
digitChar,
|
||||
either,
|
||||
eof,
|
||||
Expression\binaryOperator,
|
||||
Expression\expression,
|
||||
Expression\leftAssoc,
|
||||
Expression\nonAssoc,
|
||||
Expression\postfix,
|
||||
Expression\prefix,
|
||||
Expression\rightAssoc,
|
||||
Expression\unaryOperator,
|
||||
float,
|
||||
isDigit,
|
||||
isEqual,
|
||||
isWhitespace,
|
||||
keepFirst,
|
||||
many,
|
||||
notFollowedBy,
|
||||
optional,
|
||||
orPred,
|
||||
punctuationChar,
|
||||
recursive,
|
||||
repeat,
|
||||
satisfy,
|
||||
sepBy,
|
||||
sepBy1,
|
||||
sequence,
|
||||
skipHSpace,
|
||||
skipSpace1,
|
||||
some,
|
||||
string,
|
||||
stringI,
|
||||
takeWhile,
|
||||
upperChar,
|
||||
whitespace,
|
||||
zeroOrMore};
|
||||
use function PHPUnit\Framework\{assertEquals, assertFalse, assertInstanceOf, assertIsString, assertTrue, assertSame};
|
||||
14
vendor/parsica-php/parsica/docs/contribute/design_goals.md
vendored
Normal file
14
vendor/parsica-php/parsica/docs/contribute/design_goals.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
title: Design Goals
|
||||
sidebar_label: Design Goals
|
||||
---
|
||||
|
||||
Parsica aims to be the mainstream choice for anyone to create parsers. We want to support all use cases. When parsing a short string, Parsica should be worth picking over regular expressions; when parsing an entire language, it should be worth picking over a handwritten imperative parser. The API should be self-evident, it should be easy to get it right and hard to get it wrong.
|
||||
|
||||
Developers should not have to learn anything other than this library itself: no need to learn FP, category theory, parser theory, or even the internals of this libary. Under the hood, we use theoretical concepts. However, when adhering to these concepts would require exposing them to the developers, we will prefer a tradeoff that hides them.
|
||||
|
||||
The same goes for performance: Parsica should be performant enough to be a viable choice, but for most use cases, developers should not have to worry about learning how to achieve greater performance.
|
||||
|
||||
Parsica puts great focus on composability. To achieve this, we use immutability and referential transparency — not for the sake of perfection, but because these help to achieve effortless composition.
|
||||
|
||||
Finally, it should be easy for third party library authors to publish their own parsers as Composer packages, which in turn can be composed by other users.
|
||||
36
vendor/parsica-php/parsica/docs/expressions.php
vendored
Normal file
36
vendor/parsica-php/parsica/docs/expressions.php
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of the Parsica library.
|
||||
*
|
||||
* Copyright (c) 2020 Mathias Verraes <mathias@verraes.net>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/** @noinspection ALL */
|
||||
|
||||
namespace Docs;
|
||||
|
||||
// This code is executed by UpToDocs before each code block
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
|
||||
use Parsica\Parsica\Parser;
|
||||
use function Parsica\Parsica\Expression\{binaryOperator,
|
||||
expression,
|
||||
leftAssoc,
|
||||
postfix,
|
||||
prefix,
|
||||
rightAssoc,
|
||||
unaryOperator};
|
||||
use function Parsica\Parsica\{atLeastOne, between, char, choice, digitChar, keepFirst, recursive, skipHSpace, string, alphaChar};
|
||||
use function PHPUnit\Framework\{assertSame, assertEquals};
|
||||
|
||||
assert_options(ASSERT_ACTIVE, 1);
|
||||
|
||||
|
||||
$token = fn(Parser $parser) => keepFirst($parser, skipHSpace());
|
||||
$term = fn(): Parser => $token(atLeastOne(digitChar()))->map('intval');
|
||||
$parens = fn (Parser $parser): Parser => $token(between($token(char('(')), $token(char(')')), $parser));
|
||||
|
||||
42
vendor/parsica-php/parsica/docs/index.md
vendored
Normal file
42
vendor/parsica-php/parsica/docs/index.md
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
title: Parsica
|
||||
sidebar_label: About Parsica
|
||||
---
|
||||
|
||||
The easiest way to build robust parsers in PHP.
|
||||
|
||||
**Note:** Parsica is very early stage, expect things to break.
|
||||
|
||||
* [Releases](releases)
|
||||
* [Installation & Requirements](installation)
|
||||
* [API Reference](api/index)
|
||||
|
||||
## Donate
|
||||
|
||||
Donate via my [GitHub Sponsor Page](https://github.com/sponsors/turanct).
|
||||
|
||||
A lot of research & development went into this project. We think it can become the mainstream choice for building reliable parsers in PHP, and serve as a foundation for many advancements. Your support will help us to reach that goal.
|
||||
|
||||
## Contribute
|
||||
|
||||
* [Design Goals](contribute/design_goals)
|
||||
* Contribute by submitting code, documentation, examples, ... through pull requests.
|
||||
* [Code of Conduct](CODE_OF_CONDUCT)
|
||||
|
||||
## Support
|
||||
|
||||
### Commercial training & support
|
||||
|
||||
E-mail us at [contact@value-object.com](mailto:contact@value-object.com) to discuss options.
|
||||
|
||||
### Community support
|
||||
|
||||
Submit questions as Github issues. Help us help you by submitting short bits of code that demonstrate the problem, and that can easily be copied and run.
|
||||
|
||||
## Links
|
||||
|
||||
* Official Site: [parsica-php.github.io](https://parsica-php.github.io)
|
||||
* Twitter: [@parsica_php](https://twitter.com/parsica_php)
|
||||
* GitHub: [parsica-php/parsica](https://github.com/parsica-php/parsica)
|
||||
* Packagist: [parsica-php/parsica](https://packagist.org/packages/parsica-php/parsica)
|
||||
* License: [MIT](LICENSE)
|
||||
67
vendor/parsica-php/parsica/docs/installation.md
vendored
Normal file
67
vendor/parsica-php/parsica/docs/installation.md
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: Installation & Requirements
|
||||
---
|
||||
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
<Tabs
|
||||
defaultValue="cli"
|
||||
values={[
|
||||
{ label: 'Command line', value: 'cli', },
|
||||
{ label: 'composer.json', value: 'composer', },
|
||||
]
|
||||
}>
|
||||
<TabItem value="cli">
|
||||
|
||||
```bash
|
||||
composer require parsica-php/parsica
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="composer">
|
||||
|
||||
```json
|
||||
"require": {
|
||||
"parsica-php/parsica": "dev-main"
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
|
||||
## Requirements
|
||||
|
||||
- PHP 7.4 or higher
|
||||
- [The multibyte string extension for PHP (aka mbstring)](https://www.php.net/manual/en/book.mbstring.php)
|
||||
|
||||
(@TODO: add polyfill for mbstring).
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
In a .php file, make sure the Composer autoloader is included:
|
||||
|
||||
`require_once __DIR__.'/../vendor/autoload.php';`
|
||||
|
||||
Import parsers and combinators:
|
||||
|
||||
`use function Parsica\Parsica\char;`
|
||||
|
||||
You can combine multiple imports in one statement:
|
||||
|
||||
`use function Parsica\Parsica\{between, char, atLeastOne, alphaChar};`
|
||||
|
||||
Finally, add some code:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser = between(char('{'), char('}'), atLeastOne(alphaChar()));
|
||||
$result = $parser->tryString("{Hello}");
|
||||
echo $result->output();
|
||||
// outputs "Hello"
|
||||
```
|
||||
|
||||
43
vendor/parsica-php/parsica/docs/resources/01_development_status.md
vendored
Normal file
43
vendor/parsica-php/parsica/docs/resources/01_development_status.md
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
title: Development Status
|
||||
sidebar_label: Development Status
|
||||
---
|
||||
|
||||
|
||||
Parsica is early stage, so expect things to break all the time.
|
||||
|
||||
This is a rough wishlist of features to do before 1.0:
|
||||
|
||||
### Done
|
||||
|
||||
- [x] API Documentation
|
||||
- [x] All essential parsers
|
||||
- [x] Basic error messages
|
||||
- [x] PHPUnit tooling
|
||||
- [x] Recursive parsers
|
||||
- [x] Versioned documentation
|
||||
- [x] Essential combinators
|
||||
- [x] JSON parser
|
||||
- [x] Parser position in error messages
|
||||
- [x] Expression parser helpers
|
||||
- [x] Tutorial
|
||||
|
||||
### TODO
|
||||
|
||||
- [ ] Streaming input
|
||||
- [ ] Change the behaviour of or, add try and lookAhead
|
||||
- [ ] Better parser assertions
|
||||
- [ ] Better exceptions
|
||||
- [ ] Character categories
|
||||
- [ ] Comparison tests for canonical and performant implementations
|
||||
- [ ] Debug trees
|
||||
- [ ] Inliner
|
||||
- [ ] Lexer
|
||||
- [ ] Monoidal parser types
|
||||
- [ ] More [monad combinators](https://hackage.haskell.org/package/base-4.14.0.0/docs/Control-Monad.html#v:-62--61--62-)
|
||||
- [ ] Other popular test frameworks
|
||||
- [ ] [Permutation phrases](https://www.cs.ox.ac.uk/jeremy.gibbons/wg21/meeting56/loeh-paper.pdf)
|
||||
- [ ] Parser state
|
||||
- [ ] Profiling & performance
|
||||
- [ ] Publish documentation in e-reader and pdf formats
|
||||
|
||||
83
vendor/parsica-php/parsica/docs/resources/02_performance.md
vendored
Normal file
83
vendor/parsica-php/parsica/docs/resources/02_performance.md
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
title: Performance
|
||||
sidebar_label: Performance
|
||||
---
|
||||
|
||||
|
||||
At the time of writing, no effort has been made to measure the performance of Parsica. That doesn't mean it's slow; it means that we don't know yet. If you're going to use this on large amounts of data, do some profiling yourself first. Compute == carbon, and we'd like to keep this planet a little longer. You can help by contributing your profiling and optimisations.
|
||||
|
||||
We have some ideas that will allow us to make it very efficient, and we intend to do that before getting to a 1.0 release.
|
||||
|
||||
|
||||
## XDebug
|
||||
|
||||
Turn off XDebug, as it will make things much slower. If you do turn on XDebug, you may get `Maximum function nesting level of '256' reached, aborting!`. Increase the nesting level until the error goes away, either in code or in `php.ini`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
ini_set('xdebug.max_nesting_level', '1024');
|
||||
```
|
||||
|
||||
```ini
|
||||
xdebug.max_nesting_level=1024
|
||||
```
|
||||
|
||||
## Recursion
|
||||
|
||||
If you encounter a "Maximum function nesting level" error, the more likely problem is that you're building a recursive parser incorrectly. Have a look at the documentation page about recursion to learn more.
|
||||
|
||||
|
||||
## Performance tips
|
||||
|
||||
Below we'll list some approaches to improve performance.
|
||||
|
||||
The actual difference in performance depends on many factors, so measure your parsers' performance to know if it is actually faster.
|
||||
|
||||
### Reusing parsers is faster than rebuilding them
|
||||
|
||||
Storing parsers in a variable or property is often faster than rebuilding them. Compare the these two equivalent parsers:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$slow = between(
|
||||
choice(char('"'), char("'")),
|
||||
choice(char('"'), char("'")),
|
||||
atLeastOne(alphaNumChar())
|
||||
);
|
||||
|
||||
$quote = choice(char('"'), char("'"));
|
||||
$fast = between(
|
||||
$quote,
|
||||
$quote,
|
||||
atLeastOne(alphaNumChar())
|
||||
);
|
||||
```
|
||||
|
||||
### Use predicates over higher level combinators
|
||||
|
||||
Often, a combinator may be replaced with lower level combinators to get the same result faster. For example, the following parsers are equivalent, but the second one is a lot faster:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$somePredicate = isDigit();
|
||||
$slow = zeroOrMore(satisfy($somePredicate));
|
||||
$fast = takeWhile($somePredicate);
|
||||
```
|
||||
|
||||
The reason is that `$slow` reads one token at a time, and then appends it to the previous tokens. `$fast` on the other hand, reads all the tokens until `$predicate` fails, and then returns them all at once.
|
||||
|
||||
## Backtracking is slower
|
||||
|
||||
If your parser parses a long input, only to need to backtrack the whole thing when it fails, it's going to be slow. A better alternative is to organise your usage of choice in a way that only small chunks of the input need to be backtracked.
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser = choice(
|
||||
atLeastOne(alphaChar())->thenEof(),
|
||||
atLeastOne(alphaNumChar())->thenEof()
|
||||
);
|
||||
$result = $parser->tryString("abc123");
|
||||
```
|
||||
|
||||
In this example, the choice parser parses "abc", fails on "1", backtracks, and then parses all of "abc123". If we switch the two parsers inside the choice parser, we are more likely to reach the end of the input without doing any backtracking.
|
||||
|
||||
38
vendor/parsica-php/parsica/docs/resources/03_naming_conventions.md
vendored
Normal file
38
vendor/parsica-php/parsica/docs/resources/03_naming_conventions.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
title: Naming Conventions
|
||||
sidebar_label: Naming Conventions
|
||||
---
|
||||
|
||||
|
||||
## String and Character
|
||||
|
||||
PHP doesn't have a separate type for strings and characters, as opposed to some languages where string is defined as a list of characters. Still, as a convention in Parsica and its documentation, we generally use `'a'`, `'1'` (single quoted) to indicate a single character, and `"a"`, `"abc123"` (double quoted) to indicate a string.
|
||||
|
||||
We also use single quotes to indicate constant strings or symbols, such as `'STATUS_SUCCESS'`;
|
||||
|
||||
|
||||
## Predicates
|
||||
|
||||
Predicates are either prefixed with `is` or suffixed with `pred`.
|
||||
|
||||
```php
|
||||
<?php
|
||||
$predicate = orPred(isEqual('5'), isEqual('6'));
|
||||
assertTrue($predicate('6'));
|
||||
```
|
||||
|
||||
## Character Parsers
|
||||
|
||||
A parser for a single character is always suffixed with `Char`, as in `digitChar()`. These always output a string.
|
||||
|
||||
## Case
|
||||
|
||||
Some parsers have case-insensitive versions. These are sufficed with 'I'.
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser = stringI('hello world');
|
||||
$result = $parser->tryString("hElLO WoRlD");
|
||||
assertEquals("hElLO WoRlD", $result->output());
|
||||
```
|
||||
|
||||
27
vendor/parsica-php/parsica/docs/testdocs
vendored
Executable file
27
vendor/parsica-php/parsica/docs/testdocs
vendored
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
# Execute the docs to make sure all code examples are in sync with the Parsica code.
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
OUTPUTFORMAT=${1:-console}
|
||||
|
||||
vendor/bin/uptodocs run README.md --before=docs/before.php --output-format=$OUTPUTFORMAT
|
||||
vendor/bin/uptodocs run docs/contribute/design_goals.md --before=docs/before.php --output-format=$OUTPUTFORMAT
|
||||
vendor/bin/uptodocs run docs/resources/01_development_status.md --before=docs/before.php --output-format=$OUTPUTFORMAT
|
||||
vendor/bin/uptodocs run docs/resources/02_performance.md --before=docs/before.php --output-format=$OUTPUTFORMAT
|
||||
vendor/bin/uptodocs run docs/resources/03_naming_conventions.md --before=docs/before.php --output-format=$OUTPUTFORMAT
|
||||
vendor/bin/uptodocs run docs/tutorial/01_introduction.md --before=docs/before.php --output-format=$OUTPUTFORMAT
|
||||
vendor/bin/uptodocs run docs/tutorial/02_building_blocks.md --before=docs/before.php --output-format=$OUTPUTFORMAT
|
||||
vendor/bin/uptodocs run docs/tutorial/03_combinators.md --before=docs/before.php --output-format=$OUTPUTFORMAT
|
||||
vendor/bin/uptodocs run docs/tutorial/04_running_parsers.md --before=docs/before.php --output-format=$OUTPUTFORMAT
|
||||
vendor/bin/uptodocs run docs/tutorial/05_mapping_to_objects.md --before=docs/before.php --output-format=$OUTPUTFORMAT
|
||||
vendor/bin/uptodocs run docs/tutorial/06_order_matters.md --before=docs/before.php --output-format=$OUTPUTFORMAT
|
||||
vendor/bin/uptodocs run docs/tutorial/07_recursion.md --before=docs/before.php --output-format=$OUTPUTFORMAT
|
||||
vendor/bin/uptodocs run docs/tutorial/08_look_ahead.md --before=docs/before.php --output-format=$OUTPUTFORMAT
|
||||
vendor/bin/uptodocs run docs/tutorial/09_errors_and_labels.md --before=docs/before.php --output-format=$OUTPUTFORMAT
|
||||
vendor/bin/uptodocs run docs/tutorial/10_side_effects.md --before=docs/before.php --output-format=$OUTPUTFORMAT
|
||||
vendor/bin/uptodocs run docs/tutorial/11_dealing_with_space.md --before=docs/before.php --output-format=$OUTPUTFORMAT
|
||||
vendor/bin/uptodocs run docs/tutorial/20_expressions.md --before=docs/expressions.php --output-format=$OUTPUTFORMAT
|
||||
vendor/bin/uptodocs run docs/tutorial/90_functional_paradigms.md --before=docs/before.php --output-format=$OUTPUTFORMAT
|
||||
|
||||
149
vendor/parsica-php/parsica/docs/tutorial/01_introduction.md
vendored
Normal file
149
vendor/parsica-php/parsica/docs/tutorial/01_introduction.md
vendored
Normal file
@@ -0,0 +1,149 @@
|
||||
---
|
||||
title: What are parser combinators?
|
||||
---
|
||||
|
||||
Before you start, make sure you've had a look at the installation instructions.
|
||||
|
||||
|
||||
## Parsers
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser = char(':')
|
||||
->append(atLeastOne(punctuationChar()))
|
||||
->label('smiley');
|
||||
$result = $parser->tryString(':*{)');
|
||||
echo $result->output() . " is a valid smiley!";
|
||||
```
|
||||
|
||||
|
||||
A parser is a function that takes some unstructured input (like a string) and turns it into structured output, that's easier to work with. This output could be as simple as a slightly better structured string, or an array, an object, up to a complete abstract syntax tree. You can then use this data structure for subsequent processing.
|
||||
|
||||
You're probably using parsers all the time, such as `json_decode()`. And even just casting a string to a float <sup>[footnote 1](#floatval)</sup> really is parsing.
|
||||
|
||||
Parsica helps you build your own parsers, in a concise, declarative way. Behind the scenes it takes care of things like error handling, so you can focus on the parser itself.
|
||||
|
||||
|
||||
## Building a parser
|
||||
|
||||
There are many ways to build a parser for your own use case, ranging from formal grammars that get compiled into a parser, to regular expressions, to writing a parser entirely from scratch. They all have their own tradeoffs and limitations.
|
||||
|
||||
One of the great benefits of the parser combinator style is that, once you get the hang of it, they're generally easier to write, understand, and maintain. You start from building blocks, such as `digitChar()`, which returns a function that parses a single digit.
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser = digitChar();
|
||||
$input = "1. Write Docs";
|
||||
$result = $parser->tryString($input);
|
||||
$output = $result->output();
|
||||
assertSame("1", $output);
|
||||
assertIsString($output);
|
||||
```
|
||||
|
||||
## Parser Combinators
|
||||
|
||||
Parser Combinators are functions (or methods) that combine parsers into new parsers. Instead of writing one big parser, we can now write smaller parsers and cleverly compose them into larger parsers.
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser = char('a')->append(char('b'));
|
||||
$result = $parser->tryString("abc");
|
||||
$output = $result->output();
|
||||
assertEquals("ab", $output);
|
||||
```
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser =
|
||||
collect(
|
||||
string("Hello")->thenIgnore(char(",")),
|
||||
string("world")->thenIgnore(char("!")),
|
||||
);
|
||||
$result = $parser->tryString("Hello,world!");
|
||||
$output = $result->output();
|
||||
assertEquals(["Hello", "world"], $output);
|
||||
```
|
||||
|
||||
To make this work, we need a small change in our original definition of a parser.
|
||||
|
||||
> A parser is a function that takes some unstructured input (such as a string), and returns a more structured output, as well as the remaining unparsed part of the input.
|
||||
|
||||
This way, each parser function can parse a chunk of the input, and leave the remainder to another parser. The combinators take care of the heavy lifting: pass the input to the parser functions, pass the remainder to the next one, decide what to do with errors (eg, fail or backtrack or try another parser), ...
|
||||
|
||||
We can inspect the remainder:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser = sequence(char('a'), char('b'));
|
||||
$result = $parser->tryString("abc");
|
||||
|
||||
assertEquals("b", $result->output());
|
||||
assertEquals("c", $result->remainder());
|
||||
```
|
||||
|
||||
So when we run our parser using `$parser->tryString($input)`, the `sequence()` combinator first tries to run `char('a')` on the input `"abc"`. If it succeeds, it takes the remainder `"bc"` and successfully runs `char('b')` on it and returns the result. That result consists of the output from the last parser `"b"`, and the remainder `"c"`.
|
||||
|
||||
In imperative code, it would look something like this:
|
||||
|
||||
```php
|
||||
<?php
|
||||
final class MyParser
|
||||
{
|
||||
public function try(string $input) : array
|
||||
{
|
||||
$output1 = substr($input, 0, 1); // "a"
|
||||
if ($output1 == 'a') {
|
||||
$remainder1 = substr($input, 1); // "bc"
|
||||
$output2 = substr($remainder1, 0, 1); // "b"
|
||||
if ($output2 == 'b') {
|
||||
$remainder2 = substr($remainder1, 1); // "c"
|
||||
} else {
|
||||
throw new Exception("Parser failed");
|
||||
}
|
||||
} else {
|
||||
throw new Exception("Parser failed");
|
||||
}
|
||||
return ['output' => $output2, 'remainder' => $remainder2];
|
||||
}
|
||||
}
|
||||
$parser = new MyParser();
|
||||
$result = $parser->try("abc");
|
||||
|
||||
assertEquals('b', $result['output']);
|
||||
assertEquals('c', $result['remainder']);
|
||||
```
|
||||
|
||||
If you've been working in PHP long enough and have never used parser combinators, the code above may look more familiar for now. But imagine scaling that to parse anything from simple formats like credit card numbers, recursive structures like JSON or XML, or even entire programming languages like PHP. And that doesn't even include the code you'd need for performance, testing and debugging tooling, code reuse, and reporting on bad input. If you'd rather write `sequence(char('a'), char('b'))`, stick around.
|
||||
|
||||
|
||||
### Footnotes
|
||||
|
||||
#### <a name="floatval">Note 1</a>
|
||||
|
||||
```php
|
||||
<?php
|
||||
$v = floatval("1.23");
|
||||
assertSame(1.23, $v);
|
||||
```
|
||||
|
||||
The above looks fine at first sight, but `floatval()` really isn't a very good parser.
|
||||
|
||||
```php
|
||||
<?php
|
||||
assertSame(0.0, floatval("abc"));
|
||||
```
|
||||
|
||||
`floatval()` claims that the float of `"abc"` is `0`, which really should be an error. So you can only use `floatval` when you already know that the string doesn't contain anything non-float. Parsica can help you do that:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser = float()->map(fn($v) => floatval($v));
|
||||
try {
|
||||
// works:
|
||||
$result = $parser->tryString("1.23");
|
||||
assertSame(1.23, $result->output());
|
||||
|
||||
// throws a ParserHasFailed exception with message "Expected: float, got abc"
|
||||
$result = $parser->tryString("abc");
|
||||
} catch (ParserHasFailed $e) {}
|
||||
```
|
||||
94
vendor/parsica-php/parsica/docs/tutorial/02_building_blocks.md
vendored
Normal file
94
vendor/parsica-php/parsica/docs/tutorial/02_building_blocks.md
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
---
|
||||
title: Building Blocks
|
||||
---
|
||||
|
||||
## Predicates
|
||||
|
||||
The simplest building block is a parser that only considers the first character of an input. If the character satisfies some condition, we consume it from the input. We could write that with some `if` statements and `substr` calls, but Parsica provides abstractions for that.
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser = satisfy(isEqual('a'));
|
||||
$input = "abc";
|
||||
$result = $parser->tryString($input);
|
||||
assertEquals("a", $result->output());
|
||||
assertEquals("bc", $result->remainder());
|
||||
```
|
||||
|
||||
`isEqual('a')` is a predicate. If you call it with another argument, you get a boolean: `isEqual('a')('b') == false`.
|
||||
|
||||
`satisfy($predicate)` is a function returns a `Parser` object. You can think of it as a parser constructor. This object will do the heavy lifting of taking the first character of `$input`, and testing it with the predicate.
|
||||
|
||||
Parsica comes with some useful predicates, including boolean and/or/not combinators:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser = satisfy(orPred(isDigit(), isWhitespace()));
|
||||
```
|
||||
|
||||
## Character parsers
|
||||
|
||||
In practice, you may not need to use predicates and `satisfy` very often. The characters API provides commonly used parsers for single characters instead:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser = char('a');
|
||||
```
|
||||
|
||||
`char($x)` is defined as `satisfy(isEqual($x))` so the code above is equivalent to the first example. `charI()` is the case-insensitive version of `char()`. It preserves the case as is:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser = charI('a');
|
||||
$result = $parser->tryString("ABC");
|
||||
assertEquals("A", $result->output());
|
||||
$result = $parser->tryString("abc");
|
||||
assertEquals("a", $result->output());
|
||||
```
|
||||
|
||||
Parsica provides various parsers for groups of characters, like `alphaNumChar`, `upperChar`, `punctuationChar`, `newline`, and `digitChar`. You can find them all listed in the API Reference.
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser = digitChar();
|
||||
$result = $parser->tryString('123');
|
||||
assertEquals('1', $result->output());
|
||||
```
|
||||
|
||||
Note that even though we parsed a `digitChar`, the output is a string, not an int. That's because at this point, we're parsing characters. We'll talk about outputting other types than string later.
|
||||
|
||||
|
||||
## Strings
|
||||
|
||||
For longer sequences of characters, you can use `string` and `stringI`. Keep in mind that `stringI`is not just case-insensitive, but also case-preserving.
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser = stringI("parsica");
|
||||
$result = $parser->tryString("PARSICA");
|
||||
assertEquals("PARSICA", $result->output());
|
||||
$result = $parser->tryString("pArSiCa");
|
||||
assertEquals("pArSiCa", $result->output());
|
||||
```
|
||||
|
||||
If you want the output to be consistent, you can use `map` to convert it.
|
||||
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser = stringI("parsica")
|
||||
->map(fn($output) => strtolower($output));
|
||||
$result = $parser->tryString("pArSiCa");
|
||||
assertEquals("parsica", $result->output());
|
||||
```
|
||||
|
||||
|
||||
## Other parsers
|
||||
|
||||
Parsica comes with a growing library of other useful parsers, such as numeric types, and spaces. Always make sure to check the API documentation to know what the type of a parser is (aka the tpye of the output that the parser will produce.) For example, parsers like `space`, `tab`, and `newline` all output strings containing the characters they matched. On the other hand, `skipSpace` will output `null`, no matter if it consumed spaces or not. This makes sense because the point is to ignore them, not use them.
|
||||
|
||||
`skipSpace` consumes all kinds of space, whereas `skipHSpace` will stop consuming at newlines and carriage returns. They also come with two friends, `skipSpace1` and `skipHSpace1`, which expect at least on space to present.
|
||||
|
||||
|
||||
|
||||
|
||||
53
vendor/parsica-php/parsica/docs/tutorial/03_combinators.md
vendored
Normal file
53
vendor/parsica-php/parsica/docs/tutorial/03_combinators.md
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
title: Using Combinators
|
||||
---
|
||||
|
||||
|
||||
## Fluent interface
|
||||
|
||||
Many combinators come both as a standalone function, and as a method on the `Parser`object. They behave the same, and exist as a convenience for writing more readable code. Choosing one or the other will mostly depend on your usecase.
|
||||
|
||||
The general rule is that `combinator($parserA, $parserB) ≡ $parserA->combinator($parserB)`, in other words, they are equivalent.
|
||||
|
||||
In the example below, the `sequence` and `optional` combinators are used as functions and as methods, and both parsers are fully equivalent.
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser1 = sequence(
|
||||
optional(char('a')),
|
||||
char('b')
|
||||
);
|
||||
$parser2 = char('a')->optional()
|
||||
->sequence(char('b'));
|
||||
```
|
||||
|
||||
Sometimes combinators have different names for the same behaviour: `$parserA->or($parserB) ≡ either($parserA, $parserB)`. In this case, the reason is partially because `or` is a reserved keyword in PHP, and partially because `either` reads better in this case. Some combinators have aliases, such as `Parser#sequence()` and `Parser#followedBy()`, again these exist purely for convenience.
|
||||
|
||||
## Sequences
|
||||
|
||||
`sequence` is one of the most basic combinators you'll find. `sequence($parser1, $parser2)` means *"Try the first parser. If it fails, return the failure. If it succeeds, take the remaining input that was not consumed by `$parser1`, and try `$parser2`. Return the result of `$parser2`."*
|
||||
|
||||
It's important to understand that this drops whatever output `$parser1` produced. That's useful when you're only interested in what comes after `$parser1`. This example extracts a value that is prefixed by a string.
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser = sequence(string('My name is '), atLeastOne(alphaChar()));
|
||||
$result = $parser->tryString("My name is Parsica");
|
||||
assertEquals("Parsica", $result->output());
|
||||
```
|
||||
|
||||
## Alternatives
|
||||
|
||||
@TODO
|
||||
|
||||
## Appending
|
||||
|
||||
@TODO
|
||||
|
||||
## Folding combinators
|
||||
|
||||
There are also combinators that extend the behaviour of others. For example, `choice` is a left fold over the `either` combinator, effectively turning it from a combinator that takes two arguments, to one that take n arguments. `choice($parser1, $parser2, $parser3, ...) ≡ $parser1->or($parser2)->or($parser3)->or...`
|
||||
|
||||
The same happens with the `assemble` combinator, which call appends all its arguments. `assemble($parser1, $parser2, $parser3, ...) ≡ $parser1->and($parser2)->and($parser3)->...`
|
||||
|
||||
In general, you should use the simplest form available, so if you only have two choices, favour `or` over `choice`.
|
||||
79
vendor/parsica-php/parsica/docs/tutorial/04_running_parsers.md
vendored
Normal file
79
vendor/parsica-php/parsica/docs/tutorial/04_running_parsers.md
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
title: Running Parsers
|
||||
---
|
||||
|
||||
There are different ways of running your parser on an input.
|
||||
|
||||
## try() and tryString()
|
||||
|
||||
Most of the time, you'll want to use `try`. It will run the parser on an input `Stream`, return a `Succeed` (which implements `ParseResult`) on success, and throw a `ParserHasFailed` exception if the input can't successfully be parsed.
|
||||
|
||||
The `Stream` type generalises over a different ways of providing input. The simplest implementation is `StringStream`. This is really a wrapper around a PHP multibyte string.
|
||||
|
||||
(In v0.6.0, `StringStream` is also the _only_ implementation of `Stream`, but this will change.)
|
||||
|
||||
`ParseResult` has an `output()` method, which has the type `T` for a `Parser<T>` (see [Mapping to Objects](mapping_to_objects)). It also has a `remainder()` method, which gives you the part of the input that wasn't consumed by the parser.
|
||||
|
||||
`ParserHasFailed` has the usual `Exception` methods. It also gives you access to the `Fail implements ParseResult` object. This contains all the relevant information about the failure, such as `expected()`, `got()`, and `position()`.
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser = string('hello');
|
||||
|
||||
$result = $parser->try(new StringStream("hello world"));
|
||||
// Or, use tryString(string), which is an alias of try(StringStream):
|
||||
$result = $parser->tryString("hello world");
|
||||
|
||||
echo $result->output(); // "hello"
|
||||
echo $result->remainder(); // StringStream(" world")
|
||||
|
||||
// Now let's make it fail
|
||||
try {
|
||||
$result = $parser->tryString("hi world");
|
||||
} catch(ParserHasFailed $e) {
|
||||
$result = $e->parseResult();
|
||||
echo $result->expected(); // "string(hello)"
|
||||
echo $result->got(); // StringStream("hi world")
|
||||
$position = $result->position(); // A Position object containing the line number,
|
||||
// column, and filename where the failure happened
|
||||
}
|
||||
```
|
||||
|
||||
## run()
|
||||
|
||||
`run` is mostly intended for internal use.
|
||||
|
||||
The main difference between `run` and `try` is that `run` doesn't throw exceptions when parsing an input fails. (It might throw exceptions if your parser itself is incorrectly defined.) Instead, you'll always get a `ParseResult`, and you can inspect it with the same methods as above. You'll also get `isSuccess` and `isFail`, so you know what you're dealing with.
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser = string("hello");
|
||||
$result = $parser->run(new StringStream("some input"));
|
||||
if($result->isSuccess()) {
|
||||
echo $result->output();
|
||||
echo $result->remainder();
|
||||
} elseif ($result->isFail()) {
|
||||
echo $result->expected();
|
||||
echo $result->got();
|
||||
}
|
||||
```
|
||||
|
||||
## Continue with a result
|
||||
|
||||
Using `run` instead of `try` is a good choice when you want to do something with the result, such as:
|
||||
|
||||
- Building your own combinators
|
||||
- Interacting with `ParseResult` while in the middle of a parse flow
|
||||
|
||||
To do that, `ParseResult` lets you continue parsing:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser1 = string("hello");
|
||||
$result1 = $parser1->run(new StringStream("hello world"));
|
||||
$parser2 = string("world");
|
||||
$result2 = $result1->continueWith($parser2);
|
||||
```
|
||||
|
||||
`continueWith` takes another parser, and uses it to parse the remainder of the of the result. You may have noticed we didn't check for `isSuccess`. That's becasue we don't need to. `continueWith` is smart; if `$parser1` fails, trying to continue parsing on the result will not have any effect. In fact, the example above will fail, because `$parser1` doesn't take into account the space between "hello" and "world".
|
||||
|
||||
132
vendor/parsica-php/parsica/docs/tutorial/05_mapping_to_objects.md
vendored
Normal file
132
vendor/parsica-php/parsica/docs/tutorial/05_mapping_to_objects.md
vendored
Normal file
@@ -0,0 +1,132 @@
|
||||
---
|
||||
title: Mapping to Objects
|
||||
---
|
||||
|
||||
## Parser types
|
||||
|
||||
Most of the parsers that come with Parsica, return strings as outputs.
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser = digitChar();
|
||||
assertInstanceOf('Parsica\Parsica\Parser', $parser);
|
||||
|
||||
$result = $parser->tryString('1');
|
||||
assertIsString('Parsica\Parsica\StringStream', $result->output());
|
||||
assertEquals('1', $result->output());
|
||||
```
|
||||
|
||||
In PHP 7.x, the type of `$parser` is `Parser`, but you can think of it having the type `Parser<string>`. PHP doesn't support generics, so it doesn't enforce that. However, working with Parsica is easier if you always think of parsers having an inner type.
|
||||
|
||||
> `Parser<T>` means that if we successfully run the parser on an input, it will output a value of type `T`.
|
||||
|
||||
Here's an example of a parser of type `Parser<array<string>>`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser = sepBy(char(','), atLeastOne(digitChar()));
|
||||
$result = $parser->tryString('123,9,55');
|
||||
assertEquals(["123", "9", "55"], $result->output());
|
||||
```
|
||||
|
||||
## The map combinator
|
||||
|
||||
The point of parsing to turn strings into more useful data structures. The combinator `map` can help you with that. It does the same thing as PHP's `array_map` function. You combine a parser and a `callable`, and you get a new parser. This new parser will apply the callable to the output of the parser.
|
||||
|
||||
We can use it for manipulating the output. Here's a simple example:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser = atLeastOne(alphaChar())
|
||||
->map(fn(string $val) => strtolower($val));
|
||||
$result = $parser->tryString('PaRsIcA');
|
||||
assertEquals("parsica", $result->output());
|
||||
```
|
||||
|
||||
If the parser fails, the callable is not applied to the output (because there is no output). So you don't need to worry about error handling.
|
||||
|
||||
## Casting to scalars
|
||||
|
||||
We can now use this to cast the parser's output to scalars:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser = atLeastOne(digitChar())
|
||||
->map(fn(string $val) => intval($val));
|
||||
$result = $parser->tryString("123"); // input is still a string
|
||||
assertSame(123, $result->output()); // output is an int
|
||||
```
|
||||
|
||||
It also works inside nested parsers. We can use this on the `sepBy` example from above:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser = sepBy(
|
||||
char(','),
|
||||
atLeastOne(digitChar())
|
||||
->map(fn($val) => intval($val))
|
||||
);
|
||||
$result = $parser->tryString('123,9,55');
|
||||
assertSame([123, 9, 55], $result->output()); // array of ints
|
||||
```
|
||||
|
||||
The type of this last parser is now `Parser<array<int>>` instead of the original `Parser<array<string>>`.
|
||||
|
||||
## Casting to objects
|
||||
|
||||
We'll want to cast to much more interesting data structures than scalars and arrays. Let's parse some monetary values into a nested value object structure. `Money` is composed of an integer value and a `Currency` value object:
|
||||
|
||||
```php
|
||||
|
||||
|
||||
final class Currency
|
||||
{
|
||||
private string $currency;
|
||||
|
||||
function __construct(string $currency)
|
||||
{
|
||||
$this->currency = $currency;
|
||||
}
|
||||
}
|
||||
|
||||
// Side warning: don't actually use floats to do computations with money.
|
||||
final class Money
|
||||
{
|
||||
private float $amount;
|
||||
private Currency $currency;
|
||||
|
||||
function __construct(float $amount, Currency $currency)
|
||||
{
|
||||
$this->amount = $amount;
|
||||
$this->currency = $currency;
|
||||
}
|
||||
}
|
||||
|
||||
// $currency is a parser of type Parser<Currency>
|
||||
$currency = repeat(3, upperChar())
|
||||
->map(fn(string $c) => new Currency($c));
|
||||
|
||||
// $amount has type Parser<float>
|
||||
$amount = float()
|
||||
->map(fn(string $val) => floatval($val));
|
||||
|
||||
// $money has type Parser<[Currency, float]) because collect() has type Parser<[T]>
|
||||
$money = collect($currency, skipHSpace()->followedBy($amount));
|
||||
|
||||
// Let's change $money to type Parser<Money>
|
||||
$money = $money->map(fn(array $a) => new Money($a[1], $a[0]));
|
||||
|
||||
$result = $money->tryString('EUR 12.34');
|
||||
assertEquals(new Money(12.34, new Currency('EUR')), $result->output());
|
||||
|
||||
// We can now composer our Parser<Money> in larger parsers
|
||||
// $pricelist has type Parser<array<Money>>
|
||||
$priceList = collect(
|
||||
string("exVAT ")->followedBy($money)->thenIgnore(whitespace()),
|
||||
string("incVAT ")->followedBy($money)
|
||||
);
|
||||
$result = $priceList->tryString('exVAT EUR 100.00 incVAT EUR 121.00');
|
||||
|
||||
```
|
||||
|
||||
|
||||
27
vendor/parsica-php/parsica/docs/tutorial/06_order_matters.md
vendored
Normal file
27
vendor/parsica-php/parsica/docs/tutorial/06_order_matters.md
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: Order matters
|
||||
sidebar_label: Order matters
|
||||
---
|
||||
|
||||
|
||||
The order of clauses in an or() matters. If we do the following parser definition, the parser will consume "http", even if the strings starts with "https", leaving "s://..." as the remainder.
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser = string('http')->or(string('https'));
|
||||
$input = "https://parsica.verraes.net";
|
||||
$result = $parser->tryString($input);
|
||||
assertEquals("http", $result->output());
|
||||
assertEquals("s://parsica.verraes.net", $result->remainder());
|
||||
```
|
||||
|
||||
The solution is to consider the order of or clauses:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser = string('https')->or(string('http'));
|
||||
$input = "https://parsica.verraes.net";
|
||||
$result = $parser->tryString($input);
|
||||
assertEquals("https", $result->output());
|
||||
assertEquals("://parsica.verraes.net", $result->remainder());
|
||||
```
|
||||
108
vendor/parsica-php/parsica/docs/tutorial/07_recursion.md
vendored
Normal file
108
vendor/parsica-php/parsica/docs/tutorial/07_recursion.md
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
---
|
||||
title: Recursion
|
||||
---
|
||||
|
||||
Often we want to parse arbitrarily nested structures. Arrays, JSON, XML are such example. To do that, we need to be able to pass the parser to itself. Because of a limitation in PHP, we cannot pass a value around before it is created. The solution is to split this in two steps: create a placeholder for a recursive parser, and then define the parser in terms of itself.
|
||||
|
||||
## Example
|
||||
|
||||
We need to parse nested pairs such as `[1,[2,[3,4]]]`. The structure repeats itself, every item in the pair can be either a digit or another pair.
|
||||
|
||||
We cannot write this:
|
||||
|
||||
```
|
||||
<?php
|
||||
$pair = collect(
|
||||
ignore(char('[')),
|
||||
digit()->or($pair),
|
||||
ignore(char(',')),
|
||||
digit()->or($pair),
|
||||
ignore(char(']')),
|
||||
);
|
||||
```
|
||||
|
||||
The above results in "Undefined variable: pair" because we're trying to use `$pair` before it's defined. Instead, we need to mark the parser as `recursive` in a first step, and then define how the parser should `recurse`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
// Create a recursive parser first
|
||||
$pair = recursive();
|
||||
|
||||
// Then define the parser
|
||||
$pair->recurse(
|
||||
between(
|
||||
char('['),
|
||||
char(']'),
|
||||
collect(
|
||||
digitChar()->or($pair)
|
||||
->thenIgnore(char(',')),
|
||||
digitChar()->or($pair)
|
||||
)
|
||||
),
|
||||
);
|
||||
|
||||
$result = $pair->tryString("[1,[2,[3,4]]]");
|
||||
assertSame(['1', ['2', ['3', '4']]], $result->output());
|
||||
```
|
||||
|
||||
It's possible to nest multiple recursive parsers. Simply initialise them all first using `recursive()` and then define them in terms of each other:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$curlyPair = recursive();
|
||||
$squarePair = recursive();
|
||||
$anyPair = $curlyPair->or($squarePair);
|
||||
|
||||
$inner = collect(
|
||||
digitChar()->or($anyPair)
|
||||
->thenIgnore(char(',')),
|
||||
digitChar()->or($anyPair)
|
||||
);
|
||||
|
||||
$curlyPair->recurse(
|
||||
between(char('{'), char('}'), $inner),
|
||||
);
|
||||
|
||||
$squarePair->recurse(
|
||||
between(char('['), char(']'), $inner),
|
||||
|
||||
);
|
||||
|
||||
$mixed = "{1,[2,{3,4}]}";
|
||||
$result = $anyPair->tryString($mixed);
|
||||
assertSame(['1', ['2', ['3', '4']]], $result->output());
|
||||
```
|
||||
|
||||
Note that when you initialize a parser with `recursive()`, it is in fact mutable, and the `recurse()` method mutates it. All parsers are immutable, and this is the only exception. After calling `recurse()`, the parser is immutable again and behaves just like any other parser.
|
||||
|
||||
## Using recusion to avoid loops
|
||||
|
||||
Let's say we want to parse the character `'a'` at least one time, so that `"aaab"` outputs `"aaa"`, but `"bbb"` fails. Imperatively, you could solve this by running the `char('a')` parser in a while loop, and stop on the first failure. We can express it more concisely with recursion though:
|
||||
|
||||
1. Start by parsing `char('a')`.
|
||||
2. Append another `char('a')`, but this second one is `optional()`.
|
||||
3. Append another `optional(char('a'))`
|
||||
4. Notice the similarity between the first two steps. This suggest an opportunity for recursion.
|
||||
5. Wrap our `char('a')->append(optional(char('a')))` in a `recurse()` parser.
|
||||
6. Replace the second `char('a')` by the recursive parser.
|
||||
|
||||
The end result looks like this:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$rec = recursive();
|
||||
$rec->recurse(char('a')->append(optional($rec)));
|
||||
$result = $rec->tryString("aaab");
|
||||
assertEquals("aaa", $result->output());
|
||||
```
|
||||
|
||||
In fact the code above is how the `atLeastOne()` combinator works, so you can simplify that code by writing this:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser = atLeastOne(char('a'));
|
||||
$result = $parser->tryString("aaab");
|
||||
assertEquals("aaa", $result->output());
|
||||
```
|
||||
|
||||
|
||||
58
vendor/parsica-php/parsica/docs/tutorial/08_look_ahead.md
vendored
Normal file
58
vendor/parsica-php/parsica/docs/tutorial/08_look_ahead.md
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
title: Looking ahead
|
||||
---
|
||||
|
||||
## notFollowedBy
|
||||
|
||||
Say you want to match the `print` keyword in a programming language. You can express that with the `string("print")` parser, but it will match more than you'd like:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$print = string("print");
|
||||
|
||||
$result = $print->tryString("print('Hello World');");
|
||||
assertEquals("print", $result->output());
|
||||
|
||||
$result = $print->tryString("printXYZ('Hello World');");
|
||||
assertEquals("print", $result->output()); // oops!
|
||||
```
|
||||
|
||||
As you can see, "printXYZ" also results in "print", but it wasn't our intention, because "printXYZ" is not a valid keyword.
|
||||
|
||||
We can solve it by using the `notFollowedBy` combinator.
|
||||
|
||||
```php
|
||||
<?php
|
||||
$print = keepFirst(string("print"), notFollowedBy(alphaNumChar()));
|
||||
$result = $print->run(new StringStream("printXYZ('Hello World');"));
|
||||
assertTrue($result->isFail());
|
||||
```
|
||||
|
||||
There's a fluent interface as well:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$print = string("print")->notFollowedBy(alphaNumChar());
|
||||
$result = $print->run(new StringStream("printXYZ('Hello World');"));
|
||||
assertTrue($result->isFail());
|
||||
```
|
||||
|
||||
|
||||
In practice, we'll have a lot more keywords than just the one. A good habit is to first generalize this to all the keywords in our language. Then, using our new `$keyword` parser constructor, we can match the exact variations we like:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$keyword = fn(string $name) => keepFirst(string($name), notFollowedBy(alphaNumChar()));
|
||||
|
||||
$parser = choice(
|
||||
$keyword('printf'),
|
||||
$keyword('print'),
|
||||
$keyword('sprintf')
|
||||
);
|
||||
|
||||
$result = $parser->tryString("print('Hello World');");
|
||||
assertEquals("print", $result->output());
|
||||
|
||||
$result = $parser->tryString("printf('Hello %s', 'world');");
|
||||
assertEquals("printf", $result->output());
|
||||
```
|
||||
75
vendor/parsica-php/parsica/docs/tutorial/09_errors_and_labels.md
vendored
Normal file
75
vendor/parsica-php/parsica/docs/tutorial/09_errors_and_labels.md
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
---
|
||||
title: Errors and labels
|
||||
---
|
||||
|
||||
Error messages in Parsica give you information about what the parser expected, where the problem happened, and what it got instead.
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser = sepBy1(char(','), atLeastOne(alphaChar()));
|
||||
$input = "Ÿellow,Red,Green";
|
||||
//$parser->tryString($input);
|
||||
```
|
||||
|
||||
_(Note: We're using [UpToDocs](https://github.com/mathiasverraes/uptodocs) to automatically test all the code samples in this documentation. The downside is that throwing an exception in the docs causes the build to fail! That's why some of the code above is commented out.)_
|
||||
|
||||
If you uncomment and run the above code, you'll get an exception like this:
|
||||
|
||||
```
|
||||
<input>:1:1
|
||||
|
|
||||
1 | Ÿellow,Red,Green
|
||||
| ^— column 1
|
||||
Unexpected 'Ÿ'
|
||||
Expecting at least one A-Z or a-z, separated by ','
|
||||
```
|
||||
|
||||
It shows the filename, line number and column position, as well as an autogenerated expectation.
|
||||
|
||||
Often you'll want something a bt more meaningful than "at least one A-Z or a-z". You can do that by attaching your own labels to some of your parsers. For example, we can label the colours:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser = sepBy1(
|
||||
string(','),
|
||||
atLeastOne(alphaChar())->label("colour")
|
||||
);
|
||||
```
|
||||
|
||||
That will yield:
|
||||
|
||||
```
|
||||
...
|
||||
Expecting colour, separated by ','
|
||||
```
|
||||
|
||||
Or you can attach the label to the entire parser:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parser = sepBy1(
|
||||
string(','),
|
||||
atLeastOne(alphaChar())
|
||||
)->label("a list of colours");
|
||||
```
|
||||
|
||||
```
|
||||
...
|
||||
Expecting a list of colours
|
||||
```
|
||||
|
||||
The best approach will of course depend on your specific use case. A good habit is to keep in mind that ultimately, the errors are there for the end user who will see them. This could be a user who enters some values in a form, a programmer using your API, someone building other parsers on top of your parser... Feed your parser with some wrong inputs that you are likely to get in the real world, and get a feel for what makes a helpful error message.
|
||||
|
||||
## Doing your own error reporting
|
||||
|
||||
The information in the error message is also available from `ParseResult`. You can use this to make your own error messages, if you want to render them as HTML or send them to an API. For example, these are the line and column number where the parser ended up:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$result = string('Hello')->run(new StringStream("Hello, World"));
|
||||
assertSame(1, $result->position()->line());
|
||||
assertSame(6, $result->position()->column());
|
||||
```
|
||||
|
||||
Have a look at the `ParseResult` API to see what else it can do.
|
||||
|
||||
78
vendor/parsica-php/parsica/docs/tutorial/10_side_effects.md
vendored
Normal file
78
vendor/parsica-php/parsica/docs/tutorial/10_side_effects.md
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
title: Side Effects and Events
|
||||
---
|
||||
|
||||
Sometimes you may want to perform actions when your parser encounters something you're interested in. Parsica provides combinator called `emit()`. It allows you to inject side effects at any point. It's intentionally very barebones: It's really just a callback function, that gets called only when the parser succeeds.
|
||||
|
||||
|
||||
```php
|
||||
<?php
|
||||
// Define a function that takes the output and performs some side effect:
|
||||
$print = fn(string $output) => print($output);
|
||||
// Define a parser:
|
||||
$parser = many(either(
|
||||
char('a'),
|
||||
// Combine the 'b' parser with emit:
|
||||
char('b')->emit($print)
|
||||
));
|
||||
// Running the parser calls print() whenever a 'b' is encountered:
|
||||
$parser->tryString('aababba'); // Prints "bbb"
|
||||
```
|
||||
|
||||
Using closures and mutable objects, you can embed mutability into a parsing process.
|
||||
|
||||
```php
|
||||
<?php
|
||||
final class Counter
|
||||
{
|
||||
private int $count = 0;
|
||||
function incr(): void { $this->count++; }
|
||||
function count(): int{ return $this->count; }
|
||||
}
|
||||
|
||||
// Make a mutable object:
|
||||
$counter = new Counter();
|
||||
// Use it inside a closure:
|
||||
$incr = fn(string $output) => $counter->incr();
|
||||
$parser = many(either(
|
||||
char('a'),
|
||||
// Increment counter when we hit 'b'
|
||||
char('b')->emit($incr)
|
||||
));
|
||||
$parser->tryString('aababba');
|
||||
assertSame(3, $counter->count());
|
||||
```
|
||||
|
||||
|
||||
For most use cases, we suggest using `emit()` with an adapter for your application's event dispatching mechanism. The following shows how to adapt `emit()` to any [PSR-14](https://www.php-fig.org/psr/psr-14/) compatible event dispatcher.
|
||||
|
||||
|
||||
```php
|
||||
<?php
|
||||
// Your (or your framework's) event dispatcher:
|
||||
final class YourDispatcher implements \Psr\EventDispatcher\EventDispatcherInterface
|
||||
{
|
||||
public function dispatch(object $event) { /* ... */ }
|
||||
}
|
||||
$yourDispatcher = new YourDispatcher();
|
||||
|
||||
// An adapter that turns a value into an event and sends it to your dispatcher:
|
||||
$yourAdapter = function (Colour $colour) use ($yourDispatcher) : void {
|
||||
$timestamp = new DateTimeImmutable("now");
|
||||
$event = new ColourWasEncountered($timestamp, $colour);
|
||||
$yourDispatcher->dispatch($event);
|
||||
};
|
||||
$parser = many(
|
||||
either(
|
||||
string('red'),
|
||||
string('green'),
|
||||
string('blue'),
|
||||
)
|
||||
// The parser outputs string, the map() combinator turns those into domain objects:
|
||||
->map(fn(string $output) : Colour => new Colour($output))
|
||||
// Emit the Colour object to the adapter:
|
||||
->emit($yourAdapter)
|
||||
);
|
||||
```
|
||||
|
||||
This way, you can neatly separate the occurrence of a parsing event, from the actual side effect. If the dispatcher is asynchronous, the parsing process can keep continuing, without being interrupted by blocking side effects, such as writing to a database. Or when parsing a large input file or continuous input stream, you can start processing the results before the parsing has finished.
|
||||
107
vendor/parsica-php/parsica/docs/tutorial/11_dealing_with_space.md
vendored
Normal file
107
vendor/parsica-php/parsica/docs/tutorial/11_dealing_with_space.md
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
---
|
||||
title: Dealing with Space
|
||||
---
|
||||
|
||||
Parsica comes with a number of useful parsers for dealing with different types of whitespace and newlines, as well with required or optional whitespace. We recommend browsing `src/space.php` to see what is available, so you don't need to build your own parsers for that.
|
||||
|
||||
## Space consumers
|
||||
|
||||
When building a parser for say a language or a file format, you often have specific rules about space. Whitespace can be required or optional, and expressions can be valid or invalid if they contain newlines. All of these are could be valid or invalid depending on your case:
|
||||
|
||||
```
|
||||
// with 0, 1, or more spaces
|
||||
1+1
|
||||
1 + 1
|
||||
1 + 1
|
||||
|
||||
// multiline
|
||||
1 +
|
||||
2
|
||||
|
||||
// tabs
|
||||
1
|
||||
+ 2
|
||||
```
|
||||
|
||||
There's too much variation for Parsica to provide a single solution. However, you don't want to litter your code with space parsers everywhere:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$term = digitChar();
|
||||
$operator = char('+');
|
||||
$parser = collect(
|
||||
$term,
|
||||
skipSpace1(),
|
||||
$operator,
|
||||
skipSpace1(),
|
||||
$term,
|
||||
skipSpace1(),
|
||||
)->map(fn($o) => $o[0] + $o[4]);
|
||||
|
||||
$result = $parser->tryString("1 +\n 2\t");
|
||||
assertSame(3, $result->output());
|
||||
```
|
||||
|
||||
This is noisy. And if you want to change the rules about whitespace or build more complex parsers, you have to deal with this problem all the time, making it unmaintainable (or at least annoying).
|
||||
|
||||
The idea is to build a space consumer that you can reuse everywhere. The space consumer is a parser combinator that you wrap around another parser, and that returns the output of the inner parser, ignoring whitespace. A typical approach is to consistently ignore space after the thing you're interested in.
|
||||
|
||||
```php
|
||||
<?php
|
||||
// $token behaves just like $parser, but requires the parsed
|
||||
// value to be followed by at least 1 space
|
||||
$token = fn(Parser $parser) => keepFirst($parser, skipSpace1());
|
||||
|
||||
// Now we wrap our parsers
|
||||
$term = $token(digitChar());
|
||||
$operator = $token(char('+'));
|
||||
|
||||
// Our main parser now has the same "shape" as the expression we're trying to parse:
|
||||
$parser = collect(
|
||||
$term,
|
||||
$operator,
|
||||
$term,
|
||||
)->map(fn($o) => $o[0] + $o[2]);
|
||||
|
||||
$result = $parser->tryString("1 +\n 2\t");
|
||||
assertSame(3, $result->output());
|
||||
```
|
||||
|
||||
Now, all the logic for skipping space is nicely contained in `$token`. If we wanted to disallow multiline expressions, we only need to replace `skipSpace1()` with `skipHSpace1()` in one place.
|
||||
|
||||
As an example, here's an excerpt from the JSON parser, using the ws (whitespace) as defined in the JSON spec:
|
||||
|
||||
```php
|
||||
final class MyJSON
|
||||
{
|
||||
public static function ws(): Parser
|
||||
{
|
||||
return zeroOrMore(satisfy(isCharCode([0x20, 0x0A, 0x0D, 0x09])))->voidLeft(null)
|
||||
->label('whitespace');
|
||||
}
|
||||
|
||||
public static function token(Parser $parser): Parser
|
||||
{
|
||||
return keepFirst($parser, JSON::ws());
|
||||
}
|
||||
|
||||
public static function object(): Parser
|
||||
{
|
||||
return map(
|
||||
between(
|
||||
JSON::token(char('{')),
|
||||
JSON::token(char('}')),
|
||||
sepBy(
|
||||
JSON::token(char(',')),
|
||||
JSON::member()
|
||||
)
|
||||
),
|
||||
fn(array $members):object => (object)array_merge(...$members));
|
||||
}
|
||||
|
||||
// see src/JSON/JSON.php for the full code
|
||||
}
|
||||
```
|
||||
|
||||
If you have multiple ways of handling space in one parser, you can of course define multiple space consumers and give them relevant names.
|
||||
|
||||
246
vendor/parsica-php/parsica/docs/tutorial/20_expressions.md
vendored
Normal file
246
vendor/parsica-php/parsica/docs/tutorial/20_expressions.md
vendored
Normal file
@@ -0,0 +1,246 @@
|
||||
---
|
||||
title: Parsing Expression Languages
|
||||
---
|
||||
|
||||
Can Parsica parse expression? Why yes, I'm glad you asked!
|
||||
|
||||
An expression, roughly, is anything that can evaluated to a value, such as
|
||||
|
||||
- arithmetic expressions `(1 + 2) * 3`,
|
||||
- boolean expressions `x and (y or z)`,
|
||||
- code inside a template language `{{ user.loggedIn ? 'Hello ' ~ user.name : 'Log in' }}`,
|
||||
- spreadsheet formulas `=SUM(A1:A10) * B1`,
|
||||
- rules in a rule engine
|
||||
- logic inside a configuration language,
|
||||
- and anything else you can think of!
|
||||
|
||||
The tricky thing about parsing expressions is that you often have to deal with things like recursion, associativity, and operator precedence. These can make it pretty tricky to build a parser. Parsica provides the `expression()` function, which offers a simple way to create a parser for your custom expression language.
|
||||
|
||||
## Arithmetic
|
||||
|
||||
Let's build a simple calculator, that can evaluate expressions like `1 + 2 * (2 - 3)` to `-1`.
|
||||
|
||||
Let's handle whitespace first. (See the chapter on "Dealing with Space" for details.)
|
||||
|
||||
```php
|
||||
<?php
|
||||
$token = fn(Parser $parser) => keepFirst($parser, skipHSpace());
|
||||
```
|
||||
|
||||
Next, we define a parser for our terms. For this example, let's keep it simple and support only natural numbers:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$term = fn(): Parser => $token(atLeastOne(digitChar()))->map('intval');
|
||||
```
|
||||
|
||||
Let's do parentheses next. Parsica's `between()` combinator will do the job nicely, but let's wrap it in our combinator for clarity and reusability:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$parens = fn (Parser $parser): Parser => $token(between($token(char('(')), $token(char(')')), $parser));
|
||||
```
|
||||
|
||||
|
||||
Now let's define our first expression, using `expression()`. In our language, an expression can be:
|
||||
|
||||
1. A naked term like `12`
|
||||
2. A term between parentheses `(12)`
|
||||
3. An operator and its arguments `1 + 2`
|
||||
4. The arguments are expressions themselves, as in `1 + (2 + 3)`
|
||||
|
||||
An expression is defined using expressions, so this calls for recursion. (See the chapter on Recursion.) Let's ignore operators for now, and do the simplest recursive expression parser:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$expr = recursive();
|
||||
$primary = $parens($expr)->or($term());
|
||||
$expr->recurse(
|
||||
expression($primary, [])
|
||||
);
|
||||
|
||||
$result = $expr->tryString("(((12)))");
|
||||
assertSame(12, $result->output());
|
||||
```
|
||||
|
||||
We're saying here that `$primary` is either an expression wrapped in parens, or a term. `$expr` is an expression that uses `$primary` as its primary parser.
|
||||
|
||||
Now let's add the plus operator. We need a parser for the symbol itself, in this case a simple `char('+')` will do, but it could be anything. For example, PHP has two 'not equal' operators, which we could parse in one go `either(string('!='), string('<>'))`.
|
||||
|
||||
We also need to decide what to do with the terms that we parse, using a transformation. This is a function that will take the left and the right operands from our `+`. As we're building a calculator, we're simple going to add up the two terms, using `fn($left, $right) => $left + $right`. (Later we will use this to create abstract syntax trees.)
|
||||
|
||||
Finally, we need to tell the expression parser that `+` is a binary operator, and that we want it to be left associative. Let's put it all together:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$expr = recursive();
|
||||
$primary = $parens($expr)->or($term());
|
||||
$expr->recurse(
|
||||
expression(
|
||||
$primary,
|
||||
[
|
||||
leftAssoc(
|
||||
binaryOperator($token(char('+')), fn($l, $r) => $l + $r)
|
||||
)
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
$result = $expr->tryString("1 + 2 + 3");
|
||||
assertSame(6, $result->output());
|
||||
$result = $expr->tryString("(1 + (2 + 3) + 4)");
|
||||
assertSame(10, $result->output());
|
||||
```
|
||||
|
||||
The second argument to `expression()` is an array of operators. The order is important: it determines the precedence. `+` and `-` have the same precedence, whereas `*` and `/` have the same precedence as each other, but higher precedence than `+` and `-`. We can solve this easily by grouping each precedence level, and putting the highest precedence levels first.
|
||||
|
||||
```php
|
||||
<?php
|
||||
$expr = recursive();
|
||||
$primary = $parens($expr)->or($term());
|
||||
$expr->recurse(
|
||||
expression(
|
||||
$primary,
|
||||
[
|
||||
leftAssoc(
|
||||
binaryOperator($token(char('*')), fn($l, $r) => $l * $r),
|
||||
binaryOperator($token(char('/')), fn($l, $r) => $l / $r),
|
||||
),
|
||||
leftAssoc(
|
||||
binaryOperator($token(char('+')), fn($l, $r) => $l + $r),
|
||||
binaryOperator($token(char('-')), fn($l, $r) => $l - $r),
|
||||
),
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
$result = $expr->tryString("1 + 2 * 3");
|
||||
assertSame(7, $result->output());
|
||||
$result = $expr->tryString("(1 + 2) * 3");
|
||||
assertSame(9, $result->output());
|
||||
$result = $expr->tryString("1 - 2 - 3"); // interpreted as ((1 - 2) - 3)
|
||||
assertSame(-4, $result->output());
|
||||
```
|
||||
|
||||
You can play around with the precedence and the associativity to see how it impacts the result. As an exercise, make a parser that solves `1 - 2 - 3 = (1 - (2 - 3) = (1 - (-1)) = 2`.
|
||||
|
||||
## Non-associative operators
|
||||
|
||||
Non-associative means that an expression like `1 + 2 + 3` cannot be resolved, because there is no way to decide whether it's associates left `(1 + 2) + 3` or right `1 + (2 + 3)`. The parser will simply fail. Of course, for addition, non-associativity wouldn't make sense, but for other languages or operators it might.
|
||||
|
||||
## Unary operators
|
||||
|
||||
You can add unary operators, such as the negation prefix operator `-`, and the increment and decrement postfix operators `++` and `--`.
|
||||
|
||||
```php
|
||||
// ...
|
||||
[
|
||||
prefix(
|
||||
unaryOperator(char('-'), fn($v) => -$v)
|
||||
),
|
||||
postfix(
|
||||
unaryOperator(string('++'), fn($v) => $v + 1),
|
||||
unaryOperator(string('--'), fn($v) => $v - 1),
|
||||
),
|
||||
// ...
|
||||
];
|
||||
|
||||
```
|
||||
|
||||
## Parsing to an AST
|
||||
|
||||
Building calculators isn't that interesting of course. Typically you'll want your parser to output a datastructure that represents your expression, called an Abstract Syntax Tree or AST. This structure can then be used for whatever the next step in your program is, ranging from evaluation to compilation, static analysis, typechecking, optimisation, rendering and formatting...
|
||||
|
||||
Let's build a simple Boolean expression language, starting with the types for AST. Everything else will be pretty similar to the calculator example above, but instead of evaluating the expressions on the fly, we use the transform functions to create the datastructure.
|
||||
|
||||
```php
|
||||
<?php
|
||||
// every term or expression in our language is a Boolean:
|
||||
interface Boolean {}
|
||||
|
||||
// Literals
|
||||
class True_ implements Boolean {}
|
||||
class False_ implements Boolean {}
|
||||
|
||||
// A variable will be replaced with a value at evaluation stage
|
||||
class Variable implements Boolean {
|
||||
private string $name;
|
||||
function __construct(string $name){$this->name = $name;}
|
||||
}
|
||||
|
||||
// Our operators are Booleans that are composed of other Booleans
|
||||
class Not_ implements Boolean {
|
||||
private Boolean $boolean;
|
||||
function __construct(Boolean $boolean){$this->boolean = $boolean;}
|
||||
}
|
||||
class And_ implements Boolean {
|
||||
private Boolean $l, $r;
|
||||
function __construct(Boolean $l, Boolean $r){
|
||||
$this->l = $l;
|
||||
$this->r = $r;
|
||||
}
|
||||
}
|
||||
class Or_ implements Boolean {
|
||||
private Boolean $l, $r;
|
||||
function __construct(Boolean $l, Boolean $r){
|
||||
$this->l = $l;
|
||||
$this->r = $r;
|
||||
}
|
||||
}
|
||||
|
||||
// Now let's write the parser
|
||||
$token = fn(Parser $parser) : Parser => keepFirst($parser, skipHSpace());
|
||||
$parens = fn (Parser $parser): Parser => $token(between($token(char('(')), $token(char(')')), $parser));
|
||||
|
||||
// A term is a literal TRUE/FALSE or a variable
|
||||
$term = fn(): Parser => $token(choice(
|
||||
char('$')->followedBy(atLeastOne(alphaChar()))->map(fn($name) => new Variable($name)),
|
||||
string("TRUE")->map(fn($v) => new True_),
|
||||
string("FALSE")->map(fn($v) => new False_),
|
||||
));
|
||||
$expr = recursive();
|
||||
|
||||
// When the parser encounters NOT, AND, or OR, it returns a Not_, And_, or Or_ object.
|
||||
// The $v, $l and $r arguments can be Boolean objects themselves, creating the tree.
|
||||
$expr->recurse(expression(
|
||||
$parens($expr)->or($term()),
|
||||
[
|
||||
prefix(
|
||||
unaryOperator($token(string("NOT")), fn($v) => new Not_($v))
|
||||
),
|
||||
leftAssoc(
|
||||
binaryOperator($token(string("AND")), fn($l, $r) => new And_($l, $r))
|
||||
),
|
||||
leftAssoc(
|
||||
binaryOperator($token(string("OR")), fn($l, $r) => new Or_($l, $r))
|
||||
),
|
||||
]
|
||||
));
|
||||
|
||||
|
||||
$parser = $expr->thenEof(); // check if we reached the end of the input
|
||||
$result = $parser->tryString('$isBlue AND NOT ($isEdible OR $isDrinkable)');
|
||||
assertEquals(
|
||||
new And_(
|
||||
new Variable('isBlue'),
|
||||
new Not_(
|
||||
new Or_(
|
||||
new Variable('isEdible'),
|
||||
new Variable('isDrinkable'),
|
||||
)
|
||||
)
|
||||
),
|
||||
$result->output()
|
||||
);
|
||||
```
|
||||
|
||||
Now the AST can be used for whatever purposes you need. In our Boolean example above, as an exercise you can
|
||||
|
||||
- add a `render()` method to write the expression back to a pretty formatted string,
|
||||
- add a `reduce()` method that simplifies the AST (eg turning `TRUE AND TRUE` into `TRUE`),
|
||||
- add an `evaluate(['isBlue' => true, 'isEdible' => false, ...])` method that calculates the final result
|
||||
- ...
|
||||
|
||||
|
||||
|
||||
|
||||
142
vendor/parsica-php/parsica/docs/tutorial/90_functional_paradigms.md
vendored
Normal file
142
vendor/parsica-php/parsica/docs/tutorial/90_functional_paradigms.md
vendored
Normal file
@@ -0,0 +1,142 @@
|
||||
---
|
||||
title: Functional Paradigms
|
||||
sidebar_label: Functional Paradigms
|
||||
---
|
||||
|
||||
Internally, Parsica is designed using paradigms from functional programming. We list them here for anybody who's interested in FP, but you don't need to know them to work with Parsica.
|
||||
|
||||
Throughout this document, `$parser1 ≡ $parser2` means that you can swap `$parser1` with `$parser2` and vice-versa, and it will not affect the outcome of your program.
|
||||
|
||||
## Purity
|
||||
|
||||
Almost all the code is pure and referentially transparent. [A notable exception](recursion) is the combo of `recursive()` and `Parser::recurse()`. The latter mutates a `Parser`. We constrained this so that you can't use the parser when it's not set up yet, and after calling `recurse()`, you can't call it again. So not strictly pure, but close enough not to matter much in practice.
|
||||
|
||||
The combinators are all pure. Some combinators are implemented as instance methods on `Parser`, but these are also pure. You can think of them as functions that take `$this` as the first argument.
|
||||
|
||||
```
|
||||
$parser1->combinator($parser2)
|
||||
≡ combinator($parser1, $parser2)
|
||||
```
|
||||
|
||||
In fact, very often there are both a function and an instance method for the same combinator, where one is an alias for the other.
|
||||
|
||||
## Types
|
||||
|
||||
There are no generics in PHP 7.4, but we use the Psalm static typechecker to simulate some of it. The two type are really `Parser<T>` and `ParseResult<T>`, where `T` is the type of the resulting output in the case of a successful parse.
|
||||
|
||||
## Either
|
||||
|
||||
`ParseResult<T>` is approximately an `Either<ParseFailure, ParseSuccess<T>>` type.
|
||||
|
||||
## Functors
|
||||
|
||||
`ParseResult` and `Parser` are functors, using the `map` method.
|
||||
|
||||
For `ParseResult`, the function is only applied to the output if `ParseResult::isSuccess()` is true, and ignored in other cases.
|
||||
|
||||
Similarly, mapping over `Parser` is really mapping over the future `ParseResult`.
|
||||
|
||||
## Monoids
|
||||
|
||||
`ParseResult<T>` is a monoid under the `ParseResult::append()` operation, when `T` is a monoid as well. `discard()` is the zero value.
|
||||
|
||||
`Parser<T>` is a monoid under the `Parser::append()`, when `T` is a monoid as well. `nothing()` is the zero value.
|
||||
|
||||
### Laws
|
||||
|
||||
|
||||
#### Identity
|
||||
|
||||
```
|
||||
$parser->append(nothing()) ≡ $parser
|
||||
```
|
||||
|
||||
```
|
||||
nothing()->append($parser) ≡ $parser
|
||||
```
|
||||
|
||||
#### Associativity
|
||||
|
||||
```
|
||||
$p1->append($p2)->append($p3)
|
||||
≡ $p1->append($p2->append($p3))
|
||||
```
|
||||
|
||||
## Applicative Functors
|
||||
|
||||
`Parser<T>` is an applicative functor.
|
||||
|
||||
- `pure()` is a parser that will always output its argument, no matter what the input was. Type: `T -> Parser<T>`.
|
||||
- `apply()` is sequential application, aka `<*>`. `pure($callable)->apply($parser)` is a parser that applies `$callable` to the output of `$parser`. It works for callables with multiple arguments, if the callable is curried: `pure(curry($callable))->apply($p1)->apply($p2)`. We used [matteosister/php-curry](https://github.com/matteosister/php-curry) to test this, but any method for currying functions should work.
|
||||
- `keepFirst()` and `keepSecond()` are `<*` and `*>` respectively. Both parsers need to succeed but only the result from one of them is returned.
|
||||
|
||||
### Laws
|
||||
|
||||
#### Identity
|
||||
|
||||
```
|
||||
pure(identity())->apply($parser) ≡ $parser
|
||||
```
|
||||
|
||||
#### Homomorphism
|
||||
|
||||
```
|
||||
pure($f)->apply(pure($x)) ≡ pure($f($x))
|
||||
```
|
||||
|
||||
#### Interchange
|
||||
|
||||
```
|
||||
$p->apply(pure($x))
|
||||
≡ pure(fn($f) => $f($x))->apply($p)
|
||||
```
|
||||
|
||||
#### Composition
|
||||
|
||||
```
|
||||
// Assuming that
|
||||
$compose = fn($f, $g) => fn($x) => $f($g($x))
|
||||
|
||||
pure($compose)->apply($p1)->apply($p2)->apply($p3)
|
||||
≡ $p1->apply($p2->apply($p3))
|
||||
```
|
||||
|
||||
#### Map
|
||||
|
||||
```
|
||||
pure($f)->apply($parser) ≡ $parser->map($f)
|
||||
```
|
||||
|
||||
## Monads
|
||||
|
||||
`Parser<T>` is a monad.
|
||||
|
||||
- `pure()`: see above.
|
||||
- `sequence()` runs two parsers in sequence, dropping the result of the first one. Both parsers consume input. You may know this as `>>`. The type of sequence is `Parser<T> -> Parser<T2> -> Parser<T2>`.
|
||||
- `bind()` sequentially composes a parser and a parser-constructing function, passing the output produced by the first parser as an argument to the second. Both parsers consume input. You may know this as `>>=` or `flatmap`. Type: `Parser<T> -> (T -> Parser<T2>) -> Parser<T2>`.
|
||||
|
||||
|
||||
### Laws
|
||||
|
||||
Left identity:
|
||||
|
||||
```
|
||||
bind(pure($a), $f)
|
||||
≡ pure($a)->bind($f)
|
||||
≡ $f($a)
|
||||
```
|
||||
|
||||
Right identity:
|
||||
|
||||
```
|
||||
bind($parser, 'pure')
|
||||
≡ $parser->bind('pure')
|
||||
≡ $parser
|
||||
```
|
||||
|
||||
Associativity:
|
||||
|
||||
```
|
||||
$parser->bind($f)->bind($g)
|
||||
≡ $parser->bind(fn($x) (use $f, $g) => $f($x)->bind($g))
|
||||
```
|
||||
Reference in New Issue
Block a user